Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
0 EXP
Submission Details
Impact: high
Likelihood: high

Hook incorrectly tracks SwapRouter instead of users, leading to global protocol DoS

Author Revealed upon completion

Description

  • Normal Behavior: The TokenLaunchHook is designed to enforce individual user swap limits and cooldown periods, ensuring no single actor can manipulate the price or drain liquidity during the sensitive initial launch period.

  • Specific Issue: The contract uses the sender address passed to the _beforeSwap hook to track usage via addressSwappedAmount and addressLastSwapBlock. However, in Uniswap V4, the sender address is the address that calls PoolManager.swap(). For standard swaps, this is the SwapRouter contract address, not the user's personal wallet.

// src/TokenLaunchHook.sol
// @> Root Cause: 'sender' is the SwapRouter address for users using standard routers
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ...
// @> Every user's swap increments the same Router's limit
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
// @> Tracking is incorrectly attributed to the Router
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
}

Risk

Likelihood:

  • This occurs automatically for every user who trades using the canonical Uniswap V4 router or any standard frontend interface that interacts with the PoolManager through a periphery contract.

Impact:

  • Global Denial of Service: Once the cumulative swap volume of all users hits the phaseLimitBps, the Router is deemed to have exhausted its limit. Every subsequent user is then unfairly penalized with the maximum fee.

  • Perpetual Cooldown: Every swap resets the addressLastSwapBlock for the Router. If any trade occurs within the phaseCooldown window (e.g., 5 blocks), the "global" cooldown is extended, potentially locking the entire pool in a penalty state for all participants indefinitely.

Proof of Concept

The following Foundry test demonstrates how the SwapRouter's address is used for tracking instead of the actual user's wallet. It proves that User 2 is unfairly blocked by a cooldown triggered by User 1's previous transaction.

function test_Vulnerability_SenderIsRouter() public {
uint256 swapAmount = 0.001 ether;
// User 1 swaps through the standard router
vm.prank(user1);
swapRouter.swap{value: swapAmount}(key, params, testSettings, ZERO_BYTES);
// FAILURE: Usage is attributed to the Router contract, not User 1
uint256 routerUsed = antiBotHook.addressSwappedAmount(address(swapRouter));
uint256 user1Used = antiBotHook.addressSwappedAmount(user1);
assertGt(routerUsed, 0);
assertEq(user1Used, 0);
// FAILURE: User 2 is now on cooldown because of User 1's swap
uint256 routerLastSwap = antiBotHook.addressLastSwapBlock(address(swapRouter));
assertEq(routerLastSwap, block.number);
}

Recommended Mitigation

Identify the actual transaction initiator using tx.origin or require the periphery router to pass the actual user's address through the hookData parameter.

- addressSwappedAmount[sender] += swapAmount;
- addressLastSwapBlock[sender] = block.number;
+ address actualUser = tx.origin;
+ addressSwappedAmount[actualUser] += swapAmount;
+ addressLastSwapBlock[actualUser] = block.number;

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!