Tl;dr: The Unit 0x team, a Coinbase team dedicated to investigating and preventing attacks in the broader crypto ecosystem, investigates the Euler Finance compromise which serves as a great case-study in both general risks to lending protocols as well as incident handling applicable to other DeFi projects. In this two part analysis we will first perform an in-depth analysis of the smart contract vulnerability caused by a new feature introduced after the audit. In part two of the series we will explore a whirlwind of on-chain activity and communication leading up to the return of stolen assets as well as critical lessons learned from the incident handling.
While this compromise does not directly target Coinbase Exchange custodial services or systems, we strongly believe that it is important to address any attacks or vulnerabilities within the crypto community as a whole and hope the information in the blog will help strengthen and inform the entire ecosystem.
On March 13, 2023 Euler Finance, a lending protocol on Ethereum blockchain, suffered from one of the most devastating DeFi hacks with more than $197m stolen by a single actor in about 15 minutes. This compromise breaks the pattern of largest compromises in recent history mostly involving bridges such as the $586m BSC Token Hub, $186m Nomad Bridge and other hacks. The attacker skillfully exploited a vulnerability as well as unique borrowing and liquidation mechanisms to completely drain DAI, USDC, WETH, and other token pools. First, the attackers used the recursive lending and borrowing function to increase their collateral and debt positions. Next, they then donated 1/4 of their collateral to trigger liquidation. Finally, Euler platform's liquidator market structure allowed the attackers to liquidate themselves without upfront funds and to receive maximum profit for clearing the position.
While the attackers were skilled at identifying and exploiting vulnerabilities and mechanisms in the system, their careless and sporadic behavior during the extraction and over the following days left a detailed trail on the blockchain giving Euler Finance and volunteer investigators clues as to their true identity.
This coupled with relentless on-chain and off-chain negotiations and infighting within the attacker group resulted in the attackers returning the vast majority of the stolen funds. We will dive into the details of the negotiation and on-chain activity post-exploitation in the second part of the analysis.
While we’re relieved that most of the assets were returned, it’s important to fully understand how Euler was compromised to help protect the broader crypto economy. In this analysis, we will retrace the events that took place on the fateful day in March, including a review of the vulnerability, reverse engineered exploit, and the eventual recovery process. We hope this will educate our colleagues not only on the nature of the vulnerability but also help improve incident response capabilities when dealing with similar compromises.
Before we dive into the vulnerability and the exploit let’s discuss a few core concepts about the Euler protocol that were used in the attack.
Euler Finance is a lending /borrowing dapp on Ethereum. Users can lend tokens such as USDC, and cbETH - and any other tokens of the user’s choice - in exchange for yield bearing LP tokens. These LP tokens could be converted back to the originally deposited funds with interest and governance tokens (EUL).
Some LP tokens could also be used as collateral to borrow other assets on the Euler platform as well. Read the whitepaper here for more information.
Please note that the following discussion was purposefully kept as high level as possible and certain aspects of Euler’s operations were intentionally excluded in order to present a more streamlined discussion of the events.
eTokens and dTokens
Euler allows users to deposit (read: lend) assets to their protocol in exchange for LP tokens. These tokens are referred to as eTokens. eTokens are interest bearing tokens that you can redeem for the underlying asset. For example, if a user supplies DAI to Euler, they in return receive eDAI. This eDAI is calculated at an exchange rate in order to account for the underlying supplied DAI plus whatever interest earned on the token.
On the flipside, if a user wants to borrow assets, assets used as collateral are locked up and an equivalent eToken is minted, similar to AAVE’s aTokens. For example, a user could supply some DAI as collateral and then borrow DAI. When the user borrows this DAI, they are also issued a dToken called dDAI for the exact same value of the amount borrowed.
As a rule of thumb eTokens represent the amount of tokens a user supplied to Euler and dTokens represent the amount of the tokens a user borrowed from Euler.
Health Scores
Euler's health factor is the ratio of a borrower's borrowed assets to the assets they supplied.
For those involved in the defi space, Health Factor is the opposite of LTV.
Let's say a user supplies 1 WETH to Euler and that risk adjusted WETH is currently valued at $880 and then borrows a risk adjusted value of $500 worth of DAI. This would equal a health factor 1.76. Over the following days, that risk adjusted value of 1ETH drops to $400. But the $500 DAI is still worth $500. This now leads to a health factor of 0.8 - meaning that the borrower is undercollateralized (i.e. can't pay back the amount they borrowed because they only have $400 worth of WETH) and is underwater.
Euler requires that users have a minimum 1 health factor (HF). If they fall below a HF of 1, they get liquidated. You can read more about HFs here.
Self-collateralized loans (Mints)
Another unique feature of the Euler Protocol is a recursive borrowing functionality which allows one to leverage up their collateral using an eToken mint function. The feature allows for greater gas efficiency by using Euler’s built-in lending protocol without the need for external flash loan providers.
The maximum mint amount is bound by SCF (Self-Collateral Factor) 0.95 and a token specific BF (Borrow Factor) using a formula below:
A Borrow Factor (BF) is a maximum % that can be borrowed of an asset. It varies depending on the asset’s stability on Euler.
For example, less volatile assets like USDC have a high BF of 0.94. If a user wanted to borrow 1k USDC. They would have to consider USDC’s BF according to Euler.
In this case, the user would have to supply well over $1,634 worth of collateral to borrow 1k USDC. You can read more about BFs here.
Back to mint() function. Let’s illustrate a minting a token with a fixed SCF of 0.95, a sample BF of 0.94 (USDC) and a 1,000,000 deposit:
This allows us to leverage the initial 1,000,000 deposit up to x18 while generating up to 18,800,000 in debt without accounting for conversion rates. A more complex example takes into account existing debt and other self collateralization variables which you can learn about here.
Liquidations
Loans with health scores under 1 can be liquidated by 3rd party liquidators. In the event of a liquidation - Euler will liquidate e- and dTokens to get a borrower back to a health score of 1.25. The eTokens are given to liquidators at a discounted rate in order to incentivize them to buy up the bad debt.
Euler establishes a market where liquidators are incentivized with increasing discounts on liquidated collateral. The discount received on liquidated collateral is capped at 20%, else the borrower is at risk of getting liquidated over and over.
Let’s illustrate the liquidation mechanism using a simple example in which soft liquidation does not occur and position is entirely underwater, such was the case with the exploit. You can read more on Euler’s unique soft liquidations here.
Say a user supplies 2 WETH to Euler and receives 2 eWETH in return. ETH is valued at the time of deposit at $3k. They then use this as collateral to borrow 4k USDC and in the process mint 4k dUSDC. Their health rate is now at 1.4:
In order to make sure we don’t immediately get liquidated Instead, we chose to conservatively only borrow $4k worth of USDC.
Now the price of ETH suddenly falls to $1.8k, making their collateral only worth $3.6k. And since the user took out a $4k USDC loan.
In this case, the user (or borrower) would likely get liquidated.
From a liquidators’ perspective, if they were to acquire this position, they would only do so if it were profitable. In order to be profitable, the value of eTokens acquired at a discount must exceed the amount the liquidator pays for borrowers' debt in dTokens plus fees. The collateral they get from the eTokens can be resold on the open market at a non-discounted price.
The base discount rate is calculated using the following formula:
The maximum discount in this case would be 16% (1-0.84). This position will be sold at a maximal 16% discount rate - meaning that the liquidator can potentially obtain the underlying collateral for 16% less than the debt that they have to repay.
The liquidator would have to pay ~$3.02k USDC worth of outstanding debt in order to attain all of the underlying collateral. After spending $3.02k the liquidator would receive back $3.6k - thus making $580 in profit.
But what happens with the other 980 USDC debt that wasn’t repaid? Normally, a combination of collateral and debt factors provide a sufficient buffer to cover the outstanding debt and liquidator profit. In extreme examples where the loan is severely under-water, such as this one, the rest of the dTokens continue to be held by the platform as bad debt.
Liquidity deferrals aka flash liquidation
With Euler’s dTokens and eTokens involved in the liquidation auctions, liquidators don’t necessarily need upfront capital (such as a flashloans) in order to perform a liquidation.
In essence, a liquidation on Euler is just the repayment of up to 80% of the debt in order to receive 100% of the collateral. What Euler does in the instance of a liquidity deferral is that the liquidator assume both the e- and dTokens of the violator, withdraws enough eTokens in the underlying collateral asset to repay the debt, swap the underlying collateral asset withdrawn for the asset that needs to be repaid, and repay the outstanding debt balance and keep the profit (in this case up to 20%).
This is also referred to as “flash liquidation” as liquidators complete all of the required steps in one transaction. You can read more about flash liquidations here.
On July 5, 2022 Euler introduced a new donateToReserves method originally intended as a way to clean dust from users’ accounts. Below is the commit message on Github introducing the method:
The vulnerability was introduced to the implementation of donateToReserves in the master branch by omitting the checkLiquidity(from) verification.
So, why is checkLiquidity so important? Euler has a feature that allows users to defer their liquidity checks. Many operations can be performed, and the liquidity is checked only once at the very end. This is useful to perform rebalancing, flash loans, etc. Below is the implementation of this check in BaseLogic.sol.
Protocols rely on invariants for their security, which are system properties expected to be always true. In the case of Euler the invariant is that no protocol action should be able to result in an account where risk adjusted liability is greater than risk adjusted assets. So what happens when this check isn’t performed and the invariant is broken? The ratio of liabilities to assets increases and the protocol becomes insolvent!
The ability to effectively burn arbitrary amounts of the collateral, allows one to force their loans deep into unhealthy territory while providing maximum discounts for willing liquidators. Coupled with the ability to leverage collateral up to 19x spelled a disaster waiting to happen for the protocol.
The attack was initiated on March 13, 2023 at 8:50:23 UTC with the deployment of the exploit contract at 0x036cec. You can find a complete reverse engineered source code for the contract in Appendix A including helper violator and liquidator contracts.
The exploit contract operates in 8 stages which involve generating a leveraged and massively under-collateralized loan to take advantage of Euler’s liquidation rewards mechanism. The exploit contract includes hard-coded values X for each of the targeted lending pools designed to optimize the attack and govern operating parameters at each stage of the exploit. Table below describes all of the parameters:
It’s interesting that the attacker did not target USDT in part due to token’s non-standard ERC-20 interface requiring additional work.
Only the value X is hard-coded with the rest calculated as follows:
The MinProfit value approximates the minimum profit from liquidation at a 20% discount minus the fees. The value is used as a guidance to repeat the process if the entire pool balance can’t be emptied in a single transaction.
To illustrate how these values are used in an exploit at each stage of the exploit, let’s analyze a sample transaction 0x465a678014 which was used to steal 34m USDC:
Stage 1: Flashloan
The attacker borrowed 210m USDC from AAVE. The amount was calculated to serve as a sufficient collateral to mint a large enough self-collateralized loan to yield profit for the attacker. A more optimized approach could have been used requiring only a 75m flashloan with X factor of 25m; however, the attacker reused a more generic formula applicable to all targeted protocols.
After receiving the flashloan, the exploit contract deployed additional violator and liquidator helper contracts. The violator contract was funded with the entire 210m USDC flashloan.
Stage 2: Euler deposit / collateral
The violator contract deposited 140m USDC or 2/3 of the flashloan into Euler, thereby creating the corresponding eUSDC collateral. As discussed above eTokens are interest bearing tokens. They’re provided at a rate of ~1.028460824. So, while 140m USDC were deposited, only 136m eUSDC were minted.
The underlying USDC was then stored in the pool. Increasing the value of underlying USDC in the pool from 34m USDC to 174m USDC.
Stage 3: First Mint
Next, the violator contract requested to mint 1.4b USDC which was 10x of the existing eUSDC collateral. This increased their eTokens by 1.36b eUSDC and their dTokens by 140b dUSDC. It received a slightly smaller amount of eUSDC due to the aforementioned conversion rate while also receiving the exact number of dUSDC since it is not interest bearing and is not subject to conversion rates.
Combined with the earlier deposits, the attacker held 1.497b eUSDC and 140b dUSDC in their address.
Stage 4: Payoff + Second Mint
The attacker continued leveraging up their debt position by repaying 70m USDC or ⅓ of the flashloan to the protocol. This increased the pool’s balance by another 70m USDC, making the pool total to 244m USDC. This also decreases the attacker dToken balance by 70m dUSDC allowing them to perform another mint of 1.4b USDC.
At this point, the attacker held 2.858b eUSDC and 2.730b dUSDC.
Stage 5: Donation
In order to force themselves under-water the attacker donated 700m eUSDC to the Euler reserves effectively burning collateral from their account. At this point, the attacker held 2.158b eUSDC and 2.73b dUSDC.
This threw off the e- to dToken ratio, so that the health factor of the account is < 0.8, meaning that its collateral to loan value was underwater. It also meant that Euler automatically offered the highest incentive to get this bad loan off their books and offer a liquidator a 20% discount for it, if they purchased the underlying collateral.
Stage 6: Liquidation + Payoff
The violator contract transferred execution to the liquidator helper contract which proceeded to liquidate the bad loan. The liquidator contract accepted the violator’s e- and dToken positions and upon clearing out the position only had to repay 1.776b USDC in debt in order to receive 2.22b USDC in collateral, leaving them with a potential 400m+ underlying USDC profit.
The repay amount is calculated using the discount rate, which is 80% of the collateral. This means that the liquidator only needs to repay 1.776b USDC in order to receive 2.22b USDC. The max discount was immediately effective because unlike a TWAP oracle updating the asset price over multiple blocks, the ratio of eTokens to dTokens diverged so dramatically bringing the health score deep into liquidation territory, thereby applying the full discount. Please see here for more details on dynamic discounts on Euler.
The profit the liquidator made off with was about 20% of the total underlying collateral they received, about ~400m USDC. However, recall that only 244m underlying USDC was in the pool at the time. So, the protocol could only pay out 244m USDC to the liquidator.
Stage 7: Withdrawal + Flashloan Payoff
The Euler USDC pool has 244m USDC which includes the original 34m USDC and 210m USDC from the flashloan deposit. Since the liquidation profit covers that entire amount which allowed the attacker to withdraw the entire 244m USDC.
After repaying the 210m USDC flashloan plus fees to the AAVE protocol, the attacker gained 34,224,863 USDC in profit.
The withdrawal stage included an optional parameter minProfit which was used to repeat the entire operation in case there was any balance still remaining in the pool as was the case with wstETH.
The above steps are illustrated for the USDC and other tokens in the PoC demo below
Now that we have a complete understanding of the vulnerability and the exploit, we can dive into attackers’ on- and off-chain activity when we come back in Part 2 of the report.
Complete exploit PoC and instructions to execute it in a controlled environment are available here.