Reproduced Exploit
BaseSwapCallback Exploit — Public `uniswapV3SwapCallback` Approval Drain
An unverified Base router-like contract exposes uniswapV3SwapCallback(int256,int256,bytes) without verifying that msg.sender is a real Uniswap V3 pool created by a trusted factory.
Loss
~55.51 WETH (~$220K) drained from a single approver
Chain
Base
Category
access-control
Date
Oct 2025
EVM Playground
Source-level debugger — step opcodes and Solidity in sync
The attack is replayed in an in-browser EVM preloaded with the exact dumped fork state. The execution tree shows every call; step by Solidity line or by opcode across all depths — source, Stack, Memory, Storage, Transient storage and Return value stay in sync. Click a tree node, opcode, or source line to jump. No backend, no live RPC.
Source & credit. Exploit reproduction, trace data, and analysis adapted from DeFiHackLabs by SunWeb3Sec — an open registry of reproduced on-chain exploits. Standalone Foundry PoC and full write-up: 2025-10-BaseSwapCallback_exp in the
evm-hack-registrymirror. Upstream DeFiHackLabs PoC:src/test/…/BaseSwapCallback_exp.sol.
Vulnerability classes: vuln/access-control/missing-auth · vuln/dependency/unsafe-external-call
Reproduction: isolated Foundry project at this project folder. Verbose run: output.txt. Victim is unverified; PoC spoofs a Uniswap V3 pool via a minimal
token0()contract.
Key info#
| Loss | |
| Vulnerable contract | 0xE143b486ab0413Df0D6DAd2caf6d2f61CAC54730 (unverified) |
| Victim / approver | 0xc0fFeE479E4cD49eafBA87449225e17c53251226 |
| Attacker EOA | 0x2a49c6fd18bd111d51c4fffa6559be1d950b8eff |
| Attacker contract | 0xf1a3686f4D394E52d9fdF60132e46EBdCD231B01 |
| Attack tx | 0x4449114ceaedd11e8f1363c5e53507198323a63cb6958dc26078fc016d0d4b27 |
| Chain / block / date | Base / 37,487,849 (fork 37,487,848) / ~Oct 30, 2025 |
| Bug class | Missing callback authentication — any contract that implements token0() can invoke uniswapV3SwapCallback and transferFrom prior approvers |
TL;DR#
An unverified Base router-like contract exposes uniswapV3SwapCallback(int256,int256,bytes) without verifying that msg.sender is a real Uniswap V3 pool created by a trusted factory.
Its only “auth” is effectively:
- Call
msg.sender.token0()(and related pool surface) so the caller looks like a pool. - Decode attacker-controlled
dataas(payer, recipient). WETH.transferFrom(payer, msg.sender, amount0Delta)whenamount0Delta > 0.
Anyone who previously approved the vulnerable contract can be drained by a one-line fake pool:
function token0() external pure returns (address) { return WETH; }
function exploit(uint256 amount) external {
bytes memory data = abi.encode(VICTIM, address(this));
IVulnerable(VULN).uniswapV3SwapCallback(int256(amount), -1, data);
}
Live attacker used a MEV-style multicall bot and ~219 repeated ~0.253 WETH pulls (same encoding) totaling 55.51 WETH. The PoC drains the full remaining balance in a single callback.
Root cause#
Correct Uniswap V3 integrators must authenticate the callback, e.g. via
CallbackValidation.verifyCallback(factory, PoolAddress.PoolKey({...})) so that
msg.sender == factory.getPool(token0, token1, fee).
This contract either:
- omits factory verification entirely, or
- only checks that
msg.senderresponds totoken0()/ returns a non-zero token,
which is trivial to spoof. Combined with arbitrary payer in callback data, any residual ERC-20 allowance becomes freely stealable.
Observed pre-attack state (fork block 37,487,848):
| Parameter | Value |
|---|---|
| Victim WETH balance | 55.514876455905996937 WETH |
allowance(victim, vuln) | ~2,996 WETH (far above balance) |
| Vulnerable code size | 5,591 bytes (unverified) |
Attack steps (PoC)#
- Fork Base at
37487849 - 1. - Deploy
FakePoolwithtoken0() → WETH. - Call
uniswapV3SwapCallback(victimBal, -1, abi.encode(victim, fakePool))fromFakePool. - Vulnerable contract pulls full victim WETH into
FakePoolviatransferFrom. - Sweep WETH to the attacker test contract.
Fix class#
- Require
msg.senderis the canonical pool for the swap path (CallbackValidation/ factorygetPool). - Never accept a user-controlled
payerunless that user ismsg.senderof the initiating swap and a reentrancy/lock flag proves an in-flight swap. - Prefer pulling tokens in the outer function (before
pool.swap) rather than in the callback from arbitrary payers.
Sources#
Sources & further analysis#
Reproductions & code
- Standalone PoC + full trace: 2025-10-BaseSwapCallback_exp (evm-hack-registry mirror).
- Upstream DeFiHackLabs PoC:
BaseSwapCallback_exp.sol. - Attack transaction: view on explorer.
Alerts & third-party analyses
- Original alert / thread: post on X.
- DeFiHackLabs incident explorer: search "BaseSwapCallback Exploit".
- Web3Sec X hacked database: search.
- Rekt leaderboard: search.
- Solodit incident search: search.
These dashboards index community alerts tweets, post-mortems, and independent write-ups. Reach them through the protocol name above to cross-check this reproduction against other analyses.