By BlockSec
0x.1 Background
At 02:38 (UTC+8) on Oct 15th, 2021, our internal monitoring system (we just released an online system to engage the community: https://monitor.blocksecteam.com/) caught suspicious flashloan transactions:
After investigation, we found that it was a price manipulation attack targeting Indexed Finance. Specifically, the attacker launched the attack by exploiting the flawed formula (that is used to calculate the price) of this project, and gained $16 million profit.
There already exist some discussion on the social media, while the project has released an official post-mortem (Indexed Attack Post-Mortem: https://ndxfi.medium.com/indexed-attack-post-mortem-b006094f0bdc). However, existing analyses do not give a full understanding of this security incident. As such, in this blog, we aim to provide a comprehensive analysis, including the mechanism of the project, the vulnerability and the attack.
0x1.1 Relevant Contract Addresses
- MarketCapSqrtController: 0x120c6956d292b800a835cb935c9dd326bdb4e011
- DEFI5: 0xfa6de2697d59e88ed7fc4dfe5a33dac43565ea41
- CC10: 0x17ac188e09a7890a1844e5e65471fe8b0ccfadf3
0x1.2 Attack Transactions
- Attack TX-I: 0x44aad3b853866468161735496a5d9cc961ce5aa872924c5d78673076b1cd95aa
- Attack TX-II: 0xbde4521c5ac08d0033019993b0e7e1d29b1457e80e7743d318a3c27649ca4417
0x2. Mechanism of Indexed Finance
To better understand the vulnerability/attack, we use DEFI5 (i.e., the pool hacked by the attacker) to demonstrate the mechanism of Indexed Finance.
0x2.1 Binding Token
DEFI5 is designed to provide the trade service for Top 5 tokens of DeFi projects of Ethereum. Specifically, Indexed Finance will update the token rankings based on their market cap through MarketCapSqrtController. Because the sort of Top 5 tokens might change as time goes by, the number of tokens used by the DEFI5 pool may bigger than 5, as shown in the following code:
Figure 1 shows that, to bind a new token, DEFI5 has to trigger the _bind function invoked by the reindexTokens function, which can only be invoked by the reindexPools function of MarketCapSqrtController:
Figure 2 demonstrates that, MarketCapSqrtController first fetches token information (including the TotalSupply and the price) from the markets, and then calculates the ranking based on their market capacity. When invoking the reindexPool function, the addresses of the top tokens will be passed as arguments to invoke the reindexTokens function. Note that the new added token will be bound to DEFI5 without replacing the original tokens of DEFI5.
0x2.2 What is the Next?
After the token binding, DEFI5 has to set a variable named ready (that indicates the token status) to to be true
to enable the trade:
According to the code logic, apart from the contract initialization, the ready can only be set in the gulp function. As shown in Figure 3, it happens when the token balance in DEFI5 is greater or equal to _minimumBalances
. Meanwhile, the initial token weight (i.e., denorm
) will be calculated based on the following formula:
0x3. Vulnerability Analysis
The vulnerable code belongs to updateMinimumBalance
function of MarketCapSqrtController.
As shown in Figure 4, updateMinimumBalance can change the minimumBalance of a token whose ready is false to 1/100 of poolValue
. The calculation of poolValue is the key of the vulnerability.
The calculation of Figure 5 just implements the following formula:
However, there exist two potential problems in this formula:
- using one token’s liquidity to estimate the value of the entire pool;
- the weights of the pool (
_totalWeight
) and the token (token.denorm
) are not affected by the change of the liquidity. As a matter of fact, they are influenced by the Market Capacity of the external markets. Besides, their change is limited by the time period, i.e., increase or decrease 1% per hour.
In short, the attacker is able to manipulate poolValue
by using the flashloan to instantly cause a big change to the liquidity of a token, while the weights of the pool and the token remain unchanged accordingly. By doing so, minimumBalance
of the token (whose ready is false) can be manipulated to launch the price manipulation attack.
0x4. Attack Analysis
The attack consists of the following 9 steps:
Step 1: invoking the reindexPool
function to bind SUSHI. Before the invocation, there are 6 tokens in DEFI5 pool, including UNI, AAVE, COMP, SNX, CRV and MKR. As the Market Capacity of SUSHI becomes Top 5, SUSHI will be added to the pool.
Step 2: lending all 6 tokens (UNI, AAVE, COMP, SNX, CRV and MKR) supported by IndexPool through SushiSwap.
Step 3: swapping UNI by invoking the swapExactAmountIn
function multiple times with the borrowed tokens (take CMOP as an example).
Notice 1: here multiple times are due to the constraint of
MAX_IN_RATIO
, one can only swap half of the token balance at most.Notice 2: in this step,
poolValue
will be greatly underestimated, because the balance of UNI (as thefirstToken
) in the pool decreases a lot.
Step 4: modifying minimumBalance
of SUSHI by invoking the updateMinimumBalance
function.
Note that
minimumBalance
is smaller than the normal value since the abnormalpoolValue
calculated in step 3.
Step 5: preparing LP tokens by invoking the joinswapExternAmountIn
function to provide liquidity. We will see that these LP tokens are used to swap back more SUSHI.
Note that: the
joinswapExternAmountIn
function needs to be invoked multiple times due to the impact ofMAX_IN_RATIO
.
Step 6: manipulating the wight of SUSHI in DEFI5 pool by first lending a huge amount of SUSHI and transferring them to the pool, and then invoking the gulp
function to set SUSHI's ready to true
. By doing so, the initial weight of SUSHI (denorm
) becomes a high value.
Step 7: swapping LP tokens back to underlying tokens (UNI, AAVE, COMP, SNX, CRV, MKR and SUSHI) by invoking the exitPool
function.
Note that the
exitPool
function does NOT consider the weight of each token, as a result, the underlying tokens will be returned back with an equal proportion.
Step 8: swapping LP tokens by invoking the joinswapExternAmountIn
function with SUSHI to provide liquidity. More LP tokens can be harvested due to the abnormal weight of SUSHI at the moment.
Note that the joinswapPoolAmountIn
function will mint the LP token based on the weight of received underlying token (SUSHI in this case).
Step 9: draining the pool by invoking the exitPool
function with the harvested LP tokens in step 8.
Credits: Siwei Wu, Junjie Fei, Yufeng Hu, Lei Wu, Yajin Zhou @BlockSec
Twitter: https://twitter.com/BlockSecTeam