Gamma engaged Guardian to review the security of their limit order system using UniswapV4 hooks. From the 5th of March to the 17th of March, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- March 5 to 17, 2025
- Language
- Solidity
- Chains
- Ethereum, Arbitrum, Optimism, Base, Polygon, BNB Chain
- Sector
- DEXs and AMMs
- 7 Critical
- 5 High
- 3 Medium
- 28 Low
- 0 Informational
Scope
Overview
Gamma engaged Guardian to review the security of their limit order system using UniswapV4 hooks. From the 5th of March to the 17th of March, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 12 High/Critical issues were uncovered and promptly remediated by the Gamma team.
Security Recommendation Given the number of High and Critical issues detected as well as additional code changes made after the main review, Guardian recommends that an independent security review of the protocol at a finalized frozen commit is conducted before deployment.
Findings 43
-
C-01 Critical Anyone Can Trigger Limit Orders Access Control Resolved
Description
The
executeOrderfunction is external with no validation performed on thetickBeforeSwapandtickAfterSwapvalues to ensure that they align with the pool state.As a result a malicious actor can trigger limit orders when they are not actually filled causing accounting issues within the system and of course defeating the purpose of a limit order.
Recommendation
Add access controls to the
executeOrderfunction such that it is only callable by theLimitOrderHookcontract.Resolution
Gamma Team: The issue was resolved in LimitOrderManager.sol#L550.
-
C-02 Critical Position Liquidity Unaltered Logical Error Resolved
Description
Proof of concept: PoC
In the
cancelOrderfunction there is no logic to reduce thetotalLiquidityamount stored on the position such as in the_updateUserPositionfunction.As a result, as soon as one user cancels an order in a given range, all other users will be unable to be executed and swaps in the pool will be DoS’d past that tick as the callback reverts due to attempting to burn more liquidity than exists in the position.
Recommendation
Introduce logic into the
cancelOrderfunction such that thetotalLiquidityof the position is reduced in line with the amount that was burnt for the user.Resolution
Gamma Team: The issue was resolved in LimitOrderManager.sol#L415.
-
C-03 Critical Range Orders Arbitraged Validation Resolved
Description
The creation of range orders starts from the current pool price using a rounded tick. However the current pool rounded tick at the time in which the user decides to send their range may be significantly different then the current pool price when their order creation transaction is recorded in a block.
The Uniswap V4 pool that is used for Gamma limit orders is distinct from other Uniswap V4 liquidity sources, therefore it is entirely possible that there is little or no liquidity below the current market price when a limit order creation transaction is submitted to the mempool by a user.
A malicious actor can therefore swap with 0 input amount and a
sqrtPriceLimitset to an extremely low price to move the pool price very low directly before a user’s range is created.After the user’s orders are created, liquidity will now be available in the pool at a very low price for the provided token. The malicious actor can now buy up this provided liquidity from the user at a very advantageous price to net a profit.
Recommendation
Consider allowing users to specify a minimum and maximum tick which they will accept the range, similar to a slippage tolerance.
Resolution
Gamma Team: The issue was resolved in commit 2e003c8.
-
C-04 Critical Limit Orders Missed Due Logical Error Resolved
Description
Proof of concept: PoC
In the
_findOverlappingPositionsfunction the search for executable orders is halted as soon as a position is encountered which has an end tick after the resulting pool price and is therefore unexecutable.However, for
zeroForOnelimit orders, since thepositionTickRangeListis sorted by the bottom tick this results in orders often being skipped when they are in fact executable.Consider the following sorted
positionTickRangeList.0: Position(lower: 60, 300)
1: Position(lower: 120, 360)
2: Position(lower: 180, 300)
When the pool price swaps to tick 320 the loop in the
_findOverlappingPositionsfunction will break because the order in the first index has a top tick which is above the resulting pool price. However the subsequent order in index 2 should have been included in thepositionsIdlist and executed.Recommendation
Consider implementing a combination of a bitmap to show ticks which are the end tick for at least one order in the Gamma system and a mapping from tick to a set of positions which end at that tick.
Resolution
Gamma Team: The issue was resolved in LimitOrderManager.sol#L696.
-
C-05 Critical Wrongful Liquidity Burn Logical Error Resolved
Description
Proof of concept: PoC
When a position is executed, all of the liquidity at that position is burned. A user can also cancel their order after a position has been executed, which also burns the user's share of liquidity.
Therefore, the following scenario can occur which will result in permanent loss of funds:
- User1 creates limit order at (60,120)
- Swap pushes to tick 180, limit order is executed. Liquidity is burned.
- Swap pushes the price back down to tick 0.
- User2 creates limit order at (60,120)
- User1 can cancel their original limit order that's already been executed.
- User2's liquidity has been burned and they cannot cancel their order. Orders at this position will
revert when executed, also causing system-wide DOS for swaps around these ticks.
Recommendation
The early return
userPositions[poolId][positionKey][user].claimablePrincipal != ZERO_DELTAneeds to be amended to also include if the position is not active.This
ifclause can never be reached becauseclaimablePrincipleis never non-zero at this point. The only check should be forposition.isActiveto indicate whether the position has been executed or not.Resolution
Gamma Team: The issue was resolved in LimitOrderManager.sol#L391.
-
C-06 Critical No Access Control Access Control Resolved
Description
The vulnerability in the
LimitOrderHookcontract arises due to the lack of access control in thebeforeSwap()andafterSwap()hook callback functions, which are critical to the proper execution of limit orders during swaps.Specifically, the
beforeSwap()function stores the tick value before a swap, while theafterSwap()function processes limit orders based on the state change between the previous and new ticks. However, these functions do not have proper access control to restrict who can invoke them.Without mechanisms like the
onlyByPoolManager()modifier, unauthorized users can call these functions directly, including malicious actors who may manipulate swap behavior.This could lead to serious consequences such as unauthorized order executions, where users could trigger limit orders to be processed without them actually being filled or meet the conditions of the swap.
For example, the
executeOrder()function, which is designed to be executed only by the LimitOrderHook contract after a legitimate swap, can currently be called by any user.This allows for orders to be executed or cleared arbitrarily, completely undermining the protocol's limit order mechanism and breaking its core functionality.
Given that these hooks are integral to the protocol's operation, this access control vulnerability has the potential to severely disrupt the system, leading to manipulation of the limit order functionality.
Recommendation
Add
onlyByPoolManager()ofSafeCallback.solin all hooks for access control.Resolution
Gamma Team: The issue was resolved in LimitOrderHook.sol#L77.
-
C-07 Critical User Remains Position Contributor Logical Error Resolved
Description
Proof of concept: PoC
Users are never removed from
positionContributorsset upon cancellation of their order. This leads to the following scenario:(1) User creates an order, position count is 1.
(2) User cancels the order, position count is 0 after
userPositionKeysis cleared, but user remains a position contributor for that position key in mappingpositionContributors.(3) User creates the same order. Because the user is already seen as a position contributor, the position key is not added to
userPositionKeysand the position is not registered for the user, hence their position count remains 0. This impacts most view functions andcancelBatchOrderswhich directly rely onuserPositionKeys.(4) If the user were to attempt to cancel their "unregistered" order to retrieve their funds, there would be a revert when attempting to remove the tick range, as it does not exist.
This is because the "ghost" position was never cleared, hence a new tick range was not inserted upon order creation in step (3). Specifically, an underflow revert on
uint256 right =positionTickRangeList[poolId].length - 1;within functionfindPositionTickRangeIdxwill occur. Ultimately, a user who creates the same order after cancelling a previous order loses their funds.Recommendation
Ensure upon cancellation that a user is removed as a position contributor as necessary and that the position key is marked as inactive.
Resolution
Gamma Team: The issue was resolved in commit bf63099.
-
H-01 High Pools With Low SqrtPrices Are Unusable Rounding Resolved
Description
Because of precision loss that occurs when converting the user’s specified amount to the order’s associated liquidity, pools for quote assets with small prices relative to the base token are unusable.
This is because the creation of an order does not clear any remaining amount delta from the Uniswap
PoolManager.As a result non zero balance deltas are left in the delta for the user’s input currency and therefore the
NonzeroDeltaCountis not decremented to zero upon themodifyLiquiditycall.This applies to tokens with a
sqrtPricein the range of 1e27 ($0.0001) and below, but also for token pairs where one token has larger decimals than the other, simulating a very low price.Recommendation
At the end of the
CREATE_ORDERScallback, be sure to settle any dust that was left for the user by either minting it or clearing it from thePoolManagercontract. Be aware that if it is cleared then this amount is lost.Resolution
Gamma Team: The issue was resolved in commit ec5654f.
Guardian Team: Dust still remains in the
accountDeltawithin the PoolManager when creating orders with low prices, hence the issue remains. TriggerPositionManager.clearto clear the leftover delta on the account and decrement theNonzeroDeltaCount. Note that clearing will lock the funds in the PoolManager permanently. -
H-02 High Incorrect Positions Removed Logical Error Pending
Description
In the
executeOrderByKeeperfunction theexecutablePositionIdslist is populated with the indexes of each position tick range before any modifications have been made to thepositionTickRangeList.However in the
_handleKeeperExecuteCallbackfunction the ids are removed sequentially, thereby adjusting the indexes of some positions after each removal and invalidating the providedexecutablePositionIds.This will end up in the wrong positions being removed from the
executablePositionIdslist. This will only be avoided if the keeper provides a carefully ordered list to theexecuteOrderByKeeperfunction which is in such an order that the removal of each iterative position does not affect the index of each subsequent one.Recommendation
Consider reading the most up to date index of each position directly before removing it in the
_handleKeeperExecuteCallback forloop.Resolution
Gamma Team: The issue was resolved in commit cfc3067.
-
H-03 High Execution Of Orders Can Be Skipped Logical Error Resolved
Description
Proof of concept: PoC
Executable orders can get skipped in certain cases. Let's say we are at tick 0 and we have a limit order at 120-180 range
isToken0: true. If we make a swapzeroForOne: falsethen we will move the price up. If the price happens to land somewhere between 120-180 the limit orders will not execute.The issue appears when when we make another such swap then
beforeSwapTickis going to be between 120-180 and theafterSwapTickis going to be after 180. The limit orders will not execute again.This limit orders will get executed if the
beforeSwapTickis before 120 and theafterSwapTickis more than 180. In reality the price have already passed the limit order range and should have been executed already.In the current implementation and example the bottom tick gets compared to the
beforeSwapTickduring the binary search and this causes the orders to get skipped.Recommendation
Consider modifying
_findOverlappingPositionsto select positions that the price range have already passed, despite thebeforeSwapTickvalue.Resolution
Gamma Team: The issue was resolved in commit 6d9eff8.
-
H-04 High Order Creation And Execution Can Be DoSed DoS Resolved
Description
Proof of concept: PoC
New positions are added to the
positionTickRangeListarray. This array can be artificially bloated, causing a DOS to new order creation or existing order execution. Upon creating an order for a new position or executing an existing position, thepositionTickRangeListmust be reordered, i.e. the array indexes must be moved around.A malicious user can create many scale orders or range limit orders to inflate the size of the array which will cause out-of-gas reverts when a legitimate user attempts to create an order at a new position or a swap attempts to execute an order. Essentially, all swaps through the pool will revert. The attacker is able to recoup all of the funds used to set up the positions by cancelling them.
Furthermore, an oversight in the position removal logic allows these spam positions to stay permanently without possibility of removal.
Note: Through testing, it was determined that scale orders could be spammed to create enough positions so that a single order execution would cost ~15,000,000 gas, while the estimated block gas limit is 30,000,000. Range limit orders could be used to fill up the array more to achieve the full OOG, though this is not performed in the POC.
Recommendation
Make sure when the attacker cancels a limit order, this call to also remove the created position if his order was the only one for this position. This would make the attack more expensive but still possible since he will not be able to retrieve his funds and hold the DOS at the same time.
To prevent the issue from happening involves a design decision from the team either to limit the amount of positions that can be opened at the same time or to seek a different method of storing and identifying executable positions.
Resolution
Gamma Team: The issue was resolved in LimitOrderManager.sol#L447.
Guardian Team: Function
removePositionTickRangeremains costly and order execution may be entirely prevented due to OOG reverts. In testing, it was found that swapping across just 5 tick spacings consumes nearly 36 million gas with about 5000 orders in the tick range list. Besides creating numerous orders, users can use arbitrary orders to make emergency cancellation extremely inefficient. -
H-05 High Batch Cancels Cause Users To Cancel Incorrectly Logical Error Resolved
Description
Imagine you have 10 active limit orders and you want to cancel the first 3 orders. User position keys in storage = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]. We call
cancelBatchOrders(poolKey, 0, 3);The loop insidecancelBatchOrders()on the first iteration selects user position key at index 0.Then code executes
_cancelOrder() > _claimOrder()here.The user position keys in storage get changed from [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] to [9, 1, 2, 3, 4, 5, 6, 7, 8]. Then on the next iteration code will select again the first element of the array because i is going to be 1 but
canceledCountwill be 1 as well. When subtracted we will get the 0th index again.At the end of the looping instead of removing user positions 0, 1 and 2,
cancelBatchOrders()will remove 0, 9, 8 - this could be unexpected by the user and cancel orders by mistake. User positions will change in the following way: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] > [7, 1, 2, 3, 4, 5, 6]Recommendation
Consider removing positions that are right after the user specified
offset.Resolution
Gamma Team: The issue was resolved in LimitOrderManager.sol#L288.
-
M-01 Medium Keeper Frontrunning Frontrunning Acknowledged
Description
The
executeOrderByKeeperfunction allows the keeper to execute orders which are technically fulfillable based on the pools price but that were not executed in the swap which crossed their end tick.However if the pool price is back within their position end tick at the time of keeper execution, then the position is marked as no longer executable by the keeper and is not executed.
A bad faith actor could frontrun the keeper’s call to the
executeOrderByKeeperfunction and force the pool price to go below the position’s execution range and thus prevent these positions from being executed.Recommendation
Be aware of this risk and consider using MEV protection such as flashbots for the keeper role.
Resolution
Gamma Team: Acknowledged.
-
M-02 Medium Permissionless Use Of Pools Validation Resolved
Description
Gamma allows for orders to be created/cancelled/executed for arbitrary pools. This may allow a malicious token pair to take advantage of user's locked up funds after creating orders, to prevent token transfer when claiming/cancelling and lead to loss of user funds.
A malicious token can be used to create a pool which orders are created for on Gamma. The malicious token could intentionally allow an execution to occur of limit orders, with an overflow of orders being assigned as keeper executable.
Before the keeper’s execution of the orders the malicious token could be updated to expend a significant amount of gas and even store this gas in a canonical “gas token” to extract value from the keeper.
Recommendation
Consider whitelisting the pools that are allowed to be used with the Gamma limit system to avoid the risk of malicious tokens in arbitrary pools.
Resolution
Gamma Team: Resolved.
-
M-03 Medium Updating Fee Values Affects Fees Logical Error Acknowledged
Description
When
setHookFeePercentage()is called, the hook fee percentage will be updated (increased or decreased). An increase in this percentage will cause the user to receive less fees than expected from their already accumulated fees.A user might be checking that they should receive $100 in fees based on swaps already executed, then the admin increases the fee percentage, now the user will only receive$ 50.
Recommendation
Implement a fee change functionality that operates based off of the original fee percentage until
_retrackPositionFee()is called. This will ensure that the proper percentage is used until the pool is updated.Further, potentially create another function that performs fee checkpointing for multiple positions in a batch.
Resolution
Gamma Team: Acknowledged.
-
L-01 Low Insufficient Event Data Events Resolved
Description
In the
_claimOrderfunction theLimitOrderClaimedevent only emits thepoolIdanduseras information, however the position tick ranges are also useful to discern which position has been claimed from the pool by the user.Recommendation
Consider including the lower and upper ticks in the
LimitOrderClaimedevent.Resolution
Gamma Team: Resolved.
-
L-02 Low Lacking Keeper Execution Validation Validation Acknowledged
Description
The
executeOrderByKeeperfunction allows the keeper to pass a list ofwaitingPositionshowever no validation is performed to ensure that thepositionKeyvalues are not duplicated in this list.There are however no consequential impacts of this as the
_handleKeeperExecuteCallbackfunction willcontinuefor positions that have already been processed.Recommendation
Consider adding validation against duplicated
positionKeyentries to avoid any unexpected behavior and limit keeper error in theexecuteOrderByKeeperfunction.Resolution
Gamma Team: Acknowledged.
-
L-03 Low Scale Order Validation Does Not Take Place Logical Error Resolved
Description
The
_createOrderfunction calls thevalidateScaleOrderSizesbut does not assert that the returned boolean is true.The most important validations in the
validateScaleOrderSizesrevert upon failure, however the zero length andtotalAmountvalidations choose to return false rather than reverting. These less important sanity checks are not enforced by the invocation in_createOrder.Recommendation
Standardize the enforcement of all validations in the
validateScaleOrderSizesto all either return false or revert on failure and be sure to correctly assert that the returned boolean is true if standardizing on a boolean return value.Resolution
Gamma Team: The issue was resolved in PositionManagement.sol#L99.
-
L-04 Low Lacking safeTransferFrom Usage Best Practices Resolved
Description
In the
_handleTokenTransferfunction thetransferFromfunction is used to transfer tokens from the user. HoweversafeTransferFromshould be used in this case since arbitrary tokens may be used within this context.Recommendation
Use
safeTransferFromin the_handleTokenTransferfunction.Resolution
Gamma Team: The issue was resolved in commit 1c24bd1.
-
L-05 Low Lacking Zero Address Validations Validation Resolved
Description
In the constructor of the
LimitOrderManagercontract the_treasuryaddress is validated to be nonzero, however the_poolManageraddress is not validated to be nonzero.Recommendation
Consider also validating the
_poolManageraddress to verify that it is not errantly assigned as the zero address.Resolution
Gamma Team: The issue was resolved in commit abafc71.
-
L-06 Low Unnecessary Position State Superfluous Code Resolved
Description
In the
LimitOrderManagercontract thepsstorage variable is unnecessary and can be removed. In addition to this thePositionStoragestruct defined in thePositionManagementlibrary contains duplicate entries of the important storage values in theLimitOrderMangerand therefore can also be removed.Recommendation
Consider removing the
psvariable and theLimitOrderManagerstruct.Resolution
Gamma Team: Resolved.
-
L-07 Low Lacking setExecutablePositionsLimit Validation Validation Resolved
Description
The
executeOrderfunction reverts when theexecutablePositionsLimitvalue is assigned to 0. However thesetExecutablePositionsLimitfunction does not prevent the owner address from assigning 0 as a value and therefore preventing execution of limit orders.Recommendation
Consider including validation in the
setExecutablePositionsLimitfunction to prevent the owner from causing a DoS either accidentally or maliciously.Resolution
Gamma Team: The issue was resolved in commit 5145377.
-
L-08 Low Unnecessary Repeated Minting Gas Optimization Resolved
Description
In the
handleCreateOrdersCallbackfunction the_mintFeesToHookfunction is called repeatedly in the loop for each order creation. However because the fees are all claimed for the same pool, the_mintFeesToHookfunction can be called only once with the aggregate fees to mint to save gas.Recommendation
Consider calling the
_mintFeesToHookwith the aggregated fee amount after all orders have been created.Resolution
Gamma Team: The issue was resolved in commit c67bbfe.
-
L-09 Low Orders Can Be Made For Unconnected Pools Validation Resolved
Description
In the
_createOrderfunction there is no validation that the orders being created belong to the hook address associated with the Gamma limit system.As a result positions can be created that are not executable by the hook or keeper. No other impacts have been identified other than unexpected behavior for users.
However it would be prudent to remove this capability to limit the potential attack surface area of the contracts and prevent users from creating orders for incorrect pools.
Recommendation
Consider validating that the limit hook is a part of the pool key in the
_createOrder.Resolution
Gamma Team: The issue was resolved in commit bfc3845.
-
L-10 Low Refunds Missed For Non-Native Currencies Warning Resolved
Description
The
_handleTokenTransferfunction does not perform any validation on themsg.valuein the case where the currency specified is not native. As a result if the native currency is not provided and themsg.valueis nonzero this ether amount can become stuck in the contract.Recommendation
Consider adding validation to prevent users from errantly sending
msg.valuewhen the native currency is not being used.Resolution
Gamma Team: Resolved.
-
L-11 Low Unused Errors Best Practices Resolved
Description
In the
TickLibrarylibrary, theMinimumAmountNotMetandInvalidScaleParameterserrors are defined and yet never used in that library.Recommendation
Consider removing these errors.
Resolution
Gamma Team: The issue was resolved in commit 67cfe17.
-
L-12 Low Incorrect getUserClaimableBalances Result Logical Error Resolved
Description
In the
getUserClaimableBalancesfunction thegetPositionBalancesfunction reports an amount which includes the pending unclaimed fees for the current Gamma system position deployed at that range.This amount is reported as being claimable by the user even if the user had a previously executed limit order in the same range and thus should not be receiving fees from the new position in the same range which is now present in the pool.
Notice that the
getUserProportionateFeesfunction does not early return when a user’s position has already been executed and yet to be claimed because theposState.totalLiquidityis not reset to 0 upon execution of a position and rather the position nonce is incremented.Recommendation
For positions that are no longer active use the
_getUserFeesfunction to compute the actual fees of the user which have already been claimed by Gamma and are no longer accruing in the Uniswap pool.Resolution
Gamma Team: The issue was resolved in commit 4695dbe.
-
L-13 Low Sync DoS Attack DoS Resolved
Description
The
CurrencySettler.settlefunction neglects to call the sync function for native currency payments. The sync function can be called by a malicious actor to sync an unexpected currency at any time.Therefore actions made with the
LimitOrderManagerin the Gamma system can be DoS’d by a simple call to sync a non-native currency by a malicious actor.Recommendation
Include a call to sync for the native currency in the
settlefunction.Resolution
Gamma Team: The issue was resolved in commit d9d7f54.
-
L-14 Low Follow Import Best Practices Best Practices Resolved
Description
The
LimitOrderManager.solcontract utilizes theCurrencySettler.sollibrary in order to sync and transfer tokens to thePoolManagercontract. However,CurrencySettleris imported from Uniswap's test suite.import {CurrencySettler} from "@uniswap/v4-core/test/utils/CurrencySettler.sol";It is possible that Uniswap may alter the implementation of this library without consideration of potential integrators.Recommendation
It is recommended to recreate the library within this repository to ensure consistency.
Resolution
Gamma Team: The issue was resolved in commit eda5f41.
-
L-15 Low Follow CEI Pattern When Claiming Positions Best Practices Resolved
Description
Check-Effects-Interaction pattern should be followed when claiming an order. Firstly, the pool is unlocked to transfer tokens and after the
userPositionsmapping is deleted.No reentrancy can occur due to the pool manager's inherent reentrancy protection via
PoolManager.unlock(). However, it would be a good idea to update storage prior to making the external calls.Recommendation
Move the logic to delete the user positions and removing the user position key prior to the call to
PoolManager.unlock().Resolution
Gamma Team: The issue was resolved in commit 5f3d853.
-
L-16 Low Random Users Can Spam LimitOrderClaimed Events Validation Resolved
Description
Users without active limit orders can call
claimOrder()which will complete successfully and emitLimitOrderClaimedevents. This may cause confusion or affect backend processes that index such logs.Recommendation
Validate that the user has the specified position in the
userPositionsmapping.Resolution
Gamma Team: The issue was resolved in commit e67b6e7.
-
L-17 Low Unnecessary Reads From Storage Gas Optimization Acknowledged
Description
A collection of storage reads that can be optimized: LimitOrderManager.sol:770
Recommendation
Convert from
storagetomemoryto reduce reads from storage.Resolution
Gamma Team: Acknowledged.
-
L-18 Low Integrators May Receive Unexpected Token Validation Acknowledged
Description
Calling
cancelOrder()claims the order if it has already been executed. This feature may cause problems with downstream integrations. An integrating protocol may create an order with USDC to be swapped for ETH and then cancels the order expecting to receive USDC back.However, if the position is executed prior to the call to
cancelOrder(), the integrating contract will receive back ETH. There is no guarantee on which token (or native ETH) will be received when callingcancelOrder().Recommendation
Consider adding an
expectedOutputTokenparameter to verify the token the caller expects to receive.Resolution
Gamma Team: Acknowledged.
-
L-19 Low maxOrderLimit Behaviour Differs From Comment Informational Resolved
Description
maxOrderLimitlimits the number of orders that be created increateScaleOrders()at once and not the total number of limit orders that can be active per pool like the comments abovesetMaxOrderLimit()state.Recommendation
Adjust the documentation.
Resolution
Gamma Team: The issue was resolved in commit cf1c46d.
-
L-20 Low Variable Denoted As Constant Best Practices Resolved
Description
HOOK_FEE_PERCENTAGEis denoted in all capitals, signifying a constant. However, this value can be changed by the admin.Recommendation
Consider renaming to
hook_fee_percentageto avoid confusion.Resolution
Gamma Team: The issue was resolved in commit 3221d25.
-
L-21 Low Keeper Execution DoS'd Logical Error Resolved
Description
A user can frontrun a keeper's transaction to
executeOrderByKeeperto cancel their order. Even though the position has been removed,executeOrderByKeeperstill attempts to clear the position sinceisWaitingKeeperis still set to true after cancellation.Consequently,
executeOrderByKeeperwill trigger a Uniswap revert and fail to execute all otherwaitingPositionsbecause liquidity is attempted to be removed from ticks that have already had their liquidity removed.Recommendation
Upon cancellation, ensure the position's
isWaitingKeeperstatus is set to false.Resolution
Gamma Team: The issue was resolved in commit e9a5807.
-
L-22 Low Unchecked Msg.value In Token Transfer Validation Resolved
Description
The vulnerability arises in the
_handleTokenTransferfunction of the smart contract, which is responsible for handling token transfers based on theisToken0flag.When
isToken0is false, the function is designed to transfertoken1(a non-native token) using thetransferFrommethod of theIERC20Minimalinterface. However, the function fails to validate whethermsg.valueis zero in this scenario.If a user mistakenly sends Ether while calling this function with
isToken0set to false, the Ether will be accepted by the contract but not utilized in any way. This occurs because the function does not enforce a check to ensure thatmsg.valueis zero when dealing with non-native tokens.As a result, the Ether remains trapped in the contract, with no mechanism for the user to recover it.
Recommendation
To mitigate this vulnerability, the
_handleTokenTransferfunction should include a validation step to ensure thatmsg.valueis zero whenisToken0is false.Resolution
Gamma Team: Resolved.
-
L-23 Low Max Limit Can Prevent Scale Orders Validation Resolved
Description
The owner is able to set the
maxOrderLimitwith functionsetMaxOrderLimit. If themaxOrderLimit =1, all scale orders will be prevented because scale orders require a minimum of 2 orders:if(totalOrders < 2) revert MinimumTwoOrders();Recommendation
Be aware of this behavior or add validation within
setMaxOrderLimitthat the_limitis greater than 1.Resolution
Gamma Team: The issue was resolved in commit 90bb9d0.
-
L-24 Low Return Value Of transferFrom() Not Checked Best Practices Resolved
Description
Not all
ERC20implementationsrevert()when there's a failure intransfer()/transferFrom(). The function signature has a boolean return value and they indicate errors that way instead.By not checking the return value, operations that should have marked as failed, may potentially go through without actually making a payment.
IERC20Minimal(Currency.unwrap(key.currency0)).transferFrom(msg.sender, address(this), amount); IERC20Minimal(Currency.unwrap(key.currency1)).transferFrom(msg.sender, address(this), amount);
Recommendation
Consider using the
SafeTransferlibrary for transferringERC20tokens.Resolution
Gamma Team: The issue was resolved in commit 669e488.
-
L-25 Low _executePosition Does Not Reset isWaitingKeeper Unexpected Behavior Resolved
Description
The
_findOverlappingPositionsfunction does not skip positions which have been marked with atruevalue forisWaitingKeeper. As a result these positions can be executed normally when price re-crosses their upper boundary.When this scenario is hit the position is executed, however the
positionStateentry is left with atruevalue forisWaitingKeeper.The nonce of the position is incremented however, so the impact is limited to consumers of the
LimitOrderManagerstate which may be confused by the false reporting ofisWaitingKeeper.Recommendation
Consider if the
isWaitingKeeperpositions should be skipped in the_findOverlappingPositionsfunction. If not, then ensure that theisWaitingKeepervalue is set tofalseafter the execution of a position that was previously flagged for keeper execution.Resolution
Gamma Team: The issue was resolved in commit 983c00e.
-
L-26 Low Min/Max Tick Validation In Scale Orders Validation Resolved
Description
The
minUsableTickandmaxUsableTickvalues are defined as limits beyond which orders should not be placed. This validation is implemented in thecreateLimitOrder()function, but it is absent in thecreateScaleOrders()function.As a result, scale orders can be placed beyond the acceptable
min/maxtick range, which is not intended. This oversight could lead to invalid order placements, potentially disrupting the expected functionality and behavior.Recommendation
A check for
minUsableTickandmaxUsableTickshould be added in thecreateScaleOrders()function, similar to the implementation in thecreateLimitOrder()function. This will ensure that scale orders are only placed within the defined tick range.Resolution
Gamma Team: The issue was resolved in commit 7d342cb.
-
L-27 Low Use nonReentrant() Modifier Best Practices Resolved
Description
In the
_handleTokenTransferfunction, specifically whenisToken0is true and the native token (Ether) is being handled. In this scenario, the function checks if the sent Ether (msg.value) is sufficient to cover the required amount.If excess Ether is sent, the function refunds the difference to the sender using a low-level call. While the current implementation does not expose an immediate reentrancy risk , the use of call without a reentrancy guard violates best practices.
If the contract's logic is modified in the future or if the call is replaced with a more complex operation, it could create a reentrancy vulnerability.
Recommendation
To mitigate this,
nonReentrantmodifier fromOpenZeppelin'sReentrancyGuardcontract should be used.Resolution
Gamma Team: The issue was resolved in commit a5dd43f.
-
L-28 Low Incorrect Current Nonce On Position Logical Error Resolved
Description
function positionStateaims to return allPositionStateinformation, including the position'scurrentNonceused. However,currentNonceis never set in thepositionStatemapping upon order creation, hence it will always be 0 regardless of the actual nonce used for the position.Although
PositionState.currentNonceis not utilized within the contract, it can potentially impact external systems reading the position's incorrect nonce and confuse users.For example. a system may display an executed, inactive order with nonce 0, and simultaneously have an unexecuted order over the same tick range also with nonce 0.
Recommendation
Set the position's current nonce upon order creation.
Resolution
Gamma Team: The issue was resolved in commit de3c848.
No findings match.
Invariants 29
The review's fuzzing suite asserted 29 invariants. 25 held and 4 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
GLOB-01 | positionTickRangeList ranges have to be sorted in ascending order of bottom tick | Held |
GLOB-02 | The sum of user fees for a position never exceeds the total fees | Held |
GLOB-03 | All position ticks are within the valid range | Held |
GLOB-04 | At all times, calling _findOverlappingPositions should always return all of the positions which | Broken |
GLOB-05 | are executable by the tickAfterSwap In all positions, bottomTick must be strictly less than topTick | Held |
GLOB-06 | In all positions, bottomTick must be greater than or equal to minUsableTick(tickSpacing) | Held |
GLOB-07 | In al positions, topTick must be less than or equal to maxUsableTick(tickSpacing) | Held |
GLOB-08 | Top and bottom ticks must be rounded according to tickSpacing | Held |
GLOB-09 | getUserFees should accurately return each user's fair share of fees | Broken |
GLOB-10 | Position's total liquidity equals the sum of all user liquidities | Broken |
GLOB-11 | Order can be executed only when the order is sold completely | Broken |
GLOB-12 | A position cannot be simultaneously active and have claimable principal | Held |
GLOB-13 | After execution, a position should be removed from `positionTickRangeList` | Held |
GLOB-14 | Token0 positions (isToken0=true) should only have token1 principal and vice versa | Held |
GLOB-15 | When the last contributor to a position removes their liquidity, the position is | Held |
GLOB-16 | removed from positionTickRangeList The positionContributors set ensures each user is counted only once per position | Held |
GLOB-17 | An order that is executed should not be able to be executed again. | Held |
GLOB-18 | An order that is executed should always have >=1 positionContributors | Held |
CREATE-01 | No limit order is created with an amount less than the minimum amount | Held |
CREATE-02 | T0: currentTick must be strictly less than targetTick | Held |
CREATE-03 | T0: The difference between ticks must be at least tickSpacing | Held |
CREATE-04 | T1: currentTick must be strictly greater than targetTick | Held |
CREATE-05 | T1: The difference between ticks must be at least tickSpacing | Held |
CREATE-06 | T0: bottomTick must be strictly less than upperTick | Held |
CREATE-07 | T0: The difference between ticks must be at least tickSpacing | Held |
CREATE-08 | T1: upperTick must be strictly greater than bottomTick | Held |
CREATE-09 | T1: The difference between ticks must be at least tickSpacing | Held |
CANCEL-01 | Token0 and Token1 balances of User after claim and cancel should not differ | Held |
CANCEL-02 | After cancellation, a position must either have claimable principal or be fully claimed | Held |
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 -
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
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.
