Vanguard

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

getCurrentPhase() Uses Different Comparison Operator Than _beforeSwap() Causing View/Swap Phase Mismatch

Author Revealed upon completion

Root + Impact

Description

The getCurrentPhase() view function and _beforeSwap() internal function use different comparison operators for phase boundary checks, causing a one-block discrepancy where view functions report a different phase than what's actually applied during swaps.

In _beforeSwap() (actual swap logic):

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

In getCurrentPhase() (view function):

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

At exactly blocksSinceLaunch == phase1Duration:

  • _beforeSwap() evaluates <= → TRUE → returns Phase 1

  • getCurrentPhase() evaluates < → FALSE → returns Phase 2

Risk

Likelihood:

  • This mismatch occurs deterministically at every phase boundary block

  • Phase boundaries happen exactly twice per pool lifecycle (Phase 1→2, Phase 2→3)

  • Any user or frontend querying state at these blocks will see incorrect information

Impact:

  • getUserRemainingLimit() returns wrong limits at boundary blocks (uses getCurrentPhase())

  • getUserCooldownEnd() returns wrong cooldowns at boundary blocks

  • Frontends/DApps display Phase 2 while swaps execute with Phase 1 rules

  • Users may attempt larger swaps expecting Phase 2 limits but get penalized under Phase 1 rules

Proof of Concept

function testPhaseBoundaryMismatch() public {
// Setup: phase1Duration = 100 blocks
// Fast forward to exactly phase1Duration
vm.roll(launchStartBlock + 100);
// Query view function
uint256 viewPhase = hook.getCurrentPhase();
// Returns: 2 (because 100 < 100 is FALSE)
// But _beforeSwap internally calculates:
// 100 <= 100 is TRUE, so newPhase = 1
// MISMATCH DEMONSTRATED
// User sees Phase 2 (5% limit), swap uses Phase 1 (1% limit)
}

Recommended Mitigation

Align comparison operators - change getCurrentPhase() to use <=:

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

Support

FAQs

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

Give us feedback!