From 75150f1725bea7a94eb04cf2d717613df4c91511 Mon Sep 17 00:00:00 2001 From: 0X-SquidSol Date: Thu, 9 Apr 2026 22:45:07 -0400 Subject: [PATCH] fix: use withdrawal_amount for junior_balance decrease to fix ordering 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) --- src/processor.rs | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/processor.rs b/src/processor.rs index f4d05ef..6d85be6 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -905,17 +905,24 @@ fn process_withdraw( // (supply=0 but value>0) and locks tokens permanently in the vault. pool.set_junior_balance(0); } else { - // Proportional decrease of the RAW junior_balance based on LP share, - // not the loss-adjusted withdrawal_amount. This keeps the raw balance - // in sync with the remaining LP holders' proportional claim. - let jb = pool.junior_balance(); - let raw_decrease = (lp_amount as u128) - .checked_mul(jb as u128) - .and_then(|v| v.checked_div(junior_lp_before as u128)) - .map(|v| v as u64) - .ok_or(StakeError::Overflow)?; + // Decrease junior_balance by withdrawal_amount (the loss-adjusted + // collateral actually leaving the vault), NOT by a proportional share + // of the raw balance. + // + // During active insurance loss, withdrawal_amount < proportional raw + // share because it is based on effective_junior_balance (post-loss). + // Using the larger proportional raw decrease causes gross_senior + // (= gross_pool - junior_balance) to inflate after each junior + // withdrawal, making distribute_loss over-penalize remaining juniors. + // The last junior withdrawer can receive zero even though they hold + // a fair share of the effective pool. + // + // By subtracting withdrawal_amount from both total_withdrawn (above) + // and junior_balance here, gross_senior stays constant across partial + // junior withdrawals, preserving per-LP effective value. pool.set_junior_balance( - jb.checked_sub(raw_decrease) + pool.junior_balance() + .checked_sub(withdrawal_amount) .ok_or(StakeError::Overflow)?, ); }