Transfer-Time Reward Checkpoint Omission / Fee-Debt Inheritance Windfall

  • Name: transfer-time reward checkpoint omission / fee-debt inheritance windfall
  • Category: exploit pattern / transferable-receipt-token reward-accounting failure / per-share checkpoint omission / reward double-claim
  • Summary: This pattern appears when a transferable receipt token carries economic entitlement to fees or rewards, but the protocol tracks those rewards with per-account checkpoints such as rewardPerSharePaid, token0PerSharePaid, or feeDebt that are not synchronized during token transfers. The sender can move receipt tokens to a fresh helper address whose checkpoint still points to an older accumulator state, let that helper claim historical rewards it never economically earned, then move the same receipt tokens onward and repeat. The bug is not merely rewards can be claimed twice; it is historical fee accrual stopped moving with token ownership, so the same accumulator window became collectible by multiple holders of the same balance in sequence.
  • What it is:
    • A protocol issues transferable shares, LP receipts, vault shares, or other balance-bearing tokens.
    • Fee or reward entitlement is computed from a global accumulator such as rewardPerShare, tokenPerShareStored, or accRewardPerShare plus per-account paid checkpoints.
    • Deposits and withdrawals update those checkpoints, but plain token transfers do not.
    • A receiver therefore inherits the token balance without inheriting the economically correct already-accounted-for checkpoint state.
    • The receiver can claim rewards as if the newly received balance had been present since an earlier accumulator snapshot.
    • The attacker can shuttle the same receipt balance across multiple helper addresses, crystallizing the same historical accrual repeatedly while eventually returning the principal balance to the original account for withdrawal.
  • Typical preconditions:
    • The reward-bearing receipt token is transferable.
    • Reward accounting depends on per-account checkpoint state rather than on immutable position ids or non-transferable accounting objects.
    • transfer() or transferFrom() does not first crystallize sender and receiver rewards against the current accumulator and reset the receiver checkpoint.
    • Claimable rewards are stored separately from the receipt-token balance, so an address can keep harvested rewards even after sending the receipt tokens away.
    • The protocol holds enough real fee inventory or claimable assets that repeated historical accrual claims can be cashed out.
  • Common vulnerable surfaces:
    • yield-optimizing LP wrappers that mint ERC-20 receipt shares while tracking fees in side mappings
    • reward-bearing vault shares where deposit() and withdraw() settle reward checkpoints but transfer() inherits the raw balance only
    • staking wrappers that let users move claim-bearing shares without moving the associated rewardDebt
    • gas-optimized reward systems that rely on inherited ERC-20 transfer() / transferFrom() without overriding them to settle accounting first
    • systems that assume rewards follow balance socially while the code actually stores rewards per address
  • Exploit sequence:
    1. The attacker acquires or flash-funds a large receipt-token position in a pool with meaningful accrued fees or rewards.
    2. They deposit and receive transferable shares whose current holder-side checkpoints reflect the present accumulator state.
    3. They transfer those shares to a helper address whose checkpoint is stale or zero.
    4. The helper calls the claim path, which calculates rewards using the full transferred balance against an old checkpoint and credits historical rewards the helper never economically earned.
    5. The helper transfers the same shares to another helper address with another stale checkpoint.
    6. The claim path is repeated, multiplying one historical accrual window into several payouts.
    7. The attacker eventually returns the receipt tokens to the original address, withdraws the underlying principal, repays any flash liquidity, and keeps the duplicated rewards.
  • Variants:
    • Fresh-address replay — the attacker uses newly deployed helper contracts or EOAs whose checkpoints are zero or unset.
    • Friend-address inheritance — the attacker transfers to an address whose checkpoint is merely older than the sender’s, so the new holder claims an intermediate historical window.
    • Claim-then-forward loop — each helper claims once and forwards the same balance onward, leaving rewards behind in side mappings.
    • Inherited ERC-20 footgun — a protocol correctly customizes deposit/withdraw reward logic but leaves the parent token’s raw transfer() behavior live.
    • Reward-token drain without principal theft — the attacker does not need to steal the underlying liquidity position itself; draining the side-pocketed fee inventory is enough.

Three transfer-time entitlement promotions reviewers should not collapse

  1. A share balance moved

    • Receipt tokens, vault shares, or staking wrappers changed hands between two addresses.
    • That only answers the weakest question: token ownership moved.
  2. Historical reward entitlement was reassigned or crystallized correctly

    • The protocol had to decide whether past accumulator windows travel with the shares, stay with the old holder as already-crystallized pending rewards, or get split by some explicit transfer-time settlement rule.
    • This is the decisive accounting question preserved by this family: did the transfer also move or consume the already-accounted-for checkpoint state that makes historical rewards single-owner truth?
  3. The post-transfer state became payout authority

    • Claim paths, fee withdrawals, batch harvesters, or later redemption flows then treated the sender and receiver checkpoint state as money-good reward truth.
    • This is where the omission cashes out: a transfer that only moved balances quietly became a way to mint fresh apparent entitlement from stale per-address reward state.

Healthy review keeps those promotions separate instead of flattening the bug into transfer can double-claim rewards.

  • Detection heuristics:
    • For every transferable receipt token, ask whether economic entitlement to historical rewards moves correctly when the balance moves.
    • Fuzz the sequence deposit -> transfer to fresh address -> claim -> transfer again -> claim -> withdraw and assert that the total extracted value never exceeds what one honest holder could have earned.
    • Compare sum(claimable rewards across all addresses) before and after a pure transfer; transfers should not increase aggregate economic value.
    • Review inherited ERC-20 methods on yield-bearing share tokens. If reward logic only appears in deposit and withdraw paths, assume danger until transfer hooks are checked.
    • Treat zero or unset reward checkpoints for new addresses as a security boundary whenever balances can be transferred in before first claim.
    • Verify whether the receiver’s checkpoint is advanced to the current global accumulator when shares arrive.
  • Defenses:
    • Settle sender and receiver rewards against the current accumulator before any share balance transfer finalizes.
    • Reset or initialize the receiver’s paid checkpoint to the current accumulator for the transferred balance.
    • Keep transferable share balances and reward-entitlement accounting in one coherent state machine instead of letting rewards live in side mappings that survive balance movement unchanged.
    • Add invariants that token transfers cannot increase the combined withdrawable assets of the sender and receiver.
    • Consider non-transferable receipts or position-NFT accounting when historical reward attribution is inseparable from identity or deposit epoch.
    • Override inherited token transfer logic explicitly whenever a share token also carries fee or reward rights.

What sound design looks like

  • A healthy reward-bearing share system keeps three states separate on purpose: balance moved, historical entitlement was reassigned or crystallized, and the resulting state is now claimable payout authority. The exploit family appears when those promotions collapse into one vague assumption that ERC-20 transfer semantics will probably do the right thing for rewards too.
  • Historical fee accrual should either travel with the transferred shares or be crystallized and canceled before the transfer completes. There is no safe third state where the sender keeps past rewards and the receiver also inherits the right to claim them.
  • A pure share transfer should preserve aggregate economic value for the sender plus receiver. If moving balances can change combined claimable + withdrawable assets, the protocol has turned token mobility into a mint primitive.
  • The sharper design companion is reward entitlement identity and batch canonicalization: the protocol should be able to name one canonical reward-entitlement object and one consumed-state boundary before it decides whether that entitlement travels with the shares or is crystallized away at transfer time.
  • New addresses receiving old shares should start from the live accumulator state for those incoming shares, not from 0 or from some stale checkpoint that predates receipt.
  • Reward systems built on top of ERC-20 shares should treat transfer() as a first-class accounting path, not as a free utility inherited from a base token. The adjacent standard-surface companion is entrypoint equivalence under shared standards: deposit() and withdraw() cannot be the only verbs that preserve reward truth if transfer() and transferFrom() still move the same economic claim.
  • If the protocol instead chooses to crystallize pending rewards before every transfer, the payout side still needs one shared consume boundary so the old holder’s entitlement really dies once. The relevant payout-side companion is exit right consumed-state binding.

Failure-prevention checklist

  • Override transfer() and transferFrom() on every reward-bearing receipt token so sender and receiver accounting settles before balances move.
  • Initialize the receiver’s checkpoint for the incoming balance at the current global accumulator unless the protocol intentionally transfers already-crystallized pending rewards alongside the shares.
  • Add invariants that a pure transfer cannot increase aggregate claimable + withdrawable value across sender, receiver, and any helper addresses.
  • Fuzz deposit -> transfer -> claim -> transfer -> claim -> withdraw loops using fresh addresses, stale addresses, and flash-funded large balances.
  • Keep reward-entitlement state and share-balance state in one coherent machine; avoid side mappings that survive balance movement without synchronized checkpoint updates.
  • Consider non-transferable shares or position-NFT accounting when reward history is inseparable from deposit identity or epoch.
  • Review inherited token hooks, permit flows, and privileged transfer helpers, not just the visible user-facing transfer() path.

Audit prompts

  • When a share balance moves, what exact state changes carry or cancel the associated historical reward entitlement?
  • Can a newly funded address with zero or stale checkpoints claim rewards for accumulator windows that accrued before it received the shares?
  • Does a pure transfer ever increase total claimable rewards across all involved addresses even though no new fees entered the system?
  • Are there any alternate balance-movement paths — transferFrom, permit-based pulls, mint/burn wrappers, liquidation flows, rescue paths, bridging adapters — that bypass checkpoint settlement?
  • If pending rewards are stored separately from balances, when are they crystallized, and can the same balance then be forwarded to create new apparent entitlement elsewhere?
  • Does the protocol rely on inherited ERC-20 behavior for a token whose economics clearly require custom transfer accounting?
  • Can one large flash-funded position be walked across helper contracts to replay the same historical fee window multiple times?

Lessons that should already be standard

  • If rewards are accounted per address while balances move per token, then transfer() is part of the mint boundary whether the team intended it or not.

  • Transferability and historical entitlement cannot be designed independently. One of them must explicitly define the other.

  • Fresh-address zero-state is not harmless in reward systems; it is often the easiest replay surface.

  • Share tokens that carry fees or rewards are rarely safe to implement as plain ERC-20 plus a side mapping.

  • Aggregate-value invariants are the fastest sanity check: a pure transfer should never create more total claimable value.

  • High-signal incidents:

    • reward double-claim drain (2021-08-04) — the clean preserved exemplar: collectFees() trusted per-address token0PerSharePaid / token1PerSharePaid, but PLP transfer() left those checkpoints untouched, so one flash-funded share position could be walked across helper contracts and each helper could claim the same historical fee window.

Internal linkages

Where this pattern stops

  • Use reward-debt-desynchronization-denominator-collapse-reward-amplification when the decisive exploit primitive is a near-zero denominator that makes the global accumulator explode, and stale user debt merely fails to cancel that jump.
  • Use this note when the decisive exploit primitive is that receipt-token transfers do not correctly move or settle the historical reward checkpoint tied to that balance.
  • Use reward-index-initialization-failure when the protocol credits bogus rewards during first initialization of user indices, even without any balance transfers.
  • Use duplicate-claim-idempotency-failure when the same proof, lock, epoch, or claim right can simply be consumed twice without transfer-induced checkpoint confusion.

Control surface