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
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-
H-01 High Deterministic Token Address Collision DoS Frontrunning Resolved
Description
Proof of concept: PoC
When a user calls
deployTokenAndLaunchPresaleonTokenLaunchFactorycontract, the launchpad deploys the token (via the selected token factory) and then transferslaunchAmountto theLiquidityLauncher, while sending the remaining supply (totalSupply-launchAmount) to msg.sender.Because
TokenLaunchFactoryis always the caller, and because graffiti (and forUSUPERC20also creator) is constant, an attacker can front-run a victim by callingdeployTokenAndLaunchPresalefirst with the same visible token identity fields.The victim’s transaction then reverts due to a
CREATE2collision 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
CREATE2namespace 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.
-
H-02 High Launch Griefing Via CREATE2 Strategy Predeploy Access Control Resolved
Description
Proof of concept: PoC
In
SuperchainLBPStrategyDeployercontract,deployfunction is externally callable and permissionless, and deploysSuperchainLBPStrategyviaCREATE2using a caller-supplied salt. In the intended flow,LiquidityLauncher.distributeTokenderives a deterministic salt and passes it toSuperchainLBPStrategyFactory.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.deploydirectly. This pre-deploysSuperchainLBPStrategyat the deterministicCREATE2address the factory would later use.When the legitimate launch proceeds, the factory attempts to deploy the same contract at the same
CREATE2address via the deployer, but the address is already occupied, causing aCREATE2collision and reverting. This allows a denial-of-service against every launches.Even if
SuperchainLBPStrategyDeployer.deployis restricted, the same collision/DoS can still be triggered by callingSuperchainLBPStrategyFactory.initializeDistributiondirectly with the same salt andconfigData, sinceinitializeDistributionis also externally callable and not restricted toLiquidityLauncher.Recommendation
Restrict the
SuperchainLBPStrategyDeployer.deployfunction so only the trustedSuperchainLBPStrategyFactorycan call it, and restrictSuperchainLBPStrategyFactory.initializeDistributionso only the canonicalLiquidityLaunchercan call it.Resolution
Gamma Team: Resolved.
-
H-03 High Arbitrary auctionFactory Enables Token Siphon Validation Resolved
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
LBPStrategyBasiccontract, duringonTokensReceivedexecution, 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
auctionFactoryto 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
auctionFactoryin launches. Enforce a trusted allowlist (or a single immutable factory) in theTokenLaunchFactory.Resolution
Gamma Team: Resolved.
-
H-04 High Front-Run Pool Init To Block Limit Order Support DoS Resolved
Description
Proof of concept: PoC
OrderBookFactoryallows 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 UniswapsPoolManagercontract 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 byOrderBookFactory, 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 thebeforeInitializehook.Recommendation
For each hook that can be registered through
OrderBookFactory, include abeforeInitializehook to ensuremsg.senderis equal to theOrderBookFactorycontract, allowing only this contract to register the pools with the intended hooks.Resolution
Gamma Team: Resolved.
-
H-05 High Migration Blocked Via Hook Owner Assignment DoS Resolved
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
createVolatilityDynamicLimitOrderPoolWithManagerfunction derives thepoolIdinsidecomputePoolIdForVolatilityLimitOrder, which depends on the hook address among other parameters. IfcreatorVolatilityDynamicLimitOrderHooks[hookOwner]is already set, the function uses the existing hook address.Because
createVolatilityDynamicLimitOrderPoolWithManageris permissionless, a user can call it using the victim’shookOwnerbefore migration runs. This setscreatorVolatilityDynamicLimitOrderHooks[hookOwner]ahead of time. When the migration later executes,computePoolIdForVolatilityLimitOrderuses 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.registerPoolwithPoolParametersAlreadyUsed. If the attacker used different tokens or parameters, migration proceeds but treats the pool as unreserved and charges the creation fee, reverting withInsufficientETH.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.
-
H-06 High MPM Name Collision Bricks Migration Logical Error Partially resolved
Description
Proof of concept: PoC
In
deployMultiPositionManager, theMultiPositionFactorycontract enforces a global uniqueness on thenamestring for everyMultiPositionManagerdeployment viausedNamesmapping. Any attempt to deploy another manager with the same name reverts.LaunchPadmigration supplies a user-chosenmpmName, which is forwarded intoMultiPositionFactory.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. Whenmigratelater attempts to deploy the MPM using the samempmName, the call reverts withNameAlreadyUsederror, 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 eachmanagerOwnercan only use a given name once.Resolution
Gamma Team: Partially Resolved.
-
H-07 High Permissionless Factory Enables Migration DoS DoS Partially resolved
Description
Proof of concept: PoC
MultiPositionFactoryfunctionsdeployMultiPositionManager,deployDepositAndRebalance,deployDepositAndRebalanceSwapare all permissionless.The
deployDepositAndRebalance. function is called fromOrderBookFactoryduring token launch migrations, which proceeds to deploy a newMultiPositionManagercontract usingpoolKey,managerOwner, andnamefor the salt.The problem is an attacker can front-run the migration and directly call
deployMultiPositionManagerwhich will proceed to deploy the sameMultiPositionManagercontract 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
CREATE2address 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
deployDepositAndRebalanceanddeployDepositAndRebalanceSwapbecause it requires the (reserved) pool to be initialized first.Recommendation
Restrict
deployDepositAndRebalanceanddeployDepositAndRebalanceSwapto only be callable byOrderBookFactory(preventing DoS to pool creation with limit order support), and changedeployMultiPositionManagervisibility to internal.Resolution
Gamma Team: Partially Resolved.
-
M-01 Medium Hook Owner Can Brick Swaps Via Fee Params Validation Partially resolved
Description
VolatilityDynamicFeeLimitOrderHookandVolatilityDynamicFeeHookupdates the pool’s dynamic LP fee insidebeforeSwapby callingpoolManager.updateDynamicLPFee. Uniswap v4 enforcesMAX_LP_FEE= 1000000 (100%), andupdateDynamicLPFeereverts if the passed fee exceeds that maximum.The hook does not validate that its configured parameters (
baseFee,surgeMultiplier,surgeDuration) can never produce atotalFeeabove 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,updateDynamicLPFeereverts and the entire swap reverts.A malicious (or careless) hook owner can brick trading by setting
baseFeegreater thanMAX_LP_FEE(permanent swap reverts), or by choosing parameters wherebaseFee + surgeFeeexceeds the cap during surge mode.Recommendation
On
registerPool,updateBaseFee, andupdateSurgeParams, enforce parameter constraints sototalFeecan never exceedMAX_LP_FEE.Resolution
Gamma Team: Partially Resolved.
-
M-02 Medium Pool Reservation Griefing Via Front-Running DoS Partially resolved
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'sconfigDatawithout 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 extractshookOwner,hookSalt,tickSpacing3. Attacker callsLiquidityLauncher.distributeToken()directly with flipped parameters:token = USDC (attacker holds this) migratorParams.currency = LAUNCH_TOKEN- Both compute identical
poolIdsince currencies are sorted. - Attacker reserves poolId with dust amount (2 wei USDC), and victim's tx reverts with
PoolAlreadyReserved:
Although
LiquidityLauncherderives a unique salt askeccak256(abi.encode(msg.sender, userSalt)), this salt is not used in thepoolIdcomputation. Only the user-providedhookOwnerandhookSaltfromconfigDataare 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)) becauseLiquidityLauncher.distributeToken()requires anERC20token 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 thepoolId. Ensure access control is restricted only toLiquidityLauncher.Resolution
Gamma Team: Partially Resolved.
- Both compute identical
-
M-03 Medium One-Sided Token Toggle Ignored On Migrate Unexpected Behavior Acknowledged
Description
In the base LBP implementation, the token transfer amount is conditional - it only transfers the full
reserveSupplywhen a one-sided position is actually going to be created (encoded asdata.hasOneSidedParams), otherwise it transfers only what is needed for the main position and leaves leftover tokens inside the strategy.In
SuperchainLBPStrategy,_createPositionPlais overridden to return empty, sohasOneSidedParamsis never produced and the strategy replaces the transfer sizing with_calculateTransferAmounts, but the token side is computed.Because
data.initialTokenAmountis always less than or equal toreserveSupplyby construction, this effectively always transfers the wholereserveSupply. This means that whenreserveSupply >data.initialTokenAmountand the user hascreateOneSidedTokenPosition == false(so a one-sided token position is not desired),SuperchainLBPStrategystill transfers all reserved tokens.In the migration path, those amounts are then used as the deposit amounts into the
OrderBookFactorypool 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
_calculateTransferAmountsfunctions so leftover reserved tokens are only included when a one-sided token position is intended.Resolution
Gamma Team: Acknowledged.
-
M-04 Medium No-Strategy Branch In Factory Always Reverts Logical Error Resolved
Description
In
OrderBookFactory,_deployMultiPositionManagerincludes a branch to support deploying an MPM withrebalanceParams.strategy == address(0)by callingdeployDepositAndRebalanceonMultiPositionFactory.However, this function always deploys a fresh MPM and immediately executes
MultiPositionManager.rebalanceright after deployment.Since a newly deployed MPM has no previously stored strategy,
RebalanceLogicdeterministically reverts withNoStrategySpecifiedwhen 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.
-
L-01 Low Missing CCA Token And Supply Validation Validation Resolved
Description
The Uniswap CCA documentation states the following:
- "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."
- "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."
TokenLaunchFactorydoes not validate that launched tokens have at least 6 decimals before forwarding to Uniswap'sContinuousClearingAuction. 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.
-
L-02 Low Fee Bypass Via Direct LiquidityLauncher Call Validation Acknowledged
Description
Upon presale token launches, the
TokenLaunchFactorycontract collects acreationFeebefore callingLiquidityLauncher::distributeToken, which proceeds to callSuperchainLBPStrategyFactory::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.
-
L-03 Low Token Launch Index Can Be Overwritten Logical Error Resolved
Description
Token launches are indexed in a single
_tokenToIndexmapping 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_allLaunchesretains all entries.Because
launchPresaleWithExistingTokenis permissionless, any holder of the token can create a second launch and overwrite the index. Consequently, integrators that rely ongetLaunchByTokenwill be misled.Recommendation
Consider supporting multiple launches per token by replacing
_tokenToIndexwith a per‑token array of indices.Resolution
Gamma Team: Resolved.
-
L-04 Low Migrate Does Not Enforce Auction Graduation Logical Error Resolved
Description
LBPStrategyBasic.migrate()can be executed onceblock.number >= migrationBlockand proceeds using auction-derived values (e.g.,currencyRaisedandclearingPrice). However, migration does not require that the auction has graduated (metrequiredCurrencyRaised)._validateMigration()only checks thatcurrencyRaised > 0and that the strategy holds at leastcurrencyRaisedof 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 (sincemigrate()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()enforceauction.isGraduated().Resolution
Gamma Team: Resolved.
-
L-05 Low Missing Hook Flag Checks In Dynamic Hooks Validation Resolved
Description
Dynamic limit-order and fee-only hooks rely on
CREATE2salts 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.
-
I-01 Informational Presale Function Should Reuse _launchPresale Best Practices Resolved
Description
launchPresaleWithExistingTokenduplicates the logic already present in_launchPresalerather than reusing the internal helper. Only the token transfer mechanism differs (transferFromvs 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.
-
I-02 Informational Migration Event Uses Wrong PoolKey Events Resolved
Description
The
SuperchainLBPStrategyinherits the base migration flow, which emitsMigrated(key,data.sqrtPriceX96)event using thePoolKeyreturned by_initializePool.In
SuperchainLBPStrategy, this function is overridden to intentionally avoid initializing the pool and returns a placeholderPoolKeywithhooks = address(0)andfee = poolLPFee.However, the actual pool created during migration is deployed and initialized by
OrderBookFactoryincreateVolatilityDynamicLimitOrderPoolWithManager, which builds a differentPoolKeyusing the deployed hook address and the dynamic fee flag.As a result, the
Migratedevent emitted during migrations process reports a pool identity that does not correspond to the real pool used by the deployedMultiPositionManager, what can mislead off-chain integrators that rely on this event.Recommendation
Consider overriding
migratefunction inSuperchainLBPStrategyto emit migration metadata or emit a specific event from_transferAssetsAndExecutePlanafter the factory call, using thePoolKeyreturned bycreateVolatilityDynamicLimitOrderPoolWithManager.Resolution
Gamma Team: Resolved.
-
I-03 Informational Pool Creator Stored As Strategy Contract Informational Resolved
Description
OrderBookFactorystores the pool creator asmsg.senderduring initialization.poolCreators[poolId] = msg.sender; userToPoolInfo[msg.sender].push(PoolInfo({ poolId: poolId, poolKey: poolKey, isDynamic: isDynamic }));In the
LaunchPadflow,msg.senderis the deployed strategy contract, not the initial creator. This causes pools created viaLaunchPadto be indexed under the strategy address, sogetPoolsByCreatorwill not return the pool and off-chain attribution becomes misleading.Recommendation
Consider recording the pool creator as
hookOwnerormanagerOwner(and optionally also storingmsg.senderseparately) soLaunchPadpools remain discoverable under the expected creator address.Resolution
Gamma Team: Resolved.
-
I-04 Informational Ambiguous Registry Update Event Events Resolved
Description
OrderBookFactoryuses the sameDynamicFeeRegistryUpdatedevent for two different setters,setDynamicFeeLimitOrderRegistryandsetDynamicFeeRegistry, 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.
-
I-05 Informational Silent Failure On Staticcall Returns Address 0 Validation Acknowledged
Description
TokenLaunchFactoryuses 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-
H-01 High Migration Blocked Via Front-Run Attack DoS Resolved
Description
Proof of concept: PoC
The
_ensureFreshManagercheck indeployDepositAndRebalancecan be bypassed to block token migrations.During token launch, the
pool keyis reserved and the hook restricts initialization to theOrderBookFactoryasmsg.sender, meaning the pool can only be initialized during the migration call itself.However, an attacker can front-run the migration by calling
deployDepositAndRebalancewith a dust deposit and amalicious strategythat specifies zero base ranges andlimitWidth=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, andlimitWidth=0skips 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.
-
M-01 Medium Pool Reservation Bypass Via Duplicate poolId DoS Resolved
Description
The updated
OrderBookFactory::reservePoolForStrategyfunction keys reservations onkeccak256(abi.encode(poolId, callerSalt))instead ofpoolIdalone.This allows two callers to reserve the same
poolIdby supplying differentcallerSaltvalues, bypassing thePoolAlreadyReservedcheck. An attacker can front-runlaunchPresaleWithExistingToken(), using the victim's sametoken,positionRecipient, andhookSalt(which determine thepoolId) but with a differentsalt (callerSalt), producing a distinctreservationKeyfor 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 frommsg.sendervia graffiti). This makes it such that it is very unlikely the attacker can executemigration()prior to the victim, thus mitigating the attack.Recommendation
Key the reservation on
poolIdalone, as was done in the previous implementation.Resolution
Gamma Team: Resolved.
-
M-02 Medium Token Launch Flip Attack Still Possible DoS Resolved
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 samepoolId(sincesorted(TOKEN, USDC) == sorted(USDC, TOKEN)) and causing the victim's transaction to revert withPoolAlreadyReserved.The recommendation was to include the caller-unique salt in the poolId derivation itself.
The implemented fix instead added
callerSaltto the reservation key (keccak256(abi.encode(poolId,callerSalt)), rather than incorporating it into thepoolId.Although this prevents the immediate
PoolAlreadyReservedrevert (both callers now get distinct reservation keys), thepoolIdstill remains identical for both parties.As a result, both reservations succeed, but during
migrationonly the first to execute will initialize the pool viapoolManager.initialize(). The second migration reverts because the pool already exists.Recommendation
Include
callerSaltin thehookSaltused 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.
-
M-03 Medium Migration Blocked By Fee Validation Mismatch DoS Resolved
Description
The launch-time fee validation in
LBPStrategyBasicacceptspoolLPFeeup toLPFeeLibrary.MAX_LP_FEE(1,000,000), but migration always routes through volatility dynamic limit-order pool creation with a hardcodedsurgeMultiplier = 30000(3x).During migration,
VolatilityDynamicFeeLimitOrderHook.registerPoolenforces:baseFee + (baseFee * surgeMultiplier / 10000) <= MAX_LP_FEEWith surgeMultiplier = 30000, this becomes 4 * baseFee <= 1,000,000, so baseFee must be <=250,000.As a result, configurations with
poolLPFee > 250,000pass initial launch validation and auction proceeds normally, but migration deterministically reverts withInvalidFeeConfiguration()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.
-
M-04 Medium Hook Disable Bypassed Via OrderBookFactory Validation Resolved
Description
The
_initializePoolfunction inOrderBookFactoryunconditionally callslimitOrderManager.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 setisHook[_hook] = false, which blockscreateLimitOrder(),createScaleOrders(), andexecuteOrder()withDisabledHook.However, pool creation is permissionless, and
enableHook()allows calls from theorderBookFactoryAddr. 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
disableHookas 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.
-
I-01 Informational Unbounded Pool Creation Fee Informational Acknowledged
Description
OrderBookFactoryallowsDEFAULT_ADMIN_ROLEto set the pool creation fees for dynamic and volatility-dynamic pool types to anyuint256value, without an upper bound or activation delay.These values are then enforced as a strict
msg.valueminimum during pool creation, so if the admin sets an excessively high fee, callers will fail theInsufficientETHcheck 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-
I-01 Informational Pools Can Be Deployed With Disabled Hooks Warning Acknowledged
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
DisabledHookuntil 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.
No findings match.
More from Gamma Strategies
All 7 reports-
MultiPositionManager
83 findings1 high 83 findings: 1 high, 25 medium, 22 low, 35 informational -
Limit Order Manager
19 findings2 high 19 findings: 2 high, 17 low -
Position Managers
58 findings5 high 58 findings: 5 high, 10 medium, 32 low, 11 informational -
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.
