Skip to content
Request an audit

‹ All findings

Uniswap v4 pool initialization can be frontrunned, setting an arbitrary price, and stealing tokens

Crit/HighSpirit Protocol·0xSimao · AI revenue sharing · 28th November 2025H-2

Description:

SpiritFactory::_setupUniswapPool() mints a single sided position, from the current tick to the max tick. Thus, users can buy Child tokens for at least a price of the tick, which goes up as liquidity decreases.

However, an attacker can predict the address of the Child token (deterministic), or frontrun the transaction, and initialize the pool to the lowest price possible. This will make it add liquidity in the full range of the Uniswap pool, and an attacker can get all Child very cheaply.

solidity
function _setupUniswapPool(address childToken, uint256 childTokenAmount, uint160 initialSqrtPriceX96)
    internal
    returns (uint256 tokenId)
{
    // Ensure tokens are in the correct order (lower address first)
    Currency currency0 =
        childToken < address(SPIRIT) ? Currency.wrap(address(childToken)) : Currency.wrap(address(SPIRIT));
    Currency currency1 =
        childToken > address(SPIRIT) ? Currency.wrap(address(childToken)) : Currency.wrap(address(SPIRIT));

    // Create the pool key
    PoolKey memory poolKey = PoolKey({
        currency0: currency0,
        currency1: currency1,
        fee: DEFAULT_POOL_FEE,
        tickSpacing: DEFAULT_TICK_SPACING,
        hooks: IHooks(address(0))
    });

    // Initialize the pool
    POSITION_MANAGER.initializePool(poolKey, initialSqrtPriceX96); //@audit anyone can frontrun this

    tokenId = _mintSingleSidedLiquidityPosition(childToken, childTokenAmount, poolKey);
}

The key thing is that POSITION_MANAGER.initializePool() doesn't revert, it fails silently, so there is nothing the admin can do:

solidity
abstract contract PoolInitializer_v4 is ImmutableState, IPoolInitializer_v4 {
    /// @inheritdoc IPoolInitializer_v4
    function initializePool(PoolKey calldata key, uint160 sqrtPriceX96) external payable returns (int24) {
        try poolManager.initialize(key, sqrtPriceX96) returns (int24 tick) {
            return tick;
        } catch {
            return type(int24).max;
        }
    }
}

Impact:

Stolen Child

Recommended Mitigation:

Check the return value of the pool initialization, it must not be the maximum tick, revert if it is. Additionally, add a salt to the child token address generation, so any name/symbol token can be created. This may cause conflicts, so make sure to track the deployed name/symbol pair (can just add a mapping in the factory).

0xSimao:

Fixed in PR #8.