0xb3094734fe249a7b0110dc12d66f6c404ada28cbEthereum0xa89f6d04f4cec87ef9837593610076b5563b3569EthereumAn unprivileged attacker drained 0.66 WETH from two dormant Ethereum contracts, 0xb3094734FE249A7b0110dC12D66F6C404aDA28Cb and 0xa89f6d04f4cEc87ef9837593610076B5563B3569, by directly invoking their Uniswap V3 callback logic from spoof contract 0xa826dacf14a462bca2a6e4de4c27f20ed7b43b1d. In transaction 0x83c71a83656b0fecfa860e76a9becf738930b3f1b2510c7d0339ab585090a82d, victim A transferred 360000000000000000 wei of WETH to the spoof contract; in transaction 0xf93db4cdee0ed2af06067a9c953ebc62dd17f70be37961636c42d698cc23e932, victim B transferred 300000000000000000 wei. The root cause was that the callback settlement path trusted msg.sender as a pool-like contract and derived token0() / token1() from that caller without authenticating it as a canonical Uniswap V3 pool.
A Uniswap V3 swap callback is privileged logic. It is only safe when the callee verifies that msg.sender is the canonical pool for the expected token pair and fee tier. If a contract skips that authentication step and instead asks msg.sender for pool metadata, any attacker contract can impersonate a pool by implementing and .
token0()token1()Here, both victims held residual WETH balances before the exploit block. The state immediately before block 20683453 shows victim A holding 0.36 WETH and victim B holding 0.30 WETH, which matches the preconditions later reproduced in the PoC and described in the oracle definition.
The vulnerability is an unauthenticated callback-settlement path. Both victims expose logic reachable through callback selector 0xfa461e33, and that path treats the caller as a pool counterparty based on the caller’s own interface responses. The saved disassembly for both victims shows the same critical structure: CALLER, then STATICCALL into selectors 0xd21220a7 and 0x0dfe1681, then a later ERC20 transfer(address,uint256) call using selector 0xa9059cbb. The spoof contract’s own disassembly shows that it exposes the same selectors on its dispatcher, so it can satisfy the victims’ pool-like metadata checks. That means the attacker controls both reachability and perceived asset identity, and only needs to choose a positive callback delta to pull WETH. The invariant violation is precise: callback settlement transferred tokens before proving that msg.sender was an authentic Uniswap V3 pool.
The first seed transaction, 0x83c71a83656b0fecfa860e76a9becf738930b3f1b2510c7d0339ab585090a82d, was sent by EOA 0xfe51ffcd2af4748d77130646988f966733583dc1 to spoof contract 0xa826dacf14a462bca2a6e4de4c27f20ed7b43b1d. The transaction input embeds forged callback selector 0xfa461e33 and a positive amount0Delta equal to victim A’s entire WETH balance.
The on-chain trace shows the decisive runtime behavior:
0xA826daCf14a462bca2A6e4de4c27F20ED7B43B1D::46bfea20(...)
├─ 0xA826daCf14a462bca2A6e4de4c27F20ED7B43B1D::token0() [staticcall]
├─ WETH9::transfer(0xA826daCf14a462bca2A6e4de4c27F20ED7B43B1D, 360000000000000000)
The second seed transaction, 0xf93db4cdee0ed2af06067a9c953ebc62dd17f70be37961636c42d698cc23e932, repeats the same pattern against victim B:
0xA826daCf14a462bca2A6e4de4c27F20ED7B43B1D::46bfea20(...)
├─ 0xA826daCf14a462bca2A6e4de4c27F20ED7B43B1D::token0() [staticcall]
├─ WETH9::transfer(0xA826daCf14a462bca2A6e4de4c27F20ED7B43B1D, 300000000000000000)
The code artifacts explain why those traces succeed. Both victims share the same core callback structure in disassembly:
00000264: CALLER
0000026e: PUSH4 0xd21220a7
0000029f: STATICCALL
000002dc: CALLER
000002e6: PUSH4 0x0dfe1681
00000317: STATICCALL
...
00001d44: PUSH4 0xa9059cbb
00001d62: CALL
This proves the root cause is not merely observational trace correlation. The bytecode path itself uses CALLER as the source of token metadata and later executes an ERC20 transfer, exactly matching the exploit traces. The spoof contract disassembly independently shows a dispatcher exposing 0x46bfea20, 0x0dfe1681, and 0xd21220a7, which is sufficient to impersonate the pool-facing interface expected by the victims. Because the victim never authenticates msg.sender against a canonical Uniswap V3 factory, the attacker can call the callback directly and make the victim settle WETH to the spoof contract.
The attacker used a simple two-transaction sequence from a single cluster. The EOA 0xfe51ffcd2af4748d77130646988f966733583dc1 sent both transactions, and the spoof contract 0xa826dacf14a462bca2a6e4de4c27f20ed7b43b1d served as both the callback caller and the WETH recipient.
The spoof contract’s dispatcher evidence is:
00000013: PUSH4 0x46bfea20
0000004a: PUSH4 0xd21220a7
00000065: PUSH4 0x0dfe1681
00000702: PUSH4 0xa9059cbb
Operationally, the adversary flow is:
1. Use spoof contract as msg.sender-facing pool impostor.
2. Call victim callback with forged selector 0xfa461e33 and positive amount0Delta.
3. Victim queries spoof contract for token metadata via token0()/token1().
4. Spoof contract reports WETH as token0.
5. Victim settles the positive delta by calling WETH.transfer(msg.sender, amount0Delta).
6. Repeat against the second victim.
Both seed transactions are permissionless normal transactions, so the strategy fits the ACT model: no privileged keys, no attacker-only artifacts, and no special off-chain dependencies beyond normal transaction submission.
The measured loss is the complete WETH balances held by the two victim contracts at the exploited callback entrypoint. Victim A lost 360000000000000000 wei of WETH and victim B lost 300000000000000000 wei, for a combined total of 660000000000000000 wei, or 0.66 WETH. The spoof contract accumulated that full amount while the attacker paid only normal gas costs.