Vanguard

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

Broken Phase Transition Logic

Author Revealed upon completion

The hook implements a multi-phase anti-bot mechanism where swap limits and cooldowns should reset when transitioning between phases (Phase 1 → Phase 2 → Phase 3). However, the phase transition logic contains a critical flaw: the _resetPerAddressTracking() function only resets tracking data for address(0) instead of all user addresses. This renders the entire phase-based protection mechanism ineffective, as swap limits and cooldowns persist across phase boundaries.

https://github.com/CodeHawks-Contests/2026-01-vanguard/blob/9fa43cd0950d6baf301cada9b40d31c28b65bbe8/src/TokenLaunchHook.sol#L188C1-L193C1
function _resetPerAddressTracking() internal {
addressSwappedAmount[address(0)] = 0;
addressLastSwapBlock[address(0)] = 0;
}

Risk

Likelihood: High
Reason 1: Phase transitions occur deterministically based on block height (phase1Duration + phase2Duration), guaranteeing this bug triggers during every token launch lifecycle.
Reason 2: The flawed reset logic executes automatically during every phase transition via _beforeSwap(), requiring no special conditions or attacker interaction.

Impact: Critical
Impact 1: Attackers can exhaust Phase 1 swap limits (e.g., 5% of liquidity), then immediately continue swapping at full capacity in Phase 2 without cooldown—completely bypassing the intended protection.
Impact 2: The hook provides a false sense of security to projects deploying it, as its core anti-bot functionality is fundamentally broken while appearing operational to users and monitoring tools.


The implementation mistakenly assumes resetting address(0) clears the entire mapping. In Solidity, mappings cannot be iterated or bulk-reset—each key must be reset individually. Since user addresses are unknown at phase transition time, this approach is fundamentally unworkable.

Proof of Concept

// Assume: phase1Duration = 100 blocks, phase1LimitBps = 500 (5%)
// phase2Duration = 100 blocks, phase2LimitBps = 500 (5%)
// Block 1000: Pool initialized → Phase 1 begins
// Block 1050: Attacker swaps 5% of liquidity (hits Phase 1 limit)
// addressSwappedAmount[attacker] = 5% of liquidity
// Block 1100: Phase transition triggered (100 blocks after launch)
// _resetPerAddressTracking() executes → ONLY resets address(0)
// addressSwappedAmount[attacker] STILL = 5% of liquidity
// Block 1101: Attacker swaps another 5% of liquidity in Phase 2
// → No penalty applied (counter was never reset)
// → Successfully bypasses intended protection

Recommended Mitigation

-
+
function _resetPerAddressTracking() internal {
- addressSwappedAmount[address(0)] = 0;
- addressLastSwapBlock[address(0)] = 0;
+ // Cannot efficiently reset all users in one transaction
+ // Consider removing phase-based resetting or using different design
}
// Option 2: Track per-phase data separately (recommended)
+ mapping(address => mapping(uint256 => uint256)) public addressPhaseSwappedAmount;
+ mapping(address => mapping(uint256 => uint256)) public addressPhaseLastSwapBlock;
function _beforeSwap(...) ... {
// Track swaps per phase instead of globally
if (newPhase != currentPhase) {
- _resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
// Use phase-specific tracking
uint256 phaseSwapAmount = addressPhaseSwappedAmount[sender][currentPhase] + swapAmount;
uint256 phaseLastSwap = addressPhaseLastSwapBlock[sender][currentPhase];
// Update phase-specific tracking
addressPhaseSwappedAmount[sender][currentPhase] = phaseSwapAmount;
addressPhaseLastSwapBlock[sender][currentPhase] = block.number;
}
// Option 3: Use timestamp-based approach (alternative)
+ uint256 public phaseStartTimestamp;
+
function _beforeSwap(...) ... {
if (newPhase != currentPhase) {
currentPhase = newPhase;
+ phaseStartTimestamp = block.timestamp;
lastPhaseUpdateBlock = block.number;
}
// Check time elapsed since phase start, not last user swap
if (addressLastSwapBlock[sender] >= phaseStartTimestamp) {
// User has swapped in this phase
}
}

Support

FAQs

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

Give us feedback!