0x01 Event Background
on July 30 to on July 31, a large-scale attack occurred on the chain, resulting in the loss of funds in multiple Curve pools. The root cause of the bugs are all due to a reentrancy lock failure in certain versions of Vyper
0x02 Attack analysis
Through the preliminary analysis of the transaction data on the chain, we sort out and summarize the transactions attacked, and further analyze the attack process. Since the attack involves multiple transaction pools
The pETH/ETH pool was attacked and traded:
https://etherscan.io/tx/0xa84aa065ce61dbb1eb50ab6ae67fc31a9da50dd2c74eefd561661bfce2f1620c
msETH/ETH pool was attacked transaction:
https://etherscan.io/tx/0xc93eb238ff42632525e990119d3edc7775299a70b56e54d83ec4f53736400964
alETH/ETH pool was attacked transaction
CRV/ETH pool was attacked transaction
https://etherscan.io/tx/0x2e7dc8b2fb7e25fd00ed9565dcc0ad4546363171d5e00f196d48103983ae477c
Since the attack process is basically the same, we mainly analyze the pETH/ETH pool attack transactions in detail
0xa84aa065ce61dbb1eb50ab6ae67fc31a9da50dd2c74eefd561661bfce2f1620c
The transaction calls the attack contract 0x9420F8821aB4609Ad9FA514f8D2F5344C3c0A6Ab by 0x6ec21d1868743a44318c3c259a6d4953f9978538, and the one-time attack contract 0x466b85b49ec0c5c1eb402d5ea3c4b88864ea0f04 is created by the contract, and the next attack process is carried out in the constructor of the one-time attack contract;
The attacker obtains 80,000 WETH from Balancer through a flash loan, and withdraws all of it as ETH through the contract
Immediately afterwards, 40,000 ETH liquidity was provided to Curve’s pETH/ETH pool, and approximately 32,431.41 pETH-ETH LP Tokens were received.
Then the liquidity removal function is called in the attack contract, but we can see from the contract function call stack that the contract returns and calls the fallback function of the contract itself when executing the liquidity removal, and in the fallback function Called the add liquidity operation of the trading pair
According to the contract source code analysis of the pETH/ETH transaction pair, the contract is called again using the contract’s fallback function during the transfer, and contract reentry occurs here, but the LP contract uses reentry locks for adding liquidity and removing liquidity ,As shown below:
However, re-entrancy still occurred in the actual call stack, which resulted in adding 40,000 ETH to the LP Token through the attacker’s contract and obtaining about 82,182.76 pETH/ETH Lp Tokens, and continued to remove them at the end of the callback function of the attacking contract The initial added 32,431.41 pETH/ETH Lp Token obtained about 3,740.21 pETH and 34,316 ETH
Then call the remove liquidity function, remove about 10,272.84 pETH/ETH LP Token and get about 1,184.73 pETH and 47,506.53 ETH
The remaining more than 70,000 pETH/ETH LP Tokens are still in the attacker’s contract, as shown in the figure below
Next, the attacker exchanged approximately 4,924.94 pETH for 4,285.10 ETH through the Curve pETH/ETH pool.
Finally, the attacker converted about 86,106.65 ETH into WETH, returned the flash loan fund of 80,000 WETH to Balancer, and transferred the profit fund of about 6,106.65 WETH to the address
0x9420f8821ab4609ad9fa514f8d2f5344c3c0a6ab
So far, the analysis of the attack process against the pETH/ETH pool has been completed. It is a classic re-entry profit operation. However, by analyzing the source code of the attacked contract, the contract has a corresponding re-entry lock mechanism. It can prevent reentry operations, but does not deny the attacker’s reentry operations;
Let’s look back at the description of the reentrant lock in Vyper, and know that the implementation of the reentrant lock is to use the specified slot at the beginning of the function to store whether it is locked or not.
https://library.dedaub.com/ethereum/address/0x466b85b49ec0c5c1eb402d5ea3c4b88864ea0f04/decompiled
It can be seen that the slots for storing reentrant locks in the two functions are not consistent, so the reentrant locks are invalidated, which in turn leads to profit being exploited by attackers.
The Vyper project official also tweeted that there is indeed a reentry lock failure in some versions
By comparing its version 0.2.14 and version 0.2.15, it is found that there is a change in the reentrant lock related setting file data_positions.py corresponding to Vyper. The changed code sets the storage key of the reentrant lock separately, and , each reentrant lock will occupy a different storage slot, which makes the reentrant lock function of the contract unavailable
The bug has been fixed in PRs: #2439 and #2514
Among them, the problem of adding multiple reentrant storage slots has been fixed, as shown in the figure below:
summary
This attack involved a wide range of attacks. The root cause was that the smart contract infrastructure Vyper versions 0.2.15, 0.2.16, and 0.3.0 had unreasonable reentry lock designs, which led to the use of these versions later. The reentry lock failed in the project and was eventually hacked. It is recommended to choose a stable technology stack and corresponding version during project development, and conduct strict testing on the project to prevent similar risks;