Skip to content

fix(stake): 5 verified v17 bounty findings — senior pricing (HIGH), JIT-fee snipe, distribute_fees, cooldown bound, tranche coverage - #142

Merged
dcccrypto merged 6 commits into
mainfrom
bounty/stake-fixes
Jun 17, 2026
Merged

fix(stake): 5 verified v17 bounty findings — senior pricing (HIGH), JIT-fee snipe, distribute_fees, cooldown bound, tranche coverage#142
dcccrypto merged 6 commits into
mainfrom
bounty/stake-fixes

Conversation

@dcccrypto

@dcccrypto dcccrypto commented Jun 17, 2026

Copy link
Copy Markdown
Owner

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 c3a617a and auto-merged into the v17 CRITICAL-1 admin redesign incorrectly, so each fix was reviewed and integrated manually). Each was independently verified against source (adversarial review) before integration.

Fixes

Finding Sev Fix Credit
#134 HIGH Senior deposits priced against the senior sub-pool (mirrors the withdraw basis) + reject deposit into orphaned senior value (C9), no unconditional 1:1 bootstrap. Closes the deposit-cheap/redeem-dear value extraction from senior LPs after a junior-absorbed loss. @0x-SquidSol (#135)
#136 MED Crystallize pending trading-fee surplus into share price BEFORE pricing deposits/withdraws (shared accrue_fees_inner), closing the JIT fee-snipe. @0x-SquidSol (#137)
#120 LOW Overflow-safe mul_div_floor in distribute_fees (was handing junior 100% on u128 overflow). @Nullguy42069 (#133)
#121 LOW Upper-bound cooldown_slots (MAX_COOLDOWN_SLOTS ~1yr) so an admin can't set u64::MAX and freeze withdrawals. @Nullguy42069 (#132)
#140 cov Proptest coverage for tranche math (round-trips, conservation, partition invariants). @0x-SquidSol (#141)

Verification

  • cargo test: ~325 tests pass, 0 failures (incl. new PoCs poc_senior_deposit_mispricing, poc_senior_bootstrap_orphan, poc_jit_fee_snipe, and proptest_math).
  • cargo build-sbf: BPF artifact compiles clean (189 KB).

Notes / deferred

Closes #120, #121, #134, #136, #140. Supersedes #132, #133, #135, #137, #141 (integrated here with attribution).

Summary by CodeRabbit

Release Notes

  • Bug Fixes

    • Fixed fee distribution calculation at extreme balances to prevent incorrect tranche allocation
    • Enhanced LP minting for senior tranches with improved safety guardrails
    • Added validation to prevent withdrawal cooldowns from permanently freezing withdrawals
    • Fixed deposit and withdrawal pricing in trading pools to properly account for pending fees
    • Blocked potential exploit in senior tranche deposits during bootstrap phase
  • Tests

    • Added comprehensive regression and property tests covering edge cases and tranche scenarios

1Jarvis42069 and others added 6 commits June 18, 2026 00:07
…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>
@dcccrypto
dcccrypto merged commit a5562ef into main Jun 17, 2026
1 check was pending
@coderabbitai

coderabbitai Bot commented Jun 17, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ec4c784e-fa16-4f24-b565-4a2caaeca3b3

📥 Commits

Reviewing files that changed from the base of the PR and between bdd0539 and 3cabf8e.

📒 Files selected for processing (6)
  • src/math.rs
  • src/processor.rs
  • tests/poc_jit_fee_snipe.rs
  • tests/poc_senior_bootstrap_orphan.rs
  • tests/poc_senior_deposit_mispricing.rs
  • tests/proptest_math.rs

📝 Walkthrough

Walkthrough

Fixes three exploitable tranche/trading-pool vulnerabilities: senior LP deposits are repriced against the senior sub-pool via a new calc_senior_lp_for_deposit (with orphaned-value guard); trading-pool deposits and withdrawals now crystallize unaccrued vault surplus via a new accrue_fees_inner helper before pricing; and distribute_fees switches to an overflow-safe mul_div_floor to correct extreme-balance fee splits. Also adds MAX_COOLDOWN_SLOTS validation and comprehensive PoC/property-test coverage.

Changes

Tranche security fixes: senior deposit pricing, JIT fee-snipe prevention, overflow-safe math

Layer / File(s) Summary
mul_div_floor helper and distribute_fees overflow fix
src/math.rs
Introduces mul_div_floor using a 256-bit-style fast/slow long-division path, and replaces the overflow-clamping fallback in distribute_fees with the exact helper, preventing incorrect 100%-to-junior allocation at extreme u128 balance ranges (fixes #120).
calc_senior_lp_for_deposit with orphan guard
src/math.rs
Adds pub fn calc_senior_lp_for_deposit delegating to calc_lp_for_deposit against senior sub-pool parameters, inheriting orphaned-value blocking (senior_total_lp == 0 && senior_balance > 0None) and round-down semantics; extends unit tests for all guard cases.
accrue_fees_inner extraction and JIT pre-crystallization
src/processor.rs
Extracts fee accrual into accrue_fees_inner; wires both process_deposit and process_withdraw in trading-pool mode to crystallize pending vault surplus before LP pricing/burn; deposit path also switches to calc_senior_lp_for_deposit when tranches are enabled.
MAX_COOLDOWN_SLOTS config bound
src/processor.rs
Introduces MAX_COOLDOWN_SLOTS and extends validate_cooldown_slots to reject both zero-valued and over-maximum cooldown configuration.
JIT fee-snipe PoC and regression tests
tests/poc_jit_fee_snipe.rs
Models current vs. fixed deposit helpers; demonstrates attacker profitability with uncrystallized surplus at stale pricing, then asserts the fixed pre-accrual path prevents the snipe and preserves honest LP fair value.
Senior bootstrap orphan (C9-bypass) PoC tests
tests/poc_senior_bootstrap_orphan.rs
Constructs orphaned-state pools; demonstrates the prior 1:1 bootstrap drained orphaned balance; asserts the fix returns None for orphan deposits while legitimate first-senior deposits still mint 1:1.
Senior deposit mispricing PoC tests
tests/poc_senior_deposit_mispricing.rs
Reproduces the global-price exploit path, asserts sub-pool pricing prevents extraction without diluting incumbents, and verifies first-senior bootstrap correctness.
Proptest tranche math property suite
tests/proptest_math.rs
Adds tranche_pool helper and proptest! suite asserting loss/fee conservation, orphan blocking, first-depositor 1:1, no-profit round-trips, and valuation partitioning invariants against real production functions.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • dcccrypto/percolator-stake#22: Both PRs modify validate_cooldown_slots in src/processor.rs to add bounds checking on the cooldown configuration value.
  • dcccrypto/percolator-stake#98: Both PRs change tranche fee-accrual and junior/senior allocation logic driven by distribute_fees in processor.rs.
  • dcccrypto/percolator-stake#112: The new calc_senior_lp_for_deposit delegates to calc_lp_for_deposit, whose None-propagation behavior for total_pool_value() == None directly affects orphan-guard and senior deposit tests introduced here.

Poem

🐇 Hoppity-hop through the tranche we go,
No orphaned value shall sneak past my nose!
I crystallize fees before prices are set,
And mul-div-floor keeps the math correct yet.
The JIT sniper finds nothing to steal —
This bunny patched math with arithmetic zeal! 🌟

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bounty/stake-fixes

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.

math: distribute_fees double-overflow fallback gives 100% to junior at extreme u64 values

3 participants