The story of the $130 million theft of Slow Fog Analysis Cream: maliciously manipulating prices by exploiting lending pool vulnerabilities
Author: Kong, Slow Mist Security Team
According to Slow Mist, on October 27, 2021, Cream Finance was attacked again, resulting in a loss of approximately $130 million. The Slow Mist Security Team intervened immediately for analysis and shares a brief analysis as follows.
Core of the Attack
The core of this attack lies in exploiting the flaws in the collateral price acquisition of the Cream lending pool, maliciously manipulating and inflating the price of its collateral, allowing the attacker to borrow more tokens from the Cream lending pool.
Attack Details
First, the attacker borrowed 500 million DAI through a flash loan from DssFlash, and then collateralized the borrowed 500 million DAI in the yearn yDAI pool to obtain approximately 450 million yDAI tokens.
Subsequently, the attacker added the obtained yDAI tokens as single-coin liquidity in the Curve yDAI/yUSDC/yUSDT/yTUSD pool to obtain corresponding liquidity tokens. The attacker then collateralized the obtained tokens in the yvWBTC pool to receive yUSD tokens, preparing for subsequent collateralization in the Cream crYUSD lending pool.
After that, the attacker began to collateralize the obtained yUSD tokens in the Cream crYUSD lending pool. To expand the collateral scale, the attacker borrowed approximately 524,000 WETH through a flash loan from AAVE and collateralized it in the Cream crETH pool.
By collateralizing a large amount of ETH in the crETH pool, the attacker ensured sufficient borrowing capacity to withdraw all yUSD from the crYUSD pool and re-collateralize it in the crYUSD pool. Subsequently, the attacker leveraged the yUSD collateral in the crYUSD pool through circular lending to expand their collateral scale, preparing for price manipulation for profit.
Next, to obtain yDAI/yUSDC/yUSDT/yTUSD 4Pool tokens for price manipulation, the attacker exchanged approximately 1,873 ETH for about 7.45 million USDC from Uniswap V3 and then exchanged it for approximately 3.38 million DUSD tokens through Curve 3Pool.
The attacker then redeemed yDAI/yUSDC/yUSDT/yTUSD 4Pool tokens from YVaultPeak using the obtained DUSD tokens and used these tokens to withdraw yDAI/yUSDC/yUSDT/yTUSD tokens from the yUSD (yvWBTC) pool.
Subsequently, the attacker began the key operation of this attack, directly transferring approximately 8.43 million yDAI/yUSDC/yUSDT/yTUSD tokens back to the yUSD pool. Since these tokens were not collateralized through normal operations, the 8.43 million yDAI/yUSDC/yUSDT/yTUSD tokens were not individually accounted for but were directly distributed to the holders of the yDAI/yUSDC/yUSDT/yTUSD tokens, effectively inflating the price of their shares.
In the crToken, due to the malicious inflation of collateral prices, the large amount of yUSD collateralized by the attacker allowed them to borrow more funds, ultimately emptying all 15 other pools of Cream. Next, we will follow up on the specific lending logic in Cream's crToken lending pool.
From the cToken contract, we can see that the main lending checks are in the borrowAllowed function:
Following the borrowAllowed function, we can see that on line 427, it checks the total asset value of all cTokens corresponding to that account in real-time based on the getHypotheticalAccountLiquidityInternal function and compares the asset value of the cTokens with the value of the borrowed tokens to determine whether the user can continue to borrow.
Following the getHypotheticalAccountLiquidityInternal function, we find that the value of the collateral is obtained from line 886's oracle.getUnderlyingPrice.
Following the oracle's getUnderlyingPrice function, we can easily see that it retrieves the price through the token's getYvTokenPrice function on line 150.
Continuing to follow the getYvTokenPrice function, since yvTokenInfo.version is V2, it retrieves the price through the yVault's pricePerShare function.
Following the pricePerShare, we find that it directly returns _shareValue as the price, and _shareValue is calculated by dividing _totalAssets by the total number of shares in the contract (self.totalSupply) to determine the price of a single share. Therefore, the attacker only needs to manipulate _totalAssets to inflate it, thereby increasing the price of a single share and allowing the attacker to borrow more other tokens.
We can check how _totalAssets is obtained. From line 772, we can clearly see that _totalAssets is directly derived from the current contract's yDAI/yUSDC/yUSDT/yTUSD token quantities, plus the amount of assets collateralized in the strategy pool. Therefore, the attacker can inflate the share price by directly transferring yDAI/yUSDC/yUSDT/yTUSD tokens into the yUSD contract.
Through Ethtx.info, we can clearly see the changes in pricePerShare before and after:
Finally, after emptying the other pools, the attacker repaid the flash loan and exited with profits.
Conclusion
This attack is a typical case of price manipulation using flash loans. Since the Cream lending pool directly used its pricePerShare interface to obtain the yUSD pool share price, and this interface calculates the price of a single share by adding the contract's collateral balance to the amount of assets in the strategy pool and dividing by the total shares, users can easily inflate the price of a single share by directly transferring collateral into yUSD, ultimately allowing the Cream lending pool to lend out more funds.
Appendix: Review of the Previous Two Attacks on Cream Finance