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

BlockSec
7 min readOct 10, 2021

--

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 the 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

  • 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 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 function setCompSpeedInternal will go through the else if (compSpeed != 0) statement (line 1083). Then in line 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 reenabling the distribution of COMP tokens in an existing market (not a new market), then the statements in 1090 an 1091 will not be executed to initialize the the index and block (since the 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, 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) (line 1090 and 1091 is not executed.)

the impact of the bug 1

We further dig into the ``distributeSupplierComp` function that’s responsible for distributing the COMP tokens.

The supplierIndex is compInitialIndex. However, the supplyIndex is still zero due to the bug, it 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 see 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.

--

--

BlockSec

The BlockSec focuses on the security of the blockchain ecosystem and the research of DeFi attack monitoring and blocking. https://blocksec.com