648 total views
The attackers used Popsicle Finance’s loopholes in the machine gun pool implementation to launch an attack and made more than 20 million U.S. dollars in profits.
Written by: BlockSec
At 6 AM on August 4, 2021, Beijing time (block 12955063), multiple machine gun pools under the Popsicle Finance project were attacked, with losses exceeding US$20 million, the largest single loss in the DeFi field so far One of the attacks. By analyzing the attack transaction and project code, we found that the attack was a double-claiming attack (Double-Claiming Rewards) that used the project’s accounting vulnerabilities. Below we analyze the attack through the code and attack process.
Popsicle Finance is a machine gun pool (Yield Optimization Platform) involving multiple chains.
The user first calls the deposit function to deposit a certain amount of liquidity into the machine gun pool, and obtains the Popsicle LP Token (hereinafter referred to as PLP Token) as the deposit share certificate. Popsicle Finance will deposit the liquidity provided by users into the underlying pools such as Uniswap and obtain revenue.
The user can also call the withdraw function to retrieve liquidity from the machine gun pool based on the liquidity share represented by the PLP Token held by the user. Popsicle Finance will retrieve the liquidity corresponding to the PLP Token from the underlying pools such as Uniswap to the user.
Finally, the user’s liquidity in the machine gun pool will generate a certain amount of income (Yield) over time, which will be accumulated in the user status of the contract. The user can call the collectFees function to get back part of the deposit bonus.
The core function of this attack is the collectFees function. Let’s analyze its code step by step. First get the user status stored in userInfo. Among them, token0Rewards and token1Rewards in the user status are rewards accumulated due to user deposits.
Next, calculate the balance of the Token pair corresponding to the machine gun pool in the contract. If there is enough Balance in the contract, the Reward will be paid to the user according to the amount; otherwise, pool.burnExactLiquidity will be called to retrieve liquidity from the underlying pool and return it to the user.
Finally, the Rewards status recorded in userInfo will be updated. Seeing this, the code implementation of the machine gun pool is quite logical. But at the beginning of the function, we found the updateVault modifier. This function will run before the function body of collectFees. The vulnerability may be in updateVault-related functions.
The above is the implementation of updateVault related functions. The process is as follows:
- First call _earnFees to obtain accumulated Fee from the underlying pool;
- Then call _tokenPerShare to update the token0PerShareStored and token1PerShareStored parameters. These two parameters represent the number of token0 and token1 represented by each share in the pool, that is, the number of Token pairs represented by each share meter in the machine gun pool;
- Finally, call _fee0Earned and _fee1Earned to update the deposit Rewards corresponding to this user (ie user.token0Rewards and user.token1Rewards).
The above is the implementation of the _fee0Earned and _fee1Earned functions. The two functions have the same implementation, and both implement such a formula (take _fee0Earned as an example):
user.token0Rewards += PLP.balanceOf(account) * (fee0PerShare-user.token0PerSharePaid) / 1e18
In other words, this function will calculate the share of Fee that should be issued to the user based on the original user.token0Rewards based on the number of PLP Tokens owned by the user.
However, we noticed that this function is incremental, which means that even if the user does not hold a PLP Token (PLP.balanceOf(account) is 0), the function will still return the deposit rewards stored in user.token0Rewards.
Therefore, for the entire contract, we found two important logical flaws:
- The user’s deposit rewards are recorded in user.token0Rewards and user.token1Rewards, and are not bound to any PLP Token or other things in any form.
- The collectFees function used to retrieve the deposit revenue only depends on the user.token0Rewards and user.token1Rewards status of the account. Even if the user does not hold the PLP Token, the corresponding deposit reward can still be withdrawn.
We imagine an attack process:
- The attacker deposits a certain amount of liquidity into the machine gun pool and obtains a part of PLP Token.
- The attacker calls collectFees(0, 0), the latter will update the attacker’s deposit reward, that is, the value of the state variable user.token0Rewards, but it does not actually get back the deposit reward.
- The attacker transfers the PLP Token to other contracts under his control, and then calls collectFees(0, 0) to update the state variable user.token0Rewards. That is to say, by continuously circulating PLP Tokens and calling collectFees(0, 0), the attacker copied the deposit rewards corresponding to these PLP Tokens.
- Finally, the attacker calls the collectFees function from the above addresses to retrieve the real reward. At this time, although there is no PLP Token in these accounts, because the account is not updated in user.token0Rewards, the attacker is able to withdraw multiple rewards.
Using a real-life example to describe this attack is equivalent to depositing money in the bank. The bank gave me a deposit certificate , but this certificate has no anti-counterfeiting measures and is not tied to me. I copied several copies of the certificate and sent it to me . To different people , each of them relied on this certificate to withdraw interest from the bank.
Attack process analysis
Through the above code analysis, we found a loophole in Popsicle Finance’s machine gun pool implementation. Below we conduct an in-depth analysis of the attack transaction to see how the attacker exploited this vulnerability.
The overall process of the attacker is as follows:
- The attacker created three trading contracts. One of them is used to initiate an attack transaction, and the other two are used to receive PLP Token and call the collectFees function of the Popsicle Finance machine gun pool to retrieve the deposit reward.
- Lending a large amount of liquidity from AAVE through flash loans. The attackers selected multiple machine gun pools under the Popsicle Finance project and loaned six liquidities corresponding to these machine gun pools to AAVE.
- Carry out the Deposit-Withdraw-CollectFees cycle. The attacker performed a total of 8 cycles , attacking multiple machine gun pools under the Popsicle Finance project, and withdrawing a large amount of liquidity.
- Return the flash loan to AAVE and launder the profit through Tornado Cash.
This attack transaction is mainly composed of several Deposit-Withdraw-CollectFees cycles, and the schematic diagram of each cycle is shown in the figure above. According to our analysis, the logic is as follows:
- The attacker first deposits the liquidity borrowed from the flash loan into the machine gun pool and obtains a certain amount of PLP Token.
- The attacker transfers the PLP Token to attack contract 2.
- Attack contract 2 calls the collectFees(0, 0) function of the machine gun pool to set the user.token0Rewards and user.token1Rewards states corresponding to contract 2.
- Attack contract 2 transfers the PLP Token to attack contract 3.
- Similar to the operation of attack contract 2, attack contract 3 calls the collectFees(0, 0) function of the machine gun pool to set the user.token0Rewards and user.token1Rewards status corresponding to contract 2.
- Attack contract 2 converts the PLP Token back to the attack contract, and the latter calls the withdraw function Burn of the machine gun pool to drop the PLP Token and retrieve liquidity.
- Attack contract 2 and attack contract 3 call the collectFees function and retrieve the deposit reward with a false tokenRewards status.
According to the transaction call diagram given by our Ethereum transaction tracking visualization system , as follows, some of the important transactions are marked with red letters:
This attack made a total of profits: 2.56k WETH, 96.2 WBTC, 160k DAI, 5.39m USDC, 4.98m USDT, 10.5k UNI, and the total profit exceeded 20,000,000 US dollars.
After this attack, the attacker first used Uniswap and WETH to exchange all other tokens obtained by the attack into ETH, and then used Tornado.Cash multiple times to wash ETH.
Disclaimer: As a blockchain information platform, the articles published on this site only represent the author’s personal views, and have nothing to do with the position of ChainNews. The information, opinions, etc. in the article are for reference only, and are not intended as or regarded as actual investment advice.
Blockcast.cc does not endorse any content or product on this page. While we aim at providing you all important information that we could obtain, readers should do their own research before taking any actions related to the company and carry full responsibility for their decisions, nor can this article be considered as investment advice or recommendations. Every investment and trading move involves risk, you should conduct your own research when making a decision.