Skip to content

fix(positions): floor live withdrawable at 10% of open notional - #297

Draft
claude[bot] wants to merge 3 commits into
mainfrom
fix/live-withdrawable-notional-floor
Draft

claude[bot] wants to merge 3 commits into
mainfrom
fix/live-withdrawable-notional-floor

Conversation

@claude

@claude claude Bot commented Sep 15, 2026 •

Copy link
Copy Markdown

Summary

The live position recompute (ts-sdk/src/sync/positions-live.ts) computes withdrawable cash by deducting a margin requirement from perp equity. The protocol floors that margin deduction at 10% of total open notional, but the recompute deducted only the summed initial margin — so the reported withdrawable amount was too high whenever the 10% floor is the binding constraint.

Before

withdrawable = max(0, equity - Σ initial_margin)   // forced to 0 below maintenance margin

The deduction was always the summed initial margin. For higher-leverage positions, where Σ initial_margin falls below 10% of open notional, this understates the deduction and overstates withdrawable cash relative to the protocol.

After

margin_floor = max(Σ initial_margin, open_notional / 10)
withdrawable = max(0, equity - margin_floor)        // still forced to 0 below maintenance margin

Total open notional (Σ |size|·mark) is accumulated in the existing perp loop, and the deduction is floored at 10% of it, matching the protocol. Division by 10 gives 10% on the WAD-scaled integers, keeping the integer-faithful semantics of the surrounding math. When summed initial margin already exceeds 10% of notional, behavior is unchanged.

Tests

Added ts-sdk/src/sync/positions-live.test.ts with two cases:

  • high leverage (IM rate below 10% of notional) → the 10% floor binds and withdrawable is reduced accordingly;
  • low leverage (IM above 10% of notional) → summed IM binds and behavior is unchanged.

Typecheck, full test suite (171 tests), and build all pass.

The live position recompute deducted only the summed initial margin from
equity when computing withdrawable cash. The protocol floors the margin
deduction at 10% of total open notional, so the withdrawable amount was
overstated whenever that floor is the binding constraint (higher-leverage
positions, where summed IM is below 10% of notional).

Accumulate total open notional in the perp loop and deduct
max(summed IM, open_notional / 10) instead of summed IM. Withdrawable is
still forced to 0 below maintenance margin. Adds unit tests covering both
the floor-binding case and the IM-binding case.

Co-Authored-By: Claude <noreply@anthropic.com>
Resolve add/add conflict in positions-live.test.ts: keep both the
withdrawable-floor suite and the maintenanceMargin suite over one unified
set of test helpers. positions-live.ts auto-merged cleanly (10% open-notional
floor and the added maintenanceMargin return field are independent changes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@poszu

poszu commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

The 10 is POD_CLOB__TRANSFER_MARGIN_RATIO, a per-network config knob (node/src/config.rs:1045, default 0.1) — not a protocol constant. It isn't exposed on any RPC today, so hardcoding it means the SDK silently lies on any network that tunes it. Worth exposing on the markets/config response and reading it here.

Also: node rounds this term up (withdrawal_floor uses mul_div_ceil, trading/src/perps/mod.rs:2520 — "a fractional wei of it is met rather than forgiven"), while openNotional / 10n truncates. Over-reports withdrawable by up to 1 wei, so a max withdraw at the boundary gets rejected.

The 10%-of-open-notional term of the withdrawable margin floor rounds up,
not truncates: it is a requirement against the holder, so a fractional wei
is met rather than forgiven. Truncating over-reported withdrawable by up to
1 wei, so a max-withdraw at the boundary was rejected. Use ceil division
((openNotional + 9) / 10) and cover the not-divisible-by-10 case.

Co-Authored-By: Claude <noreply@anthropic.com>
@claude

claude Bot commented Sep 16, 2026

Copy link
Copy Markdown
Author

Fixed the rounding in ddf6ebd — the 10% term now rounds up (ceil(open_notional / 10)) to match the venue, so it no longer over-reports withdrawable by up to 1 wei at the boundary. Added a test where open notional isn't divisible by 10 to lock it in.

On the ratio being a per-network config value rather than a constant — agreed, and the SDK can't read the tuned value until it's exposed. Two ways to close that, and it's the team's call:

  1. Expose the transfer-margin ratio on the config/markets response and read it here (needs a node/RPC change). Until then the SDK uses the 0.1 default, which only matches networks that haven't tuned it.
  2. Drop the client-side recompute and read withdrawableBalance from the contract instead — always correct, at the cost of a call per update.

Left it hardcoded at the 0.1 default for now so this PR still lands as a strict improvement. Happy to do either follow-up once the team decides.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants