Hack Analysis: Cream Finance Oct 2021

Immunefi
Immunefi
Published in
11 min readNov 9, 2022

Introduction

Cream Finance experienced a major exploit on October 27, 2021, resulting in a loss of $130m in available liquidity.

In this article, we will be walking through and explaining the fundamental flaw that the protocol experienced before recreating our own proof of concept (PoC) that drains the protocol of millions in collateral using the same attack vector. You can find a working PoC for this attack here.

Along the way, we’ll be making use of crucial DeFi primitives across the space. This will include flash minting DAI from MakerDAO, flashloans from AAVE V2, and swapping heist funds for other tokens via UniswapV2.

This article was written by Hephyrius.eth, an Immunefi Whitehat Scholar.

Background

Let’s begin by delving into some background context about Cream Finance. Cream is a cross-chain lending protocol that has deployments on Ethereum, BNB, Avalanche, Fantom, Arbitrum, and Polygon. At the time of the attack, the protocol’s main deployment on Ethereum had over $130m in available liquidity.

As with all Compound forks, Cream allows users to borrow tokens against tokens they have deposited. Borrowing is done in an over-collateralized manner. For instance, a user may deposit $1m worth of ETH in order to borrow $750k of USDT. Lending protocols allow users to deposit a handful of tokens that the protocol has whitelisted beforehand. Each of these lending markets has its own parameters which dictate how much of a token can be supplied, how much can be borrowed against supplied tokens, the oracle that will dictate the underlying price, and whether there is a global deposit cap on supplying tokens.

A crucial element that helps lending protocols operate is the concept of account health. When an account supplies collateral to the protocol, the account increases in health. When borrowing occurs, health decreases. A fundamental element of this health system is the token price oracle. These oracles inform the protocol of the intrinsic value of the collateral and borrowed tokens, denoted in a single common value such as dollars or ETH. Account health is a number, where anything above 1 is a healthy account, and anything below is an account that can be liquidated.

There are a number of oracles that Compound forks may opt to use. A few of the common ones are:

  • Off-chain aggregators such as Band, Chain Link, and Tellor
  • Time Weighted Average Price (TWAP) based on AMM swaps
  • Constant price oracles — for instance, USDT always being $1

Alongside these common oracles, we can find hybrid oracles that mix one or more of the other oracle types. Some hybrid oracles may use token balances or exchange rates of vault contracts when calculating market value of a token.

Another crucial element in a lending protocol is liquidations and liquidation incentives. In order for a protocol to remain solvent, bad debts need to be settled before they are no longer over-collateralized, as in this situation, the protocol needs to absorb the debt. Usually, protocols solve this by offering collateral at a discount to liquidators who repay bad debt for an unhealthy account. Essentially, liquidators receive a discount on tokens as a reward for repaying bad debts and keeping the protocol solvent.

Root Cause

Now that we understand the basics of the protocol, we can explore the heist in depth. Let’s begin by looking at the attack transaction and looking at the steps that the attacker did during the heist:

  1. Flash mint $500m DAI from MakerDAO
  2. Deposit DAI into Yearn 4-Curve pool
  3. Deposit Yearn 4-Curve into Yearn yUSD vault
  4. Deposit yUSD into Cream yUSD market
  5. Borrow Over 500,000 Ether from AAVE v2
  6. Deposit Ether from another smart contract into Cream eth market — Account 2
  7. Borrow yUSD from Account 2 and deposit into Account 1 as collateral twice
  8. Borrow yUSD from Account 2 and send to Account 1
  9. Withdraw Yearn 4-Curve from yUSD vault
  10. Send $10m Yearn 4-Curve to yUSD vault
  11. Borrow all available liquidity using Account 1
  12. Swap stolen funds for DAI and WETH
  13. Withdraw DAI from Yearn 4-Curve
  14. Repay AAVE Eth flash loan
  15. Repay DAI flashmint
  16. Escape with profits

The majority of the damage can be narrowed down to two key reasons:

(I) An easily manipulatable hybrid oracle, which is manipulated at step 10
(II) Uncapped supplying of a token, which is manipulated in steps 7 and 8

The attacker’s action of supplying the same asset multiple times tricks the protocol into thinking that the address supplying the tokens is one that has a lot of collateral. As the attacker supplies the same $500m of tokens three times, they have a virtual balance of $1.5b in collateral, while having a further $2b in collateral and $1.5b in debts from supplying ETH and then borrowing yUSD against it from a secondary contract.

Snippet 1. CREAM oracle deriving price from pricePerShare

The magic of the attack happens when the attacker sends $10m of Yearn 4-Curve to the yUSD contract, as this contract is used as part of a hybrid oracle that dictates what the health system of Cream should value yUSD at.

Snippet 2. Price Per Share calculations given by yUSD

Let’s take a step back and decipher. Essentially, the yUSD contracts exchange rate is dictated by the ratio of 4-Curve tokens that the yUSD vault has access to, in relation to the total number of yUSD tokens in circulation. When the attacker sends Yearn 4-Curve tokens to the vault, they are doubling the value of 4-curve tokens that the yUSD token has access to, and therefore increasing the exchange rate that the vault reports. By proxy, the increase of the exchange rate of the vault also changes the value that the oracle reports the yUSD token as having.

The increased exchange rate has the following effects:

  • Account 1 now has $3 billion in collateral and $0 in borrowing
  • Account 2 now has $2 billion in collateral and $3 billion in debt and is now insolvent
  • The attacker has $2 billion in ETH to repay and $500m in DAI to repay
  • Therefore, the attacker has $500m in excess value that they can use to drain the protocol

The attacker now needs to borrow back the ETH they supplied in order to pay back their AAVE flash loan. This leaves $1 billion of over collateralized collateral on the table, which is sufficient enough for the attacker to borrow against in order to take the remaining $130m in liquid collateral that is still available within the protocol’s lending markets.

After exchanging the stolen collateral for enough DAI and ETH to repay flash borrowing, the attacker’s heist is complete. In this heist, the proceeds of the attack were split between two wallets, and some theories suggest that this may have been due to there being two blackhats involved in the exploit.

Recreation

We now have enough of a background and understand the flaw in the protocol well enough to create our own PoC contract that replicates this attack. We should begin by selecting an RPC provider that has archive access. For this demonstration, the free eth public rpc aggregator provided by Ankr should be sufficient. We will be using block number 6920000 as our fork block. This block was hours before the attack occurred.

Snippet 3: Contract B

Let’s begin by creating two contracts. One contract will be Account A, which is the smart contract responsible for the heist. The second will be Account B, which will be the source of the bad debt and price manipulation amplification. Code Snippets 3 & 4 show what basic contracts may look like. Ideally, we want to deploy Contract B when we deploy Contract A, by deploying an instance of B in the constructor of Contract A.

Snippet 4: Main Attack Contract

Getting Collateral

Snippet 5: DAI Flash Mint

The next action we need to do is flash minting DAI. In order to do this, we need to interact with the MakerDAO DssFlash contract which allows us to momentarily borrow tokens, on the condition that we repay along with a fee before the end of the transaction. In order to flash loan, we need to have a function within our contract called onFlashLoan() which can be called by the DssFlash contract, when we call its flashLoan() function. Snippet 5 covers this flash loan.

Snippet 6: Ether Flash Loan

Following this, we need to flash loan $2 billion worth of Ether from AAVE V2. Again, this flash loan is a temporary loan that must be paid before the end of the transaction, or else the transaction will fail. In order to flash loan from AAVE V2, we need to call the flashloan() function on the lending pool contract. AAVE will then call the executeOperation() function hook of our own contract, which passes control of the transaction back to Contract A.

Collateralization

We have $2.5 billion in tokens that we can play with when control is passed back to Contract A. What we need to do now is to lay some foundations by using the assets we hold, in order to create additional value.

Snippet 7: DAI to yUSD

Let’s begin by converting this $500m of DAI into yUSD. This is a two-step process. We first need to supply DAI as a liquidity provider to the Yearn 4-Curve pool on Curve. We then need to deposit the LP token we receive from Curve, into the Yearn yUSD pool in order to mint yUSD. Snippet 7 covers the practical steps involved in this process.

Snippet 8. Depositing yUSD as CREAM Collateral

Next, we need to deposit our yUSD into Cream so that we can borrow against it when we pull off our heist, and so that we can borrow it from Contract B. We do this in Snippet 8. We should note that the interfaces we need for Cream are inherited from Contract B rather than in the scope of Contract A, as both contracts need access to these interfaces.

Snippet 9. Unwrapping WETH and Passing control to Contract B

Once we supply our yUSD to Cream, we need to shift the context to Contract B and borrow this yUSD back from Cream. In order to do this, we must first deposit some ETH from Contract B into Cream, which we can use to borrow all yUSD in the market. We can then send this yUSD back to Contract A. Snippet 9 shows the aspects that Contact A needs to do, which is unwrapping the Wrapped Ether that we have and then sending it to Contract B. Whereas Snippet 10 shows the supply and borrowing steps that Contract B executes. We are supplying ETH as collateral, and borrowing all yUSD available, before sending it back to Contract A.

Snippet 10. Depositing Eth using Contact B and Borrowing yUSD Against It

The next step is one of the key steps in the process. Contract A needs to inflate its supplied yUSD balance as much as it possibly can, against Contract B’s $2 billion collateral. The method for doing this is, borrowing yUSD from Cream via Contract B, and depositing into Cream from Contract A, twice. This leads to a situation where the original $500m of yUSD supplied by Contract A, is now $1.5b yUSD. This is demonstrated in Snippet 11.

Snippet 11. Recursive Borrowing.

Contract A needs to borrow yUSD one further time so that the conversion from DAI to yUSD can be reversed, and to inflate the value of yUSD from the perspective of Cream. This is demonstrated in Snippet 12, where the Yearn 4-Curve tokens are withdrawn from the yUSD vault, which reduces the total supply of yUSD. Some of these withdrawn tokens are then sent back to the yUSD vault via a transfer.

Snippet 12. Inversion and Value Inflation

When we transfer Yearn 4-Curve tokens into the yUSD vault, we are increasing the pricePerShare of the vault, which increases the value that the hybrid oracle reports to the Cream protocol. In the case of snippet 12, we are making yUSD worth $2 instead of the expected $1. This oracle manipulation has a huge impact on Cream.

We now have a borrower, Contract B, with over $3 billion in debt. And, a supplier, Contract A, with over $3 billion in collateral. What this enables is a situation where Contract A can now borrow the remaining collateral in Cream, as Cream believes that this account is extremely over-collateralized. The first thing Contract A needs to do is borrow back all of the ETH that Contract B provided to Cream, along with any available liquidity that was already in this market. We need to do this in order to repay the flash loan that we took from AAVE.

Snippet 13. Borrowing all liquidity from target markets

We have $2 billion borrowed against our $3 billion in yUSD. We can now proceed to drain all of the protocol’s markets. Let’s begin by taking all of the stablecoins, and then take tokens from any other market we feel like pilfering. Snippet 12 shows the ETH and other borrowings.

Now, our $2.5 billion of flash borrows have grown to a value of $2.63 billion. In order to keep the difference between the two valuations, we need to convert our gains into DAI and ETH, and then repay the loans that we took from AAVE and MakerDAO. Luckily for us, the amount of ETH that we received from borrowing all ETH from Cream is more than enough to cover our AAVE flash loan. We can simply wrap the Ether back to WETH.

Snippet 14. Depositing Eth into WETH contract

In order to repay our DAI loan, we need to jump through a few hurdles. First, we need to withdraw DAI tokens from our remaining Yearn 4-curve token holdings. This should be well over $490m. When we transferred over $10m in these LP tokens to the yUSD pool, we essentially donated these tokens to holders of yUSD. This leaves $10m in DAI that we need to make up for.

Snippet 15. Withdrawing from 4-curve to DAI

We need to convert one of our heisted tokens into a stablecoin, and then convert this stablecoin into DAI. This is one of the lower slippage options that we have if we want to repay the loan without sacrificing too much profit. We can do this by first swapping portions of ETH to USDC and DAI via Uniswap V2. We can then swap any USDC we have to DAI, via Curves 3-pool.

Snippet 16. Swapping extra WETH to USDC/DAI and Swapping USDC to DAI

At this point, we should have enough ETH to pay back to AAVE and enough DAI to repay the flash mint. Any other tokens that we may hold in Contract B are pure heist profits. We are able to successfully exit our flash loans, which means that there is now no risk of transactions reverting, which means our heist was successful.

An attacker may then use a withdrawal method that they can call to send tokens from their attack contract to an EOA of their choice, where they can launder away the funds they obtained.

Tying everything together in Snippet 15 and 16, we have a viable attack that allows us to drain all of Cream Finance’s available liquidity using two fairly simple smart contracts. In total, Contract A is 330 lines of code, while Contract B is 50 lines of code.

Conclusion

The Cream Finance exploit shows us just how valuable oracles can be within DeFi. And by extension, how brutal the effects of manipulation can be. As a smart contract security researcher, it’s important to be able to understand how values are derived, such as those an oracle calculates.

The PoC demonstrated in this article is a simpler attack when compared to the attack that occurred. In the original attack, the attackers swapped yUSD for DeFi Dollars DUSD. This allowed them to reduce the total supply of yUSD, which allowed them to reduce the number of tokens they needed to donate in their transfer to the Yearn USD vault.

We propose that as an extension of your own PoC and to consolidate your learning, you increase your PoC profits by reducing the supply of yUSD, by withdrawing from the DUSD protocol.

Entire Contract A

--

--

Immunefi
Immunefi

Immunefi is the premier bug bounty platform for smart contracts, where hackers review code, disclose vulnerabilities, get paid, and make crypto safer.