Description
-
The hook uses global state variables for launchStartBlock, initialLiquidity, and currentPhase.
-
If the same hook instance is used for multiple pools, initializing a second pool will overwrite the first pool's state, breaking its anti-bot protection.
uint256 public launchStartBlock;
uint256 public initialLiquidity;
uint256 public currentPhase;
function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
launchStartBlock = block.number;
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
currentPhase = 1;
}
Risk
Likelihood:
Impact:
-
First pool's launch start block is overwritten
-
First pool's initial liquidity is overwritten (limits become incorrect)
-
First pool's phase is reset to 1
-
Complete loss of anti-bot protection for first pool
Proof of Concept
Initialize a second pool with the same hook - first pool's launchStartBlock gets overwritten, breaking its phase timing.
function test_MultiPoolStateOverwrite() public {
uint256 pool1StartBlock = antiBotHook.launchStartBlock();
uint256 pool1Liquidity = antiBotHook.initialLiquidity();
MockERC20 token2 = new MockERC20("TOKEN2", "TK2", 18);
token2.mint(address(this), 1000 ether);
Currency token2Currency = Currency.wrap(address(token2));
vm.roll(block.number + 50);
(PoolKey memory key2,) = initPool(
ethCurrency,
token2Currency,
antiBotHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1_s
);
assertGt(antiBotHook.launchStartBlock(), pool1StartBlock, "Start block overwritten");
assertEq(antiBotHook.currentPhase(), 1, "Phase reset to 1");
}
Recommended Mitigation
Use pool-indexed mappings for state variables: mapping(PoolId => uint256).
- uint256 public launchStartBlock;
- uint256 public initialLiquidity;
- uint256 public currentPhase;
+ mapping(PoolId => uint256) public poolLaunchStartBlock;
+ mapping(PoolId => uint256) public poolInitialLiquidity;
+ mapping(PoolId => uint256) public poolCurrentPhase;
function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
if (!key.fee.isDynamicFee()) {
revert MustUseDynamicFee();
}
+ PoolId poolId = key.toId();
- launchStartBlock = block.number;
+ poolLaunchStartBlock[poolId] = block.number;
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
- initialLiquidity = uint256(liquidity);
+ poolInitialLiquidity[poolId] = uint256(liquidity);
- currentPhase = 1;
+ poolCurrentPhase[poolId] = 1;
// ...
}
function _beforeSwap(address sender, PoolKey calldata key, SwapParams calldata params, bytes calldata)
internal
override
returns (bytes4, BeforeSwapDelta, uint24)
{
+ PoolId poolId = key.toId();
- if (launchStartBlock == 0) revert PoolNotInitialized();
+ if (poolLaunchStartBlock[poolId] == 0) revert PoolNotInitialized();
// Use pool-specific state throughout...
}