Vanguard

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

Phase Change Does Not Reset Per Address Tracking

Author Revealed upon completion

Root + Impact

Description

When the protocol transitions from Phase 1 to Phase 2 (or Phase 2 to Phase 3), the per address swap totals (addressSwappedAmount) are not reset. Users can accumulate unlimited swaps across phases, combining phase limits.

Normal behavior: Phase 1 has a 1% limit, Phase 2 has a 5% limit. On phase transition, tracking should reset so Phase 2 users start fresh with their 5% allocation. Instead, accumulated swaps carry over.

Root Cause

// File: src/TokenLaunchHook.sol, lines 189-191
function _resetPerAddressTracking() internal {
addressSwappedAmount[address(0)] = 0; // @> ISSUE: Only resets address(0), not all users
}
// Called only in specific conditions, never on phase change

The function is defined but never called during phase transitions. Users accumulate swaps across all phases.

Risk

Likelihood: HIGH

  • Phase transitions happen automatically based on block numbers

  • No additional action needed, accumulation is passive

  • Affects every user transitioning between phases

Impact: HIGH

  • User can swap 1% in Phase 1 + 5% in Phase 2 = 6% total (should be max 1% then max 5%)

  • Early launch protection is weakened across multiple phases

  • Bot can gradually accumulate beyond intended limits

  • Direct fund loss: cumulative limit bypass

Proof of Concept

The PoC makes a swap in Phase 1, then advances blocks into Phase 2 and swaps again. The “swapped amount” counter keeps increasing instead of resetting, proving the contract doesn’t clear per user tracking when the phase changes.

function test_Finding_PhaseChangeDoesNotResetTracking() public {
vm.deal(user1, 1 ether);
vm.startPrank(user1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.05 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings = PoolSwapTest
.TestSettings({takeClaims: false, settleUsingBurn: false});
// Swap in Phase 1
swapRouter.swap{value: 0.05 ether}(key, params, testSettings, ZERO_BYTES);
uint256 phase1Swapped = antiBotHook.addressSwappedAmount(address(swapRouter));
// Jump to Phase 2
vm.roll(block.number + phase1Duration + 1);
// Swap again in Phase 2
swapRouter.swap{value: 0.05 ether}(key, params, testSettings, ZERO_BYTES);
uint256 phase2Swapped = antiBotHook.addressSwappedAmount(address(swapRouter));
assertGt(
phase2Swapped,
phase1Swapped,
"Phase 2 accumulates on Phase 1—tracking not reset"
);
}

Recommended Mitigation

eset each user’s tracked amount whenever the phase changes. That way each phase starts with a clean limit, and users can’t carry over old swaps into the next phase.

function _beforeSwap(...) {
address user = msg.sender;
uint256 currentPhaseNum = getCurrentPhase();
// Check if phase has changed since last tracking
if (lastPhaseForAddress[user] < currentPhaseNum) {
addressSwappedAmount[user] = 0; // @> Reset accumulated amount on phase change
lastPhaseForAddress[user] = currentPhaseNum;
}
// ... remaining logic
}

Support

FAQs

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

Give us feedback!