Skip to content

Latest commit

 

History

History

README.md

title Curve Pool Oracle
description Exploiting AMM oracle prices during liquidity withdrawal callbacks
type Exploit
network
polygon
date 2022-11-23
loss_usd 180000
returned_usd 0
tags
reentrancy
subcategory
vulnerable_contracts
0x3dC7E6FF0fB79770FA6FB05d1ea4deACCe823943
tokens_lost
ETH
MATIC
attacker_addresses
0x8ca72f46056d85db271dd305f6944f32a9870ff0
0xEb4c67E5BE040068FA477a539341d6aeF081E4Eb
malicious_token
attack_block
34716801
reproduction_command forge test --match-contract Exploit_Qi_ReadOnlyReentrancy -vvv
attack_txs
0xb8efe839da0c89daa763f39f30577dc21937ae351c6f99336a0017e63d387558
sources
title url
Curve LP Manipulation Post Mortem

Step-by-step

  1. Take a flashloan
  2. Add a lot of liquidity to an eth/{something} pool to get LP tokens
  3. Remove liquidity triggering fallback when receiving ETH
  4. Request a loan from a system using get_virtual_price as a price oracle for LP tokens
  5. Loan will be given taking a higher-than-real price for LP tokens
  6. Repay flashloan

Detailed Description

Understanding this attacks needs some familiarity with AMMs and how crypto system compose with each other.

We will start by reviewing Curve's Stable Swaps. There's no need to go deep into the math, we can just understand that is is an AMM that is optimized for trades between assets that are really close in price (different from constant product AMMs).

Stableswaps keep a value D as the swap-invariant (like constant product keeps k). Just like in constant product, this is a semi-invariant in in practice as it will increase due to fees.

Now, we must understand some particularities of Curve before moving forward. We probably can't do it better than the folks at ChainSecurity have already done, so we really recommend you go and read the blog post. Let's do a very condensed summary just in case you don't have time to read the full story today:

A primer on Curve Pool

Curve Pools have a get_virtual_price that returns the prices of LP tokens (calculated as D / total_lp_token_supply ~= total_underlying_supply / total_lp_token_supply).

When you burn your LP tokens, this code is executed:

// _amount is the amount of lp tokens burn
// amount[i] is the total amount of each token
CurveToken(lp_token).burnFrom(msg.sender, _amount)
for i in range(N_COINS):
    value: uint256 = amounts[i] * _amount / total_supply
    if i == 0:
        raw_call(msg.sender, b"", value=value)
    else:
    assert ERC20(self.coins[1]).transfer(msg.sender, value)

Note that if i == 0 the value is sent natively. This means msg.sender is suddenly executed. At this point, the contract has burned the LP tokens but has only sent part of the underlyings. If you remember, get_virtual_price was total_underlying_supply / total_lp_token_supply: this number is now off, as only a part of the total_underlying_supply has been sent while all of the corresponding total_lp_token_supply has been burned.

Here is a the implementation of get_virtual_price:

@view
@external
def get_virtual_price() -> uint256:
    """
    @notice The current virtual price of the pool LP token
    @dev Useful for calculating profits
    @return LP token virtual price normalized to 1e18
    """
    D: uint256 = self.get_D(self._balances(), self._A())
    # D is in the units similar to DAI (e.g. converted to precision 1e18)
    # When balanced, D = n * x_u - total virtual value of the portfolio
    token_supply: uint256 = ERC20(self.lp_token).totalSupply()
    return D * PRECISION / token_supply

If an attacker finds a protocol that uses get_virtual_price as a price oracle for LP tokens, they could be exploited.

Unfortunately, this is exactly what QiDao and Market XYZ did. For the purposes of this reproduction, we are gonna concentrate on QiDAO but the concept for Market XYZ is the same.

At this point, you only need to know that QiDAO is a lending platform which used MasterPriceOracle to get the prices for their collateralized borrows. Unfortunately, this oracle used get_virtual_price, which we now know how to exploit.

Possible mitigations

  • Ensure that the tokens addresses provided match the addresses from the targeted pool or check if they are whitelisted.
  • Use a reentrancy mutex if arbitrary tokens are meant to be handled.
  • Review the checks-effects-interactions pattern and evaluate the steps at which tokens flow in and out the contract.

Related

  • Polter Finance - Oracle price manipulation to drain lending reserves
  • One Ring Finance - Manipulating price feeds via reserve balance changes
  • DFX Finance - Reentrancy exploiting balance-based validation checks