Mai Finance stETH Curve Vaults get_virtual_price() Read-Only Reentrancy Near Miss (2022-10-18)
- Name: Mai Finance stETH Curve vaults
get_virtual_price()read-only reentrancy near miss - Date: 2022-10-18
- Protocol: Mai Finance / QiDao
- Chain: Ethereum
- Category: read-only reentrancy / Curve LP oracle manipulation / collateral inflation / undercollateralized stablecoin mint / whitehat near miss
- Loss: no realized theft; Amber Group’s proof of concept showed the vulnerable pricing path could overstate collateral enough to mint about
$15kof excessMAIin a test exploit, and the team warned the affected vaults could otherwise have been drained into bad debt - Summary: Amber Group found that two Ethereum Mai Finance vaults,
SCSEMVTandYCSEMVT, trusted Curve’sget_virtual_price()while valuing stETH/ETH Curve-LP-derived collateral. That was unsafe after ChainSecurity’s 2022 read-only reentrancy disclosure. An attacker could build a large temporary ETH/stETH Curve LP position, then enter Curve’s liquidity-removal path from a contract recipient so LP supply had already been reduced while pool balances were still mid-withdrawal. During that callback window,get_virtual_price()returned an inflated LP mark. Mai’s deeper failure was not justCurve was callback-sensitive, but that the vault borrow path had no canonical settled-state read surface for that collateral and simply monetized the raw callback-window mark as stablecoin-issuance truth. QiDao patched the affected oracle path before public exploitation by deploying a new oracle contract on 2022-10-20. - Trigger / entry point:
- Mai Finance accepted stETH/ETH Curve-LP-derived collateral in Ethereum vaults including
SCSEMVTandYCSEMVT. - Amber’s writeup says the vault pricing path used Curve
get_virtual_price()as one ingredient of collateral valuation. - Curve’s
get_virtual_price()had already been shown by ChainSecurity to be callback-sensitive during liquidity removal in affected pools. - The attacker only needed a contract recipient, enough temporary pool liquidity, and a vault whose borrow path trusted the callback-window price immediately.
- Mai Finance accepted stETH/ETH Curve-LP-derived collateral in Ethereum vaults including
- Root cause:
- Mai did not have a classical state-corruption bug in its own vault storage. The decisive mistake was consuming an upstream Curve
viewduring Curve’s mid-transition state. - In the vulnerable Curve withdrawal flow, LP supply could already be reduced while pool balances had not finished updating.
get_virtual_price()then divided a still-too-large invariant / balance view by the already-lower LP supply, temporarily overstating LP value.remove_liquidity_imbalance()made the setup sharper because the attacker could withdraw only1 weiof ETH, trigger the contract fallback, and keep the remaining pool balances largely intact while the inflated ratio was live.- Mai’s oracle path did not reject that callback-window read or force borrowers onto one canonical settled-state valuation surface, so a transient Curve accounting mismatch became borrowable collateral truth.
- Mai did not have a classical state-corruption bug in its own vault storage. The decisive mistake was consuming an upstream Curve
- Exploit path:
- The attacker accumulates or flash-borrows a large amount of ETH and stETH, then adds liquidity to the Curve ETH/stETH pool to mint a large LP position.
- They route the relevant stETH/ETH LP-derived collateral into a vulnerable Mai vault such as
SCSEMVTorYCSEMVT. - From an attacker-controlled contract, they call Curve’s liquidity-removal path, with
remove_liquidity_imbalance()being the more dangerous variant highlighted by Amber. - Curve burns LP supply and begins paying out ETH, which hands execution to the attack contract’s fallback before pool balances have fully settled.
- Inside that callback window, the attacker triggers Mai’s borrow path.
- Mai’s oracle reads Curve
get_virtual_price()while LP supply is already lower but balances are still temporarily too high, so the collateral mark is inflated. - Mai mints more
MAIthan the position should support under fully settled pricing. - After Curve finishes settling, the virtual price falls back toward reality, but the excess
MAIdebt remains, leaving the protocol exposed to bad debt.
- What the attacker needed to be true:
- The relevant Mai vault had to accept Curve-LP-derived stETH collateral and trust same-transaction oracle reads.
- The attacker needed enough temporary ETH/stETH liquidity to make the callback-window mark-up economically meaningful.
- The borrow path had to consume the inflated collateral value immediately, without a lock probe or delayed oracle read that would reject mid-transition Curve state.
- The vault needed enough
MAImint capacity / downstream liquidity for the printed borrow power to matter.
- Technical anatomy worth remembering:
- This near miss preserves the exact bridge between ChainSecurity’s generic Curve disclosure and the later live exploits against Market.xyz, dForce, Conic, and EraLend: real downstream systems were already shipping same-transaction LP pricing assumptions that a whitehat could turn into undercollateralized borrowing or liquidation authority.
- Mai’s bug was on the overvaluation side of the read-only-reentrancy family. Unlike dForce, which monetized a transient markdown through liquidation, Mai would have let the attacker borrow stablecoins against a transient markup of their own collateral.
- ChainSecurity’s upstream assessment matters for severity: even without an exotic pool, the ETH/stETH Curve pool’s LP price was realistically manipulable by roughly a factor of two during the callback window, which is more than enough distortion for a CDP mint path that trusts same-transaction collateral marks.
- The most dangerous detail is not merely
Curve was unsafe. It is that a zero-interest CDP system treated rawget_virtual_price()as canonical settled-state proof, so one unsafe callback-window read became stablecoin issuance and therefore protocol bad debt. - Amber’s PoC amount was modest, but the reusable lesson is larger: once a collateral oracle is willing to believe callback-window
get_virtual_price()instead of a guarded or delayed settled-state object, the limiting factor is pool depth and vault liquidity rather than any deeper economic invariant.
- Representative contracts and artifacts:
- Affected Mai vault symbols called out by Amber:
SCSEMVTandYCSEMVT - Example affected collateral type called out by Amber:
steCRV-based collateral in the Yearn Curve stETH Ethereum MAI Vault (YCSEMVT) - Mitigation contract deployed by QiDao on 2022-10-20:
0x027493C97A8c3E7f40930e9cb1404EC41C2d5626
- Affected Mai vault symbols called out by Amber:
- Response / mitigation:
- Amber said it discovered the issue and completed an internal proof of concept on 2022-10-18.
- Amber contacted the QiDao team on 2022-10-19.
- QiDao confirmed the issue and deployed a new oracle contract on 2022-10-20.
- Amber later noted that a similar Curve-LP read-only-reentrancy path was exploited on Polygon on 2022-10-24, but said that incident was unrelated to QiDao because QiDao had already patched its Ethereum vault path.
- On 2022-12-20 QiDao publicly acknowledged the stETH-ETH vault price-feed upgrade and credited ChainSecurity and Amber Group.
- Key transaction hashes:
- No public exploit transaction hashes are known because the issue was preserved as a whitehat-disclosed near miss rather than a live drain.
- The key public preservation artifacts are Amber Group’s proof-of-concept writeup, ChainSecurity’s upstream Curve analysis, and QiDao’s published oracle-upgrade acknowledgement.
Pattern tags
stablecoin mint authority sat directly on top of Curve callback-window virtual price truthwrapper and vault labels did not create a new safe valuation boundary once borrow power still bottomed out in get_virtual_price()temporary LP markup only needed to exist for one borrow-relevant instant because Mai monetized the raw read immediatelywhitehat near miss preserved the stablecoin-printing cousin of the later lending-market Curve LP oracle exploits
Why the exploit worked
- Mai’s decisive mistake was not merely
using Curve LP collateral; it was letting the vault borrow path consume Curve’s callback-windowget_virtual_price()answer as if it were canonical settled-state collateral truth. The false mark only needed to live for one mint-authoritative moment. - The collateral objects were wrapped and vault-specific (
SCSEMVT,YCSEMVT,steCRV-derived positions), but those wrappers did not create a new truth surface. Their economic meaning still ultimately resolved to Curve’s LP pricing path, so the real oracle boundary stayed upstream in Curve’s liquidity-removal sequencing. - Amber’s PoC profit was modest, but the architectural failure was not. Once
MAImint authority was willing to believe a callback-window virtual price, the limiting factor became pool depth and mint liquidity rather than any deeper invariant inside the vault itself. - This is the overvaluation / stablecoin-issuance branch of the same family later seen in Market.xyz, Conic, EraLend, and dForce. The difference is monetization rail, not control-surface lesson: a transient upstream
viewbecame balance-sheet truth before settlement finished.
Detection and audit pivots
- Treat borrow paths as first-class oracle consumers even in
0% interestCDP systems. If a transient LP markup can increase mint capacity for one callback window, the protocol is already granting counterfeit collateral truth. - Trace wrapper and vault-specific collateral back to the first upstream state object that actually authors value. If the final answer still bottoms out in Curve
get_virtual_price(), the wrapper stack did not add a safe valuation boundary by itself. - Test both
remove_liquidity()andremove_liquidity_imbalance()variants when reviewing Curve-style integrations. Amber’s writeup is especially useful because it shows why a1 weiETH removal can maximize the imbalance between reduced supply and not-yet-updated balances. - Ask whether the upstream venue exposes any machine-checkable lock or safe-read surface at all. ChainSecurity’s disclosure is a useful reminder that, for affected Curve pools, downstream protocols often had to invent awkward lock-probing workarounds because the native reentrancy state was not publicly introspectable.
- Ask whether the right healthy-side answer is a guarded safe live read or a delayed settled snapshot. If no same-transaction Curve-derived mark deserves mint authority, the protocol should quarantine same-block collateral truth instead of merely hoping integrators remember the unsafe window.
- Review whether stablecoin minting, collateral ratio checks, liquidation previews, and UI display all consume the same freshest LP mark for convenience.
Useful current estimateandsafe to authorize debt right noware different claims.
Failure-prevention checklist
- Route vault mint capacity, collateral-ratio checks, and any borrow-side preview or helper path through one canonical safe-read surface. A callback-window Curve LP mark must never become
MAIissuance authority merely because the code path is labeledoracle,borrow, orvault valuationinstead ofliquidation. - Treat wrapper and vault-specific collateral labels such as
SCSEMVT,YCSEMVT, andsteCRVas packaging, not as new truth boundaries. If the economic answer still bottoms out in Curveget_virtual_price(), the integration needs an explicit lock/context check or a delayed settled snapshot before mint authority is granted. - Prefer delayed settled snapshots and same-block quarantine when the upstream venue cannot expose a trustworthy
safe to read nowprimitive. Stablecoin issuance does not need the freshest possible LP mark if freshness is only available inside a callback frame. - Quarantine callback-sensitive LP collateral with conservative mint caps, collateral caps, and fast pause rights even after oracle hardening. The healthy fallback assumption is that one unsafe read may still slip through somewhere in the wrapper stack.
- Monitor for same-block sequences where large ETH/stETH liquidity moves or imbalanced removals are immediately followed by borrow calls against the same Curve-derived collateral. That pattern is not normal vault usage; it is a production signature for callback-window mint inflation.
- Document the upstream callback assumption as executable integration policy. If the defense depends on Curve not being mid-withdrawal, that requirement should live in code and tests rather than in a postmortem memory.
- Keep informational pricing and mint-authoritative pricing separate. UI convenience quotes or optimistic previews should not silently share the same raw Curve
get_virtual_price()surface that decides whether newMAImay be printed.
Audit prompts
- Which exact functions turn Curve-LP-derived collateral into
MAImint capacity, collateral-ratio changes, or vault health updates, and do all of them consume the same guarded safe-read surface? - Do
SCSEMVT,YCSEMVT, or any Yearn / wrapper layer create a genuinely new settled valuation boundary, or do they merely relay Curve’s callback-sensitiveget_virtual_price()one abstraction layer higher? - Can an attacker still combine a large liquidity add with
remove_liquidity_imbalance()and a trivial ETH withdrawal to force the borrow path to read the inflated virtual price inside the callback window? - If Curve offers no public machine-checkable lock-state probe, why is same-transaction borrowing against Curve-derived collateral allowed at all instead of being forced onto a delayed settled snapshot?
- Are stablecoin minting, collateral checks, liquidation previews, and UI pricing all sharing the same freshest LP mark for convenience, or is there an explicit distinction between
display estimateanddebt-authoritative collateral truth? - What concrete controls bound loss even if one bad read is consumed: mint caps, debt ceilings, market pauses, per-vault supply caps, or nothing until bad debt already exists?
- Would a simple invariant like
a Curve withdrawal callback must not increase borrowable MAI against the same collateral before pool settlement completesfail under current tests? - Does the protocol treat stablecoin issuance as part of the oracle threat model, or is read-surface hardening only reviewed when people say
liquidationorprice feedexplicitly?
Lessons that should already be standard
- Stablecoin mint paths are oracle consumers. A protocol that prints debt against callback-window collateral truth is already treating transient state as money-good reserves.
- Wrapper names, vault IDs, and strategy layers do not sanitize upstream state. If value still bottoms out in Curve LP pricing, the wrapper moved the trust boundary; it did not repair it.
- Same-transaction LP pricing is optional, but bad debt is permanent. If no trustworthy safe-read surface exists, delayed settled snapshots are the healthier design for mint-authoritative collateral.
- A modest whitehat proof-of-concept does not imply modest protocol risk. Once the borrow path will believe callback-window virtual price, the real limiter is pool depth and mint liquidity, not the demo borrow amount.
- Upstream disclosures are already downstream security events. After ChainSecurity documented Curve’s unsafe read window, every CDP still minting against raw
get_virtual_price()was carrying live balance-sheet risk. Useful current estimateandsafe to authorize new debtare different claims. Collapsing them into one freshest-price surface is a design shortcut, not a neutral implementation detail.- Bounded-loss controls still matter after oracle hardening. If one bad read can print fresh stablecoins immediately, caps and pause rights are part of the same solvency boundary as the oracle fix itself.
Why this incident matters
- It is an unusually clean whitehat snapshot of a live CDP protocol sitting directly on top of Curve’s callback-window oracle hazard before a public drain forced the lesson.
- It broadens the read-only-reentrancy corpus beyond lending-market borrows against wrappers by preserving the stablecoin-mint version of the same failure.
- It also shows why
view-path fixes matter operationally: the protocol did not need a storage bug, governance compromise, or unusual asset admission mistake to become drainable; it only needed to let the borrow path treat a transient callback-frame read as canonical collateral truth. - The healthier design lesson is upstream and downstream at once: Curve exposed a monetizable raw read, while Mai failed to wrap that dependency in one canonical safe-read or delayed-settlement valuation surface before granting mint authority.
Internal linkages
- Core recurring pattern: read-only-reentrancy.
- Sharper healthy-side companion: safe-read-surface-discipline — Mai’s real failure was not only
Curve virtual price could be pumped, but that the vault borrow path had no canonical settled-state valuation surface before mintingMAI. - Timing-boundary companion: delayed snapshots and same-block quarantine.
- Adjacent oracle surface: lp-token-oracle-mispricing.
- Broader parent pattern: reentrancy.
- Useful live-exploit comparison: market-xyz-quickswap-lend-stmatic-curve-lp-oracle-read-only-reentrancy-2022-10-24 — a later production exploit where the same Curve callback-window pricing family was monetized against a Polygon lending market.
- Useful liquidation-path cousin: dforce-wsteth-eth-curve-vault-read-only-reentrancy-liquidation-manipulation-2023-02-09 — the same upstream read-only-reentrancy family, but monetized through a temporary markdown and liquidation authority instead of stablecoin over-minting.
Control surface
-
The leverage sat in Mai’s collateral oracle path, Curve’s
remove_liquidity/remove_liquidity_imbalance()callback sequencing, and the protocol’s willingness to convert a callback-window LP price into immediate stablecoin issuance authority. -
Mai did not need a bug in
borrow()itself to become unsafe. It only needed to letborrow()consume raw upstream state that was true for one callback window and false for every fully settled state that mattered. -
The preserved healthy boundary is that any mint-authoritative LP mark should come from one guarded safe-read or delayed settled-state surface, not from whichever
viewanswer exists mid-transition. -
Sources:
-
Last reviewed: 2026-07-19 UTC