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
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
-
M-01 Medium Collateral Lost When Combining Swaps Logical Error Acknowledged
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
orderVaultas collateral, and later use atomic swaps to receive WNT in order to pay the execution and relay fees.However, the unrecorded collateral in the
orderVaultis at risk if the atomic swap uses the same collateral fortokenIn. 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
createOrderis triggered, and collateral is recorded withrecordTransferIn cache.initialCollateralDeltaAmountwill be 0, and user lost the USDC collateral
The issue relies on the fact that
orderVaultis aStrictBank, so whenSwapUtils.swapcallsparams.bank.transferOutit also triggers_afterTransferOut, syncing thetokenBalanceswith 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.
- User swaps ARB for USDC in external calls, sends USDC to
-
L-01 Low Return Handling For Order Creation Best Practices Resolved
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
_createOrdercalls within the batch. If the batch contains only updates or cancellations, the function should return an empty array.Resolution
GMX Team: Resolved.
-
L-02 Low minPrice Used For maxRelayFeeSwapUsd Logical Error Resolved
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
minoracle price instead of themaxoracle price.To enhance safety and prevent potentially underestimating the swap size, it would be preferable to use the
maxoracle price during this calculation.Recommendation
Consider using the
maxoracle price for themaxRelayFeeSwapUsdcheck to provide greater safety and ensure the cap is accurately enforced.Resolution
GMX Team: Resolved.
-
L-03 Low Batch Orders Do Not Work As Expected Logical Error Acknowledged
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
orderVaultvia 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
createOrdersactions, each requiring 100 USDC as collateral. The total of 200 USDC will be transferred to theorderVaultthrough external calls, after which the batch function will iterate_createOrdertwice.Since the unaccounted balance of the
orderVaultis treated as theinitialCollateralDeltaAmount(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
orderVaultis 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
_createOrderfunction by specifying a non-zeroinitialCollateralDeltaAmountwhenever 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.
-
L-04 Low Modifier Execution Order Logical Error Resolved
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) nonReentrantreturns (bytes32). ThewithRelaymodifier will be executed first, followed bynonReentrant. SincenonReentrantis checked after_handleRelayBeforeActionand 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
permitmechanism in_handleTokenPermits. Even so, the only effect would be an increase ingasUsed, resulting in the user paying more gas than necessary—without gaining any real advantage.Additionally,
_handleRelayAfterActioninteracts withwnttokens 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
nonReentrantmodifier beforewithRelayfor all relevant actions.Resolution
GMX Team: Resolved.
-
L-05 Low Fixed _getCalldataGas Parameters Best Practices Acknowledged
Description
The
_getCalldataGasfunction uses hardcoded parameters that cannot be changed after deployment. Specifically:calldataLengthLimitis fixed:if (calldataLength > 50000) { ... }calldataCostPerByteis 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.
-
L-06 Low Over/Under Estimation Logical Error Resolved
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.
-
L-07 Low Superfluous Return Statement Best Practices Resolved
Description
The
_handleRelayBeforeActionfunction includes a return statement that attempts to return the result of_handleRelayFee(), but neither of these functions has a defined return valueRecommendation
Remove the
returnstatement.Resolution
GMX Team: Resolved.
-
L-08 Low Incorrect Comment: GelatoRelayRouter Best Practices Resolved
Description
The
GelatoRelayRouter.batchfunction contains the following comment:// @note all params except subaccount should be part of the corresponding struct hashHowever, the
GelatoRelayRouterdoes not contain anysubaccountparam.Recommendation
Modify the comment so it refers to
accountinstead.Resolution
GMX Team: Resolved.
-
L-09 Low EIP-712 Signature Readability Best Practices Acknowledged
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
bytes32hashes 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.
- Parameters reduced to
-
L-10 Low Subaccounts: Gas Griefing Risk Trust Assumptions Acknowledged
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
handleTokenPermitslogic 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.
-
L-11 Low Less Premium Is Charged In Config Logical Error Acknowledged
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.
No findings match.
Invariants 19
The review's fuzzing suite asserted 19 invariants. 19 held.
Every invariant tested
| ID | Invariant | Result |
|---|---|---|
INC-01 | Position size in USD should increase after successful increase position call. | Held |
INC-02 | Long Open Interest should increase after successful increase position call. | Held |
INC-03 | Collateral amount of position should increase after successful increase position call. | Held |
INC-04 | Collateral sum for longs should increase after successful increase position call. | Held |
INCI-04 | Collateral sum for shorts should increase after successful increase position call. | Held |
DEC-01 | Position size in USD should decrease after successful decrease position call. | Held |
DEC-02 | Collateral amount of position should decrease after successful decrease position call. | Held |
DEC-03 | Long Open Interest should decrease after successful decrease position call. | Held |
DEC-04 | Collateral sum for longs should decrease after successful decrease position call. | Held |
CLOSE-01 | Position size in USD should be 0 after closing the position. | Held |
CLOSE-02 | Position size in tokens should be 0 after closing the position. | Held |
CLOSE-03 | Position collateral amount should be 0 after closing the position. | Held |
CLOSE-04 | Auto cancel order list should be empty after closing the position. | Held |
CNCL-ORD-1 | User should receive the same amount of long tokens he sent to create an order | Held |
CNCL-ORD-2 | User should receive the same amount of short tokens he sent to create an order | Held |
SWP-01 | Received token balance after swap should be equal to simulated amounts before | Held |
GEN-1 | swap Function call should not silently revert | Held |
GEN-2 | Fee should be covered and refund sent to the callback contract | Held |
UPDT-01 | RelayFeeAddress WNT increase should match gas spent | Held |
More from GMX
All 44 reportsPut 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.
