Skip to content

fix(stake,v17): pre-accrue mode-1 fees on the junior deposit path (closes #146) - #147

Closed
0x-SquidSol wants to merge 1 commit into
dcccrypto:mainfrom
0x-SquidSol:fix/v17-junior-deposit-preaccrue
Closed

fix(stake,v17): pre-accrue mode-1 fees on the junior deposit path (closes #146)#147
0x-SquidSol wants to merge 1 commit into
dcccrypto:mainfrom
0x-SquidSol:fix/v17-junior-deposit-preaccrue

Conversation

@0x-SquidSol

@0x-SquidSol 0x-SquidSol commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Closes #146.

What

process_deposit_junior was the one pricing path that did not crystallize pending mode-1 trading fees before pricing — process_deposit (senior/global) and process_withdraw already did (the #136 fix). So a junior depositor could mint LP at the stale pre-fee price and, after a permissionless AccrueFees, capture a multiplier-weighted (up to 5×) share of fees earned before they joined — a permissionless JIT fee-snipe against the existing junior LPs.

PoC (2× junior multiplier): deposit 1,000,000 → withdraw 1,400,000 (+400,000); with the fix → 999,999 (no profit).

This is a v17-convergence regression: the original #136 fix covered all three pricing paths; v17 kept it on deposit/withdraw and dropped it on the junior path.

Fix

The #136 pre-accrue block was inline-duplicated across the paths — which is exactly how the junior copy went missing. I extracted it into a single shared helper pre_accrue_mode1(pool, vault) and routed all three sites through it, so a future pricing path cannot silently drift again.

  • The process_deposit / process_withdraw change is a behavior-preserving extraction (the helper body is byte-identical to the removed inline blocks).
  • process_deposit_junior gets the call placed before pricing — after the existing cap / token-program / user-ATA checks and before the user→vault transfer, matching process_deposit's ordering. So it folds only the fee surplus (vault balance read pre-transfer, never the deposit's own collateral) and prices against the post-accrual junior balance.

The vault.owner == spl_token::id() check is retained in the helper (it validates the vault account is SPL-owned — distinct from verify_token_program, which validates the token program). Mode-0 is a no-op; the helper is idempotent.

Verification

  • cargo build --lib and cargo build-sbf: clean.
  • New regression test tests/poc_junior_jit_fee_snipe.rs (repo convention, mirrors tests/poc_jit_fee_snipe.rs): junior_jit_fee_snipe_is_profitable_with_current_pricing documents the +400,000; pre_accrue_before_junior_pricing_prevents_snipe guards the fix. Both verified out-of-band against the v17 production functions (current → 1,400,000; fixed → 999,999).
  • Reviewed line-by-line; three independent solution proposals converged on this fix, and the helper extraction was chosen specifically to eliminate the duplication that caused the regression.

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced junior liquidity provider pricing to apply fee accrual consistently before computing deposit shares, preventing unintended profit extraction.
  • Tests

    • Added regression test confirming junior liquidity provider pricing integrity under fee-accrual scenarios.

…ccrypto#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 dcccrypto#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 dcccrypto#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>
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 49d4f693-64e2-410d-9aed-808b68628fb0

📥 Commits

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

📒 Files selected for processing (2)
  • src/processor.rs
  • tests/poc_junior_jit_fee_snipe.rs

📝 Walkthrough

Walkthrough

Introduces a pre_accrue_mode1 helper in processor.rs that crystallizes pending mode-1 trading-fee surplus before LP pricing. The deposit and withdraw paths are refactored to use this helper, and process_deposit_junior gains the previously missing pre-accrual call. A new PoC test file proves the vulnerability and validates the fix.

Changes

Junior JIT Fee-Snipe Fix

Layer / File(s) Summary
pre_accrue_mode1 helper
src/processor.rs
New function at line 1676–1699 that, for mode-1 pools only, verifies SPL Token vault ownership, reads vault token balance, and calls accrue_fees_inner; is a no-op for other modes.
Wire helper into all three LP pricing paths
src/processor.rs
process_deposit (line 468–472) and process_withdraw (line 802–806) replace their inline fee-accrual blocks with pre_accrue_mode1; process_deposit_junior (line 2074–2082) gains the previously absent pre-accrual call before calc_junior_lp_for_deposit.
PoC regression tests
tests/poc_junior_jit_fee_snipe.rs
Pool/accrual/deposit/withdraw simulators and two #[test] cases: one asserting the bug is reproducible with the old stale-price flow (+400,000 profit), and one asserting the fixed flow prevents any profit (returned ≤ deposited).

Sequence Diagram(s)

sequenceDiagram
  participant Eve
  participant process_deposit_junior
  participant pre_accrue_mode1
  participant accrue_fees_inner
  participant AccrueFees

  Note over Eve,AccrueFees: Bug scenario (no pre-accrue)
  Eve->>process_deposit_junior: DepositJunior(1_000_000) — vault has un-accrued 1_000_000 surplus
  process_deposit_junior->>process_deposit_junior: calc_junior_lp_for_deposit at stale price → 1_000_000 LP
  Eve->>AccrueFees: trigger permissionless accrual
  AccrueFees->>accrue_fees_inner: crystallize 1_000_000 surplus → junior_balance += 800_000
  Eve->>process_deposit_junior: Withdraw(1_000_000 LP) → receives 1_400_000 (+400_000 profit)

  Note over Eve,AccrueFees: Fixed scenario (pre_accrue_mode1 added)
  Eve->>process_deposit_junior: DepositJunior(1_000_000)
  process_deposit_junior->>pre_accrue_mode1: crystallize surplus before pricing
  pre_accrue_mode1->>accrue_fees_inner: fee_delta applied, junior_balance updated
  process_deposit_junior->>process_deposit_junior: calc_junior_lp_for_deposit at post-accrual price → fair LP
  Eve->>AccrueFees: trigger accrual (no pending surplus left)
  Eve->>process_deposit_junior: Withdraw → receives ≤ 1_000_000 (no profit)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • dcccrypto/percolator-stake#98: Modifies process_deposit_junior LP mint pricing inputs and process_accrue_fees, directly overlapping with the junior pricing path this PR fixes.
  • dcccrypto/percolator-stake#142: Adds shared fee-accrual helpers before deposit/withdraw/junior pricing in processor.rs for the same JIT fee-snipe class of fix, making it directly related at the code-logic level.
  • dcccrypto/percolator-stake#103: Adjusts process_withdraw junior tranche accounting, overlapping with the withdraw pre-accrual path refactored in this PR.

Poem

🐇 Hop, hop — no sniping here today,
The fees are crystallized before you play.
Eve tried to mint at prices stale,
But pre-accrue put that snipe to fail.
The junior tranche is safe at last —
No profit from the fee-filled past! 🌸

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately identifies the main change: adding pre-accrue logic to the junior deposit path to fix issue #146.
Description check ✅ Passed The PR description comprehensively covers the vulnerability, root cause, solution approach, and verification steps, though it deviates from the standard template sections.
Linked Issues check ✅ Passed The PR fully addresses all objectives from issue #146: prevents fee siphoning via pre-accrue on junior path, ensures consistency across all three pricing paths, eliminates regression risk through helper extraction, and validates impact with PoC and regression tests.
Out of Scope Changes check ✅ Passed All changes are scoped to issue #146: refactoring fee-accrual logic into a shared helper and applying it consistently across deposit/withdraw/junior paths, plus a regression test.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

Copy link
Copy Markdown
Owner

Integrated into main via the consolidated PR above (cherry-picked with your attribution; verified build-sbf + 327 tests). Closes #146. Thanks — clean refactor into pre_accrue_mode1.

@dcccrypto dcccrypto closed this Jun 18, 2026
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