Skip to content

fix(amm): harden LP reward accounting against transfer-based claims - #3

Merged
OxToF merged 3 commits into
mainfrom
fix/lp-reward-accounting
Jul 21, 2026
Merged

fix(amm): harden LP reward accounting against transfer-based claims#3
OxToF merged 3 commits into
mainfrom
fix/lp-reward-accounting

Conversation

@OxToF

@OxToF OxToF commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Summary

Two AMM LP-reward defects sharing one root cause: rewards were computed on a freely-transferable SPL LP balance, never on a program-recorded deposit. The same lesson already applied to the founder/team tranches ("restrictions must live in state or escrow, never in a token balance") had never been applied to LP.

Finding B — claim_lp_rewards: near-unbounded oSOLA mint — Critical (fixed + deployed)

LpUserInfo is init_if_needed → a fresh wallet reads reward_debt = 0 → claims the full osola_reward_per_lp accumulator since pool creation. An attacker deposits, transfers LP to a fresh wallet, claims, withdraws, repeats. Confirmed on devnet (PoC minted ~900 oSOLA from a zero-deposit wallet; unbounded without the cap).

Finding A — checkpoint_lp: epoch-emission over-mint — latent

Back-dated weight from epoch_start + per-wallet checkpoint PDA let the same LP be walked through N wallets against a denominator counted once, with no cumulative vs osola_allocated. Not exploitable while osola_emission_initial = 0, but arms at mainnet emission config. Fixed by the same change.

Fix

  • Reward basis = min(lp_amount, wallet_lp), where lp_amount is a new LpUserInfo field maintained by add/remove.
  • last_change_ts restarts the epoch-weight window on any position change (no back-dating to epoch_start).
  • claim_lp_emissions caps the per-epoch pot via a cumulative osola_claimed <= osola_allocated.
  • No account realloc — new fields carved from existing spare bytes (LEN unchanged, existing accounts deserialize).
  • Frontend checkpoint_lp updated to pass the lp_user_info account.

Verification

  • cargo check (devnet + --no-default-features), clippy -D warnings, cargo fmt clean.
  • Deployed to devnet under pause; replaying the exact exploit post-fix reverts with NothingToClaim (6007).

Migration note

Existing LP positions read lp_amount = 0 and earn nothing until a remove + add re-registers the deposit. Do NOT seed lp_amount from the wallet balance — that reopens the hole.

Full audit trail (PoC, tx signatures, attack matrix) in SECURITY_WATCH.md (2026-07-21 entry).

🤖 Generated with Claude Code

OxToF and others added 3 commits July 21, 2026 23:45
Fresh wallet + transferred LP claimed the full osola_reward_per_lp accumulator
(reward_debt init 0). Confirmed on devnet: 900 oSOLA minted from zero deposit
(tx 4RGNQYvmpV37abcrvKUdroQqCgFHVkkkZGbTTH3nuHifwejBPSY32KHiJb6QYVrxYwCj5AqjaRSMp5752VdnBQBe).

Reward basis is now min(lp_amount, wallet_lp), lp_amount maintained by add/remove;
epoch weight accrues only over time actually deposited (last_change_ts); per-epoch
pot capped by osola_claimed. No account realloc (fields carved from spare bytes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… fixed+deployed, A latent)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
soladrome-lzn5 Ready Ready Preview, Comment Jul 21, 2026 10:24pm

@OxToF
OxToF merged commit f2eb8d2 into main Jul 21, 2026
5 checks passed
@OxToF
OxToF deleted the fix/lp-reward-accounting branch July 21, 2026 22:33
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.

1 participant