Summary
The Portfolio live position card calculates liquidation distance using an absolute price delta divided by the current mark price:
Math.abs(Number(markE6) - Number(liquidationPriceE6))
/ Number(markE6)
* 100
This calculation is not direction-aware and does not preserve the canonical denominator used for short positions.
As a result, the Portfolio UI can:
- suppress a valid liquidation warning;
- downgrade a crossed position from
danger to warning;
- mark a crossed short position as
safe;
- display risk severity inconsistent with the Portfolio snapshot.
This does not modify the protocol liquidation engine. However, it can present materially incorrect risk information to users when they are deciding whether to add margin or close a position.
Affected Code
app/app/portfolio/page.tsx
app/hooks/usePortfolio.ts
The snapshot calculation in usePortfolio.ts is direction-aware:
Long:
(mark - liquidation) / mark
Short:
(liquidation - mark) / liquidation
The live PositionCard instead uses:
abs(mark - liquidation) / mark
The two paths can therefore produce different liquidation-risk severity for the same position.
Severity Assessment
High user-impact UI risk; no direct protocol or on-chain state impact.
This issue is classified as High severity from a user-impact and trading-risk perspective, rather than as a direct smart-contract or protocol exploit.
User Impact
The calculated percentage is passed to the Portfolio liquidation-severity logic:
distance <= 10% → danger
distance <= 30% → warning
distance > 30% → safe
An incorrect distance can affect:
- liquidation warning labels;
- card border and status styling;
- the perceived urgency of a position;
- the user's decision to deposit margin or close the position.
The strongest failure occurs after a short position's mark price crosses its liquidation price: the absolute delta starts increasing again, allowing the position to appear safe.
Root Cause
Math.abs() removes the position direction.
Liquidation boundaries are directional:
Long position:
The position approaches liquidation as mark decreases.
Short position:
The position approaches liquidation as mark increases.
After the mark crosses the boundary, liquidation distance must remain 0%.
The current live formula instead calculates a new positive distance after the boundary is crossed.
The short calculation also incorrectly uses the mark price as its denominator, while the canonical Portfolio calculation uses the liquidation price.
Proof of Concept
Case 1 — Short warning incorrectly displayed as safe
Input:
Position size: Short
Mark price: 77
Liquidation price: 110
Expected canonical calculation:
(110 - 77) / 110 × 100
= 30.00%
= warning
Current live-card calculation:
abs(77 - 110) / 77 × 100
= 42.857142857%
= safe
Result:
Expected severity: warning
Actual severity: safe
The user receives no warning even though the canonical distance is exactly at the warning threshold.
Case 2 — Crossed short position incorrectly displayed as safe
Input:
Position size: Short
Mark price: 160
Liquidation price: 110
For a short position:
mark >= liquidation price
Therefore the position is already at or beyond the liquidation boundary.
Expected result:
Distance: 0%
Severity: danger
Current live-card calculation:
abs(160 - 110) / 160 × 100
= 31.25%
= safe
Result:
Expected severity: danger
Actual severity: safe
This is the highest-impact case because a crossed short position can be presented as safe.
Case 3 — Crossed long position downgraded to warning
Input:
Position size: Long
Mark price: 80
Liquidation price: 90
For a long position:
mark <= liquidation price
Expected result:
Distance: 0%
Severity: danger
Current live-card calculation:
abs(80 - 90) / 80 × 100
= 12.5%
= warning
Result:
Expected severity: danger
Actual severity: warning
Regression Test
A minimal regression test reproduces the three incorrect results:
import { describe, expect, it } from "vitest";
import { computeLiquidationDistancePct } from "@/lib/liquidation-distance";
describe("computeLiquidationDistancePct", () => {
it("preserves the canonical warning distance for a short position", () => {
const distance = computeLiquidationDistancePct(
-1n,
77_000_000n,
110_000_000n,
);
expect(distance).toBe(30);
});
it("returns zero after a short crosses its liquidation boundary", () => {
const distance = computeLiquidationDistancePct(
-1n,
160_000_000n,
110_000_000n,
);
expect(distance).toBe(0);
});
it("returns zero after a long crosses its liquidation boundary", () => {
const distance = computeLiquidationDistancePct(
1n,
80_000_000n,
90_000_000n,
);
expect(distance).toBe(0);
});
});
Run:
cd app
pnpm vitest run \
__tests__/lib/liquidation-distance.test.ts \
--reporter=verbose
Before the fix, the reproduced failures are:
Expected: 30
Received: 42.857142857142854
Expected: 0
Received: 31.25
Expected: 0
Received: 12.5
These results match the incorrect live Portfolio formula.
Expected Behavior
Liquidation distance must preserve the position direction.
Long:
mark <= liquidation
→ 0%
otherwise
→ (mark - liquidation) / mark × 100
Short:
mark >= liquidation
→ 0%
otherwise
→ (liquidation - mark) / liquidation × 100
All Portfolio consumers should use one shared calculation so that:
- live cards and snapshot data remain consistent;
- exact and crossed boundaries always return
0%;
- short positions use the correct denominator;
- missing live prices continue using the existing snapshot fallback.
Proposed Fix
Introduce a shared direction-aware helper:
app/lib/liquidation-distance.ts
Use the helper in:
app/app/portfolio/page.tsx
app/hooks/usePortfolio.ts
The helper should:
- distinguish long and short positions;
- return
0% at or beyond the liquidation boundary;
- preserve the canonical long and short denominators;
- use BigInt arithmetic before converting the final percentage to
number;
- preserve the existing fallback when position or price data is unavailable.
Regression Coverage
The proposed test suite covers:
- canonical short warning distance;
- healthy long distance;
- healthy short distance;
- crossed long boundary;
- crossed short boundary;
- exact long boundary;
- exact short boundary;
- zero-position fallback;
- invalid mark-price fallback;
- invalid liquidation-price fallback;
- prices above
Number.MAX_SAFE_INTEGER.
Validation
Targeted regression tests: 11 passed
Related Portfolio tests: 38 passed
TypeScript validation: passed
Prettier check: passed
Production build: passed
The full application suite currently contains unrelated pre-existing failures.
A clean upstream/playground worktree produced:
101 failed tests
3 unhandled errors
The patched branch produced the same failed-test signatures, the same failure count, and the same unhandled-error count. No additional failure was introduced by this change.
Summary
The Portfolio live position card calculates liquidation distance using an absolute price delta divided by the current mark price:
This calculation is not direction-aware and does not preserve the canonical denominator used for short positions.
As a result, the Portfolio UI can:
dangertowarning;safe;This does not modify the protocol liquidation engine. However, it can present materially incorrect risk information to users when they are deciding whether to add margin or close a position.
Affected Code
The snapshot calculation in
usePortfolio.tsis direction-aware:The live
PositionCardinstead uses:The two paths can therefore produce different liquidation-risk severity for the same position.
Severity Assessment
High user-impact UI risk; no direct protocol or on-chain state impact.
This issue is classified as High severity from a user-impact and trading-risk perspective, rather than as a direct smart-contract or protocol exploit.
User Impact
The calculated percentage is passed to the Portfolio liquidation-severity logic:
An incorrect distance can affect:
The strongest failure occurs after a short position's mark price crosses its liquidation price: the absolute delta starts increasing again, allowing the position to appear
safe.Root Cause
Math.abs()removes the position direction.Liquidation boundaries are directional:
After the mark crosses the boundary, liquidation distance must remain
0%.The current live formula instead calculates a new positive distance after the boundary is crossed.
The short calculation also incorrectly uses the mark price as its denominator, while the canonical Portfolio calculation uses the liquidation price.
Proof of Concept
Case 1 — Short warning incorrectly displayed as safe
Input:
Expected canonical calculation:
Current live-card calculation:
Result:
The user receives no warning even though the canonical distance is exactly at the warning threshold.
Case 2 — Crossed short position incorrectly displayed as safe
Input:
For a short position:
Therefore the position is already at or beyond the liquidation boundary.
Expected result:
Current live-card calculation:
Result:
This is the highest-impact case because a crossed short position can be presented as safe.
Case 3 — Crossed long position downgraded to warning
Input:
For a long position:
Expected result:
Current live-card calculation:
Result:
Regression Test
A minimal regression test reproduces the three incorrect results:
Run:
cd app pnpm vitest run \ __tests__/lib/liquidation-distance.test.ts \ --reporter=verboseBefore the fix, the reproduced failures are:
These results match the incorrect live Portfolio formula.
Expected Behavior
Liquidation distance must preserve the position direction.
All Portfolio consumers should use one shared calculation so that:
0%;Proposed Fix
Introduce a shared direction-aware helper:
Use the helper in:
The helper should:
0%at or beyond the liquidation boundary;number;Regression Coverage
The proposed test suite covers:
Number.MAX_SAFE_INTEGER.Validation
The full application suite currently contains unrelated pre-existing failures.
A clean
upstream/playgroundworktree produced:The patched branch produced the same failed-test signatures, the same failure count, and the same unhandled-error count. No additional failure was introduced by this change.