Meta Pool mpETH ERC-4626 mint() Without Asset Transfer / Unbacked Mint Pool Drain (2025-06-17)

  • Name: Meta Pool mpETH ERC-4626 mint() without asset transfer / unbacked mint pool drain
  • Date: 2025-06-17
  • Protocol: Meta Pool mpETH staking contract / fast-unstake liquidity path
  • Chain: Ethereum
  • Category: ERC-4626 override mismatch / unbacked mint / inheritance footgun / liquidity-pool drain
  • Loss: about 52.5 ETH on Ethereum mainnet, roughly $132k at the time, despite roughly 9,705 mpETH (about $27M notional) being minted without backing
  • Summary: Meta Pool’s Ethereum mpETH staking contract inherited OpenZeppelin’s ERC-4626 flow but split the ordinary deposit() path in a dangerous way: its custom deposit() pulled WETH/ETH in before calling an overridden _deposit(), while the inherited mint() entrypoint was left live and still assumed _deposit() itself enforced the asset-transfer side of the invariant. After draining Meta Pool’s internal fast-unstake pool, the attacker called inherited mint() with a huge share amount and no corresponding ETH transfer. Meta Pool’s custom _deposit() then minted the residual mpETH anyway, because once the pool had no mpETH left to hand out, the function fell through to the sharesToMint branch and treated previewMint()’s computed _assets as if they had already arrived. The attacker could not cash out the full notional because mpETH liquidity was thin, but they still drained roughly 52.5 ETH from Ethereum-side liquidity pools before the contract was paused.
  • Trigger / entry point:
    • On 2025-06-17, attackers targeted the Ethereum mpETH staking contract at 0x48AFbBd342F64EF8a9Ab1C143719b63C2AD81710.
    • Meta Pool publicly confirmed that the incident involved unauthorized minting through the ERC-4626 mint() function and that about 9,705 mpETH had been minted.
    • Public traces and reproductions show two key exploit transactions on Ethereum: a frontrunning Yoink transaction 0x4f43fc6d674e85f7d306debb4a3d48e7688c2fe5a6332dd9ad57558a15c86ef9, then the main attacker transaction 0x57ee419a001d85085478d04dd2a73daa91175b1d7c11d8a8fb5622c56fd1fa69.
    • The exploit path depended on the interaction between the inherited OpenZeppelin mint() entrypoint and Meta Pool’s overridden _deposit() implementation, not on a stolen key, oracle failure, or bridge message.
  • Root cause:
    • OpenZeppelin ERC-4626 expects both deposit() and mint() to converge on _deposit() as the state-changing function that finalizes the asset-for-shares exchange.
    • Meta Pool changed that contract boundary: its custom deposit() first pulled WETH from the user and unwrapped it into ETH, then called its own _deposit().
    • But Meta Pool did not override or disable inherited mint(). That meant the original ERC-4626 mint() remained publicly callable and still routed directly into Meta Pool’s custom _deposit().
    • Meta Pool’s _deposit() no longer enforced the asset-transfer side of the invariant. Instead it assumed the needed assets were already present, tried to source shares from the liquidUnstakePool, and minted any remainder via sharesToMint.
    • Once the attacker had emptied the fast-unstake pool, the _getmpETHFromPool() leg no longer satisfied the request, so _deposit() minted the full residual share amount even though no ETH had been transferred in by mint().
    • The decisive invariant failure was therefore: mpETH shares could be issued from the inherited issuance entrypoint without the backing ETH ever entering the system, while downstream liquidity pools still honored those shares as redeemable value.
  • Exploit path:
    1. The attacker flash-loaned 200 WETH from Balancer.
    2. They converted about 107 WETH to ETH and called Meta Pool’s depositETH() / deposit path to acquire real mpETH and drain the protocol’s internal fast-unstake liquidity.
    3. With the internal pool emptied, they called inherited ERC-4626 mint() for a large mpETH amount.
    4. mint() computed the notional asset amount through previewMint() and then called Meta Pool’s overridden _deposit().
    5. Because _deposit() no longer pulled assets itself and the internal pool could no longer satisfy the requested shares, it fell through to the residual sharesToMint branch and minted unbacked mpETH directly.
    6. The attacker swapped part of the newly minted mpETH through the Meta Pool ETH fast-unstake pool and the Uniswap mpETH/WETH pool, repaid the flash loan, and kept the remaining ETH plus residual mpETH inventory.
    7. A frontrunner (Yoink) copied the path first, so the realized Ethereum loss split across two exploit transactions rather than one uninterrupted attacker drain.
  • What the attacker needed to be true:
    • The inherited ERC-4626 mint() function had to remain publicly reachable.
    • Meta Pool’s custom _deposit() had to assume assets were already transferred rather than verifying or pulling them itself.
    • The internal liquidUnstakePool had to be drainable first, so _deposit() would mint residual shares instead of sourcing them from existing pool inventory.
    • External ETH-side liquidity pools had to still accept mpETH at nontrivial value after the unbacked mint.
    • No mint cap, per-call reserve-outflow cap, or same-transaction circuit breaker could stop the drain pool -> free mint -> dump into liquidity sequence.
  • Technical anatomy worth remembering:
    • The bug was not simply mint() was public; standard ERC-4626 expects mint() to be public. The important mistake was changing where the asset-transfer guarantee lived without also changing every inherited entrypoint that depended on that guarantee.
    • OpenZeppelin’s ordinary model is deposit() and mint() both calling _deposit(), with _deposit() responsible for the asset-for-share exchange boundary.
    • Meta Pool moved the transfer work into its custom deposit() implementation but left inherited mint() pointing at a custom _deposit() that now only distributed shares and updated internal accounting.
    • That inheritance mismatch meant the protocol still looked ERC-4626-shaped at the surface while no longer preserving ERC-4626’s internal assumptions.
    • The attacker first removed the honest sharesFromPool leg by draining the fast-unstake pool, then forced the protocol onto the unsafe sharesToMint fallback.
    • The case is especially useful because the issuance bug alone only printed claims; the realized theft still required a second step where external or sibling liquidity honored those claims for real ETH.

Three entrypoint-and-backing promotions this incident collapsed

  1. An inherited ERC-4626 sibling verb still existed and was publicly reachable

    • mint() remained live on the mpETH contract exactly because Meta Pool kept the OpenZeppelin standard surface.
    • That only proved a standard-shaped issuance path still exists.
  2. That sibling verb was still treated as if it hit the same proof-of-backing boundary as deposit()

    • This was the decisive false promotion. Meta Pool had already moved the real asset-receipt work into its custom deposit() path, but inherited mint() still routed into _deposit() as if _deposit() itself proved backing.
    • Once the attacker drained the honest sharesFromPool leg, the stale sibling path kept its old economic authority while the backing boundary had already moved elsewhere.
  3. The claims printed through that accepted sibling path still deserved live ETH-side exit authority

    • The exploit only became a reserve loss because the newly minted mpETH could still hit fast-unstake and AMM liquidity that treated those shares as redeemable value.
    • This is where the inheritance bug became an actual theft instead of only an accounting absurdity.
  • Representative transactions and addresses:
    • Vulnerable mpETH staking contract: 0x48AFbBd342F64EF8a9Ab1C143719b63C2AD81710
    • Frontrunning / Yoink transaction: 0x4f43fc6d674e85f7d306debb4a3d48e7688c2fe5a6332dd9ad57558a15c86ef9
    • Main attacker transaction: 0x57ee419a001d85085478d04dd2a73daa91175b1d7c11d8a8fb5622c56fd1fa69
    • Attacker EOA highlighted in public traces: 0x48f1d0F5831Eb6e544f8cBDe777b527b87a1BE98
    • Attacker cash-out wallet highlighted in public traces: 0x45DD46C45FE6681168927A261842ABb55a8B910a
    • Main Ethereum fast-unstake pool referenced in public analysis: 0xdF261F967E87B2aa44e18a22f4aCE5d7f74f03Cc
  • Response / mitigation:
    • Meta Pool publicly acknowledged the incident the same day, confirmed unauthorized mpETH minting through mint(), and said all validator-backed staked ETH remained safe.
    • The protocol paused the mpETH contract and disabled transfers to stop further dumping of unbacked shares into available liquidity.
    • Meta Pool stated that the directly affected ETH-side liquidity was largely DAO-provided and committed to make affected users whole.
    • The incident also prompted wider discussion of ERC-4626 inheritance hazards: when teams override only part of the standard flow, inherited public entrypoints can quietly preserve assumptions the custom contract no longer satisfies.

Pattern tags:

  • Shared-standard entrypoint drift: Meta Pool moved the asset-receipt proof out of the shared ERC-4626 _deposit() primitive but left inherited mint() live and economically authoritative.
  • Issuance / redemption solvency asymmetry: the contract could print mpETH claims before backing arrived, while sibling liquidity venues still honored those claims for real ETH.
  • Fallback-path cash-out dependence: the exploit only realized loss after the attacker first exhausted the honest sharesFromPool branch and forced issuance onto the unsafe sharesToMint remainder path.
  • Thin-liquidity partial monetization: market depth limited realized loss, but did not change the core severity that roughly 9,705 mpETH had been minted without backing.
  • Design companions: shared-standard-entrypoint-invariant-drift, issuance-redemption-solvency-asymmetry, entrypoint-equivalence-under-shared-standards, and unsupported-standard-verb-revocation-and-fail-closed-surface.

Why the exploit worked

  • OpenZeppelin ERC-4626 treats deposit() and mint() as sibling verbs that share one receipt-and-issuance primitive. Meta Pool preserved the surface shape of that standard but broke the shared invariant underneath it.
  • By moving the transfer / unwrap step into custom deposit() logic, Meta Pool made _deposit() depend on assets already being present. Inherited mint() still called _deposit() directly, so one standard verb now reached an issuance primitive that no longer proved receipt.
  • The attacker first removed the honest source of shares by draining the liquidUnstakePool. That forced _deposit() onto the residual minting branch, where the protocol treated previewMint()’s notional asset calculation as if the assets had already crossed the boundary.
  • Once unbacked mpETH existed, external and sibling ETH liquidity provided the redemption rail. The exploit therefore needed both halves of the asymmetry: false issuance first, then still-honest downstream pricing of the counterfeit claim.
  • The frontrunning Yoink transaction also underlines that once an unsupported inherited verb remains publicly reachable, monetization becomes mechanically obvious to any fast follower watching the mempool.

Detection and audit pivots

  • For every inherited standard like ERC-4626, trace all sibling public verbs back to the one primitive that proves receipt, burn, or solvency. If a custom override moves that proof boundary, unsupported siblings must be disabled explicitly.
  • Diff overridden functions against inherited entrypoints, not only against each other. The sharp review question here was what still calls custom _deposit() after we moved asset transfer out of it?
  • Fuzz branch forcing, not just nominal flows: empty the internal liquidity leg, then exercise mint() / deposit() / helper paths to see whether fallback issuance logic still assumes assets arrived.
  • Treat preview helpers as quotes, not proof. If previewMint() or similar math computes a notional asset amount, some later step still has to prove those assets actually crossed the boundary before shares become final.
  • Audit whether thin external liquidity, internal fast-unstake pools, or router integrations can still honor counterfeit shares immediately after an issuance bug. The ability to dump matters because it turns a printing flaw into realized reserve loss.

Failure-prevention checklist

  • Keep one shared proof-of-backing primitive for every issuance verb. If deposit() and mint() are both live, both must still converge on a function that proves assets actually arrived before shares become final.
  • If a customization moves receipt, unwrap, or custody-transfer logic out of a standard primitive like _deposit(), explicitly disable or override every inherited sibling entrypoint that still depends on the old invariant.
  • Treat internal liquidity branches and residual mint branches as separate trust boundaries. If one leg sources shares from inventory and another leg mints new shares, each leg must independently prove backing rather than inheriting a proof performed elsewhere.
  • Force branch exhaustion in tests. Empty the fast-unstake / internal-inventory path first, then exercise mint(), deposit(), helper paths, and router paths to verify that the fallback branch cannot print claims against zero fresh assets.
  • Treat preview functions as math helpers only. previewMint() and similar routines may quote required assets, but a later state-changing step still has to verify the assets crossed the boundary.
  • Put caps on unsupported blast radius: per-call mint ceilings, per-block reserve-outflow throttles, supply-growth alarms, and same-transaction circuit breakers help keep an inheritance mistake from turning instantly into pool-wide loss.
  • Keep standard-surface compatibility explicit in code review. We only use depositETH() is not a safety argument if inherited deposit() or mint() remains publicly callable.
  • Rehearse liquidity-side containment. If counterfeit shares ever print, pause authority, transfer disablement, and DEX / fast-unstake response paths should already be ready, because thin liquidity limits loss only if the protocol can close exits fast.

Audit prompts

  • After every override, which inherited public verbs still reach this customized internal primitive?
  • Where, exactly, is the proof that underlying assets arrived: in deposit(), in _deposit(), in a wrapper-specific helper, or nowhere on one path?
  • If the internal inventory / liquidity branch is fully exhausted, what does the fallback branch do next, and what evidence of backing does it require before minting residual shares?
  • Can mint() or another sibling verb still be called directly even though the team mentally treats the contract as depositETH()-only or deposit()-only?
  • Does any branch confuse quoted assets needed with assets received, especially when preview helpers or cached balances feed the next step?
  • What invariant test proves that total supply cannot grow unless assets, collateral, or another explicit backing object grew in the same operation?
  • If unbacked shares were printed right now, which pools, routers, AMMs, or fast-unstake paths would still honor them for stronger assets before operators can pause?
  • Are pause and transfer-disable controls scoped tightly enough to stop the exit rail that would monetize a printing bug, not just the original mint entrypoint?

Lessons that should already be standard

  • Shared standards are shared-invariant systems, not just shared function names. If a team moves the real receipt check out of the standard primitive, every inherited sibling verb becomes a solvency review item immediately.
  • Inherited but unused is often fake safety. Public entrypoints stay part of the protocol’s balance sheet whether product docs mention them or not.
  • Branch-local correctness is not enough. Meta Pool’s customized deposit() path and customized _deposit() path only looked safe when read in isolation; the exploit lived in the gap between them.
  • Preview math is not settlement proof. A correct previewMint() result does not justify issuance unless a later step proves the quoted assets actually arrived.
  • Thin liquidity reduces realized theft, not root severity. Printing roughly $27M of unbacked mpETH was already catastrophic even though only a much smaller slice could be dumped before containment.
  • Fast-unstake pools, AMMs, and sibling liquidity venues are part of the same issuance security boundary. A counterfeit-claim bug becomes a reserve-loss bug the moment one honest exit path still treats the printed receipt token as money-good.
  • ERC-4626-family review should split at least three questions that teams still collapse too often: where receipt is proved, which sibling verbs still rely on that proof, and which downstream venues will honor the shares if that proof quietly disappears on one path.

Why this incident matters

  • It preserves a clean issuance side prints claims without assets, exit side still honors them exploit path outside the older bonding-curve examples.
  • It is a strong example of an inheritance footgun where the individual overridden functions looked plausible in isolation, but the composition of inherited and overridden entrypoints broke the real solvency boundary.
  • The healthier preserved boundary was not just audit ERC-4626 carefully; it was keep one shared proof-of-backing primitive across deposit/mint siblings, or explicitly kill the unsupported inherited verb once that shared primitive stops proving receipt itself.
  • It also shows why thin liquidity can limit realized losses without reducing exploit severity: the protocol still minted roughly $27M of unbacked mpETH; the market simply could not absorb the whole fiction before defenders reacted.
  • The note is especially useful beside pure ERC-4626 first-deposit discussions because the exploit was not about vault-share rounding at all. It was about moving the receipt check out of the shared issuance primitive and forgetting that another standard entrypoint still depended on that primitive to keep the vault honest.

Internal linkages

  • Core recurring pattern: issuance-redemption-solvency-asymmetry.
  • Sharper standards-level recurring pattern: shared-standard-entrypoint-invariant-drift — the direct standard surface stayed live after the receipt invariant moved abstraction of this incident.
  • Positive-design companion: entrypoint-equivalence-under-shared-standards — the sharper healthy-side question is how deposit() and mint() were supposed to keep one shared proof-of-backing boundary after Meta Pool customized the ERC-4626 flow.
  • Positive-design companion: unsupported-standard-verb-revocation-and-fail-closed-surface — the alternate safe answer would have been to make inherited mint() fail closed once Meta Pool moved receipt enforcement out of _deposit() and no longer intended the sibling verb to remain economically equivalent.
  • Adjacent standards-level cousin: first-deposit-inflation — another ERC-4626-family failure where the standard surface looks familiar but the real asset-for-share boundary becomes attacker-shaped at low-liquidity edges.
  • Adjacent share-accounting family: donation-share-price-manipulation — also about receipt tokens whose printed value outruns the backing actually entering the system, though Meta Pool failed at issuance rather than at exchange-rate accounting.
  • Useful contrast: truebit-purchase-bonding-curve-overflow-zero-cost-mint-eth-drain-2026-01-08 — both incidents let a broken issuance path print claims that a still-live exit path honored against real reserves, but Truebit failed through legacy arithmetic while Meta Pool failed through inherited-standard boundary drift.

Control surface