margin: add place_reduce_only_market_order_and_repay_loan#1035
margin: add place_reduce_only_market_order_and_repay_loan#1035tonylee08 wants to merge 3 commits into
Conversation
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>
|
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 Let's say the bids look the following way:
Then, the result of 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 Thanks for your attention. |
Summary
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.place_reduce_only_market_order_v2checks 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 onEReduceOnlyMustImproveRiskRatio. Repaying first deleverages, lifting the ratio and absorbing the slippage.margin_manager::risk_ratio_intand::repayaspublic(package)(they were already the internal impls) sopool_proxycan compute the single-debt-pool risk ratio and repay — no new wrapper functions.min_borrow1.25 and aboveliquidation1.10, then market-closes and climbs back above the borrow floor), and a reduce-only gating failure..claude/rules/move.mdcapturing the oracle-vs-execution-price distinction and the swap-only-vs-net-state subtlety.Key decisions
price_toleranceband viaassert_price(unchanged); the risk-ratio check is not relaxed to make room for slippage. The repay is what creates headroom.min_borrowfloor — so danger-zone managers (inliquidation..min_borrow) aren't trapped. They can't reach 1.25 in one swap but can climb out by deleveraging.&mut MarginPool<DebtAsset>(mirrorsliquidate) rather than both margin pools: the debt pool must be borrowed mutably to repay, which rules out the two-poolrisk_ratiohelper, sorisk_ratio_intis used.calculate_debtsvalidates both the pool binding and thatDebtAssetis the borrowed side.amount: noneon repay → repays as much of the debt asset as available, capped at outstanding debt (maximal deleverage). Settles viawithdraw_settled_amountsbefore repaying so the taker proceeds are drawable.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 100000000000inpackages/deepbook_margin— 335 passedsui move test --gas-limit 100000000000inpackages/margin_liquidation(dependent package) — 16 passedbunx prettier-moveon all modified.movefilesreduce_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