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
mpETHstaking contract / fast-unstake liquidity path - Chain: Ethereum
- Category: ERC-4626 override mismatch / unbacked mint / inheritance footgun / liquidity-pool drain
- Loss: about
52.5 ETHon Ethereum mainnet, roughly$132kat the time, despite roughly9,705 mpETH(about$27Mnotional) being minted without backing - Summary: Meta Pool’s Ethereum
mpETHstaking contract inherited OpenZeppelin’s ERC-4626 flow but split the ordinarydeposit()path in a dangerous way: its customdeposit()pulled WETH/ETH in before calling an overridden_deposit(), while the inheritedmint()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 inheritedmint()with a huge share amount and no corresponding ETH transfer. Meta Pool’s custom_deposit()then minted the residualmpETHanyway, because once the pool had nompETHleft to hand out, the function fell through to thesharesToMintbranch and treatedpreviewMint()’s computed_assetsas if they had already arrived. The attacker could not cash out the full notional becausempETHliquidity was thin, but they still drained roughly52.5 ETHfrom Ethereum-side liquidity pools before the contract was paused. - Trigger / entry point:
- On
2025-06-17, attackers targeted the EthereummpETHstaking contract at0x48AFbBd342F64EF8a9Ab1C143719b63C2AD81710. - Meta Pool publicly confirmed that the incident involved unauthorized minting through the ERC-4626
mint()function and that about9,705 mpETHhad been minted. - Public traces and reproductions show two key exploit transactions on Ethereum: a frontrunning
Yoinktransaction0x4f43fc6d674e85f7d306debb4a3d48e7688c2fe5a6332dd9ad57558a15c86ef9, then the main attacker transaction0x57ee419a001d85085478d04dd2a73daa91175b1d7c11d8a8fb5622c56fd1fa69. - 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.
- On
- Root cause:
- OpenZeppelin ERC-4626 expects both
deposit()andmint()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-4626mint()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 theliquidUnstakePool, and minted any remainder viasharesToMint. - 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 bymint(). - 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.
- OpenZeppelin ERC-4626 expects both
- Exploit path:
- The attacker flash-loaned
200 WETHfrom Balancer. - They converted about
107 WETHto ETH and called Meta Pool’sdepositETH()/ deposit path to acquire realmpETHand drain the protocol’s internal fast-unstake liquidity. - With the internal pool emptied, they called inherited ERC-4626
mint()for a largempETHamount. mint()computed the notional asset amount throughpreviewMint()and then called Meta Pool’s overridden_deposit().- Because
_deposit()no longer pulled assets itself and the internal pool could no longer satisfy the requested shares, it fell through to the residualsharesToMintbranch and minted unbackedmpETHdirectly. - The attacker swapped part of the newly minted
mpETHthrough the Meta Pool ETH fast-unstake pool and the UniswapmpETH/WETHpool, repaid the flash loan, and kept the remaining ETH plus residualmpETHinventory. - A frontrunner (
Yoink) copied the path first, so the realized Ethereum loss split across two exploit transactions rather than one uninterrupted attacker drain.
- The attacker flash-loaned
- 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
liquidUnstakePoolhad 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
mpETHat 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 liquiditysequence.
- The inherited ERC-4626
- Technical anatomy worth remembering:
- The bug was not simply
mint() was public; standard ERC-4626 expectsmint()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()andmint()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 inheritedmint()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
sharesFromPoolleg by draining the fast-unstake pool, then forced the protocol onto the unsafesharesToMintfallback. - 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.
- The bug was not simply
Three entrypoint-and-backing promotions this incident collapsed
-
An inherited ERC-4626 sibling verb still existed and was publicly reachable
mint()remained live on thempETHcontract exactly because Meta Pool kept the OpenZeppelin standard surface.- That only proved
a standard-shaped issuance path still exists.
-
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 inheritedmint()still routed into_deposit()as if_deposit()itself proved backing. - Once the attacker drained the honest
sharesFromPoolleg, the stale sibling path kept its old economic authority while the backing boundary had already moved elsewhere.
- This was the decisive false promotion. Meta Pool had already moved the real asset-receipt work into its custom
-
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
mpETHcould 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.
- The exploit only became a reserve loss because the newly minted
- Representative transactions and addresses:
- Vulnerable
mpETHstaking contract:0x48AFbBd342F64EF8a9Ab1C143719b63C2AD81710 - Frontrunning /
Yoinktransaction: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
- Vulnerable
- Response / mitigation:
- Meta Pool publicly acknowledged the incident the same day, confirmed unauthorized
mpETHminting throughmint(), and said all validator-backed staked ETH remained safe. - The protocol paused the
mpETHcontract 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.
- Meta Pool publicly acknowledged the incident the same day, confirmed unauthorized
Pattern tags:
- Shared-standard entrypoint drift: Meta Pool moved the asset-receipt proof out of the shared ERC-4626
_deposit()primitive but left inheritedmint()live and economically authoritative. - Issuance / redemption solvency asymmetry: the contract could print
mpETHclaims 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
sharesFromPoolbranch and forced issuance onto the unsafesharesToMintremainder path. - Thin-liquidity partial monetization: market depth limited realized loss, but did not change the core severity that roughly
9,705 mpETHhad 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()andmint()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. Inheritedmint()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 treatedpreviewMint()’s notional asset calculation as if the assets had already crossed the boundary. - Once unbacked
mpETHexisted, 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
Yoinktransaction 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()andmint()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 inheriteddeposit()ormint()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 asdepositETH()-only ordeposit()-only? - Does any branch confuse
quoted assets neededwithassets 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 unusedis 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
$27Mof unbackedmpETHwas 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 themexploit 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 waskeep 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
$27Mof unbackedmpETH; 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 movedabstraction of this incident. - Positive-design companion: entrypoint-equivalence-under-shared-standards — the sharper healthy-side question is how
deposit()andmint()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
-
The decisive leverage sat in Meta Pool’s ERC-4626 inheritance boundary: which function actually enforced asset receipt, which entrypoints still reached the shared issuance primitive, how the fast-unstake pool interacted with the residual mint path, and how much external liquidity would still honor
mpETHonce unbacked supply existed. -
Put differently, the preserved healthy object was
one backing boundary across sibling issuance verbs, or one explicit fail-closed retirement of the sibling you no longer support. Meta Pool kept neither. -
The practical lesson is blunt: if a protocol moves the receipt check out of a standard shared primitive like
_deposit(), every inherited entrypoint that still relies on that primitive becomes part of the solvency boundary whether the team remembers it or not. -
Sources:
- https://medium.com/meta-pool/meta-pool-security-incident-on-mpeth-contract-on-ethereum-ba7a328d45c6
- https://www.quillaudits.com/blog/hack-analysis/meta-pool-hack-analysis
- https://blog.solidityscan.com/meta-pool-hack-analysis-fef7796f2303/
- https://medium.com/@lodelux/meta-pool-27m-june-hack-a-technical-breakdown-057cf44e850c
- https://raw.githubusercontent.com/SunWeb3Sec/DeFiHackLabs/main/src/test/2025-06/MetaPool_exp.sol
- https://docs.openzeppelin.com/contracts/4.x/erc4626
-
Last reviewed: 2026-08-01 UTC