Reward-Index Initialization Failure / Zero-Sentinel Accrual Windfall

  • Name: reward-index initialization failure / zero-sentinel accrual windfall
  • Category: exploit pattern / incentive-accounting failure / flywheel initialization bug / reward-index sentinel mismatch
  • Summary: This exploit family appears in reward flywheels that track a global market index plus a per-user index, while also using sentinel values like 0 for user index not initialized yet and a fixed constant like 1e36 for market index initialized. The protocol breaks when an upgrade or activation path sets the market-wide index to its initial nonzero value, but the first-touch user logic only normalizes a 0 user index when marketIndex > initialIndex instead of >= initialIndex, or otherwise mismatches the sentinel rules. At that moment the user-side delta is computed as initialIndex - 0, so one ordinary supplier or borrower balance is multiplied by a gigantic synthetic index jump. The result is a reward windfall that did not come from elapsed emissions, tiny liquidity, or price manipulation. It came from the protocol disagreeing with itself about what uninitialized means. The recurring reviewer mistake is collapsing three different facts into one vague rewards are initialized now story: the market side published a live baseline, this specific user was normalized to that baseline, and this exact path was already allowed to monetize the resulting delta into real token outflow.
  • What it is:
    • The protocol distributes incentives through a cumulative market index such as compSupplyState.index, compBorrowState.index, or an analogous reward scaler.
    • Each user also stores a personal checkpoint index intended to measure only the delta since their last interaction.
    • The system uses sentinel conventions like 0 == user not initialized and initialIndex == market initialized but no meaningful accrual yet.
    • An upgrade, market activation, or migration initializes the market index to a nonzero constant.
    • The user-initialization branch mishandles the equality boundary or migration state, so the user stays at 0 while the market is already at initialIndex.
    • The next claim, mint, borrow, or other flywheel-touch path computes a huge fake delta from 0 to initialIndex and credits reward that was never earned.
  • Typical preconditions:
    • Rewards are accounted for through global and per-user index checkpoints rather than a simple per-block transfer.
    • The protocol uses sentinel values instead of an explicit boolean initialized flag.
    • A governance upgrade, reward-speed split, market activation, or migration changes how indices are initialized.
    • The first-touch logic assumes market index > initialIndex is the only dangerous case, missing the exact-boundary case where market index == initialIndex.
    • The reward-transfer path can still pay out real tokens once bogus accrual has been recorded.
  • Common vulnerable surfaces:
    • lending flywheels with supplier and borrower reward indices
    • reward systems that migrate from one speed map to separate per-side speed maps
    • upgrade routines that initialize market state globally but leave user state to lazy first-touch logic
    • codebases that encode uninitialized through magic numbers instead of separate state flags
    • governance proposals whose simulations do not include pre-existing users in newly initialized reward markets
  • Failure sequence:
    1. Governance or an admin upgrade initializes one or more market reward indices to a fixed starting constant such as 1e36.
    2. Some users still have personal reward indices at 0, either because they predated the migration or because lazy initialization is deferred until first touch.
    3. A claim, mint, borrow, or other distribution path reads userIndex == 0 and marketIndex == initialIndex.
    4. Because the initialization guard uses the wrong boundary or sentinel rule, the code fails to rewrite the user’s 0 checkpoint to initialIndex before computing the delta.
    5. The protocol treats initialIndex - 0 as legitimate accrued reward and multiplies it by the user’s full balance or borrow amount.
    6. The user can now claim or hold a huge bogus reward balance, bounded economically only by how many real reward tokens the distributor actually controls.
  • Variants:
    • Equality-boundary bug — the code checks > where the initialization boundary really needs >=.
    • Lazy-user / eager-market mismatch — market state is upgraded eagerly, user state only lazily.
    • Side-split migration bug — a reward system moves from one emission map to separate supplier/borrower maps and one side keeps a bad sentinel assumption.
    • Dormant-side activation — a market side with zero prior speed or no prior index accrual becomes initialized without consistent user-state normalization.
    • Bounded-outflow, unbounded-ledger fiction — on-paper accrual can reach absurd sizes even when the live distributor only holds a much smaller reward balance.
  • Detection heuristics:
    • Audit every reward-index system for exact sentinel semantics: what values mean never initialized, initialized at baseline, and already accrued?
    • Search for first-touch clauses like if (userIndex == 0 && marketIndex > initialIndex) and review the equality case explicitly.
    • Simulate upgrades against forked live state with users who already have balances in markets whose reward configuration is changing.
    • Compare theoretical reward accrual after migration against physically possible emissions and actual distributor inventory.
    • Treat magic-number sentinels as a hazard: if 0, initialIndex, and > initialIndex all have different meanings, test every boundary.
  • Defenses:
    • Prefer an explicit initialized flag over overloaded numeric sentinels.
    • When using sentinel values, prove the equality boundary with tests: marketIndex == initialIndex must behave intentionally.
    • Initialize user checkpoints consistently during migration if the protocol cannot guarantee safe lazy initialization later.
    • Run fork simulations for governance upgrades with real account state, not only clean-room unit tests.
    • Add invariant checks that claimed rewards cannot exceed plausible emissions or distributor inventory by impossible margins after a migration.
  • High-signal incidents:
    • Compound V2 Proposal 62 COMP distribution index-initialization bug (2021-09-29) — the canonical case: Proposal 62 initialized market reward indices to compInitialIndex = 1e36, but distributeSupplierComp() / distributeBorrowerComp() only normalized zero user indices when the market index was > compInitialIndex, so some first-touch accounts were credited from 0 to 1e36 and one address later showed about 70.0M COMP of bogus over-accrual on paper.

Three initialization facts reviewers should not collapse

1. The market side published a live baseline

  • Market initialized is only the first promotion: one supplier-side or borrower-side index now exists and the market-side flywheel says reward accounting may begin from that baseline.
  • That can be true even while many pre-upgrade users still carry stale 0 checkpoints that do not yet mean the same thing.

2. This specific user was normalized to that same baseline

  • A dormant supplier or borrower is not economically aligned just because the market index now sits at initialIndex.
  • The protocol still needs one explicit transition that rewrites the user’s stale sentinel state so userIndex and marketIndex agree on what initialized but no accrual yet means.

3. This path may already monetize the resulting delta

  • Even after the market baseline exists and the user’s checkpoint is understood, there is one more security decision: can this exact claim, mint, borrow, or helper path already convert the computed delta into live token outflow?
  • Compound Proposal 62 was expensive because those three answers drifted apart: market-side initialization happened, user normalization lagged, and payout logic still treated the disagreement as immediately payable reward.

Failure-prevention checklist

  • Replace overloaded numeric sentinel conventions with explicit initialization state wherever possible. Market-side initialization and user-side checkpoint initialization should not have to be inferred from 0, initialIndex, and > initialIndex branches alone.
  • When a nonzero baseline index such as 1e36 is introduced, test every equality boundary explicitly across supplier and borrower paths. marketIndex == initialIndex must be a first-class reviewed state, not an accidental branch gap.
  • Normalize live user checkpoints during migrations when practical instead of assuming later lazy initialization will be harmless.
  • If lazy initialization must remain, prove with tests and simulations that every first-touch path rewrites stale 0 checkpoints before any accrual delta is computed.
  • Keep reward activation, market initialization, user checkpointing, and payout authorization tightly coupled. A market should not become claimable until those four pieces agree on the same state machine.
  • Run governance-upgrade fork tests against real account state, including dormant suppliers, dormant borrowers, newly activated reward sides, and users who have not interacted since before the migration.
  • Add plausibility guards at the payout rail: first-touch accrual should not be able to claim more than plausible emissions, configured distributor inventory, or a sane per-market bound.
  • Force migration reviews to answer three separate questions explicitly: has the market side published a live baseline, has this user been normalized to that baseline, and may this path already monetize the resulting delta?
  • Maintain per-market and per-side migration runbooks so supplier and borrower indices cannot diverge semantically during upgrades.
  • Monitor for impossible post-upgrade events such as one user accruing more rewards in a single touch than the market could have emitted since initialization.
  • Prefer the ability to pause reward claiming or isolate a newly migrated flywheel quickly if live accounting disagrees with expected emissions.

Audit prompts

  • What exactly do 0, initialIndex, and any larger value mean for market state and for user state on each reward side?
  • Which code paths still use lazy first-touch initialization, and do they all normalize stale user checkpoints before computing reward deltas?
  • Are there any > versus >= boundaries, zero checks, or migration shortcuts whose equality semantics were never tested against real pre-upgrade users?
  • Can a previously dormant supplier or borrower in a newly activated reward market realize a synthetic 0 -> initialIndex accrual jump on first touch?
  • Do supplier and borrower flywheels share the same sentinel assumptions, or could one side be migrated correctly while the other preserves an unsafe convention?
  • What exactly constrains payout if accounting suddenly credits an absurd reward balance: distributor inventory, claim caps, manual pause authority, or nothing at all?
  • Where, exactly, does the protocol decide that market baseline exists has become this user is aligned to it rather than merely this user has now touched the code path?
  • Were governance simulations run against forked live balances and outstanding user checkpoints, or only against clean-room unit-test accounts?
  • How does the protocol distinguish initialized but zero accrual so far from never initialized without relying on ambiguous numeric coincidence?
  • If a migration partially executes or is rolled back, can users get stranded in mixed market/user initialization states that later first-touch logic misinterprets?
  • What invariant proves that cumulative claimed rewards after migration cannot exceed plausible emissions plus explicitly funded inventory?

Lessons that should already be standard

  • Numeric sentinels are a state machine whether teams admit it or not. If the protocol encodes lifecycle state in magic numbers, every boundary needs the same rigor as access control.
  • Equality boundaries are production logic, not edge-case trivia. > versus >= can be the whole exploit.
  • Lazy initialization after migration is only safe when it is proved safe against old accounts, not when it feels cheaper than normalizing state eagerly.
  • Reward flywheels need sanity bounds at the transfer rail, not just faith in upstream accounting. Impossible accrual should fail closed.
  • Governance upgrades are security events. If a proposal changes initialization semantics, fork-testing live user state is mandatory, not a nice-to-have.
  • Market initialization and user initialization should be modeled as one coordinated truth. When the protocol has two different answers to is this initialized?, attackers collect the difference.
  • Market initialized, user normalized, and this payout is valid now are three different promotions. Treating them as one fuzzy meaning of initialized is how a bookkeeping boundary becomes a mint rail.

What sound design looks like

  • A reward flywheel should have one explicit answer to is this market initialized? and one explicit answer to is this user checkpoint initialized for this market side? Numeric index values should not have to carry both meanings by convention alone.
  • If a migration chooses a nonzero baseline like initialIndex, then every code path that touches user state should treat marketIndex == initialIndex as an intentionally handled state, not an ambiguous halfway point between zero and already accrued.
  • Governance or admin rollouts should normalize live user checkpoints eagerly when practical, or else ship fork-tested lazy-initialization logic that proves pre-upgrade suppliers and borrowers cannot ever realize a fake 0 -> initialIndex delta.
  • Reward rails should include plausibility checks at the transfer boundary: if one first-touch claim suddenly implies more rewards than the market could have emitted or more than the distributor should safely disburse, the system should fail closed instead of trusting the accounting entry.
  • Positive-design companion: reward-flywheel-initialization-and-checkpoint-coherence — use this when the sharper question is how market-side initialization, user-side checkpointing, and payout authorization should behave like one security object rather than three loosely coordinated sentinels.

Where this pattern stops

  • Use reward-debt-desynchronization-denominator-collapse-reward-amplification when the core bug is not first-touch sentinel handling but stale reward debt plus a tiny denominator that makes later per-share accrual explode.
  • Use supplier-index inflation when the protocol is distributing real reserve or interest growth to one dust supplier and the exploit works by monopolizing the accumulator, not by inventing a bogus initialization delta out of thin air.
  • Use fresh-market initialization failure when the sharper story is launch sequencing, seed-liquidity absence, or collateral admission before a market is safely live. This family is narrower: the market may be perfectly live, but the reward flywheel still disagrees with itself about what initialized means.
  • Use state-publication authority failure when the exploit only becomes real because an attacker-timed checkpoint, sync, or harvest publishes pending state into canonical truth. Here the decisive error is earlier and quieter: the protocol misclassifies baseline checkpoint state before any meaningful accrual exists.
  • In short: this family begins when a reward system’s own initialization semantics manufacture impossible accrual at the exact first-touch boundary, and it stops once the more precise story is instead denominator collapse, real-index monopolization, broader launch-state failure, or attacker-timed publication of otherwise valid pending state.

Internal linkages

Control surface