Description
-
When phases transition (Phase 1 → Phase 2 → Phase 3), the contract calls _resetPerAddressTracking() to reset user swap amounts and cooldowns, allowing fresh limits in the new phase.
-
The implementation only resets the mappings for address(0), which is never used as a swap sender. All actual user/router tracking data persists across phase transitions.
function _resetPerAddressTracking() internal {
addressSwappedAmount[address(0)] = 0;
addressLastSwapBlock[address(0)] = 0;
}
Called during phase transition:
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
Risk
Likelihood:
Impact:
-
Users who reached their limit in Phase 1 will still be at/over limit in Phase 2
-
Users will be immediately penalized in Phase 2 based on Phase 1 activity
-
Phase transition feature is completely broken - no "fresh start" for new phases
-
Conflicts with documented behavior that phases should have separate tracking
Proof of Concept
Swap in Phase 1, then transition to Phase 2 and swap again. The tracked amount accumulates across phases instead of resetting.
function test_ResetTrackingBroken() public {
vm.deal(user1, 10 ether);
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});
vm.startPrank(user1);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
uint256 phase1Amount = antiBotHook.addressSwappedAmount(address(swapRouter));
assertGt(phase1Amount, 0, "Should have tracked amount");
vm.roll(block.number + phase1Duration + 1);
vm.startPrank(user1);
swapRouter.swap{value: 0.0001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
uint256 phase2Amount = antiBotHook.addressSwappedAmount(address(swapRouter));
assertGt(phase2Amount, phase1Amount, "Amount accumulated, not reset");
}
Recommended Mitigation
Use phase-indexed mappings so each phase has separate tracking, or check lastPhaseUpdateBlock to auto-reset stale data.
Option 1: Track phase-specific amounts
+ mapping(address => mapping(uint256 => uint256)) public addressSwappedAmountByPhase;
+ mapping(address => mapping(uint256 => uint256)) public addressLastSwapBlockByPhase;
function _beforeSwap(...) internal override returns (...) {
// ... phase logic ...
- addressSwappedAmount[sender] += swapAmount;
- addressLastSwapBlock[sender] = block.number;
+ addressSwappedAmountByPhase[sender][currentPhase] += swapAmount;
+ addressLastSwapBlockByPhase[sender][currentPhase] = block.number;
- if (addressLastSwapBlock[sender] > 0) {
- uint256 blocksSinceLastSwap = block.number - addressLastSwapBlock[sender];
+ if (addressLastSwapBlockByPhase[sender][currentPhase] > 0) {
+ uint256 blocksSinceLastSwap = block.number - addressLastSwapBlockByPhase[sender][currentPhase];
// ...
}
}
- function _resetPerAddressTracking() internal {
- addressSwappedAmount[address(0)] = 0;
- addressLastSwapBlock[address(0)] = 0;
- }
+ // No reset needed - each phase has separate tracking automatically
Option 2: Use phase start block for expiration
function _beforeSwap(...) internal override returns (...) {
// ... phase transition ...
+ // Check if user's tracking is from a previous phase
+ if (addressLastSwapBlock[sender] < lastPhaseUpdateBlock) {
+ // Reset for this user since it's a new phase
+ addressSwappedAmount[sender] = 0;
+ }
// Continue with normal logic...
}