Vanguard

First Flight #56
Beginner FriendlyDeFiFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

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

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.

Updates

Appeal created

chaossr Lead Judge 17 days ago
Submission Judgement Published
Validated
Assigned finding tags:

sender parameter is the router address rather than the actual user

Support

FAQs

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

Give us feedback!