fix(#159): gate senior deposits while an insurance loss has spilled past junior — rebased from #160 - #164
Conversation
…(complete #150 senior side) junior side of the recovery-snipe (#145). But #145 noted "senior is the same once a loss spills past junior" and the SENIOR deposit path was never gated. When a flushed loss Φ exceeds the junior balance J, junior is wiped and the senior sub-pool is marked down by Φ−J (distribute_loss: senior_loss > 0 IFF net_loss > junior_balance). senior_balance() is depressed for the open-loss window, so a late senior depositor can mint cheap LP and redeem at the restored price after ReturnInsurance, capturing a pro-rata slice of the recovery from the incumbent seniors who bore the loss (conservation-exact, unprivileged). Reproduced: senior 900k + junior 100k, flush 600k → senior_balance 400k; attacker deposits 400k (mints 900k LP); return 600k → senior_balance 1.3M; attacker withdraws 650k (+250k), the incumbent senior loses exactly 250k. Fix: in process_deposit, before pricing, pause the senior (tranche) path while net_loss > junior_balance. The condition is PRECISE rather than the junior gate's bare flushed > returned: senior_balance() is a pure function of current state, so senior is depressed IFF net_loss > junior_balance(now). A junior-ABSORBED loss (net_loss <= junior_balance) leaves senior_balance() unchanged with nothing to snipe, so senior deposits stay open — important because the symmetric gate would DoS senior deposits for the entire (possibly never-returned) life of a loss junior fully covers. Reuses StakeError::InsuranceLossOutstanding; self-lifts as ReturnInsurance raises total_returned. Scoped to tranche_enabled() so the non-tranche/global path (#139) is untouched; the senior WITHDRAW path is left unchanged (separate finding). Adds tests/poc_senior_deposit_loss_snipe.rs: reproduces the +250k/-250k transfer; shows the gate fires only when senior is marked down and lifts on return; and locks in the dynamic-junior case (a junior partial exit during a junior-absorbed loss does NOT false-trigger the precise gate, because the raw junior_balance decrement is loss-adjusted, not proportional). Builds on #150 (uses the InsuranceLossOutstanding gate it introduced). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
More reviews will be available in 14 minutes and 23 seconds. Learn how PR review limits work. Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file). ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits. 🚦 How do rate limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
✨ 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 |
Rebased version of #160 (authored by @0x-SquidSol — authorship preserved; finding credit for #159 stands). Fresh PR because the fork branch can't be force-updated from here. #160 was a superset of the already-merged #150/#153 junior gate; this PR keeps only the new senior-side gate + its PoC (the duplicated junior gate/error/PoC are already on main).
Verified real on v17
The senior recovery-snipe: when tranches are on and a flushed loss spills past junior into senior (
distribute_loss:senior_loss > 0IFFnet_loss > junior_balance),senior_balance()is depressed. A late senior depositor mints cheap LP and redeems at the restored price afterReturnInsurance, stealing recovery from the incumbent seniors who bore the loss. The mirror of the #150 junior bug.Fix
In
process_deposit_senior, afterpre_accrue_mode1(kept — #148 refactor; #160's branch predated it and carried the old inline accrue, which I dropped in the rebase), gate exactly the snipe window:tranche_enabled && net_loss > junior_balance() → InsuranceLossOutstanding. Precise — not the junior gate's bareflushed > returned— so a junior-absorbed loss doesn't DoS senior deposits. Self-lifts onReturnInsurance. Tranche-only; non-tranche/global path untouched.Verified locally
cargo build-sbfclean;poc_senior_deposit_loss_snipe(3) +error_codes(3) pass; full unit/integration suite green. The 4v17_stake_insurance_e2efailures are pre-existing on main (LiteSVM bind/rotate, documented in the #152 review) — identical set fails without this PR.Supersedes #160. Closes #159 (senior side).