Vanguard

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

[L-04] Global State Variables Prevent Multi-Pool Support

Author Revealed upon completion

Root + Impact

Description

All state variables are global rather than per-pool, meaning the hook can only effectively serve one pool at a time.

Location: src/TokenLaunchHook.sol:46-56

// All global - not per-pool!
uint256 public currentPhase;
uint256 public lastPhaseUpdateBlock;
uint256 public launchStartBlock;
uint256 public initialLiquidity;
mapping(address => uint256) public addressSwappedAmount;
mapping(address => uint256) public addressLastSwapBlock;

If the same hook is attached to multiple pools:

  1. Pool A initializes: sets launchStartBlock = 100, initialLiquidity = 1000

  2. Pool B initializes: overwrites to launchStartBlock = 200, initialLiquidity = 500

  3. Pool A's protection is now broken (wrong start block and liquidity)

Risk

Likelihood:

  • Occurs when the same hook contract is attached to more than one pool

  • Hook reuse is a common pattern in Uniswap V4 to save deployment costs

  • No protection or documentation prevents multi-pool attachment

Impact:

  • Hook can only serve ONE pool correctly

  • Second pool initialization corrupts first pool's state

  • Phase calculations become incorrect for earlier pools

  • Swap limits based on wrong liquidity values

  • Potential for deliberate attack by creating malicious pool with same hook

Proof of Concept

The state variables are declared as single values rather than mappings keyed by PoolId. When a second pool initializes with the same hook, it overwrites the first pool's configuration.

function test_PoC_H04_GlobalStateNotPerPool() public {
// Record state from first pool initialization
uint256 originalLaunchBlock = hook.launchStartBlock();
uint256 originalLiquidity = hook.initialLiquidity();
console.log("After Pool 1 init:");
console.log(" launchStartBlock:", originalLaunchBlock);
console.log(" initialLiquidity:", originalLiquidity);
// State variable analysis - all are GLOBAL, not per-pool:
// - currentPhase: uint256 (not mapping(PoolId => uint256))
// - launchStartBlock: uint256 (not mapping(PoolId => uint256))
// - initialLiquidity: uint256 (not mapping(PoolId => uint256))
// - addressSwappedAmount: mapping(address => uint256) - NOT indexed by PoolId!
// If a second pool were initialized with this hook:
// poolStates would be overwritten, breaking Pool 1's protection
// Verify single-value storage (not per-pool)
assertTrue(hook.currentPhase() == 1, "currentPhase is single value");
assertTrue(hook.launchStartBlock() > 0, "launchStartBlock is single value");
}

Recommendations

Use per-pool state with PoolId mapping:

using PoolIdLibrary for PoolKey;
struct PoolState {
uint256 currentPhase;
uint256 launchStartBlock;
uint256 initialLiquidity;
uint256 lastPhaseUpdateBlock;
}
mapping(PoolId => PoolState) public poolStates;
mapping(PoolId => mapping(address => uint256)) public addressSwappedAmount;
mapping(PoolId => mapping(address => uint256)) public addressLastSwapBlock;
function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
PoolId poolId = key.toId();
poolStates[poolId].launchStartBlock = block.number;
poolStates[poolId].initialLiquidity = uint256(StateLibrary.getLiquidity(poolManager, poolId));
poolStates[poolId].currentPhase = 1;
// ...
}

Support

FAQs

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

Give us feedback!