Gamma Strategies engaged Guardian to review the security of its GMX V2 perpetual vault. From the 1st of August to the 15th of August, a team of 6 auditors reviewed the source code in scope.
- Published
- Review window
- August 1 to 15, 2024
- Language
- Solidity
- Chains
- Arbitrum
- Sector
- Yield and vaults
- 4 Critical
- 14 High
- 12 Medium
- 10 Low
- 0 Informational
Scope
Overview
Gamma Strategies engaged Guardian to review the security of its GMX V2 perpetual vault. From the 1st of August to the 15th of August, a team of 6 auditors reviewed the source code in scope.
Issues Detected Throughout the engagement 18 High/Critical issues were uncovered and promptly remediated by the Gamma team. Several issues impacted the fundamental behavior of the protocol, following their remediation Guardian believes the protocol to uphold the main functionality described for the vault.
Security Recommendation Given the number of High and Critical issues detected, Guardian supports an independent security review of the protocol at a finalized frozen commit. Furthermore, the Gamma team should increase testing with a variety of callback scenarios, such as ADL and liquidations.
Findings 40
-
C-01 Critical DoS Attack In Withdraw Function DoS Resolved
Description
The
withdrawfunction in thePerpetualVaultcontract allows users to specify a recipient when withdrawing collateral.The internal
_withdrawfunction is then called to withdraw funds from GMX if the position is open. Once withdrawn, the collateral tokens are transferred to the user-specified recipient address to complete the withdrawal flow. No other actions are permitted until this process is completed.This allows a malicious actor to perform the following attack:
- Call the
withdrawfunction with the recipient set to a blacklisted USDC address. - The
afterOrderExecutionfunction in theGmxUtilscontract will revert when trying to send the
collateral (USDC) to the blacklisted address.
- The flow variable will still be 3 (WITHDRAW) and
_gmxLockset to true, preventing any further
actions.
This effectively causes a DOS attack by locking the contract in a state where no further actions can be taken.
Recommendation
Do not transfer tokens directly to the recipient, instead make the token claimable for them in a separate transaction and store their claimable balance in a mapping.
Resolution
Gamma Team: The issue was resolved in commit a557f1d.
- Call the
-
C-02 Critical Missing Access Control On Settle Function Access Control Resolved
Description
Proof of concept: PoC
A user is able to arbitrarily call
settleand overwrite any order that is in the queue. By doing this, the user will not receive funds from their withdrawal.An attacker could easily time this to repeatedly cause users to have their withdrawn funds locked in the
PerpetualVaultcontract.Recommendation
Implement access control to the function so that it can only be called from the
PerpetualVaultcontract.Resolution
Gamma Team: The issue was resolved in commit b4022d5.
-
C-03 Critical Callback Check Bypassed Via Self Liquidation Access Control Resolved
Description
GMX allows users to set their callback contract in case of liquidation through the
setSavedCallbackContractfunction.function setSavedCallbackContract(address market,address callbackContract) external payable
nonReentrantThis feature can be leveraged to bypass the
validCallbackfunction because the order is a liquidation whichGMXUtilswill not prevent.An attacker would do so by executing the following attack:
- Attacker will set the callback contract to
GMXUtils - Open small highly leveraged position
- Attacker gets liquidated and the
GMXUtilscontractsafterOrderExecutionwill be called queuewill be deleted, removing the pending order.PerpetualVaultcontractsafterOrderExecutionwill then be called and all conditionals will be skipped
leaving the protocol in a flow state
- Protocol is bricked as it is stuck in a flow state, and no pending deposits/withdrawals.
The result of this attack would be DoS of the protocol as well as a lost order from the deleted queue.
Recommendation
Validate that the liquidation is for the vault
positionKeyrather than any arbitrary position in thevalidCallbackmodifier.Resolution
Gamma Team: The issue was resolved in commit 57b3fae. 14
- Attacker will set the callback contract to
-
C-04 Critical Users Avoid Losses From GMX Logical Error Resolved
Description
In the perpetual vault when withdrawing value from the vault there is no accounting for the negative
PnLthat a GMX perpetual position may have.Instead the user’s decrease order contains the
collateralDeltaAmountandsizeDeltaInUsdamounts that are proportional to the shares the user holds.The
sizeDeltaInUsdvalue will indeed realize the proportion of the negativePnLthat the user owns, however this negativePnLamount will be deducted from the position’s collateral that remains, and not from thecollateralDeltaAmountwhich will be rewarded to the withdrawer.As a result withdrawers avoid losses and disburse them onto other vault holders.
Recommendation
Account for the
PnLof a position that will be realized when computing thecollateralDeltaAmountfor a withdrawal.Resolution
Gamma Team: The issue was resolved in commit 94347b8.
-
H-01 High cancelDeposit Not Implemented In KeeperProxy Logical Error Resolved
Description
The
cancelDepositfunction has anonlyKeepermodifier that restricts its execution to theKeeperProxycontract.However, the
KeeperProxydoes not implement the necessary functionality to call this function, making it currently impossible to invoke.The
cancelDepositfunction may be needed to cancel a reverting deposit to GMX, making it essential for the system's proper operation.Recommendation
Implement a function in the
KeeperProxycontract to call thecancelDepositfunction.Resolution
Gamma Team: The issue was resolved in commit 24f5790.
-
H-02 High cancelDeposit Missing totalDepositAmt Deduction Logical Error Resolved
Description
The
cancelDepositfunction allows the protocol to cancel an ongoing deposit, resetting the state and deleting the deposit request.However, it currently forgets to deduct the deposit request's amount from the
totalDepositAmountstate variable. This variable is used to ensure that the deposited amount within the protocol remains under the deposit cap.Consequently, the missing deduction in the
cancelDepositfunction will lead to an overestimation of the total deposits. Every timecancelDepositis called, the remaining possible deposit amount (maxDepositAmount - totalDepositAmount) will be incorrectly reduced by the cancelled amount.This will incorrectly limit deposits, affecting the vault strategy and, in the worst case, could cause a DOS for the entire vault.
Recommendation
Ensure that the
cancelDepositfunction deducts the cancelled deposits amount from thetotalDepositAmountstate variable.Resolution
Gamma Team: The issue was resolved in commit 5c40cff.
-
H-03 High Long Funding Is Never Claimed Logical Error Resolved
Description
When the
afterOrderExecutionfunction is called, Gamma has the opportunity to collect funding fees for both the long and short tokens.It is important for Gamma to claim these fees, otherwise they will remain in GXM. However, only the short funding fee is currently being claimed (
tokens[0] = order.addresses.initialCollateralToken).This issue means that some funding fees will go unclaimed, resulting in the expected yield not being distributed to users as intended.
Recommendation
Consider claiming both long and short funding fees.
Resolution
Gamma Team: The issue was resolved in commit f2008b6.
-
H-04 High Unhandled ADL Case Can Lead To Stuck Funds Logical Error Partially resolved
Description
The
GmxUtilscontract doesn’t account for Automatic Deleveraging (ADL) from GMX, which can partially or fully close profitable positions if pending profits exceed the market's threshold.During an ADL event, the receiver variable is set to the account address, which in this case is the
GmxUtilscontract. Since theGmxUtilscontract doesn’t set asavedCallbackContract(which is used as the callback contract for ADLs), the callback address will default to the zero address.As a result, the ADL callback will not be triggered, meaning the ADL event won't be properly handled. This causes any funds sent to the
GmxUtilscontract to become stuck with no way to retrieve them.Additionally, these funds will no longer be included in share calculations, leading to discrepancies in share distribution and vault accounting, as they aren’t in the
PerpetualVaultcontract or the GMX position.If the position is closed, further calls to GMX, such as withdrawal requests, could lead to a denial of service (DOS) because the
PerpetualVaultis incorrectly marked as open while the GMX position is closed, causing those calls to repeatedly fail.Recommendation
The
GmxUtilscontract should set thesavedCallbackContractso theafterOrderExecutionfunction in theGmxUtilscontract is called during ADLs or liquidations. Additionally thevalidCallbackmodifier should allow theAdlHandlerto call theGmxUtilscontract for the currentpositionKey. TheafterOrderExecutionfunction should also account for ADL calls from GMX.This update should include a mechanism to transfer any funds sent to the
GmxUtilscontract due to ADL to thePerpetualVaultcontract. Additionally, theafterOrderExecutionfunction in thePerpetualVaultcontract should check if the position is still open and handle the position decrease accordingly.If the position is closed, it should adjust the flow to reflect that closure. It is also important to consider the various flows (deposit, signal change, withdraw, compound) that the vault can be in during the
PerpetualVaultcallback.Resolution
Gamma Team: Will handle liquidation risk via offchain logic. 19
-
H-05 High Decrease Orders Can Output 2 Tokens Logical Error Resolved
Description
The
afterOrderExecutionfunction in theGmxUtilscontract assumes that only one output token will be returned. However, GMX decrease position orders can output two tokens instead of a single token if the decrease position swap fails.As a result, this scenario is not properly accounted for in the
_handleReturnfunction, which is responsible for calculating the user's withdrawal amounts and updating global states. This can result in users receiving less than they should and can corrupt the accounting.Recommendation
Update the
afterOrderExecutionfunction in theGmxUtilscontract to read and pass down thesecondaryOutputTokenandsecondaryOutputAmountin the same way as theoutputTokenandoutputAmount.Then, modify the
_handleReturnfunction in thePerpetualVaultcontract to account for both tokens.Resolution
Gamma Team: The issue was resolved in commit f2008b6.
-
H-06 High Missing Liquidation Callback Case Logical Error Partially resolved
Description
Proof of concept: PoC
During a liquidation, the receiver variable is set to the account address, which in this case is the
GmxUtilscontract. Since theGmxUtilscontract doesn’t set asavedCallbackContract(which is used as the callback contract for liquidations), the callback address will default to the zero address.As a result, the callback will not be triggered, meaning the
afterOrderExecutionfunction in theGmxUtilscontract, which is meant to send the liquidated funds to theperpetualVaultcontract won’t be triggered. This causes the funds sent to theGmxUtilscontract to become stuck with no way to retrieve them.Additionally, interacting with the protocol after said liquidation can lead to DoS's and incorrect fund and share allocation. This is in part because liquidations will close the position which results in withdraw attempts failing when the settle order executes.
After the failed execution Gamma will retry the settle over and over failing each time. Deposits also present a special case where the deposit may underflow
_totalAmount(marketPrices)could be less than amount:totalAmountBefore = _totalAmount(marketPrices) - amount;Recommendation
The
GmxUtilscontract should set thesavedCallbackContractso theafterOrderExecutionfunction in theGmxUtilscontract is called during ADLs or liquidations.Additionally, consider handling the liquidation case where after funds are sent back to the
perpetualVaultthere are specific actions taken to get the protocol to a state where DOS's will not occur. This includes updating the state so that Gamma has the position as closed. It also includes preparing the state for any upcoming or incoming deposits/withdrawals.Set
positionIsClosedtotrueandcurPositionKeytobytes32(0)inafterOrderExecution(). Also, configure the callback viasetSavedCallback(), and expect the call for liquidations to originate from GMX’sLiquidationHandlercontract invalidCallback().Resolution
Gamma Team: Liquidation issue; will handle liquidation risk via offchain logic. 21
-
H-07 High Users Can Be Charged Extra Fees Logical Error Resolved
Description
When creating an increase order for a deposit the
amountInis assigned to thecollateralTokenbalance of the vault, however the user may have only deposited a portion of these tokens.Indeed when a user receives shares for their deposited amount, the minted shares are computed only based on the
depositInfo[counter].amountand does not include any additional balance that may have been used to create the order.However the user pays fees for the balance which was not a direct part of their deposit. A malicious actor may leverage this to cause the
afterOrderExecutionexecution to revert by forcing thefeeAmountto be larger than thedepositInfo[counter].amount.Consider the following example:
- The vault holds a 5x Long position in GMX
- User A deposits the minimum deposit amount of 10 USDC
- The
positionFeeFactorfee in GMX is 30 basis points - User A then donates 990 USDC to the vault before the next action is run
- The keeper runs the next action and initiates a GMX increase order for $1,000 * 5x leverage =
$5,000
sizeDeltaUsd- The order is executed and the order fee is 0.003 * 5,000 = $15
- The $15 fee is larger than the user’s actual deposit amount which is stored in the
depositInfo[counter].amount- An underflow occurs when attempting to compute the value to mint based on:
depositInfo[counter].amount - feeAmount= 10 - 15As a result the active increase order is not recorded as completing in Gamma’s vault and the deposit flow is never completed, locking all user deposits. A malicious actor only needs to spend $1,000 in this example and could profit off of this activity by short-selling the GAMMA token and spreading negative press about the loss of funds for users.
Recommendation
Consider only charging the fee that the user’s deposit amount would directly be responsible for when determining the user’s shares amount. Otherwise consider only creating an increase order for the amount of USDC that the user deposited rather than the entire vault balance.
Resolution
Gamma Team: The issue was resolved in commit 97159d6.
-
H-08 High Disproportional Share Allocation Logical Error Partially resolved
Description
In the
afterOrderExecutionfunction the amount of shares minted to a depositor is dependent on thesizeDeltaUsdof the order. However this method ignores price impact which the vault position experiences due to the increase order.This allows depositors to receive more shares of the vault than they ought to when their order is negatively impacted.
For example:
- Vault is 2x long on GMX with $100,000
sizeInUsd - PnL is currently 0 and the vault position is worth $50,000
- User A deposits with $5,000 and the sizeInUsd for their order is $10,000
- User A’s order is negatively impacted such that it experiences a $1,000 loss relative to the current
market price
- Ignoring fees, User A is credited with their 5,000
collateralTokendeposit for share calculations - The vault worth excluding the trader’s collateral is $49,000 as the negative price impact is
attributed
- The trader receives $5,000/$49,000 ~= 20.41% of the share supply
- Instead the trader should have received $4,000/$50,000 ~= 18% of the share supply by rightfully
charging the impact to the depositor
Recommendation
Compute the amount of price impact that the depositor experienced by comparing the
sizeInUsdof the order to thesizeInTokensincrease of the position with respect to the current market prices in GMX.Then deduct this amount from the user’s credited deposit value when calling the_mintfunction.Resolution
Gamma Team: The issue was resolved in commit cb4ae42.
Guardian Team: The share calculation has been adjusted to account for the difference between the balance before and after. This modification will more accurately distribute shares, although there may still be a disproportionate distribution of shares in certain cases due to price fluctuations 23 between creation and execution.
- Vault is 2x long on GMX with $100,000
-
H-09 High Asset Transfer During Liquidation Can Fail Logical Error Resolved
Description
During a liquidation, the
afterOrderExecutionfunction in theGmxUtilscontract is responsible for sending thequeue.tokenInbalance from theGmxUtilscontract to thePerpetualVaultcontract.The problem is because the
queue.tokenInvariable is set during thecreateOrderfunction’s specifically for aMarketIncreaseorder and is deleted in theafterOrderExecutionorafterOrderCancellationfunctions.If a liquidation occurs when there is no
MarketIncreaseorder in progress, thequeue.tokenInvariable will be set to the zero address, causing the transfer of funds during liquidation to fail.As a result, these funds in the
GmxUtilscontract become stuck with no way to retrieve them, leading to losses for users, as they are not included in thePerpetualVaultor GMX position and will not be accounted for in share calculations.Recommendation
The
outputTokenshould be used from the eventData instead, similar to how it's done in theafterOrderExecutionfunction. This ensures that the correct token address is used.Resolution
Gamma Team: Liquidation issue; will handle liquidation risk via offchain logic
-
H-10 High Price Impact Cap On DecreasePosition Logical Error Resolved
Description
GMX can cap negative price impact on decrease orders, leaving the excess amount in a claimable collateral pool that must be manually claimed using the
ExchangeRouter.claimCollateralfunction.This scenario is not currently handled, meaning that if it occurs during a user withdrawal, the user could receive less than they should have.
Additionally, the
GmxUtilscontract does not currently implement theclaimCollateralfunction, which means those funds cannot be retrieved, leading to potential losses for users.Recommendation
Update the
GmxUtilscontract to implement theclaimCollateralfunction, ensuring that any excess funds due to capped negative price impact can be claimed and properly accounted for.Additionally, modify the withdrawal process to handle this scenario, ensuring users receive the full amount they are entitled to, even when negative price impact is capped.
Resolution
Gamma Team: The issue was resolved in commit 018ab42.
-
H-11 High Retrying Actions Causes DoS DoS Partially resolved
Description
When an attempted order is canceled by GMX, the protocol chooses to handle this scenario by attempting to retry the previously attempted action.
However, there are numerous reasons why the transaction may fail again. For instance, if the reserve ratio or open interest is out of balance for a market, if a market or action is disabled, or if GMX triggers Auto Deleveraging.
A malicious user could even force this to occur by depositing, withdrawing, or swapping on GMX to attain one of these states. When this occurs Gamma’s flow will be stuck in its current state, and no other actions will be executable.
Recommendation
Reset the transaction flow, and require the user or keeper to reattempt the action again. In the case of deposits, make sure that amount is refunded.
Also consider adding additional admin privileged functions to terminate a flow, similar to
cancelDeposit().Resolution
Gamma Team: The issue was resolved in commit 15f3967.
Guardian Team: There may still be DOS when actions are forced to repeatedly retry. Consider implementing the entire recommendation.
-
H-12 High Vault Liquidated By Malicious Depositor Gaming Resolved
Description
A malicious actor may observe that Gamma currently has a high leverage and intentionally push this leverage up to force the
PerpetualVaultposition to be liquidated. A user may do this by creating large deposits and then initiating withdrawals for those deposits as soon as possible to levy theMarketDecreaseorder position fee on the vault position.Users do not pay for the
MarketDecreaseorder position fee and thus this fee is deducted from the remaining position thus changing the leverage of the remaining position. Once this has been repeated several times the vault position will have a very high leverage which makes it liquidatable almost instantaneously.The malicious actor can profit off of this liquidation by setting a limit order which can only be triggered once the liquidation has occurred and the
priceImpactallows for immediate profit.Recommendation
Adjust the
initialCollateralDeltaamount for the Position fees that will be experienced for theMarketDecreaseorder.This way the
withdrawertakes on the burden of the fee and the vault position leverage cannot be manipulated.Resolution
Gamma Team: The issue was resolved in commit f03e60e.
-
H-13 High cancelDeposit Misses swapProgressData Logical Error Acknowledged
Description
In the
cancelDepositfunction there is no accounting for theswapProgressData, this causes several issues when a deposit is cancelled after a composite swap through Paraswap and GMX where the GMX swap failed.Firstly the transfer of collateral tokens is likely to fail as part of the deposited collateral tokens will have been swapped to the index token and are recorded in the
swapProgressData.swappedvalue.safeTransferis not used for this transfer and therefore depending on thecollateralTokenused, users could lose their funds as a result.Secondly the
swapProgressData.swappedvalue is not cleared when the deposit occurs, therefore the next deposit will be credited with receiving these swapped tokens.Therefore if the vault did have enough collateral tokens to pay out the user, this swapped value would then be double counted and awarded to the next depositor at the expense of previous vault shareholders.
Recommendation
Account for the
swapProgressData.swappedvalue in thecancelDepositfunction such that if theswapProgressData.swappedvalue is nonzero that amount of index tokens is transferred to the user and deducted from the amount of collateral tokens transferred.Then clear the
swapProgressDataat the end of the function. Additionally, usesafeTransferin thecancelDepositfunction.Resolution
Gamma Team:
cancelDepositfunction is allowed to call whenrunNextAction()fails. Not used to call afterrunNextAction. -
H-14 High Withdrawers Lose Funds Due To Collateral Check Logical Error Partially resolved
Description
In GMX if a decrease order is deemed to leave the remaining position with insufficient collateral for its open interest, then the
initialCollateralDeltais reassigned to 0.This will result in withdrawals which are deemed to leave behind insufficient collateral via the
willPositionCollateralBeSufficientcheck in receiving no collateral tokens out from GMX.Therefore,
withdrawerswhich are affected by this case lose all collateral and will only receive a portion of any profit which was gained from the position.Recommendation
Validate that the
willPositionCollateralBeSufficientcheck in GMX will not be triggered upon creating withdrawals from GMX, and if it is triggered upon execution of a decrease order for a withdrawal, adjust the user’s shares such that they can claim their portion of the collateral that did not come out of the decrease order.Resolution
Gamma Team: The issue was resolved in commit 75723b7.
Guardian Team: Only assign the
sizeDeltaInUsdto the entire position size if thewillCollateralBeSufficientcheck is false. And additionally be sure to carefully account for the user only receiving the funds out of the order which they should when this happens. -
M-01 Medium Hardcoded GMX Address Can Lead To DoS Configuration Acknowledged
Description
The
GmxUtilscontract uses thegExchangeRouterand reader contracts, saving their addresses as constants during deployment.However, it is recommended by GMX to allow these addresses to be changeable (not immutable) and to provide setter functions.
These contracts are currently used in essential functions by the protocol, meaning if they were to be changed, the protocol will not function correctly until the addresses are updated through an upgrade.
Recommendation
Reference GMX's datastore and use the address that is stored their for applicable GMX addresses.
Resolution
Gamma Team: We use TransparentProxy so we can upgrade it later. We will take account for that later.
-
M-02 Medium Missing Feature Execution Validation DoS Resolved
Description
Orders can become stuck halting protocol functionality when order execution is disabled. When an order is created GMX will check that the create order feature is enabled.If it is not, it will revert.
However, during order creation there is no check that the order execution feature will be disabled.
This means that when order execution is disabled Gamma orders will still successfully be created but will not execute due to the feature validation in
_executeOrderuntil the feature is enabled again. Halting protocol functionality for a period of time.Recommendation
Before sending an order to GMX check that the execution feature is enabled.
Resolution
Gamma Team: The issue was resolved in commit 52e272d.
-
M-03 Medium Deprecated latestAnswer Used In _check Function Logical Error Resolved
Description
The
_check functionin theKeeperProxycontract checks the price difference between the given price and theChainlinkprice.The issue is that the
Chainlinkprice is fetched using thelatestAnswerfunction, which is deprecated and should no longer be used reference.Recommendation
The
_check functionshould instead use thelatestRoundDatafunction for price retrieval, as it allows for additional validations to ensure that the price data is current and reliable.Refer to the Chainlink documentation for implementation details.
Resolution
Gamma Team: The issue was resolved in commit c863800.
-
M-04 Medium Chainlink Price Lacks Price/Sequencer Validation Validation Resolved
Description
The
_checkfunction in theKeeperProxycontract retrieves theChainlinkprice; however, the price is fetched and used without performing any validation on the price or the sequencer status.Chainlinkrecommends following certain security practices, such as checking for stale or invalid prices.Additionally, when using
Chainlink with L2 chains like Arbitrum, it is crucial to check whether the L2 Sequencer is down.Recommendation
Add validation checks to ensure the price retrieved by the
_checkfunction is not stale or invalid Ref.Also, implement sequencer feed checks to confirm the L2 Sequencer is operational before using the price data.
Follow the
Chainlinkexample for these checks as outlined in their documentation.Resolution
Gamma Team: The issue was resolved in commit c863800.
-
M-05 Medium Excess executionFee Not Refunded Logical Error Resolved
Description
In
withdraw(),_payExecutionFee()is called, which validates themsg.valueis large enough to cover GMX’s execution cost.However, if a user goes to withdraw when there are no open positions, there will be no calls made to GMX.
Additionally, when calls to GMX are made, any excess execution fee refund by GMX is not returned to the user. This can occur when a user calls
deposit()orwithdraw().Recommendation
Move the
_payExecutionFeecall into thecurPositionKey != bytes32(0)case for withdrawing.Additionally, implement functionality to refund excess GMX execution fees to the creator of the deposit or withdrawal.
Resolution
Gamma Team: The issue was resolved in commit 1087920.
-
M-06 Medium DoS When Orders Are Stuck In GMX DoS Resolved
Description
GMX keepers will not always execute orders in a reasonable amount of time. For reasons outside of the creators control.
With no functionality to manually cancel these orders an order can be sitting for a prolonged period of time halting any other order operations.
Recommendation
Consider adding functionality for keepers to cancel orders after a certain amount of time.
Resolution
Gamma Team: The issue was resolved in commit 15f3967.
-
M-07 Medium Gas Estimation Uses Old Key Logical Error Resolved
Description
In the
getExecutionGasLimitfunction theESTIMATED_GAS_FEE_BASE_AMOUNTkey is used to estimate the gas.However with the upgrade of GMX V2.1 the
ESTIMATED_GAS_FEE_BASE_AMOUNT_V2_1key is now used to validate theexecutionFeebase amount upon creating an order.Recommendation
Use the updated
ESTIMATED_GAS_FEE_BASE_AMOUNT_V2_1key in thegetExecutionGasLimitfunction.Resolution
Gamma Team: The issue was resolved in commit b4022d5.
-
M-08 Medium Liquidations Don't Account For longTokens Logical Error Resolved
Description
It is possible for a liquidation to send a long token from GMX. For instance, a liquidation can occur when the position was in profit, but fees caused a liquidation.
Alternatively, if GMX failed to swap into the collateral token, you can receive the long token. If this were to occur, the token would be stuck within
GMXUtils.sol, and unrescuable.Recommendation
Check if the long token was sent with a liquidation, and if so transfer it to
PerpVault.sol. Additionally, consider adding an admin privileged function to rescue unexpected token transfers.Resolution
Gamma Team: Liquidation issue; will handle liquidation risk via offchain logic
-
M-09 Medium Execution Fee Required For Swaps Logical Error Partially resolved
Description
In the deposit and withdraw functions the
_payExecutionFeefunction is called regardless of if the action will require a GMX order.For example, if the vault is 1x long and a paraswap swap will be used to execute the swap, the execution fee for a GMX swap is still collected.
This unnecessarily charges the user for an action that will not occur and offers them no refund in the event that a normal swap is used.
Recommendation
Consider refunding the
executionFeeto the user if a GMX action is not performed on a swap.Resolution
Gamma Team: The issue was resolved in commit 8a10486.
Guardian Team: If the intention is to only refund the execution fee when it is unused then consider refunding the full execution fee when there is sufficient funds.
-
M-10 Medium Keeper Actions Can Be Stalled By Deposits DoS Resolved
Description
The protocol only allows one action to be processed at a time and depending on the action and circumstances each execution can take up to minutes to be finalized.
As a result a malicious actor could intentionally create many small positions when the vault has no open position, and then when the vault has an open GMX position start triggering withdrawals from each account to DoS the protocol for an extended period.
If each action takes 1 minute to process and the
minimumDepositamount is $10 then an attacker can DoS the protocol for one hour with $600 + gas. The attacker would recoup the majority of this upfront cost after they withdraw all shares.This cost may be profitable if the actor were a GM holder wanting to keep Gamma in a poor position for longer to reap PnL gains.
Additionally a short-seller may be able to profit from such a DoS, spreading word of the trapped funds.
Recommendation
Consider batching deposits and withdrawals to GMX to avoid DoS’s triggered by small individual accounts. Otherwise assign the minimum deposit value such that this attack is unprofitable.
Resolution
Gamma Team: Will set
minDepositat $1,000 upon initialization. -
M-11 Medium setPerpVault Lacks Validation Access Control Acknowledged
Description
setPerpVault()will initializeperpVault, and then will revert anytime it is called again. If a user monitors deployment and callssetPerpVault(), they will be able to setperpVault.This will give a user control over execution flow for numerous function calls, if not dealt with. You could redeploy the contracts again, but you would be at risk of the same attack repeating itself.
Recommendation
Make
setPerpVault()an access controlled function.Resolution
Gamma Team: Acknowledged. There's no reason that the attacker does that action.
-
M-12 Medium Oracle Price Counts Ignored In Gas Estimation Logical Error Resolved
Description
In the
GMXUtilscontract, when creating orders for GMX the Oracle price count is not accounted for when determining the size of theexecutionFee.However in GMX V2.1 an additional amount of
executionFeeis necessary to account for the number of oracle prices to execute the order.This logic is seen here:
https://github.com/gmx-io/gmx-synthetics/blob/1938e365dc009342aa288aa6b42fc1fd3cd9e45d/c
Recommendation
Include the oracle price count estimated costs in the execution fee sent to GMX.
Resolution
Gamma Team: The issue was resolved in commit b4022d5.
-
L-01 Low Protocol Fee Does Not Count Secondary Token Logical Error Resolved
Description
When the outputted amount is in both the long and the short underlying token, the
amountvariable will be less than expected.Causing the governance fee to be less than expected or potentially no fee when a position was actually in profit.
Recommendation
Consider accounting for the long token when determining how much profit a user is in for governance fees.
Or Document to members of the governance team that some profitable positions may generate less yield than expected.
Resolution
Gamma Team: The issue was resolved in commit f2008b6.
-
L-02 Low Users Always Charged Negative Impact Fees Documentation Resolved
Description
When computing the
feeAmountwhich a user pays theforPositiveImpactishardcodedas false, therefore even if a user’s order positively impacted the GM market skew and received a lower fee rate the user has to pay the higher rate when Gamma accounts for their orders fee.Recommendation
This behavior is likely more trouble to fix than it is worth, but be aware of this disagreement between the fees charged by the two systems and clearly document this for users.
Resolution
Gamma Team: This behavior is likely more trouble to fix than it’s worth, so we will let users know about that. Always pay fee for negative price impact case.
-
L-03 Low Redundant Boolean Expressions Optimization Resolved
Description
Throughout the contracts the boolean comparison
== trueis used to check if a boolean value is true. However the boolean value itself can be used to save gas.Recommendation
Remove all instances of
== true.Resolution
Gamma Team: The issue was resolved in commit a3f8827.
-
L-04 Low Run Can Be Stalled Via Acceptable Price Logical Error Resolved
Description
A strict acceptable price can lead to a
runfailing. It would fail because of the position that is being closed is large enough that the price impact would exceed the threshold. This can happen naturally as the positions get larger and larger. This can also happen as an attack.Since a user would be able to know roughly when a
runis going to occur they can either open a position right before Gamma which would increase the price impact gamma will face. Or the attacker could withdraw from GMX, with less capital in the market newly created positions will face greater price impact.By preventing the run from executing the rest of the protocol will not be able to be used for a period of time. And more importantly the protocol will be forced to hold a position that does not align with their strategy since they won’t be able to close or change it.
Recommendation
On
runsconsider passing in anacceptablePrice. This flexibility would allow position changes despite market conditions.Also consider allowing the acceptable price to be changed when the
afterOrderCancellationis called. Preventing situations where the order repeatedly fails.Resolution
Gamma Team: The issue was resolved in commit 5f387ce.
-
L-05 Low cancelDeposit Doesn’t Update Mapping Logical Error Resolved
Description
The
cancelDepositfunction allows the protocol to cancel a deposit request, but it currently doesn’t fully remove the deposit as it forgets to remove the entry from theuserDepositsmapping.As a result, the
getUserDepositsfunction, which retrieves all deposit IDs for a user, will still include the cancelled deposit.Recommendation
Ensure the cancelled deposit is removed from the
userDepositsmapping, similar to how it is handled in the_burnfunction.Resolution
Gamma Team: The issue was resolved in commit 5c40cff.
-
L-06 Low cancelDeposit Function Should Use safeTransfer Logical Error Resolved
Description
The
cancelDepositfunction allows the protocol to cancel a deposit request, and the user's deposited funds are then sent back to the user.However, the funds are currently transferred using the transfer method instead of the
safeTransfermethod, which is used elsewhere in the code.This could be problematic, as some tokens may not revert on failure and instead return false, leading to undetected transfer failures.
Recommendation
Replace the transfer method with
safeTransferin thecancelDepositfunction to ensure that token transfers are properly handled and any potential failures are detected.Resolution
Gamma Team: The issue was resolved in commit 1087920.
-
L-07 Low Max Deposit Cap Can Be Exceeded Warning Resolved
Description
It is possible to exceed the deposit cap by donating USDC to the vault after a deposit, these funds would essentially be donated to the vault, but if there were a single large holder they would be donating to themselves and surpassing the deposit cap.
Recommendation
Be aware of this possibility and ensure vaults are made up of a diverse number of depositors.
Resolution
Gamma Team: The issue was resolved in commit b4022d5.
-
L-08 Low Position Accounting Mismatch Logical Error Acknowledged
Description
If a user, who holds the entirety of the shares, withdraws while a position is open, it will lead to the GMX position being closed.
This will cause
curPositionKeyto be set tobytes32(0), butpositionIsClosedwill not be updated in the withdrawal flow.This will lead to unexpected issues when calling
run(), as the current state will not match the expected state.Recommendation
Check if the position has been fully closed in the withdrawal flow. If that is the case, set
positionIsClosedtotrueso there are no discrepancies betweencurPositionKeyandpositionIsClosed.Resolution
Gamma Team: Acknowledged.
-
L-09 Low Settlement Can Unexpectedly Hit Withdrawal Case Logical Error Resolved
Description
If the current action is a
settlethen in theafterOrderExecutionthe condition to be met is based onsizeDeltaand output amount.This relies on strict equality and may not be future-proof for any GMX changes to how
outputAmountis calculated.If this case occurred and the
settlecondition was not met, the execution flow will enter the withdrawal case directly.Consequently, the user will get the
settleamount, instead of their withdrawal amount. Additionally the shares from the withdrawal will be burned but the position will be relatively unchanged.Recommendation
Add a
SETTLEto the Flow enum and use that in theafterOrderExecutioncallback.Resolution
Gamma Team: The issue was resolved in commit 565919f.
-
L-10 Low 1x Long State Siphoning collateralToken Gaming Resolved
Description
In the
PerpetualVaultwhen the vault is in a 1x long state deposits are only valued based upon the proportion of index tokens they contribute to the vault.However upon withdrawals the user receives their shares proportional value of the collateral tokens in the vault.
As a result if there are any leftover collateral tokens in the vault from unswapped funding fees or for any other reason, these collateral tokens can be siphoned by an
arbitraguer.Recommendation
Consider including the collateral token balance value when computing the shares to issue for a deposit when the vault is in a 1x long state.
Resolution
Gamma Team: The issue was resolved in commit f2008b6.
No findings match.
Invariants 16
The review's fuzzing suite asserted 16 invariants. 14 held and 2 did not.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
GAMMA-01 | The distribution of fees should be proportional to each depositor's | Held |
GAMMA-02 | share of the total vault The sum of all individual depositor shares must always equal the | Held |
GAMMA-03 | totalShares variable in the contract. The value of a depositor's shares should never decrease due to the | Held |
GAMMA-04 | actions of other depositors. After all actions completed, nextAction should be empty. | Held |
GAMMA-05 | After all actions completed, swapProgressData should be | Held |
GAMMA-06 | empty. PositionKey should be zero when there is no position. | Held |
GAMMA-07 | No depositor should be able to withdraw their funds before the lockTime period has passed since | Held |
GAMMA-08 | their deposit. If GmxUtils (account) has a GMX position open, positionIsClosed == | Held |
GAMMA-09 | false After order execution FLOW should be cleared | Broken |
GAMMA-10 | After order execution GMX lock should be cleared | Held |
GAMMA-11 | If withdraw function is called on a open GMX position, callback should always hit the “settle” case in the afterOrderExecution | Held |
GAMMA-12 | and afterOrderCancellation function. If user deposits they will get a non-zero amount of shares | Held |
GAMMA-13 | The keeper should never be able to do a DEX swap or a GMX swap (any swap) when there is a | Held |
GAMMA-14 | nonzero curPositionKey If user withdraws they will get non zero amount in return | Held |
GAMMA-15 | Withdraw should succeed after liquidation | Broken |
GAMMA-16 | Withdraw should succeed after ADL | 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.
