Calculated from recorded token losses using historical USD prices at the incident time.
0xf3158a7ea59586c5570f5532c22e2582ee9adba2408eabe61622595197c507130x9536a78440f72f5e9612949f1848fe5e9d4934ccEthereumOn Ethereum mainnet at block 13229001, an unprivileged attacker drained almost all WETH and USDT liquidity from the Nowswap V1 WETH/USDT pool at 0x9536a78440f72f5e9612949f1848fe5e9d4934cc. The attacker’s externally owned account (EOA) 0x5676e585bf16387bc159fd4f82416434cda5f1a3 deployed a bespoke router 0xa14660a33cc608b902f5bb49c8213bd4c8a4f4ca and, in a single transaction 0xf3158a7ea59586c5570f5532c22e2582ee9adba2408eabe61622595197c50713, executed a sequence of alternating WETH and USDT swap legs against the pool.
Each swap leg exploited a mis-scaled invariant check in the pool’s swap(0x022c0d9f) implementation. The pool mixed packed 112‑bit reserves, raw ERC20 balances, and the stored kLast value using inconsistent 2^112-style scaling, so the “K loss” guard effectively enforced K_post ≥ 0.7 × K_pre instead of the intended K_post ≥ K_pre. This allowed trades where the pool paid out roughly 10× more of WETH or USDT than it received (same-asset in/out), while the true constant product reserve0 * reserve1 shrank by ~30% per leg but the transaction still did not revert.
Before the attack, the pool held 158292004512062229651 WETH wei (~158.29 WETH) and 535718798195 USDT units (~535,718.80 USDT). After the attack transaction, the pool held only 3567797354993498 WETH wei (~0.0036 WETH) and 12075082 USDT units (~12,075.082 USDT). The attacker EOA ended with WETH (~158.29 WETH) and USDT (~535,706.723113 USDT), yielding approximately WETH-equivalent profit after paying ETH in gas.
158288436714707236153535706723113316.290.079428083724520515Nowswap V1 is a Uniswap-V2-style constant-product automated market maker (AMM). For each pool, it stores reserves as uint112 reserve0 and uint112 reserve1 packed into a single storage slot alongside a timestamp, and tracks a K-like invariant through a stored kLast. The implementation uses DS-math-like helpers for multiplication and subtraction that revert on overflow or underflow, and K-related helpers that enforce per-operation accounting constraints and a “K loss” check.
The victim pool in this incident is the Nowswap V1 WETH/USDT pair at:
0x9536a78440f72f5e9612949f1848fe5e9d4934cc (Ethereum mainnet chainid = 1)0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2 (18 decimals)0xdac17f958d2ee523a2206206994597c13d831ec7 (6 decimals)The pool contract is unverified on Etherscan, so the analysis is based on opcode-level disassembly, Heimdall decompilation, and traces. The disassembled runtime bytecode in artifacts/root_cause/data_collector/iter_2/contract/1/0x9536a7_disassembly.txt shows a dispatcher that routes selector 0x022c0d9f to the main swap implementation:
00000166: JUMPDEST
00000167: DUP1
00000168: PUSH4 0x022c0d9f
0000016d: EQ
0000016e: PUSH2 0x01ae
00000171: JUMPI
...
000005bb: JUMPDEST ; entry into core swap path
The adversary-related cluster consists of:
0x5676e585bf16387bc159fd4f82416434cda5f1a3 (attacker): deploys the router and receives the stolen assets.0xa14660a33cc608b902f5bb49c8213bd4c8a4f4ca: an adversary-deployed helper contract that orchestrates repeated calls to the Nowswap pool’s swap(0x022c0d9f) and getReserves(0x0902f1ac) and forwards all accumulated tokens back to the EOA.The router is deployed in transaction 0xe8a689ba79028e28e0289af956ee6970bb8e9bdef453875c252f9e01bfed3f68 at block 13228994 by the same EOA, as recorded in artifacts/root_cause/data_collector/iter_1/address/1/0xa14660a3…/txlist.json. The decompiled router code in artifacts/root_cause/data_collector/iter_1/contract/1/0xa14660a3…/decompile/0xa14660a3…-decompiled.sol confirms that the attacker controls a bespoke helper that receives the pool address and token addresses and can perform arbitrary call sequences. Critically, the Nowswap swap entrypoint itself is fully permissionless: there are no owner-only checks in the disassembly or trace, and any unprivileged EOA with sufficient ETH for gas can call it either directly or via their own router.
The core vulnerability lies in Nowswap V1’s implementation of the K-tracking invariant on the swap(0x022c0d9f) path for this pool. Conceptually, the pool intends to maintain a non-decreasing constant product K = reserve0 * reserve1 (up to fees), enforced via DS-math helpers and a K_LOSS check that compares a K-like quantity derived from the reserves to a stored kLast. However, the swap path combines:
getReserves, scaled via 2^112 shifts,balanceOf for WETH (18 decimals) and USDT (6 decimals),kLast,using inconsistent scaling factors and divisions. The result is that the “K loss” inequality on the swap path does not enforce K_post ≥ K_pre; empirically and from the arithmetic derivation, it permits trades where:
amount_out / amount_in ≈ 10 for WETH→WETH and USDT→USDT legs, andK_post / K_pre ≈ 0.7 (a ~30% decrease),without triggering a revert.
This mis-scaled K_LOSS check is the root cause: it allows invariant-breaking same-asset trades that drastically reduce the true constant product while appearing acceptable under the implemented guard. The attacker’s router simply exploits this by alternating WETH and USDT legs to compound the K reduction until the pool is effectively drained.
Immediately before block 13229001 and transaction 0xf3158a7e…0713, the system is in pre-state σ_B defined by:
getReserves:
reserve0_pre = 158292004512062229651 WETH weireserve1_pre = 535718798195 USDT unitserc20_state_diff_balanceOf.json:
0 WETH and 0 USDT.13228994 by the EOA.The intended invariant (as reconstructed from the disassembly and invariant analysis) is a constant product K based on reserve0 * reserve1, tracked and checked using DS-math helpers and a stored kLast. The helpers identified in nowswap_swap_formula_and_invariant_iter3.md and the disassembly include:
mul helper (e.g., at PC 0x1973) that computes a * b and reverts if (a * b) / b != a.sub helper (e.g., at PC 0x1b70) that computes x - y and reverts on underflow.0x219d and 0x21af) that pack/unpack reserve0 and reserve1 into the combined storage slot using 2^112 shifts.0x1bc0, 0x1ea1, 0x1fff, 0x2051) that validate reserve sizes, compute K-like quantities, and perform the K_LOSS check, reverting with "NowswapV1: K_LOSS" if the inequality fails.These helpers are used consistently in mint/burn paths to ensure K does not decrease unexpectedly. However, on the swap(0x022c0d9f) path for this pool, the way these helpers are wired to actual ERC20 balances and kLast introduces a unit mismatch.
From the disassembly and invariant analysis, the swap path for selector 0x022c0d9f behaves roughly as follows:
0x0166) compares msg.sig to 0x022c0d9f and jumps into the swap implementation at PC ~0x01ae.reserve0 and reserve1.transfer calls to move tokens between the pool and msg.sender (the router).balanceOf(address(this)) for both tokens to obtain the actual post-transfer balances b0 and b1.mul and sub helpers, combining:
reserve0, reserve1) that are logically 112‑bit,b0, b1) with 18/6 decimals,kLast,
with several SHL/DIV operations by 2^112 and 2^224.kLast and emits Swap/Sync events.In the victim pool’s configuration, this computation is mis-scaled. Instead of comparing a consistently scaled version of reserve0 * reserve1 before and after the swap, it compares a value that effectively behaves like:
K_check_post >= c * K_check_pre for some c ≈ 0.7,
where K_check_pre and K_check_post are built from a mix of scaled and unscaled quantities. This is confirmed empirically by inspecting the actual Swap and Sync events.The file artifacts/root_cause/data_collector/iter_3/analysis/nowswap_leg_arithmetic.json reconstructs each swap leg from the pool’s Sync and Swap events. Two representative legs are:
{
"logIndex": 161,
"direction": "WETH",
"amount0In": "5276400150402074421",
"amount0Out": "52764001504020743217",
"pre_reserve0": "110804403158443560855",
"pre_reserve1": "535718798195",
"post_reserve0": "110804403158443560855",
"post_reserve1": "375003158837",
"K_pre": "59360001694755646587976946656725",
"K_post": "41552001197464795129007440525635",
"K_ratio_post_over_pre": 0.7000000001875984,
"amount_ratio_out_over_in": 10.0
},
{
"logIndex": 165,
"direction": "USDT",
"amount1In": "17857293373",
"amount1Out": "178572932731",
"pre_reserve0": "110804403158443560855",
"pre_reserve1": "375003158837",
"post_reserve0": "77563082210910492698",
"post_reserve1": "375003158837",
"K_pre": "41552001197464795129007440525635",
"K_post": "29086400838225356627618022672226",
"K_ratio_post_over_pre": 0.7,
"amount_ratio_out_over_in": 9.999999944056471
}
Snippet origin: Nowswap V1 WETH/USDT leg arithmetic, derived from Swap and Sync events for tx 0xf3158a7e…0713.
Observations:
reserve0 (WETH) remains unchanged, while reserve1 (USDT) decreases. However, the Swap event shows a same-asset trade: the pool receives amount0In WETH and sends out ~10× that amount as amount0Out WETH. The true constant product K falls by ~30% (K_post / K_pre ≈ 0.7).reserve1 (USDT) remains unchanged while reserve0 (WETH) decreases. The pool receives USDT and pays out ~10× more USDT, while K again falls by ~30%.Despite these large K reductions, the transaction does not revert with "NowswapV1: K_LOSS". The same pattern repeats across subsequent legs, each time reducing K by approximately 30% and paying out ~10× more of the in-token than the pool receives, as documented for all legs in nowswap_leg_arithmetic.json and corroborated by the Sync events in summary_13228800_13229050.json.
The ERC20 state diff in artifacts/root_cause/data_collector/iter_2/tx/1/0xf3158a7…/erc20_state_diff_balanceOf.json confirms:
{
"tokens": {
"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": {
"0x5676e585bf16387bc159fd4f82416434cda5f1a3": { "pre": 0, "post": 158288436714707236153 },
"0x9536a78440f72f5e9612949f1848fe5e9d4934cc": { "pre": 158292004512062229651, "post": 3567797354993498 }
},
"0xdac17f958d2ee523a2206206994597c13d831ec7": {
"0x5676e585bf16387bc159fd4f82416434cda5f1a3": { "pre": 0, "post": 535706723113 },
"0x9536a78440f72f5e9612949f1848fe5e9d4934cc": { "pre": 535718798195, "post": 12075082 }
}
}
}
Snippet origin: ERC20 balance and reserve diff for attack transaction 0xf3158a7e…0713.
These balances match the leg-by-leg arithmetic and the final getReserves state, confirming that:
0xa1466… finishes with zero WETH and zero USDT.Taken together, the code-level reconstruction, invariant analysis, per-leg K ratios, and ERC20 state diff converge on the same conclusion: the mis-scaled K_LOSS invariant on the swap path allows the constant product to shrink by ~30% per leg while still passing the guard, enabling a chain of same-asset trades that drains the pool.
The adversary-related cluster consists of:
0x5676e585bf16387bc159fd4f82416434cda5f1a30xa14660a33cc608b902f5bb49c8213bd4c8a4f4catxlist.json for the router address shows the following key transactions:
0xe8a689ba79028e28e0289af956ee6970bb8e9bdef453875c252f9e01bfed3f68 at block 13228994:
from: attacker EOA 0x5676…f1a3to: empty (contract creation)contractAddress: 0xa14660a3…f4cavalue: 0methodId: consistent with contract deployment.0xf3158a7ea59586c5570f5532c22e2582ee9adba2408eabe61622595197c50713 at block 13229001:
from: attacker EOAto: router 0xa1466…f4cavalue: 0input: function selector 0x4f4c7231 with arguments including the pool address and token addresses.The Etherscan-style metadata in artifacts/root_cause/seed/1/0xf3158a7…/metadata.json confirms that this transaction is EIP-1559, pays 0.079428083724520515 ETH in gas, and has no ETH value transferred to the router.
The internal trace in trace.cast.log shows the router calling the pool’s swap(0x022c0d9f) multiple times in a single transaction, with intervening calls to getReserves and ERC20 transfers. Combined with Swap and Sync logs, the leg arithmetic file reconstructs the sequence:
swap to execute a WETH leg where WETH is both sent in and paid out (same-token in/out), while the recorded WETH reserve stays constant and the USDT reserve drops.The per-leg details are captured in nowswap_leg_arithmetic.json and the Sync event summary summary_13228800_13229050.json, which together show K ratios of approximately 0.7 for each leg and amount_out/amount_in ratios around 10.
At the end of the sequence:
This incident is an ACT (anyone-can-take) opportunity:
swap(0x022c0d9f) on the Nowswap V1 pool is fully permissionless; there are no owner-only checks in the disassembly or traces.Thus, any unprivileged actor with access to the same public information and sufficient gas budget can reproduce the exploit against the same vulnerable Nowswap pool.
Using the ERC20 state diffs and getReserves snapshots:
158292004512062229651 wei (~158.292004512062229651 WETH).535718798195 units (~535,718.798195 USDT).3567797354993498 wei (~0.003567797354993498 WETH).12075082 units (~12,075.082 USDT).Effectively, the Nowswap V1 WETH/USDT pool lost:
158.288436714707236153 (difference between pre and post WETH balances).535706.723113 (difference between pre and post USDT balances).The constant product invariant collapsed by many orders of magnitude, and the pool’s liquidity became unusable at any reasonable price. LPs in this pair effectively lost their deposited WETH and USDT to the attacker.
The attacker portfolio analysis in attacker_portfolio_pnl_weth.json shows:
158288436714707236153 wei (~158.288436714707236153 WETH).535706723113 units (~535,706.723113 USDT).price_usdt_in_weth ≈ 0.0002950805850088843312255761868.0 WETH-equivalent.316.3650899640836932259039222 WETH-equivalent.fee_paid_eth = 0.079428083724520515 ETH.Net profit in reference asset (WETH):
316.3650899640836932259039222 - 0.079428083724520515 ≈ 316.2856618803591727109039222 WETH.There is no evidence, from the traces or cross-protocol checks in the artifacts, of secondary impacts such as price oracle manipulation, cascading liquidations, or governance side effects. The damage appears localized to the Nowswap V1 WETH/USDT pool and its LPs.
Seed transaction trace and metadata
0xf3158a7ea59586c5570f5532c22e2582ee9adba2408eabe61622595197c50713 (Ethereum mainnet, block 13229001).trace.cast.log and metadata.json under artifacts/root_cause/seed/1/0xf3158a7….Victim pool code and invariant analysis
0x9536a78440f72f5e9612949f1848fe5e9d4934cc.artifacts/root_cause/data_collector/iter_2/contract/1/0x9536a7…/disassemble/0x9536a7_disassembly.txt.artifacts/root_cause/data_collector/iter_3/analysis/nowswap_swap_formula_and_invariant_iter3.md.On-chain logs and per-leg arithmetic
artifacts/root_cause/data_collector/iter_2/contract/1/0x9536a7…/logs/summary_13228800_13229050.json.0xf3158a7e…0713: artifacts/root_cause/data_collector/iter_3/analysis/nowswap_leg_arithmetic.json.ERC20 balance diffs and reserves
artifacts/root_cause/data_collector/iter_2/tx/1/0xf3158a7…/erc20_state_diff_balanceOf.json.getReserves snapshots embedded in the same file.Attacker cluster and router
0x5676e585bf16387bc159fd4f82416434cda5f1a3.0xa14660a33cc608b902f5bb49c8213bd4c8a4f4ca.artifacts/root_cause/data_collector/iter_1/address/1/0xa14660a3…/txlist.json.artifacts/root_cause/data_collector/iter_1/contract/1/0xa14660a3…/decompile/0xa14660a3…-decompiled.sol.Attacker profit computation
artifacts/root_cause/data_collector/iter_3/analysis/attacker_portfolio_pnl_weth.json.13229001 used for valuation.