OAK — OnChain Attack Knowledge

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

Loss
~$220K of MAI stablecoin extracted from a Market.xyz lending market hosted on QuickSwap (Polygon). Reported figures vary by snapshot (~$188K–$220K). The drained MAI had been seeded to the market by QiDAO, so no end-user deposits were lost; the loss fell on the protocol-supplied liquidity. Two attack transactions netted the attacker roughly 138 ETH and 700 MATIC after swaps. QuickSwap shut down its lending service in response.
OAK Techniques observed
OAK-T9.010 (Read-Only Reentrancy — primary, confirmed mechanism. Market.xyz priced Curve LP-token collateral by reading the Curve pool's get_virtual_price() view. The attacker re-entered that view while the pool was mid-execution inside remove_liquidity: the pool transfers native tokens to the caller before it finishes updating its accounting, so during that callback the LP-token supply had already decreased while the invariant D had not been re-settled, and get_virtual_price() returned an inflated value. Market.xyz consumed the inflated price and extended a loan against over-valued collateral, which the attacker withdrew. The Curve pool itself lost nothing — the staticcall read did not move its assets. See techniques/T9.010-read-only-reentrancy.md). OAK-T9.002 (Flash-Loan-Enabled Exploit — the working capital: the position that made the transient mispricing worth ~$220K was flash-loan-funded, with add-liquidity, the re-entrant read, the over-collateralised borrow, and repayment all inside one transaction. See techniques/T9.002-flash-loan-enabled-exploit.md).
Attribution
pseudonymous. No named individual or group. On-chain identifiers from the public write-ups: attacker EOA 0x4206d62305d2815494dcdb759c4e32fca1d181a0 with exploit contract 0xeb4c67e5be040068fa477a539341d6aef081e4eb (first transaction 0xb8efe839da0c89daa763f39f30577dc21937ae351c6f99336a0017e63d387558), and a second EOA 0xe3671a41c44f50048e60939df3d26704c9652d9d with contract 0xC6CD77fcf37ffcd97B3429F0284ecfF50616EA74. The 138 ETH proceeds were bridged from Polygon to Ethereum and sent to Tornado Cash. The read-only-reentrancy trace path (a staticcall to get_virtual_price() while the pool's outer remove_liquidity frame had not returned) is reproducible from the transaction trace.
Key teaching point
A view function is only a safe price source if the contract it reads cannot be mid-execution at the moment you read it — and a Curve pool can be, because remove_liquidity hands control back to the caller before it settles. Market.xyz's own lending logic was not reentrant and was not the flaw; the flaw was the cross-protocol trust assumption that get_virtual_price() always returns settled state. That assumption is false during the pool's native-token transfer callback, and any consumer that reads the price then sees an inflated number. The load-bearing mitigation is entirely on the consumer side and independent of Curve: before using an external pool's view output for a value-transfer decision, assert the pool is not mid-execution — the pattern later standardised as Curve/Balancer read-only-reentrancy guards (calling a state-mutating no-op that reverts if the pool's reentrancy lock is held, or Balancer's ensureNotInVaultContext). This case is one of the earliest in-the-wild exploitations of that surface, predating the higher-profile Sentiment (April 2023) and Curve-Vyper (July 2023) events, and ChainSecurity had already warned about exactly this Curve-LP-oracle class before it was exploited here.

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 view reads 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 staticcall to get_virtual_price() (or an equivalent pool view) executed while that pool's outer remove_liquidity frame 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 ReentrancyGuard on the consumer does not help, because the re-entry is into a view, 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

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.

Techniques demonstrated (2)