Vanguard

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

_resetPerAddressTracking() only clears address(0) entries, so per-address limits/cooldowns never reset across phases

Author Revealed upon completion

Root + Impact

During phase changes, the hook attempts to “reset per-address tracking” but only writes to mapping slots for address(0). This does not affect any real user/router entries, so addressSwappedAmount[sender] and addressLastSwapBlock[sender] persist across phases. Consequently, phase 2 does not start fresh for existing participants; it inherits phase 1 accumulated usage and cooldown status.

// TokenLaunchHook.sol
if (newPhase != currentPhase) {
// @> Intended: reset per-address tracking on phase change
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
function _resetPerAddressTracking() internal {
// @> Only resets address(0); does not affect actual tracked senders
addressSwappedAmount[address(0)] = 0;
addressLastSwapBlock[address(0)] = 0;
}

Risk

Likelihood:

  • Phase transitions occur deterministically as blocks progress from launch start, so _resetPerAddressTracking() is executed on the first swap in the new phase.


Impact:

  • Phase 2 “relaxed limits” are not actually applied to existing participants; they may immediately trigger penalties or appear to have no remaining limit.

Proof of Concept

function test_POC_ResetPerAddressTracking_DoesNotResetAcrossPhase() 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});
// Phase 1 swap records usage for sender (= swapRouter)
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
uint256 recorded = antiBotHook.addressSwappedAmount(address(swapRouter));
assertGt(recorded, 0, "Expected recorded swapped amount in phase 1");
vm.stopPrank();
// Move to phase 2 and trigger phase transition logic with a swap
vm.roll(antiBotHook.launchStartBlock() + phase1Duration + 1);
vm.deal(user2, 10 ether);
vm.startPrank(user2);
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
// Phase changed to 2, but mapping for swapRouter was NOT reset
assertEq(antiBotHook.currentPhase(), 2, "Should be in phase 2");
assertEq(
antiBotHook.addressSwappedAmount(address(swapRouter)),
recorded + 0.001 ether, // it accumulates; it did not reset
"Swapped amount should have been reset on phase transition but was not"
);
}

Recommended Mitigation

You can’t iterate mappings, so you need lazy reset keyed by phase.

One robust pattern: store the last phase seen by each sender and reset their counters when they first swap in a new phase.

- remove this code
+ add this code

Support

FAQs

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

Give us feedback!