Vanguard

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

Incorrect Phase State Management Leads to Inconsistent Enforcement (DESIGN FLAW + LOGIC BREAK)

Author Revealed upon completion

Root + Impact

The contract maintains two independent phase determination mechanisms, which are expected to represent the same logical state but are not guaranteed to remain synchronized.

This architectural inconsistency causes the enforcement logic used during swaps to diverge from the phase information exposed to users and off-chain systems, resulting in incorrect application of limits and penalties.

Description

The contract determines the current phase in two different ways:

  • A mutable storage variable currentPhase, which is updated during _beforeSwap

  • A derived phase calculation via getCurrentPhase(), which computes the phase dynamically based on block.number

The hook’s enforcement logic relies on the stored currentPhase, while view and helper functions rely on getCurrentPhase().

Because currentPhase is only updated when a swap occurs, it may lag behind the actual phase determined by block height. If no swap happens at the exact block where a phase transition occurs, the stored phase becomes stale.

This introduces a state divergence between execution-time enforcement and user-visible phase information.

// @> Stored phase, updated only during swap execution
uint256 public currentPhase;
// @> Derived phase, calculated dynamically
function getCurrentPhase() public view returns (uint256) {
if (launchStartBlock == 0) return 0;
uint256 blocksSinceLaunch = block.number - launchStartBlock;
if (blocksSinceLaunch < phase1Duration) {
return 1;
} else if (blocksSinceLaunch < phase1Duration + phase2Duration) {
return 2;
} else {
return 3;
}
}

Risk

Likelihood:

  • Reason 1: Phase transitions are based solely on block height and do not require a swap to occur at the transition boundary

Reason 2: currentPhase is only updated inside _beforeSwap, allowing it to remain outdated until the next swap

Impact:

  • Impact 1: Users may be penalized under stricter rules even though the protocol has already entered a later phase

Impact 2: Frontends and off-chain systems may display incorrect limits, cooldowns, or penalties compared to actual enforcement

Proof of Concept

This issue occurs naturally during normal operation and does not require a malicious actor.

  1. The pool is initialized at block N

  2. Phase 1 is configured to last X blocks and should end at block N + X

  3. No swap is executed at block N + X

  4. At block N + X + 1:

    • getCurrentPhase() returns Phase 2

    • currentPhase remains Phase 1

  5. The next swap is executed and enforcement logic applies Phase 1 limits and penalties instead of Phase 2

This demonstrates that enforcement logic can lag behind the actual phase state.

// PoC: Demonstrates phase desynchronization between stored and derived state
contract PhasePoC {
TokenLaunchHook hook;
constructor(TokenLaunchHook _hook) {
hook = _hook;
}
function proofOfPhaseDesync()
external
view
returns (uint256 derivedPhase, uint256 storedPhase)
{
// Derived phase is computed dynamically using block.number
derivedPhase = hook.getCurrentPhase();
// Stored phase is only updated during beforeSwap
storedPhase = hook.currentPhase();
// When no swap occurs at the phase boundary:
// derivedPhase > storedPhase
}
}

Recommended Mitigation

Phase is a derived state that can be deterministically calculated from launchStartBlock and block.number. Storing it in contract state introduces unnecessary complexity and creates opportunities for state desynchronization.

To ensure consistent behavior, the contract should rely on a single phase calculation mechanism that is evaluated at execution time.

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

Support

FAQs

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

Give us feedback!