Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · March 2026

Unilaunch Launchpad and Limit Order Book

for Gamma Strategies

Gamma engaged Guardian to review the security of their Unilaunch - LaunchPad + LimitOrderBook. From the 2nd of January 2026 to the 4th of February 2026, a team of 4 auditors reviewed the source code in scope.

Published
Review window
January 2 to February 4, 2026
Rounds
Main Review, Remediation Review V1, Remediation Review V2
Language
Solidity
Chains
Ethereum, Arbitrum, Optimism, Base, Polygon, BNB Chain
Sector
Token launches
  • 0 Critical
  • 8 High
  • 8 Medium
  • 5 Low
  • 7 Informational

19 resolved · 4 partially resolved · 5 acknowledged

Scope

Overview

Gamma engaged Guardian to review the security of their Unilaunch - LaunchPad + LimitOrderBook. From the 2nd of January 2026 to the 4th of February 2026, a team of 4 auditors reviewed the source code in scope.

Findings 28

Main Review

21 findings
  1. H-01 High Deterministic Token Address Collision DoS Frontrunning Resolved
    Location
    TokenLaunchFactory.sol
    Round
    Main Review

    Description

    Proof of concept: PoC

    When a user calls deployTokenAndLaunchPresale on TokenLaunchFactory contract, the launchpad deploys the token (via the selected token factory) and then transfers launchAmount to the LiquidityLauncher, while sending the remaining supply (totalSupply - launchAmount) to msg.sender.

    Because TokenLaunchFactory is always the caller, and because graffiti (and for USUPERC20 also creator) is constant, an attacker can front-run a victim by calling deployTokenAndLaunchPresale first with the same visible token identity fields.

    The victim’s transaction then reverts due to a CREATE2 collision and the front-runner receive the non-launch portion of supply. Consequently, any user can be permanently DoS’d from launching a desired launchpad, and an attacker can hijack a launch and receive the non-auction token allocation.

    Recommendation

    Make the CREATE2 namespace unique per user/launch by using a non-constant graffiti derived from the original caller (and ideally a user-provided salt/nonce), and pass that into the token factory.

    Resolution

    Gamma Team: Resolved.

  2. H-02 High Launch Griefing Via CREATE2 Strategy Predeploy Access Control Resolved
    Location
    SuperchainLBPStrategyDeployer.sol: 27
    Round
    Main Review

    Description

    Proof of concept: PoC

    In SuperchainLBPStrategyDeployer contract, deploy function is externally callable and permissionless, and deploys SuperchainLBPStrategy via CREATE2 using a caller-supplied salt. In the intended flow, LiquidityLauncher.distributeToken derives a deterministic salt and passes it to SuperchainLBPStrategyFactory.initializeDistribution, so the strategy address becomes deterministic for a given salt and constructor inputs.

    An attacker can observe a pending launch transaction, recover the same salt and constructor inputs, and front-run by calling SuperchainLBPStrategyDeployer.deploy directly. This pre-deploys SuperchainLBPStrategy at the deterministic CREATE2 address the factory would later use.

    When the legitimate launch proceeds, the factory attempts to deploy the same contract at the same CREATE2 address via the deployer, but the address is already occupied, causing a CREATE2 collision and reverting. This allows a denial-of-service against every launches.

    Even if SuperchainLBPStrategyDeployer.deploy is restricted, the same collision/DoS can still be triggered by calling SuperchainLBPStrategyFactory.initializeDistribution directly with the same salt and configData, since initializeDistribution is also externally callable and not restricted to LiquidityLauncher.

    Recommendation

    Restrict the SuperchainLBPStrategyDeployer.deploy function so only the trusted SuperchainLBPStrategyFactory can call it, and restrict SuperchainLBPStrategyFactory.initializeDistribution so only the canonical LiquidityLauncher can call it.

    Resolution

    Gamma Team: Resolved.

  3. H-03 High Arbitrary auctionFactory Enables Token Siphon Validation Resolved
    Location
    LBPStrategyBasic.sol: 126
    Round
    Main Review

    Description

    Proof of concept: PoC

    The launch flow allows creator to provide MigratorParameters.auctionFactory, and this address is forwarded into the deployed strategy unchanged.

    The strategy then fully trusts that factory deploys a correct Continuous Clearing Auction and to follow the expected behaviour around raised funds, clearing price, and recipient handling.

    In LBPStrategyBasic contract, during onTokensReceived execution, the strategy calls the user-specified factory and then transfers the auction’s token supply to the returned contract.

    A malicious creator can therefore point auctionFactory to a custom factory that returns a fake auction contract which can siphon the auction token supply, accept bidders’ currency and make it unrecoverable, sweep it to an attacker or lock and misdirect the auction supply and raised currency.

    Recommendation

    Do not allow arbitrary auctionFactory in launches. Enforce a trusted allowlist (or a single immutable factory) in the TokenLaunchFactory.

    Resolution

    Gamma Team: Resolved.

  4. H-04 High Front-Run Pool Init To Block Limit Order Support DoS Resolved
    Location
    OrderBookFactory.sol: 588
    Round
    Main Review

    Description

    Proof of concept: PoC

    OrderBookFactory allows users to create Uniswap V4 pools with limit order support, allowing users to register their pools with various different hook addresses.

    An attacker can front-run pool creation by calling PoolManager.initialize() directly in Uniswaps PoolManager contract with the extracted pool parameters. The victim's transaction then reverts at _initializePool() since the pool already exists, rolling back all hook registration and configuration.

    The resulting pool exists in Uniswap V4 but permanently lacks the intended dynamic fee/limit order support.

    No recovery path exists since the registerPool() function within the hook contracts is only callable by OrderBookFactory, and no admin functions can manually configure the hook state.

    The only hook that is not vulnerable to this is VolatilityDynamicFeeLimitOrderHook, which checks access control within the beforeInitialize hook.

    Recommendation

    For each hook that can be registered through OrderBookFactory, include a beforeInitialize hook to ensure msg.sender is equal to the OrderBookFactory contract, allowing only this contract to register the pools with the intended hooks.

    Resolution

    Gamma Team: Resolved.

  5. H-05 High Migration Blocked Via Hook Owner Assignment DoS Resolved
    Location
    OrderBookFactory.sol: 308
    Round
    Main Review

    Description

    Proof of concept: PoC

    During a token launch, the system reserves a specific poolId in advance so that the later migration step can reuse that same poolId.

    The createVolatilityDynamicLimitOrderPoolWithManager function derives the poolId inside computePoolIdForVolatilityLimitOrder, which depends on the hook address among other parameters. If creatorVolatilityDynamicLimitOrderHooks[hookOwner] is already set, the function uses the existing hook address.

    Because createVolatilityDynamicLimitOrderPoolWithManager is permissionless, a user can call it using the victim’s hookOwner before migration runs. This sets creatorVolatilityDynamicLimitOrderHooks[hookOwner] ahead of time. When the migration later executes, computePoolIdForVolatilityLimitOrder uses the pre-set hook address and derives a different poolId than the one reserved during launch.

    If the attacker created a pool with the same parameters, migration reverts inside VolatilityDynamicLimitOrderHook.registerPool with PoolParametersAlreadyUsed. If the attacker used different tokens or parameters, migration proceeds but treats the pool as unreserved and charges the creation fee, reverting with InsufficientETH.

    This allows any user to grief a token launch by invalidating its pool reservation and causing migration to fail.

    Recommendation

    Modify the token launch process to bind the hook address to the reserved pool at launch so it can’t be changed later.

    Resolution

    Gamma Team: Resolved.

  6. H-06 High MPM Name Collision Bricks Migration Logical Error Partially resolved
    Location
    MultiPositionFactory.sol: 133
    Round
    Main Review

    Description

    Proof of concept: PoC

    In deployMultiPositionManager, the MultiPositionFactory contract enforces a global uniqueness on the name string for every MultiPositionManager deployment via usedNames mapping. Any attempt to deploy another manager with the same name reverts.

    LaunchPad migration supplies a user-chosen mpmName, which is forwarded into MultiPositionFactory.deployMultiPositionManager.

    However, because this function is permissionless and the uniqueness check is global (not scoped to a pool or owner), an attacker can deploy an arbitrary MPM using the victim’s intended mpmName (read from the deployed strategy).

    This permanently sets usedNames[mpmName] = true. When migrate later attempts to deploy the MPM using the same mpmName, the call reverts with NameAlreadyUsed error, and migration cannot be completed, permanently blocking the launchpad’s intended migration flow.

    Recommendation

    Remove the global usedNames[name] revert entirely and treat name as non-unique metadata.

    Alternatively, keep name uniqueness but scope it to the owner (e.g. implement mapping(address => mapping(string => bool)) usedNamesByOwner) so each managerOwner can only use a given name once.

    Resolution

    Gamma Team: Partially Resolved.

  7. H-07 High Permissionless Factory Enables Migration DoS DoS Partially resolved
    Location
    OrderBookFactory.sol: 770
    Round
    Main Review

    Description

    Proof of concept: PoC

    MultiPositionFactory functions deployMultiPositionManager, deployDepositAndRebalance, deployDepositAndRebalanceSwap are all permissionless.

    The deployDepositAndRebalance. function is called from OrderBookFactory during token launch migrations, which proceeds to deploy a new MultiPositionManager contract using poolKey, managerOwner, and name for the salt.

    The problem is an attacker can front-run the migration and directly call deployMultiPositionManager which will proceed to deploy the same MultiPositionManager contract first. The victim's migration call will revert due to address collision.

    This issue is distinct from the MPM Name Collision finding - even if name uniqueness validation is removed, the CREATE2 address collision would still cause the deployment to fail. Both fixes are required for complete mitigation.

    Note that the attacker cannot execute the front-run attack for functions deployDepositAndRebalance and deployDepositAndRebalanceSwap because it requires the (reserved) pool to be initialized first.

    Recommendation

    Restrict deployDepositAndRebalance and deployDepositAndRebalanceSwap to only be callable by OrderBookFactory (preventing DoS to pool creation with limit order support), and change deployMultiPositionManager visibility to internal.

    Resolution

    Gamma Team: Partially Resolved.

  8. M-01 Medium Hook Owner Can Brick Swaps Via Fee Params Validation Partially resolved
    Location
    VolatilityDynamicFeeLimitOrderHook.sol VolatilityDynamicFeeHook.sol
    Round
    Main Review

    Description

    VolatilityDynamicFeeLimitOrderHook and VolatilityDynamicFeeHook updates the pool’s dynamic LP fee inside beforeSwap by calling poolManager.updateDynamicLPFee. Uniswap v4 enforces MAX_LP_FEE = 1000000 (100%), and updateDynamicLPFee reverts if the passed fee exceeds that maximum.

    The hook does not validate that its configured parameters (baseFee, surgeMultiplier, surgeDuration) can never produce a totalFee above this cap.

    On every swap, the hook computes a time-decaying surge fee and adds it to the fixed baseFee, then attempts to write the sum as the pool’s LP fee. If the sum exceeds the v4 maximum, updateDynamicLPFee reverts and the entire swap reverts.

    A malicious (or careless) hook owner can brick trading by setting baseFee greater than MAX_LP_FEE (permanent swap reverts), or by choosing parameters where baseFee + surgeFee exceeds the cap during surge mode.

    Recommendation

    On registerPool, updateBaseFee, and updateSurgeParams, enforce parameter constraints so totalFee can never exceed MAX_LP_FEE.

    Resolution

    Gamma Team: Partially Resolved.

  9. M-02 Medium Pool Reservation Griefing Via Front-Running DoS Partially resolved
    Location
    OrderBookFactory.sol: 1127
    Round
    Main Review

    Description

    Proof of concept: PoC

    An attacker can permanently DoS a victim's token launch via front-run by calling LiquidityLauncher.distributeToken() directly with the victim's configData without owning any of the victim's launch token.

    This means sorted(TOKEN, USDC) == sorted(USDC, TOKEN), allowing an attacker to flip the pair. Attack Flow: 1. Victim submits launch for (token=LAUNCH_TOKEN, currency=USDC) 2. Attacker sees pending tx and extracts hookOwner, hookSalt, tickSpacing 3. Attacker calls LiquidityLauncher.distributeToken() directly with flipped parameters: token = USDC (attacker holds this) migratorParams.currency = LAUNCH_TOKEN

    1. Both compute identical poolId since currencies are sorted.
    2. Attacker reserves poolId with dust amount (2 wei USDC), and victim's tx reverts with PoolAlreadyReserved:

    Although LiquidityLauncher derives a unique salt as keccak256(abi.encode(msg.sender, userSalt)), this salt is not used in the poolId computation. Only the user-provided hookOwner and hookSalt from configData are used, causing the victim's transaction to revert:

    if (reservedPools[poolId] != address(0)) revert PoolAlreadyReserved();
    

    This attack does not work for ETH-paired launches (currency = address(0)) because LiquidityLauncher.distributeToken() requires an ERC20 token and cannot accept ETH for the distribute token.

    The victim must change their pool parameters to launch, while the attacker can repeat this indefinitely at minimal cost.

    Recommendation

    Utilize the caller-unique salt parameter in SuperchainLBPStrategyFactory.initializeDistribution() when deriving the poolId. Ensure access control is restricted only to LiquidityLauncher.

    Resolution

    Gamma Team: Partially Resolved.

  10. M-03 Medium One-Sided Token Toggle Ignored On Migrate Unexpected Behavior Acknowledged
    Location
    SuperchainLBPStrategy.sol: 152
    Round
    Main Review

    Description

    In the base LBP implementation, the token transfer amount is conditional - it only transfers the full reserveSupply when a one-sided position is actually going to be created (encoded as data.hasOneSidedParams), otherwise it transfers only what is needed for the main position and leaves leftover tokens inside the strategy.

    In SuperchainLBPStrategy, _createPositionPla is overridden to return empty, so hasOneSidedParams is never produced and the strategy replaces the transfer sizing with _calculateTransferAmounts, but the token side is computed.

    Because data.initialTokenAmount is always less than or equal to reserveSupply by construction, this effectively always transfers the whole reserveSupply. This means that when reserveSupply > data.initialTokenAmount and the user has createOneSidedTokenPosition == false (so a one-sided token position is not desired), SuperchainLBPStrategy still transfers all reserved tokens.

    In the migration path, those amounts are then used as the deposit amounts into the OrderBookFactory pool and MPM creation call.

    The factory pulls the tokens from the strategy and deposits them into the newly deployed MultiPositionManager, so the extra tokens do not remain in the strategy for later sweeping as intended, and they are moved into the MPM regardless of the one-sided token setting.

    Recommendation

    Adjust _calculateTransferAmounts functions so leftover reserved tokens are only included when a one-sided token position is intended.

    Resolution

    Gamma Team: Acknowledged.

  11. M-04 Medium No-Strategy Branch In Factory Always Reverts Logical Error Resolved
    Location
    RebalanceLogic.sol: 158
    Round
    Main Review

    Description

    In OrderBookFactory, _deployMultiPositionManager includes a branch to support deploying an MPM with rebalanceParams.strategy == address(0) by calling deployDepositAndRebalance on MultiPositionFactory.

    However, this function always deploys a fresh MPM and immediately executes MultiPositionManager.rebalance right after deployment.

    Since a newly deployed MPM has no previously stored strategy, RebalanceLogic deterministically reverts with NoStrategySpecified when the passed strategy is zero.

    This makes the “no strategy” branch unreachable and breaks integrations that try to create a pool and MPM without a strategy.

    Recommendation

    Add an explicit initialization path that deploys and deposits without rebalancing (skip the initial rebalance when strategy == address(0)) and route the mentioned branch to it.

    Resolution

    Gamma Team: Resolved.

  12. L-01 Low Missing CCA Token And Supply Validation Validation Resolved
    Location
    TokenLaunchFactory.sol: 270
    Round
    Main Review

    Description

    The Uniswap CCA documentation states the following:

    1. "Do NOT use the Auction with low-decimal (< 6) tokens. Bidders will lose significant amounts of

    token due to rounding errors in price and amount calculations."

    1. "The maximum total supply that can be sold in the auction is 1e30 wei of token. For a token with

    18 decimals, this is 1 trillion tokens."

    TokenLaunchFactory does not validate that launched tokens have at least 6 decimals before forwarding to Uniswap's ContinuousClearingAuction. The CCA explicitly warns that low-decimal tokens cause bidders to lose significant amounts due to rounding errors in price calculations.

    In addition, there is no validation regarding the total supply for the auction (other than checking against type(uint128).max, which is insufficient in this case). Since the maximum that can be sold is 1e30, sending more can lead to unexpected behaviour or stuck funds.

    Recommendation

    Add decimal and total auction supply validation, for example:

    uint8 decimals = IERC20(token).decimals();
    if (decimals < 6) revert TokenDecimalsTooLow();
    if (auctionSupply > 1e30) revert SupplyTooLarge();
    

    Resolution

    Gamma Team: Resolved.

  13. L-02 Low Fee Bypass Via Direct LiquidityLauncher Call Validation Acknowledged
    Location
    SuperchainLBPStrategyFactory.sol: 65
    Round
    Main Review

    Description

    Upon presale token launches, the TokenLaunchFactory contract collects a creationFee before calling LiquidityLauncher::distributeToken, which proceeds to call SuperchainLBPStrategyFactory::initializeDistribution.

    This deploys a new strategy for the token, and then reserves the pool for the strategy. After this, the launch is officially registered.

    A user can bypass the creation fee by directly calling LiquidityLauncher::distributeToken. This will still process the strategy deployment and pool reservation, allowing users to launch tokens with full protocol functionality while paying zero fees, causing a loss of revenue for the protocol.

    Recommendation

    Within initializeDistribution, check if the creation fee has already been paid for the given unique launch parameters.

    Resolution

    Gamma Team: Acknowledged.

  14. L-03 Low Token Launch Index Can Be Overwritten Logical Error Resolved
    Location
    TokenLaunchFactory.sol: 463
    Round
    Main Review

    Description

    Token launches are indexed in a single _tokenToIndex mapping that uses the token address as a unique key, but the mapping is overwritten on every new launch.

    The registration path always updates _tokenToIndex[token] without checking whether that token was already registered, while _allLaunches retains all entries.

    Because launchPresaleWithExistingToken is permissionless, any holder of the token can create a second launch and overwrite the index. Consequently, integrators that rely on getLaunchByToken will be misled.

    Recommendation

    Consider supporting multiple launches per token by replacing _tokenToIndex with a per‑token array of indices.

    Resolution

    Gamma Team: Resolved.

  15. L-04 Low Migrate Does Not Enforce Auction Graduation Logical Error Resolved
    Location
    LBPStrategyBasic.sol: 139
    Round
    Main Review

    Description

    LBPStrategyBasic.migrate() can be executed once block.number >= migrationBlock and proceeds using auction-derived values (e.g., currencyRaised and clearingPrice). However, migration does not require that the auction has graduated (met requiredCurrencyRaised).

    _validateMigration() only checks that currencyRaised > 0 and that the strategy holds at least currencyRaised of the auction currency, so migration can be forced even when the owner’s minimum-raise requirement is not met.

    In particular, a dust bid that makes currencyRaised > 0, combined with the strategy contract being funded, can allow any third party (since migrate() is permissionless) to trigger listing and liquidity deployment despite the auction failing its graduation condition.

    This is especially impactful when launching with an existing token, since a failed auction would otherwise allow the owner to reuse that token allocation in a future launch; premature migration can consume the reserved supply and commit the token to an unintended listing event.

    Recommendation

    Require auction graduation as a prerequisite to migration. For example, in _validateMigration() enforce auction.isGraduated().

    Resolution

    Gamma Team: Resolved.

  16. L-05 Low Missing Hook Flag Checks In Dynamic Hooks Validation Resolved
    Location
    OrderBookFactory.sol
    Round
    Main Review

    Description

    Dynamic limit-order and fee-only hooks rely on CREATE2 salts to achieve required Uniswap v4 hook flags.

    Unlike the volatility variants, these creation paths do not verify that the deployed hook’s address actually has the required flags, allowing pools to be created with hooks that will not receive necessary callbacks, breaking fee logic/limit-orders.

    Recommendation

    After deploying the dynamic limit-order/dynamic fee hook, validate the deployed address against the required Hook flags (as done for volatility hooks) and revert if they don’t match.

    Resolution

    Gamma Team: Resolved.

  17. I-01 Informational Presale Function Should Reuse _launchPresale Best Practices Resolved
    Location
    TokenLaunchFactory.sol: 203-220
    Round
    Main Review

    Description

    launchPresaleWithExistingToken duplicates the logic already present in _launchPresale rather than reusing the internal helper. Only the token transfer mechanism differs (transferFrom vs transfer). Consolidating shared logic into a single internal function is a best practice.

    Recommendation

    Add a boolean parameter to _launchPresale to differentiate transfer type:

    function _launchPresale(
    LaunchParams calldata params,
    address token,
    bool useTransferFrom
    ) internal returns (address) {
    ...
    if (useTransferFrom) {
    IERC20(token).safeTransferFrom(msg.sender, address(strategy), tokenAmount);
    } else {
    IERC20(token).safeTransfer(address(strategy), tokenAmount);
    }
    ...
    }
    

    Then update both external functions:

    function deployTokenAndLaunchPresale(...) external returns (...) {
    address token = _deployToken(...);
    return _launchPresale(params, token, false);
    }
    function launchPresaleWithExistingToken(...) external returns (...) {
    return _launchPresale(params, token, true);
    }
    

    Resolution

    Gamma Team: Resolved.

  18. I-02 Informational Migration Event Uses Wrong PoolKey Events Resolved
    Location
    LBPStrategyBasic.sol: 150
    Round
    Main Review

    Description

    The SuperchainLBPStrategy inherits the base migration flow, which emits Migrated(key, data.sqrtPriceX96) event using the PoolKey returned by _initializePool.

    In SuperchainLBPStrategy, this function is overridden to intentionally avoid initializing the pool and returns a placeholder PoolKey with hooks = address(0) and fee = poolLPFee.

    However, the actual pool created during migration is deployed and initialized by OrderBookFactory in createVolatilityDynamicLimitOrderPoolWithManager, which builds a different PoolKey using the deployed hook address and the dynamic fee flag.

    As a result, the Migrated event emitted during migrations process reports a pool identity that does not correspond to the real pool used by the deployed MultiPositionManager, what can mislead off-chain integrators that rely on this event.

    Recommendation

    Consider overriding migrate function in SuperchainLBPStrategy to emit migration metadata or emit a specific event from _transferAssetsAndExecutePlan after the factory call, using the PoolKey returned by createVolatilityDynamicLimitOrderPoolWithManager.

    Resolution

    Gamma Team: Resolved.

  19. I-03 Informational Pool Creator Stored As Strategy Contract Informational Resolved
    Location
    OrderBookFactory.sol: 594
    Round
    Main Review

    Description

    OrderBookFactory stores the pool creator as msg.sender during initialization.

    poolCreators[poolId] = msg.sender;
    userToPoolInfo[msg.sender].push(PoolInfo({
    poolId: poolId,
    poolKey: poolKey,
    isDynamic: isDynamic
    }));
    

    In the LaunchPad flow, msg.sender is the deployed strategy contract, not the initial creator. This causes pools created via LaunchPad to be indexed under the strategy address, so getPoolsByCreator will not return the pool and off-chain attribution becomes misleading.

    Recommendation

    Consider recording the pool creator as hookOwner or managerOwner (and optionally also storing msg.sender separately) so LaunchPad pools remain discoverable under the expected creator address.

    Resolution

    Gamma Team: Resolved.

  20. I-04 Informational Ambiguous Registry Update Event Events Resolved
    Location
    OrderBookFactory.sol: 910
    Round
    Main Review

    Description

    OrderBookFactory uses the same DynamicFeeRegistryUpdated event for two different setters, setDynamicFeeLimitOrderRegistry and setDynamicFeeRegistry, which makes the emitted logs indistinguishable.

    Off‑chain indexers and monitoring tools cannot determine whether the dynamic fee registry or the dynamic fee limit‑order registry was updated what may lead to ambiguity.

    Recommendation

    Emit a distinct event for each function so off‑chain consumers can unambiguously identify which registry was updated.

    Resolution

    Gamma Team: Resolved.

  21. I-05 Informational Silent Failure On Staticcall Returns Address 0 Validation Acknowledged
    Location
    TokenLaunchFactory.sol: 362
    Round
    Main Review

    Description

    TokenLaunchFactory uses staticcalls to fetch the auction address and token metadata, but silently continues when the call fails:

    (bool success, bytes memory data) =
    strategy.staticcall(abi.encodeWithSignature("auction()"));
    if (success && data.length >= 32) {
    auction = abi.decode(data, (address));
    }
    

    If the staticcall fails, auction is registered with address(0) for the launch, thus making the factory's indexing and query functions incorrect for the launch's auction.

    Recommendation

    Consider reverting on staticcall failure.

    Resolution

    Gamma Team: Acknowledged.

Remediation Review V1

6 findings
  1. H-01 High Migration Blocked Via Front-Run Attack DoS Resolved
    Location
    MultiPositionFactory.sol: 351-359
    Round
    Remediation Review V1

    Description

    Proof of concept: PoC

    The _ensureFreshManager check in deployDepositAndRebalance can be bypassed to block token migrations.

    During token launch, the pool key is reserved and the hook restricts initialization to the OrderBookFactory as msg.sender, meaning the pool can only be initialized during the migration call itself.

    However, an attacker can front-run the migration by calling deployDepositAndRebalance with a dust deposit and a malicious strategy that specifies zero base ranges and limitWidth=0.

    Because the pool is uninitialized, a normal rebalance would revert when minting liquidites, but with zero base ranges the minting step is skipped entirely, and limitWidth=0 skips limit ranges, allowing the rebalance to succeed without the pool needing to be initialized.

    This marks the MPM as non-fresh (totalSupply != 0, strategy is set), causing the victim's migration transaction to revert at _ensureFreshManager.

    Recommendation

    At the beginning of rebalance, revert if the pool has not yet been initialized:

    (uint160 sqrtPriceX96,,,) = poolManager.getSlot0(poolKey.toId());
    if (sqrtPriceX96 == 0) {
    revert PoolNotInitialized(poolKey);
    }
    

    Resolution

    Gamma Team: Resolved.

  2. M-01 Medium Pool Reservation Bypass Via Duplicate poolId DoS Resolved
    Location
    OrderBookFactory.sol: 1224-1230
    Round
    Remediation Review V1

    Description

    The updated OrderBookFactory::reservePoolForStrategy function keys reservations on keccak256(abi.encode(poolId, callerSalt)) instead of poolId alone.

    This allows two callers to reserve the same poolId by supplying different callerSalt values, bypassing the PoolAlreadyReserved check. An attacker can front-run launchPresaleWithExistingToken(), using the victim's same token, positionRecipient, and hookSalt (which determine the poolId) but with a different salt (callerSalt), producing a distinct reservationKey for the same pool.

    Both reservations succeed, but during migration only the first to execute will initialize the pool. The second migration reverts at poolManager.initialize() since the pool already exists, effectively blocking the victim's migration.

    New token launches via deployTokenAndLaunchPresale() are only affected if the attacker can obtain some of the launch token, which cannot be done until the auction is complete (front-run not possible since token address is derived from msg.sender via graffiti). This makes it such that it is very unlikely the attacker can execute migration() prior to the victim, thus mitigating the attack.

    Recommendation

    Key the reservation on poolId alone, as was done in the previous implementation.

    Resolution

    Gamma Team: Resolved.

  3. M-02 Medium Token Launch Flip Attack Still Possible DoS Resolved
    Location
    OrderBookFactory.sol: 1224-1230,
    Round
    Remediation Review V1

    Description

    The original finding, M-04, reported that an attacker could permanently DoS a victim's token launch via front-run by calling LiquidityLauncher.distributeToken() with flipped currency parameters, producing the same poolId (since sorted(TOKEN, USDC) == sorted(USDC, TOKEN)) and causing the victim's transaction to revert with PoolAlreadyReserved.

    The recommendation was to include the caller-unique salt in the poolId derivation itself.

    The implemented fix instead added callerSalt to the reservation key (keccak256(abi.encode(poolId, callerSalt)), rather than incorporating it into the poolId.

    Although this prevents the immediate PoolAlreadyReserved revert (both callers now get distinct reservation keys), the poolId still remains identical for both parties.

    As a result, both reservations succeed, but during migration only the first to execute will initialize the pool via poolManager.initialize(). The second migration reverts because the pool already exists.

    Recommendation

    Include callerSalt in the hookSalt used for poolId computation so each caller produces a unique pool: SuperchainLBPStrategyFactory_reservePool():

    bytes32 effectiveHookSalt = keccak256(abi.encode(hookSalt, salt));
    bytes32 poolId = orderBookFactory.computePoolIdForVolatilityLimitOrder(
    hookOwner, effectiveHookSalt, currency0, currency1, migratorParams.poolTickSpacing
    );
    

    Resolution

    Gamma Team: Resolved.

  4. M-03 Medium Migration Blocked By Fee Validation Mismatch DoS Resolved
    Location
    LBPStrategyBasic.sol: 201
    Round
    Remediation Review V1

    Description

    The launch-time fee validation in LBPStrategyBasic accepts poolLPFee up to LPFeeLibrary.MAX_LP_FEE (1,000,000), but migration always routes through volatility dynamic limit-order pool creation with a hardcoded surgeMultiplier = 30000 (3x).

    During migration, VolatilityDynamicFeeLimitOrderHook.registerPool enforces: baseFee + (baseFee * surgeMultiplier / 10000) <= MAX_LP_FEE With surgeMultiplier = 30000, this becomes 4 * baseFee <= 1,000,000, so baseFee must be <= 250,000.

    As a result, configurations with poolLPFee > 250,000 pass initial launch validation and auction proceeds normally, but migration deterministically reverts with InvalidFeeConfiguration() at pool registration.

    This creates a permanent migration failure for affected launches because strategy parameters are immutable post-deployment.

    Recommendation

    Add an upfront validation in launch initialization to enforce compatibility with the hook constraints.

    Resolution

    Gamma Team: Resolved.

  5. M-04 Medium Hook Disable Bypassed Via OrderBookFactory Validation Resolved
    Location
    OrderBookFactory.sol: 574
    Round
    Remediation Review V1

    Description

    The _initializePool function in OrderBookFactory unconditionally calls limitOrderManager.enableHook(address(poolKey.hooks)) whenever a pool is created, with no check for whether that hook was already initialized in earlier pool setups or explicitly disabled by the admin.

    LimitOrderManager.disableHook() is intended to set isHook[_hook] = false, which blocks createLimitOrder(), createScaleOrders(), and executeOrder() with DisabledHook.

    However, pool creation is permissionless, and enableHook() allows calls from the orderBookFactoryAddr. This means any external user can create a new pool that references the same hook address and, in doing so, re-enable the hook after it was disabled by an admin.

    Consequently, an admin cannot rely on disableHook as an emergency stop. If a hook is disabled during an incident, it can be reactivated without admin approval, restoring order placement/execution paths that were intentionally shut down.

    Recommendation

    Add a hard disabled state (or equivalent) that enableHook() cannot override for factory-triggered calls.

    Resolution

    Gamma Team: Resolved.

  6. I-01 Informational Unbounded Pool Creation Fee Informational Acknowledged
    Location
    OrderBookFactory.sol: 881-894
    Round
    Remediation Review V1

    Description

    OrderBookFactory allows DEFAULT_ADMIN_ROLE to set the pool creation fees for dynamic and volatility-dynamic pool types to any uint256 value, without an upper bound or activation delay.

    These values are then enforced as a strict msg.value minimum during pool creation, so if the admin sets an excessively high fee, callers will fail the InsufficientETH check and pool creation will revert, effectively pausing those creation flows until the fee is corrected.

    Recommendation

    Add a reasonable maximum cap for these pool creation fees to prevent accidental misconfiguration from halting pool creation.

    Resolution

    Gamma Team: Acknowledged.

Remediation Review V2

1 finding
  1. I-01 Informational Pools Can Be Deployed With Disabled Hooks Warning Acknowledged
    Location
    LimitOrderManager.sol: 917
    Round
    Remediation Review V2

    Description

    The original issue where a user could create a new pool with the same hook and effectively re-enable an admin-disabled hook is now fixed.

    However, pool creation still proceeds even if that hook stays disabled. This means a pool can be deployed while its hook is still disabled, and limit-order execution and swap paths can later revert with DisabledHook until admin re-enables the hook.

    Recommendation

    Add clear UI/doc warnings so users can see when a hook is disabled and avoid launching pools against disabled hooks.

    Resolution

    Gamma Team: Acknowledged.

More from Gamma Strategies

All 7 reports
  1. MultiPositionManager

    83 findings1 high 83 findings: 1 high, 25 medium, 22 low, 35 informational
  2. Limit Order Manager

    19 findings2 high 19 findings: 2 high, 17 low
  3. Position Managers

    58 findings5 high 58 findings: 5 high, 10 medium, 32 low, 11 informational
  4. PerpetualVault Mitigation Review

    24 findings 24 findings: 7 medium, 17 low

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote