fix(stake): junior JIT fee-snipe (#146) + junior-mult lock & tranche Kani (#143) - #148
Conversation
process_deposit_junior was the only pricing path that did not crystallize pending mode-1 trading-fee surplus before pricing — process_deposit (senior/ global) and process_withdraw already do (the #136 fix). A junior depositor could mint LP at the stale pre-fee price and, after a permissionless AccrueFees, capture a multiplier-weighted (up to 5x) share of fees earned before they joined (PoC: deposit 1,000,000 -> withdraw 1,400,000, +400,000). This is a v17-convergence drop: the original #136 fix covered all three paths. The pre-accrue block was inline-duplicated across the paths, which is how the junior copy went missing. Extract it into one shared helper pre_accrue_mode1( pool, vault) and route all three sites through it so they cannot drift again. The deposit/withdraw change is a behavior-preserving extraction (verbatim body); the junior path gets the call before pricing (after the cap/token-program/ATA checks, matching process_deposit) so it folds only the fee surplus (vault read pre-transfer) and prices against the post-accrual junior balance. Adds tests/poc_junior_jit_fee_snipe.rs (mirrors poc_jit_fee_snipe.rs): documents the +400,000 snipe under current pricing and asserts the pre-accrue neutralizes it. Verified against the v17 production functions (current 1,400,000; fixed 999,999). cargo build --lib + cargo build-sbf clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
) The v17-convergence line forked before two main PRs landed and didn't re-incorporate them. Both are absent from v17; restored here. 1. junior_fee_mult_bps governance lock (originally #127): process_admin_set_tranche_config validated the multiplier range but no longer blocked changing it once junior LPs exist. process_accrue_fees reads the multiplier live (no per-epoch snapshot), so a mid-life change re-prices the junior/senior fee split for already-committed junior LPs (admin can pump before AccrueFees to extract an outsized share, or depress to cut promised junior yield). Restore: reject any change when junior_total_lp() > 0; idempotent same-value rewrites still allowed; freely configurable once all juniors exit. 2. Tranche-math Kani proofs (§15, originally part of the #140/#141 coverage): the Kani suite covered only the global path. Restore the 10 tranche harnesses + their u32/u64 mirrors: distribute_loss conservation + junior-first; distribute_fees conservation + no-senior-strands-to-junior; sub-pool C9 guard + first-depositor 1:1; sub-pool round-trip no-profit; senior_balance non-underflow; tranche decomposition. (Tranche proptests already survived.) Verification: cargo build --lib + cargo build-sbf clean; kani crate compiles; the §15 invariants verified exhaustively (405,121 cases) against the v17 production functions out-of-band; cargo kani + cargo test run them in CI. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (3)
📝 WalkthroughWalkthroughFixes a permissionless junior-tranche JIT fee-snipe by adding a ChangesJunior JIT Fee Snipe Fix, Tranche Math Proofs, and Regression Tests
Sequence Diagram(s)sequenceDiagram
participant Eve as Eve (Attacker)
participant process_deposit_junior
participant pre_accrue_mode1
participant accrue_fees_inner
participant AccrueFees as AccrueFees (permissionless)
participant process_withdraw_junior
Note over process_deposit_junior: BEFORE FIX — no pre-accrue
Eve->>process_deposit_junior: DepositJunior(1_000_000)
process_deposit_junior->>process_deposit_junior: price LP at stale junior balance (no fee crystallization)
process_deposit_junior-->>Eve: 1_000_000 junior LP minted at stale price
Eve->>AccrueFees: AccrueFees (permissionless)
AccrueFees->>accrue_fees_inner: fold 1_000_000 surplus into balances
accrue_fees_inner-->>AccrueFees: junior_balance += 800_000
Eve->>process_withdraw_junior: Withdraw(1_000_000 LP)
process_withdraw_junior-->>Eve: 1_400_000 collateral (profit +400_000)
Note over process_deposit_junior: AFTER FIX — pre_accrue_mode1 added
Eve->>process_deposit_junior: DepositJunior(1_000_000)
process_deposit_junior->>pre_accrue_mode1: pool, vault (pre-transfer balance)
pre_accrue_mode1->>accrue_fees_inner: crystallize surplus before pricing
accrue_fees_inner-->>pre_accrue_mode1: fees already folded
pre_accrue_mode1-->>process_deposit_junior: done
process_deposit_junior->>process_deposit_junior: price LP at updated junior balance
process_deposit_junior-->>Eve: fewer LP minted at fair post-accrue price
Eve->>process_withdraw_junior: Withdraw(LP)
process_withdraw_junior-->>Eve: ≤1_000_000 collateral (no profit)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 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 |
#149) — rebased from #150 (#153) * fix(stake,v17): pause junior deposits while an insurance loss is outstanding (#149) effective_junior_balance() applies the pool's CURRENT net_loss to the junior tranche with no baseline for when the cohort began. So a junior depositing into a pool that already has an outstanding loss (total_flushed > total_returned) — borne by the global/senior cohort — instantly inherits that pre-existing loss, making the incumbents whole at the new junior's expense (PoC: Greg deposits 3M, admin flushes 600k; Jane deposits 1M as first junior and is immediately worth 400k; Greg recovers his full 3M). Gate process_deposit_junior to reject (StakeError::InsuranceLossOutstanding) while total_flushed > total_returned. Both this loss-inheritance AND the mirror recovery-snipe (#145) require a junior deposit during an outstanding loss, so this one gate closes both. Junior-only: a senior deposit prices against the current marked-down senior_balance and never perturbs effective_junior_balance, so it neither inherits a loss nor needs gating. Mode-1 is unaffected (flush is mode-0 only, so the condition is never true there). Deposits resume once insurance is returned (total_flushed/returned move only via admin Flush/Return, so no unprivileged DoS). Considered but rejected for THIS fix: a per-cohort loss-baseline snapshot (keeps the junior tranche open + fairly priced during a loss). More flexible, but it rewrites the audited effective_junior_balance loss math and adds StakePool state + cohort bookkeeping — wrong risk profile for a security fix. Recommended as a follow-up feature if continuous junior subscriptions during a claim are wanted. Adds tests/poc_junior_preexisting_loss.rs: documents the loss-reassignment math and pins the gate condition (fires while outstanding, lifts on full return). The handler revert is covered by the v17 LiteSVM e2e. cargo build --lib + cargo build-sbf clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(error-codes): enumerate InsuranceLossOutstanding (+ backfill 22/23) for #150 rebase Adds StakeError::InsuranceLossOutstanding (24) to both error_codes test arrays. Also backfills ZeroSharesMinted (22) + NoPendingAdmin (23), which were already missing from the enumeration on main, and bumps the sequential check to 0..24 so test_all_error_codes_unique stays complete. Rebase-completion for #150 (authored by 0x-SquidSol): the gate was re-applied AFTER pre_accrue_mode1 (PR #148) so the JIT fee-snipe guard stays intact. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: 0X-SquidSol <david.laszczynski@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: dcccrypto <dcccrypto@users.noreply.github.com>
Integrates two verified stake fixes (cherry-picked with attribution; build-sbf clean + 327 tests pass):
process_deposit_junioromitted the mode-1 fee pre-accrue (junior twin of the senior Trading-LP deposits do not crystallize pending fees before pricing — JIT depositor captures fees earned by existing LPs #136 fix) → permissionless JIT fee-snipe on the junior tranche. Centralizes the guard intopre_accrue_mode1(pool, vault)used by senior deposit, withdraw, AND junior deposit.junior_fee_mult_bpslock (block value-change in AdminSetTrancheConfig while junior_total_lp>0; idempotent same-value allowed; unlocks when juniors exit) + tranche-math Kani proofs. (Low/hardening — verified it does NOT break the existing admin-set-tranche tests.)Closes #146, #143. Supersedes #147, #144.
Summary by CodeRabbit
New Features
Bug Fixes
Tests