Gamma engaged Guardian to review the security of their Gamma - MultiPositionManager. 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
- Yield and vaults
- 0 Critical
- 1 High
- 25 Medium
- 22 Low
- 35 Informational
Scope
Overview
Gamma engaged Guardian to review the security of their Gamma - MultiPositionManager. From the 2nd of January 2026 to the 4th of February 2026, a team of 4 auditors reviewed the source code in scope.
Findings 83
Main Review
69 findings-
H-01 High Missing Deployer Access Control Causes DoS DoS Resolved
Description
Proof of concept: PoC
The
MultiPositionDeployer.deploy()function is externally callable without access control. This function deploysMultiPositionManagercontracts usingCREATE2with a caller-supplied salt.In the intended flow,
MultiPositionFactory.deployMultiPositionManager()computes a deterministic salt and callsMultiPositionDeployer.deploy(). This flow is used by:- Token Launch Migration.
- Pool Creation with Limit Order Support.
An attacker can extract parameters from the mempool or from deployed
SuperchainLBPStrategycontracts (all fields are public). Using these parameters, the attacker computes the exact same salt and constructor arguments, then front-runs by callingMultiPositionDeployer.deploy()directly.When the legitimate deployment proceeds, the factory attempts to deploy at the same address via the deployer, causing a
CREATE2collision and reverting.This will block migration and/or creating pools with limit order support.
Recommendation
Restrict
MultiPositionDeployer::deploy()to only be callable byMultiPositionFactory.Resolution
Gamma Team: Resolved.
-
M-01 Medium Unbounded Loop In deployMultiPositionManager DoS Resolved
Description
The
deployMultiPositionManagerfunction loops through the_allTokenPairsarray to locate and update a token pair’s manager count.As the number of unique token pairs grows, this linear scan becomes increasingly expensive and can lead to gas exhaustion or DoS, potentially blocking new deployments.
Recommendation
Add a hard cap or redesign the accounting so deployment cost doesn’t grow with the number of pairs.
Resolution
Gamma Team: Resolved.
-
M-02 Medium RebalanceSwap Skips Protocol Fee Split Logical Error Resolved
Description
MultiPositionManager.rebalanceSwapunwinds all existing positions viapoolManager.unlock(Action.BURN_ALL, ...).The
BURN_ALLcallback burns liquidity across positions and clears position state, but it does not execute the fee-splitting logic used elsewhere (thezeroBurnAllflow), where the treasury cut is computed astotalFee / s.feeand minted as ERC-6909 claims.Because this unwind path skips that step, any accrued fees realized during the full burn are treated as ordinary vault assets and can be swapped/redeployed immediately, while the protocol fee recipient receives no claimable share.
Any caller permitted to call
rebalanceSwapcan bypass protocol fees by callingrebalanceSwapfirst and then proceeding with other actions.Recommendation
Apply protocol fee accounting on all unwind paths. Run
zeroBurnAll(or equivalent treasury-cut logic) before burning liquidity, or implement the treasury cut directly in the burn-all flow so fees are split consistently.Resolution
Gamma Team: Resolved.
-
M-03 Medium ETH Accounting Breaks In Multicall Logical Error Resolved
Description
MultiPositionManagerinheritsMulticalland executes batches viadelegatecall, meaning every subcall observes the samemsg.value. For native-asset pools,deposit()ultimately relies on_transferIn(), which validates ETH payment usingrequire(msg.value >= amount).Because the contract does not track or decrement ETH across subcalls, multiple deposits in a single
multicallcan each independently satisfy the ETH check using the samemsg.value.Batching multiple deposits could be used when depositing for multiple recipients or when deposits are split across different
fromaddresses/flows.As a result, shares can be minted multiple times even though ETH was only provided once, inflating supply and diluting existing shareholders.
Depending on the deposit amounts and refund behavior, the contract may also attempt to incorrectly run refund logic multiple times within the same transaction, creating inconsistent ETH flows and potentially leaving the contract’s ETH balance misaligned with assumptions in the share/accounting logic.
Recommendation
Disallow batching with ETH (revert if
msg.value != 0anddata.length > 1), or implement an explicit ETH “remaining value” budget for multicall and consume it inside_transferIn()instead of readingmsg.valueper subcall.Resolution
Gamma Team: Resolved.
-
M-04 Medium withdrawCustom Reverts On Fee Collection DoS Resolved
Description
WithdrawLogic.processWithdrawCustomcan select a “balance + fees” path and callszeroBurnAllWithoutUnlock(s, poolManager)directly. That function callsPoolManagerUtils.zeroBurnAll(), which invokespoolManager.modifyLiquidity()to realize fees.In Uniswap v4,
modifyLiquidityis expected to be executed while thePoolManageris unlocked (duringpoolManager.unlock()via the callback). Calling it directly fromwithdrawCustomoutside an unlock is incompatible with standard v4 behaviour and can cause this branch to revert, makingwithdrawCustomfail whenever it selects the “balance + fees” path.This can also impact automation.
RelayerLogic.withdrawSingleToken(used byRelayer.executeWithdrawal) callsmanager.withdrawCustomand may then callmanager.rebalanceto redeploy the remaining asset.If
withdrawCustomselects theUSE_BALANCE_PLUS_FEESpath, the relayer execution will revert due to fee collection occurring outside unlock, preventing the withdrawal and any subsequent rebalance in that transaction.Recommendation
When
withdrawCustomneeds fee realization, execute it viapoolManager.unlock(Action.ZERO_BURN, ...) (or via the existingAction.WITHDRAWcallback flow) rather than callingzeroBurnAllWithoutUnlockdirectly.Resolution
Gamma Team: Resolved.
-
M-05 Medium Zero Fee Can Brick Core Functions Math Resolved
Description
setFeeinMultiPositionManagerallowss.feeto be set to zero, with no minimum enforced. Several core execution paths assumes.feeis non-zero and divide by it to compute the protocol’s share of accrued fees.For example,
WithdrawLogic.getTotalAmounts()adjusts TVL and fee totals using expressions liketotalFee / s.fee, and fee-claim logic similarly derives mint/burn amounts fromtotalFee / s.fee. Ifs.feeis set to zero, these divisions revert.As a result, setting
s.feeto zero can cause withdrawals, fee claims, and other fee-dependent flows to revert, effectively bricking core functionality.Recommendation
Enforce a non-zero minimum fee in
setFee, or explicitly handle the zero-fee case in all accounting and fee-distribution logic to avoid division-by-zero reverts.Resolution
Gamma Team: Resolved.
-
M-06 Medium Aggregator Address Not Validated Validation Resolved
Description
Proof of concept: PoC
_executeAggregatorSwapis intended to prevent arbitrary external calls by restricting swaps to a approved set of aggregator integrations. It does this by validating params.aggregator (≤ 3), which corresponds to the supported options (ZERO_X,KYBERSWAP,ODOS,PARASWAP).However, the actual call target,
params.aggregatorAddress, is user-controlled and is not derived from or validated against the selected enum value. A caller can therefore pass a validparams.aggregatorvalue while settingparams.aggregatorAddressto an arbitrary contract.Because
_executeAggregatorSwapalso grantsparams.aggregatorAddressan allowance for the input token (forceApprove) and then performs a low-level external call with user-controlled calldata, the enum validation does not prevent arbitrary external calls or malicious approval usage.Any caller that can reach swap-based paths (including relayer/automation flows) can route execution to untrusted contracts and potentially drain or misdirect funds.
Even with a semi-trusted relayer, this is misleading: owners may grant relayer permissions believing swaps are constrained to known aggregators, but in practice the relayer can execute arbitrary external calls with token approvals because
params.aggregatorAddressis not properly constrained.Recommendation
Bind each aggregator enum value to a fixed, trusted aggregator address (or a whitelist per enum), and derive
aggregatorAddressinternally based on params.aggregator rather than accepting it as user input. Alternatively, explicitly validate thatparams.aggregatorAddressmatches an address from an allowlisted set for the selected aggregator.Resolution
Gamma Team: Resolved.
-
M-07 Medium ERC20 Shares Not Redeemable By Holders Logical Error Partially resolved
Description
MultiPositionManagerissues transferableERC20shares, but redemption is tied only to owner instead of the shareholder.Deposits can mint shares to an arbitrary to address. However, both withdrawal paths (
withdrawandwithdrawCustomfunctions) pay assets to and burn shares from owner.Consequently, if shares are held by any address other than owner, those holders cannot redeem, and withdrawals may revert if owner address lacks the shares to burn.
Recommendation
Make shares non-transferable and always mint them to owner.
Resolution
Gamma Team: Partially Resolved.
-
M-08 Medium Rebalance Overflow When Price Near Lower Tick Logical Error Resolved
Description
In
calculateCurrentRangeExcess, when the current price is close to the lower tick of a range, the denominatorsqrtPriceX96 - sqrtPriceLowerbecomes small, producing an extremely large liquidity result that may exceeduint128.maxand silently truncate.Consider the following example (low priced pool):
sqrtPriceLower ≈ 1e18sqrtPriceX96 ≈ 1.00005e18denominator = sqrtPriceX96 - sqrtPriceLower ≈ 5e13token1Allocation: 5e23 (18 decimals)liquidityFrom1 = 5e23 * 7.9e28 / 5e13 = 7.9e38However,
uint128.max ≈ 3.4e38, therefore the result will silently truncate.This will result in incorrect excess calculations and liquidity allocation across MPM positions. This may also cause DoS or unexpected behaviour to token launch migrations, depending on the launch and migration configuration.
Also consider applying the changes to
mintFromAllocations.Recommendation
Consider reverting upon overflow for both
liquidityFrom1andactualLiquiditycalculations withincalculateCurrentRangeExcess.Resolution
Gamma Team: Resolved.
-
M-09 Medium Single-Token Withdraw DoS With Limit Positions DoS Resolved
Description
WithdrawSingleTokenimplemented inRelayerLogic, is used by the relayer withdrawal automation when configured forwithdrawToken0OnlyorwithdrawToken1Only.The function first executes
withdrawCustomto withdraw all amount of one token from the pool, then conditionally calls rebalance onMultiPositionManagerto redeploy the remaining token into fresh positions.The rebalance call is constructed with an
outMinarray sized only to the number of base positions. However, inRebalanceLogic, therebalancefunction strictly requires theoutMinlength to match the number of currently active positions being burned (basePositionsLength+limitPositionsLength).If the vault has any active limit positions, the call always reverts because
withdrawSingleTokenpasses anoutMinarray that ignores limit positions. Consequently, single-token withdrawals will be permanently unusable for managers with active limit positions.Recommendation
Size the rebalance
outMinto cover all current positions -basePositionsLength +limitPositionsLength.Resolution
Gamma Team: Resolved.
-
M-10 Medium Relayer Owner Desync From MPM Owner Configuration Resolved
Description
Relayer admin control is permanently bound to the deploy-time manager owner via an immutable
ownerparameter, while theMultiPositionManagerowner can change. All relayer admin functions useonlyOwneragainst this captured address.However, execution already treats the MPM owner as dynamic:
address mpmOwner = Ownable(address(manager)).owner(); uint256 ownerShares = manager.balanceOf(mpmOwner); manager.withdraw(ownerShares, outMin, true);After MPM ownership is transferred, the new MPM owner cannot pause/configure/withdraw relayer ETH, while the old owner still can. Since the factory enforces a single relayer per manager, deploying a replacement is blocked, making this loss of control permanent.
Recommendation
Remove the captured immutable relayer owner and derive it from the current
MultiPositionManagerowner.Resolution
Gamma Team: Resolved.
-
M-11 Medium Missing Token0 Fallback When liquidityFrom1 Is 0 Logical Error Resolved
Description
In proportional mode (
weight0 == 0 && weight1 == 0),mintFromAllocationscomputes liquidity starting fromtoken1.When
liquidityFrom1 = 0(price at lower tick boundary, zerotoken1allocation after swaps, or one-sided deposits), the code never calculates liquidity fromtoken0as a fallback. This causesliquidities[i] = 0even when sufficienttoken0exists.The same issue exists in
calculateCurrentRangeExcess, whereactualLiquiditybecomes 0 and all tokens are marked as excess and redistributed away from the current range.With carpet mode enabled,
_validateCarpetLiquidityrequires non-zero liquidity for the outer carpet ranges and reverts if edge range liquidity is 0 (even with sufficient one-sided funds). This creates DoS when price lands exactly on a range boundary during initialization or rebalances.Recommendation
Add fallback to calculate liquidity from
token0whentoken1cannot contributecalculateCurrentRangeExcess.Alternatively, replace the custom boundary handling with
LiquidityAmounts.getLiquidityForAmountswhich handles all boundary cases correctly.Resolution
Gamma Team: Resolved.
-
M-12 Medium Relayer Off-By-One Ratio Extraction Logical Error Resolved
Description
In Relayer contract,
getRatiosreturnsinPositionRatioandoutOfPositionRatioas respectively 5th and 6th values. However, Relayer unpacks the returned values as ifoutOfPositionRatiowere the 5th return value.(,,,, uint256 outOfPositionRatio,,,,,,,) = manager.getRatios();This binds the 5th return value (
inPositionRatio) to a variable namedoutOfPositionRatio. That means the relayer checks the wrong condition and it effectively allows compounding when assets are in position (inPositionRatio >= threshold) instead of when assets are idle (outOfPositionRatio >=threshold).In practice, this makes
executeCompoundSwapbehave like it is almost always enabled even when the vault is not meaningfully out of position, wasting ETH on reimbursements and potentially causing unnecessary swapsRecommendation
Update Relayer’s
getRatiosunpacking to use the correctoutOfPositionRatioposition.Resolution
Gamma Team: Resolved.
-
M-13 Medium Unaligned TWAP Center Tick Logical Error Resolved
Description
When TWAP-based centering is used (
baseTwapTickTriggeroruseTwapCenter), the relayer sets params.center to the raw TWAP tick returned by the oracle. That TWAP tick is not guaranteed to be aligned to the pool’stickSpacing.Range generation does not use that raw value directly. For example,
SingleUniformStrategysnaps the receivedcenterTickonto thetickSpacinggrid usingcenterTick = (centerTick / tickSpacing) *tickSpacing.This uses truncating division, so the “center” actually used to build ranges can differ from the value stored in
lastStrategyParams.centerTickwhenever the TWAP tick is not already aligned.This mismatch is more severe for negative ticks because truncation rounds toward zero (e.g.,
tickSpacing = 60, TWAP = -1snaps to 0 instead of -60).Any subsequent logic that references the stored center (for example, trigger calculations or comparisons relative to
centerTick) can become inconsistent because the system records one center while liquidity was deployed around a different one.Recommendation
Align the TWAP tick to
tickSpacingbefore using it asparams.center, using the same rounding convention as the manager’s tick alignment (round down for negatives), and store that aligned value aslastStrategyParams.centerTick. Furthermore, update strategies such asSingleUniformStrategyto align ticks using floor-style rounding for negative ticks rather than truncating division.Resolution
Gamma Team: Resolved.
-
M-14 Medium Lens Ignores Fee Claim Before Rebalance Logical Error Resolved
Description
When
compoundFeesis false, the relayer always callsclaimFeeon MPM before rebalancing, and triggersWithdrawLogic.processClaimFee, which zero-burns fees, then pays the owner’s share out of the MPM and forwards the treasury share to the factoryfeeRecipient.Consequently, those transfers remove fee amounts from the manager’s token balances immediately before the rebalance.
However, in
RelayerLens,previewRebalanceignores this step and bases expected positions,swapAmountandinMinongetTotalAmountsthat still include the unpaid fees.With non‑zero fees, the preview overstates available tokens and automation can submit a rebalance that reverts once fees are paid, despite the lens reporting it should succeed, causing failed automation and wasted gas.
Recommendation
Update
previewRebalanceto simulateclaimFeewhencompoundFeesis false by using balances after the claim.Resolution
Gamma Team: Resolved.
-
M-15 Medium Slippage Bypass When Limit Position 0 Empty Logical Error Resolved
Description
Proof of concept: PoC
During rebalancing, the limit positions are updated only if the positions are non-empty.
This allows a state where
limitPositions[0]is empty whilelimitPositions[1]is non-empty, resulting inlimitPositionsLength = 1.However, when burning limit positions, the
outMinindex is derived from the slot index i.Applying slippage will not be possible if the
outMinlist is limited only to the length ofs.basePositionsLength +s.limitPositionsLengthin this case, forcing the owner to remove the limit position liquidity with 0 slippage.Consider the following example:
basePositionsLength = 3limitPositions[0]is empty (0, 0)limitPositions[1]is non-empty (100, 200)limitPositionsLength = 1(only position 1 counted)
Validation requires
outMin.length = basePositionsLength + limitPositionsLength = 3 + 1 = 4User providesoutMinwith indices [0, 1, 2, 3].During
burnLimitPositions:i = 0: limitRanges[0]is empty, so this iteration is skipped.i = 1: limitRanges[1]is non-empty, sooutMinIndex= baseRangesLength + i = 3 + 1 = 4. Check: 4 < 4 → False. Falls back to [0, 0] → zero slippage.This will remove liquidity from the position with 0 slippage, with MPM owners having no way around this, leading to loss of funds (i.e sandwich attack, price movement, etc).
There is another issue in a separate function, called during compounds,
DepositLogic::addLiquidityToPositions. Thefor (uint8 i = 0; i < limitLength;) {loop will skip adding liquidity tolimitPosition[1]iflimitPosition[0]is empty, causing yield loss (same root cause).Recommendation
Consider utilizing a limit counter to properly fetch the correct index.
Resolution
Gamma Team: Resolved.
-
M-16 Medium Rebalance Slippage Ignores Principal Vs Fees Error Resolved
Description
When burning liquidity positions during rebalancing,
outMinslippage checks are performed againstcallerDeltawhich includes both principal and accrued fees.Uniswap V4's
PositionManagerexplicitly subtracts fees before slippage validation becauseminOutparameters are intended to protect the principal value.There are two cases during rebalances that are affected:
1.
rebalanceSwap(): UsesBURN_ALLwithout collecting fees prior.rebalance()with only limit positions. TheZERO_BURNcheck only considers base positions.
Because accrued fees are included in the slippage comparison, they can mask losses on the principal portion.
Due to price changes or manipulation before the owner's rebalance transaction is executed, the principal value can be reduced, while the slippage check still passes due to accumulated fees acting as a buffer.
Recommendation
For
rebalanceSwap(): Either follow Uniswap V4PositionManager's pattern by subtractingfeesAccruedfromcallerDeltabefore checking againstoutMin, or collect fees prior to burning positions (similar to the withdrawal flow).For
rebalance(): Update theZERO_BURNcondition to also check for limit positions (s.basePositionsLength > 0||s.limitPositionsLength > 0).Resolution
Gamma Team: Resolved.
-
M-17 Medium Gas Refund Ignores L1 Data Fee Unexpected Behavior Partially resolved
Description
_reimburseGasrefunds only the L2 execution cost using the following calculations:// Calculate gas used uint256 gasUsed = gasBefore - gasleft() + BASE_GAS_OVERHEAD; // Calculate reimbursement with 10% buffer uint256 reimbursement = (gasUsed * tx.gasprice * GAS_BUFFER_NUMERATOR) / GAS_BUFFER_DENOMINATOR;However, Unichain (where the protocol is intended to be deployed) is an Ethereum L2 built on the OP Stack.
On OP Stack chains, sequenced L2 transactions are charged an additional L1 data fee that covers publishing the transaction batch data to Ethereum, which is not captured by
gasUsed * tx.gasprice.This means automation services will be systematically under-reimbursed, and the gap can be especially large for calls with large calldata
Recommendation
Reimburse the full transaction cost by adding the L1 data fee.
Resolution
Gamma Team: Partially Resolved.
-
M-18 Medium limitPositionsLength Can Skip Active Limit Slot Logical Error Resolved
Description
The
MultiPositionManagerstores limit positions in two fixed slots,s.limitPositions[0]for the lower range ands.limitPositions[1]for the upper range. The variables.limitPositionsLengthis intended to represent how many of these slots are active.Several code paths iterate limit positions using
for (i < s.limitPositionsLength)and then accesss.limitPositions[i]. This logic assumes that when exactly one limit position is active, it must be stored at index0. That assumption is not guaranteed.If the lower limit position collapses into an empty range due to tick rounding or clamping near the minimum usable tick while the upper limit remains valid,
s.limitPositionsLengthbecomes1even though the only active position resides at index1. In this case, loops bounded bys.limitPositionsLengthwill only reads.limitPositions[0]and will skip the active upper limit.As a result, deployed liquidity and accrued fees can be undercounted in aggregation logic such as total amount calculations. Liquidity add or rebalance flows may also ignore an active limit range, leading to incorrect allocations, understated vault value, or unexpected idle balances.
Recommendation
Do not use
s.limitPositionsLengthas an index bound for accessings.limitPositions. Always iterate over both fixed slots and skip inactive entries by checking thatlowerTick != upperTick.Resolution
Gamma Team: Resolved.
-
M-19 Medium Carpet Blocks Relayer Single-Token Withdraw Logical Error Resolved
Description
When a withdrawal trigger is configured with
withdrawToken0OnlyorwithdrawToken1Only, the relayer callsRelayerLogic.withdrawSingleToken, which first withdraws all of the specified token usingwithdrawCustom, and then attempts to rebalance the remaining assets.Then, the relayer fetches the last strategy parameters and calls
rebalanceon MPM. usinglastUseCarpetparameter.However, the rebalance implementation enforces that carpet mode requires both tokens to be non-zero, reverting when either available token amount is zero.
if (ctx.useCarpet && (available0 == 0 || available1 == 0)) { revert CarpetRequiresBothTokens(); }Since the single-token withdrawal intentionally makes one token amount zero, the subsequent rebalance reverts whenever
lastUseCarpetis true, causing the entire automated withdrawal transaction to revert.Recommendation
Either change
withdrawSingleTokento not attempt a carpet rebalance after making the vault one-sided or explicitly disallow configuringwithdrawToken0Only/withdrawToken1OnlywhenuseCarpetis enabled.Resolution
Gamma Team: The issue was resolved in commit af04adb.
-
M-20 Medium Proportional Mode Handled Inconsistently Logical Error Resolved
Description
The protocol treats proportional mode (
weight0 == 0 && weight1 == 0) differently depending on the rebalance entrypoint.In the
rebalance()flow,_processRebalance()explicitly disables limit logic by forcinglimitWidth = 0, stating that limit positions do not make sense when weights are derived from amounts. This guarantees that proportional rebalances only construct base ranges.However, the
rebalanceSwap()flow does not apply the same restriction. When building the strategy context,_buildStrategyContext()forwardsparams.limitWidthunchanged even ifweight0 == 0 &&weight1 == 0. As a result,rebalanceSwap()can execute in proportional mode with a nonzero limit width, allowing strategies to construct limit positions.This leads to inconsistent behaviour where two operations that appear equivalent can result in different final positions depending on the entrypoint used. In particular, operators assumptions that proportional mode always results in base-only positions may break if a rebalance is executed through
rebalanceSwap()instead.Recommendation
Normalize proportional mode handling by setting
limitWidth = 0wheneverweight0 == 0&weight1 ==0in therebalanceSwap()path as well, or clearly document that proportional mode may include limit position construction.Resolution
Gamma Team: Resolved.
-
M-21 Medium rebalanceSwap Bypasses Carpet Checks Unexpected Behavior Resolved
Description
The standard rebalance path enforces carpet invariants when
useCarpetis set to true. It requires both tokens to be non-zero and it the edge carpet positions to have non-zero liquidity.The
rebalanceSwappath does not execute this validation logic. It burns positions, optionally swaps and persistslastStrategyParams.useCarpet=true, without validating carpet liquidity and total available amounts.As a result,
rebalanceSwapcan persistuseCarpetset to true, even when the post-swap state is one-sided or assigns zero liquidity to the carpet edges, violating the intended carpet mechanism.Recommendation
Apply the same carpet checks in the
rebalanceSwappath.Resolution
Gamma Team: The issue was resolved in commit af04adb.
-
M-22 Medium Unreachable Single-Token Withdrawal Rebalance Logical Error Resolved
Description
The withdrawal path is initiated in
executeWithdrawalby checking in-position ratios returned bygetRatiosand then callingwithdrawSingleTokenthat is supposed to withdrawtoken0ortoken1and rebalance with the other token.This function withdraws the entire selected token amount using
getTotalAmounts, which includes position amounts, fees and idle balances.When the relayer trigger for “withdraw only
token0” is met,pool0Ratiomust be non-zero (it is computed from in-position amounts). In that state,total0necessarily includes sometoken0that is currently inside positions.Therefore,
withdrawCustomcannot be satisfied from idle balances alone and must enter the burn positions path, which computes how many shares worth of positions must be burned to source the requested token amount.Because
amount0Desiredis equal tototal0in this flow,sharesForToken0becomestotalSupply, so it burns 100% of positions. After positions are fully burned, the in-position amounts are zero, sogetRatiosreportspool0Ratio == 0andpool1Ratio == 0regardless of any remaining idle token balances (as idle balances are not included inpool0Ratio/pool1Ratio).This makes the subsequent rebalance block unreachable in the intended, triggered case, even if the burn left a non-zero amount of the other token sitting idle in the manager, what consequently break the intended “withdraw and rebalance” behaviour.
Also, the only scenario where the rebalance block could run is when the selected token is entirely idle (so no burn happens and positions still contain the other token), but in that scenario the relayer cannot trigger “withdraw
token0only” or “withdrawtoken1only” because the corresponding in-position ratio is zero and cannot meet a non-zerothreshold.Recommendation
Change the post withdraw rebalance check to use a value that includes idle balances rather than
pool0Ratio/pool1Ratiowhich reflect positions only.Resolution
Gamma Team: Resolved.
-
L-01 Low Relayer Deployment Griefing DoS Resolved
Description
Relayer deployment relies on
CREATE2to deterministically compute relayer addresses from the MPM address, owner address, and configuration parameters.While
RelayerFactoryenforces authorization checks, the underlyingRelayerDeployer.deploy()function itself is permissionless and can be called directly.As a result, any user can front-run a legitimate deployment by calling
RelayerDeployer.deploy()with the same parameters the owner intends to use. This preemptive deployment occupies the expected relayer address.When the rightful MPM owner later attempts to deploy through the factory, the call reverts because the contract is already deployed.
This enables a griefing attack where attackers can block MPM owners from deploying relayers with their intended configuration.
Recommendation
Restrict RelayerDeployer.deploy()so it can only be called by the authorized factory, ensuring relayer creation cannot be front-run or griefed by external callers.Resolution
Gamma Team: Resolved.
-
L-02 Low Deposit Function Lacks Slippage Protection Error Acknowledged
Description
The
deposit()function calculates shares using theslot0price without any slippage protection parameter (minSharesOut).While the owner is typically the sole shareholder and cannot lose value to themselves, in edge cases where the owner mints shares to third-party addresses via the
toparameter, or in future integrations, this could lead to share recipients receiving fewer shares than expected due to price movements or manipulation (i.e., sandwich attack).Recommendation
Add
minSharesOutparameter todeposit()for slippage protection, and consider validatingsqrtPriceX96against a slippagethreshold.Resolution
Gamma Team: Acknowledged.
-
L-03 Low TWAP Validation Mismatch After Deployment Configuration Resolved
Description
Relayer deployment and post deploy updates enforce different TWAP constraints.
RelayerFactoryrejects overly large TWAP windows and verifies the oracle can serve the requestedtwapSeconds.After deployment, the relayer owner can call
setRebalanceParams, which uses_validateTwapParamsand does not enforce the sametwapSecondsupper bound or re-check oracle availability.If
twapSecondsis set beyond the oracle’s retained history,oracle.consultcan revert and brick all paths that rely on TWAP until the params are corrected.Recommendation
Consider enforcing the same TWAP bounds and oracle availability validation in
Relayer._validateTwapParamsas in the factory.Resolution
Gamma Team: Resolved.
-
L-04 Low useRebalanceSwap Not Enforced On-Chain Unexpected Behavior Resolved
Description
In
StrategyParamsstruct,useRebalanceSwapparameter is treated as the configuration setting that indicates whether rebalances should use the swap path, but the relayer does not enforce it at execution time.A whitelisted automation service can always call either
executeRebalanceorexecuteRebalanceSwap, regardless of the configured flag.Off-chain preview contract (
RelayerLens) relies onuseRebalanceSwapto decide whether a swap is expected, but on-chain the flag is not enforced at execution time.If the automation service is misconfigured, it can execute the unintended path (swap when disabled or no-swap when swap is required), potentially leading to unnecessary and unexpected executions.
Recommendation
Consider gating
executeRebalanceandexecuteRebalanceSwapso the callable entrypoint must matchuseRebalanceSwapparameter, making the configuration enforceable on-chain.Resolution
Gamma Team: Resolved.
-
L-05 Low Incomplete Limit Width Collision Check Error Resolved
Description
In
setLimitRanges, when a base range width matcheslimitWidth, the code incrementslimitWidthonce and immediately exits the loop. The new value is not rechecked against remaining base ranges, allowing another collision to remain undetected.for (uint256 i = 0; i < baseRangesLength;) { int24 rangeWidth = baseRanges[i].upperTick - baseRanges[i].lowerTick; if (rangeWidth == int24(limitWidth)) { limitWidth = uint24(int24(limitWidth) + tickSpacing); break; // Exits without checking the rest } unchecked { ++i; } }If the resulting limit range later matches a base range’s (
lowerTick,upperTick),checkRanges()will revert withDuplicatedRange, causing the rebalance or migration to fail.Recommendation
Consider iterating until
limitWidthno longer collides with any base range width.Resolution
Gamma Team: Resolved.
-
L-06 Low TWAP Protection Gas Not Reimbursed Logical Error Resolved
Description
executeRebalanceandexecuteRebalanceSwaprun_checkTwapProtectionbefore samplinggasleft.Since
_reimburseGasonly accounts for gas spent aftergasBeforeis captured, the oracle/TWAP check gas is excluded from reimbursement even on successful executions.Consequently, automation service is systematically under-reimbursed when TWAP protection is enabled.
Recommendation
Capture
gasBeforebefore calling_checkTwapProtection.Resolution
Gamma Team: Resolved.
-
L-07 Low Native Currency Breaks getUniqueTokenPairs() Logical Error Resolved
Description
RelayerFactory.getUniqueTokenPairs()iterates through deployed relayers and reads each relayer’spoolKeyto derive token metadata (symbol and decimals).It unwraps each Currency into an address and treats it as an
ERC-20by callingIERC20Metadata(token).symbol()andIERC20Metadata(token).decimals().For Uniswap v4 pools that use the native currency (ETH), the corresponding Currency unwraps to address(0). As a result,
getUniqueTokenPairs()will callIERC20Metadata(address(0)), which will revert.Recommendation
Handle native currency explicitly before calling
ERC-20metadata methods. Iftoken == address(0), return a fixed symbol ("ETH") and decimals (18).Resolution
Gamma Team: Resolved.
-
L-08 Low Proportional Rebalance Can Revert At Min Price DoS Resolved
Description
In proportional mode (
weight0 == 0 && weight1 == 0), the current range math computestoken0Neededusing the return value of the following equation, as a denominator:FullMath.mulDiv(sqrtPriceUpper, sqrtPriceX96, FixedPoint96.Q96)Near extreme
MIN_SQRT_PRICE(2^32), this innermulDivcan floor to 0 (whensqrtPriceUpper *sqrtPriceX96 < 2^96), causing the outermulDivto revert due to division by zero.This is only reachable at very extreme pool prices, but it is still a valid Uniswap state. In this scenario, proportional rebalances can revert, halting automated rebalancing and compounding at those prices.
Recommendation
Be aware of this edge case and add a simple guard before the outer
mulDivso proportional rebalances never revert due to division by zero.Resolution
Gamma Team: Resolved.
-
L-09 Low User-Specified Weights Silently Overridden Logical Error Resolved
Description
When a user provides explicit weights (e.g., weight0=0.7e18, weight1=0.3e18), the system validates they sum to 1e18 but may silently override them to 50/50 in
calculateWeightsWithPoolKeyif the strategy doesn't support weighted distribution:if (!params.useCarpet && !supportsWeightedDist && (params.weight0 != 0.5e18 || params.weight1 != 0.5e18)) { params.weight0 = 0.5e18; params.weight1 = 0.5e18; }The ctx struct is never updated to reflect this change. Subsequently,
_calculateLiquiditiesFromWeightsoperates on weights calculated with 50/50 weighting, causing a mismatch between user intent and actual execution.A user requesting 70/30 may get silently ignored, and would instead get 50/50 weighting from the strategy’s density computation, leaving idle token/unused balances.
Recommendation
Consider reverting when weights are requested (explicit) but unsupported.
Resolution
Gamma Team: Resolved.
-
L-10 Low ExactOut Swaps Assume Full Input Consumed Logical Error Acknowledged
Description
In
_executeProvidedSwap, the code assumes the fullswapAmountis consumed by the aggregator:if (swapParams.swapToken0) { return (amount0 - swapParams.swapAmount, amount1 + amountOut); } return (amount0 + amountOut, amount1 - swapParams.swapAmount);For
exactOutswaps, the aggregator may consume less thanswapAmount(which represents maximum input). The code subtracts the fullswapAmountregardless, underestimating the remaining input token balance.In the
rebalanceSwapflow, this causes liquidity calculations based on understated balances resulting in under-minting of liquidity positions. The difference remains idle in the contract.Recommendation
Track actual input spent by checking balance difference before and after swap (for ETH and
ERC20).Resolution
Gamma Team: Acknowledged.
-
L-11 Low Dust Pricing Can Brick withdrawCustom DoS Resolved
Description
The
withdrawCustomflow relies on converting the vault’s total value and the requested withdrawal value intotoken1terms, then computing how many shares to burn.This has two related failure modes caused by integer rounding:
poolValueInToken1can become 0 even though the vault holds assets. This happens when the vault
is one-sided in
token0(sopool1 == 0) and the conversionFullMath.mulDiv(pool0, price,PRECISION)rounds down to 0 because the entiretoken0balance is worth less than 1 smallest unit oftoken1.- even when
poolValueInToken1is nonzero, the same rounding behavior can
make
withdrawalValue0InToken1round down to 0 for smalltoken0-only withdrawals, which can produceshares == 0.Consequently,
withdrawCustomcan be DoS’d for valid, edge-case states.Recommendation
Be aware of these edge cases in
calculateSharesToBurnand add explicit guards/fallbacks sopoolValueInToken1can’t be 0 and any nonzerowithdrawCustomburns at least 1 share.Resolution
Gamma Team: Resolved.
-
L-12 Low Relayer Deploy Lacks Ratio Validation Configuration Resolved
Description
Relayer is intended to be deployed via
RelayerFactory, but neither the factory nor the Relayer constructor validates the ratio fields inTriggerConfig.The constructor only validates deltas and weights before storing
_triggerConfig, and_validateRatiosis only enforced later insetRebalanceParams.This means a relayer can be deployed through the factory with out‑of‑range ratios (>1e18) or
baseMinRatio > baseMaxRatio, resulting in permanently misconfigured triggers until the owner updates parameters.The deployment path therefore allows invalid trigger thresholds that the update path would reject.
Recommendation
Validate
TriggerConfigratios at deployment by calling_validateRatiosin the constructor or factory.Resolution
Gamma Team: Resolved.
-
L-13 Low Relayer Address Computation Uses Arbitrary Owner Logical Error Resolved
Description
computeRelayerAddressaccepts an arbitraryownerparameter and uses it in theCREATE2address computation, butdeployRelayerignores any external owner input and always deploys the relayer with the actual MPM owner.If a caller computes the address with any owner other than the MPM owner, the computed address will not match the deployed address and consequently, external integrations can end up configured with the wrong address.
Recommendation
Consider removing the
ownerparameter and always useMultiPositionManager(mpm).ownerfor address computation.Resolution
Gamma Team: Resolved.
-
L-14 Low Owner Can Block Protocol claimFee For ETH Pools DoS Resolved
Description
When
MultiPositionManager::claimFeeis executed by the factoryCLAIM_MANAGER, address(0) is encoded to skip owner fee collection. However, an MPM owner can callgrantRelayerRole(CLAIM_MANAGER), causing theCLAIM_MANAGERto match the first condition:function claimFee() external { if (msg.sender == owner() || s.relayers[msg.sender]) { poolManager.unlock(abi.encode(IMultiPositionManager.Action.CLAIM_FEE, abi.encode(owner()))); }This will attempt to send ETH fees to the owner, triggering the
receive()function. A malicious owner can revert this transaction, thus blocking protocol treasury fee collection.Eventually, the owner must withdraw and claim fees, which will allow fee collection by skipping owner ETH fee collection.
However, if the owner decides to burn up to 99% of their positions, and follow-up with a small donation via
PoolManager::donate, this will accrue fees in the existing in-range positions, thus blocking protocol fee collection permanently (as it will always attempt to send small ETH fees to the owner).Recommendation
Do not allow MPM owners to grant relayer role to the
CLAIM_MANAGERaddress.Resolution
Gamma Team: Resolved.
-
L-15 Low Owner Can Blacklist Protocol Fee Recipient DoS Acknowledged
Description
When fees are collected via
MultiPositionManager::claimFee, bothtoken0andtoken1fees are collected together:_claimFeeCurrency(poolManager, s.factory, s.currency0); _claimFeeCurrency(poolManager, s.factory, s.currency1);If any of these calls fail, this blocks fee collection for both
currency0andcurrency1. Given the design of the protocol is to facilitate token launches, any custom token can contain blacklist mechanisms.If a malicious owner blacklists the protocol fee recipient, the
claimFeetransaction will revert, and the protocol will also lose fees on the other currency (which, given the auction mechanism, in most cases will be ETH/USDC).Recommendation
Consider separating the fee collection for both tokens in two different functions, so if fee collection fails for one token, it can still be possible to collect for the other token.
Resolution
Gamma Team: Acknowledged.
-
L-16 Low Carpet Mode Reverts On Small Deposits DoS Resolved
Description
Carpet positions use a fixed weight of 0.005% (
CARPET_WEIGHT = 0.00005e18) and span the full tick range (min to max usable ticks). When calculating liquidity:liquidity = token1 * Q96 / (sqrtPriceUpper - sqrtPriceLower)The extreme tick range can create a large denominator, which combined with the relatively small fixed allocation, liquidity rounds down to zero for small deposit amounts. The validation then reverts:
function _validateCarpetLiquidity(...) { if (baseRanges[0].lowerTick == minUsable && liquidities[0] == 0) { revert InsufficientLiquidityForCarpet(); } }For regular rebalances, users can disable carpet mode. However, during migration via
LBPStrategyBasic, ifcurrencyRaisedfrom the auction is small and carpet mode is enabled, migration will be permanently blocked since parameters are preset at deployment.Recommendation
Similar to base positions, skip carpet positions gracefully when liquidity rounds to zero instead of reverting. Alternatively, allow users to disable carpet mode during migration if needed.
Resolution
Gamma Team: Resolved.
-
L-17 Low MPM Fee Revenue Bypassed Via Custom Hook Pools Compatibility Acknowledged
Description
The protocol's revenue model relies on splitting LP fees collected via
zeroBurnAllbetween the position owner and the fee recipient.However, Uniswap V4 hooks can capture swap fees before they accrue to LP positions through mechanisms like
BEFORE_SWAP_RETURNS_DELTAor LP fee overrides.Since MPM allows creating positions on any pool, users can select pools with fee-capturing hooks. In such pools, LP positions accrue reduced or zero fees, leaving the protocol with no revenue despite providing a managed liquidity service.
Recommendation
Consider validating pool hooks during position creation, or implementing an alternative fee mechanism that doesn't depend solely on LP fee accrual.
Resolution
Gamma Team: Acknowledged.
-
I-01 Informational Unused ETH Causes Revert On Deposits DoS Acknowledged
Description
When creating pools through
OrderBookFactory, the desired deposits andethForDeploymentis calculated, followed by MGM deployment through eitherMultiPositionFactory.deployDepositAndRebalance()ordeployDepositAndRebalanceSwap().In case a user sends ETH exceeding the deposit desired, it is refunded to
msg.sender:MultiPositionManager::_transferIn:function _transferIn(address from, Currency currency, uint256 amount) internal { if (currency.isAddressZero()) { require(msg.value >= amount); if (msg.value > amount) { payable(msg.sender).transfer(msg.value - amount); } ... }However, in the
OrderBookFactoryorToken Launch Migrationflow,msg.senderis theMultiPositionFactorycontract, which lacks areceive()function.This means any transaction with refunded ETH will revert, causing DoS to pool launches with limit order support.
Recommendation
Add a
receive()function toMultiPositionFactoryand And refund excess ETH to the original caller at the end ofdeployDepositAndRebalance()anddeployDepositAndRebalanceSwap().Resolution
Gamma Team: Acknowledged.
-
I-02 Informational Missing Deadline Parameter On MPM Functions Validation Acknowledged
Description
Multiple functions in
MultiPositionManagerlack deadline parameters, includingdeposit(),withdraw(),withdrawCustom(),rebalance(), andcompound().Without deadlines, transactions can remain pending in the mempool and execute at a much later time when market conditions have changed significantly, potentially resulting in unfavorable outcomes for the user (i.e., during deposits, users may receive shares at an outdated price).
Recommendation
Add a deadline parameter to the above MPM functions
Resolution
Gamma Team: Acknowledged.
-
I-03 Informational Dead Code In scaleAllocations Function Best Practices Acknowledged
Description
The
scaleAllocationsfunction inRebalanceLogiccontains an unreachable else branch that handles explicit weights mode (useAssetWeights = false).This code path can never be executed because the only caller,
_calculateLiquiditiesFromWeightsalways calls this function withuseAssetWeights = true, and returns early when explicit weights are used.Recommendation
Remove the
elsebranch and theuseAssetWeightsparameter fromscaleAllocations.Resolution
Gamma Team: Acknowledged.
-
I-04 Informational Redundant Loop In scaleAllocations Gas Optimization Acknowledged
Description
The
scaleAllocationsfunction inRebalanceLogiciterates over the same array indices twice (once fortoken0Allocationsand once fortoken1Allocations). Since both arrays are always the same length, these loops can be combined into a single iteration to save gas.Recommendation
Combine the loop into one to save gas:
for (uint256 i = 0; i < rangesLength;) { if (data.totalToken0Needed != 0) { data.token0Allocations[i] = FullMath.mulDiv(data.token0Allocations[i], available0, data.totalToken0Needed); } if (data.totalToken1Needed != 0) { data.token1Allocations[i] = FullMath.mulDiv(data.token1Allocations[i], available1, data.totalToken1Needed); } unchecked { ++i; } }Resolution
Gamma Team: Acknowledged.
-
I-05 Informational Unused Slippage Parameters In _executeRebalance Best Practices Acknowledged
Description
The
_executeRebalancefunction acceptsoutMinandinMinparameters but never uses them:function _executeRebalance( SharedStructs.ManagerStorage storage s, IPoolManager poolManager, StrategyContext memory ctx, IMultiPositionManager.Range[] memory baseRanges, uint256[] memory weights, uint256[2][] memory, /* outMin */ // Unused uint256[2][] memory /* inMin */ // Unused )The actual slippage protection occurs later in
processRebalanceInCallback.Recommendation
Remove the unused parameters from
_executeRebalancesignature to improve code clarity and save gas.Resolution
Gamma Team: Acknowledged.
-
I-06 Informational Unreachable Code In calculateCurrentRangeExcess Best Practices Resolved
Description
In
calculateCurrentRangeExcess, the else branch settingactualLiquidity = 0is unreachable:if (sqrtPriceX96 < sqrtPriceUpper) { uint256 intermediate = FullMath.mulDiv(sqrtPriceUpper, sqrtPriceX96, FixedPoint96.Q96); actualLiquidity = uint128(FullMath.mulDiv(data.token0Allocations[idx], intermediate, sqrtPriceUpper - sqrtPriceX96)); } else { actualLiquidity = 0; // Unreachable }If
token0Neededis non-zero, then that already meanssqrtPriceX96 < sqrtPriceUpper(due to the previous lines). Therefore, for theif (data.token0Allocations[idx] < token0Needed)branch to execute,if (sqrtPriceX96 < sqrtPriceUpper)will always be true, thus never entering theelsebranch.The unreachable branch also exists in
mintFromAllocationsfor thecurrent rangebranch.Recommendation
Remove the unreachable else branch.
Resolution
Gamma Team: Resolved.
-
I-07 Informational processRebalanceAfterWithdraw Function Is Unused Best Practices Resolved
Description
RebalanceLogic.processRebalanceAfterWithdrawis implemented but never called. This function is intended for automatic rebalancing after single token withdrawals.Note that owners can achieve the same result by manually calling
rebalance()after withdrawing.Recommendation
Remove the dead code or integrate into withdrawal flow.
Resolution
Gamma Team: Resolved.
-
I-08 Informational Only Last Overlapping Range Fixed In Rebalance Documentation Acknowledged
Description
In
RebalanceLogic::calculateInitialAllocations, when multiple position ranges contain the current tick, only the last one is recorded ascurrentRangeIndex:for (uint256 i = 0; i < rangesLength;) { ... if (baseRanges[i].lowerTick <= data.currentTick && data.currentTick < baseRanges[i].upperTick) { data.currentRangeIndex = i; // Overwrites on each match data.hasCurrentRange = true; } unchecked { ++i; } }Subsequently,
fixCurrentRangeAndRedistributeonly fixes the range atcurrentRangeIndexand neglects redistributing for the previous positions that were also in the current range.The impact is that the excess tokens from these other positions will remain idle in the MPM, until the owner withdraws them.
Currently, the protocol strategies enforce that the ranges list will not have any overlapping ranges. However, if an MPM owner decides to utilize custom strategies (with overlapping ranges), then they should be aware of this edge case.
Recommendation
Consider documenting this edge case for MPM owners who decide to utilize custom strategies.
Resolution
Gamma Team: Acknowledged.
-
I-09 Informational Single-Step Ownership Transfer Risk Best Practices Acknowledged
Description
The contracts
MultiPositionManager,MultiPositionFactory,RelayerFactory,DynamicFeeHook,DynamicFeeLimitOrderHook,VolatilityDynamicFeeHook, andVolatilityDynamicFeeLimitOrderHookrely onOpenZeppelinOwnable to guard sensitive actions, but ownership transfers take effect immediately.If the owner key is compromised or a transfer is made by mistake, control moves instantly with no explicit acceptance step and no time for monitoring to react.
Recommendation
Consider implementing
OpenZeppelinOwnable2Stepin these contracts so ownership transfers require a separateacceptOwnershipcall beforeonlyOwnerprivileges move to the new owner.Resolution
Gamma Team: Acknowledged.
-
I-10 Informational Unreachable Withdraw Flags Condition Superfluous Code Resolved
Description
In Relayer,
executeWithdrawalcontains a check that includes(withdrawToken0Only &&withdrawToken1Only).However,
validateWithdrawalParamsfunction forbids setting bothwithdrawToken0OnlyandwithdrawToken1Onlyat the same time andexecuteWithdrawalvalidates params before this branch.This part of the condition is effectively unreachable under any allowed configuration.
Recommendation
Consider removing the unreachable branch.
Resolution
Gamma Team: Resolved.
-
I-11 Informational Limit Positions Minted Without Slippage Checks Warning Acknowledged
Description
Limit positions are minted using all remaining balances with
inMin = [0,0], meaning no slippage protection is applied.This is not currently exploitable because limit positions are single-sided and use only leftover tokens, so no excess spending or token loss can occur.
If future changes introduce different execution assumptions, this could become a real slippage risk.
Recommendation
Ensure protocol team is aware of this, or consider documenting it.
Resolution
Gamma Team: Acknowledged.
-
I-12 Informational Compound Calls zeroBurn Twice Wasting Gas Gas Optimization Acknowledged
Description
The
compound()(andcompoundSwap) function callszeroBurnAllWithoutUnlock()twice:function compound(uint256[2][] calldata inMin) external payable onlyOwnerOrRelayerOrFactory { if (s.basePositionsLength > 0) { poolManager.unlock(abi.encode(IMultiPositionManager.Action.ZERO_BURN, "")); } poolManager.unlock(abi.encode(IMultiPositionManager.Action.COMPOUND, abi.encode(inMin))); }Then,
processCompound()calls it again:function processCompound(...) external { if (s.basePositionsLength == 0) return; WithdrawLogic.zeroBurnAllWithoutUnlock(s, poolManager);The second call collects zero fees since they were already collected. This wastes gas from looping through all positions twice and two separate UniswapV4 unlock calls.
Recommendation
Remove the
zeroBurnAllWithoutUnlockcall fromprocessCompound().Resolution
Gamma Team: Acknowledged.
-
I-13 Informational Burn Event Emitted When Shares Are Not Burned Events Acknowledged
Description
In
WithdrawLogic.processWithdraw, whenwithdrawToWallet = false, a Burn event is emitted even though shares are not actually burned. The main contract only burns shares whenwithdrawToWallet= true:if (withdrawToWallet) { _burn(owner(), shares); }This causes a mismatch between emitted events and actual state, misleading off-chain indexers and integrations that track share burns.
Recommendation
Either rename the event to accurately reflect the action (e.g.,
LiquidityRemoved), or only emit Burn when shares are actually burned.Resolution
Gamma Team: Acknowledged.
-
I-14 Informational Withdraw Sends ETH Before Burn Reentrancy Resolved
Description
In withdraw, ETH is transferred to the owner before shares are burned:
WithdrawLogic.processWithdraw():s.currency0.transfer(to, amount0); // ETH sent hereMultiPositionManager.withdraw():if (withdrawToWallet) { _burn(owner(), shares); // Shares burned after }The owner could re enter deposit (which lacks
nonReentrant) during the ETH callback, potentially receiving inflated shares sincetotalSupplyhasn't been reduced yet.Currently unexploitable because only the owner can deposit/withdraw, meaning they would be the only user affected. However, this violates CEI pattern and could become exploitable if the design changes to support multiple users.
Recommendation
Either add
nonReentrantto deposit, or burn shares before transferring tokens.Resolution
Gamma Team: Resolved.
-
I-15 Informational Stale Position Storage After Full Withdrawal Unexpected Behavior Resolved
Description
When a full withdrawal occurs, the
WITHDRAWaction burns all liquidity but doesn't clear position storage (basePositionsLength,basePositions,limitPositionsLength,limitPositions).As a result, the vault is left in a state where no liquidity exists, yet the position storage still reflects old ranges.
Subsequent operations (e.g. future deposit + rebalance) rely on these stale lengths for validation:
if (outMin.length != s.basePositionsLength + s.limitPositionsLength) revert OutMinLengthMismatch();Users must provide
outMinarrays sized for stale positions with zero values, otherwise the transaction reverts. This also causes wasted gas due to unnecessary iteration over zero-liquidity positions.Recommendation
Clear all position storage when
totalSupplybecomes zero.Resolution
Gamma Team: Resolved.
-
I-16 Informational Missing Events For Configuration Updates Events Acknowledged
Description
Several admin and configuration functions, as well as ETH flow functions, update important state without emitting events, reducing on-chain observability for monitoring, alerting, and incident response.
RelayerFactory.solsetAutomatedManagementFeeupdatesautomatedManagementFeewith no event.
Relayer.solsetAutomatedManagementFeeupdatesautomatedManagementFeewith no event,setWithdrawalParamsupdatesstate.withdrawalParamswithout emitting an event,setCompoundSwapParamsupdatesstate.compoundSwapParamswithout emitting an event,pause/unpauseupdatestate.isPausedwithout emittingPaused/Unpausedevent declared
in
IRelayer.sol,fundContract/receiveaccept ETH with no event.withdrawFundstransfers ETH out with no event.executeRebalance/executeRebalanceSwapcan use TWAP centering without
emitting
TwapCenterUsed(declared inIRelayer.sol).MultiPositionFactory.solsetFeeRecipientupdatesfeeRecipientwith no event,setProtocolFeeupdatesprotocolFeewith no event.
MultiPositionManager.solclaimFeetriggers fee distribution without an event that records amounts/recipients.
Recommendation
Consider emitting dedicated events for these functions.
Resolution
Gamma Team: Acknowledged.
-
I-17 Informational MIN_BALANCE Check Counts Msg.value Unexpected Behavior Resolved
Description
In
executeRebalanceSwapandexecuteCompoundSwap, the initial funding check usesaddress(this).balance, which temporarily includesmsg.value.However,
msg.valueis later forwarded to MPM before_reimburseGas. If the relayer has little or no pre-funded ETH, the call can pass theMIN_BALANCEcheck using the caller-suppliedmsg.value, then revert during reimbursement due to insufficient remaining balance, reverting the entire transaction and causing wasted executions.Recommendation
Check the relayer’s balance excluding
msg.valuebefore proceeding.Resolution
Gamma Team: Resolved.
-
I-18 Informational Missing Weight Validation In rebalanceSwap Path Unexpected Behavior Resolved
Description
The
rebalance()path validates that weights sum to 1e18:if (ctx.weight0 + ctx.weight1 != 1e18) revert InvalidWeightSum();However,
_buildStrategyContext()used by therebalanceSwappath lacks this validation. Malformed weights (e.g.,weight0=0.8e18,weight1=0.8e18) can be passed into density calculations and be stored inlastStrategyParams.Since only trusted callers can invoke this functions, impact is limited to user error.
Recommendation
Add the same validation in
_buildStrategyContext()for consistency.Resolution
Gamma Team: Resolved.
-
I-19 Informational Redundant Msg.value Check In deployRelayer Best Practices Resolved
Description
In
RelayerFactory.deployRelayer, the function requires a minimum payment:if (msg.value < 0.001 ether) revert InsufficientPayment();Later, the function conditionally forwards ETH to the relayer:
if (msg.value > 0) { (bool success,) = relayer.call{value: msg.value}(""); if (!success) revert InvalidAddress(); }The if (
msg.value > 0) check is redundant since the earlier validation guaranteesmsg.value >= 0.001ether. The condition will always be true.Recommendation
Remove the redundant check:
(bool success,) = relayer.call{value: msg.value}(""); if (!success) revert InvalidAddress();Resolution
Gamma Team: Resolved.
-
I-20 Informational BURN_AND_REBALANCE Enum Name Is Misleading Documentation Resolved
Description
The
WithdrawPathenum inWithdrawLogic.soldefines a path namedBURN_AND_REBALANCE, but the implementation explicitly does not rebalance:// Withdrawal path enum enum WithdrawPath { USE_CURRENT_BALANCE, // Step 1: sufficient idle balance USE_BALANCE_PLUS_FEES, // Step 2: need zeroBurn for fees BURN_AND_REBALANCE // Step 3: burn all + rebalance remaining }After burning, there is no rebalance, which the following comments confirm:
// NO REBALANCING - excess remains as unused balanceThis creates confusion about the intended behavior and could mislead integrators.
Recommendation
Rename the enum to accurately reflect its behavior, e.g.,
BURN_POSITIONSResolution
Gamma Team: Resolved.
-
I-21 Informational Mint May Allow Overpaying At Manipulated Price Logical Error Acknowledged
Description
The
_mintLiquidityForAmountsfunction usesminAmountInslippage protection instead ofmaxAmountIn.This protects only against spending too little, but provides no protection against spending more than expected at an unfavourable price.
In most cases, the
outMinslippage from burning will provide protection against this attack. However, burning existing positions (nooutMinprotection) can be skipped in the following cases: 1. The first deposit + rebalance on a pool with existing liquidity. Note that multiple MPMs (with different owners) can exist for a single pool. 2. Users deploying MPMs on popular trading pairs (WETH/USDC, etc.) with existing liquidity (i.e through directdeployDepositAndRebalancecall). 3. The first deposit + rebalance after a full withdrawal (no existing positions to burn).Combined with
slot0spot price reads, attackers can sandwich this transaction: 1. Front-run: manipulate price with a swap. 2. Rebalance mints liquidity at the manipulated price. 3. Back-run: restore price and extract profit.The slippage check still passes because
min tokenswere spent, even though they were spent at a worse exchange rate.Unless
minInis always set high enough to prevent this attack, the transaction reverts easily due to small price changes, causing DoS.Recommendation
Similar to Uniswap, consider also using
maxAmountInslippage when adding liquidity to a position.Resolution
Gamma Team: Acknowledged.
-
I-22 Informational Slippage Bypass On Single-Token Rebalance MEV Acknowledged
Description
Proof of concept: PoC
In
RelayerLogic,withdrawSingleTokenfunction first callsMultiPositionManager.withdrawCustom(using the caller suppliedoutMin) to withdraw only token0 or token1 to the owner, then, if the other token still remains, callsrebalanceto redeploy the leftover assets.For this rebalance leg it constructs fresh
outMinandinMinarrays and leaves them at their default zero values, effectively disabling slippage checks for both burning existing liquidity and minting new liquidity.Because the rebalance flow derives allocations from the pool’s current slot0 spot price, an attacker can sandwich the transaction by moving the price before the rebalance and restoring it after, extracting value from the vault and reducing the owner’s remaining total value.
Since the rebalance mechanism in the
withdrawSingleTokenflow is currently unreachable due to M-26 issue, the severity of this finding has been reduced.Recommendation
Accept and enforce
outMin/inMinfor the rebalance step and consider adding a TWAP deviation check.Resolution
Gamma Team: Acknowledged.
-
I-23 Informational withdrawCustom Missing Shares Slippage Warning Acknowledged
Description
The
calculateSharesToBurnfunction inWithdrawLogic.soluses theslot0spot price to determine the amount of shares users must burn when callingwithdrawCustom.For
PATH_1(USE_CURRENT_BALANCE) andPATH_2(USE_BALANCE_PLUS_FEES), there is no slippage protection on this calculation.If the spot price moves unfavourably (natural volatility or attacker manipulation swapping
token1 ->token0and swap back at a small cost), the owner ends up burning significantly more shares than intended.Since PATH 1 and PATH 2 withdraw from idle balances without burning positions, the
outMinparameter provides no protection.The user receives their requested tokens but permanently lose the excess shares burned.
Since the owner remains the sole shareholder, they can still withdraw all remaining vault value using their remaining shares. However, if the MPM ever supports multiple shareholders, this becomes problematic.
Recommendation
Ensure the protocol team is aware of this, in case there are any future changes. Consider adding a
maxSharesToBurnparameter towithdrawCustom.Resolution
Gamma Team: Acknowledged.
-
I-24 Informational Misleading StrategyParams Packing Comment Documentation Resolved
Description
According to the comment, the
StrategyParamsstruct is intended to fit in two storage slots, but the current field sizes exceed 32 bytes in the second slot.The second slot contains two
uint120fields (30 bytes) and three bool flags (3 bytes), totaling 33 bytes.This forces the last flag to spill into a third slot. As a result, the existing comment claiming two-slot packing is misleading.
Recommendation
Change the mentioned comment or consider replacing the three bool fields with a single
uint8flags value to keep the struct within two slots.Resolution
Gamma Team: Resolved.
-
I-25 Informational Uint8 Loop Index Can Overflow Warning Acknowledged
Description
Multiple loops that iterate over base positions use uint8 as the counter, which implicitly assumes
basePositionsLength <= 255.There is no explicit guard in the contract to enforce this bound, so if a strategy (especially a custom strategy) returns more than 255 ranges, the loop counter will overflow the loop will behave incorrectly.
Recommendation
Consider using
uint256for loop counters or enforcing a hard upper bound on the number of ranges returned by strategies and revert if exceeded.Resolution
Gamma Team: Acknowledged.
-
I-26 Informational Zero Liquidity Positions Skip Compounding Informational Acknowledged
Description
Base positions with zero liquidity can be stored in the
basePositions[]array. Unlike carpet positions which revert viaInsufficientLiquidityForCarpet()when liquidity rounds to zero, base positions have no such validation.Once stored, these positions are completely ignored —
compound()distributes tokens proportionally based on existing token holdings:if (positionToken0[i] != 0) { amounts0[i] = FullMath.mulDiv(amount0ToDistribute, positionToken0[i], totalToken0InPositions); }Positions with zero liquidity hold zero tokens, so they receive zero allocation and remain empty indefinitely. These dead positions consume gas during iteration in every operation (compound, withdraw, rebalance) while contributing nothing.
Recommendation
Consider allowing compound for these positions, or validate minimum liquidity/skip storing zero-liquidity positions during rebalancing.
Resolution
Gamma Team: Acknowledged.
-
I-27 Informational Lens Preview Uses totalSupply For outMin Informational Resolved
Description
The lens computes withdrawal slippage bounds using
manager.totalSupply(), butRelayer.executeWithdrawalwithdraws only the MPM owner’s shares. Since shares areERC20and deposit can mint to arbitrary to addresses, the owner may not hold the full supply.This inconsistency can overstate the shares actually withdrawn, producing misleading
outMinvalues and inaccurate withdrawal previews.Recommendation
Update the preview to mirror the actual withdrawal logic by using the MPM owner’s share balance (
balanceOf(Ownable(manager).owner())) when calculatingoutMinForShares.Resolution
Gamma Team: Resolved.
-
I-28 Informational Native ETH Transfers Use Transfer Best Practices Resolved
Description
Native ETH payouts use transfer, which forwards only 2300 gas and can fail for valid contract recipients. This creates a dos risk: fee claims or withdrawals can revert and leave ETH stranded.
Recommendation
Use a safe call pattern for native transfers (
(bool ok, ) = recipient.call{value: amount}("")) and require success.Resolution
Gamma Team: Resolved.
-
I-29 Informational Asymmetric Limit Trigger At Lower Boundary Suggestion Acknowledged
Description
_checkLimitTickTriggerdecides whether the current price is inside the active limit range using onlyslot0.tick. Uniswap v4 documents an edge case whereslot0.tickcan be one less than the tick implied bysqrtPriceX96when the price is exactly on a lower tick boundary.In that state, the observed
currentTickequalslowerTick - 1even though the price is on the lower boundary.The implementation uses asymmetric inequalities for below/above distance checks. Using
>below the range but>=above makes the threshold asymmetric by one tick.It requires
lowerTick - currentTickto exceedlimitDeltaTicksto trigger below, but triggers as soon ascurrentTick - upperTickreacheslimitDeltaTicksabove.This subtle one-tick directional buffer may surprise integrators expecting symmetric distance checks.
Recommendation
Consider explicitly treating the lower-boundary edge case as inside. When
currentTick == lowerTick -1andsqrtPriceX96 == TickMath.getSqrtPriceAtTick(lowerTick), consider treating the price as on-boundary and do not trigger.Resolution
Gamma Team: Acknowledged.
Remediation Review V1
12 findings-
M-01 Medium Inaccurate OP Stack L1 Fee Refund Unexpected Behavior Resolved
Description
The relayer tries to reimburse OP Stack full transaction cost by adding an L1 data fee to the normal L2 gas refund, by calling the OP Stack
GasPriceOraclewith the current call data.However, this is inaccurate for two reasons:
getL1Feeis designed to estimate the L1 data cost from the bytes of an unsigned, RLP‑encoded
transaction, but the relayer passes only
msg.data. Because this omits the transaction envelope bytes (transaction type prefix, nonce, gas limit,maxFeePerGas/maxPriorityFeePerGas, to, value, access list, etc.), the oracle is not pricing the same data that will actually be published to L1, so the L1 fee estimate will be consistently too low.gasUsedis computed before calling the oracle, so the L2 gas spent to computel1Feeis not
included in
gasUsedand is never reimbursed.Consequently, automation services can be systematically under-reimbursed, causing rebalances and withdrawals to stop running reliably.
Recommendation
Reorder the accounting so the oracle call is included in
gasUsed, and estimate L1 fee using a more accurate input, commonly by appending a small constant padding tomsg.databefore callinggetL1Fee.Resolution
Gamma Team: Resolved.
-
M-02 Medium Carpet Mode Disable Mint Slippage Frontrunning Resolved
Description
During
processRebalanceInCallback, the contract validates the length of the user-suppliedinMinarray. WhenrebalanceParams.useCarpetis true andinMin.length != baseRanges.length, the code discards the caller’sinMinand replaces it with a zero-filled array.Since
inMinis mint-side slippage protection, filling it with zeros effectively disables the caller’s intended safeguards. A rebalance can then proceed under worse than expected execution conditions without reverting.Consequently, a MEV actor can sandwich the rebalance and it may still succeed because mint
inMinslippage checks have been bypassed.Recommendation
Do not auto-zero slippage parameters on length mismatch. Consider always reverting when
inMin.length != baseRanges.length, and only acceptinMin.length == 0as an explicit no slippage protection signal.Resolution
Gamma Team: Resolved.
-
M-03 Medium Center Tick Rounding Causes Strategy Overflow Logical Error Resolved
Description
The protocol uses a sentinel value
type(int24).maxto derive the strategy center from the pool’s current tick when executing rebalances. The relayer constructsRebalanceParamswith this value inconstructRebalanceParamsfunction.Downstream, the
rebalanceandrebalanceSwapflows interpret this sentinel by readingcurrentTickfrom slot0 and rounding it down to atickSpacingmultiple.This rounding is not constrained to Uniswap’s usable tick range (multiples of
tickSpacingwithin[MIN_TICK, MAX_TICK]). WhencurrentTickis close to the minimum tick boundary, the floor rounding can produce actx.centerbelowminUsableTick(tickSpacing).That invalid
ctx.centeris then passed tostrategy.generateRanges, where range generation can compute an invalid span (right bound below left bound).When several strategy converts this signed span into a
uint256, the negative value becomes a huge number and later arithmetic overflows, causing the rebalance to revert.Recommendation
Clamp the derived
ctx.centerto Uniswap’s usable tick range before calling the strategy.Resolution
Gamma Team: Resolved.
-
L-01 Low Rebalance Liquidity Overflow At Edge States DoS Resolved
Description
Proof of concept: PoC
The rebalance flow can revert while minting liquidity under specific edge-state combinations. In the rebalance callback path, liquidity is computed using
LiquidityAmounts.getLiquidityForAmounts()and must fit intouint128.In edge states (price near usable tick boundaries, edge-aligned/narrow limit ranges, and large effective per-range allocations), computed liquidity can exceed
type(uint128).max, causing a revert (liquidity overflow).When this happens, the rebalance transaction reverts, liquidity is not redeployed, and repeated attempts can continue to fail under similar conditions, creating a temporary DoS.
Recommendation
Add a pre-mint bound check before calling
getLiquidityForAmounts()and cap or rescale per-range allocations whenever projected liquidity would exceedtype(uint128).max.Resolution
Gamma Team: Resolved.
-
L-02 Low withdrawCustom Burns Shares For Zero Output Logical Error Resolved
Description
In WithdrawLogic,
processWithdrawCustomfunction computessharesBurnedfor the requested amounts and then, when idle balances and fees are insufficient, takes the PATH 3 branch. In this branch it burns a pro-rata portion of positions and then transfers the idle balances.The underlying burn is performed by computing how much liquidity corresponds to the burned shares. However, in
PoolManagerUtils.burnLiquidityForShare, the amount of liquidity to burn is truncated with integer division, so no liquidity is burned and no tokens are released.uint256 liquidityForShares = FullMath.mulDiv(liquidity, shares, totalSupply);The
withdrawCustomthen computes outputs from the post-burn idle balances, which can remain 0, and the caller still burnssharesBurnedafterprocessWithdrawCustomreturns.The protocol intends each MPM to have a single trusted shareholder, which reduces the impact of this finding - however this assumption is not enforced at the contract level because shares are standard
ERC20and deposits can mint to arbitrarytoaddress.Recommendation
Prevent share burns when the withdrawal produces no assets by reverting PATH 3 when both outputs are zero.
Resolution
Gamma Team: Resolved.
-
L-03 Low Paused Relayer Still Charges Increased Fee Logical Error Resolved
Description
When
RelayerFactory.deployRelayeris called, it setsautomatedManagementFeeon the MPM viasetFee. This increases the protocol fees from 5% to 10%.If the MPM owner later pauses the relayer, automated rebalances stop but the increased fee remains.
Fees accumulated during the paused period are still split at the higher
automatedManagementFeerate when claimed, even though no automation service is being provided.Recommendation
Consider rebooting the fee to the factory's default
protocolFeewhen the relayer is paused (ensure to collect fees just before changing the protocol fee, so previous accumulated fees still apply), and increase it again when unpaused.Resolution
Gamma Team: Resolved.
-
L-04 Low Double-Counted Amounts In Withdraw Burn Event Events Resolved
Description
In
processWithdraw, thewithdrawToWallet = falsepath calculatesunusedAmountsas a proportional share ofbalanceOfSelf(). However, at this pointbalanceOfSelf()already includes the tokens received from the burn callback (settled via_closePair).Adding these to the already-set
amount0/amount1double counts the burned amounts in the emitted Burn event. ThewithdrawToWallet = truepath avoids this by transferring burned amounts before calculating unused balances.Recommendation
Subtract the burned amounts from
balanceOfSelf()before calculating unused amounts, or compute unused amounts only from the pre-existing idle balance.Resolution
Gamma Team: Resolved.
-
I-01 Informational Dead Fallback Branches In withdrawCustom Flow Informational Acknowledged
Description
In
WithdrawLogic,calculateSharesToBurnincludes a fallback forpoolValueInToken1 == 0intended to handle non zerotoken1values.However, in the real
withdrawCustomflow these branches are effectively unreachable, asprocessWithdrawCustomenforcesamount1Desired <= total1before callingcalculateSharesToBurn.if (params.amount1Desired > pathInfo.total1) revert InsufficientBalance();processWithdrawCustompassespathInfo.total1intocalculateSharesToBurnaspool1. When the fallback triggers (poolValueInToken1 == 0),pool1must be 0, which forcesamount1Desiredto also be 0 due to the precondition above.Therefore, the fallback logic intended to handle nonzero
token1(the price conversion branches gated byamount1Desired != 0orpool1 != 0) cannot be executed in thewithdrawCustompath.Recommendation
Move the nonzero-
token1fallback logic to preview-only code or document that it cannot occur inwithdrawCustomflow.Resolution
Gamma Team: Acknowledged.
-
I-02 Informational Uncapped Gas Reimbursement Suggestion Acknowledged
Description
The relayer reimburses automation callers with no cap on the effective gas price or per-call reimbursement.
Because execution is restricted to
AUTOMATION_SERVICE_ROLE, this is a privileged-abuse path rather than a permissionless exploit; however, a malicious or compromised automation address can still overpay gas and extract disproportionate ETH reimbursements, draining the relayer balance and causing automation to stop once balance falls belowminBalance.Recommendation
Consider adding configurable caps on reimbursable gas price and per-call reimbursement to limit overpayment risk.
Resolution
Gamma Team: Acknowledged.
-
I-03 Informational withdrawCustom Burns Shares For Claimable Fee Logical Error Acknowledged
Description
calculateSharesToBurncomputes shares to burn using fee-inclusive totals fromgetTotalAmounts.When the owner uses
withdrawCustomand the withdrawal is satisfied partly by claimable fees (viaUSE_BALANCE_PLUS_FEESorBURN_AND_WITHDRAWpaths), shares are burned proportional to the full withdrawal amount including the fee portion.Since the owner can claim fees via
claimFee()without burning any shares, this effectively costs the owner shares for value they are entitled to for free.Since the owner is the sole shareholder, they're not losing value to anyone, just burning more shares than needed.
Recommendation
Exclude the owner's claimable fee portion from the share calculation, or automatically call
claimFeebefore computingsharesBurnedso that fee value is already extracted before the withdrawal share math is applied.Resolution
Gamma Team: Acknowledged.
-
I-04 Informational Unnecessary S.fee != 0 Guard In processClaimFee Superfluous Code Resolved
Description
In
processClaimFee,if (s.fee != 0)is checked prior to executing owner transfer logic. However,s.feeis guaranteed non-zero (explicitly requires newFee != 0 in setFee) sincetotalFee0 / s.feewould revert due to division-by-zero.Recommendation
Remove the redundant check.
Resolution
Gamma Team: Resolved.
-
I-05 Informational Fee Change Retroactively Affects Unclaimed Fees Suggestion Acknowledged
Description
setFeeupdatess.feeimmediately, but fee splits are calculated at claim time using the currents.feeas a denominator.If fees accumulated under a previous rate are not claimed before changing the fee, the new rate applies retroactively.
For example, fees accumulated at
s.fee = 20(5% treasury) would be split at 10% ifs.feeis changed to10before claiming (i.e., during relayer deployment).Recommendation
Call
claimFeebefore updating the fee, either by enforcing it withinsetFeeitself or documenting it as a required precondition. InRelayerFactory::deployRelayer, trigger a fee claim on the MPM before callingsetFee.Resolution
Gamma Team: Acknowledged.
Remediation Review V2
2 findings-
L-01 Low TickLiquidityOverflow In Rebalance DoS Acknowledged
Description
Proof of concept: PoC
The rebalance flow can still revert during mint under edge-state combinations due to per-tick liquidity capping, causing
TickLiquidityOverflowin Uniswap v4.In the rebalance callback path (
PoolManagerUtils._mintLiquidityForAmounts), liquidity is derived viaLiquidityAmountsCapped.getLiquidityForAmountsCapped()and capped toint128.max, but not capped against the pool’smaxLiquidityPerTickconstraint.When price is near extreme usable ticks and ranges become narrow/edge-aligned (for example, near 887271 -> 887272), the computed
liquidityDeltacan remain valid foruint128yet still exceed per-tick headroom, sopoolManager.modifyLiquidity(...)reverts withTickLiquidityOverflow.Recommendation
Cap rebalance mint liquidity by tick headroom, not just
uint128bounds.Before
modifyLiquidity, computemaxLiquidityPerTickfromtickSpacingand clampliquidityDeltato the smaller remaining headroom acrosstickLowerandtickUpper. If headroom is zero, skip that mint.Resolution
Gamma Team: Acknowledged.
-
I-01 Informational Donation Griefing Via Tight inMin Warning Acknowledged
Description
The
MultiPositionManagermints new liquidity using the full on-contract balances of the pool currencies (including any unsolicitedERC20or ETH sent to the contract).If an operator or automation service supplies tight
inMinvalues for rebalance minting, a third party can grief those rebalances by donating a small amount of one of the pool tokens immediately before execution, shifting the mint outcomes and a base position to fall below the precomputedinMinthresholds.The minting path enforces per-position minimums and will revert the entire rebalance when any position under-mints either token.
Consequently, rebalances can be DoS’d as long as the attacker is willing to donate assets to the vault, forcing repeated reverts until
inMinis loosened.Recommendation
Be aware of this vector. While the attack is infeasible since an attacker would burn funds, a tightly set
inMincould still be violated by a donation and cause aSlippageExceededrevert.Resolution
Gamma Team: Acknowledged.
No findings match.
More from Gamma Strategies
All 7 reports-
Unilaunch Launchpad and Limit Order Book
28 findings8 high 28 findings: 8 high, 8 medium, 5 low, 7 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.
