Full Report
Curve is a popular Automated Market Maker (AMM) that uses a Liquidity Pool (LP) to get the funds. Many contracts interact with Curve to find out the going rate of a token. The get_virtual_price() function implements the logic for creating the stable swap mechanism within Curve. LP tokens are commonly priced by computing the underlying tokens per share. Essentially, assets total price/amount of LP tokens. Reentrancy is an attack where a contract can be left then reentered with only SOME state being changed. With a partial change of state, it may be possible to recover funds or overwrite other state into a exploitable position. In order to prevent attacks like this, a reentrancy modifier is commonly used on a single contract to prevent going back into it. However, this is typically only on the main state changing functions on the contract. So, what if we entered a contract, left it via an external call then made a read only call to the service from another contract? Since that is not a major state changing function, it likely does not have the reentrancy modifier on it. Additionally, the code may be in an unintended state, creating opportunity for financial manipulation. When removing liquidity from Curve, there is a reentrancy modifier/decorator (written in Vyper) on the function remove_liquidity(). When calculating the price that the LP token should be swapped for the code gets the balances of the contract, balances of the user and the total supply. Once it does this, the LP token is burned (removing the tokens from circulation) and returns all of the funds to the original caller. Above, the total_supply has been updated but NOT the individual amount of each token. This leaves the contract in a very strange place for new calls being made. Since the price of the LP token is based upon the assets total price/amount of LP tokens, we can make the amount of LP tokens very little but still keep the assets very high. While in this state, calling get_virtual_price() would have an inflated cost as a result. Any pool using this function from Curve would have been severely open to oracle manipulation at this point. This ended up being exploited on Market.xyz in the wild. There's a Youtube video that demonstrates this as well. Overall, reentrancy attacks are extremely hard to mitigate. Reentrancy guards and integer overflow protection are simply not enough. Updating all of the state properly prior to an external call is crucial for the security. It is sickening for defensive people but awesome for offensive folks. To me, it's like memory safety bugs in C - it is too easy to mess up and the ecosystem should make it possible to do easily.
Analysis Summary
# Incident Report: Curve LP Oracle Manipulation (Read-Only Reentrancy)
## Executive Summary
A critical "read-only reentrancy" vulnerability was discovered in several Curve Finance liquidity pools, primarily those involving ETH or tokens with callbacks (ERC-677/777). Attackers could manipulate the `get_virtual_price()` function to artificially inflate or deflate LP token prices, leading to potential oracle manipulation. This flaw put over $100M at risk across integrated protocols like MakerDAO, Abracadabra, and Market.xyz.
## Incident Details
- **Discovery Date:** April 14, 2022
- **Incident Date:** Exploited in the wild on Market.xyz; disclosed/patched April–October 2022
- **Affected Organization:** Curve Finance and integrated protocols (MakerDAO, Enzyme, Abracadabra, TribeDAO, Opyn, Market.xyz)
- **Sector:** Decentralized Finance (DeFi)
- **Geography:** Global / Decentralized
## Timeline of Events
### Initial Access
- **Date/Time:** April 14, 2022 (Identification of vulnerability)
- **Vector:** Exploitation of the `remove_liquidity` function in Curve stable swap pools.
- **Details:** The attacker initiates a liquidity removal. Because ETH or certain tokens trigger a callback to the user (e.g., via `raw_call` for ETH), the attacker gains control of execution flow before the contract state is fully updated.
### Lateral Movement
- **Mechanism:** Read-Only Reentrancy. While the pool is "locked" for state-changing calls, "view" functions like `get_virtual_price()` remain accessible. The attacker calls these functions from a malicious contract during the callback.
### Data Exfiltration/Impact
- **Mechanism:** Price Manipulation. By reentering during a specific window where LP tokens have been burned but underlying balances remain high, the "virtual price" becomes massively inflated.
- **Impact:** Exploitation of lending protocols (like Market.xyz) that used the inflated virtual price as an oracle to borrow against undervalued collateral or liquidate positions unfairly.
### Detection & Response
- **Discovery:** Identified by ChainSecurity and disclosed to Curve and affected teams.
- **Response Actions:** Affected protocols updated their oracles to trigger Curve's reentrancy lock (by calling a state-changing function like `withdraw_admin_fees`) before reading prices.
## Attack Methodology
- **Initial Access:** Smart contract interaction with `remove_liquidity`.
- **Persistence:** Not applicable (Atomic transaction-based exploit).
- **Defense Evasion:** Bypassed standard `@nonreentrant` guards because the guard only protected state-changing functions, not "read-only" view functions.
- **Discovery:** Reconnaissance of Vyper contract state update order.
- **Impact:** Oracle manipulation resulting in the drain of funds from integrated lending/borrowing platforms.
## Impact Assessment
- **Financial:** Estimated $100M+ at risk; specific losses occurred on Market.xyz.
- **Operational:** Required immediate oracle architecture redesigns for dozens of DeFi protocols.
- **Reputational:** Highlighted a new class of "Read-Only Reentrancy" risks previously overlooked by many auditors.
## Indicators of Compromise
- **Behavioral:** Transactions involving `remove_liquidity` followed immediately by calls to `get_virtual_price` and subsequent borrowing/liquidation actions within the same sub-graph of a transaction.
- **Contract Patterns:** Use of `raw_call` or `fallback` functions to trigger external logic during liquidity withdrawal.
## Response Actions
- **Containment:** Coordinated disclosure to major integrators (MakerDAO, etc.) to implement "proxy" calls that trigger reentrancy guards.
- **Eradication:** Implementation of "lock-aware" view functions in newer contract versions.
- **Recovery:** Integration of more robust oracle safeguards that do not rely solely on a single `get_virtual_price` point.
## Lessons Learned
- **View Functions are Not Safe:** Reentrancy is not just about writing to state; reading from an inconsistent state is equally dangerous if that data is used for financial decisions.
- **State Update Order:** The "Checks-Effects-Interactions" pattern must be strictly followed; updating global supply before transferring tokens (as seen in the Vyper code) created the window of inconsistency.
- **Guard Visibility:** Reentrancy guards should ideally be queryable by external contracts.
## Recommendations
- **Developer Action:** Ensure all state variables (including total supply and balances) are updated *before* any external calls or ether transfers.
- **Integration Protection:** When consuming an oracle price, first perform a low-gas state-changing call (e.g., a dummy trade or fee withdrawal) to ensure the target contract is not currently being reentered.
- **Audit Focus:** Future security audits must specifically test for "read-only" reentrancy scenarios in all view functions used by external oracles.