Cover Protocol Blacksmith Stale-Pool-Cache rewardWriteoff Desynchronization / Infinite COVER Mint (2020-12-28)
- Name: Cover Protocol Blacksmith stale-pool-cache
rewardWriteoffdesynchronization / 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 ofCOVERbecausedeposit()cached the pool struct in memory, then calledupdatePool(_pid)to raise the pool’s storageaccRewardsPerToken, but still computed the miner’srewardWriteofffrom the stale cached accumulator. In a newly added Balancer pool (0x59686e01aa841f622a43688153062c2f24f8fded), attackers first withdrew until only1 weiof 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 minted40,796,131,214,802,500,000.212114436030863813 COVERin one claim after restoring a large deposit on top of a1 weipool. - 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)loadedPool memory pool = pools[_pid], then calledupdatePool(_pid), but continued using the stale in-memorypool.accRewardsPerTokenwhen updating the user’srewardWriteoff.- The exploit became extreme only after the attacker or surrounding withdrawals reduced the pool’s
totalLpTokensto nearly zero, because the nextaccRewardsPerTokenupdate 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.
- Cover’s core insurance product was not the entry point; the exploitable surface was the separate shield-mining / farming contract
- Root cause:
- Cover used a standard farming design with a global per-share accumulator (
accRewardsPerToken) and per-user reward debt / writeoff tracking, butdeposit()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
rewardWriteoffno 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 / totalLpTokensbecame enormous when only1 weiremained staked. - Operational mitigation focused on calling
updatePoolmore often and updating before deposits, but it still underestimated the1 weidenominator amplifier and therefore did not remove the real exploit path. - More broadly, the protocol treated
fresh pool + low stakeas an operations concern rather than part of the security boundary around reward-mint authority.
- Cover used a standard farming design with a global per-share accumulator (
- Exploit path:
- Cover added a new Balancer LP token pool to
Blacksmithvia multisig transaction0xe5173fffaed3342b53d41319dc538e7923e287e962df2d27f5e425c633db45d4. - The first exploiter deposited into the new pool in tx
0xd721b0ef2886f14b75548b70d2d1fd82bea085ca24f5de29b833a64cfd8f7a50, then withdrew almost all LP in tx0xadf27f5dd052482d46fdf69a5208a27cc7352522c7c19bbde5aee18f6ea4373b, already extracting about703.64 COVERbecause the pool was still tiny. - Grap Finance later executed the clearer amplifier sequence: deposit
15,255.5528...BPT in tx0x77490baee41a9b35a6e87d49453c7329c7517c10ce6ce26b4c142692a2877e65, then withdraw almost everything in tx0x88ce99fc1cb695db82d83ce5fe587396744841d3a123687f95b18df6a3106818, leaving only1 wei. - Another user withdrew the rest of their balance in tx
0xa27fb73caddb1cf24aa7a5afe84eed13db2f0a889a6ee0f3d5e6226a76c0fd9c, so Grap alone controlled the pool’s remaining1 weiof stake. - Grap deposited the large BPT amount back in tx
0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215; during this call,updatePool()pushed storageaccRewardsPerTokensky-high because the denominator was effectively1 wei, butrewardWriteoffstill used the stale pre-update cached value. - Grap then claimed rewards in tx
0xca135d1c4268d6354a019b66946d4fbe4de6f7ddf0ff56389a5cc2ba695b035f, minting roughly4.0796e19 COVER, sold as much as possible through 1inch, burned part of the position, and later returned4,351 ETH; several other addresses also copied the exploit before Cover revoked Blacksmith’s minting rights.
- Cover added a new Balancer LP token pool to
- 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
totalLpTokensclose 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 weiof 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 poollaunch state belongs in security reviews. A harmless-looking accumulator mistake became catastrophic only because a just-added pool could be driven into the1 weiregime before organic stake diluted it.
- This was not merely
- 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 weistaker:0xa27fb73caddb1cf24aa7a5afe84eed13db2f0a889a6ee0f3d5e6226a76c0fd9c - Grap redeposit:
0xbd1fcda7006ddd58b18cb3bfbd01ef2d1a979be596e1c73be1d7d65fd7eb8215 - Grap claim / huge mint:
0xca135d1c4268d6354a019b66946d4fbe4de6f7ddf0ff56389a5cc2ba695b035f - Mitigation tx removing minting rights from Blacksmith:
0xaaa09c1246cf2ff111464d2947d764967c5c28c7cc353d5d906a320d4a6588fc
- Blacksmith contract:
- 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
totalLpTokensdown to1 weibefore honest stake could dilute the next accumulator jump - Stale-cache reward settlement /
deposit()updated storageaccRewardsPerTokenthroughupdatePool(_pid)but still wrote the user’srewardWriteofffrom a pre-updatePool memory poolsnapshot - Reward-ledger mint authority collapse / impossible farming-accounting deltas could mint fresh
COVERdirectly rather than merely over-allocate a fixed prefunded reward pot
Three reward-authority promotions this incident collapsed
-
Blacksmith observed a pool-side reward update under an attacker-owned dust denominator
- Grap and surrounding withdrawals pushed
totalLpTokensdown to1 wei, so the nextupdatePool()call produced an enormousaccRewardsPerTokenjump from an otherwise ordinary elapsed-emissions interval. - That only answers the weakest question:
the pool's reward accumulator changed.
- Grap and surrounding withdrawals pushed
-
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 computedrewardWriteofffrom the stale cachedPool memory poolsnapshot.- This was the decisive broken promotion:
pool-side reward truth movedwas treated as ifuser-side reward cancellation truth described the same momentwhen it did not.
-
The uncancelled gap became live COVER mint authority
claimRewards()later treated the mismatch between the huge live accumulator and the stalerewardWriteoffas 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 storageaccRewardsPerToken, butdeposit()kept the stale cachedpool.accRewardsPerTokenin memory when computing the depositor’srewardWriteoff, 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
totalLpTokensdown to1 wei, the nextrewards / totalLpTokensupdate turned an ordinary elapsed-emissions increment into an enormousaccRewardsPerTokenjump. BecauserewardWriteoffstill anchored to the old accumulator, almost the entire jump survived as claimable reward. - The accounting contradiction had direct line-of-sight to token minting.
Blacksmithwas not merely redistributing a capped bucket of already-funded rewards; it held live mint authority overCOVER, 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 APRretelling. 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 cachedmemorystruct survive acrossupdatePool()/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, thenaccRewardsPerTokenis 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 the1 weiamplifier 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 weipool 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 /
rewardWriteofffrom 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 aPool memory poolsnapshot that survives acrossupdatePool()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 weiof 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()anddeposit(), not in the accumulator formula by itself. - Make
lpTotal == 0,lpTotaldust, and rapidwithdraw -> redeposit -> claimsequences 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 thatuser.amount * globalAccumulator - user.rewardDebtmatches the intended claimable reward under the post-update state? - What happens when
totalLpTokensfalls to0,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 claimbefore 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 nowis 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?butcan one state transition let a user settle against a different accumulator version than the protocol itself considers canonical?
Internal linkages
- Recurring exploit pattern: reward-debt-desynchronization-denominator-collapse-reward-amplification — the direct home for this
stale reward debt + near-zero denominatorfamily. - Positive-design companion: fresh-market-seeding-and-activation-discipline — the healthier rollout answer when a newly added reward pool should not become attacker-owned dust state before emissions meaningfully start.
- Low-supply hardening companion: bootstrap-state-irreversibility-and-supply-floor-design — the deeper denominator lesson is that a reward pool should not be able to rewind back into a
1 weiattacker-owned security regime once activated. - Reward-right companion: reward-entitlement-identity-and-batch-canonicalization — useful when the next design question is how reward entitlements should stay canonical and consumed once the pool has already computed legitimate accrual.
- Adjacent recurring pattern: fresh-market-initialization-failure — the amplifier mattered because the pool had just been added and had not yet built a healthy stake base.
- Adjacent recurring pattern: supplier-index-inflation — a useful analogue on the lending side, where a dust-sized supplier base captures outsized value when protocol-wide scalers reprice against near-zero supply.
Control surface
-
The decisive leverage sat in who controlled the first meaningful stake of a new reward pool, how
accRewardsPerTokenwas updated, whether user reward debt / writeoff used the same state version, and whether dust-denominator reward spikes were bounded before they could mint real governance or incentive tokens. -
Cover did not fail because per-share reward accounting is inherently unsafe. It failed because the contract let a near-zero-denominator accumulator update and a stale cached writeoff coexist as if they referred to the same economic state.
-
Sources:
- https://coverprotocol.medium.com/12-28-post-mortem-34c5f9f718d4
- https://github.com/CoverProtocol/cover-token-mining/blob/main/contracts/Blacksmith.sol#L118-L121
- https://rekt.news/cover-rekt
- https://mudit.blog/cover-protocol-hack-analysis-tokens-minted-exploit/
- https://etherscan.io/address/0xe0b94a7bb45dd905c79bb1992c9879f40f1caed5#code
-
Last reviewed: 2026-08-01 UTC