Worked example · 2022-10
Market.xyz — a lending market prices Curve LP collateral through get_virtual_price() and is drained by re-entering the pool's view during remove_liquidity — Market.xyz / QuickSwap (Polygon) — 2022-10-24
Summary
Market.xyz was a permissionless lending-market platform (Fuse/Compound-style isolated money markets) whose markets were hosted on QuickSwap, a DEX on Polygon. One Market.xyz market accepted a Curve LP token as collateral and priced it through a Curve-LP oracle that read the pool's get_virtual_price() view function.
Curve's get_virtual_price() returns the pool's invariant value per LP token. It is safe to read only when the pool is in a settled state. But remove_liquidity on a pool holding a native asset transfers that asset to the caller before it finishes updating the pool's internal accounting — a legitimate callback that momentarily leaves the pool mid-execution, with total LP supply already reduced but the invariant not yet recomputed. During that window, get_virtual_price() reports an inflated value.
On 2022-10-24, an attacker exploited this. Using flash-loaned capital, the attacker added liquidity to the Curve pool, then called remove_liquidity; inside the native-token transfer callback, while the pool was mid-execution, the attacker's contract invoked Market.xyz's borrow path, which read the inflated get_virtual_price() and valued the attacker's LP collateral far above its true worth. Market.xyz extended a loan against that inflated valuation, and the attacker withdrew the borrowed MAI, repaying the flash loan and keeping the difference — about $220K across two transactions.
The Curve pool was untouched: the read was a staticcall, no assets left the pool. The loss fell entirely on the Market.xyz market, and specifically on MAI that QiDAO had seeded to bootstrap it, so no user deposits were lost. QuickSwap closed its lending service after the exploit and initially attributed the failure to the Curve/QiDAO oracle configuration; QiDAO stated the exploit was unrelated to its own smart contracts. The 138 ETH of proceeds were bridged to Ethereum and routed through Tornado Cash.
Timeline (UTC)
| When | Event | OAK ref |
|---|---|---|
| (standing) | Curve remove_liquidity transfers a native asset to the caller before settling pool accounting, leaving get_virtual_price() inflated mid-callback |
(standing T9.010 surface) |
| (pre-event) | ChainSecurity publicly warns that Curve-LP oracles reading get_virtual_price() are exposed to read-only reentrancy |
(advisory, pre-event) |
| (standing) | Market.xyz market on QuickSwap prices a Curve LP token via get_virtual_price() with no mid-execution check |
(T9.010 surface present) |
| 2022-10-24 | Attacker flash-borrows, adds liquidity, calls remove_liquidity; re-enters Market.xyz's borrow path during the callback and reads the inflated get_virtual_price() |
T9.002 → T9.010 |
| 2022-10-24 | Loan extended against over-valued LP collateral; attacker withdraws MAI, repays flash loan; ~$220K extracted across two transactions | T9.010 extraction |
| 2022-10-24 onward | 138 ETH bridged Polygon → Ethereum → Tornado Cash | (laundering) |
| post-event | QuickSwap closes its lending service; QiDAO states the exploit is unrelated to its contracts | (operator response) |
What defenders observed
- Pre-event (the class was already published). ChainSecurity had documented the Curve-LP-oracle read-only-reentrancy exposure before this incident. The pre-event control is inventorying which of a protocol's price sources are external pool
viewreads that can be reached mid-execution, and treating any that lack a mid-execution check as a live exposure rather than a theoretical one. - At-event (the vulnerable protocol shows no anomaly). The Curve pool's own balances and invariants were intact throughout — forensic monitoring of the pool alone finds nothing. The anomaly lives on the consumer: a loan extended against a collateral valuation that was correct against the oracle read at that instant but wrong against settled state. This is what makes T9.010 harder to catch at the per-contract layer than a direct drain (T9.005), and why cross-protocol call-trace analysis is the detection surface.
- At-event (the diagnostic trace). The direct signature is a
staticcalltoget_virtual_price()(or an equivalent pool view) executed while that pool's outerremove_liquidityframe has not returned, followed in the same transaction by a borrow against the value it produced. A per-transaction call-trace rule for view-reads of a mid-execution target is the highest-leverage at-event signal. - Mitigation (consumer-side, Curve-independent). The fix does not depend on Curve changing anything: the consumer asserts the pool is not mid-execution before trusting its view — invoking the pool's reentrancy lock via a state-mutating no-op that reverts if the lock is held, the pattern later standardised across Curve-LP and Balancer-LP integrations. Per-contract
ReentrancyGuardon the consumer does not help, because the re-entry is into aview, not a state-mutating function. - Disposition (seeded liquidity, not user funds). The drained MAI was QiDAO-seeded bootstrap liquidity, so end users lost nothing — a disposition detail, neutral to how much of an attack this was. The mechanism and the missing control are identical whether the drained liquidity was protocol-seeded or user-deposited.
Public references
[quillauditsmarketxyz2022]— QuillAudits, "Decoding $220K Read-Only Reentrancy Exploit" (primary technical breakdown:get_virtual_price()manipulation viaremove_liquidity, theD-not-updated-while-supply-decreased window, flash loan, both attacker addresses and transaction hashes, ~$220K MAI / 138 ETH + 700 MATIC, QiDAO-seeded funds): https://quillaudits.medium.com/decoding-220k-read-only-reentrancy-exploit-quillaudits-30871d728ad5[chainsecuritycurvelp2022]— ChainSecurity, "Curve LP Oracle Manipulation: Post Mortem" (authoritative post-mortem of the Curve-LP-oracle read-only-reentrancy class and the consumer-side guard; the firm's prior "Heartbreaks & Curve LP Oracles" advisory pre-dated the exploit): https://www.chainsecurity.com/blog/curve-lp-oracle-manipulation-post-mortem[ambergroupmaifinance2022]— Amber Group, "Mai Finance's Oracle Manipulation Vulnerability Explained" (independent mechanism analysis of the QuickSwap/Market.xyz oracle manipulation): https://medium.com/amber-group/mai-finances-oracle-manipulation-vulnerability-explained-55e4b5cc2b82[web3isgreatmarketxyz2022]— Web3 Is Going Great, "Oracle manipulation attack on a QuickSwap market earns exploiter $188,000" (contemporaneous record; QuickSwap closes lending service; QiDAO states its contracts unrelated): https://www.web3isgoinggreat.com/?id=oracle-manipulation-attack-on-a-quickswap-market-earns-exploiter-188000
Discussion
Market.xyz is OAK's canonical anchor for the read-only reentrancy via AMM LP-token oracle shape of T9.010, and it earns the slot by being one of the earliest clean in-the-wild instances — October 2022, before the higher-profile 2023 cases. It pairs with Sentiment (April 2023, Balancer-Vault callback) and Cygnus (March 2024) as the cross-AMM anchor set: three different pool architectures, one identical consumer-side vulnerability — a lending protocol trusting a pool view output at a moment the pool was mid-execution. The AMM design and the specific callback differ; the trust assumption that breaks is the same.
The case is worth separating cleanly from the July 2023 Curve/Vyper incident, with which it has been confused, because they are different events with different mechanisms. The Vyper incident (examples/2023-07-curve-vyper.md) was a compiler-level @nonreentrant-lock failure that made Curve pools reentrancy-capable when they should not have been, and its primary losses were direct reentrancy drains of the pools themselves (T9.005, ~$73M aggregate). Market.xyz was not involved in that event, and the read-only-reentrancy surface it exploited here needed no compiler bug at all: remove_liquidity's native-token callback is a legitimate, by-design mid-execution window. Conflating the two overstates both and misattributes the mechanism; OAK keeps Market.xyz as the Oct-2022 Polygon T9.010 anchor and the Vyper chain as the July-2023 T9.005 anchor.
The durable lesson is the one ChainSecurity had already published before the exploit: every external view read consumed for a pricing decision is a trust assumption that the read reflects settled state, and that assumption fails whenever the target can be mid-execution. The mitigation is the consumer's to implement and does not require the target pool to change — which is exactly why a protocol cannot outsource this risk to the AMM it integrates.