Reward-Debt Desynchronization / Denominator-Collapse Reward Amplification

  • Name: reward-debt desynchronization / denominator-collapse reward amplification
  • Category: exploit pattern / incentive-accounting failure / reward-distributor exploit / stale-accumulator bug
  • Summary: This pattern hits staking, farming, and liquidity-mining systems that distribute emissions through a global rewardPerShare-style accumulator plus per-user reward debt, writeoff, or checkpoint state. The dangerous combination is: the global accumulator updates from one view of state, the user’s debt settles against a different or stale view, and the denominator used for the global update can be driven close to zero. Once the pool’s reward accumulator is divided across dust-sized stake, even a tiny desynchronization between global reward truth and user reward cancellation can become an enormous mint primitive. The exploit is not simply tiny pool gets high APR; it is the protocol lets a near-zero denominator amplify a stale-accounting mismatch into real token issuance.
  • What it is:
    • A reward pool tracks a global accumulator such as accRewardsPerToken, rewardPerTokenStored, or another per-share emission index.
    • Each user also carries reward debt, writeoff, or snapshot state intended to cancel rewards already accounted for.
    • The contract updates the global accumulator from one state version, but updates the user’s debt from stale memory, an old checkpoint, or a mismatched denominator context.
    • The attacker makes total stake tiny so the next global accumulator jump becomes enormous.
    • Because the user-side offset no longer matches that jump, the next claim or withdrawal mints rewards far beyond scheduled emissions.
  • Typical preconditions:
    • The target pool is fresh, thin, or manipulable enough that one actor can reduce effective total stake toward zero.
    • Rewards are minted or made claimable from a global per-share accumulator rather than distributed from a hard-capped fixed balance in the same transaction.
    • The code path updates global pool state and user debt in different places or from different cached objects.
    • No hard bound checks whether claimable rewards exceed plausible emissions for the elapsed time.
    • The attacker can re-enter with a large deposit after first pinning the denominator to dust.
  • Common vulnerable surfaces:
    • liquidity-mining or shield-mining contracts with accRewardsPerToken + rewardDebt / rewardWriteoff
    • new staking pools added before healthy stake exists
    • gas-optimized contracts that cache pool structs in memory, then later mix those stale values with freshly written storage
    • reward distributors where updatePool() and settleUser() are separate steps that can accidentally see different pool states
    • systems that assume tiny denominator is just a UX or APR oddity rather than a security boundary
  • Exploit sequence:
    1. The attacker finds or creates a pool with extremely small total stake, often by targeting a newly added pool or withdrawing until only dust remains.
    2. They trigger a global reward update while that denominator is tiny, causing the pool’s per-share accumulator to jump sharply.
    3. Because the contract still uses stale or mismatched accumulator state for the user’s reward debt, the user-side cancellation lags behind the pool-side reality.
    4. The attacker restores or keeps a large nominal stake associated with the under-cancelled reward position.
    5. On claim, withdraw, or later settlement, the contract treats the gap between the huge live accumulator and the stale user writeoff as legitimate claimable rewards.
    6. The protocol mints or transfers rewards far beyond intended emissions and the attacker sells or otherwise monetizes the newly created tokens.
  • Variants:
    • Stale memory pool cache — a Pool memory struct is loaded before updatePool() mutates storage, but user debt later uses the stale memory copy.
    • Checkpoint ordering bug — user reward debt is written before the final pool accumulator update for the same action.
    • Launch-state amplifier — a newly listed pool with almost no honest stake makes a modest accounting mismatch become catastrophic.
    • Dust-denominator capture — the attacker withdraws to 1 wei or another microscopic balance so the next accumulator update explodes.
    • Soft-mitigated but still live — operators add keeper updates or UI-side extra calls, but the true stale-state mismatch remains exploitable onchain.

Three reward-authority promotions reviewers should not collapse

  1. The pool’s accumulator moved under a dust denominator

    • Some elapsed-emissions interval, pool-weight change, or reward-update call advanced accRewardsPerToken / rewardPerShare while effective stake was tiny.
    • This only answers the weakest question: pool-side reward state changed.
  2. That pool-side update was treated as the same state the user’s debt had already canceled against

    • The protocol had to decide whether the user’s rewardDebt, rewardWriteoff, or checkpoint was being advanced from that exact post-update accumulator version.
    • This is the decisive accounting question preserved by this family: did pool-side reward truth and user-side cancellation truth still describe the same moment once the denominator spike had happened?
  3. The resulting mismatch became live payout or mint authority

    • Claim, withdraw, or later settlement paths then treated the uncancelled gap between the huge live accumulator and the stale user checkpoint as money-good reward entitlement.
    • This is where the bug cashes out: a dust-denominator update that should only have changed internal accounting becomes direct token-issuance or reward-payout authority.

Healthy review keeps those promotions separate instead of flattening the bug into tiny pool paid too much APR.

  • Detection heuristics:
    • Map every reward path and verify that global accumulator used for claim and user debt written this action come from the exact same state version.
    • Fuzz deposit / withdraw / deposit / claim sequences where total stake temporarily falls to 1 wei, 1e-8, or other dust values.
    • Compare total claimable rewards against theoretical emissions under adversarial low-liquidity states; if one user can claim more than elapsed emissions should allow, stop.
    • Audit gas optimizations that cache pool structs in memory before calling functions that mutate storage.
    • Treat pool launch state as adversarial. A new pool with nearly zero stake is a security boundary, not only an APR oddity.
    • Ask whether a user can first minimize the denominator, then re-expand their position before the claim path settles the true reward debt.
  • Defenses:
    • Write user reward debt only from the same post-update accumulator that the pool will later use for claims.
    • Fail closed or cap accrual when total stake is below a meaningful threshold instead of letting reward / dust create enormous per-share jumps.
    • Seed new reward pools with non-burnable stake or delay activation until healthy participation exists.
    • Add invariant tests that sum(claimable rewards) cannot exceed plausible emissions plus any explicitly prefunded remainder.
    • Avoid mixing cached memory pool structs with later storage writes unless the code deliberately refreshes the cache before any user accounting.
    • Consider pausing or rate-limiting claims if observed accumulator growth exceeds physical emission bounds.

What sound design looks like

  • A healthy reward system keeps three states separate on purpose: the pool accumulator moved, the user-side debt or writeoff was reconciled against that exact post-update state, and the remaining delta is now payout authority. This family appears when those promotions collapse into one vague assumption that a huge pool-side update and a stale user checkpoint are close enough to mean the same thing.
  • Reward accounting has one canonical post-update state per action: the same accumulator value, denominator, and elapsed-emission view are used both to advance global pool state and to write each user’s debt or checkpoint.
  • Pools cannot enter a security-sensitive reward / dust regime without explicit handling. If effective stake falls too low, accrual pauses, caps, or routes through a protected bootstrap mode rather than letting tiny denominators amplify tiny accounting slips into mint authority. Fresh-market seeding and activation discipline and bootstrap-state irreversibility and supply-floor design are the broader positive-design companions for keeping launch-state and low-supply regimes from becoming attacker-owned accounting surfaces.
  • Emission logic is bounded by physical reality. A single claim path cannot mint more than scheduled emissions plus clearly prefunded remainder, even if accounting state becomes adversarial.
  • Reward entitlements and reward accumulators should still collapse into one consumption story: the same state transition that recognizes pool-side reward growth should also leave user-side debt, writeoff, or claimed state in the canonical post-update frame before any later claim can treat the balance as real. Reward entitlement identity and batch canonicalization is the adjacent design note for the broader one canonical reward right, one consume boundary discipline.
  • Gas optimizations that cache pool structs in memory are subordinate to accounting truth: stale cache reuse is never allowed to decide user debt after storage mutation.

Failure-prevention checklist

  • Write user reward debt from the exact post-update accumulator value that storage will preserve for later claims; never settle user debt from a pre-update or stale cached pool snapshot.
  • Enforce denominator floors, bootstrap seeding, or paused accrual when total stake is too small for rewardPerShare jumps to stay economically safe.
  • Add invariants that aggregate pending rewards cannot exceed elapsed emissions plus any explicitly prefunded budget.
  • Fuzz adversarial sequences where stake collapses to dust, then expands again before claim or withdraw settlement.
  • Review every Pool memory / storage mix for stale-state reuse after updatePool(), massUpdatePools(), or any helper that mutates the accumulator.
  • Treat newly launched or newly reactivated pools as hostile environments until they have healthy seed liquidity and tested reward-debt initialization.
  • Consider rate limits, mint ceilings, or pause conditions when observed per-block accumulator growth materially exceeds design expectations.

Audit prompts

  • Does every claim, deposit, withdraw, and harvest path write user debt from the same accumulator version that the pool will later use to compute rewards?
  • What happens if total stake temporarily becomes 1 wei, one LP token, or another near-zero denominator right before updatePool()?
  • Can an attacker minimize the denominator, trigger a huge accumulator jump, then restore a large position before user debt is brought current?
  • Are any cached pool structs read before a storage mutation and then reused afterward for reward-debt settlement?
  • Is there a hard cap or invariant that stops one user from claiming more than plausible elapsed emissions?
  • How are fresh pools seeded, and what prevents launch-state dust from becoming a mint primitive before honest liquidity arrives?
  • If keepers or UI workflows are part of the mitigation story, does the onchain contract remain safe when those offchain habits disappear?

Lessons that should already be standard

  • Near-zero denominators are security boundaries, not just weird APR moments.
  • Global accumulator math and user reward-debt cancellation are one invariant. If they read different state versions, the protocol is minting against a contradiction.
  • Gas-saving memory caches deserve the same suspicion as arithmetic code in emission systems; stale reads can become unlimited issuance bugs.
  • Reward systems should be judged against emission reality, not against whether the accumulator equations look standard in isolation.
  • Launching a reward pool before safe seed state exists is often equivalent to launching an exploit amplifier.

Where this pattern stops

How to use this note with the exploit corpus

  • Start here when the question is did the protocol let pool-side reward truth and user-side reward-cancellation truth settle from different state versions, especially after the denominator was driven toward dust?

  • Pair with reward entitlement identity and batch canonicalization when the next question is how the protocol should have kept reward rights, claim-source provenance, and consumed-state updates canonical across single and batch claim paths.

  • Pair with fresh-market seeding and activation discipline and bootstrap-state irreversibility and supply-floor design when the sharper lifecycle question is why the pool was allowed to become or remain economically dust-sized at all.

  • Pair with supplier-index-inflation when the analytical value comes from comparing reward-accumulator monopolization against lending-index monopolization under the same low-denominator conditions.

  • Pair with reward-recognition, presentation, and collateral-admission separation when the reward-like value later escaped into wrapper pricing, rebasing balances, or borrowable collateral rather than staying inside a pure farming payout rail.

  • High-signal incidents:

    • [[../../../incidents/accounting-issuance-claims/cover-protocol-blacksmith-stale-pool-cache-rewardwriteoff-desynchronization-infinite-cover-mint-2020-12-28|Cover Protocol Blacksmith stale-pool-cache rewardWriteoff desynchronization / infinite COVER mint (2020-12-28)]] — the clearest preserved case: a fresh mining pool was driven to 1 wei, updatePool() raised storage accRewardsPerToken against that dust denominator, and deposit() still computed the attacker’s rewardWriteoff from a stale cached accumulator, leaving the next claim free to mint tens of quintillions of COVER.

Internal linkages

Control surface