fix(stake,v17): pre-accrue mode-1 fees on the junior deposit path (closes #146) - #147
fix(stake,v17): pre-accrue mode-1 fees on the junior deposit path (closes #146)#1470x-SquidSol wants to merge 1 commit into
Conversation
…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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughIntroduces a ChangesJunior JIT Fee-Snipe Fix
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
|
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. |
Closes #146.
What
process_deposit_juniorwas the one pricing path that did not crystallize pending mode-1 trading fees before pricing —process_deposit(senior/global) andprocess_withdrawalready did (the #136 fix). So a junior depositor could mint LP at the stale pre-fee price and, after a permissionlessAccrueFees, 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.process_deposit/process_withdrawchange is a behavior-preserving extraction (the helper body is byte-identical to the removed inline blocks).process_deposit_juniorgets the call placed before pricing — after the existing cap / token-program / user-ATA checks and before the user→vault transfer, matchingprocess_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 fromverify_token_program, which validates the token program). Mode-0 is a no-op; the helper is idempotent.Verification
cargo build --libandcargo build-sbf: clean.tests/poc_junior_jit_fee_snipe.rs(repo convention, mirrorstests/poc_jit_fee_snipe.rs):junior_jit_fee_snipe_is_profitable_with_current_pricingdocuments the +400,000;pre_accrue_before_junior_pricing_prevents_snipeguards the fix. Both verified out-of-band against the v17 production functions (current → 1,400,000; fixed → 999,999).Summary by CodeRabbit
Bug Fixes
Tests