Uniswap toxic pool trap, $130,000 collected in 29 hours

Bitsfull2026/09/17 15:549636

Summary:

A Uniswap v4 Hook quoted a 0% fee to aggregator inquiries but charged 12.8% on actual trades, quietly pocketing approximately $130,000 over 29 hours.


From August 22 to 23, 2026, a Uniswap v4 USDC/WBNB dynamic fee pool on BNB Chain displayed a 0% fee in aggregator quotes, while users were actually charged a 12.8% LP fee on execution. Over about 29 hours, this pool processed 21,086 trades and accumulated $131,888 in fees, then was deregistered. All victim transactions succeeded: the contract was not hacked, outputs were above users' set minimums, and there were no abnormal signals—the difference quietly became fees.


This article uses one sample transaction as the entry point:

(0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0: user deposited 5,926.90 USDT, received 757.43 USDT less than expected), dissecting how this pool achieved "quoted 0%, executed 12.8%": the three-layer determination of the fee function, the bypassed slippage protection, and the whitelist that kept other LPs out. All conclusions are based on on-chain data and locally controlled verification.


01 The path from quote to execution


After a user initiates a swap on the aggregator frontend, the entire path proceeds as follows:



There are two background facts along this path, and every subsequent layer of determination is built on these two points.


First, on the v4 side: it allows Hooks to override the fee on a per-swap basis. PoolKey.fee = 0x800000 declares a dynamic fee pool; the Hook carries 0x400000 | fee in the third return value of beforeSwap. The code in v4-core that receives this return value (Pool.sol:303-305):



isOverride() checks the override flag, removeOverrideFlagAndValidate() strips 0x400000 and validates that it does not exceed MAX_LP_FEE = 1,000,000 (100%), then executes this swap at that fee rate and writes it to the Swap event. In other words, whatever the Hook returns is what the user pays—as long as the output is still above amountOutMin. This is an official feature, and adjusting fees based on volatility and inventory are legitimate uses.


Now look at this Hook itself: it only declares two permissions. v4 encodes permissions in the lower 14 bits of the contract address (Hooks.sol:29-47), which are validated at deployment and cannot be changed afterward. The flag definitions on the v4-core side correspond to the address decoding as follows:



On-chain testing shows that getHookPermissions() returns a result consistent with the bitmap: only these two bits are true.


The Hook has no source code. This article decompiles the full fee function from the bytecode—the following is a behavioral breakdown of this logic that has already run on-chain:



Next, let's examine it layer by layer. The first two layers answer the question "Is this a simulation?" while the third layer determines how much a real transaction pays.


02 Three-Layer Determination of the Fee Function


Determination One: Distinguish the Caller


After the fee function is called, it first compares tx.origin: if it equals any of three constant addresses, it directly returns the fallback tier. The fallback tier is currently configured at 0%.


The three addresses are not chosen arbitrarily. When eth_call does not specify from, the transaction's origin falls on the zero address—aggregators typically do not specify it during batch price queries. Direct verification in a local fork:



The first layer only recognizes these three addresses. The next layer looks at how the Hook can still distinguish after a normal address is specified.


Determination Two: Check Gas


Replace tx.origin with a normal address and retry; the return is still 0%—the first layer was bypassed, but the fee rate did not change. This indicates that the first layer is not the only determination. Locate the second layer: gasleft() * 10100 / 10000 >= gate.


Aggregators tend to provide very high gas for quotes to prevent candidate routes from failing due to insufficient gas during simulation; before a real transaction enters the Hook, gas has already been consumed by the Router, approvals, and preceding hops. The same code, three gas scenarios:



The dividing line is measured to be between 16.85M–16.9M gas. The difference between 30M and 2.59M is the difference between "quoting" and "execution."


Finding 3: Per-Transaction Pseudo-Randomness


Real transactions that pass the first two layers enter the third layer: the block environment fingerprint is hashed and taken modulo 10,000, and the corresponding fee tier is returned based on the interval the result falls into. The current configuration has three tiers at 8% / 10% / 10%, with a fallback to 0% if no tier is hit.


There are two notable design choices in this layer. Three fields of calldata are mixed into the fingerprint — the aggregator quote's calldata and the real route's calldata are inherently different, so the fingerprints naturally differ as well. Additionally, the per-transaction pseudo-randomness gives the fee rate a distribution, making it hard to detect a pattern from just a few transactions, and simple rule enumeration cannot catch it.


Across all 21,086 on-chain events, 19 historical fee tiers appeared (6.8%–28%), indicating that fee tiers have been continuously reconfigured on demand by an admin function; the effective rate at the time the sample transaction executed was 12.8%.


The Picture of Three Layers Combined



At this point, we can define a "poison pool" — a v4 pool that simultaneously satisfies three conditions: PoolKey.fee carries a dynamic fee flag; the fee decision reads execution environment signals (gas, origin, block environment) rather than public market state; and a systematic divergence appears between the quoted simulation and on-chain execution fee rates, with the beneficiary being the poison pool deployer. The dividing line is what the fee rate reads: returning the same fee rate to any caller is programmable market design; returning different fee rates based on "who is asking for the quote" is deception of the routing system.


At this point, the mechanism of fee rate divergence is complete. But why the transaction can still succeed after the fee is taken is the next question.


03 Why Slippage Protection Did Not Revert


First, let's look at the complete path and amounts of this transaction:



The Swap event for this hop records the effective fee rate in the fee field: 128000. In v4, the fee unit uses 1,000,000 as 100%, so 128000 is 12.8%. The loss on this transaction can be measured from two angles: from the pool's perspective, the fee is charged on WBNB input; from the perspective of the entire route, the stablecoin in-out difference is 757.43 USDT, a loss of 12.7794%, which also includes the front hop's fee and depeg deviation. The two figures differ by only 0.02 percentage points, indicating that the primary source of the loss is this pool's LP fee.


amountOutMin only validates the final output lower bound and does not look at how much fee each intermediate hop charged:



The 12.8% fee rate falls entirely within the 20.32% buffer, and the output remains above the floor—the transaction succeeds without reverting. The conventional default for mainstream stablecoin routes is 0.1%–1%, and this route offers more than 20 times that space. Users assume a wide buffer means "more stability," but in reality, it hands every contract along the path over to counterparty self-discipline. Visualize this calculation:



04 Operations of the Toxic Pool Deployer


Whitelist: Keeping Other LPs Out


The whitelist logic is in beforeAddLiquidity, and the decompiled result is as follows:



Adding liquidity requires passing three gates: first, the PoolManager call check that any v4 Hook has; then, it must go through the official PositionManager; finally, the whitelist of the position holder is verified. Addresses not on the list are reverted by the contract here, preventing other LPs from entering, so fee revenue is not diluted and all goes to the deployer. The list itself is maintained by an admin function (selector 0xc4452e52) on an address-by-address basis.


0% Tier and Wash Trading


Looking at the 21,086 transactions together, the distribution shows a clear pattern: 6,946 transactions in the 0% tier with $12,947,751 in volume, averaging $1,864 per transaction; 14,140 transactions in the fee tier with $1,120,106 in volume, averaging $79 per transaction. Large transactions are concentrated in the 0% tier, while fees are concentrated on small transactions. The most reasonable explanation for large 0% transactions is the deployer self-trading with high gas: high gas and aggregator quote hits are the same determination, and the fee cost of self-trading is near zero. The wash volume pushes the pool up the rankings on market sites (snapshot shows about $10.82 million in 24h volume, 16,671 transactions), and in the eyes of aggregators, this is a pool with good depth and low fees.


Fee Tiers Reconfigured on Demand


Determination three mentioned 19 historical fee tiers, which come from the decompiled fee configuration function (selector 0x4d909a45, not included in public signatures):



The four fee tiers and thresholds are packaged together and written into the storage slot keccak(poolId, 2). On-chain measurement shows the slot reads 0x0138800186a00186a0, corresponding segment by segment to the function's packing format. The fee tiers here are parameters that can be adjusted at any time, not fixed at deployment.


Lifecycle


This pool was created at 08-22 06:57, saw its first swap at 07:13, and the sample transaction in this article occurred at 21:39; the last trade was executed at 08-23 12:25, after which it was deregistered. The entire active period lasted about 29 hours, with total volume of $14,067,857 and fee revenue of $131,888 (before deducting deployer costs). At the time of review, the registration flag had already returned to enabled—the pool is still there and can be re-enabled at any time.


05 Full Path


Connecting the entire attack chain:



The causality of the entire chain is all in the image above, and all three conditions are indispensable: the simulation hits the first two layers, the real transaction lands on the third layer, and the buffer is larger than the fee rate. If any one of them does not hold, this technique will fail.


06 Fixes and Recommendations


The first issue lies in the aggregator's quoting method. The root cause is that simulation and execution do not follow the same path: quotes use idealized pool-level queries, paired with the zero address and high gas; real transactions carry a real identity and already-consumed gas. The Hook happens to be able to read these differences, so the fee rate diverges. The remedy is to make simulation closely match execution—simulate using the actual Router calldata to be sent, the real from/to, and gas close to an on-chain transaction; after execution, decode the Swap event to verify the fee, and if it does not match the quote, downgrade or delist it.


The second issue lies in the user's slippage settings. The root cause is that min_out is set too loosely: the 20.32% buffer completely swallows the 12.8% fee rate, and the transaction still succeeds as usual. For mainstream stablecoin routes, 0.1%–1% is sufficient for daily use; check this number before initiating a transaction; wallets and frontends tightening the default value can block most risks for users.


The third issue lies in routing admission. The root cause is that any dynamic fee pool can directly participate in quote competition: a Hook with no open source and no audit record can still enter recommended routes by relying on wash-traded volume. For dynamic fee pools without a credible track record, excluding them from routing by default or significantly downweighting them is the safest approach.


Data and Notes


·Sample transaction:

https://bscscan.com/tx/0x7615dec2ce80506ec461a47739eb8533ac2bc7c605ca56c255964481b76363d0 (block 117497524)


· Pool initialization transaction:

https://bscscan.com/tx/0x8fa72ef72d77b61f715dc9ce90548249ab045c6d78716078cd5ab425bbe69a05


· PoolManager:

0x28e2ea090877bf75740558f6bfb36a5ffee9e9df


· Pool ID:

0x36e5540e9dedc02229fe8a82aa5b10c0bf07d1fa74e4f2ffe0efd00fa1a36aea


· Hook:

0xd111b3ddd92e627f1864520c770e913ec04e0880 (no source code; pseudocode independently decompiled for this article; admin 0x08b03e1a5444d469f4dc954e74d3f662c94a6b13)


· Trades and fees: all 21,086 Swap events in this pool cumulatively totaled transaction by transaction (eth_getLogs), WBNB converted at the execution price of the same transaction; revenue is gross and does not deduct deployer costs


Original link


Welcome to join the official BlockBeats community:

Telegram Subscription Group: https://t.me/theblockbeats

Telegram Discussion Group: https://t.me/BlockBeats_App

Official Twitter Account: https://twitter.com/BlockBeatsAsia