The hook determines the current protection phase using block height relative to the launch start block. Phase transitions should occur deterministically at consistent block boundaries to ensure predictable swap limits and cooldown enforcement.
The contract uses inconsistent boundary conditions between execution logic (_beforeSwap) and view functions (getCurrentPhase, getUserRemainingLimit, getUserCooldownEnd), causing a one-block window where the enforced phase differs from the reported phase—breaking monitoring tools and creating user confusion.
Likelihood: High
Reason 1: Phase transitions occur deterministically at fixed block intervals during every token launch lifecycle—this discrepancy triggers predictably at each phase boundary.
Reason 2: External systems (frontends, monitoring bots, analytics dashboards) exclusively rely on view functions like getCurrentPhase() to report user limits and cooldown status, guaranteeing exposure to the inconsistency.
Impact: Medium
Impact 1: Users receive incorrect remaining limit/cooldown information from view functions during the boundary block, potentially causing failed transactions when attempting swaps that appear allowed by the UI.
Impact 2: Security monitoring systems cannot reliably detect phase transitions or anomalous swap patterns, undermining incident response during actual bot attacks.
uint256 blocksSinceLaunch = block.number - launchStartBlock;
uint256 newPhase;
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
if (newPhase != currentPhase) {
_resetPerAddressTracking();
currentPhase = newPhase;
lastPhaseUpdateBlock = block.number;
}
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;
}
}
https:
Proof of Concept
Recommended Mitigation
- function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal override
returns (bytes4, BeforeSwapDelta, uint24)
{
// ...
uint256 blocksSinceLaunch = block.number - launchStartBlock;
uint256 newPhase;
- if (blocksSinceLaunch <= phase1Duration) {
+ if (blocksSinceLaunch < phase1Duration) {
newPhase = 1;
- } else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
+ } else if (blocksSinceLaunch < phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
// ...
}
// @> getCurrentPhase already uses correct < boundaries - no change needed
// @> Add NatSpec documentation to clarify boundary semantics:
/**
* @notice Returns current protection phase based on blocks since launch
* @dev Phase durations use exclusive upper bounds:
* Phase 1: [0, phase1Duration) blocks
* Phase 2: [phase1Duration, phase1Duration + phase2Duration) blocks
* Phase 3: [phase1Duration + phase2Duration, ∞) blocks
*/
function getCurrentPhase() public view returns (uint256) {
// ...
}
+