Vanguard

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

Global Shared Quota (Sender Is Router)

Author Revealed upon completion

Root + Impact

Description

The hook tracks anti-bot limits and cooldowns using the sender parameter from PoolManager.swap(). In Uniswap V4, this sender is the contract that initiates the swap call to the PoolManager (typically UniversalRouter or SwapRouter), not the actual end user. This causes tracking to aggregate at the router level rather than per-user, effectively collapsing the entire user base into a single shared quota.

// TokenLaunchHook.sol:171-172
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
internal override
{
// 'sender' is the router contract, not the actual trader
uint256 userSwapped = addressSwappedAmount[sender]; // WRONG: tracks router, not user
// sharedPenaltyTracker is also per-sender (router address)
uint256 sharedPenaltyCount = sharedPenaltyTracker[sender];
}

Risk

Likelihood:

  • High - All standard trades route through UniversalRouter or similar contracts. The protocol is designed for public launch where users interact via standard interfaces, guaranteeing this vulnerability triggers on every transaction.

Impact:

Critical - Global quota exhaustion:

  1. First N trades consume the shared 1% (Phase 1) or 3% (Phase 2) limit

  2. All subsequent users hit the limit immediately and face penalties (50% Phase 1, 20% Phase 2)

  3. Cooldown tracking is also global: if the router swaps in block X, ALL users must wait N blocks before penalty-free trading resumes

  4. Legitimate users who haven't traded yet inherit the "sin" of previous traders through the router address

Proof of Concept

Demonstrates that two distinct users swapping via the standard router share the same tracking state, causing the second user to be penalized based on the first user's activity.

function test_SharedGlobalLimitPoC() public {
address alice = address(0xAAAA);
address bob = address(0xBBBB);
vm.deal(alice, 10 ether);
vm.deal(bob, 10 ether);
// Max limit in Phase 1 is 1% of 10 ETH = 0.1 ETH
// Alice swaps 0.06 ETH (Legal)
vm.prank(alice);
swapRouter.swap{value: 0.06 ether}(
key,
SwapParams({
zeroForOne: true,
amountSpecified: -0.06 ether,
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
ZERO_BYTES
);
// Bob swaps 0.05 ETH.
// Even though Bob hasn't swapped before, the router's cumulative amount is now 0.11 ETH
// which exceeds the 0.1 ETH limit. Bob will be penalized.
uint256 bobBalanceBefore = token.balanceOf(bob);
vm.prank(bob);
swapRouter.swap{value: 0.05 ether}(
key,
SwapParams({
zeroForOne: true,
amountSpecified: -0.05 ether,
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
}),
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false}),
ZERO_BYTES
);
console.log("Bug Confirmed: Bob was penalized because Alice already used a portion of the shared limit");
console.log("Router Total Swapped Amount:", hook.addressSwappedAmount(address(swapRouter)));
}

Recommended Mitigation

Pass the actual user address through hookData (standard V4 pattern for passing trader context through routers) instead of relying on the sender parameter which represents the router contract.

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata hookData)
internal override
{
// Extract actual user from hookData passed by router
address actualUser = abi.decode(hookData, (address));
// Use actual user for all tracking
uint256 userSwapped = addressSwappedAmount[actualUser];
uint256 userLastBlock = addressLastSwapBlock[actualUser];
// Apply checks against per-user limits, not router-global limits
if (userLastBlock > 0 && block.number - userLastBlock < phaseCooldown) {
applyPenalty = true;
}
if (!applyPenalty && userSwapped + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
// Update per-user tracking
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!