feat(perps-controller): add isolated position modify preview - #9968
feat(perps-controller): add isolated position modify preview#9968michalconsensys wants to merge 10 commits into
Conversation
Give clients a read-only HyperLiquid isolated projection so Pro order forms can show before→after margin and liquidation from the same result used for TP/SL, including full-position leverage changes and maintenance tiers.
Reject same-direction reduce-only and missing fill prices instead of projecting a false decrease or falling back to entry, and lock in short, limit, and leverage-down geometry with tests.
Add JSDoc param fields, drop the untyped notional IIFE, and rewrite preview assertions so CI lint:eslint passes.
HyperLiquid isolated leverage is mark notional / margin. Deriving it from entry notional drifted after updateLeverage whenever the position had unrealized PnL.
Increase leverage used live mark notional plus fill notional, which is not HyperLiquid's post-fill mark when those prices differ. Use the resulting size at the fill so leverage matches the displayed position.
Automated Review — PR #9968
SummaryAdds a read-only The scope is well chosen: protocol math lives in Core, client wiring is deliberately excluded, Full review detailsCorrectness of the protocol math — verifiedThe liquidation formula was re-derived from first principles and checked against Liquidation condition
This is exactly Tier handling matches the docs verbatim:
Notably, The three modify branches were each checked against what the placement path actually does:
Leverage reallocation on the whole position is also right: Downstream compatibility — Mobile / ExtensionThe PR is labelled BREAKING for adding a required method to
Practical impact: breaking in name, non-breaking in effect for Mobile and Extension. The Non-blocking observations
None of these block merge — each requires an already-degraded input, and all fail toward Tests815 new lines of unit tests plus provider/service/aggregator coverage. Long and short are Coverage of the new module: 93.54% stmts / 91.42% branch / 100% funcs / 93.33% lines — Validation commands and resultsEmitted-artifact check (public export drift):
Working tree left clean; a temporary probe spec used for edge-case exploration was removed. Per the static-review contract, no runtime QA, CDP, recipes, or screenshots were run, and no VerdictAPPROVE. The math is correct against a first-principles derivation and the HyperLiquid docs, |
abretonc7s
left a comment
There was a problem hiding this comment.
Automated review — see comment above for full details.
geositta
left a comment
There was a problem hiding this comment.
Requesting changes because the preview can still return incorrect pretrade risk values. Hyperliquid states that liquidations use mark price, but this implementation calculates from average entry while marginUsed is mark-based isolated equity. Aggregated mode can also apply Hyperliquid calculations to a MYX position, as described in Bugbot’s open provider-routing thread, and missing margin-table identity currently falls back to an assumed single tier instead of returning liquidation as unavailable. Bugbot’s open decrease-fill finding is also valid under this API’s expected-fill contract and should be addressed with the mark-price correction.
The overall direction is good: protocol math belongs in the controller, the discriminated result types are clear, and independent margin/liquidation availability is modeled well. Please address these cases and add independent numeric fixtures covering unrealized PnL, explicit provider routing, and missing table identity.
| }): PositionModifyPreviewResult => { | ||
| const liquidationPrice = estimateIsolatedLiquidationPriceAtTier({ | ||
| isLong: preview.resultingDirection === 'long', | ||
| entryPrice: preview.resultingEntryPrice, |
There was a problem hiding this comment.
Use projected mark price for liquidation, not average entry. Hyperliquid documents that liquidations use mark price. Here newMargin is derived from marginUsed, which is mark-based isolated equity and already includes unrealized PnL, so pairing it with resultingEntryPrice mixes two different reference prices. Using this PR’s existing unrealized-PnL fixture (mark=2500, resulting entry≈2166.67, margin=375, size=1.5, mmr=1/50) produces liquidation of approximately 1955.78; the mark-based result is approximately 2295.92. Please carry projected mark separately from average entry and use it for liquidation; this is material because clients will use the result for TP/SL validation.
There was a problem hiding this comment.
Agreed. Isolated marginUsed is mark-based equity, so liquidation now uses the projected mark (expected fill, or live mark when a reduce has no price) instead of average entry. The unrealized-PnL fixture now asserts liquidation 2295.918… rather than the entry-based ~1955.78.
| return tiers && tiers.length > 0 ? tiers : null; | ||
| } | ||
|
|
||
| if (isPositiveFinite(maxLeverage)) { |
There was a problem hiding this comment.
Fail closed when the margin-table ID is unknown. Hyperliquid specifies that only table IDs below 50 are single-tier and that tier definitions come from meta. If meta.universe does not contain this asset, the provider passes marginTableId: undefined; this branch then fabricates a single tier from maxLeverage. A missing multi-tier asset can therefore return an available but incorrect liquidation instead of unavailable. Please return null unless a known table ID establishes that the asset is single-tier, and add a missing-asset/table-ID test.
There was a problem hiding this comment.
Agreed. resolveHyperLiquidMarginTiers now returns null unless a finite table id is present. IDs below 50 stay single-tier; unknown identity (asset missing from meta.universe) withholds liquidation instead of inventing a tier from max leverage. Added a missing-asset provider test.
…nd routing Use projected mark for isolated liquidation, withhold liquidation when the margin-table id is unknown, mark decreases at the expected fill, and route aggregated previews by providerId.
Release 13.0.0 landed on main while this PR still listed the preview under Unreleased together with those released notes. Merging parked #9968 under 13.0.0, which fails the merge-queue changelog check.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 8305e8e. Configure here.
| Math.abs(selectedLeverage - currentLeverage) > SIZE_EPSILON; | ||
| const existingMarginAfterLeverage = leverageChanged | ||
| ? currentNotional / selectedLeverage | ||
| : currentMargin; |
There was a problem hiding this comment.
Leverage drop strips extra margin
Medium Severity
When selected leverage falls below the position's current setting, existingMarginAfterLeverage is always set to currentNotional / selectedLeverage. That discards any isolated cushion already above the new initial-margin target. HyperLiquid only tops up when collateral is short of the new IM; extras from updateIsolatedMargin or mark-based equity stay with the position. Previews then understate remaining margin and shift projected leverage and liquidation for any modify that also lowers leverage.
Reviewed by Cursor Bugbot for commit 8305e8e. Configure here.


Explanation
Pro mode's increase/decrease UX (TAT-3705) needs a current → projected margin and liquidation. That projection is protocol math, not UI formatting: HyperLiquid applies selected leverage to the whole isolated position before the fill, and maintenance margin depends on the tier at the liquidation notional. Clients that recompute this locally get both cases wrong, and they can also show a false projection for cross-margin positions or drop a valid margin preview when
liquidationPriceis null.This adds a read-only
previewPositionModifyAPI onPerpsController/PerpsProvider. The client supplies the live position and proposed order; the HyperLiquid provider loads the asset's margin table from cachedmetaand returns a discriminated result:open— increase, decrease, or flip with remaining size > 0full_close— no remaining size (invalid leftover states are unrepresentable)unsupported—cross_marginor a provider that cannot project (MYX)none— no meaningful modifyMargin and liquidation availability are independent, so a missing live liquidation or missing multi-tier table withholds only liquidation. Isolated liquidation uses the maintenance tier at
size * liqPrice, including that tier's maintenance deduction.Clients should use
resulting.direction(not the order direction) when validating TP/SL against the projected liquidation.This is a breaking
PerpsProviderinterface addition. Mobile/Extension should consume it after the next@metamask/perps-controllerrelease; they do not implementPerpsProviderthemselves. Client wiring is intentionally not in this PR.References
positionModifyPreviewarithmetic with this API after the controller is released.Checklist
@metamask/perps-controllerrelease; no client source changes are included here.Note
Medium Risk
New trading projection logic affects margin/liquidation UX and TP/SL validation inputs; incorrect formulas could mislead users, though the API is read-only and does not submit orders.
Overview
Adds a breaking read-only
previewPositionModifyAPI onPerpsControllerandPerpsProviderso clients can show post-trade isolated margin and liquidation before placing an order, without duplicating HyperLiquid math locally.Callers pass the live position plus proposed order (size, direction, expected price, selected leverage, optional fees). Results are a discriminated union:
open(increase / decrease / flip with projected size, entry, mark-based leverage, margin, and optional liquidation),full_close,unsupported(cross-margin or MYX), ornonewhen the order would not change the position. Margin and liquidation are independently optional so a valid margin projection can still return when tier data or live liquidation is missing.HyperLiquid loads margin tiers from cached
metaand projects leverage reallocation, fees, and tier-aware isolated liquidation (mark-based, not entry). Aggregated routing usesproviderId/position.providerId. Pure helpers inhyperLiquidPositionPrevieware exported for tests and advanced consumers; messenger action types, mocks, and broad unit coverage were updated.Reviewed by Cursor Bugbot for commit 8305e8e. Bugbot is set up for automated code reviews on this repo. Configure here.