Skip to content

margin: add place_reduce_only_market_order_and_repay_loan#1035

Draft
tonylee08 wants to merge 3 commits into
mainfrom
tlee/margin-close-and-repay
Draft

margin: add place_reduce_only_market_order_and_repay_loan#1035
tonylee08 wants to merge 3 commits into
mainfrom
tlee/margin-close-and-repay

Conversation

@tonylee08

Copy link
Copy Markdown
Collaborator

Summary

  • Adds pool_proxy::place_reduce_only_market_order_and_repay_loan<BaseAsset, QuoteAsset, DebtAsset>: an atomic close that places a reduce-only market order, settles the taker proceeds, repays as much of the loan as the settled balance allows, then enforces the monotonic solvency invariant (risk_ratio_after >= risk_ratio_before) on the net (post-repay) state.
  • Solves a real gap: the standalone place_reduce_only_market_order_v2 checks the ratio on the swap-only state, before any repay. A market close pays the spread, which strictly lowers the oracle-valued ratio (debt is unchanged until repay), so that path aborts on EReduceOnlyMustImproveRiskRatio. Repaying first deleverages, lifting the ratio and absorbing the slippage.
  • Exposes margin_manager::risk_ratio_int and ::repay as public(package) (they were already the internal impls) so pool_proxy can compute the single-debt-pool risk ratio and repay — no new wrapper functions.
  • Tests: bid and ask closes that succeed where the swap-only path fails, the danger-zone scenario (ratio drifts to exactly 1.20, below min_borrow 1.25 and above liquidation 1.10, then market-closes and climbs back above the borrow floor), and a reduce-only gating failure.
  • Docs: adds a "DeepBook Margin: risk ratio, solvency, and slippage" section to .claude/rules/move.md capturing the oracle-vs-execution-price distinction and the swap-only-vs-net-state subtlety.

Key decisions

  • Slippage and solvency are kept orthogonal. Allowable slippage is bounded by the per-pool price_tolerance band via assert_price (unchanged); the risk-ratio check is not relaxed to make room for slippage. The repay is what creates headroom.
  • Monotonic check on the net state, not a min_borrow floor — so danger-zone managers (in liquidation..min_borrow) aren't trapped. They can't reach 1.25 in one swap but can climb out by deleveraging.
  • Single &mut MarginPool<DebtAsset> (mirrors liquidate) rather than both margin pools: the debt pool must be borrowed mutably to repay, which rules out the two-pool risk_ratio helper, so risk_ratio_int is used. calculate_debts validates both the pool binding and that DebtAsset is the borrowed side.
  • amount: none on repay → repays as much of the debt asset as available, capped at outstanding debt (maximal deleverage). Settles via withdraw_settled_amounts before repaying so the taker proceeds are drawable.
  • Only the reduce-only market variant was added (not a non-reduce-only place_market_order_and_repay_loan): the reduce-only path is the one that unblocks danger-zone users; a healthy manager can already swap-then-repay across a PTB.

Test plan

  • sui move test --gas-limit 100000000000 in packages/deepbook_margin — 335 passed
  • sui move test --gas-limit 100000000000 in packages/margin_liquidation (dependent package) — 16 passed
  • bunx prettier-move on all modified .move files
  • New tests: reduce_only_and_repay_bid_succeeds_where_swap_only_fails, reduce_only_and_repay_ask_succeeds_where_swap_only_fails, reduce_only_and_repay_closes_from_danger_zone, reduce_only_and_repay_quantity_exceeds_debt_aborts

🤖 Generated with Claude Code

tonylee08 and others added 3 commits May 22, 2026 19:04
Bundles a reduce-only market order with a loan repayment so a leveraged
position can be wound down atomically. The standalone reduce-only entry
checks risk_ratio on the swap-only state, before any repay — a market
close pays the spread, which strictly lowers the oracle-valued ratio and
aborts. Repaying first deleverages the manager, lifting the ratio and
absorbing the slippage; the monotonic check then passes on the net state.
Slippage stays bounded by the price-tolerance band, so deferring the
solvency check past the repay does not weaken security.

This is also the only wind-down path for a manager whose ratio has
drifted into the liquidation..min_borrow danger zone: it cannot reach the
borrow floor in a single swap, but it can climb out by deleveraging.

Exposes margin_manager::risk_ratio_int and ::repay as public(package) so
pool_proxy can compute the single-debt-pool risk ratio and repay; both
were already the internal impls (no new wrappers).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Keep `repay` at its original location, only widening it to public(package)
  (minimal in-place visibility change rather than a relocation).
- Drop the post-trade `withdraw_settled_amounts` call: `place_market_order`
  already settles the taker fill into the manager's balance via
  `vault.settle_balance_manager`, so the proceeds are drawable by `repay`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Rework the entry to take both margin pools (distinct objects, so no &mut
aliasing) and use the existing public `risk_ratio` (two-pool) and
`repay_base`/`repay_quote`, mirroring `place_reduce_only_market_order_v2`.

This reverts margin_manager.move entirely: `risk_ratio_int` and `repay`
stay private — no public(package) widening was needed. Also tightens the
entry's doc comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@bathord

bathord commented May 24, 2026

Copy link
Copy Markdown
Contributor

The single endpoint for reduce-only market orders and repayments is quite useful. However, there is one edge case that makes reduce-only orders difficult, and sometimes impossible, to use for closing positions due to their limitations.

To close a position, the debt must be fully repaid. In most cases, a user needs to convert their position's asset (SUI in a long case) into the debt asset (USDC in a long case) using a market order. Currently, when a position's risk ratio is in the 1.1 to 1.25 range (for 5x max leverage), only reduce-only orders are allowed, so we must use them to convert assets for repayment. Reduce-only orders have a size limitation: their size – either base or quote – must not exceed the position's net debt.

Imagine a user has 80 USDC in net debt. They need to place an ask order selling N SUI to receive 80 USDC to repay the debt. To find N, we should use get_quantity_in(80 USDC). This results in "how much base (SUI) should I sell to receive at least 80 USDC."

Let's say the bids look the following way:

Level Quantity Price
Bid 1 30 SUI 2.0 USDC
Bid 2 100 SUI 1.5 USDC

Then, the result of get_quantity_in(80 USDC) is 43.4 SUI (as base quantity in) and 80.1 USDC (as reached quote out). So, the minimum amount of SUI the user must sell to receive at least 80 USDC is 43.4 SUI. With this result, the user calls place_reduce_only_market_order_v2 with quantity = 43.4 SUI and is_bid = false. These parameters are passed into calculate_effective_price, which calls get_quote_quantity_out(43.4 SUI), resulting in quote_out = 80.1 USDC. Then, this quote_out is compared with the net debt: quote_quantity <= 80 USDC. Since 80.1 USDC > 80 USDC, the reduce-only order aborts, making it impossible to swap the minimum SUI amount required to fully repay the debt. If we provide the closest smaller quantity – 43.3 SUI – into place_reduce_only_market_order_v2, we will receive quote_out = 79.95 USDC, which will pass the net debt check but will not be enough to fully repay the debt.

That is why we used to use the general market order instead of the reduce-only one when closing a position, as it does not have the net debt limitation. I would appreciate any feedback on this case.

The general question is: Is it really required to have the net debt check for reduce-only orders, now that there is a reduce_only_monotonic check? It already guarantees that the position's health is either the same or better. Is it really important how much the user traded?

Thanks for your attention.

@tonylee08 tonylee08 marked this pull request as draft May 27, 2026 20:02
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