Skip to content

fix(#159): gate senior deposits while an insurance loss has spilled past junior — rebased from #160 - #164

Merged
dcccrypto merged 1 commit into
mainfrom
fix/v17-senior-deposit-loss-gate-rebased
Jun 18, 2026
Merged

fix(#159): gate senior deposits while an insurance loss has spilled past junior — rebased from #160#164
dcccrypto merged 1 commit into
mainfrom
fix/v17-senior-deposit-loss-gate-rebased

Conversation

@dcccrypto

Copy link
Copy Markdown
Owner

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 > 0 IFF net_loss > junior_balance), senior_balance() is depressed. A late senior depositor mints cheap LP and redeems at the restored price after ReturnInsurance, stealing recovery from the incumbent seniors who bore the loss. The mirror of the #150 junior bug.

Fix

In process_deposit_senior, after pre_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 bare flushed > returned — so a junior-absorbed loss doesn't DoS senior deposits. Self-lifts on ReturnInsurance. Tranche-only; non-tranche/global path untouched.

Verified locally

cargo build-sbf clean; poc_senior_deposit_loss_snipe (3) + error_codes (3) pass; full unit/integration suite green. The 4 v17_stake_insurance_e2e failures 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).

…(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>
@dcccrypto
dcccrypto merged commit 682fb4b into main Jun 18, 2026
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@dcccrypto, we couldn't start this review because you've reached your PR review rate limit.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53cad197-1e3b-443f-8a98-de7e95705379

📥 Commits

Reviewing files that changed from the base of the PR and between 8c4ceec and 870673c.

📒 Files selected for processing (2)
  • src/processor.rs
  • tests/poc_senior_deposit_loss_snipe.rs
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/v17-senior-deposit-loss-gate-rebased

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Senior-deposit recovery-snipe: #145 only half-fixed — senior path never gated (loss spills past junior)

2 participants