Vanguard

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

Sender is used instead of the actual user address, causing **EOA addresses** that use the same Router to accumulate penalties based on shared stats.

Author Revealed upon completion

Root + Impact

Description

  • The phase limit and penalties should apply to individual addresses based on their own swap activity.

  • However, since _beforeSwap uses the sender input parameter as the user identification address, all stats are accumulated on the Router address instead of the actual user.

@> function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
if (launchStartBlock == 0) revert PoolNotInitialized();
if (initialLiquidity == 0) {
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
}
uint256 blocksSinceLaunch = block.number - launchStartBlock;
uint256 newPhase;
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
if (currentPhase == 3) {
return (BaseHook.beforeSwap.selector, BeforeSwapDeltaLibrary.ZERO_DELTA, LPFeeLibrary.OVERRIDE_FEE_FLAG);
}
uint256 phaseLimitBps = currentPhase == 1 ? phase1LimitBps : phase2LimitBps;
uint256 phaseCooldown = currentPhase == 1 ? phase1Cooldown : phase2Cooldown;
uint256 phasePenaltyBps = currentPhase == 1 ? phase1PenaltyBps : phase2PenaltyBps;
uint256 swapAmount =
params.amountSpecified < 0 ? uint256(-params.amountSpecified) : uint256(params.amountSpecified);
uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
bool applyPenalty = false;
if (addressLastSwapBlock[sender] > 0) {
@> uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
@> if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
@> addressSwappedAmount[sender] += swapAmount;
@> addressLastSwapBlock[sender] = block.number;
uint24 feeOverride = 0;
if (applyPenalty) {
feeOverride = uint24((phasePenaltyBps * 100));
}
return (
BaseHook.beforeSwap.selector,
BeforeSwapDeltaLibrary.ZERO_DELTA,
feeOverride | LPFeeLibrary.OVERRIDE_FEE_FLAG
);
}

Risk

Likelihood:

  • Does not require any specific action to occur during normal swap interactions.

  • High likelihood, as most users route through the same router, causing stats to hit limits much faster.

Impact:

  • Core functionality meant to mitigate and penalize bot activity does not work.

  • Regular users and bots pay penalty fees based on shared activity.

  • Users are likely to leave the protocol due to this buggy logic.

Proof of Concept

This test verifies that the hook does not track individual user addresses and only tracks the router address.

Add this snippet of code to test/TokenLaunchHookUnit.t.sol

function test_usersShare_address_tracking_stats_POC() public {
uint256 user1SwapAmount = 0.001 ether;
uint256 user2SwapAmount = 0.01 ether;
vm.deal(user1, 1 ether);
vm.startPrank(user1);
SwapParams memory paramsUser1 = SwapParams({
zeroForOne: true, amountSpecified: -int256(user1SwapAmount), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettingsUser1 =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: user1SwapAmount}(key, paramsUser1, testSettingsUser1, ZERO_BYTES);
vm.stopPrank();
vm.deal(user2, 1 ether);
vm.startPrank(user2);
SwapParams memory paramsUser2 = SwapParams({
zeroForOne: true, amountSpecified: -int256(user2SwapAmount), sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettingsUser2 =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: user2SwapAmount}(key, paramsUser2, testSettingsUser2, ZERO_BYTES);
vm.stopPrank();
assertEq(antiBotHook.addressSwappedAmount(user1), 0, "User1 swapped amount should be 0");
assertEq(antiBotHook.addressSwappedAmount(user2), 0, "User2 swapped amount should be 0");
assertEq(
antiBotHook.addressSwappedAmount(address(swapRouter)),
user1SwapAmount + user2SwapAmount,
"swapRouter swapped amount should equal user1 + user2 swap amounts"
);
}

Recommended Mitigation

Based on https://docs.uniswap.org/contracts/v4/guides/accessing-msg.sender-using-hook

+// Define interface
+interface IMsgSender {
+ function msgSender() external view returns (address);
+}
+// Add mapping of trusted routers
+mapping(address => bool) public verifiedRouters;
+// Add setter functions for trusted routers (with proper access control)
+function addRouter(address _router) external {
+ verifiedRouters[_router] = true;
+ // console.log("Router added:", _router); ← remove in production
+}
+function removeRouter(address _router) external {
+ verifiedRouters[_router] = false;
+ // console.log("Router removed:", _router); ← remove in production
+}
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
+ address user = address(0);
+
+ if (verifiedRouters[sender]) {
+ try IMsgSender(sender).msgSender() returns (address swapper) {
+ user = swapper;
+ } catch {
+ revert("Router does not implement msgSender()");
+ }
+ } else {
+ user = sender;
+ }
+
+ if (user == address(0)) revert("Invalid user address");
...
- if (addressLastSwapBlock[sender] > 0) {
+ if (addressLastSwapBlock[user] > 0) {
- uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
+ uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[user];
...
}
- 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;
...
}

Support

FAQs

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

Give us feedback!