Cover Protocol Blacksmith Stale-Pool-Cache rewardWriteoff Desynchronization / Infinite COVER Mint (2020-12-28)

  • Name: Cover Protocol Blacksmith stale-pool-cache rewardWriteoff desynchronization / infinite COVER mint
  • Date: 2020-12-28
  • Protocol: Cover Protocol
  • Chain: Ethereum
  • Category: reward-accounting desynchronization / liquidity-mining amplifier / stale accumulator bug / infinite mint
  • Loss: about 6.2M
  • Summary: On 2020-12-28, Cover Protocol’s shield-mining contract Blacksmith (0xe0b94a7bb45dd905c79bb1992c9879f40f1caed5) let attackers mint enormous amounts of COVER because deposit() cached the pool struct in memory, then called updatePool(_pid) to raise the pool’s storage accRewardsPerToken, but still computed the miner’s rewardWriteoff from the stale cached accumulator. In a newly added Balancer pool (0x59686e01aa841f622a43688153062c2f24f8fded), attackers first withdrew until only 1 wei of LP balance remained, which made the next accumulator update explode. The stale writeoff no longer offset that jump, so the next reward claim minted wildly more COVER than the emission schedule could justify. The best-known exploit path was Grap Finance’s sequence, which minted 40,796,131,214,802,500,000.212114436030863813 COVER in one claim after restoring a large deposit on top of a 1 wei pool.
  • Trigger / entry point:
    • Cover’s core insurance product was not the entry point; the exploitable surface was the separate shield-mining / farming contract Blacksmith.
    • The team had added a new Balancer LP pool only hours earlier, leaving a fresh reward pool with almost no preexisting stakers.
    • deposit(_pid, _amount) loaded Pool memory pool = pools[_pid], then called updatePool(_pid), but continued using the stale in-memory pool.accRewardsPerToken when updating the user’s rewardWriteoff.
    • The exploit became extreme only after the attacker or surrounding withdrawals reduced the pool’s totalLpTokens to nearly zero, because the next accRewardsPerToken update divided scheduled rewards by an almost nonexistent denominator.
    • Once the stale writeoff under-accounted for that jump, claimRewards() or a later reward-settling path would mint the uncancelled difference as real COVER.
  • Root cause:
    • Cover used a standard farming design with a global per-share accumulator (accRewardsPerToken) and per-user reward debt / writeoff tracking, but deposit() mixed stale memory state with freshly updated storage state.
    • The contract updated pool storage correctly, yet user accounting still referenced the old cached pool accumulator, so rewardWriteoff no longer matched the reward basis the pool itself now stored.
    • A near-empty or brand-new pool turned that desynchronization from a mild over-reward into an effectively unbounded mint amplifier, because rewards / totalLpTokens became enormous when only 1 wei remained staked.
    • Operational mitigation focused on calling updatePool more often and updating before deposits, but it still underestimated the 1 wei denominator amplifier and therefore did not remove the real exploit path.
    • More broadly, the protocol treated fresh pool + low stake as an operations concern rather than part of the security boundary around reward-mint authority.
  • Exploit path:
    1. Cover added a new Balancer LP token pool to Blacksmith via multisig transaction 0xe5173fffaed3342b53d41319dc538e7923e287e962df2d27f5e425c633db45d4.
    2. The first exploiter deposited into the new pool in tx 0xd721b0ef2886f14b75548b70d2d1fd82bea085ca24f5de29b833a64cfd8f7a50, then withdrew almost all LP in tx 0xadf27f5dd052482d46fdf69a5208a27cc7352522c7c19bbde5aee18f6ea4373b, already extracting about 703.64 COVER because the pool was still tiny.
    3. Grap Finance later executed the clearer amplifier sequence: deposit 15,255.5528... BPT in tx 0x77490baee41a9b35a6e87d49453c7329c7517c10ce6ce26b4c142692a2877e65, then withdraw almost everything in tx 0x88ce99fc1cb695db82d83ce5fe587396744841d3a123687f95b18df6a3106818, leaving only 1 wei.
    4. Another user withdrew the rest of their balance in tx 0xa27fb73caddb1cf24aa7a5afe84eed13db2f0a889a6ee0f3d5e6226a76c0fd9c, so Grap alone controlled the pool’s remaining 1 wei of stake.
    5. Grap deposited the large BPT amount back in tx 0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215; during this call, updatePool() pushed storage accRewardsPerToken sky-high because the denominator was effectively 1 wei, but rewardWriteoff still used the stale pre-update cached value.
    6. Grap then claimed rewards in tx 0xca135d1c4268d6354a019b66946d4fbe4de6f7ddf0ff56389a5cc2ba695b035f, minting roughly 4.0796e19 COVER, sold as much as possible through 1inch, burned part of the position, and later returned 4,351 ETH; several other addresses also copied the exploit before Cover revoked Blacksmith’s minting rights.
  • What the attacker needed to be true:
    • The mining contract had to mint real COVER directly from reward-accounting state rather than distributing only pre-funded emissions.
    • A pool had to be new, thin, or manipulable enough that the attacker could drive totalLpTokens close to zero.
    • The contract had to update the global reward accumulator and the user’s reward debt / writeoff from different views of state.
    • The same pool needed to remain claimable after the attacker restored a large stake against the manipulated writeoff baseline.
    • No hard cap or sanity bound could stop rewards from greatly exceeding scheduled emissions.
  • Technical anatomy worth remembering:
    • This was not merely rewards too high in a tiny pool. The decisive bug was desynchronization between the storage accumulator used as global truth and the stale cached accumulator used to cancel user rewards.
    • Cover’s own postmortem is valuable because it explicitly names the forgotten amplifier: the denominator collapse from ordinary-sized deposits down to 1 wei of remaining stake.
    • The exploit family is structurally similar to empty-market lending attacks in one narrow sense: once a global scaler is divided by dust supply, whoever owns that dust position captures absurd accounting value. But here the target was a reward-emission accumulator, not lending collateral.
    • The bug also shows why new pool launch state belongs in security reviews. A harmless-looking accumulator mistake became catastrophic only because a just-added pool could be driven into the 1 wei regime before organic stake diluted it.
  • Representative transactions and addresses:
    • Blacksmith contract: 0xe0b94a7bb45dd905c79bb1992c9879f40f1caed5
    • Newly added Balancer pool: 0x59686e01aa841f622a43688153062c2f24f8fded
    • Pool-add tx: 0xe5173fffaed3342b53d41319dc538e7923e287e962df2d27f5e425c633db45d4
    • Original exploiter first deposit: 0xd721b0ef2886f14b75548b70d2d1fd82bea085ca24f5de29b833a64cfd8f7a50
    • Original exploiter first over-rewarding withdrawal: 0xadf27f5dd052482d46fdf69a5208a27cc7352522c7c19bbde5aee18f6ea4373b
    • Grap deposit: 0x77490baee41a9b35a6e87d49453c7329c7517c10ce6ce26b4c142692a2877e65
    • Grap near-full withdrawal to 1 wei: 0x88ce99fc1cb695db82d83ce5fe587396744841d3a123687f95b18df6a3106818
    • Another user’s withdrawal leaving Grap as sole 1 wei staker: 0xa27fb73caddb1cf24aa7a5afe84eed13db2f0a889a6ee0f3d5e6226a76c0fd9c
    • Grap redeposit: 0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215
    • Grap claim / huge mint: 0xca135d1c4268d6354a019b66946d4fbe4de6f7ddf0ff56389a5cc2ba695b035f
    • Mitigation tx removing minting rights from Blacksmith: 0xaaa09c1246cf2ff111464d2947d764967c5c28c7cc353d5d906a320d4a6588fc
  • Response / mitigation:
    • Cover publicly stated that the core insurance product was unaffected; the damage was isolated to the mining contract and token economics.
    • The team removed Blacksmith’s minting rights and pointed token mint authority at a dummy contract to stop further exploitation.
    • Cover later proposed compensating users through a snapshot-based new token distribution and by redistributing the returned 4,351 ETH.
    • The incident highlighted that mitigation-through-more-frequent updatePool() calls was not enough; the real fix was to eliminate stale-state reward accounting and treat dust-denominator states as adversarial.

Pattern tags

  • reward-debt-desynchronization-denominator-collapse-reward-amplification
  • Fresh-pool / dust-denominator capture / a newly added Balancer pool let one actor drive totalLpTokens down to 1 wei before honest stake could dilute the next accumulator jump
  • Stale-cache reward settlement / deposit() updated storage accRewardsPerToken through updatePool(_pid) but still wrote the user’s rewardWriteoff from a pre-update Pool memory pool snapshot
  • Reward-ledger mint authority collapse / impossible farming-accounting deltas could mint fresh COVER directly rather than merely over-allocate a fixed prefunded reward pot

Three reward-authority promotions this incident collapsed

  1. Blacksmith observed a pool-side reward update under an attacker-owned dust denominator

    • Grap and surrounding withdrawals pushed totalLpTokens down to 1 wei, so the next updatePool() call produced an enormous accRewardsPerToken jump from an otherwise ordinary elapsed-emissions interval.
    • That only answers the weakest question: the pool's reward accumulator changed.
  2. Cover promoted that storage-side accumulator jump as if the depositor’s cancellation state had already caught up

    • deposit() wrote pool storage using the post-update accumulator, but still computed rewardWriteoff from the stale cached Pool memory pool snapshot.
    • This was the decisive broken promotion: pool-side reward truth moved was treated as if user-side reward cancellation truth described the same moment when it did not.
  3. The uncancelled gap became live COVER mint authority

    • claimRewards() later treated the mismatch between the huge live accumulator and the stale rewardWriteoff as money-good pending reward.
    • This was the final and most dangerous promotion. A stale accounting contradiction stopped being internal bookkeeping drift and became direct token issuance the attacker could dump into the market.

Why the exploit worked

  • Cover’s reward system stopped using one coherent state version for pool truth and user-cancellation truth. updatePool(_pid) correctly advanced the pool’s storage accRewardsPerToken, but deposit() kept the stale cached pool.accRewardsPerToken in memory when computing the depositor’s rewardWriteoff, so the user-side offset no longer matched the live pool accumulator.
  • The fresh-pool / low-liquidity state made that stale-accounting mismatch economically extreme. Once Grap and surrounding withdrawals pushed totalLpTokens down to 1 wei, the next rewards / totalLpTokens update turned an ordinary elapsed-emissions increment into an enormous accRewardsPerToken jump. Because rewardWriteoff still anchored to the old accumulator, almost the entire jump survived as claimable reward.
  • The accounting contradiction had direct line-of-sight to token minting. Blacksmith was not merely redistributing a capped bucket of already-funded rewards; it held live mint authority over COVER, so a bogus reward delta became a real token-issuance event the attacker could immediately sell.
  • This is why Cover is more useful than the flattened tiny pool got huge APR retelling. The tiny pool was the amplifier, not the root cause; the real break was letting post-update pool state and user reward-debt state disagree while also allowing the denominator to collapse into attacker-owned dust.

Detection and audit pivots

  • Review every reward path as one combined state machine: does the same post-update accumulator version drive both global pool state and the user’s new rewardDebt / rewardWriteoff, or does a cached memory struct survive across updatePool() / massUpdatePools() and later settle the user from stale data?
  • Treat reward-pool launch state as adversarial, especially when new pools can be added with almost no mandatory seed liquidity. If a first actor can own nearly all of totalLpTokens, then accRewardsPerToken is already a dust-denominator security surface, not just a weird APR edge case.
  • Compare claimable rewards against plausible emissions under adversarial low-liquidity conditions. If one deposit / withdraw / redeposit / claim sequence can mint more rewards than elapsed time should allow, the protocol needs a denominator floor, bootstrap mode, or payout-rail fail-closed check.
  • Be suspicious of operational mitigations that depend on more frequent updatePool() calls, keeper cron jobs, or UI-side pre-updates. Cover’s own postmortem is the reminder that such measures can reduce ordinary drift while leaving the true stale-state mismatch and the 1 wei amplifier fully live onchain.
  • Ask whether the reward rail mints new governance or incentive tokens directly from accounting state or merely streams out of a prefunded budget. Direct mint authority turns reward-ledger bugs into supply-integrity incidents, not only APR misallocations.

Why this incident matters

  • Cover preserves a clean reward-distribution exploit where the protocol did not misprice collateral, skip ownership checks, or break an AMM invariant; it simply let stale reward-debt math sit next to a global accumulator that could be amplified through a 1 wei pool balance.
  • It is a strong exemplar for launch-state risk outside lending markets: fresh mining pools can be as dangerous as fresh lending reserves when one actor can monopolize the first meaningful unit of accounting state.
  • For the exploit corpus, the note is useful because the official postmortem cleanly distinguishes the base bug from the denominator-collapse amplifier, which is exactly the reusable lesson later systems keep relearning in other accounting contexts.

Failure-prevention checklist

  • Recompute user reward debt / rewardWriteoff from the same post-update pool state that the protocol just committed to storage. A reward path should have exactly one canonical accumulator version per state transition, not a Pool memory pool snapshot that survives across updatePool() and later settles the user from stale values.
  • Treat launch-state and near-empty-pool conditions as adversarial. New reward pools should either require minimum seeded liquidity, delayed emissions, denominator floors, or other bootstrap controls so that one actor cannot turn 1 wei of stake into the whole reward-accounting security boundary.
  • Keep reward-accounting bugs away from direct mint authority whenever possible. If farming logic can mint governance or incentive tokens directly, add emission caps, sanity bounds, or kill switches that fail closed before impossible reward deltas become real supply.
  • Verify deposit, withdraw, claim, emergency, and batch-update helpers as one combined accounting machine. Cover’s bug lived in the seam between updatePool() and deposit(), not in the accumulator formula by itself.
  • Make lpTotal == 0, lpTotal dust, and rapid withdraw -> redeposit -> claim sequences explicit invariant-test regimes. A reward system that is only safe at ordinary TVL is not actually safe.
  • Do not rely on UI-side pre-updates, cron jobs, or more frequent keeper refreshes as the security fix for stale-state accounting. Operational freshness can reduce drift while leaving the exploitable state-machine mismatch fully live.
  • Keep newly added pools under tighter activation policy than mature pools: lower emissions, observation mode, manual review, or delayed mint authority until the denominator is no longer attacker-owned dust.
  • Add impossible-emissions alarms onchain and offchain: if claimable rewards for one miner or one block exceed plausible scheduled issuance by orders of magnitude, the system should pause or revoke reward mint authority before the market absorbs the new supply.

Audit prompts

  • Does any reward path cache a pool, vault, or market struct in memory, then call an updater, then continue to settle user debt or writeoffs from the stale snapshot?
  • After every deposit, withdraw, claim, harvest, or helper path, what exact invariant proves that user.amount * globalAccumulator - user.rewardDebt matches the intended claimable reward under the post-update state?
  • What happens when totalLpTokens falls to 0, 1, or other dust values? Is there a denominator floor, launch gate, or bounded-emissions mode, or does ordinary reward logic keep running as if the pool were healthy?
  • Can a first depositor or temporary sole staker own the entire denominator long enough to amplify one accumulator update into protocol-scale reward authority?
  • Does the farming contract mint new tokens directly, or does it only stream from a prefunded budget? If it mints directly, what prevents accounting nonsense from becoming supply-integrity failure?
  • Are new pools activated with the same emission and mint privileges as mature pools before they have any meaningful stake depth?
  • Would more frequent updater calls merely smooth ordinary accounting, or would they actually remove the exploit if an attacker can still engineer a near-zero denominator and force one stale-state transition?
  • Is there any path where emergency withdrawal, batch claim, pool-weight change, or governance-admin helper can reintroduce stale accumulator state even if the obvious deposit path is fixed?
  • What exact test or invariant catches withdraw almost everything, leave 1 wei, redeposit, then claim before deployment?
  • If an impossible reward delta appears, what shuts off mint authority fastest: a pool-local pause, a contract-wide pause, or a separate token-minter revocation path?

Lessons that should already be standard

  • Reward accounting is balance-sheet logic, not cosmetic APR math. If the contract can mint directly from reward deltas, a farming bug is already a token-supply bug.
  • Launch state is part of the security boundary. A fresh pool with attacker-owned dust denominator should be reviewed like a fresh lending market, not like a harmless incentives rollout.
  • Storage truth and user-cancellation truth must advance together. One stale cached struct is enough to turn a standard accumulator design into an infinite-mint rail.
  • We update more often now is not a fix when the real bug is state-version disagreement. Operational cadence cannot paper over a broken accounting transition.
  • Dust-denominator amplification should be assumed anywhere a global scaler is divided by attacker-shrinkable supply. The exact object can be LP stake, shares, debt units, or reward weight; the review habit should be the same.
  • Newly added pools deserve tighter activation than steady-state pools. If the first meaningful unit of stake can own the denominator, then the first depositor effectively owns the accounting truth.
  • Reward rails should fail closed under impossible issuance. If one account can claim many orders of magnitude more than elapsed emissions justify, the protocol should stop minting before traders discover price.
  • The right question is not just can rewards drift upward? but can one state transition let a user settle against a different accumulator version than the protocol itself considers canonical?

Internal linkages

Control surface