Curve is an automated market maker (AMM). User can trade token X against a liquidity pool for token Y. For performing these trades, there is a small fee that goes back to the liquidity providers.
Curve uses a liquidity provider (LP) token to represent a share of the liquidity pool. When a share is redeemed, users get their assets back proportional to their share to the total supply. The trade price is proportional to the tokens in the pool. For instance, if the trade ratio for DAI to ETH is 1848, then the pool would have 1848 DAI for every ETH.
Pools commonly have a mechanism for evaluating how much value an LP token has. This is commonly done by computing the underlying tokens for each share then dividing by the total supply (amountOfX/TotalSupply
). Since these tokens are pegged to each other, this can be used to estimate the amount of total value of underlying tokens in the pool.
In Curve, the function get_virtual_price()
implements an oracle for the conversion rate. In particular, dividing the underlying tokens by the total supply will give a good oracle. This is used to estimate fee growth or estimate the value of LP tokens.
What if we could manipulate the totalSupply
or amountOfToken
for a brief moment? If we did this, then the function get_virtual_price()
used by other contracts could cause major problems. In this case, there is a read only reentrancy bug in this.
The
remove_liquidity()
function is used to remove liquidity from a contract. This happens in a few steps:
- Tokens are burned in
burnFrom
, which decreases the supply of LP tokens.
- Share of burned tokens is computed.
- The tokens are sent back based upon the shares provided.
- If the tokens are ETH, then a call is made to the fallback function of the contract.
During the execution of this code, there are 'N' tokens that run in a loop. If the callback function is hit, there is a major problem! The totalSupply
has already been changed but the amount of each token has not been updated yet. By executing other code during the fallback, we can interact with the contact while it's in this state.
There is a reentrancy guard on most of the state-altering functions. However, there is NOT one on
get_virtual_price()
. Protocols that use this function can be manipulated with the following steps:
- Deposit a large amount of liquidity. This could even be done with a flash loan to cause crazy problems.
- Remove the liquidity
- Use the callback with the pool in a weird state.
- Profit.
Who was vulnerable? All of the pools with ETH could be exploited. Additionally, tokens with callbacks were also vulnerable to the same types of attacks. By calling this, it was possible to drastically change the price. Overall, a super interesting post on a well-known contract.