# Meta Pool mpETH exploit - June 2025, Ethereum

Citations are numbered; URLs are in Sources.

## 1. What happened
On **June 17, 2025** Meta Pool's Ethereum liquid-staking vault (`Staking`; share token **mpETH**,
`0x48AFbBd342F64EF8a9Ab1C143719b63C2AD81710`) was exploited: an attacker **minted 9,705 mpETH
without depositing any ETH**, then swapped part of it into ETH via Meta Pool's own mpETH/ETH swap
pool and a Uniswap V3 mpETH/WETH pool [1][2]. Meta Pool's own report says the unauthorised minting
caused **~52.5 ETH of loss** from the swap pools (15 ETH in its mpETH<>wETH pool; 37.5 ETH on
Uniswap), all DAO-provided liquidity [3]. QuillAudits gives the same 37.51 WETH + 15 ETH = 52.5 ETH
(~$130k) [2]; OKX and crypto.news put the realised loss at **52.5 ETH (~$132k-$133k)** [4][5] while
the minted tokens were notionally worth **~$27M** [4][5].

Sources disagree on some figures: SolidityScan reports a **combined multi-chain loss ~56.35 ETH
(~$142k)** across Ethereum, Optimism and Linea [1]; SlowMist's figure, cited by Quadriga, is **~$25k**
[6]. A separate MEV front-runner ("Yoink") took ~37.5 WETH (~$117k) in a different transaction,
which Meta Pool later said was returned [2][6]. A DeFiHackLabs reproduction measures ~94.7 ETH of
value per attack tx, a PoC figure, not the realised loss [7]. What was taken was pool ETH; stakers'
913 ETH principal (delegated to SSV operators) was untouched and users were promised reimbursement
[3].

## 2. Root cause
Not a stolen key or tricked signer: a smart-contract logic/access-control bug. Meta Pool's `Staking`
inherits OpenZeppelin `ERC4626Upgradeable`, but its asset is **native ETH**, not the ERC-20 the
standard assumes, so the team overrode the internal `_deposit` to drop OZ's
`safeTransferFrom(_asset, caller, address(this), assets)` and consume the ETH arriving as
`msg.value`. That adaptation was applied to `depositETH()` (payable) and `deposit(WETH)` only; the
**inherited, non-payable `mint(uint256 shares, address receiver)` was left untouched** [7][1].
Since `mint()` is not payable, `msg.value == 0`, yet it still calls the overridden `_deposit` with
`assets = previewMint(shares)`; that override mints the shares and does `totalUnderlying += assets`
while pulling in nothing. Any caller could therefore call `mint(shares, self)` and get freshly
minted mpETH for free; the whitelist gate that might have limited access was disabled at the time
[7]. An unrestricted, permissionless `mint()` plus the missing "did we actually receive the assets?"
check sit at the centre - matching Meta Pool's own words that the attack "involved the unauthorized
minting of tokens through the ERC4626 mint() function" [3].

## 3. Pattern
Class: **missing access control / unvalidated input on an inherited function** (a mis-adapted
ERC-4626 vault), shape = free mint + instant liquidity. SolidityScan maps it to OWASP **SC01: Access
Control / SCWE-016** [1]; the DeFiHackLabs entry tags it logic/incorrect-state-transition +
access-control/missing-check [7]. Shape:

```
_deposit(caller, receiver, assets, shares):   // ETH arrived via msg.value
    // swap path only runs if ETH received and pool holds mpETH
    _mint(receiver, shares)                   // no check that assets were received
    totalUnderlying += assets

mint(shares, receiver):                        // inherited, NOT payable -> msg.value == 0
    assets = previewMint(shares)
    _deposit(msg.sender, receiver, assets, shares)   // mints for free
```

The general pattern: a standard funnels both `deposit` and `mint` through one internal `_deposit`;
if a fork removes asset collection from `_deposit` but keeps an entry point that no longer delivers
the asset, that path becomes a faucet. Thin AMM liquidity made the free shares immediately
redeemable for real ETH (a mint-and-dump arbitrage: mint at the vault rate, dump into a pricier
pool) [8].

## 4. The check that would have caught it
An **invariant/fuzz test asserting the vault's accounting identity**: every path that increases
shares or `totalUnderlying` must have increased the vault's *actual* custodied assets (native ETH
balance + WETH) by at least the `assets` credited. Fuzz each public entry point (`depositETH`,
`deposit`, `mint`, ...): snapshot `balanceOf(vault)` and `totalUnderlying()` before/after and assert
that the increase in `totalUnderlying` is <= the increase in assets actually received. Add a unit
test calling `mint(shares, attacker)` with **no `msg.value` and no approval** and assert it reverts
(it must, since `mint` is not payable, instead of emitting a `Deposit` event) [7]. Review rule: whenever a fork drops the transfer step from
`_deposit`, enumerate **every** inherited entry point that reaches `_deposit` and either make it
payable-with-check (`require(msg.value == previewMint(shares))`) or disable it (`override` to
revert; `maxMint = 0`) [7]. Operational backstop (which Meta Pool actually used): alarm when
`totalUnderlying` rises with no matching ETH inflow, then pause [3][7].

## 5. Sources
1. SolidityScan, Meta Pool Hack Analysis - https://blog.solidityscan.com/meta-pool-hack-analysis-fef7796f2303/
2. QuillAudits, Meta Pool hack analysis - https://www.quillaudits.com/blog/hack-analysis/meta-pool-hack-analysis
3. Meta Pool, security incident on mpETH (2025-06-17) - https://medium.com/meta-pool/meta-pool-security-incident-on-mpeth-contract-on-ethereum-ba7a328d45c6
4. OKX Learn - https://www.okx.com/en-us/learn/meta-pool-mpeth-eth-exploit
5. crypto.news - https://crypto.news/meta-pool-exploited-for-133k-after-attacker-mints-27m-worth-of-tokens/
6. Quadriga Initiative case study - https://www.quadrigainitiative.com/casestudy/metapoolerc4626mpethmintwithoutethflawexploited.php
7. crypto.training / DeFiHackLabs reproduction - https://crypto.training/hacks/2025-06-MetaPool/
8. Web3 Security Notion, mpETH Mint/Swap Price Discrepancy - https://web3sec.notion.site/MetaPool-mpETH-Liquid-Staking-Mint-Swap-Price-Discrepancy-383729274113814d9404cc2952237319
