Description
The TokenLaunchHook implements a phased anti-bot system where user swap limits reset when transitioning between phases (e.g., from Phase 1's 1% limit to Phase 2's 5% limit). The _resetPerAddressTracking() function is called during phase transitions to clear accumulated user swap amounts, giving users a fresh start with the new phase's higher limits.
However, the reset function only clears tracking data for address(0) instead of actual users, causing all user swap amounts to persist across phase transitions. This means users who swapped in Phase 1 have those amounts counted against their Phase 2 limits, effectively reducing their available swap capacity in subsequent phases.
function _resetPerAddressTracking() internal {
addressSwappedAmount[address(0)] = 0;
addressLastSwapBlock[address(0)] = 0;
}
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
Risk
Likelihood: High
-
The bug triggers on every phase transition (Phase 1 → 2, Phase 2 → 3) for every user who swapped in the previous phase
-
Any user who legitimately swaps in Phase 1 will be affected when Phase 2 begins
Impact: Medium
Proof of Concept
Add this test to the TokenLaunchHookUnit.t.sol:
function test_BrokenTrackingReset() public {
vm.deal(user1, 10 ether);
vm.startPrank(user1);
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});
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
uint256 phase1Amount = antiBotHook.addressSwappedAmount(address(swapRouter));
console.log("Phase 1 swapped amount:", phase1Amount);
vm.roll(block.number + phase1Duration + 1);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
uint256 phase2Amount = antiBotHook.addressSwappedAmount(address(swapRouter));
console.log("Phase 2 swapped amount:", phase2Amount);
assertGt(phase2Amount, phase1Amount, "Tracking persists across phases!");
vm.stopPrank();
}
Output:
Phase 1 swapped amount: 1000000000000000
Phase 2 swapped amount: 2000000000000000
Tracking persists across phases!
Recommended Mitigation
Implement an epoch-based tracking system that invalidates previous phase data without requiring expensive storage resets:
+ uint256 public currentEpoch;
+ mapping(address => mapping(uint256 => uint256)) public addressSwappedAmountByEpoch;
+ mapping(address => mapping(uint256 => uint256)) public addressLastSwapBlockByEpoch;
- mapping(address => uint256) public addressSwappedAmount;
- mapping(address => uint256) public addressLastSwapBlock;
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ... existing code ...
if (newPhase != currentPhase) {
- _resetPerAddressTracking();
+ currentEpoch++; // Increment epoch to invalidate all previous tracking
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
// ... existing code ...
- if (addressLastSwapBlock[sender] > 0) {
- uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
+ if (addressLastSwapBlockByEpoch[sender][currentEpoch] > 0) {
+ uint256 blocksSinceLastSwap = block.number - addressLastSwapBlockByEpoch[sender][currentEpoch];
if (blocksSinceLastSwap < phaseCooldown) {
applyPenalty = true;
}
}
- if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) {
+ if (!applyPenalty && addressSwappedAmountByEpoch[sender][currentEpoch] + swapAmount > maxSwapAmount) {
applyPenalty = true;
}
- addressSwappedAmount[sender] += swapAmount;
- addressLastSwapBlock[sender] = block.number;
+ addressSwappedAmountByEpoch[sender][currentEpoch] += swapAmount;
+ addressLastSwapBlockByEpoch[sender][currentEpoch] = block.number;
// ... rest of function ...
}
- function _resetPerAddressTracking() internal {
- addressSwappedAmount[address(0)] = 0;
- addressLastSwapBlock[address(0)] = 0;
- }
This approach:
-
Efficiently resets tracking by incrementing an epoch counter instead of clearing storage
-
Ensures each phase uses isolated tracking data
-
Maintains gas efficiency by avoiding storage deletion loops
-
Provides users with the intended fresh limits for each phase