diff --git a/src/processor.rs b/src/processor.rs index 7f5c03c..ccf9a57 100644 --- a/src/processor.rs +++ b/src/processor.rs @@ -664,9 +664,11 @@ fn process_withdraw( // PERC-303: Determine withdrawal amount based on tranche let withdrawal_amount = if pool.tranche_enabled() && is_junior { - // Junior withdrawal: valued against junior sub-pool only + // Junior withdrawal: valued against junior sub-pool after loss absorption. + // effective_junior_balance() deducts insurance losses that junior absorbs first, + // so junior LP holders correctly receive a reduced payout when the pool lost funds. let junior_lp = pool.junior_total_lp(); - let junior_bal = pool.junior_balance(); + let junior_bal = pool.effective_junior_balance(); crate::math::calc_junior_collateral_for_withdraw(junior_lp, junior_bal, lp_amount) .ok_or(StakeError::Overflow)? } else { diff --git a/src/state.rs b/src/state.rs index 165c136..e89ac9d 100644 --- a/src/state.rs +++ b/src/state.rs @@ -257,9 +257,30 @@ impl StakePool { self.total_lp_supply.saturating_sub(self.junior_total_lp()) } - /// Derived: senior balance = total_pool_value - junior_balance. + /// Junior balance after absorbing insurance losses. + /// + /// When `total_flushed > total_returned` there is an outstanding loss. + /// Junior tranche absorbs that loss first (up to its full balance). + /// `distribute_loss` is the canonical implementation: junior absorbs first, + /// senior only loses once junior is wiped out. + pub fn effective_junior_balance(&self) -> u64 { + let jb = self.junior_balance(); + let loss = self.total_flushed.saturating_sub(self.total_returned); + if loss == 0 { + return jb; + } + // senior_balance is the pool value not attributed to junior — used by + // distribute_loss to cap the allocation of loss to the junior tranche. + let pv = self.total_pool_value().unwrap_or(jb); + let sb = pv.saturating_sub(jb); + let (junior_loss, _) = crate::math::distribute_loss(jb, sb, loss); + jb.saturating_sub(junior_loss) + } + + /// Derived: senior balance = total_pool_value - effective_junior_balance. pub fn senior_balance(&self) -> Option { - self.total_pool_value()?.checked_sub(self.junior_balance()) + let pv = self.total_pool_value()?; + Some(pv.saturating_sub(self.effective_junior_balance())) } /// Current struct version. Increment when layout changes.