Description
-
The TokenLaunchHook is designed to track individual user swap amounts and enforce cooldowns to prevent bot activity during token launches. Each user should have their own addressSwappedAmount and addressLastSwapBlock entries.
-
The _beforeSwap function receives sender as a parameter, but in Uniswap V4's architecture, this sender is the swap router contract address, not the actual user initiating the transaction. All swap tracking is therefore accumulated against the router address.
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
}
Risk
Likelihood:
Impact:
-
All users share the same swap limit bucket (the router's), meaning the collective activity triggers penalties instead of individual limits
-
Cooldowns apply to the router, so if any user swaps, ALL subsequent users must wait the cooldown period
-
Bots can bypass completely by using direct pool access or different router contracts
-
The anti-bot protection mechanism is completely non-functional
Proof of Concept
Two users swap through the same router. Both users' tracked amounts are 0, while the router accumulates both swaps - proving per-user tracking is broken.
function test_SenderIsRouterNotUser() public {
vm.deal(user1, 10 ether);
vm.deal(user2, 10 ether);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.001 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
vm.startPrank(user1);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
assertEq(antiBotHook.addressSwappedAmount(user1), 0, "User1 should have 0 tracked");
assertGt(antiBotHook.addressSwappedAmount(address(swapRouter)), 0, "Router has the tracked amount");
vm.startPrank(user2);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
assertEq(antiBotHook.addressSwappedAmount(user2), 0, "User2 should have 0 tracked");
}
Recommended Mitigation
Use tx.origin instead of sender to track the actual user. Alternatively, pass user address via hookData.
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
+ // Decode the actual user from hookData or use tx.origin
+ address actualUser = tx.origin; // Or decode from hookData if passed by router
+
// ... phase logic ...
- addressSwappedAmount[sender] += swapAmount;
- addressLastSwapBlock[sender] = block.number;
+ addressSwappedAmount[actualUser] += swapAmount;
+ addressLastSwapBlock[actualUser] = block.number;
- if (addressLastSwapBlock[sender] > 0) {
+ if (addressLastSwapBlock[actualUser] > 0) {
- uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
+ uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[actualUser];
// ...
}
}