NOON Pool Drain via Public transfer
Exploit Transactions
0x23fb7f093e827ed061aafb574cfd420eab879621c7f78cb341292e106a3a88c0Victim Addresses
0x421a5671306cb5f66ff580573c1c8d536e266c93Ethereum0x6feac5f3792065b21f85bc118d891b33e0673bd8EthereumLoss Breakdown
Similar Incidents
V3Utils Arbitrary Call Drain
35%Vortex approveToken Drain
34%WETH Drain via Unprotected 0xfa461e33 Callback on 0x03f9-62c0
34%PumpToken removeLiquidityWhenKIncreases Uniswap LP Drain
34%ORAAI/BUBAI backdoored tokens drain WETH from LP pools
33%WBTC Drain via Insecure Router transferFrom Path
33%Root Cause Analysis
NOON Pool Drain via Public _transfer
1. Incident Overview TL;DR
In Ethereum mainnet transaction 0x23fb7f093e827ed061aafb574cfd420eab879621c7f78cb341292e106a3a88c0 at block 17366980, attacker EOA 0x9748c8540a5f752ba747f1203ac13dae789033de called its helper contract 0xe9458215bef3aa61a039eb59ffc283bf82b81c84 and drained nearly the entire WETH side of the NO/WETH Uniswap V2 pair 0x421a5671306cb5f66ff580573c1c8d536e266c93. The exploit extracted 1136449063099364001 wei WETH and left the pair with only 1 wei WETH.
The root cause is a broken authorization invariant in the NO token 0x6feac5f3792065b21f85bc118d891b33e0673bd8. Its runtime exposes _transfer(address,address,uint256) as an unrestricted external entry point, so any caller can move NO balances out of arbitrary holders, including the AMM pair. Once the pair's NO balance was temporarily reduced to 1, sync() recorded a poisoned reserve state. Restoring the NO balance before swap() let the attacker trade against stale reserves and withdraw almost all WETH.
2. Key Background
The incident depends on two public protocol components:
- The NO token at
0x6feac5f3792065b21f85bc118d891b33e0673bd8. - The NO/WETH Uniswap V2 pair at
0x421a5671306cb5f66ff580573c1c8d536e266c93.
At the independently validated pre-state immediately before block 17366980:
pair.token0() = 0x6fEAc5F3792065b21f85BC118D891b33e0673bD8andpair.token1() = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2.NO.balanceOf(pair) = 88104775387217598184058610577.WETH.balanceOf(pair) = 1136449063099364002.WETH.balanceOf(0xe9458215bef3aa61a039eb59ffc283bf82b81c84) = 0.
Uniswap V2 assumes token balances can only move according to normal ERC-20 authorization rules. sync() snapshots the pair's live token balances into reserves, and swap() prices output against those stored reserves. If a token lets any third party arbitrarily decrease the pair's balance, an attacker can make sync() record a fake reserve state and then restore the balance before swapping.
That is exactly what happened here. The NO side is token0, so forcing the stored reserve to 1 NO makes the pair price the restored NO balance as fresh input against the full WETH reserve.
3. Vulnerability Analysis & Root Cause Summary
This is an ATTACK case and an ACT opportunity, not a privileged compromise. The vulnerable primitive is NO token selector 0x30e0789e, resolved to _transfer(address,address,uint256). Independent bytecode inspection confirms that this path debits balance[from] and credits balance[to] for arbitrary addresses, but does not authenticate msg.sender against from and does not consult allowance. That violates the core ERC-20 safety invariant that only the holder or an approved spender may reduce a holder's balance.
The broken token invariant is sufficient to break Uniswap V2 reserve accounting. By calling _transfer(pair, helper, pairBalance - 1), the attacker reduced the pair's live NO balance to 1 while leaving its WETH balance unchanged. pair.sync() then stored reserves of (1 NO, 1136449063099364002 WETH). After sending the NO back to the pair, the attacker called swap(); Uniswap interpreted the restored NO as massive fresh input against reserveIn = 1, so getAmountOut() returned 1136449063099364001 wei WETH and the pair paid it out.
The exploit preconditions were minimal and fully public: the vulnerable token had to expose arbitrary-balance transfer functionality, the pair had to custody a valuable second asset, and the attacker had to be able to call sync() before restoring the NO balance and executing swap(). The violated security principles are equally direct: internal transfer helpers must not be exposed as unrestricted external entry points, AMM integrations depend on standard ERC-20 authorization semantics, and pools should not rely on assets whose balances can be arbitrarily rewritten by third parties.
4. Detailed Root Cause Analysis
The ACT pre-state sigma_B is Ethereum mainnet at block 17366979, where all required information was public: the NO token bytecode, the NO/WETH pair balances, and the helper deployment by the same sender in transaction 0x37cc59c61a1fc5bc5af7be530bb4969f08895c3c58a03a8f7f8581bfb3aeb339.
Independent runtime inspection of the NO token shows the dangerous selector and the unauthorized balance rewrite path. The critical branch begins at runtime offset 0x054d:
NO token runtime disassembly
0000054d: JUMPDEST
00000579: SLOAD
0000059e: SLOAD
000005c9: SLOAD
000005d4: SLOAD
000005da: SSTORE
000005e0: SLOAD
000005e5: SSTORE
0000061d: LOG3
00000649: JUMPDEST
0000064e: JUMP
00000659: CALLER
The function rewrites the balance mapping and emits a Transfer event before execution reaches any CALLER-dependent logic. That matches the observed exploit behavior: an unprivileged external caller can move balances out of the pair.
The seed transaction trace then shows the full exploit sequence:
Seed transaction trace
0x6fEA...::_transfer(0x421A..., 0xE945..., 88104775387217598184058610576)
emit Transfer(src: 0x421A..., dst: 0xE945..., wad: 88104775387217598184058610576)
0x421A...::sync()
emit Sync(: 1, : 1136449063099364002)
0x6fEA...::_transfer(0xE945..., 0x421A..., 88104775387217598184058610576)
0x421A...::swap(0, 1136449063099364001, 0xE945..., 0x)
WETH9::transfer(0xE945..., 1136449063099364001)
emit Sync(: 88104775387217598184058610577, : 1)
The exploit is deterministic:
- The helper contract calls
NO._transfer(pair, helper, pairNoBalance - 1), leaving the pair with exactly1NO. - The helper calls
pair.sync(), which records reserves(1, 1136449063099364002). - The helper returns the NO to the pair, so the live NO balance is restored but the stored reserve remains poisoned until the next AMM update.
- Using the same public state,
UniswapV2Router.getAmountOut(88104775387217598184058610576, 1, 1136449063099364002)yields1136449063099364001. pair.swap(0, 1136449063099364001, helper, "")transfers that WETH to the attacker helper and leaves the pair with only1wei WETH.
This satisfies the stated invariant breakpoint from the root cause analysis: a third party was able to decrease the pair's NO balance without authorization, and that single violation poisoned Uniswap's reserve accounting.
5. Adversary Flow Analysis
The adversary cluster contains:
- EOA
0x9748c8540a5f752ba747f1203ac13dae789033de, which deployed the helper and sent the exploit transaction. - Helper contract
0xe9458215bef3aa61a039eb59ffc283bf82b81c84, which executed the exploit logic and received the drained WETH.
The victim-side components are:
- NO/WETH Uniswap V2 pair
0x421a5671306cb5f66ff580573c1c8d536e266c93. - NO token
0x6feac5f3792065b21f85bc118d891b33e0673bd8.
The on-chain execution flow is:
- In block
17366973, the attacker EOA deployed helper contract0xe9458215bef3aa61a039eb59ffc283bf82b81c84in transaction0x37cc59c61a1fc5bc5af7be530bb4969f08895c3c58a03a8f7f8581bfb3aeb339. - In block
17366980, the attacker EOA called the helper in transaction0x23fb7f093e827ed061aafb574cfd420eab879621c7f78cb341292e106a3a88c0. - The helper moved
88104775387217598184058610576NO from the pair to itself via the public_transferprimitive. - The helper called
sync()so the pair stored the false reserve state(1 NO, 1136449063099364002 WETH). - The helper transferred the NO back to the pair and immediately called
swap()to withdraw1136449063099364001wei WETH. - The exploit completed with the pair's NO balance restored, the pair's WETH reserve reduced to
1, and the helper holding the extracted WETH.
Two additional related transactions were identified in the incident set: 0xcf77be5591f4aa955d55b26861951fd869fcd02a448dd8e5f025b779d1843dd3 and 0x94f9878df62988dae69939328c3365550223a423a5b143dcff7b59af7709880d. They are related context, but the deterministic ACT realization is fully captured by the helper deployment transaction and the exploit transaction above.
6. Impact & Losses
The measurable pool loss is:
WETH:1136449063099364001wei (1.136449063099364001WETH).
After the exploit, the pair still held 88104775387217598184058610577 NO but only 1 wei WETH. Liquidity providers in the NO/WETH pair absorbed the WETH loss.
The attack was also clearly profitable. The helper finished with 1136449063099364001 wei WETH, while the combined execution cost was 4652911844773156 wei ETH-equivalent (4652910844773156 wei sender gas plus a 1000000000 wei native payment from the helper), leaving a net gain of 1131796151254590845 wei ETH-equivalent.
7. References
- Exploit transaction:
0x23fb7f093e827ed061aafb574cfd420eab879621c7f78cb341292e106a3a88c0 - Helper deployment transaction:
0x37cc59c61a1fc5bc5af7be530bb4969f08895c3c58a03a8f7f8581bfb3aeb339 - Related transactions:
0xcf77be5591f4aa955d55b26861951fd869fcd02a448dd8e5f025b779d1843dd3,0x94f9878df62988dae69939328c3365550223a423a5b143dcff7b59af7709880d - NO token:
0x6feac5f3792065b21f85bc118d891b33e0673bd8 - NO/WETH Uniswap V2 pair:
0x421a5671306cb5f66ff580573c1c8d536e266c93 - WETH:
0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 - Independent evidence used for validation: seed transaction metadata, seed trace, seed balance diff, on-chain receipts, and direct runtime bytecode inspection of the NO token