[The Butterfly Effect] The Compound Security Incident Caused by a Bugfix

The article describes two bugs in the Compound protocol and their impact on the distribution of COMP tokens to users.

[The Butterfly Effect] The Compound Security Incident Caused by a Bugfix

By BlockSec Team (@BlockSecTeam)

In the last week, the Compound protocol has a bug that will accidentally send a large number of COMP tokens to users. The cause of this bug (bug 2 in this blog) is due to the incorrect fix of another bug (Bug 1 in this blog) that was previously discovered.

In this blog, we will elaborate the root cause of the first bug and the reason why the fix to the first bug causes the second bug.

Background

The Compound protocol is based on the Compound Whitepaper. Through the cToken contracts, accounts on the blockchain supply capital (Ether or ERC-20 tokens) to receive cTokens or borrow assets from the protocol (holding other assets as collateral). The Compound cToken contracts track these balances and algorithmically set interest rates for borrowers.

To incentive users, users who provide liquidity to Compound (supplying capital) can receive the interest. Specifically, users provide assets (e.g., Ether or other ERC20 tokens) to Compound and receive the corresponding cTokens. When the cToken is returned back to Compound, the underlying assets (Ether or ERC20 tokens) and the interests will be returned to the user, if the user does not have any debt in Compound. For instance, if a user has 1000 Ether, then he/she can put the asset into Compound through cEth.mint(1000) to obtain the cToken.

The cToken represents the underlying assets that have been locked in Compound. The user can further use the cToken as the collateral to borrow other assets. For instance, a user can deposit 1000 Ether through ceth.mint(1000) and then use the obtained cTokens to borrow x Dai worth 75 Ether (over-collateralization -- this number depends on the collateral factor) through cDai.borrow(x).

The core logic is implemented in the Comptroller contract. It maintains the states of a user, e.g., how many tokens have been deposited into Compound by the user, how many tokens have been borrowed by the user, and whether the user can borrow more tokens. The functions invoked in this process include getHypotheticalAccountLiquidityInternal(), borrowAllowed(), mintAllowed(), and etc.

The Compound also has the governance token called COMP. The COMP token can be used to vote for proposals. Besides, the COMP token can be traded in exchanges. Currently, the price of the COMP is around $300.

Bug 1

On September 31, 2021, there was a new proposal (Proposals 62) in the Compound DAO, which aims to fix a bug in the Comptroller.

The bug is related to CompSpeed, which represents the number of COMP tokens that can be distributed to users in each block.

The Flow of the `mint` Function

In the following, we will use the mint function to describe the cause of this bug. The invocation chain of the mint function is: mintmintInternalmintFresh.

In the function mintFresh, it invokes the mintAllowed and then updates the user's balance of the cToken.

In the function mintAllowed, it first invokes updateCompSupplyIndex and then distributeSupplierComp to 1) update the compSupplyState of the market and 2) distribute the COMP tokens to users.

updateCompSupplyIndex

The function updateCompSupplyIndex will update the status of each market, mainly the compSupplyState[cToken].

In the CompMarketState structure, it records the block number (block) of this update, and the bonus index (index) that will affect the number of COMP tokens that should be distributed to the users (who hold the cToken.)

What is the bonus index (index) for each token? This is the value accumulated over time (shown in the following formula).

This shows the number of COMP that should be distributed to users (for each cToken the user holds).

distributeSupplierComp

The another function distributeSupplierComp is responsible for recording the number of COMP tokens that should be distributed to the user (supplier) in compAccrued[supplier].

Specifically, it updates the global bonus index in compSupplyState (in updateCompSupplyIndex function). Then in the distributeSupplierComp function, the supplyIndex records the current bonus index, and the supplierIndex shows the last bonus index for the user (supplier). The delta value (supplyIndex - supplierIndex) * the user's cToken balance shows the number of COMP tokens that should be distributed to the user.

The Cause of Bug 1

There is another function setCompSpeed to adjust the supplySpeed of the market (compSpeeds[address[cToken]]).

That’s because if we set the CompSpeed for a market to zero, that means the COMP token will not be distributed to users in that market. So if we want to first disable the distribution of COMP for a market and then re-enable it, we can follow these steps:

  • Step I: Set the CompSpeed[cToken] as zero to disable the distribution of COMP tokens.
  • Step II: Invoke the setCompSpeed function to set CompSpeed[cToken] as a non-zero value.

Step I: For markets that have been disabled for the distribution of COMP tokens in Step I (supplySpeed == 0), the block is not zero, since the block is continuously updated in updateCompSupplyIndex (else if (deltaBlocks > 0)).

Step II: When executing the operation in Step II, the setCompSpeedInternal function will go through the else if (compSpeed != 0) statement (line 1083). Then, in lines 1088 to 1093, there is a check if (compSupplyState[address(cToken)].index == 0 && compSupplyState[address(cToken)].block == 0) to initialize the index and block for a new market. However, since we are re-enabling the distribution of COMP tokens in an existing market (not a new market), the statements in lines 1090 and 1091 will not be executed to initialize the index and block (since compSupplyState[address(cToken)].block is not zero).

In summary, for a currently disabled market, the index is zero. However, the block is not zero. This means that when we re-enable the disabled market by invoking setCompSpeed to set CompSpeed[cToken] as a non-zero value, the index value will NOT be reinitialized to CompInitialIndex (1e36) (lines 1090 and 1091 are not executed).

The Impact of Bug 1

We further dig into the distributeSupplierComp function that is responsible for distributing the COMP tokens.

The supplierIndex is compInitialIndex. However, the supplyIndex is still zero due to the bug, which will cause an underflow for Double memory deltaIndex = sub_(supplyIndex=0, supplierIndex=1e36).

Bug 2: Introduced by the Fix to the Bug 1

To fix the bug, the project owner changes the code logic. Specifically, it immediately initializes the index to compInitialIndex when initializing a new market.

Since the global bonus index (index) has been initialized to compInitialIndex, the user bonus index should also be initialized to this value. Let's take a look at the distributeSupplierComp function.

The if condition on line 1234 cannot be satisfied even if supplierIndex == 0 since the supplyIndex is equal to (not bigger than) compInitialIndex (1e36). This causes that the supplierIndex is NOT properly initialized to compInitialIndex (its value is 0). Then the deltaIndex (supplyIndex - supplierIndex) will be compInitialIndex, instead of zero. The supplierTokens will become a large value if the user's balance of the cToken is not zero.

In summary, if a user happens to perform the mint operation before the fix of the bug 1, then he/she has cTokens and the supplierIndex will become zero (since the COMP token has been distributed). Then after the fix of the bug 1 (which introduces the bug 2), when the user invokes the mint function again, he/she can get a large number of COMP token (1e36*ctoken.balanceOf(user)).

Real World

We show the affected markets in the following:

0xF5DCe57282A584D2746FaF1593d3121Fcac444dC: cSAI
0x12392F67bdf24faE0AF363c24aC620a2f67DAd86: cTUSD
0x95b4eF2869eBD94BEb4eEE400a99824BF5DC325b: cMKR
0x4B0181102A0112A2ef11AbEE5563bb4a3176c9d7: cSUSHI
0xe65cdB6479BaC1e22340E4E755fAE7E509EcD06c: cAAVE
0x80a2AE356fc9ef4305676f7a3E2Ed04e12C33946: cYFI

For the user (0xa7b95d2a2d10028cc4450e453151181cbcac74fc), the user gets 4,466.542459954989867175 COMP tokens in this transaction (0x6416ed016c39ffa23694a70d8a386c613f005be18aa0048ded8094f6165e7308).

The further debugging of the transaction shows that, due to the bug 2, the deltaIndex is 1e36 and the user happens to have the cToken at that time.

The Fix to the Bug 2

The fix to the bug 2 is simple. It changes the if condition in the distributeSupplierComp function.

Lessons

  • This is a bug caused by the fix of another bug. How to thoroughly review the code changes for high-profile projects is still an open question.
  • The DAO can eliminate the risk of centralization. However, it also makes the response to security incidents a slow process.
  • The high profile DeFi projects can take good security practices in traditional programs, e.g., deploying an efficient fuzzing system with a continuous testing process.

About BlockSec

BlockSec is a pioneering blockchain security company established in 2021 by a group of globally distinguished security experts. The company is committed to enhancing security and usability for the emerging Web3 world in order to facilitate its mass adoption. To this end, BlockSec provides smart contract and EVM chain security auditing services, the Phalcon platform for security development and blocking threats proactively, the MetaSleuth platform for fund tracking and investigation, and MetaSuites extension for web3 builders surfing efficiently in the crypto world.

To date, the company has served over 300 esteemed clients such as MetaMask, Uniswap Foundation, Compound, Forta, and PancakeSwap, and received tens of millions of US dollars in two rounds of financing from preeminent investors, including Matrix Partners, Vitalbridge Capital, and Fenbushi Capital.

Official website: https://blocksec.com/

Official Twitter account: https://twitter.com/BlockSecTeam

Sign up for the latest updates