Euler Finance Donation Hack
Euler Finance forgot to add a health check to the donation function
Description
The Euler Finance hack exploited two key protocol features:
-
Users could create artificial leverage by minting and depositing assets in the same transaction via
EToken::mint
-
Users could donate their balance to protocol reserves via
EToken::donateToReserves
without any health checks
The attack worked by:
- Creating an over-leveraged position by minting excess tokens
- Donating collateral to intentionally make the position under-collateralized
- Self-liquidating the position to take advantage of the 20% liquidation discount
This resulted in the attacker keeping significant “bad debt” while their liquidator account received discounted collateral, profiting from the protocol’s liquidation incentives.
Attack Explanation: The attack was executed through two main contracts:
- Primary Contract:
- Got a 30M DAI flash loan from AAVE V2
- Deployed violator and liquidator contracts
- Sent the DAI to the violator
- Violator Contract:
- Deposited 20M DAI to get ~19.56M eDAI
- Created artificial leverage twice:
- First time: Minted ~195.68M eDAI and 200M dDAI
- Second time: Minted another ~195.68M eDAI and 200M dDAI
- Repaid 10M DAI to reduce dDAI to 190M
- Donated 100M eDAI to reserves
This left the violator with:
- ~310.93M eDAI
- 390M dDAI
The key vulnerability was that the donation created an under-collateralized position (more dDAI than eDAI).
- Liquidator Contract:
- Liquidated the violator’s position
- Due to liquidation discount (20%), got ~310.93M eDAI but only ~259.31M dDAI
- Withdrew DAI by burning eDAI at a favorable rate
The attacker profited 8.88M DAI ($8.78M USD) from this attack, with their liquidator contract maintaining a healthy collateralization ratio of ~1.05.
Attack was carried out for several different assets, including DAI, WETH, and USDC.
Proposed Solution
Proper health checks should be performed on the account that is performing the donation. It is worth noting that the below assertion checks for modifications made by the user in the transaction. The user should never be allowed to make changes to their own collateral or debt that brings their positions under water. Assuming we can check run assertions on each call in the transaction and that we can get all modified accounts in the transaction, we can implement the following assertion:
This assertion ensures that users don’t have more debt than collateral after the transaction.
Additional Considerations
The Euler devs forgot to add a health check to the donateToReserves
function, so why would they have added an assertion for this?
The answer is that they wouldn’t. They would have added the assertion way in the beginning when the protocol was deployed first as an additional layer of security. They would have done that to exactly prevent what happened. Someone forgot to perform basic invariant checks at a protocol extension, and that’s why the assertion acts as an additional layer of security which enforces protocol wide invariants, exactly to prevent insufficient checks in protocol extensions.
In other words, regardless of whether the devs simply forgot to do the checks or if they thought they don’t need to do the checks, it shows that extensions are prone to forgetting protocol wide checks. Whereas assertions implicitly check protocol wide assertions by default.