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 $15k of excess MAI in 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, SCSEMVT and YCSEMVT, trusted Curve’s get_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 just Curve 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 SCSEMVT and YCSEMVT.
    • 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.
  • Root cause:
    • Mai did not have a classical state-corruption bug in its own vault storage. The decisive mistake was consuming an upstream Curve view during 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 only 1 wei of 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.
  • Exploit path:
    1. 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.
    2. They route the relevant stETH/ETH LP-derived collateral into a vulnerable Mai vault such as SCSEMVT or YCSEMVT.
    3. From an attacker-controlled contract, they call Curve’s liquidity-removal path, with remove_liquidity_imbalance() being the more dangerous variant highlighted by Amber.
    4. Curve burns LP supply and begins paying out ETH, which hands execution to the attack contract’s fallback before pool balances have fully settled.
    5. Inside that callback window, the attacker triggers Mai’s borrow path.
    6. 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.
    7. Mai mints more MAI than the position should support under fully settled pricing.
    8. After Curve finishes settling, the virtual price falls back toward reality, but the excess MAI debt 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 MAI mint 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 raw get_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: SCSEMVT and YCSEMVT
    • 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
  • 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 truth
  • wrapper 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 immediately
  • whitehat 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-window get_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 MAI mint 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 view became balance-sheet truth before settlement finished.

Detection and audit pivots

  • Treat borrow paths as first-class oracle consumers even in 0% interest CDP 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() and remove_liquidity_imbalance() variants when reviewing Curve-style integrations. Amber’s writeup is especially useful because it shows why a 1 wei ETH 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 estimate and safe to authorize debt right now are 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 MAI issuance authority merely because the code path is labeled oracle, borrow, or vault valuation instead of liquidation.
  • Treat wrapper and vault-specific collateral labels such as SCSEMVT, YCSEMVT, and steCRV as packaging, not as new truth boundaries. If the economic answer still bottoms out in Curve get_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 now primitive. 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 new MAI may be printed.

Audit prompts

  • Which exact functions turn Curve-LP-derived collateral into MAI mint 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-sensitive get_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 estimate and debt-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 completes fail 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 liquidation or price feed explicitly?

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 estimate and safe to authorize new debt are 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

Control surface