Analysis of Warp Finance hacked incident

SlowMist
9 min readJan 7, 2021

Background

On December 18, 2020, according to the SlowMist Zone Intelligence DeFi project Warp Finance suffered a flash loan attack. The following is a detailed analysis of the entire attack process by the SlowMist security team.

Attack analysis

  1. Through the attack transaction, it can be seen that the attacker borrowed about 2.9 million DAI and 345,000 WETH through Uniswap and dydx lightning loans:
    https://etherscan.io/tx/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090

2. Next, the attacker first used the borrowed WETH and DAI to add liquidity to Uniswap’s WETH-DAI trading pair, and obtained approximately 94,000 LP Tokens to prepare for the subsequent mortgage of LP in Warp.

3. The attacker then mortgages the previously obtained LP Token through the provideCollateral function of the WarpVaultLP contract of the Warp project.

function provideCollateral(uint256 _amount) public {        require(            LPtoken.allowance(msg.sender, address(this)) >= _amount,            "Vault must have enough allowance."        );        require(            LPtoken.balanceOf(msg.sender) >= _amount,            "Must have enough LP to provide"        );        LPtoken.transferFrom(msg.sender, address(this), _amount);        collateralizedLP[msg.sender] = collateralizedLP[msg.sender].add(            _amount        );
emit CollateralProvided(msg.sender, _amount); }

From the above code, we can see that the contract records the number of LP Token mortgaged by the attacker through collateralizedLP[msg.sender].

4. Afterwards, the attacker’s operation is the most critical step of this attack: the attacker exchanges approximately 340,000 WETH into approximately 47.62 million DAI through Uniswap’s WETH-DAI transaction pair. At this time, there are about remaining in the WETH-DAI pool 436,000 WETH and 13.288 million DAI, while before that, there were approximately 95,000 WETH and 60.91 million DAI in the pool. We can find that the amount of WETH in the pool has greatly increased after the attacker exchanged it. Next, we use Warp’s specific code to analyze why the attacker did this.

5. According to the official introduction, the Warp Finance project allows users to lend stablecoins such as DAI, USDC and USDT by mortgage LP Token. Next, let’s take a look at how Warp calculates the number of stablecoins that users can lend:

  1. In Warp, users can borrow stablecoins through the borrowSC function of the WarpControl contract:
function borrowSC(address _StableCoin, uint256 _amount) public {     uint256 borrowedTotalInUSDC = getTotalBorrowedValue(msg.sender);     uint256 borrowLimitInUSDC = getBorrowLimit(msg.sender);     uint256 borrowAmountAllowedInUSDC = borrowLimitInUSDC.sub(         borrowedTotalInUSDC     );
uint256 borrowAmountInUSDC = getPriceOfToken(_StableCoin, _amount);
//require the amount being borrowed is less than or equal to the amount they are aloud to borrow require( borrowAmountAllowedInUSDC >= borrowAmountInUSDC, "Borrowing more than allowed" );
//retreive stablecoin vault address being borrowed from and instantiate it WarpVaultSCI WV = WarpVaultSCI(instanceSCTracker[_StableCoin]); //call _borrow function on the stablecoin warp vault WV._borrow(_amount, msg.sender); emit NewBorrow(msg.sender, _StableCoin, _amount); }

2) From lines 3 and 4 of the code above, we can find that the WarpControl contract uses the getBorrowLimit function to obtain a stable amount that users can lend. Next, let’s look at the getBorrowLimit function in detail:

function getBorrowLimit(address _account) public returns (uint256) {     uint256 availibleCollateralValue = getTotalAvailableCollateralValue(         _account     );
return calcBorrowLimit(availibleCollateralValue); }

3) Through the analysis, we can find that the getBorrowLimit function first calculates the availableCollateralValue through the getTotalAvailableCollateralValue function, and then passes the calculation result as a parameter to the calcBorrowLimit function, and finally returns the specific amount. Let’s analyze the getTotalAvailableCollateralValue function first:

function getTotalAvailableCollateralValue(address _account)     public     returns (uint256){     //get the number of LP vaults the platform has     uint256 numVaults = lpVaults.length;     //initialize the totalCollateral variable to zero     uint256 totalCollateral = 0;     //loop through each lp wapr vault     for (uint256 i = 0; i < numVaults; ++i) {         //instantiate warp vault at that position         WarpVaultLPI vault = WarpVaultLPI(lpVaults[i]);         //retreive the address of its asset         address asset = vault.getAssetAdd();         //retrieve USD price of this asset         uint256 assetPrice = Oracle.getUnderlyingPrice(asset);
uint256 accountCollateral = vault.collateralOfAccount(_account); //emit DebugValues(accountCollateral, assetPrice);
//multiply the amount of collateral by the asset price and return it uint256 accountAssetsValue = accountCollateral.mul(assetPrice); //add value to total collateral totalCollateral = totalCollateral.add(accountAssetsValue); } //return total USDC value of all collateral return totalCollateral.div(1e18); }

4) A detailed analysis of the getTotalAvailableCollateralValue function, we can see that this function obtains the sum of the loanable quantities of DAI, USDT, and USDC through a for loop. We can find that in the logic of the for loop, the price of LP is obtained through Oracle.getUnderlyingPrice(asset), and the number of LP pledged by the user is obtained through vault.collateralOfAccount(_account), and finally the two are multiplied to obtain the loanable quantity.

Here we can guess: since the amount of mortgage is certain, there must be a problem with the price calculation. Next, we will analyze the process of price acquisition in detail:

function getUnderlyingPrice(address _lpToken) public returns (uint256) {     address[] memory oracleAdds = LPAssetTracker[_lpToken];     //retreives the oracle contract addresses for each asset that makes up a LP     UniswapLPOracleInstance oracle1 = UniswapLPOracleInstance(         oracleAdds[0]     );     UniswapLPOracleInstance oracle2 = UniswapLPOracleInstance(         oracleAdds[1]     );
(uint256 reserveA, uint256 reserveB) = UniswapV2Library.getReserves( factory, instanceTracker[oracleAdds[0]], instanceTracker[oracleAdds[1]] );
uint256 priceAsset1 = oracle1.consult( //SlowMist// instanceTracker[oracleAdds[0]], reserveA ); uint256 priceAsset2 = oracle2.consult( instanceTracker[oracleAdds[1]], reserveB );
// Get the total supply of the pool IERC20 lpToken = IERC20(_lpToken); uint256 totalSupplyOfLP = lpToken.totalSupply(); //SlowMist//
return _calculatePriceOfLP( totalSupplyOfLP, priceAsset1, priceAsset2, lpToken.decimals() ); //return USDC price of the pool divided by totalSupply of its LPs to get price //of one LP }

5) By analyzing the getUnderlyingPrice function, we can find that it first uses UniswapV2Library.getReserves to obtain the real-time quantity of two tokens (such as WETH, DAI) in the Uniswap LP pool. The LP pool here can be obtained in turn through the WarpControl contract (the following only List the first one), which are WETH-DAI, WETH-WBTC, WETH-USDT, WETH-USDC.

6) After obtaining the number of two tokens (such as WETH, DAI) in the LP pool, obtain the prices of these two tokens separately through oracle1.consult(), and the price here is obtained by Uniswap oracle Implementation method, while Uniswap oracle price acquisition is time-weighted, that is, the delayed price feed method. The price acquired by the user is the price of the previous block, which cannot be manipulated. Therefore, we can know that the token price gets the correct price.

7) Next, the specific price of LP Token will be calculated through the _calculatePriceOfLP function:

function _calculatePriceOfLP(     uint256 supply,     uint256 value0,     uint256 value1,     uint8 supplyDecimals ) public pure returns (uint256) {     uint256 totalValue = value0 + value1;     uint16 shiftAmount = supplyDecimals;     uint256 valueShifted = totalValue * uint256(10)**shiftAmount;     uint256 supplyShifted = supply;     uint256 valuePerSupply = valueShifted / supplyShifted;
return valuePerSupply; }

Through the above code, we can know how the LP price is obtained. Take the WETH-DAI pool as an example: it is calculated by multiplying the number of WETH in the pool by the price of WETH plus the number of DAI in the pool multiplying the price of DAI and finally dividing by the total of the pool The quantity of LP Token can get the price of a single LP Token. The specific calculation formula is as follows:

Through the above analysis, we can know that the price of WETH and the price of DAI are normal and cannot be manipulated maliciously. Therefore, we can boldly guess: the attacker can exchange a huge amount of WETH into the WETH-DAI pool in exchange for DAI. At this time, the pool The amount of WETH in the middle will increase greatly, and due to the existence of slippage, this huge exchange operation will inevitably lose a large part of WETH. So let’s look at the calculation method of the unit price of LP above. Due to the huge increase in the number of WETH, the amount of WETH in the pool * WETH price + the number of DAI in the pool * DAI price will be much greater than before the huge amount exchange, which is the pool. The total value of it has greatly increased. Therefore, the unit price of LP has also increased, so the attacker can borrow more stablecoins through the LP Token pledged.

Analysis and verification

We can use Ethtx.info to verify whether our guess is correct:
https://ethtx.info/mainnet/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090

1. Through the analysis of point 4 above, we can know that the attacker exchanged about 340,000 WETH into about 47.62 million DAI through Uniswap’s WETH-DAI trading pair. At this time, there are about 436,000 pieces left in the WETH-DAI pool. WETH and 13.288 million DAI, and before that, there were approximately 95,000 WETH and 60.91 million DAI in the pool.

2. We can find on Ethtx.info that the unit price of the LP Token in the WETH-DAI pool before exchange is 58815427.

After the massive exchange, the unit price of LP Token in the WETH-DAI pool is 135470392.

We can see that the total value of the pool after the exchange has almost doubled due to the increase in the number of WETH, so a single LP Token can lend more stablecoins in Warp.

3. Next, as we guessed, the attacker used the borrowSC function of the WarpControl contract to lend DAI and USDC respectively after raising the price of LP Token.

4. Finally, in the WETH-DAI pool of Uniwsap, DAI was always returned, and 340,000 WETH was retaken to complete the attack operation. Finally, you only need to return the flash loan step by step to make a profit.

The complete attack process

1. The attacker deploys an attack contract and lends DAI and WETH through dydx and Uniswap lightning loans.

2. The attacker takes out a small part of DAI and WETH to add liquidity to Uniswap’s WETH-DAI pool and obtains LP Token.

3. The attacker uses the LP Token obtained by adding liquidity to mortgage it to Warp Finance in preparation for lending stable currency.

4. The attacker uses a huge amount of WETH to convert it into DAI in Uniswap to increase the total value of the WETH-DAI pool, making the unit price of LP Token in Warp Finance higher. (Note that the prices of WETH and DAI here are correct and have not been manipulated. What is manipulated is the amount of WETH, which increases the total value of the pool by increasing the amount of WETH).

5. As the unit price of LP Token becomes higher, the LP Token mortgaged by the attacker can lend more stable coins to make profits.

Summary

The essence of this attack is to profit by manipulating the unit price of LP Token to obtain more stable currency loans. This is because the price of LP Token in Warp Finance is obtained by dividing the total value of the LP pool by the total number of LP Tokens. Although the token price is correct, the number of tokens can be manipulated, so the unit price of LP is Can be manipulated, which forms a necessary condition for attack. In the end, the project party lost about 8 million U.S. dollars, but the LP pledged by the attacker also remained in the Vault. If this part of the pledged LP can be subsequently liquidated, it can make up for the loss of the project party to a certain extent.

Related reference links are as follows:
Introduction to Uniswap oracle machine implementation:
https://uniswap.org/docs/v2/core-concepts/oracles/

Attack transactions in this analysis:
https://etherscan.io/tx/0x8bb8dc5c7c830bac85fa48acad2505e9300a91c3ff239c9517d0cae33b595090

About us

SlowMist Technology is a company focused on blockchain ecological security. It was founded in January 2018 and is headquartered in Xiamen. It was founded by a team with more than ten years of front-line network security attack-defense experiences, and the team members have created the security project with world-class influence. SlowMist Technology is already a top international blockchain security company, served many global well-known projects mainly through “the security solution that integrated the threat discovery and threat defense while tailored to local conditions,” including: cryptocurrency exchanges (such as Huobi, OKEx, Binance, etc.), cryptocurrency wallets (such as imToken, RenrenBit, MYKEY, etc.), smart contracts (such as TrueUSD, HUSD, OKUSD, etc.), DeFi projects (such as : JUST, BlackHoleSwap, DeFiBox, etc.), the underlying public chain (such as EOS, OKChain, PlatON, etc.), there are nearly a thousand commercial customers, customers distributed in more than a dozen major countries and regions.

--

--

SlowMist

SlowMist is a Blockchain security firm established in 2018, providing services such as security audits, security consultants, red teaming, and more.