Skip to content

fix(perps): order 0: insufficient margin to place order - #9694

Merged
abretonc7s merged 8 commits into
mainfrom
TAT-3344-fix-core-order-0-insufficient-marg
Jul 31, 2026
Merged

fix(perps): order 0: insufficient margin to place order#9694
abretonc7s merged 8 commits into
mainfrom
TAT-3344-fix-core-order-0-insufficient-marg

Conversation

@abretonc7s

@abretonc7s abretonc7s commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

A max-size ("Max" / 100% slider) limit order resting above the market price — in
practice a sell — was refused by HyperLiquid with order 0: insufficient margin to place order.

getMaxAllowedAmount derived the maximum from the market (mid) price, but HyperLiquid
reserves initial margin for a resting order against the price that order is submitted
at
. An order resting above mid therefore reserves more margin than a mid-priced notional
budgets for, and the venue refuses it.

Root cause

  • packages/perps-controller/src/utils/orderCalculations.tsgetMaxAllowedAmount
    computed theoreticalMax = spendableBalance * leverage from the market price only.
  • The client then derives the order size from mid (calculateFinalPositionSize:
    size = usdAmount / currentPrice) and submits it at the limit price.
  • Margin the venue reserves is size * limitPrice / leverage. With size budgeted at mid,
    that is spendableBalance * (limitPrice / mid) — above the balance whenever the order
    rests above mid.

Measured live on testnet: spendableBalance = 771.21, mid 64227, max 2301 → size
0.03584 resting at +5% (67438.35) → venue reserves 805.51 against 771.21
available → refused.

A resting buy sits below mid and reserves less, so it cannot trip this. A market
order is charged at the fill price, not the padded price it is submitted with, so it is
unaffected — see Validation for the evidence.

Changes

File Change
src/utils/orderCalculations.ts getMaxAllowedAmount scales the maximum by limitPrice / marketPrice when a limit order rests above the market price. New optional orderType and limitPrice params; the post-rounding margin check uses the same ratio.
tests/src/utils/orderCalculations.test.ts New file — 5 tests including the TAT-3344 regression.
CHANGELOG.md Unreleased → Fixed entry.

Both new params are optional and default to the previous behaviour, so existing callers are
unaffected until they pass a limit price.

Validation

Live reproduction, then verification — HyperLiquid testnet

The bug was reproduced live before claiming a fix, using a task-local recipe that sizes
the order through getMaxAllowedAmount from live account state and submits it as a real
order. Same recipe, same market, same parameters, before and after:

Before After
getMaxAllowedAmount 2301 2163
Client size at mid 0.03584 0.03366
Margin reserved at limit price 805.51 757.05
Spendable balance 771.21 761.28
Venue verdict refusedorder 0: Insufficient margin to place order. asset=3 accepted, order rests

The red run asserted the exact venue error by code and that no position or resting order was
left behind; the green run asserted a live resting order. Both ended with deterministic
cleanup back to flat.

Harness: mm-harness 0.24.0 (source 4d0dd235be723334eb22fc045dd356eda10797f8), perps recipe
library 421fd1576616b66a5f7dda3cacc27ba3abd9fabb. The reproduction harness is task-local
validation tooling and is deliberately not part of this diff.

Why the market-order path is not changed

Four attempts to refuse a max-size market buy — all accepted, so no fix is warranted
there:

Scenario Result
Max sizing, 300 bps slippage accepted, filled
Max sizing, 1000 bps slippage (order price = mid × 1.10) accepted, filled
2% above the mid-price margin cap not refused — partially filled to available margin, remainder dropped
Max sizing on top of an existing BTC position accepted, fully filled

For a marketable order the venue charges margin at the fill/mark price and caps the fill
rather than refusing.

Checks

Check Result
tests/src/utils/orderCalculations.test.ts 5 passed
Same tests against the previous implementation 3 failed, 2 passed — regression is real
yarn workspace @metamask/perps-controller test (suite + coverage thresholds) pass
yarn build (monorepo) pass
yarn lint:tsc (monorepo typecheck) pass
yarn eslint, oxfmt --check on changed files clean
yarn changelog:validate, yarn constraints, yarn lint:teams clean

Not addressed (follow-ups worth filing)

  1. Increasing an existing isolated position. The max is computed as if already-locked
    margin were reusable. Not reproducible on the market path (see above), but untested for
    resting orders.
  2. No preflight margin check in placeOrder for the main DEX — the venue rejection is
    still the first and only check.
  3. Error is unmapped. #errorMappings does not map "insufficient margin to place order", so PERPS_ERROR_CODES.INSUFFICIENT_MARGIN is never produced — no user-facing
    translation and no downsize/retry path.
  4. HIP-3 could not be exercised: this testnet reports hip3Enabled: false,
    availableHip3Dexs: 0.

Note

Low Risk
Localized change to max-order sizing math with optional params and regression tests; no auth or payment paths touched.

Overview
Fixes max-size limit orders (e.g. 100% slider) that HyperLiquid rejected with order 0: insufficient margin to place order when the order rests above mid—common on sells.

getMaxAllowedAmount now takes optional orderType and limitPrice. For limit orders with limitPrice > marketPrice, the max USD notional is divided by limitPrice / marketPrice so reserved margin at the submitted price stays within spendable balance; the post-rounding margin check uses the same ratio. Market orders and limits at or below mid behave as before when callers omit the new params.

Adds getMaxAllowedAmount unit tests (including TAT-3344 regression) and an Unreleased → Fixed changelog entry.

Reviewed by Cursor Bugbot for commit dc62582. Bugbot is set up for automated code reviews on this repo. Configure here.

Market buys are sent to HyperLiquid as limit orders priced at
market * (1 + slippage), and the exchange charges initial margin
against that submitted price. getMaxAllowedAmount sized the max off
the market price with only a 0.5% buffer, so a max-size market buy
asked for ~3% more margin than the account had and was rejected with
'order 0: insufficient margin to place order'.

Apply the slippage haircut to the max for market buys, with optional
orderType/isBuy/maxSlippageBps params so sells and limit orders are
unaffected.
@abretonc7s

Copy link
Copy Markdown
Contributor Author

Automated fix-bug run — TAT-3344

Metric Value
Run 62004dac
Duration ?
Model claude/opus
Nudges 0
Worker report

TAT-3344 — [Core] order 0: insufficient margin to place order

Branch: TAT-3344-fix-core-order-0-insufficient-marg (local only, not pushed)
Commit: 98903d27afix(perps): size max order amount off the submitted order price

Summary

Max-size ("Max" / 100% slider) market buy orders were structurally guaranteed to be
rejected by HyperLiquid. The max order amount was sized off the market (mid) price, but
market orders are submitted as limit orders priced ~3% above the market price, and the
exchange charges initial margin against that submitted price. The order therefore asked for
more margin than the account had.

Root cause

  • packages/perps-controller/src/utils/orderCalculations.ts:159 (pre-fix) —
    getMaxAllowedAmount computes theoreticalMax = spendableBalance * leverage from the
    market price, then shaves only MAX_ORDER_MARGIN_BUFFER = 0.5%
    (src/constants/perpsConfig.ts:132).
  • packages/perps-controller/src/utils/orderCalculations.ts:335-337
    calculateOrderPriceAndSize submits market orders as FrontendMarket limit orders priced
    at currentPrice * (1 + slippage) for buys. Default slippage is 300 bps
    (ORDER_SLIPPAGE_CONFIG.DefaultMarketSlippageBps), user-configurable up to 1000 bps
    (MAX_SLIPPAGE_BOUNDS, PerpsController.setMaxSlippage).
  • HyperLiquid charges initial margin against the submitted order price. The repo's own
    HIP-3 path already models it this way — #calculateHip3RequiredMargin uses orderPrice
    (src/providers/HyperLiquidProvider.ts:2793-2880, see the comment at :2810-2818).

Arithmetic for a max market buy at default slippage:

maxAmount        ≈ spendableBalance * leverage * 0.995      (mid-price notional)
requiredMargin   ≈ maxAmount * 1.03 / leverage
                 ≈ spendableBalance * 1.025                  > spendableBalance  → rejected

At the 1000 bps slippage setting it is ~1.09 × spendableBalance.

Confirmed the client is the caller of the broken calc:
metamask-mobile app/components/UI/Perps/hooks/usePerpsOrderForm.ts:132,192 calls
getMaxAllowedAmount with the market price and no direction/slippage information.

Aggravating factor (not fixed here, see Not addressed): the main-DEX order path has no
client-side margin preflight at all — HyperLiquidProvider.validateOrder
(src/providers/HyperLiquidProvider.ts:6578-6738) never reads account balance, so the
exchange rejection is the first and only check.

Changes

File Change
packages/perps-controller/src/utils/orderCalculations.ts getMaxAllowedAmount now prices the max off the worst-case execution price. New optional params orderType, isBuy, maxSlippageBps; the post-rounding margin check uses the same execution price.
packages/perps-controller/tests/src/utils/orderCalculations.test.ts New file — 6 tests, including the TAT-3344 regression.
packages/perps-controller/CHANGELOG.md Unreleased → Fixed entry.

The new params default to a market buy at default slippage — the conservative case — so
existing callers (mobile, which passes none of them) get the fix without any client change.
Sells and limit orders can opt out of the haircut by passing isBuy: false /
orderType: 'limit'.

Test results

Check Result
tests/src/utils/orderCalculations.test.ts (new) 6 passed
Same file with the fix reverted 4 failed, 2 passed — regression is real
tests/src/providers/HyperLiquidProvider.trading.test.ts 42 passed (baseline 42 passed)
tests/src/providers/HyperLiquidProvider.validation.test.ts passed (92 total across the 3 suites)
yarn eslint on both changed/added files clean
yarn changelog:validate (perps-controller) clean
yarn build (packages/perps-controller) fails — pre-existing, unrelated. Byte-identical failure with the change stashed (TS2339: Property 'state' does not exist on type 'PerpsController' etc. in PerpsController.ts, a file this change does not touch); diff of the two build logs is empty.
e2e none exist — packages/perps-controller/e2e/ is not present.

Not addressed (follow-ups worth filing)

These are separate contributors to the same error message; each needs its own change and was
out of scope for a minimal fix:

  1. Increasing an existing isolated position. HyperLiquid validates
    spendableBalance >= TOTAL position margin before reallocating already-locked margin.
    The code handles this for HIP-3 only (HyperLiquidProvider.ts:2812-2844); on the main DEX
    the max is computed as if existing margin were reusable. Affects longs and shorts, market
    and limit — the likeliest explanation for the short-side failures in the Mixpanel data.
  2. No preflight margin check in placeOrder for the main DEX.
  3. Error is unmapped. #errorMappings (HyperLiquidProvider.ts:394-398) does not map
    "insufficient margin to place order", so PERPS_ERROR_CODES.INSUFFICIENT_MARGIN is never
    produced — no user-facing translation and no downsize/retry path.
  4. Limit orders: size is derived from the mid price while the order is placed at the limit
    price, so a buy-limit above mid needs more margin than was reserved.

@abretonc7s abretonc7s changed the title chore: prepare farmslot publication pkg-62004dac-ms67xxl9 fix(perps): [Core] order 0: insufficient margin to place order Jul 29, 2026
@abretonc7s
abretonc7s marked this pull request as ready for review July 29, 2026 15:08
@abretonc7s
abretonc7s requested review from a team as code owners July 29, 2026 15:08
@abretonc7s abretonc7s changed the title fix(perps): [Core] order 0: insufficient margin to place order fix(perps): order 0: insufficient margin to place order Jul 30, 2026
Sizes a max market buy through getMaxAllowedAmount against live account
state and submits that exact amount, so the max-sizing path is covered by
a real order rather than a fixed notional. mode=prefix recomputes the
pre-fix formula from the same inputs as a counterfactual. Testnet-guarded
with deterministic cleanup.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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 0ed32d9. Configure here.

Comment thread packages/perps-controller/e2e/tat-3344/compute-max-order-amount.mjs Outdated
@abretonc7s
abretonc7s marked this pull request as draft July 30, 2026 07:09
The live reproduction harness is task-local validation tooling, not package
source. Kept out of the shipped diff; the two commits above retain it in
branch history.
HyperLiquid reserves initial margin for a resting order against the price
that order is submitted at, not the market price its size was derived
from. A max-size limit order resting above the market price - typically a
sell - therefore reserved more margin than the account had and the venue
refused it with "order 0: insufficient margin to place order".

getMaxAllowedAmount now takes optional orderType and limitPrice and scales
the maximum by limitPrice / marketPrice when the order rests above the
market. Orders at or below the market price, and market orders, are
unchanged.

Reproduced live on HyperLiquid testnet before the change and verified
accepted after it.
@abretonc7s
abretonc7s marked this pull request as ready for review July 30, 2026 10:20
geositta
geositta previously approved these changes Jul 31, 2026
@abretonc7s
abretonc7s added this pull request to the merge queue Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jul 31, 2026
@abretonc7s
abretonc7s enabled auto-merge July 31, 2026 12:47
…er-0-insufficient-marg

# Conflicts:
#	packages/perps-controller/CHANGELOG.md
#	packages/perps-controller/tests/src/utils/orderCalculations.test.ts

@michalconsensys michalconsensys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

restamp

@abretonc7s
abretonc7s added this pull request to the merge queue Jul 31, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Jul 31, 2026
…er-0-insufficient-marg

# Conflicts:
#	packages/perps-controller/CHANGELOG.md
@abretonc7s
abretonc7s enabled auto-merge July 31, 2026 14:52
@abretonc7s
abretonc7s added this pull request to the merge queue Jul 31, 2026
Merged via the queue into main with commit 6c7d875 Jul 31, 2026
46 checks passed
@abretonc7s
abretonc7s deleted the TAT-3344-fix-core-order-0-insufficient-marg branch July 31, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants