fix: lock junior_fee_mult_bps once juniors are deposited - #127
Conversation
process_admin_set_tranche_config allowed the admin to change junior_fee_mult_bps at any time, even while junior LPs were deposited. Since process_accrue_fees reads the multiplier live (no per-epoch snapshot), a mid-life change immediately rewrites the fee split for every junior LP. Attack path: 1. Juniors deposit at multiplier M_1 2. Vault accumulates trading fees 3. Admin bumps multiplier to M_2 > M_1 4. Anyone calls AccrueFees — junior sub-pool captures outsized share 5. Admin (holding a junior position) withdraws at inflated value 6. Admin resets multiplier to M_1 to cover tracks The inverse also works: admin depresses the multiplier to silently reduce junior yield below what depositors were promised at deposit time. Fix: block any multiplier change when junior_total_lp() > 0. Idempotent re-writes (same value) still succeed so admin tooling can reapply config. Once all juniors withdraw (junior_total_lp back to 0), the multiplier is freely configurable for the next cohort. The "no disable" issue (set_tranche_enabled always hardcoded true) is left as-is — it's a design limitation, but because the current code cannot disable tranches, the related "disable mid-flight" attack surface that Agents A/C raised does not exist in practice. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 46 minutes and 20 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ✨ 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 |
…Kani (#143) (#148) * fix(stake,v17): pre-accrue mode-1 fees on the junior deposit path (#146) 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> * fix(stake,v17): restore protections dropped in the v17 convergence (#143) 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> --------- Co-authored-by: 0X-SquidSol <david.laszczynski@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
process_admin_set_tranche_configallowed the admin to changejunior_fee_mult_bpsat any time, even while junior LPs were already deposited. Becauseprocess_accrue_feesreads the multiplier live (no per-epoch snapshot, seedistribute_feescall at L1696), a mid-life change immediately rewrites the fee split for every junior LP.Attack
M_1(economic term they accepted)M_2 > M_1AccrueFees— junior sub-pool captures outsized shareM_1to cover tracksInverse attack: Admin depresses multiplier to silently reduce junior yield below what was promised at deposit time. Depositors have no recourse — they can't back out without losing cooldown/HWM protections.
Severity
MEDIUM — governance/economic risk. Not immediate theft but a clear avenue for admin value extraction or unilateral repricing of outstanding obligations.
Fix
Block any multiplier change when
junior_total_lp() > 0. Idempotent re-writes (same value) still succeed so admin tooling can reapply config. Once all juniors withdraw (junior_total_lpback to 0), the multiplier is freely configurable for the next cohort.Design rationale
set_tranche_enabled(true)hardcoded) is left as-is. Agents A/C raised a "disable mid-flight" attack vector, but because the current code literally cannot disable tranches, that attack surface doesn't exist in practice. A separate follow-up PR can add a properenabled: boolparameter (wire-format change) if the product requires it.Verification
cargo checkvia sbpf toolchain — compiles cleanly🤖 Generated with Claude Code