fix: junior withdrawal ordering unfairness during active insurance loss - #124
Conversation
…g unfairness During active insurance loss (total_flushed > total_returned), partial junior withdrawals used a proportional share of the RAW junior_balance as raw_decrease (lp_amount * jb / junior_lp_before). But the actual withdrawal_amount was computed from the loss-adjusted effective balance, which is smaller. This mismatch caused gross_senior (= gross_pool - junior_balance) to inflate after each junior withdrawal, making distribute_loss over-penalize remaining junior holders. Example: 2 junior holders with 1000 raw balance and 500 loss. - User A withdraws 1 LP: gets 250 (correct), raw_decrease=500 - User B withdraws 1 LP: effective_jb recomputes to 0, gets NOTHING Fair outcome: 250 each. Fix: subtract withdrawal_amount (the actual loss-adjusted collateral leaving the vault) from junior_balance instead of the proportional raw share. Since total_withdrawn also increases by withdrawal_amount, gross_senior = (deposited - withdrawn) - junior_balance stays constant across partial junior withdrawals, preserving per-LP effective value. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/processor.rs (1)
908-926: Please add a regression test for the reproduced loss case.This arithmetic is subtle and easy to regress. A focused test for “2 junior LPs, active insurance loss, sequential 50% withdrawals” would pin the intended behavior: both users receive the same 250 payout, and the second withdraw does not collapse to zero.
I can draft that test if you'd like.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/processor.rs` around lines 908 - 926, Add a focused regression test that reproduces the “2 junior LPs, active insurance loss, sequential 50% withdrawals” scenario to ensure the arithmetic in pool.set_junior_balance and distribute_loss remains correct: create a pool with known gross_pool and two junior LPs each with 500 effective value, apply an insurance loss so effective_junior_balance < raw balance, have the first junior withdraw 50% (expect payout 250), then the second junior withdraw 50% and assert they also receive 250 (not zero); locate helpers that construct pools and perform withdrawals (functions referencing pool.set_junior_balance, withdrawal_amount, junior_balance, total_withdrawn, and distribute_loss) and implement the test (e.g., test_sequential_junior_withdrawals_under_loss) asserting both payouts equal 250 and that pool balances update without inflating gross_senior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/processor.rs`:
- Around line 908-926: Add a focused regression test that reproduces the “2
junior LPs, active insurance loss, sequential 50% withdrawals” scenario to
ensure the arithmetic in pool.set_junior_balance and distribute_loss remains
correct: create a pool with known gross_pool and two junior LPs each with 500
effective value, apply an insurance loss so effective_junior_balance < raw
balance, have the first junior withdraw 50% (expect payout 250), then the second
junior withdraw 50% and assert they also receive 250 (not zero); locate helpers
that construct pools and perform withdrawals (functions referencing
pool.set_junior_balance, withdrawal_amount, junior_balance, total_withdrawn, and
distribute_loss) and implement the test (e.g.,
test_sequential_junior_withdrawals_under_loss) asserting both payouts equal 250
and that pool balances update without inflating gross_senior.
Summary
During active insurance loss (
total_flushed > total_returned), partial junior withdrawals used a proportional share of the rawjunior_balanceas the decrease amount. Butwithdrawal_amountwas computed from the loss-adjustedeffective_junior_balance, which is smaller. This mismatch causedgross_senior(= gross_pool - junior_balance) to inflate after each junior withdrawal, makingdistribute_lossover-penalize remaining junior holders.Reproduction
Initial state:
junior_balance=1000,junior_total_lp=2,total_flushed=500, 1 senior LP.Fair outcome: A gets 250, B gets 250. Actual (buggy): A gets 250, B gets 0.
Root Cause
raw_decrease = lp_amount * junior_balance / junior_lp_beforeremoves a proportional share of the gross balance (500), buttotal_withdrawnonly increases bywithdrawal_amount(250). The 250 discrepancy shifts intogross_senior, causingdistribute_lossto re-apply the fullnet_lossagainst the diminished junior balance.Fix
Subtract
withdrawal_amountfromjunior_balanceinstead of the proportional raw share. Sincetotal_withdrawnalso increases bywithdrawal_amount,gross_senior = (deposited - withdrawn) - junior_balancestays constant across partial junior withdrawals.Verification with fix:
Both get 250. Fair.
Severity
MEDIUM — first-mover advantage for junior tranche holders during loss periods; last withdrawer can receive zero and have funds permanently locked
Test plan
cargo build— compiles cleanlycargo clippy— zero warnings🤖 Generated with Claude Code
Summary by CodeRabbit