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, orfeeDebtthat 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 merelyrewards can be claimed twice; it ishistorical 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, oraccRewardPerShareplus 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-forcheckpoint 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()ortransferFrom()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()andwithdraw()settle reward checkpoints buttransfer()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 balancesocially while the code actually stores rewards per address
- Exploit sequence:
- The attacker acquires or flash-funds a large receipt-token position in a pool with meaningful accrued fees or rewards.
- They deposit and receive transferable shares whose current holder-side checkpoints reflect the present accumulator state.
- They transfer those shares to a helper address whose checkpoint is stale or zero.
- 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.
- The helper transfers the same shares to another helper address with another stale checkpoint.
- The claim path is repeated, multiplying one historical accrual window into several payouts.
- 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
-
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.
-
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?
-
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 rewardsmoves correctly when the balance moves. - Fuzz the sequence
deposit -> transfer to fresh address -> claim -> transfer again -> claim -> withdrawand 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
zeroorunsetreward 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.
- For every transferable receipt token, ask whether
- 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, andthe resulting state is now claimable payout authority. The exploit family appears when those promotions collapse into one vague assumption thatERC-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
0or 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()andwithdraw()cannot be the only verbs that preserve reward truth iftransfer()andtransferFrom()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()andtransferFrom()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 + withdrawablevalue across sender, receiver, and any helper addresses. - Fuzz
deposit -> transfer -> claim -> transfer -> claim -> withdrawloops 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-addresstoken0PerSharePaid/token1PerSharePaid, but PLPtransfer()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.
- reward double-claim drain (2021-08-04) — the clean preserved exemplar:
Internal linkages
- Best current exemplar: popsicle-finance-sorbetto-fragola-plp-transfer-fee-checkpoint-omission-reward-double-claim-drain-2021-08-04.
- Positive-design companion: reward-entitlement-identity-and-batch-canonicalization — useful when the sharper question is what exact reward right and consumed-state object should have stayed canonical before transferred shares were allowed to inherit or crystallize historical fees.
- Positive-design companion: entrypoint-equivalence-under-shared-standards — useful when the sharper question is why inherited ERC-20 verbs had to preserve the same reward-accounting truth as deposit and withdrawal entrypoints.
- Positive-design companion: exit-right-consumed-state-binding — useful when the protocol chooses to crystallize pending rewards before transfer and still needs every sibling claim / payout path to consume that entitlement exactly once.
- Adjacent accumulator family: reward-debt-desynchronization-denominator-collapse-reward-amplification — both families break the tie between the global accumulator and user-side cancellation state, but here the amplifier is transfer-time checkpoint omission rather than dust-denominator explosion.
- Adjacent initialization family: reward-index-initialization-failure — use that note when the exploit comes from first-touch
index == 0semantics rather than from moving balances between accounts with stale checkpoints. - Adjacent claim family: duplicate-claim-idempotency-failure — that family is about one explicit entitlement being reused; this family is about a balance transfer fabricating new apparent entitlement from stale reward checkpoints.
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
-
The leverage sits in transferable share-token behavior, per-account reward checkpoint storage, claim-side accumulator math, and whether pure balance movement can change aggregate withdrawable value.
-
The practical lesson is blunt: if historical rewards are accounted per address while balances are transferable per token, then
transfer()itself is part of the reward mint boundary. -
Sources:
-
Last reviewed: 2026-07-31 UTC