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

Security review · April 2025

Gasless Sponsored Calls, Part 2

for GMX

GMX engaged Guardian to review the security of GMX's Gelato Sponsored Call Integration. From the 31st of March to the 4th of April, a team of 6 auditors reviewed the source code in scope.

Published
Review window
March 31 to April 4, 2025
Language
Solidity
Chains
Arbitrum, Avalanche
Sector
Perpetuals
  • 0 Critical
  • 0 High
  • 1 Medium
  • 11 Low
  • 0 Informational

6 resolved · 6 acknowledged

Scope

Overview

GMX engaged Guardian to review the security of GMX's Gelato Sponsored Call Integration. From the 31st of March to the 4th of April, a team of 6 auditors reviewed the source code in scope.

Findings 12

  1. M-01 Medium Collateral Lost When Combining Swaps Logical Error Acknowledged
    Location
    BaseGelatoRelayRouter.sol: 293

    Description

    Proof of concept: PoC

    The current implementation allows both external calls and internal swaps to be executed in the same transaction. Users can externally swap tokens and send them to orderVault as collateral, and later use atomic swaps to receive WNT in order to pay the execution and relay fees.

    However, the unrecorded collateral in the orderVault is at risk if the atomic swap uses the same collateral for tokenIn. The following scenario could occur:

    • User swaps ARB for USDC in external calls, sends USDC to orderVault.
    • User swaps USDC to WNT using atomic swap, sending USDC to orderVault, and later executing

    SwapUtils.swap

    • Now createOrder is triggered, and collateral is recorded with recordTransferIn
    • cache.initialCollateralDeltaAmount will be 0, and user lost the USDC collateral

    The issue relies on the fact that orderVault is a StrictBank, so when SwapUtils.swap calls params.bank.transferOut it also triggers _afterTransferOut, syncing the tokenBalances with the current bank balance (which includes the user's collateral).

    Recommendation

    Prevent users from combining external calls and internal atomic swaps within the same transaction, either through the UI or by implementing on-chain protections.

    Resolution

    GMX Team: Acknowledged.

  2. L-01 Low Return Handling For Order Creation Best Practices Resolved
    Location
    GelatoRelayRouter.sol, BaseGelatoRelayRouter.sol

    Description

    When creating an order using a standalone call, the newly created order's key is returned, which can be utilized for future updates or cancellations. However, when an order is created as part of a batch process, its key is neither validated nor returned during the execution of the batch.

    Recommendation

    Consider implementing a bytes array as the return value for the batch function. This array should include the newly generated keys for any _createOrder calls within the batch. If the batch contains only updates or cancellations, the function should return an empty array.

    Resolution

    GMX Team: Resolved.

  3. L-02 Low minPrice Used For maxRelayFeeSwapUsd Logical Error Resolved
    Location
    BaseGelatoRelayRouter.sol: 283

    Description

    The maximum relay fee swap size for sub accounts is capped, and execution reverts if the calculated USD amount exceeds maxRelayFeeSwapUsd.

    However, the calculation for this check is currently performed using the min oracle price instead of the max oracle price.

    To enhance safety and prevent potentially underestimating the swap size, it would be preferable to use the max oracle price during this calculation.

    Recommendation

    Consider using the max oracle price for the maxRelayFeeSwapUsd check to provide greater safety and ensure the cap is accurately enforced.

    Resolution

    GMX Team: Resolved.

  4. L-03 Low Batch Orders Do Not Work As Expected Logical Error Acknowledged
    Location
    BaseGelatoRelayRouter.sol: 111C1-113C10

    Description

    With the newly introduced batch functionality, users can now perform multiple actions in a single operation. GMX also intends to enable users to transfer the required tokens to the orderVault via external calls during the _handleRelayBeforeAction.

    // External calls can be used to send tokens to OrderVault. In this case,
    initialCollateralDeltaAmount can be zero,
    // and there is no need to call _sendTokens.
    

    However, an issue arises when combining the 'batch orders' feature with 'token transfers via external calls' in the following scenario:

    Consider a user batching two createOrders actions, each requiring 100 USDC as collateral. The total of 200 USDC will be transferred to the orderVault through external calls, after which the batch function will iterate _createOrder twice.

    Since the unaccounted balance of the orderVault is treated as the initialCollateralDeltaAmount (as seen in the code here), the first order will receive the entire 200 USDC as collateral.

    The second order, however, will receive 0 USDC, as the entire balance of the orderVault is allocated to the first order during its iteration.

    Recommendation

    Consider disabling token transfers via external calls for batch orders. Instead, enforce the transfer of tokens directly within the _createOrder function by specifying a non-zero initialCollateralDeltaAmount whenever a batch is used.

    This restriction could be implemented at the contract level and also managed in the UI when constructing the batch for the user, ensuring smooth operation and preventing potential issues.

    Resolution

    GMX Team: Acknowledged.

  5. L-04 Low Modifier Execution Order Logical Error Resolved
    Location
    GelatoRelayRouter, SubaccountGelatoRelayRouter.sol

    Description

    Solidity serializes modifiers linearly, meaning the order of execution depends on how they are arranged.

    For example, in the function signature: external withRelay(relayParams, account, false) nonReentrant returns (bytes32). The withRelay modifier will be executed first, followed by nonReentrant. Since nonReentrant is checked after _handleRelayBeforeAction and cleared before _handleRelayAfterAction, reentrancy is theoretically possible through both of these functions. However, we couldn't identify a incentive for a normal user to sign off on actions that would enable such behavior.

    Theoretically, a sub account might attempt reentrancy via the permit mechanism in _handleTokenPermits. Even so, the only effect would be an increase in gasUsed, resulting in the user paying more gas than necessary—without gaining any real advantage.

    Additionally, _handleRelayAfterAction interacts with wnt tokens and hence doesn't allow arbitrary operations. That said, just because we haven't found a concrete exploit path today doesn't mean one won't emerge in the future.

    Recommendation

    Consider placing the nonReentrant modifier before withRelay for all relevant actions.

    Resolution

    GMX Team: Resolved.

  6. L-05 Low Fixed _getCalldataGas Parameters Best Practices Acknowledged
    Location
    GasUtils.sol: 600-601

    Description

    The _getCalldataGas function uses hardcoded parameters that cannot be changed after deployment. Specifically:

    • calldataLengthLimit is fixed: if (calldataLength > 50000) { ... }
    • calldataCostPerByte is also hardcoded: uint256 txCalldataGasUsed = calldataLength * 10;

    This lack of configurability limits the protocol's ability to adapt to future changes in gas costs or desired behavior.

    Recommendation

    Consider making these parameters configurable via the datastore, so they can be adjusted post-deployment if necessary.

    Resolution

    GMX Team: Acknowledged.

  7. L-06 Low Over/Under Estimation Logical Error Resolved
    Location
    GasUtils.sol: 559-560

    Description

    To verify that there’s minimal delta between what GMX is charged by Gelato and what users refund back to GMX in the case of a sponsoredCall, we fuzzed the following invariant: UPDT-01: WNT increase should match gas spent

    • Invariant location
    • Setup logic
    • Foundry test case

    Example observation

    Charged:      385,982
    Actual:       357,827
    Delta:        28,155 (~7%)
    

    We observed a delta in the range of +7–8%. Initially, we attributed part of this to overestimation of calldata bytes (e.g., counting 0-bytes with 10 as a cost), which accounted for 14,562 units in our test case.

    Subtracting this gives: 28,155 - 14,562 = 14,593

    Later, we realized that:

    • The base gas cost includes Gelato contract overhead (e.g., verification and external call)
    • Also includes intrinsic Ethereum transaction cost
    • However, in our fuzzing setup, we call updateOrder directly and measure gas around the external call, so these should

    be excluded

    Subtracting both: 14,593 - 21,000 - 10,000 = -16,407

    This shows that some overestimations (e.g., calldata cost) and underestimations (e.g., base cost) offset each other, making the final delta difficult to evaluate reliably in a test environment.

    Recommendation

    • Review the base gas cost configuration and consider increasing it — it is currently set to 40,000, which may not be

    sufficient to account for two WNT transfers, as intended. One WNT transfer will always occur.

    • Deploy the the gasless routers to a testnet or in a trial phase on mainnet.
    • Let a set of on-chain transactions execute under real parameters.
    • Analyze the actual delta between what is charged and what is refunded, and adjust the estimation logic accordingly.

    Given a list of on-chain transactions, we can assist in perfecting these estimations. On-chain measurements provide more accurate data compared to local test environments (like Foundry or Hardhat), which rely on assumptions that may not reflect production behavior.

    Resolution

    GMX Team: Resolved.

  8. L-07 Low Superfluous Return Statement Best Practices Resolved
    Location
    BaseGelatoRelayRouter.sol: 205

    Description

    The _handleRelayBeforeAction function includes a return statement that attempts to return the result of _handleRelayFee(), but neither of these functions has a defined return value

    Recommendation

    Remove the return statement.

    Resolution

    GMX Team: Resolved.

  9. L-08 Low Incorrect Comment: GelatoRelayRouter Best Practices Resolved
    Location
    GelatoRelayRouter.sol: 26

    Description

    The GelatoRelayRouter.batch function contains the following comment:

    // @note all params except subaccount should be part of the corresponding struct hash

    However, the GelatoRelayRouter does not contain any subaccount param.

    Recommendation

    Modify the comment so it refers to account instead.

    Resolution

    GMX Team: Resolved.

  10. L-09 Low EIP-712 Signature Readability Best Practices Acknowledged
    Location
    RelayUtils.sol: 211

    Description

    When signing transactions with wallets supporting EIP-712 (like MetaMask), users should see a structured, human-readable representation of all critical parameters. However, there are some issues with the current implementation:

    • Parameters reduced to bytes32 hashes appear as unreadable hex strings in the signing interface
    • Users have no way to verify the content of important parameters like relay fee information or

    subaccount permissions

    • This creates blind spots where users must trust the frontend application completely

    Recommendation

    Expand all critical parameters into full EIP-712 typed structures

    Resolution

    GMX Team: Acknowledged.

  11. L-10 Low Subaccounts: Gas Griefing Risk Trust Assumptions Acknowledged
    Location
    BaseGelatoRelayRouter.sol: 254-255

    Description

    GMX has implemented various protections to minimize the potential damage that malicious subaccounts could cause to their associated primary accounts.

    One such potential vector involves subaccounts deliberately increasing gas usage, leading to higher-than-necessary refunds paid by the main account to GMX.

    We previously flagged a similar issue in our earlier review, where we discussed the possibility of padding calldata with empty data to artificially inflate gas usage.

    A comparable scenario exists with token permits. Since permits are called on arbitrary tokens, a malicious subaccount could pass a poorly optimized or deliberately gas-heavy token, causing the handleTokenPermits logic to consume excessive gas.

    While it’s true that the subaccount would be spending its own funds to inflict this kind of griefing on the main account—and would gain no tangible benefit from doing so—we’re raising this again given your decision to address the calldata padding issue in our prior review.

    Recommendation

    • Either treat this as an accepted trust assumption for subaccounts,

    or

    • Consider adding guardrails such as:
    • Limiting the gas forwarded for permit calls
    • Restricting the number of permits processed in handleTokenPermits

    Resolution

    GMX Team: Acknowledged.

  12. L-11 Low Less Premium Is Charged In Config Logical Error Acknowledged
    Location
    general.ts: 132

    Description

    In the config files, a 6% relay fee premium is applied to the gas cost. However, according to the Gelato docs, a fixed 10% fee premium is charged on both Arbitrum and Avalanche.

    Recommendation

    Consider setting the premium to 10% if GMX does not have a protocol-specific discount agreement with Gelato.

    Resolution

    GMX Team: Acknowledged.

Invariants 19

The review's fuzzing suite asserted 19 invariants. 19 held.

Every invariant tested
IDInvariantResult
INC-01Position size in USD should increase after successful increase position call.Held
INC-02Long Open Interest should increase after successful increase position call.Held
INC-03Collateral amount of position should increase after successful increase position call.Held
INC-04Collateral sum for longs should increase after successful increase position call.Held
INCI-04Collateral sum for shorts should increase after successful increase position call.Held
DEC-01Position size in USD should decrease after successful decrease position call.Held
DEC-02Collateral amount of position should decrease after successful decrease position call.Held
DEC-03Long Open Interest should decrease after successful decrease position call.Held
DEC-04Collateral sum for longs should decrease after successful decrease position call.Held
CLOSE-01Position size in USD should be 0 after closing the position.Held
CLOSE-02Position size in tokens should be 0 after closing the position.Held
CLOSE-03Position collateral amount should be 0 after closing the position.Held
CLOSE-04Auto cancel order list should be empty after closing the position.Held
CNCL-ORD-1User should receive the same amount of long tokens he sent to create an orderHeld
CNCL-ORD-2User should receive the same amount of short tokens he sent to create an orderHeld
SWP-01Received token balance after swap should be equal to simulated amounts beforeHeld
GEN-1swap Function call should not silently revertHeld
GEN-2Fee should be covered and refund sent to the callback contractHeld
UPDT-01RelayFeeAddress WNT increase should match gas spentHeld

More from GMX

All 44 reports
  1. Timelock Updates

    4 findings 4 findings: 3 low, 1 informational
  2. LayerZeroProvider Routing

    1 finding 1 finding: 1 medium
  3. Open Interest Updates

    5 findings 5 findings: 2 medium, 3 low
  4. Updates Branch

    2 findings 2 findings: 2 low

Put your code through the same review.

This review started with a conversation about scope. Tell us what you are building and we will plan yours with you.

Get a quote