# RouterDrain: fake V3 pool drains router approvals via unverified callback

DEX router's V3 swap callback never verifies the calling pool, so a fake pool re-enters it to transferFrom any address that approved the router, draining ~62 WBNB.

- Payout: Real-world hack
- Severity: critical
- Program: RouterDrain
- Credited: DeFiHackLabs
- Disclosed: 2026-09-07
- Write-up quality: 8/10
- Original: https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/2026-09/RouterDrain_exp.sol
- Library page: https://bugbountylibrary.com/r/dhl%3A20260907-routerdrain
- Tags: domain:web3 impact:funds-loss kind:incident platform:defihacklabs sev:critical tech:evm tech:solidity technique:chained technique:reverse-engineering technique:source-review vuln:access-control vuln:business-logic

**Target:** DEX router / Uniswap-V3-style swap callback on BNB Chain
**Root cause:** The router's permissionless swap entry (0x33411b5e) calls pool.swap() on an attacker-supplied pool address with no factory-derived verification (factoryV3() returns address(0)), and uniswapV3SwapCallback decodes an attacker-controlled `payer` from pool-echoed data and runs transferFrom(payer, msg.sender, amount) without binding msg.sender to a real pool or payer to the swap initiator.
**Steps:**
1. Fork BNB Chain at block 120524633 (parent of attack tx 0x40eb2236...).
2. Confirm `IRouterVuln(ROUTER).factoryV3() == address(0)` so no canonical pool can be derived.
3. Deploy FakePool implementing only `swap()`; arm it with victim address, tokenIn, and amount.
4. Ensure victim has a standing approval to ROUTER (real victims did).
5. Seed attacker with a small token balance since the entry gates on `balanceOf(recipient) >= amountIn`.
6. Call router entry `0x33411b5e` with a Route struct whose `pool` = FakePool and tokenIn = victim token.
7. Router calls FakePool.swap(); FakePool re-enters `router.uniswapV3SwapCallback(0xfa461e33)` with `data` = abi.encode({path, payer: victim, recipient, amount}).
8. Router executes `transferFrom(victim, FakePool, amount)` under victim's approval; repeat to drain full balance.
**How found:** Router was unverified; researcher recovered the ABI from the attack tx trace, noticed factoryV3() returns zero (no pool verification possible), and observed the callback decodes a caller-supplied `payer` — hypothesizing any contract implementing swap() could re-enter and pull arbitrary approvals.
**Exploitation:** Attacker bootstrapped ~1 WBNB via PancakeV2 flash-swap into the target token, then repeatedly invoked the router's permissionless 0x33411b5e entry with a fake pool that re-entered the router's own uniswapV3SwapCallback naming each victim as payer, pulling their tokens under standing router approvals; looted tokens were sold to WBNB. ~62.28 WBNB drained across 29 victims.
**Escalation:** Iterated the drain per victim and per token, growing the gated amountIn each loop to capture full balances across 29 approvals.
**Insight:** On unverified routers/aggregators, read factoryV3()/factory getters and the callback's `data` decoding: if the callback trusts a pool-echoed `payer` and there's no factory to derive canonical pool addresses, any contract implementing the pool selector can re-enter and transferFrom arbitrary approvers.
