Vanguard

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

All users share single limit due to the tracking of router address

Author Revealed upon completion

Description

The hook should track each user's swap amount individually to enforce per-user limits and cooldowns, ensuring fair token distribution during the launch phase.

The _beforeSwap function receives sender parameter which is the swap router contract address, not the actual user address. This causes all users to share the same limit and cooldown, completely breaking the anti-bot protection mechanism.

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ... phase calculation ...
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
bool applyPenalty = false;
// @> sender is the swap router address, not the actual user
if (addressLastSwapBlock[sender] > 0) {
uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
// @> All users share the same limit (router's limit)
if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
// @> Accumulates for the router, not per user
addressSwappedAmount[sender] += swapAmount;
addressLastSwapBlock[sender] = block.number;
// ...
}

Risk

Likelihood: High

  • Every swap goes through the swap router contract, so the sender parameter is always the router address

  • The vulnerability is present in 100% of transactions during Phase 1 and Phase 2

Impact: High

  • All users share a single global limit instead of having individual limits (Phase 1: 1% limit shared among all users instead of 1% per user)

  • First users to swap consume the shared limit, causing all subsequent users to receive penalties even if they individually haven't exceeded limits

  • Cooldown applies globally - if anyone swaps, all other users must wait the cooldown period before their next swap

Proof of Concept

Add this test to TokenLaunchHookUnit.t.sol, here it is shown how only the router address is tracked, not an individual user:

function test_PROOF_TracksRouterNotUsers() public {
address alice = makeAddr("alice");
address bob = makeAddr("bob");
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});
// Alice swaps through swapRouter
vm.deal(alice, 10 ether);
vm.prank(alice);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
// Bob swaps through the same swapRouter
vm.deal(bob, 10 ether);
vm.roll(block.number + 10);
vm.prank(bob);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
// PROOF: Users are NOT tracked individually
assertEq(antiBotHook.addressSwappedAmount(alice), 0, "Alice not tracked");
assertEq(antiBotHook.addressSwappedAmount(bob), 0, "Bob not tracked");
// PROOF: Only swapRouter is tracked, accumulating all users' swaps
assertGt(antiBotHook.addressSwappedAmount(address(swapRouter)), 0.001 ether, "Only router tracked");
}

Recommended Mitigation

Uniswap V4 hooks don't receive the original user's address. The architecture needs to change to track actual users. Consider these options:

Option 1: Use tx.origin (simple but has limitations)

function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
+ address user = tx.origin; // Get actual user instead of router
// ... phase calculation ...
- if (addressLastSwapBlock[sender] > 0) {
+ if (addressLastSwapBlock[user] > 0) {
- uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
+ uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[user];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
- if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
+ if (!applyPenalty && addressSwappedAmount[user] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
- addressSwappedAmount[sender] += swapAmount;
- addressLastSwapBlock[sender] = block.number;
+ addressSwappedAmount[user] += swapAmount;
+ addressLastSwapBlock[user] = block.number;
// ...
}

Note: Using tx.origin works for EOAs but may have issues with smart contract wallets and can be bypassed by contracts. Consider implementing additional validation or alternative tracking mechanisms based on your security requirements.

Support

FAQs

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

Give us feedback!