Vanguard

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

Global State Variables Prevent Safe Multi-Pool Usage

Author Revealed upon completion

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.

// Global state - shared across all pools using this hook
uint256 public launchStartBlock;
uint256 public initialLiquidity;
uint256 public currentPhase;
function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
// @> Overwrites any previous pool's state
launchStartBlock = block.number;
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity);
currentPhase = 1;
// ...
}

Risk

Likelihood:

  • Requires deploying one hook for multiple pools (may not be common use case)

  • Could happen if owner attempts to reuse hook for multiple token launches

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 {
// First pool is already initialized in setUp()
uint256 pool1StartBlock = antiBotHook.launchStartBlock();
uint256 pool1Liquidity = antiBotHook.initialLiquidity();
// Create a second token and pool with same hook
MockERC20 token2 = new MockERC20("TOKEN2", "TK2", 18);
token2.mint(address(this), 1000 ether);
Currency token2Currency = Currency.wrap(address(token2));
// Roll forward some blocks
vm.roll(block.number + 50);
// Initialize second pool with same hook
(PoolKey memory key2,) = initPool(
ethCurrency,
token2Currency,
antiBotHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1_s
);
// State has been overwritten
assertGt(antiBotHook.launchStartBlock(), pool1StartBlock, "Start block overwritten");
assertEq(antiBotHook.currentPhase(), 1, "Phase reset to 1");
// Pool 1 now has wrong launch timing
}

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...
}

Support

FAQs

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

Give us feedback!