fix(stake): 5 verified v17 bounty findings — senior pricing (HIGH), JIT-fee snipe, distribute_fees, cooldown bound, tranche coverage - #142
Conversation
…al lock (closes #121) validate_cooldown_slots only rejected 0. An admin could set cooldown_slots = u64::MAX via InitPool or UpdateConfig; process_withdraw's saturating_add then yields a deadline clock.slot can never reach, permanently freezing all withdrawals. Add an upper bound (~1 year of slots ≈ 78.84M) — long enough for any realistic cooldown, finite enough to rule out a permanent lock. Enforced on both the InitPool (L288) and UpdateConfig (L1192) paths, which both call validate_cooldown_slots. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…#120) In the overflow path, part2 computed r * junior_weight / total_weight, but r (< total_weight, ~2^81) * junior_weight (~2^80) overflows u128, and the unwrap_or(total_fee) fallback then handed the junior tranche 100% of the fee (0% to the protected senior tranche). Compute part2 with an exact, overflow-safe 256-bit mul-div (mul_div_floor: full 256-bit product via u64 limbs, then bitwise long division). Added unit tests with arbitrary-precision-verified expected splits for the overflow cases (symmetric/junior-heavy/senior-heavy/mid). Full suite incl. proptest_math passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With tranches enabled, process_deposit minted senior LP at the GLOBAL pool price while senior withdrawals redeem at the SENIOR sub-pool price. After the junior tranche absorbs an insurance loss (total_flushed > total_returned) the global per-LP price falls below the senior per-LP price, so an unprivileged user could mint senior LP cheap (global) and redeem dear (senior), extracting value from existing senior LP holders and the junior first-loss buffer. Price senior deposits via math::calc_senior_lp_for_deposit(senior_total_lp(), senior_balance(), amount) when tranche_enabled() — the same basis the senior withdraw path uses (calc_senior_collateral_for_withdraw) and symmetric with the junior deposit path. A senior_total_lp() == 0 first-depositor 1:1 bootstrap avoids bricking the senior tranche when junior deposited first and a fee/loss left orphaned senior value with zero senior LP. No state-layout change, no ABI change, no new error variants; senior accounting remains fully derived from the global counters. Adds tests/poc_senior_deposit_mispricing.rs (regression: old global pricing is exploitable; sub-pool pricing is not; bootstrap does not brick) and math unit tests for calc_senior_lp_for_deposit. Refs #134 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…conditional bootstrap The senior deposit path special-cased `senior_total_lp() == 0` into an unconditional 1:1 mint, which bypassed the orphaned-value (C9) guard inside `calc_lp_for_deposit`. In the reachable state where all LP has exited (`total_lp_supply == 0`) but the pool still holds value (e.g. insurance was returned post-resolution), a tranche pool has `senior_total_lp() == 0` with `senior_balance() > 0`. The bootstrap minted 1 senior LP for a 1-token deposit against the whole orphaned balance, which the depositor could then withdraw in full at the senior sub-pool price — draining the orphan. Fix: always price senior deposits via `calc_senior_lp_for_deposit` (which delegates to `calc_lp_for_deposit`), with no `senior_total_lp() == 0` special case. It mints 1:1 only for a true first senior (`senior_balance == 0`) and returns `None` for orphaned senior value (`senior_balance > 0`), rejecting the deposit exactly as the non-tranche path does. A legitimate first senior always has `senior_balance == 0` (empty pool, or a junior-first pool where junior captures 100% of fees and absorbs 100% of loss because `gross_senior == 0`), so this does not brick the first senior deposit. Adds tests/poc_senior_bootstrap_orphan.rs: reproduces the orphan-theft under the old bootstrap, asserts the fix rejects it, and proves the first senior still mints 1:1 across empty / junior-only / junior-only-post-loss-and-recovery states. Updates the existing senior-mispricing regression test to the no-bootstrap helper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hdraws JIT fee-snipe (MEDIUM, verified): process_deposit/process_withdraw priced LP against total_pool_value(), which excludes engine-paid fees sitting in the vault until the permissionless AccrueFees folds them. A depositor could mint right before AccrueFees and capture a slice of fees earned before joining; a withdrawer could redeem at the stale price. Extract the fold logic into shared accrue_fees_inner() (byte-identical accounting) and pre-accrue in both deposit and withdraw before pricing; process_accrue_fees now delegates to the same helper. Manually integrated onto v17 (PR #137 auto-merge misaligned with the v17 CRITICAL-1 admin redesign). Co-Authored-By: 0x-SquidSol <david.laszczynski@gmail.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…age gap) Adds proptests for calc_lp/calc_senior/calc_junior round-trips, distribute_fees conservation, and senior/junior pool partition invariants. Fixed the prop_senior_balance_never_underflows generator to construct valid pool states (clamp into range) instead of generate-and-reject, which tripped proptest's global-reject cap. Kani proofs from PR #141 deferred (need merge into v17 kani crate). Original coverage by 0x-SquidSol (#141). Co-Authored-By: 0x-SquidSol <david.laszczynski@gmail.com> 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 (6)
📝 WalkthroughWalkthroughFixes three exploitable tranche/trading-pool vulnerabilities: senior LP deposits are repriced against the senior sub-pool via a new ChangesTranche security fixes: senior deposit pricing, JIT fee-snipe prevention, overflow-safe math
Sequence Diagram(s)sequenceDiagram
rect rgba(180, 60, 60, 0.5)
note over Attacker,process_deposit: Vulnerable path (before fix)
Attacker->>process_deposit: deposit at stale total_pool_value()
process_deposit->>calc_lp_for_deposit: price LP (surplus not crystallized)
calc_lp_for_deposit-->>process_deposit: over-minted LP shares
Attacker->>process_accrue_fees: trigger AccrueFees
process_accrue_fees->>accrue_fees_inner: crystallize surplus
Attacker->>process_withdraw: withdraw (profit extracted)
end
rect rgba(40, 140, 80, 0.5)
note over Depositor,process_deposit: Fixed path
Depositor->>process_deposit: deposit(amount)
process_deposit->>accrue_fees_inner: crystallize vault surplus first
accrue_fees_inner->>distribute_fees: allocate delta to junior (mul_div_floor)
process_deposit->>calc_senior_lp_for_deposit: price LP against senior sub-pool
calc_senior_lp_for_deposit-->>process_deposit: correct LP shares (or None if orphaned)
process_deposit-->>Depositor: mint LP
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 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 |
Consolidates the 5 verified v17-relevant stake findings from the 2026-06-17 bounty wave, applied + adapted to the current v17 stake program (the contributors' PRs branched off pre-v17
c3a617aand auto-merged into the v17CRITICAL-1admin redesign incorrectly, so each fix was reviewed and integrated manually). Each was independently verified against source (adversarial review) before integration.Fixes
accrue_fees_inner), closing the JIT fee-snipe.mul_div_floorindistribute_fees(was handing junior 100% on u128 overflow).cooldown_slots(MAX_COOLDOWN_SLOTS~1yr) so an admin can't setu64::MAXand freeze withdrawals.Verification
cargo test: ~325 tests pass, 0 failures (incl. new PoCspoc_senior_deposit_mispricing,poc_senior_bootstrap_orphan,poc_jit_fee_snipe, andproptest_math).cargo build-sbf: BPF artifact compiles clean (189 KB).Notes / deferred
AdminSetInsurancePolicyvault_auth check) — obviated: that handler doesn't exist in v17 (replaced bybind/burn/rotate insurance_authority) — and fix: lock junior_fee_mult_bps once juniors are deposited #127/ci: add BPF tree-shake verification for dev-only CVE ignores #129/test: add Kani proofs for effective_junior_balance wrapper composition #130, left out of this PR (separate optional hardening, not in the verified-findings set).kani-proofscrate + the kani toolchain to run; proptest coverage lands now.Closes #120, #121, #134, #136, #140. Supersedes #132, #133, #135, #137, #141 (integrated here with attribution).
Summary by CodeRabbit
Release Notes
Bug Fixes
Tests