Description
-
The contract has two places where phase is calculated: _beforeSwap (for actual enforcement) and getCurrentPhase (view function for external queries).
-
These two functions use different comparison operators (<= vs <), causing a 1-block discrepancy in phase reporting.
function _beforeSwap(...) internal override returns (...) {
uint256 blocksSinceLaunch = block.number - launchStartBlock;
uint256 newPhase;
if (blocksSinceLaunch <= phase1Duration) {
newPhase = 1;
} else if (blocksSinceLaunch <= phase1Duration + phase2Duration) {
newPhase = 2;
} else {
newPhase = 3;
}
}
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:
-
Occurs at every phase boundary block (e.g., block launchStartBlock + phase1Duration)
-
Users querying during these exact blocks will see incorrect phase
Impact:
-
getUserRemainingLimit() and getUserCooldownEnd() return values based on wrong phase
-
Frontend/UI displays incorrect phase information
-
Users may make decisions based on incorrect phase data
-
At boundary blocks: getCurrentPhase() returns Phase 2, but swap executes with Phase 1 rules
Proof of Concept
At phase boundary block: getCurrentPhase() returns 2, but swap executes with Phase 1 rules.
function test_PhaseOffByOne() public {
vm.roll(block.number + phase1Duration);
assertEq(antiBotHook.getCurrentPhase(), 2, "View says Phase 2");
vm.deal(user1, 10 ether);
vm.startPrank(user1);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.001 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 0.001 ether}(key, params, testSettings, ZERO_BYTES);
vm.stopPrank();
assertEq(antiBotHook.currentPhase(), 1, "Swap executed with Phase 1 rules");
}
Recommended Mitigation
Change < to <= in getCurrentPhase() to match _beforeSwap logic.
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;
}
}