On December 28, 2020 at 12:02:04 PM UTC a transaction minted ~40 quintillion (40e18) $COVER tokens. This was one of the transactions that had exploited a bug in one of Cover Protocol’s contracts.
Cover Protocol is a decentralized insurance provider used by the DeFi ecosystem on Ethereum. They provide coverage for virtually anything provided there are people willing to provide coverage. Any DeFi user looking for coverage on a protocol simply has to purchase CLAIM tokens issued to cover that protocol. They are bought from Balancer and are redeemable for an equivalent amount of collateral if that protocol is exploited before the expiry date. If you are interested in how the other parts of Cover work, check out this excellent article by Andre Cronje.
Before the hack, Cover incentivized liquidity in its pools on Balancer by allowing Balancer Pool liquidity providers to mine $COVER, it was called shield mining. To engage in shield mining, you had to provide liquidity in one of the Cover pools on Balancer, receive Balancer Pool Tokens (BPTs), and stake on Cover. It’s no longer active though but you can find out more about how it worked. The Blacksmith (I wonder why it’s called that) smart contract was used for shield mining.
BlackSmith had a bug which the developers said they were unaware of when it was deployed. The bug isn’t easily noticed but it’s there alright. What’s even more fascinating was the way it was exploited. Let’s find out why.
It’s easy for me to explain because it’s already been exploited. Exploiting this bug for real would have been tedious. Took me time to understand it.
How does Blacksmith work?
To start earning $COVER on Blacksmith you have to deposit BPT tokens. Each address that deposits BPT tokens is a miner and is represented by the struct below.
amount: the number of tokens the miner has in a pool
rewardWriteoff: the number of $COVER the contract assumes has been paid to the miner
bonusWriteoff: the number of bonus tokens the contract assumes has been paid to the miner
Each BPT token is added to a pool which is also represented by the struct below.
weight: A proportional value that determines the size of the allocatable $COVER tokens to be given to each pool.
accRewardsPerToken: the total number of $COVER tokens allocated to each token in the pool since when it was created to when it was last updated(we’ll talk about pool updates soon)
lastUpdatedAt: the timestamp for when the accRewardsPerToken of this pool was last updated
Each pool is updated periodically to make sure its
accRewardsPerToken field is up to date. A pool update calculates the amount of $COVER tokens that should be given to each token in the pool since it was last updated and adds it to the
accRewardsPerToken field of the pool struct.
Now here’s what I like about the contract, the way it allocates the correct number of $COVER tokens to each miner’s token in a pool — this is what was exploited. The trick is in the
rewardWriteoff field of the miner struct which is obtained using this snippet of code.
miner.rewardWriteoff = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER)
Blacksmith uses CAL_MULTIPLIER to ensure precision it can be ignored for the sake of this article.
The contract assumes it has already paid the rewardWriteoff to the miner. It is updated on each deposit to the number of tokens the miner is supposed to receive if he had his tokens in the pool from the time it was created.
Anytime he claims his $COVER token reward this line of code runs:
uint256 minedSinceLastUpdate = miner.amount.mul(pool.accRewardsPerToken).div(CAL_MULTIPLIER).sub(miner.rewardWriteoff);
He only receives the number of tokens he has earned since his last deposit. The contract calculates this by subtracting his
rewardWriteoff from the total number of tokens he is supposed to receive if he had his BPT tokens in the pool from the time it was created,
miner.amount.mul(pool.accRewardsPerToken). They are not equal since
accRewardsPerToken of the pool is updated before he claims his tokens. To ensure he always gets his old rewards when he makes a deposit, he automatically claims them before his
rewardWriteoff field is updated.
The exploit was centered around the misuse of storage and memory references of the pool in the deposit function.
In the deposit function above, the pool which was to receive a deposit was read from storage and assigned to a memory variable in line 4, which created a copy of it in memory. But
updatePool(_lpToken) updated the same pool in line 7 this updated the pool stored in storage but since the pool in memory is a copy of the pool in storage and not a reference, it was not updated which meant the pool still had the same
accRewardsPerToken and unfortunately it was used in the remaining part of the function to claim the miner’s $COVER reward in line 11 and calculate his
rewardWriteoff in line 16. Using this stale pool to claim the miner’s reward meant he would get fewer rewards because it would not include the $COVER tokens earned since the last update and using it to calculate the rewardWriteoff meant it would be less than what it was meant to be which would let him claim more rewards — this was the exploit.
In Cover protocol’s post-mortem of the exploit, they noticed this error in the code and tried mitigating it by doing the following:
1. Adding an update pool transaction before the deposit so that the pool was updated when the user deposits, will mitigate the issue for users using UI.
2. Starting a cron job to run every 20 mins to update any pool that hadn’t been updated recently.
But they missed something they called “The Amplifier”. This occurred when the difference between the amount already in the pool and the amount to be deposited by the miner was great e.g (1 wei was in the pool and 1e18 wei was being deposited by the miner). The Amplifier lets a miner mint an almost infinite amount of $COVER tokens. They had initially thought the bug would only give little bumps in $COVER rewards but had failed to think about a scenario like this.
Blacksmith was exploited more than once using The Amplifier but the most significant of them was a whitehat hack done by Grap Finance. Where they approximately 40 quintillions $COVER tokens were minted effectively crashing the price. It is important to note that the Cover protocol worked just fine and wasn’t exploited. Let’s take a look at the timeline of Grap Finance: Deployer (the deployer of Grap Finance’s smart contract) white hack using screenshots from Tenderly.
Remember, the hacker aims to amplify the number of tokens he will receive as a reward by increasing the difference between the number of tokens in the pool and the number of tokens he wants to deposit.
Dec-28–2020 11:54:47 AM +UTC
Action: Grap Finance: Deployer deposited 15,255.552810089260015362 BPT into the Blacksmith contract.
Dec-28–2020 11:58:04 AM +UTC
Action: Grap Finance: Deployer withdraws 15,255.552810089260015361 BPT from Blacksmith, leaving just 1 wei in their balance on the Blacksmith farming contract.
- Grap Finance Deployer Address
- Grap Finance Deployer’s balance reduces to 1 wei after withdrawal
Dec-28–2020 11:58:56 AM +UTC
Action: Another user withdraws all his tokens (1,007.599009946121991627 BPT) from the Blacksmith. Now Grap Finance alone has all liquidity for the DAI/Basis pool on the shield mining Blacksmith contract, exactly 1 wei.
- The balance of the user’s BPT token increases to the amount he withdraws from Blacksmith
- The balance of the Blacksmith contract BPT token reduces to the 1 wei left in the pool by the Grap Finance Deployer address.
Dec-28–2020 12:00:21 PM +UTC
Action: Grap Finance: Deployer deposited back 15,255.552810089260015361 BPT (DAI/Basis pool) in the Blacksmith farming contract.
Tenderly State-Changes for the Grap Finance: Deployer second deposit
- Grap Finance: Deployer deposited 15,255.552810089260015361 BPT tokens and there was already 1 BPT in the pool.
- The stale accRewardsPerToken was used in calculating the rewardWriteoff of the miner because of the storage/memory bug which resulted in a smaller rewardWriteoff.
- The new accRewardsPerToken was meant to be used in calculating the rewardWriteoff but wasn’t because of the storage/memory bug.
Dec-28–2020 12:02:04 PM +UTC
Action: Grap Finance: Deployer claimed the rewards by calling
claimRewards, and because of only 1 wei of balance combined with the storage/memory bug this led to the minting of 40,796,131,214,802,500,000.212114436030863813 $COVER.
Tenderly State-Changes screenshot for Grap Finance: Deployer claim of rewards.
- Grap Finance: Deployer calls
claimRewards()to get his rewards from the contract. His balance increases by 40,796,131,214,802,500,000.212114436030863813 $COVER which is far greater than the $COVER token total supply.
- This was meant to be his
rewardWriteoffif there was no memory/storage bug.
accRewardsPerTokenmeant to be used in calculating Grap Finance: Deployer's accRewardsPerToken. The
accRewardsPerTokenwas updated in line 2 of
- This caused the cover rewards to amplify as it was the value of the
accRewardsPerTokenfield of the pool in line 8 used to calculate the $COVER rewards of the miner.
Grap Finance: Deployer returned the $COVER tokens after swapping a portion for ETH and burning the rest of them. It looked like he/they had seen previous transactions exploiting the bug and decided that exploiting the bug and returning the value taken would help. It did crash the price of $COVER though that I can say for sure.
Decentralized Finance (DeFi) is still nascent and there will be more hacks to come but we know where we are headed, join us on this journey to redefine finance. Check out this Guide to Decentralized Finance by Finematics to get started.