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

Security review · May 2025

PerpetualVault Mitigation Review

for Gamma Strategies

Guardian's review of PerpetualVault Mitigation Review for Gamma Strategies, published May 2025. The report records 24 findings, including 7 medium and 17 low.

Published
Review window
May 3 to 9, 2025
Language
Solidity
Chains
Ethereum, Arbitrum, Optimism, Base, Polygon, BNB Chain
Sector
Yield and vaults
  • 0 Critical
  • 0 High
  • 7 Medium
  • 17 Low
  • 0 Informational

24 acknowledged

Scope

Findings 24

  1. M-01 Medium Incorrect Fee Amount May Be Calculated Logical Error Acknowledged

    Description

    GMX is moving to a balanceWasImproved model instead of a hasPositiveImpact approach for the position fees. This upcoming change is listed here: https://github.com/gmx-io/gmx-synthetics/pull/93

    However the Gamma contracts assume that the hasPositiveImpact approach is still being used. As a result, for markets with no price impact configured the fee calculated by Gamma will not match the fee calculated by GMX.

    Additionally, when an order flips the skew, moving the absolute skew closer to balanced the order may improve the skew but still receive negative impact due to the negative impact factor being larger than the positive impact factor. Orders which meet this criteria will have the fee overestimated for the user.

    Recommendation

    The balanceWasImproved value for the order is not easily accessible without comparing the pool state directly before and after an order’s execution. Therefore the best course of action may be to either ensure that no zero slippage GM markets are used with the Gamma system, or to acknowledge the imperfection in the fee calculations for these markets.

  2. M-02 Medium Lacking setGovernanceFee Implementation Logical Error Acknowledged
    Location
    PerpetualVault.sol: 710

    Description

    The setGovernanceFee function lacks an implementation to assign the related governanceFee value.

    Recommendation

    Implement the logic to assign the governanceFee value in the setGovernanceFee function.

  3. M-03 Medium Position Fees May Be Paid Twice Logical Error Acknowledged
    Location
    PerpetualVault.sol: 1120

    Description

    In the _withdraw function the feeAmount is removed from the collateralDeltaAmount regardless of the sign and magnitude of the pnl.

    If the pnl is positive and the swapPnlToCollateral token swap succeeds, the profit will be used to cover the fee amount in the decrease order execution. As a result the withdrawer would pay the position fee by having it deducted from the collateralDeltaAmount specified by Gamma and also by having it deducted by the pnl amount returned by GMX.

    Recommendation

    If the position pnl is positive then account for the fact that the positive pnl amount can be used to cover the feeAmount.

    However be aware that the positive pnl amount cannot be used to cover the feeAmount if the current position is long and the swapPnlToCollateral swap fails. If this swap is assumed to always succeed in the system, then consider carefully monitoring for the failure of this swap, denoted by the SwapReverted event emitted by GMX, and taking manual action.

  4. M-04 Medium gmxLock Errantly Set To False Warning Acknowledged
    Location
    PerpetualVault.sol

    Description

    In the afterLiquidationExecution function the gmxLock variable is assigned to false, however it is possible that an order is still pending in GMX for the vault.

    This may lead to unintended consequences such as un-cancellable orders through the cancelOrder keeper function or accidental order creation by the keeper when one is already pending in GMX.

    Recommendation

    Remove the gmxLock assignment in the afterLiquidationExecution.

  5. M-05 Medium Excessive Refund During Liquidations Logical Error Acknowledged
    Location
    PerpetualVault.sol

    Description

    In the _withdraw function, if a liquidation occurred and the curPositionKey has been assigned to zero then refundFee is assigned as true in the _handleReturn invocation.

    As a result, a refund may be granted to the user when the settle action in GMX has already taken place for their withdrawal.

    Consider the following scenario:

    • User A initiates a withdrawal
    • The settle decrease order is submitted and is executed in GMX
    • The nextAction.selector is a withdrawal action
    • Before the Gamma keeper executes the next action, the vault position is liquidated and the curPositionKey is set to 0.
    • The Gamma keeper executes the next action and the _withdraw invocation invokes a _handleReturn with refundFee as true.
    • The user is refunded for the majority of their executionFee, such that refund + executionFee paid to GMX for the settle action is greater than the executionFee initially paid by the user.

    Recommendation

    When the _withdraw function is invoked in runNextAction and the vault position has been liquidated consider not refunding the fee in this case, since the settle action will have already taken place.

  6. M-06 Medium Liquidations During Withdrawal Settlement Loop Warning Acknowledged
    Location
    PerpetualVault.sol

    Description

    When a liquidation occurs during the withdrawal flow, it is possible for the settlement action to get stuck and continue to fail and be retried in a cycle.

    Consider the following order of events:

    • A user initiates a withdrawal, the settlement action is created in GMX.
    • The vault position is liquidated in GMX, the afterLiquidationExecution sets the nextAction to be NextActionSelector.WITHDRAW_ACTION to restart the withdrawal and allow the user to exit.
    • The settlement action is executed after the liquidation and is therefore cancelled.
    • In the afterOrderCancellation callback the nextAction is reset to NextActionSelector.SETTLE_ACTION, therefore overwriting the nextAction that was set by the liquidation callback.
    • The settlement order is submitted again, and will again fail and continue the cycle, preventing the user from withdrawing.

    Recommendation

    Consider adding a check in the afterOrderCancellation callback such that if the order was a settlement order and the curPositionKey has been set to zero then the next action is set to NextActionSelector.WITHDRAW_ACTION so that the withdrawal flow can be reset and the user can receive their funds.

  7. M-07 Medium totalShares Accounting Perturbed By CancelFlow Logical Error Acknowledged
    Location
    PerpetualVault.sol

    Description

    In the _cancelFlow function during the final step of a deposit flow the deposit object can have a nonzero shares amount associated which is not removed from the total upon cancellation.

    As a result, if a deposit is cancelled at the last step before the FINALIZE action, the totalShares accounting will be perturbed.

    Recommendation

    Consider explicitly disallowing the _cancelFlow function from being called for deposits which have already been minted a nonzero amount of shares.

  8. L-01 Low Outdated Reader Used Warning Acknowledged
    Location
    Global

    Description

    The tests and PositionInfo interface relies on an outdated Reader contract that was deployed almost a year ago. This Reader may become deprecated with future releases and increases risk of an interface mismatch bug.

    Recommendation

    Consider updating to the most recent Reader deployment seen here: https://github.com/gmx-io/gmx-synthetics/blob/updates/deployments/arbitrum/Reader.json

    And ensure that the interfaces in that Reader deployment match the interfaces used by Gamma, namely the inclusion of a positionKey bytes32 variable at the start of the PositionInfo struct: https://github.com/gmx-io/gmx-synthetics/blob/cb47fb783b017cfa815a353f6f39d415f60646e8/contracts/reader/ReaderPositionUtils.sol#L20

  9. L-02 Low Outdated NatSpec Documentation Acknowledged
    Location
    Global

    Description

    The NatSpec across many functions in the Gamma codebase is outdated.

    Recommendation

    Consider updating the NatSpec documentation for each function in the Gamma PerpetualVault codebase.

  10. L-03 Low _swapOutIndexToken DoS DoS Acknowledged
    Location
    PerpetualVault.sol: 358

    Description

    In the runNextAction function the _swapOutIndexToken function is used to swap any remaining index tokens from the contract balance to the collateral token.

    When submitting the runNextAction transaction to the mempool, the keeper may not supply any metadata[1] entry if it doesn’t observe any index token contract balance.

    However a malicious actor could frontrun the runNextAction transaction and supply index tokens to the contract in order to cause the _swapOutIndexToken logic to occur and therefore revert when the metadata[1] entry is decoded, thus censoring the runNextAction invocation.

    Recommendation

    Be aware of this DoS vector, on Arbitrum this is not currently a concern, however for any Gamma vaults on Avalanche this is noteworthy. Consider ensuring the keeper always provides a metadata[1] entry to handle even malicious indexToken transfers.

  11. L-04 Low Zero Position Key Used For totalAmount Warning Acknowledged
    Location
    PerpetualVault.sol: 826

    Description

    In the _totalAmount function when the positionIsClosed value is false the positionData is always queried from GMX for the curPositionKey.

    However there are cases such as liquidation where the curPositionKey is reset to 0, while positionIsClosed is intentionally not marked as true.

    Recommendation

    No cases have been identified where the curPositionKey is zero and positionIsClosed is false for a leveraged position during an invocation of the _totalAmount function. However out of an abundance of caution, consider handling the zero curPositionKey case to avoid any unintended consequences of future updates.

    When the curPositionKey is 0 the _totalAmount function should not consult GMX for the position value with the getPositionInfo function.

  12. L-05 Low Misleading Comment Documentation Acknowledged
    Location
    GmxProxy.sol: 530

    Description

    The documentation for the fundEth function mentions that the GmxProxy attempts to avoid native ether from GMX. However the GmxProxy specifically wants to receive native Ether from GMX and does so by implementing the refundExecutionFee function.

    Recommendation

    Remove the misleading fundEth comment.

  13. L-06 Low Misleading Variable Name Naming Acknowledged
    Location
    VaultReader.sol: 52

    Description

    In the getPositionInfo, getNegativeFundingFeeAmount, and getPnl functions the sizeInTokens variable is assigned as the result of the getPositionSizeInUsd function.

    Recommendation

    Correct the sizeInTokens variables to be the result of the getPositionSizeInTokens function.

  14. L-07 Low Fee Discounts Are Not Accounted For Warning Acknowledged
    Location
    Global

    Description

    When computing the position fee during a withdrawal there is no consideration for discounts that may be granted to the vault position due to pro tiers or in the case that a nonzero referral code is used.

    As a result withdrawers may be charged a slightly higher fee than what is actually levied against the vault position.

    Recommendation

    Be aware of this behavior in the event that the vault receives a pro tier discount or if a referral code is used. This inaccuracy goes in the favor of the vault so it is acceptable.

  15. L-08 Low Lacking Withdrawal Execution Fee Refunds Warning Acknowledged
    Location
    PerpetualVault.sol

    Description

    In the withdrawal flow for perpetual positions the refundFee value in the _nextAction.data is always assigned to false. As a result users will never receive refunds for the overestimated portion of the execution fee they paid up front.

    Recommendation

    Consider if this is the expected behavior, if it is not consider assigning the refundFee value to true.

  16. L-09 Low Unused COMPOUND Action Superfluous Code Acknowledged
    Location
    PerpetualVault.sol

    Description

    The COMPOUND_ACTION NextActionSelector is unused in the codebase.

    Recommendation

    Consider removing the COMPOUND_ACTION from the NextActionSelector enum.

  17. L-10 Low Signal Change Flow Left Incomplete Warning Acknowledged
    Location
    PerpetualVault.sol

    Description

    During the signal change flow it is possible for a switch from 1x long to 1x short to be left incomplete if a GMX swap is used to close the existing position and the GMX swap is cancelled.

    When the GMX Market Swap action is cancelled the nextAction.selector is assigned as a swap action, overwriting the increase action. Therefore when the MarketSwap is retried it will be the final action in the flow and the signal change will complete without opening the new short position.

    Recommendation

    Be aware of this edge case when the MarketSwap action fails during a signal change. And consider only using dex swaps to avoid this failure when changing from long 1x to short 1x.

  18. L-11 Low Zero Position Key Used For flowData Warning Acknowledged
    Location
    PerpetualVault.sol

    Description

    When a liquidation occurs directly after a user initiates a deposit the flowData is assigned as the result of the getPositionSizeInTokens for a curPositionKey of 0.

    It is unlikely that a position may exist in GMX with a positionKey of 0, however in the event that such a position does exist the flowData would be errantly assigned to a value that is not accurate to the Gamma vault position.

    Recommendation

    Out of an abundance of caution in the getPositionSizeInTokens VaultReader function, return 0 if the positionKey provided is zero. Additionally, consider adopting the same behavior for other view functions which accept a position key as a parameter.

  19. L-12 Low Incorrect Price Impact Rounding Rounding Acknowledged

    Description

    The rounding used in the getPriceImpactInCollateral function does not match the rounding performed by GMX when determining the sizeDeltaInTokens for a position. As a result the getPriceImpactInCollateral function will slightly miscalculate the price impact result.

    The expectedSizeInTokensDelta is calculated as:

    uint256 expectedSizeInTokensDelta = isLong ?
      sizeDeltaInUsd / prices.indexTokenPrice.max :
      sizeDeltaInUsd / prices.indexTokenPrice.min;
    

    Meanwhile GMX calculates the sizeInTokensDelta as:

    if (params.position.isLong()) {
        // round the number of tokens for long positions down
        cache.baseSizeDeltaInTokens = params.order.sizeDeltaUsd() / indexTokenPrice.max;
    } else {
        // round the number of tokens for short positions up
        cache.baseSizeDeltaInTokens = Calc.roundUpDivision(params.order.sizeDeltaUsd(), indexTokenPrice.min);
    }
    

    Recommendation

    Use round up division for shorts to match GMX’s calculation.

  20. L-13 Low Arbitrary Price Impact Conversion Unexpected Behavior Acknowledged
    Location
    VaultReader.sol: 171

    Description

    The priceImpactInCollateralTokens value is computed using the max or min indexTokenPrice and shortToken price depending on if the position isLong.

    This may seem correlated to the way that the expectedSizeInTokensDelta is computed, however it is not. The expectedSizeInTokensDelta max and min price usage is simply necessary to compute the actual amount of price impact which was experienced in the GMX system.

    Therefore the translation to a collateral token value of the price impact index token amount is arbitrarily assigned by the Gamma system. The calculation currently decides to use a maximum and minimum price interchangeably based upon isLong.

    However, technically the most resilient method would be to use the price combination that rounds the amount of price impact experienced towards negative infinity. This follows the best practice of rounding in favor of the protocol over the favor of the user.

    Recommendation

    Consider implementing rounding in the favor of the protocol by assigning the minimum value of both calculations as the resulting priceImpactInCollateralTokens.

  21. L-14 Low Execution Fee Charged with No GMX Call Unexpected Behavior Acknowledged
    Location
    contracts/PerpetualVault.sol:1110

    Description

    When a liquidation reduces the GMX position to zero, the curPositionKey is deleted and deposits are paused. However, since positionIsClosed remains false, users who withdraw are still charged an execution fee under the assumption that GMX calls like settle and withdrawGMX will be executed.

    Since the curPositionKey is empty, these GMX calls are skipped. The _handleReturn function does issue a refund, but because no callback occurred, the refunded amount is incorrect. This causes users to be overcharged

    Recommendation

    Update the positionIsClosed flag after the GMX position is liquidated and the curPositionKey is cleared, or alternatively, deposit funds back into GMX immediately after liquidation so that future withdrawals trigger the expected callback and execution fee logic behaves correctly.

  22. L-15 Low Deposit Reverts After Full Liquidation DoS Acknowledged
    Location
    contracts/PerpetualVault.sol:766

    Description

    When a liquidation occurs, the vault processes it in afterLiquidationExecution and allows the keeper to create a new order using any remaining funds. However, if the vault is fully liquidated and no collateral is recovered, deposits become blocked.

    This is due to a division-by-zero condition in the _mint function: totalAmountBefore becomes zero, while totalShares remains greater than zero.

    As a result, the share calculation:

    _shares = amount * totalShares / totalAmountBefore

    Will revert due to a division by zero error, preventing any new deposits.

    Recommendation

    Add a case to handle the scenario where totalAmountBefore is zero but totalShares is non-zero. Depending on the desired behavior the depositor can receive the entire vault share or a diluted amount of it.

  23. L-16 Low ADLs May Cause Excessive Fees Warning Acknowledged
    Location
    PerpetualVault.sol

    Description

    When an ADL occurs during the last step of a withdrawal action the entire withdrawal flow is re-started to account for any tokens which might have been sent to the PerpetualVault as a result of the ADL.

    This however can mean that a withdrawal action can expend even more than the executionFee allocated for the settle and decrease actions.

    Recommendation

    Be aware of this behavior and be sure to top up the GmxProxy contract accordingly in the event that this occurs.

  24. L-17 Low totalDepositAmount Not Decremented In Insolvency Logical Error Acknowledged
    Location
    PerpetualVault.sol

    Description

    In the _handleReturn function the _transferToken function is only invoked when the withdrawal amount is nonzero. If a withdrawal amount is zero e.g. due to an insolvent liquidation of the vault, then the totalDepositAmount decrement inside of the _transferToken function will not be reached.

    Recommendation

    Consider refactoring the _transferToken function such that it handles the 0 amount case gracefully, decrementing the totalDepositAmount in all cases and early returning if the specified amount is 0.

More from Gamma Strategies

All 7 reports
  1. Unilaunch Launchpad and Limit Order Book

    28 findings8 high 28 findings: 8 high, 8 medium, 5 low, 7 informational
  2. MultiPositionManager

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

    19 findings2 high 19 findings: 2 high, 17 low
  4. 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.

Get a quote