In 2020 and at the beginning of 2021, one of the worst phrases you could hear either as a DeFi security researcher or developer was, “Project X was hacked due to Price Oracle manipulation using flashloans.” This was the main vulnerability at the time, and it made it difficult for auditors to sleep at night. Why? Because there were so many exploits and vulnerabilities regarding faulty integration with price oracles, and they weren’t always obvious.
We don’t hear as much about flashloan-enabled price oracle manipulation nowadays. The reasons for that are twofold:
- There are many great examples of how to integrate with AMM price oracles or how to use Chainlink.
- The second reason is thanks to bug bounties and the amazing work of whitehats.
This is the story of an excellent bug find and exemplifies Enzyme’s commitment to security. Although the funds at risk was quite low, Enzyme has given a generous payout to incentivize whitehats to find good vulnerabilities like this in the future.
Whitehat setuid0 of SSLab at Georgia Tech reported a critical vulnerability on November 17 in the way Enzyme Finance calculated the prices of Idle tokens when buying shares. Using a flashloan from
IdleTokenGovernance.sol affected the
totalSupply of the Idle tokens, which was used to calculate the price of the token. Price calculations were based on the
totalNav / totalSupply of the tokens. It’s worth noting the initial Idle Token integration was with v4, which did not have any flashloan logic. That was later added in v5, thus unintentionally introducing a bug into Enzyme’s Finance protocol.
The report contained all necessary details, including a working Proof of Concept which detailed all the steps needed to reproduce the attack. It was escalated within five minutes (remember, a working PoC means a quick escalation) and confirmed by the Enzyme team early the next day. Enzyme issued a payout of $90,000 to setuid0 the same week.
Before discussing the bug report itself, let’s first dive into a brief background on oracles and flashloans.
Introduction to Price Oracles and Flashloans
A price oracle is a tool used to view price information of a given asset. In the DeFi world, this means that whenever we want to know the price of a token (how much it is worth against any other asset or U.S. dollars (DAI/USDC/USDT), we use price oracles.
There are two main types of oracles: Onchain and offchain.
- Onchain oracles are nothing more than smart contracts. Nowadays, DeFi depends on various Automated Market Makers or simply Decentralized Exchanges (Uniswap, Sushiswap, PancakeSwap, Curve, Bancor, Balancer etc.) to get a token price. For constant-product AMMs, like UniswapV2, users rely on the current ratio of two tokens. For example, the ETH-DAI ratio gives us the current price of an ETH.
- Offchain oracles like Chainlink depend on the data being put into the smart contracts by a decentralized network. When we want to get a price of ETH in U.S. dollars, we query the Chainlink aggregator smart contract and get the latest price reported by the Chainlink nodes.
Onchain and offchain oracles have pros and cons. With offchain oracles, we need to trust the network to always post the correct price and the assets available are limited. A flashy new token probably won’t end up as one of the Chainlink price feeds right away.
Onchain oracles are different. A project can create a pool of newly created tokens against ETH or DAI. But the issue in this case is price feed volatility. When taking a current price, aka spot price, we risk huge price fluctuations. For example, a user wants to sell a huge amount of his tokens and get ETH instead.
Knowing the characteristics above, some whales (users with a lot of funds) could change the price of any token either way in a single transaction, but is it possible to do it for normal people? How can hackers manipulate an onchain price oracle?
Everybody understands what regular loans are. You need to provide proof of income, reserves, or other information a bank may need to get one. There are, of course, uncollateralized loans where you don’t need to put forward any collateral, i.e., you don’t need to provide any asset in case you don’t repay the loan. But there are also secured loans where you need to put up collateral, especially when you want to borrow a large amount of money.
Flashloans are a way to borrow a large amount of money from a lending protocol like Aave without collateralization, only for a certain fee paid at the end. The caveat is it needs to be returned within one transaction. If not, the transaction will be reversed.
Flashloans use smart contracts. The main enforced rule is the borrower must pay back the loan before the transaction ends. Otherwise, the smart contract reverses the transaction, making your transaction invalid.
The transaction to the flashloan contract can be divided into three parts:
- Receive the loan
- Perform actions with the loan
- Repay the loan
All of the above needs to happen within the confines of one transaction.
The primary use case is for arbitrage. You can’t purchase anything long-term with a flashloan, as you need to repay the loan quickly, but you can make a profit with an arbitrage opportunity on DEXes. Imagine 1% difference in value due to arbitrage. It may not seem much, but that 1% is actually a substantial amount of money.
There are many other use cases that we won’t cover here. You can read Aave’s sneak peek into that. We’re interested in flashloans because they can be used nefariously to manipulate the price oracle.
With substantial WETH/ETH available to us via flashloan, we can exchange them for any other asset we want through an AMM. It doesn’t need to be a direct swap. If a token we’re interested in only has a pool against DAI, we can swap flashloaned ETH for DAI and use some of the DAI we got for purchasing the token we’re interested in, thus manipulating the price.
If you’re interested in knowing more about flashloans and oracle manipulation, we recommend Samczsun’s great post about the subject.
Now that we know some background context of the concepts we’ll be dealing with, let’s discuss the actual vulnerability.
Enzyme Finance is an Ethereum-based protocol for decentralized on-chain asset management. It allows users and investors to create and invest in various funds. A fund owner configures the rules of their fund: fees and policies, the denomination asset by which share price and performance are measured, the time-lock between shares actions (buying or redeeming shares) for a given user, etc.
When we want to invest in a fund, we need to send the underlying asset of the fund to the vault. In return users get shares that represent their part in the fund. Users can call the
buyShares() function on the
ComptrollerLib.sol. We need to provide the amount of underlying assets of the fund with which we will be purchasing the shares and the minimal amount of shares.
How much we will need to pay for a share is calculated by the public function
calcGav(). This function simply calculates the gross asset value (GAV) of the fund. It’s responsible for telling us how much the user will need to pay for a share based on the current assets value of the fund. Based on the price returned by the function, protocol will mint X number of shares to the user or redeem Y number of shares.
calcGav consults the
IDerivativePriceFeed.sol for the canonical asset total value in a fund. The price feed we’re interested in is
IdlePriceFeed.sol for Idle Tokens like idleUSDCYield. In this case, the underlying asset would be USDC and the derivative asset would be idleUSDCYield.
IdlePriceFeed gets the unit price of an Idle Token by calling the
tokenPrice function of the token. Return value of this function is something an attacker needs to manipulate to get better prices for a share when buying and selling for a profit.
We see in the code that price is calculated using net asset value (NAV) divided by total supply. NAV represents the per share/unit price of the fund on a specific date or time.
Looking at the
totNav calculations, it seems like we could potentially manipulate one value here. The current balance of the Idle Token contract’s backing assets. How? By using contract’s internal flashloan functionality which would temporarily emptied the contract by flash loaning the max amount from the IdleToken.
Unraveling all of this we can affect the final GAV calculations by flashloaning derivative assets and as a result buy shares at a discount and redeem them later for a profit.
A step-by-step guide on how to run this attack is as follows:
- Fund malicious contract with WETH to be able to swap it later for USDC to pay for a flashloan
- Make a flashloan of IdleUSDCYield tokens. This will in fact affect GAV calculations as we just greatly impacted the contract balance of IdleUSDCYield tokens.
- During a flashloan, swap WETH to USDC. This amount will let you later buy shares as we’re using underlying asset for buying shares.
- During a flashloan, call
buyShares. As GAV calculations are affected, efectively we’re buying shares at a discount now.
- Repay flashloan
- After flashloan is repaid, call
redeemSharesto sell all the bought shares of Idle Fund for a profit.
- Enjoy the profit.
The vulnerability here isn’t a common flashloan-enabled price manipulation as flashloans, and the price feeds are all inside the
IdleTokenGovernance contract. The attacker needs to exploit the logic of Idle tokens without making a traditional AMM oracle manipulation
Enzyme Finance has delisted IDLE tokens and will have a larger discussion with their council and auditors about idle tokens and other (future) integrations with upgradable protocols. IDLE additionally removed the flash loan feature to protect other potential integrators who may have been impacted by the same issue. Their postmortem can be found here.
We would like to thank setuid0 for the interesting price oracle manipulation submission.
We also want to thank the Enzyme Finance team for their swift response. Although not much was at risk (~$400k), Enzyme decided to issue a generous payout to incentivize whitehats to find good vulnerabilities like this in the future. As a sign of gratitude for this bug being discovered and the cooperation of Enzyme with IDLE, IDLE leagues are voting to partake in the bounty by granting Enzyme with 6250 IDLE tokens (about $19k).
To report additional vulnerabilities, please see Enzyme Finance’s bug bounty program with Immunefi. If you’re interested in protecting your project with a bug bounty, visit the Immunefi services page and fill out the form.