Vanguard

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

The initializer lacks reentrancy protection and access control, making it easy for attackers to maliciously modify state variables

Author Revealed upon completion

Root + Impact

Description

  • The _afterInitialize function is intended to set up critical launch parameters (such as startBlock and phase durations) exactly once for the specific legitimate pool associated with the token launch. Once initialized, these time-sensitive state variables should remain immutable to ensure the integrity of the anti-bot phases.

  • Lacks validation to verify the identity of the PoolKey or check if the hook has already been initialized. Since Uniswap V4 allows any user to attach an existing hook to a new malicious pool, an attacker can trigger _afterInitialize repeatedly. This action overwrites global state variables.

function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
@> VULNERABILITY: Missing check for 'isInitialized' or 'key' validation.
if (!key.fee.isDynamicFee()) {
revert MustUseDynamicFee();
}
launchStartBlock = block.number;
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
currentPhase = 1;
lastPhaseUpdateBlock = block.number;
return BaseHook.afterInitialize.selector;
}

Risk

Likelihood:

The V4 allows any one to create pool, automatically executing the initialization logic of hook without restriction

Impact:

Attacker can repeatedly resetting the `lauchStartBlock`, so that this initial phase never changes

Proof of Concept

_afterInitialize function fails to prevent re-initialization.
Attacker can create a new pool with a different tickSpacing to trigger
_afterInitialize again.
Can overwrites launchStartBlock,
resetting the anti-bot phases.

function test_Poc_MaliciousPoolResetsState() public {
uint256 originalBlock = antiBotHook.launchStartBlock();
vm.roll(block.number + 100);
// attack
PoolKey memory maliciousKey = key;
// here, we reset the tickSpacing using different value
maliciousKey.tickSpacing = 100;
manager.initialize(maliciousKey, SQRT_PRICE_1_1_s);
assertEq(antiBotHook.launchStartBlock(), block.number, "Start block reset");
assertGt(antiBotHook.launchStartBlock(), originalBlock, "Original block unchanged");
}

Recommended Mitigation

Let's write a decorator that uses an existing variable (launchStartBlock) as the sole check:
if it has already been initialized once, launchStartBlock will be greater than 0,
which ensures the initialization runs only once.

+ error PoolAlreadyInitialized();
+ modifier initOnce() {
+ if (launchStartBlock !=0) revert PoolAlreadyInitialized();
+ _;
+ }
- function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4)
+ function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override initOnce returns (bytes4)

Support

FAQs

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

Give us feedback!