Calculated from recorded token losses using historical USD prices at the incident time.
0x661505c39efe1174da44e0548158db95e8e71ce867d5b7190b9eabc9f314fe910xe51d3de9b81916d383ef97855c271250852ec7b7Ethereum0xa718aa1b3f61c2b90a01ab244597816a7ee69fd2EthereumThe exploit transaction 0x661505c39efe1174da44e0548158db95e8e71ce867d5b7190b9eabc9f314fe91 turned Uniswap UniversalRouter into a permissionless minting surface for the Scroll token at 0xe51d3de9b81916d383ef97855c271250852ec7b7. The router started with zero Scroll, but the token's whitelisted transfer path allowed router-originated transfers to underflow the sender balance modulo 2^256 instead of reverting. The attacker used that synthetic balance to push 136275915391323680414683000 Scroll into the WETH/Scroll pair at 0xa718aa1b3f61c2b90a01ab244597816a7ee69fd2, drained 76360109345510532175 WETH, unwrapped it, and realized 76351767010597843990 wei of net ETH profit.
UniversalRouter at 0x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad exposes execute(bytes,bytes[],uint256) publicly. Its 0x05 TRANSFER command instructs the router to call token.transfer(recipient, amount) from the router's own address. That matters because the Scroll token had already whitelisted both UniversalRouter and the WETH/Scroll pair, while tradeEnable() was still false in the exploit pre-state at block 19971610.
The relevant pre-state is visible from chain data:
Scroll.balanceOf(UniversalRouter) = 0
Scroll.isW(UniversalRouter) = true
Scroll.isW(WETH/Scroll pair) = true
Scroll.tradeEnable() = false
pair reserves = (76436699224492990081 WETH, 136275915391323680414683 Scroll, 1716938231)
Those values are asserted in the PoC and match the collector artifacts for the block immediately before the exploit.
The violated invariant is straightforward: if balanceOf(sender) == 0, then any positive ERC20 transfer from that sender must revert or otherwise fail. Scroll violated that invariant for transfers initiated by a whitelisted caller. In this case the whitelisted caller was UniversalRouter, and its public entrypoint made the buggy path permissionlessly reachable by any EOA.
The deterministic signature is the post-transfer remainder on the router. During the exploit, the router transferred 1 Scroll to the token owner and 136275915391323680414683000 Scroll to the pair, both from an initial router balance of zero. After those two transfers, the router still held 115792089237316195423570985008687907853269984665640427763542192684232714956935 Scroll, which is exactly the complementary modulo-2^256 remainder. The attacker then swept that remainder with the router's CONTRACT_BALANCE sentinel, confirming that the token used unchecked sender-balance arithmetic instead of enforcing ERC20 balance conservation.
The collector trace shows the helper contract created inside the exploit transaction calling UniversalRouter directly:
UniversalRouter::execute(0x05, [... owner ..., 1])
Scroll::transfer(owner, 1)
emit Transfer(from: UniversalRouter, to: owner, value: 1)
UniversalRouter::execute(0x05, [... pair ..., 136275915391323680414683000])
Scroll::transfer(pair, 136275915391323680414683000)
emit Transfer(from: UniversalRouter, to: pair, value: 136275915391323680414683000)
That pair of transfers should have been impossible because the router held zero Scroll before execution. Instead, the collector trace and the independent forge replay both show the router balance becoming the exact modulo remainder:
Scroll::balanceOf(UniversalRouter)
=> 115792089237316195423570985008687907853269984665640427763542192684232714956935
This is not normal swap behavior or ordinary MEV. The pair only becomes drainable because the router-originated token transfer manufactures a synthetic Scroll input larger than the entire pre-state pair reserve. Once that synthetic input lands in the pair, standard UniswapV2 pricing releases almost all WETH liquidity:
WETH_Scroll_Pair::swap(76360109345510532175, 0, attacker, 0x)
WETH::transfer(attacker, 76360109345510532175)
The attacker then unwraps WETH to ETH and forwards profit to the originating EOA. The seed balance diff records the attacker EOA moving from 90987921237294200 wei to 76442754931835138190 wei, for a net gain of 76351767010597843990 wei after gas. The final router sweep uses the remaining synthetic balance, proving the bug is not limited to one over-large transfer but to the sender-balance accounting itself.
The attack is a single-transaction ACT sequence executed by EOA 0x55db954f0121e09ec838a20c216eabf35ca32cdd, which deploys helper contract 0x55f5aac4466eb9b7bbeee8c05b365e5b18b5afcc and then performs the exploit from that helper.
1. Deploy helper contract with public protocol addresses baked in.
2. Call UniversalRouter TRANSFER to send 1 Scroll from router to token owner.
3. Call UniversalRouter TRANSFER again to send 136275915391323680414683000 Scroll from router to pair.
4. Swap the synthetic Scroll input against the pair for 76360109345510532175 WETH.
5. Withdraw WETH to ETH and forward ETH profit to the attacker EOA.
6. Call UniversalRouter TRANSFER with CONTRACT_BALANCE semantics to sweep the router's modulo remainder.
The execution is permissionless because every dependency is public: the router function is public, the token owner is public, whitelist state is public, pair reserves are public, and no attacker-only artifact is needed to realize the path.
The measured loss is the WETH removed from the WETH/Scroll pool:
{
"token_symbol": "WETH",
"amount": "76360109345510532175",
"decimal": 18
}
That WETH was immediately unwrapped and realized as ETH profit. The exploit also demonstrates that any whitelisted-router balance on the broken token could be synthetically created and spent, so the accounting failure is broader than the single drained pool.
0x661505c39efe1174da44e0548158db95e8e71ce867d5b7190b9eabc9f314fe910xe51d3de9b81916d383ef97855c271250852ec7b70x3fc91a3afd70395cd496c647d5a6cc9d4b2b7fad0xa718aa1b3f61c2b90a01ab244597816a7ee69fd20x72c509b05a44c4bb53373efc2e76fb75fa8108a6