Skip to content

fix(stake): junior JIT fee-snipe (#146) + junior-mult lock & tranche Kani (#143) - #148

Merged
dcccrypto merged 2 commits into
mainfrom
bounty/stake-junior-jit-lock
Jun 18, 2026
Merged

fix(stake): junior JIT fee-snipe (#146) + junior-mult lock & tranche Kani (#143)#148
dcccrypto merged 2 commits into
mainfrom
bounty/stake-junior-jit-lock

Conversation

@dcccrypto

@dcccrypto dcccrypto commented Jun 18, 2026

Copy link
Copy Markdown
Owner

Integrates two verified stake fixes (cherry-picked with attribution; build-sbf clean + 327 tests pass):

Closes #146, #143. Supersedes #147, #144.

Summary by CodeRabbit

  • New Features

    • Added governance lock for tranche configuration to prevent changes when junior LPs are active.
  • Bug Fixes

    • Fixed fee crystallization in junior deposit pricing to prevent LP price manipulation.
  • Tests

    • Expanded formal verification from 44 to 54 harnesses.
    • Added regression test for junior tranche fee exploit scenario.

0x-SquidSol and others added 2 commits June 18, 2026 17:45
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>
@dcccrypto
dcccrypto merged commit 9addf4e into main Jun 18, 2026
@dcccrypto
dcccrypto deleted the bounty/stake-junior-jit-lock branch June 18, 2026 16:47
@coderabbitai

coderabbitai Bot commented Jun 18, 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: a17c5391-ebd1-4692-a3d9-3d49c2e112dc

📥 Commits

Reviewing files that changed from the base of the PR and between a5562ef and 8200b1f.

📒 Files selected for processing (3)
  • kani-proofs/src/lib.rs
  • src/processor.rs
  • tests/poc_junior_jit_fee_snipe.rs

📝 Walkthrough

Walkthrough

Fixes a permissionless junior-tranche JIT fee-snipe by adding a pre_accrue_mode1 helper that crystallizes pending trading fees before pricing, called from process_deposit_junior (the missing site), process_deposit, and process_withdraw. Adds a governance lock on junior_fee_mult_bps while junior LPs exist. Extends kani-proofs/src/lib.rs with tranche math helpers and 9 new proof harnesses, plus a PoC regression test.

Changes

Junior JIT Fee Snipe Fix, Tranche Math Proofs, and Regression Tests

Layer / File(s) Summary
Tranche math helpers
kani-proofs/src/lib.rs
Adds calc_subpool_lp_for_deposit, calc_subpool_collateral_for_withdraw, distribute_loss, distribute_fees, total_pool_value_mode0, effective_junior_balance, and senior_balance as pub functions; updates Kani suite harness count header.
Kani SECTION 15 tranche proof harnesses
kani-proofs/src/lib.rs
Adds 9 Kani harnesses verifying loss/fee conservation, junior-first absorption, all-fees-to-junior when senior is zero, sub-pool C9 orphan rejection, 1:1 first deposit, deposit→withdraw no-profit, senior valuation non-underflow, and senior_balance + effective_junior_balance == pool_value.
pre_accrue_mode1 helper and processor wiring
src/processor.rs
Extracts the mode-1 pre-accrue inline blocks from process_deposit and process_withdraw into shared private helper pre_accrue_mode1(pool, vault); adds the missing call in process_deposit_junior before LP pricing.
junior_fee_mult_bps governance lock
src/processor.rs
process_admin_set_tranche_config rejects multiplier changes while junior_total_lp() > 0; rewrites to the same stored value are still allowed.
PoC regression tests
tests/poc_junior_jit_fee_snipe.rs
Two-test file: first test asserts the snipe is profitable with current (unfixed) pricing; second test asserts the fixed pre-accrue path yields no profit for the attacker.

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

  • dcccrypto/percolator-stake#98: Introduces the same effective_junior_balance / tranche accounting functions and distribute_fees logic that this PR mirrors in kani-proofs/src/lib.rs and applies in processor.rs.
  • dcccrypto/percolator-stake#142: Fixes production distribute_fees / senior pricing that feeds the same tranche accounting invariants now formally proven in this PR's Kani SECTION 15.

Poem

🐇 Hop hop, the sniper set a trap so sly,
Minting LP shares while fees slipped by.
But now we pre-accrue before the price,
The junior tranche is guarded — nice!
No free lunch for Eve, the math rings true,
Senior, junior, balanced as dew. 🌿

✨ 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-junior-jit-lock

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.

dcccrypto added a commit that referenced this pull request Jun 18, 2026
#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>
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.

Junior deposit path omits the mode-1 fee pre-accrue — permissionless JIT trading-fee snipe on the junior tranche

2 participants