Vanguard

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

Phase Boundary Inconsistency: _beforeSwap Uses <= While getCurrentPhase Uses <, Causing View Functions to Report Wrong Phase

Author Revealed upon completion

Root + Impact

Description

The _beforeSwap function and getCurrentPhase view function use inconsistent comparison operators for phase boundary checks, causing them to report different phases at exact boundary blocks.

In _beforeSwap (lines 139-145):

if (blocksSinceLaunch <= phase1Duration) { // Uses <=
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) { // Uses <=
newPhase = 2;
} else {
newPhase = 3;
}

In getCurrentPhase (lines 197-203):

if (blocksSinceLaunch < phase1Duration) { // Uses <
return 1;
} else if (blocksSinceLaunch < phase1Duration + phase2Duration) { // Uses <
return 2;
} else {
return 3;
}

At the exact boundary block where blocksSinceLaunch == phase1Duration:

  • getCurrentPhase() returns Phase 2 (because 100 < 100 is false)

  • _beforeSwap calculates Phase 1 (because 100 <= 100 is true)

Impact

Users calling getUserRemainingLimit() or getUserCooldownEnd() at phase boundaries receive incorrect phase information:

  1. getUserRemainingLimit() reports Phase 2 limits (e.g., 5% of liquidity)

  2. User swaps amount within reported Phase 2 limit

  3. _beforeSwap actually applies Phase 1 limit (e.g., 1% of liquidity)

  4. User gets unexpected penalty fees despite following the view function's guidance

This breaks the trust model of the hook's user-facing view functions.

Risk

Likelihood: High - Occurs at every phase boundary (Phase 1→2 and Phase 2→3), predictable timing

Impact: Medium - Users suffer unexpected penalty fees when trusting view functions

Proof of Concept

// At boundary block where blocksSinceLaunch == phase1Duration (e.g., 100):
//
// getCurrentPhase():
// if (100 < 100) → FALSE
// else if (100 < 300) → TRUE
// return 2 ← Reports Phase 2
//
// _beforeSwap():
// if (100 <= 100) → TRUE
// newPhase = 1 ← Applies Phase 1
//
// Result: View says Phase 2, swap enforces Phase 1

Recommended Mitigation

Align the operators in getCurrentPhase to match _beforeSwap:

function getCurrentPhase() public view returns (uint256) {
if (launchStartBlock == 0) return 0;
uint256 blocksSinceLaunch = block.number - launchStartBlock;
if (blocksSinceLaunch <= phase1Duration) { // Changed < to <=
return 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) { // Changed < to <=
return 2;
} else {
return 3;
}
}

Support

FAQs

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

Give us feedback!