[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.
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
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
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.
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
In the following, we will use the
mint function to describe the cause of this bug. The invocation chain of the
mint function is:
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 will update the status of each market, mainly 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).
The another function
distributeSupplierComp is responsible for recording the number of COMP tokens that should be distributed to the user (
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 (
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
setCompSpeedfunction 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
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
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
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.
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
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
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)).
We show the affected markets in the following:
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
- 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.