GMX engaged Guardian to review the security of their GMX Crosschain architecture. From the 23rd of February to the 17th of March, a team of 7 auditors reviewed the source code in scope.
- Published
- Review window
- February 23 to March 17, 2025
- Language
- Solidity
- Chains
- Arbitrum, Avalanche
- Sector
- Perpetuals
- 9 Critical
- 12 High
- 10 Medium
- 25 Low
- 0 Informational
Scope
Overview
GMX engaged Guardian to review the security of their GMX Crosschain architecture. From the 23rd of February to the 17th of March, a team of 7 auditors reviewed the source code in scope.
Findings 56
-
C-01 Critical Incorrect Message Decoding Leads Logical Error Resolved
Description
GMX decodes the message received from Stargate inside
lzComposeusing:(address account, address token, uint256 srcChainId) = MultichainProviderUtils.decodeDeposit(message);However, the message that Stargate sends will always be in the
OFTComposeMsgCodecformat: stargate-v2/packages/stg-evm-v2/src/StargateBase.sol at 8ac1688ca419296c2f8c34098b3152d6c039e49c · stargate-protocol/stargate-v2composeMsg = OFTComposeMsgCodec.encode(_origin.nonce, _origin.srcEid, amountLD, _composeMsg);As a result, the account, token, and
srcChainIdare incorrectly decoded, causing thelzComposecall to fail. Since the funds were already credited to theLayerZeroProvider, the user loses these funds.The funds will instead be credited to whoever directly calls
lzComposewith the correct token encoded in the message parameter.Recommendation
Consider using the
OFTComposeMsgCodeclibrary to correctly decode the compose message and extract the parameters needed to credit the user's multichain balance.Resolution
GMX Team: Resolved.
-
C-02 Critical Front-Running lzCompose Allows Fund Theft Validation Resolved
Description
Stargate’s messaging bridge operates in a two-step process. In the first step, tokens are sent to the receiver, which in this case is the
LayerZeroProvider, increasing the receiver's balanceThen, it is expected that
LayerZero’s endpoint will calllzCompose, accounting for the balance sent in the first step. The issue here is that these two calls are not guaranteed to be atomic.Since GMX’s
LayerZeroProvider.lzComposeis permissionless, anyone can intervene between these two steps and steal the funds deposited inLayerZeroProvider.Additionally, Stargate provides a
retryReceiveTokenfeature for cases where there was insufficient liquidity at the time of token delivery. When this occurs, this makes this attack even easier by publicly exposing extraction opportunity.Recommendation
Consider:
- Restricting the
lzComposefunction so that only theLayerZeroendpoint can call it. - Verifying the from address and allowing it only if it originates from one of Stargate’s pools.
For example:
Resolution
GMX Team: Resolved.
- Restricting the
-
C-03 Critical Incorrect Balance Accounting Validation Resolved
Description
Currently,
LayerZeroProviderrecords the entire remaining balance for the account passed inlzCompose.As explained in C-02, Stargate’s messaging follows a two-step process: 1. Token Delivery – Tokens are sent to
LayerZeroProvider. 2. Message Delivery –lzComposerecording the deposit.Since the balance is assigned based on
lzComposeexecution, if multiple users (e.g., A, B, and C) bridge the same token with different amounts, the first user whose message gets executed inlzComposereceives all tokens—including those meant for others.For example, User A could receive User B and C’s funds.
Recommendation
Instead of transferring the entire balance of the
LayerZeroProvider, use the exact bridged token amount fromOFTComposeMsgCodecto ensure users receive only their own funds.Example Fix:
uint256 amountLD = OFTComposeMsgCodec.amountLD(_message);Resolution
GMX Team: Resolved.
-
C-04 Critical Token Spoofing: Bridge Token X, Get Accounted For Token Y Validation Resolved
Description
As explained in C-02, Stargate’s messaging follows a two-step process: 1. Token Delivery – Tokens are sent to
LayerZeroProvider. 2. Message Delivery –lzComposerecording the deposit.We have already covered in another critical issues of this report that anyone can intercept these two steps and directly call the permissionless
lzCompose, thereby stealing the funds.However, even if you make
lzComposepermissioned withonlyLayerZeroEndpointand from stargate pool addresses, one critical issue still remains unresolved. Stargate has separate handlers for each token, meaning that whatever message it constructs is independent of the token itself.composeMsg = OFTComposeMsgCodec.encode(_origin.nonce, _origin.srcEid, amountLD,_composeMsg);Relevant code referenceAs a result, GMX intends to derive the token address from the
_composeMsg, which the user has access to and can modify. This is where a critical issue arises. Since the_composeMsgfield is a user-controlled field, one can pass any token of their choice and effectively bridge a different token using Stargate.For example, they can pass WBTC but actually bridge USDC instead. This allows a user to steal whatever balance remains for accounting in
LayerZeroProviderby frontrunning innocent's user's WBTClzCompose.Recommendation
Consider deriving the token address from the from parameter passed in
lzCompose, instead of themsgfield.Relevant examples:
Resolution
GMX Team: Resolved.
-
C-05 Critical Order Vault Drain Via Incorrect Collateral Handling Logical Error Resolved
Description
With the multichain changes, GMX now allows users to create, update, or cancel orders using their balance in the Multichain vault. Users are expected to sign these changes, and Gelato is responsible for calling these functions on
MultichainOrderRouter.The relay fee for Gelato is paid to the fee recipient via
_handleRelayFee. To ensure that the signing account has sufficient balance in their Multichain vault,_handleFeePaymentis called beforehand._handleFeePaymentfirst checks if the Multichain balance is enough to cover the fee.If it's insufficient, it attempts to recover the fee from
initialCollateralDeltaAmountin the order and then from the user's positions by reducing their collateral. However, when transferring funds from a position, GMX incorrectly transfers the funds from the order vault instead of the market.This allows users to pull fees from the shared order vault rather than their own positions. At the time of writing (block 22019054 on Arbitrum), the order vault balance exceeds $4M. Thus, an attacker can repeatedly call
updateOrderwith a fee, continuously draining the order vault as long as theirpositionCollateralAmountis greater than the fee.One might argue that after the first transaction, the attacker's
positionCollateralAmountwould decrease, preventing further exploitation. However, two additional mistakes allow this attack to continue:- Incorrect Storage Key – The key used updates the order's key instead of the position's key, modifying
unrelated storage space instead of the attacker's position.
- Incorrect Value Update – The value set in storage is
positionCollateralAmountinstead of
positionCollateralAmount - unpaidAmount, meaning the collateral remains unchanged.As a result, the attacker can continue calling the function indefinitely until the order vault is empty.
Recommendation
- Ensure that the correct key and value are used when updating position collateral.
- Withdraw funds from the market instead of the order vault, as implemented here.
Resolution
GMX Team: Resolved.
-
C-06 Critical Order Vault Drain Via Incorrect Fee Deduction Logical Error Resolved
Description
Multichain users can pay the Gelato relay fee using their Multichain balance, the pending order's
initialCollateralAmount, or the position'scollateralAmount. This applies only toupdateOrderandcancelOrder.However,
decreaseOrdersdo not have aninitialCollateralAmountdeposited at order creation—this is only applicable to increase and swap orders.As a result,
_handleFeePaymentincorrectly reducesinitialCollateralAmountfrom the order and withdraws funds from theorderVault, effectively stealing collateral from other users and LPs. A malicious user can exploit this by: 1. Opening a position. 2. Creating a limit decrease order withinitialCollateralAmountset to the total assets available in theorderVault. 3. Executing_handleFeePaymentwithrelayParams.fee.feeAmountequal toinitialCollateralAmount, which then:- Withdraws tokens from the
orderVault. - Pays the relay fee.
- Sends the remaining amount to the attacker's Multichain balance, effectively draining the vault.
Recommendation
In
_handleFeePayment, verify whether the order type is an increase or swap order—where collateral was actually deposited during order creation—before allowing the user to pay the Gelato relay fee usinginitialCollateralAmount.Resolution
GMX Team: Resolved.
- Withdraws tokens from the
-
C-07 Critical Missing validatePosition After Fee Deduction Validation Resolved
Description
Multichain users can pay the Gelato relay fee using their position's collateral if their Multichain balance or pending order's
initialCollateralAmountis insufficient.However, after deducting collateral from the position, there is no additional check to verify:
- Whether the position remains solvent or becomes liquidatable.
- The impact on markets, funding, and borrowing updates.
This not only allows users to arbitrarily remove collateral from their position in a single transaction but also creates bad debt for the protocol.
An attacker can front-run liquidations by updating or canceling a pending order and setting a high
feeAmount, effectively withdrawing all collateral from their position before liquidation occurs.Recommendation
- Do not allow paying the relay fee using the position’s collateral.
- If the protocol intends to support this feature, implement proper validation and state updates to
ensure accurate collateral accounting.
Resolution
GMX Team: Resolved.
-
C-08 Critical GM Tokens Sent To MultichainVault Instead Of GLV Logical Error Resolved
Description
During a Multichain
glvDepositexecution, if the user deposited GM tokens, it will early return in_processMarketDepositby transferring these tokens to the GLV contract.Otherwise, if long/short tokens are deposited, an
executeDepositwill be triggered to deposit them into the market, setting thereceiveras the GLV contract.However, if it's a Multichain action, the
glvDeposit.srcChainId()is used as thesrcChainIdin theExecuteDepositParams.During the GM market deposit, specifically in
_executeDeposit, the non-zerosrcChainIdwill mistakenly mint the GM tokens to themuiltichainVaultand record them under the GLV contract address.Consequently, the
glvValuecalculation will be incorrect, as the GM tokens were never transferred to the GLV contract address.This leads to insolvency when users attempt to withdraw their GLV tokens. The contract lacks sufficient
gmTokento process withdrawals in_processMarketWithdrawalbecause a portion of its balance is tracked as a Multichain balance.Recommendation
Set the
srcChainIdparam inExecuteDepositParamsto zero to avoid sending the GM tokens to themultichainVaultResolution
GMX Team: Resolved.
-
C-09 Critical Backward Compatibility With Callback Contracts Logical Error Acknowledged
Description
The callback function signatures changed from the previous iterations (i.e. new
dataListparam was added).This will impact with a lot of GMX integrators as they heavily rely on this callbacks, but suddenly can't receive them anymore after this update. Keep in mind that same issue applies to handlers, as the
srcChainIdwas added.Recommendation
Consider implementing a try/catch system: try the new callback function signature first and if it doesn't work try the old callback. Additionally make sure there are no issues for GMX integrations in terms of creating, updating and cancelling orders.
Resolution
GMX Team: Acknowledged.
-
H-01 High Pending Impact Missing In isPositionLiquidatable Logical Error Resolved
Description
The
isPositionLiquidatabledetermines if a position can be liquidated. This check includes position PnL, price impact due to the size being closed, and fees.However, the position might have a high negative pending price impact amount, which is not accounted for. During a liquidation, the
DecreasePositionUtils.decreasePosition()will check ifisPositionLiquidatablebefore processing collateral (where the pending price impact is applied).Therefore, a position might be liquidatable with the pending negative price impact, but it can return false as that value is not added (or may not be liquidatable adding the positive price impact, but the check returns true).
Recommendation
Include the position's pending price impact in the
isPositionLiquidatablecheck, adding it to the calculatedcache.priceImpactUsdResolution
GMX Team: Resolved.
-
H-02 High Impact Pool Cap Bypassed Logical Error Resolved
Description
When executing an
increasePositionorder, thepriceImpactUsdis calculated. In case of positive price impact, this amount is capped by the amount available in the impact pool at the time of increase. There are several issues with this behavior.Firstly, there is a time-mismatch between when funds are paid by traders to increase the impact pool and when funds are paid out to traders from the impact pool. This issue is clear when examining a new market:
- Market A is created and has initially 0 long and 0 short open interest, the impact pool amount begins at 0
- User A opens 10 long open interest and is negatively impacted
- User A’s negative impact does not increase the impact pool amount because it is stored as pending impact
- User B opens 10 short open interest and is positively impacted, but this positive impact is capped to 0 as there are no
tokens in the impact pool
This time-mismatch applies to markets under normal operation as well, but is most prevalent in new markets where the impact pool starts from 0.
Secondly, there is no guarantee that the trader will actually receive the purported positive impact that is calculated based on the size of the impact pool at the time of increase. This is because the size of the impact pool can be much smaller or larger at the time of the trader’s decrease order.
This presents an issue because the execution price on increase is computed with the assumption that the exact price impact amount will be ultimately realized by the trader. This behavior can be misleading to users and will result in positions which do not receive execution prices in line with the
acceptablePricespecified.Recommendation
To solve both issues, consider taking the following measures:
- Apply delta to the position impact pool for the positive or negative impact which is applied on increase
- Track the summation of the
pendingPriceImpactacross all positions in a market, with a
totalPendingPriceImpactAmountvariable- Add the
totalPendingPriceImpactAmountvalue in USD to the pool value in thegetPoolValueInfofunction to offset the
difference in the
positionImpactPool.- Do not apply delta to the position impact pool for the pending impact on decrease, instead only apply the delta for the
price impact associated with the decrease order that is being currently executed
Resolution
GMX Team: Resolved.
-
H-03 High Positive Pending Impact Perturbs Negative Capping Logical Error Partially resolved
Description
When a user has positive pending price impact this is combined with the price impact of the decrease order being executed to compute the
totalImpactUsd.This is done before the negative price impact is capped, therefore during times of volatility when the negative price impact cap would be in use users with positive pending price impact will experience loss without receiving their price impact rebates.
Recommendation
When the
proportionalImpactPendingUsdis positive, then add it to the finalcollateralCache.totalImpactUsdafter the negative price impact cap is applied.Resolution
GMX Team: Partially Resolved.
-
H-04 High Inability To Pay Bridge Fee For bridgeOut Logical Error Resolved
Description
The
LayerZeroProvider.bridgeOutrequires a native fee value sent alongstargate.send(). This native value is required to pay for the bridging fee.However, the
bridgeOutfunction is not payable and there is no logic to withdraw ETH from the Multichain user balance.Recommendation
Consider implementing some logic for the user to pay for the bridging fee, withdrawing WETH from multichain balance to unwrap it.
Resolution
GMX Team: Resolved.
-
H-05 High Inability To Bridge Out Logical Error Resolved
Description
When bridging out tokens using the
LayerZeroProvider, the user withdraws these tokens from the multichain balance. Additionally, anativeFeevalue needs to be sent to the stargate pool to pay for the bridge fee.However, the function mistakenly withdraws
valueToSendinstead of theamountof tokens. ThisvalueToSendis the native fee calculated to pay for the bridge fee and notamount + bridging feeas stated in the comment.Therefore, when
stargate.send()is executed, thesafeTransferFromwill revert as the tokens are not available in the router balance.Recommendation
Withdraw the
amountinstead ofvalueToSendfrom multichain balanceResolution
GMX Team: Resolved.
-
H-06 High Funds Bridged To Wrong Chain bridgeOut Validation Resolved
Description
In the
getBridgeOutStructHashthedatavalue is not included in the resulting hash and therefore cannot be validated. This param is thedstEidused when sending a message throughLayerZero.Therefore, a malicious user with the user's signature can send a relay transaction through Gelato with a different
dataparam.Validations will succeed as the signature does not include this param, and funds will be bridged to a different chain from what user requested. Additionally, the
providerparam is also not included in the struct hash.Even though there is a validation that checks if this address is whitelisted in GMX, there could be different providers and the transaction might be executed with the wrong address.
Recommendation
Include the
dataandproviderparameters in thegetBridgeOutStructHashResolution
GMX Team: Resolved.
-
H-07 High Missing Relay Fee Payment In Case Of bridgeOut Logical Error Resolved
Description
The
MultichainTransferRouterallows users to bridge funds in or out of their multichain balance. These are Gelato relay enabled functions to allow gasless transactions.This relay will execute
callWithSyncFeeV2, the function in charge to call the GMX router contracts with a specific fee amount, token and collector address encoded in the calldata.The
bridgeOutfunction contains theonlyGelatoRelaymodifier. In order to fully enable the gasless feature, the fee infeeTokenneeds to be paid to thefeeCollectorbefore the execution finishes.However, there is no logic to pay for this relay fee, different from the other routers. Consequently, the
bridgeOutfunction can't be gasless.Recommendation
Implement the
_handleRelayfunction to pay the Gelato relay fee to thefeeCollector.Resolution
GMX Team: Resolved.
-
H-08 High Transfer To EOA Instead Of Multichain Balance Logical Error Resolved
Description
In multiple places throughout the codebase, tokens that should have been included in the multichain balance are instead transferred directly to the user's EOA address. This issue occurs when transferring the residual fee in the
BaseGelatoRelayRouter.- In
_cancelOrder, the residual fee receiver is the account itself, and the balance is transferred to the EOA
instead of the
multichainVault.- In
_updateOrder, similarly, the residual fee receiver is the account itself whenincreaseExecutionFeeis false.
Additionally, the same issue occurs when multichain orders fail during the execution process. While successful executions are handled as multichain, failed executions and refunds are processed as non-multichain actions. This issue occurs in the following cases:
_handleDepositErrorduring multichain GM deposits._handleGlvDepositErrorduring multichain GLV deposits._handleWithdrawalErrorduring multichain GM withdrawals._handleGlvWithdrawalErrorduring multichain GLV withdrawals._handleShiftErrorduring multichain GM shifts._handleOrderErrorwhen any multichain orders fail during the execution phase._handleSwapErrorwhen decreasing positions, even though the outer position decrease doesn't fail, but only
the inner swap fails.
Lastly, the issue occurs when users cancel their multichain order using
MultichainOrderRouter.cancelOrder. The cancellation is processed as a non-multichain order becauseOrderUtils.cancelOrderdoesn't checksrcChainIdwhen determining thecancellationReceiverhere. However, theexecutionFeeReceiveris determined based onsrcChainIdhere.Recommendation
Use the
srcChainIdwhen handling execution errors and during cancellations to determine the correct destination for transferring the tokens. If tokens are transferred toMultichainVault,recordTransferInmust be called post that to record the balance for the account.Resolution
GMX Team: Resolved.
- In
-
H-09 High Incorrect Verification Of gasLeft Logical Error Resolved
Description
As you can in following code, GMX has added the
createEventDatacall for all callbacks (after executions) starting from multichain push.However, since GMX performs the
gasleft() / 64 * 63 > callbackGasLimitcheck before callingcreateEventData, the receiver may end up with a lowercallbackGasLimitthan what is specified in thetry/catchblock.This happens because
createEventDataconsumes some of the available gas (gasleft()), effectively forcing thecatchblock to trigger.If the callback does not receive the required gas, it can lead to critical issues in downstream integrations. These integrations may fail to validate or update their state, causing a desynchronization with GMX.
Recommendation
Consider validating gas left :
validateGasLeftForCallback(order.callbackGasLimit()), afterOrderEventUtils.createEventData(order)EventUtils.EventLogData memory orderData = OrderEventUtils.createEventData(order); validateGasLeftForCallback(order.callbackGasLimit());Resolution
GMX Team: Resolved.
-
H-10 High Bypassing Validations Validation Resolved
Description
MultichainGmRouter:_createShiftMultichainGlvRouter:_createGlvWithdrawalBoth functions allow users to bypass key validations in their respective handlers by leveraging Multichain routers.
Since they directly use internal utility libraries instead of handlers, they skip critical checks such as: 1. Feature availability 2. Global reentrancy modifier 3. Data list length validation
Recommendation
Consider using dedicated handlers instead of directly using internal library functions.
Resolution
GMX Team: Resolved.
-
H-11 High Zero Amount Transfer Causing Reverts Logical Error Resolved
Description
With the Multichain update, whenever the receiver is a multichain vault,
recordTransferInmust be called afterward to account for the transfer.However, since
recordTransferInreverts on zero amount transfers, GMX must always ensure amount > 0 before calling it.There are instances throughout the codebase where this check is missing, causing a revert if the amount is zero, leading to a DoS risk for the related actions.
Affected Areas: 1. Multichain Decrease Orders
- Multichain Withdrawals
- Residual Fee Transfers
There are other occurrences, such as in swaps, but these are less likely to trigger the issue due to existing validations (e.g.,
minOut).Recommendation
- If reverting on zero amount transfers is unnecessary, consider returning early instead of reverting to
avoid potential DoS risks.
- If reverting is essential, ensure an
amount > 0check is present everywhererecordTransferInis
called.
Resolution
GMX Team: Resolved.
-
H-12 High Decrease Impact Pool Cap Perturbs Price Impact Logical Error Partially resolved
Description
When a position is decreased the positive price impact for the decrease is capped at the amount in the price impact pool. Later on the total price impact (decrease + pending) is capped again at the amount in the price impact pool.
Capping the total price impact is necessary but capping the decrease price impact can lead to loss for the user. For example:
- The amount in the price impact pool is 200
- A user closes a position with a pending price impact of -300
- The user receives a price impact of 300 for the decrease
- Therefore the total price impact the user should receive is 300 - 300 = 0
- But in reality, the user receives a negative price impact as the price impact from the decrease was
capped at the amount in the price impact pool (200)
- Therefore the user received 200 - 300 = -100 price impact
As we can see this cap is unnecessary before calculating the total price impact, as the total price impact would not even have touched the price impact pool. This could therefore punish users who balance the market and therefore have a negative effect on the health of the system.
Recommendation
Remove the capping of positive price impact for the execution of the decrease order and rely only on the capping of
totalImpactUsd. The effective price impact of the decrease order used for execution price should then be calculated as:decreaseOrderImpact = totalCappedImpactUsd - pendingImpactUsdThis ensures the correct validation of the user’s acceptable price.
Resolution
GMX Team: Partially Resolved.
-
M-01 Medium Execution Not Paid When Freezing Orders Logical Error Resolved
Description
During
OrderUtils.freezeOrder, theexecutionFeeis set to 0. This is the same value passed toGasUtils.payExecutionFee. which early returns if its 0.Therefore, the order keeper does not get paid the execution fee, and the user does not receive the refund.
This opens the possibility of gas griefing the keeper, where a malicious user can update their order, force it to fail and become frozen; keeper receives no compensation.
Recommendation
Cache the current order execution fee before setting it to 0, and pass that value to
payExecutionFeeinstead.Resolution
GMX Team: Resolved.
-
M-02 Medium Pending Price Impact Affects pnlToPoolFactor Logical Error Acknowledged
Description
Previously, whenever a position was opened, the price impact was realized instantly. And the pool as a whole used to realize the PnL depending on the price impact. For example, if the price impact was positive, the trader’s overall PnL in
getPoolValueInfowould decrease, reducing the value of the market token. If the price impact was negative, the trader’s overall PnL would increase, distributing that profit to LPs over time. This was enforced using the corresponding change inopenInterestInTokenswhile positions were opened.However, now that traders receive a
sizeDeltaInTokensin correspondence with the market price,openInterestInTokens* indexTokenPriceis exactly equal toopenInterestSizefor that particular trader, and the price impact incurred is stored as pending. This pending price impact, depending on its sign, is nothing but profit or loss for the pool as far as LPs are concerned. Since it is not considered in functions likegetPnl(), this causes several issues around thepnlToPoolFactorwhich affects several areas of the exchange.isPnlFactorExceeded
This function may report false positives or false negatives, leading to unintended consequences: In cases where a user has large positive pending price impact ADLs may be allowed when they shouldn’t be or disallowed when they should be. Considering both their PnL and pending price impact, their position should now be auto-deleveraged. But since
getPnl()does not consider the pending price impact, the ADL wouldn’t go through.This can impact the deposit and withdrawal behavior as well, because
isPnlFactorExceededdoes not verify whether pending price impacts on positions push the pool beyond the allowed PnL factor. As a result, actions like withdrawals or deposits may still be processed even if theWithdrawalPnLFactororDepositPnLFactorhas already been exceeded.getPositionPnlUsd
Previously, price impact was already factored into PnL, ensuring a trader’s PnL-to-pool ratio stayed within the specified limits. Now, traders would no longer be guaranteed to be capped at a predefined max PnL-to-pool factor percentage.
getPnlToPoolFactor
The value could be either lower or higher than reality, creating potential problems for integrators. Integrators rely on
getPnlToPoolFactorreturning what they consider the correct value. However, with the v2.2 change incorporating pending price impact, this creates a discrepancy between the state assumed by integrators and the actual state in GMX, potentially leading to costly mismatches.Recommendation
Consider factoring in pending price impacts within
openInterestInTokens, ensuring thatgetPnl()accurately reflects the current state of the pool.Resolution
GMX Team: Acknowledged.
-
M-03 Medium Missing Max Data List Validation Validation Resolved
Description
The
executeAtomicWithdrawalfunction does not perform thevalidateDataListLengthvalidation on thedataListprovided on the withdrawal object.This may cause issues for integrations which do not expect the
dataListto be above the max length in the callback, or experience unexpected reverts due to OOG of performing memory operations with a sufficiently largedataList.Recommendation
Perform the
validateDataListLengthvalidation in theexecuteAtomicWithdrawalfunction.Resolution
GMX Team: Resolved.
-
M-04 Medium Incorrect Modifier In bridgeIn DoS Resolved
Description
The
bridgeInfunction has theonlyGelatoRelaymodifier; however, this function is intended to be called by users via a multicall to record their balances.Recommendation
Remove the
onlyGelatoRelayand relevant validations forbridgeInResolution
GMX Team: Resolved.
-
M-05 Medium Missing dataList Validation In GlvHandler Validation Resolved
Description
The
createGlvWithdrawalfunction in theGlvHandlercontract does not validate thedataListlength, unlike the deposit function.Recommendation
Call
validateDataListLengthduring thecreateGlvWithdrawalfunction as well.Resolution
GMX Team: Resolved.
-
M-06 Medium Missing withOraclePricesForAtomicAction Modifier DoS Resolved
Description
The relay fee can be paid using a token other than wnt by swapping it in the
_swapFeeTokensfunction, which requires token prices to be set.However, actions requiring a fee token swap in
MultichainGlvRouter,MultichainGmRouter, andMultichainTransferRouterlack thewithOraclePricesForAtomicActionmodifier. As a result, token prices are not set for the swap, causing fee payments to fail.Recommendation
Add the
withOraclePricesForAtomicActionmodifier and set prices for atomic swaps.Resolution
GMX Team: Resolved.
-
M-07 Medium Bridged Amount Lower Than Expected DoS Resolved
Description
Stargate Protocol is used to bridge funds in and out to Arbitrum/Avalanche. When bridging out, the
prepareSendfunction is in charge of calculating the bridge fee and the amounts sent and received in Local Decimals asOFTReceipt{amountSentLD,amountReceivedLD}.However, the amount requested by the user might not be equal to the amountSentLD, for the following reasons:
where
amountSentLDis rounded down for tokens decimals not equal to 6 (shared decimals)Consequently, this issue causes impacts in both
ERC20and ETH bridging:ERC20: Usingapprove()may cause a DoS for someERC20tokens if stargate does not transfer all
amount, and allowance is non-zero after bridging.
- ETH: Stargate will treat the precision lost in the amount as excess bridge fee and refund this to the
account directly.
Recommendation
Instead of using the
amountfunction param, consider using theOFTReceipt.amountSentLDfor the approval and theSendParam.amountLDstruct.Resolution
GMX Team: Resolved.
-
M-08 Medium Missing Refund In externalCalls For Multichain Logical Error Resolved
Description
Multichain users have the option to use the
externalHandlerto perform actions outside of GMX. This can be utilized to use Uniswap swap feature and get a better output amount.However, the
_handleRelayFeeonly handles scenarios where 100% of thefeeTokenis converted to WNT (i.e.swapExactIn).In case other actions are used (i.e.
swapExactOut), the handler will refund both thefeeTokenandwntto the GMX router.After the external calls execution, only the
wntamount is recorded in the multichain balance as residual fee.Even if the external call has a
refundTokenandrefundReceiverset for thefeeToken, it can only be sent to the user, but never recorded in the user's multichain balance.Recommendation
After the external call is executed, consider verifying the contract's balance for the
feeToken, send it to themultichainVaultand record the transfer for the user's balance.Resolution
GMX Team: Resolved.
-
M-09 Medium Inability To Pay Fee From Order Collateral Logical Error Resolved
Description
Multichain users are allowed to pay for fees using their Multichain balance, order collateral or position collateral.
In case of pending swap orders or limit orders without position, it should only allow payment from collateral in the order, as positions do not exist for these orders.
However, the function reverts with
UnableToPayOrderFeeifrelayParams.fee.feeToken =position.collateralToken().This check is performed before collateral is deducted from the order. As
position.collateralToken()isaddress(0)for the orders mentioned above, it will always revert.Recommendation
Consider validating the
feeTokenagainst theorder.initialCollateralToken()instead.Resolution
GMX Team: Resolved.
-
M-10 Medium updateOrder() Doesn’t Update dataList Logical Error Acknowledged
Description
Currently,
updateOrderdoesn't allow updating thedataList, a feature GMX intends to allow.Recommendation
Consider allowing updation of
dataListinupdateOrderwith the validation ofvalidateDataListLengthResolution
GMX Team: Acknowledged.
-
L-01 Low Missing Max Fee Multiplier Validation Validation Resolved
Description
In the
validateRangfunction in theConfigUtilsfile theMAX_EXECUTION_FEE_MULTIPLIER_FACTORvalidation has been errantly removed.Recommendation
Add the
MAX_EXECUTION_FEE_MULTIPLIER_FACTORvalidation to the end of thevalidateRangefunction in theConfigUtilsfile.Resolution
GMX Team: Resolved.
-
L-02 Low Unused _validateRange Function Superfluous Code Resolved
Description
In the
Configcontract the_validateRangefunction is no longer used and can be removed.Recommendation
Remove the
_validateRangefunction from theConfigcontract.Resolution
GMX Team: Resolved.
-
L-03 Low Missing Params In Natspec Documentation Resolved
Description
With the new addition of
dataListandsrcChainIdmultiple functions are missing theNatSpecfor these new params.Recommendation
Consider reviewing all functions with these new params and add the corresponding
NatSpec.Resolution
GMX Team: Resolved.
-
L-04 Low Unnecessary baseSizeDeltaInTokens Declaration Superfluous Code Resolved
Description
In the
getExecutionPriceForIncreasefunction on line 694 thecache.baseSizeDeltaInTokensvalue is unnecessarily referenced, with no use.Recommendation
Remove this reference to the
cache.baseSizeDeltaInTokensvalue.Resolution
GMX Team: Resolved.
-
L-05 Low Incorrect Rounding In _getProportionalImpactPendingValues Rounding Resolved
Description
In the
_getProportionalImpactPendingValuesfunction theproportionalPendingImpactAmountis always rounded towards zero.For cases where positions have a positive pending impact amount this correctly rounds down the user’s positive impact.
However for cases where the user experiences negative impact, the protocol should round in the direction of a larger magnitude of negative impact rather than a smaller magnitude.
Recommendation
Consider using an overloaded
mulDivfunction call which accepts aroundUpMagnitudeboolean to round the magnitude up only in the case where users experience a negative price impact.Resolution
GMX Team: Resolved.
-
L-06 Low Auto-Deleveraging (ADL) Reverts Logical Error Acknowledged
Description
In the current version of GMX, while executing ADL, GMX first executes the order and then checks whether the
pnlToPoolFactorhas improved. However, due to howpnlToPoolFactoris validated, this process can lead to unnecessary reverts for ADLs.pnlToPoolFactor = PnL / poolValueUSD(excluding PnL and the position impact pool)Example Scenario: Initial state of the pool:
- PnL = 30
- Pool Value = 100
pnlToPoolFactor = 30 / 100 = 0.3
After Auto-Deleveraging (ADL) a Position with 5 PnL:
- New PnL = 25
- New Pool Value = 95
nextPnlToPoolFactor = 25 / 95 = 0.26
Since 0.3 > 0.26, the condition passes, and ADL proceeds as expected. Now, consider a scenario where closing a position benefits from a positive price impact. In this case, the denominator (pool value) in
nextPnlToPoolFactordecreases further, inflating the ratio and potentially triggering an unnecessary revertExample with Positive Price Impact:
- Trader receives a positive price impact of 15
- Adjusted Pool Value = 80
nextPnlToPoolFactor = 25 / 85 = 0.3125
Since 0.3125 > 0.3, the revert condition triggers, blocking the ADL. These cases are rare as the decrease in PnL from large profitable positions will in the majority of cases overshadow the decrease in pool value resulting from positive impact.
However with the addition of the pending price impact it is more likely that this edge case arises because any previously unrealized positive price impact now gets realized during the decrease.
Recommendation
Be aware of the increased likelihood of this edge case resulting in blocked ADL orders. Otherwise consider accounting the positive price impacts in the validations done post ADL order execution to allow the ADLing of positions which meet this edge case.
Resolution
GMX Team: Acknowledged.
-
L-07 Low Pending Price Updates Affect Reserve Validations Logical Error Acknowledged
Description
Previously, whenever a position was opened, the price impact was realized instantly. As a result, traders either received a higher or lower number of index tokens based on the current market price, with a corresponding change in
openInterestInTokens.However, now that traders receive an execution price equal to the market price,
openInterestInTokensis either inflated or deflated compared to its actual intended value. This is later adjusted through the pending price impact when the position is closed.However, other actions that depend on
openInterestInTokensbefore the user closes their position would be affected in the interim. For example, methods usinggetReservedUsd, which directly depends ongetOpenInterestInTokens, or vaults/strategies built on top of GMX that referencegetOpenInterestInTokens.Now, depending on the pending impact across all positions, the
getReservedUsdfunction would return:- Inflated
reservedUsdif the sum of pending price impacts is negative - Deflated
reservedUsdif the sum of pending price impacts is positive
This would result in the reserve validations with
validateReserveandvalidateOpenInterestReserveeither allowing actions when they technically should not be allowed or not allowing actions when they technically should be allowed.Recommendation
Be aware of this change in behavior and consider accounting for the total pending impact in the reserve validation if parity with the previous version is desired for these validations.
Resolution
GMX Team: Acknowledged.
- Inflated
-
L-08 Low Pending Price Updates Affect Borrowing Fees Logical Error Acknowledged
Description
Previously, whenever a position was opened, the price impact was realized instantly. As a result, traders either received a higher or lower number of index tokens based on the current market price, with a corresponding change in
openInterestInTokens.However, now that traders receive an execution price equal to the market price,
openInterestInTokensis either inflated or deflated compared to its actual intended value. This is later adjusted through the pending price impact when the position is closed.However, other actions that depend on
openInterestInTokensbefore the user closes their position would be affected in the interim. For example, methods usinggetReservedUsd, which directly depends ongetOpenInterestInTokens, or vaults/strategies built on top of GMX that referencegetOpenInterestInTokens.As a result, traders may end up paying higher or lower borrowing fees than they should. Since borrowing fees are directly proportional to the ratio of reserves to pool value, incorrect reserve calculations can cause deviations in borrowing fees.
Recommendation
Be aware of this change in behavior and consider accounting for the total pending impact in the borrowing calculations if parity with the previous version is desired for borrowing rates.
Resolution
GMX Team: Acknowledged.
-
L-09 Low Param dataList Not Removed In Withdrawals Logical Error Resolved
Description
The
WithdrawalStoreUtilscontains logic to set and get the newdataListparam, but there is not a logic to remove it from storage.Although there is no clear impact besides gas related issues, this param should be removed to avoid future conflicts, just like its removed in all other actions.
Recommendation
Add the
removeBytes32ArrayinWithdrawalStoreUtils.remove()to removedataListfrom storage.Resolution
GMX Team: Resolved.
-
L-10 Low Misleading Comment For Position Fee Factor Documentation Resolved
Description
When increasing or decreasing a position there are cases where
balanceWasImproved = truebut thepriceImpactUsdis negative.This happens when the position incurs both positive and negative price impact values and the negative price impact factor is larger than the positive impact factor.
The
balanceWasImprovedis used ingetPositionFeesAfterReferral()when thepositionFeeFactoris calculated. According to the commentin this case the fee factor for the negative price impact wouldbe charged.However, if
balanceWasImprovedis true, then the POSITIVE position fee factor is used, so the comment above is incorrect. The same issue applies togetSwapFees.Recommendation
Update the comment about the
positionFeeFactor:in this case the fee factor for the positive price impact would be chargedfor the case whenpriceImpactUsdis negative andbalanceWasImproved.Resolution
GMX Team: Resolved.
-
L-11 Low Multichain Routers Can Be Used From Local Chain Logical Error Partially resolved
Description
The Multichain routers are supposed to be used in a Multichain context but can be used from the local chain. This might lead to unexpected behaviour or could introduce bugs in the future.
Recommendation
Add a check that the given
srcChainIdis not zero or the local chain id.Resolution
GMX Team: Partially Resolved.
-
L-12 Low Account Receives Fee Refunds Logical Error Resolved
Description
During the
stargate.send()call, theaccountis set as the refund address. In case there is any surplus sent for the bridge fee, this refund address will receive native tokens back.The current implementation sets
refundAddressas theaccount, but as this is a multichain transaction, all funds should be credited to the multichain balance instead.The fee quote and actual message sending is done in the same transaction, so it's not likely that there will be any refunds. Nevertheless, if there is any refund in the future, the
accountaddress should not receive it.Recommendation
The
stargate.sendreturns aMessagingReceipt. This should be used to verify if there was any refund.Consider setting the
MultichainVaultas therefundReceiverand trigger aMultichainUtils.recordTransferInif there is a difference from the amount paid and thenativeFeesent.Resolution
GMX Team: Resolved.
-
L-13 Low Missing Event In bridgeIn Events Resolved
Description
The
bridgeInfunction in theMultichainTransferRoutercontract callsMultichainUtils.recordTransferIninstead ofMultichainUtils.recordBridgeIn. Therefore theemitMultichainBridgeInevent is not emitted.Recommendation
Use
MultichainUtils.recordBridgeIninstead or emit abridgeInevent in thebridgeInfunction.Resolution
GMX Team: Resolved.
-
L-14 Low Multichain Balance Lost For Contracts Validation Resolved
Description
Smart contracts / smart accounts are not able to sign transactions and therefore can't use the Multichain functions. This means when a smart contract receives funds in the Multichain vault the funds are lost forever.
Not every end user might be aware of this fact and know how the Multichain feature works on such a technical level. This can therefore likely lead to user mistakes with big financial consequences.
For example:
- A user is active on multiple chains and also Arbitrum and Base (like most traders in the web3
space)
- The user has liquidity on Base and wants to use it in GMX on Arbitrum and decides to use the new
Multichain feature to do so
- The user opens a position and decides to close it after a while
- As the user also has a smart wallet on Arbitrum which he usually uses for GMX trades he decides
to save himself the bridging fees and set the smart wallet as the receiver of the decrease order
- The funds are lost forever as the smart wallet can't interact with the Multichain functions
Recommendation
Add a checks to ensure that the order receiver is not a smart contract.
Resolution
GMX Team: Resolved.
-
L-15 Low Same Hash If srcChainId = 0 Or = Block.chainid Validation Acknowledged
Description
The
BaseGelatoRelayRouter._validateCallfunction treats a multichain transaction withsrcChainId =0equal to a multichain transaction withsrcChainId = block.chainid.This is a dangerous pattern as the system checks if an order is local or multichain based on a
srcChainId = 0check and could therefore lead to exploits in future versions of the protocol.Recommendation
Remove this line in
_validateCall:uint256 _srcChainId = srcChainId = 0 block.chainid : srcChainId;and revert instead if the givensrcChainIdis the current chainResolution
GMX Team: Acknowledged.
-
L-16 Low Typo In MultichainGmRouter Best Practices Resolved
Description
"...will be recorder as relay fee" comment in line 73 of
MultichainGmRoutershould be recorded.Recommendation
Fix the typo.
Resolution
GMX Team: Resolved.
-
L-17 Low Balance Decreased After Transfer Best Practices Resolved
Description
In
MultichainsUtils.transferOut, tokens are transferred first, and then the balance is decremented, which is contrary to best practices.multichainVault.transferOut(token, receiver, amount); dataStore.decrementUint(Keys.multichainBalanceKey(account, token), amount);Recommendation
Consider following the CEI pattern and transferring tokens only after decrementing the balance.
Resolution
GMX Team: Resolved.
-
L-18 Low Inconsistent Use Of srcChainId For Events Events Resolved
Description
The
MultichainUtils.recordTransferInis used to record funds sent to themultichainVault, and add them to the user's tracked balance. ThesrcChainIdparam is only used to emit an event with the id of the action's source chain.However, there are multiple parts of the code where this
srcChainIdis hardcoded to 0 and some others where the action'ssrcChainIdis used (i.e. used when executingglvDepositbut not used when executing a normal deposit).Although there is no major impact in the contract, events will be emitted with the wrong param for multichain users.
Additionally, according to the changelog doc, in the future
the balance for contracts will be separatedby chainId. Therefore, this param can actually have an impact if its value is set to 0 or thesrcChainId.Recommendation
Send the action's
srcChainIdto therecordTransferInso the event is emitted with the correct value. Alternatively, if the idea of hardcoding thesrcChainIdto 0 was to point out where the call originated from (normal action or Gelato multichain), be sure to be consistent with the value used during executions.Resolution
GMX Team: Resolved.
-
L-19 Low Token Permits Allowed For Multichain Logical Error Resolved
Description
Multichain actions will utilize user's Multichain balance collateral. The
MultichainRouteroverrides the_sendTokensso that it will useMultichainUtils.transferOutinstead ofrouter.pluginTransfer.Consequently, there is no need for token permits, specially because user is not suppose to have funds in the protocol's chain. Additionally, this permit will be left unused.
Recommendation
For multichain actions, verify that
tokenPermits.length = 0.Resolution
GMX Team: Resolved.
-
L-20 Low Allowed _handleFeePayment For Non-Multichain Validation Acknowledged
Description
The
_handleFeePaymentfeature allows Multichain users to use position's and order's collateral to pay for relay and execution fee.This is not a feature enabled for non-multichain routers. However, there is no validation for
srcChainId, so users can create a gasless transaction withsrcChainId = 0, and freely move collateral from order to multichain balance.Recommendation
If this is intended behavior, consider documenting it for users. Otherwise, validate if
srcChainIdis non zero and equal to theorder.srcChainId.Resolution
GMX Team: Acknowledged.
-
L-21 Low Incorrect Amount In emitMultichainBridgeOut Events Resolved
Description
The
bridgeOutfunctionality will emit an eventemitMultichainBridgeOutafter the bridge transaction is made using Stargate.However, there are cases where the amount requested is not the amount actually sent, due to rounding or path limits, so the event will be emitted with the incorrect amount.
Recommendation
Return the amount actually sent through Stargate in the
LayerZeroProvider.bridgeOut(), and use this amount in the event emitted.Resolution
GMX Team: Resolved.
-
L-22 Low Warning About Native Tokens Documentation Resolved
Description
The
LayerZeroProviderand Multichain contracts are not designed to hold native assets. However, Stargate supports native token transfers, allowing users to initiate transfers from any source chain to a destination chain.As a result, while Stargate will accept the user's native tokens on the source chain, the
LayerZeroProviderwill be unable to receive these tokens on the destination chain, leading to a loss of funds for the user.Recommendation
Document this behavior and warn users against transferring native tokens via Stargate.
Resolution
GMX Team: Resolved.
-
L-23 Low Stargate Has Limited Support For Token Transfers Logical Error Resolved
Description
Stargate has limited support for tokens that it can transfer. Through withdrawals and swaps users can obtain tokens that cannot be bridged back to their original chains.
This will force users to open additional positions in order to swap their funds to the proper assets, and can cause them to face price impact, slippage, and fees.
Recommendation
Add a function that will allow users to create swaps with their multichain vault balances. Additionally, document to users that they should verify the tokens they receive can be bridged back to their desired blockchain.
Resolution
GMX Team: Resolved.
-
L-24 Low Execution Fee Params Estimated Twice Gas Optimization Resolved
Description
During order creation, the
estimatedGasLimitandoraclePriceCountis calculated once, stored in the cache struct, but thenvalidateAndCapExecutionFeedoes not use these values, as it calculates them again.Recommendation
Use the cache values from previous calculation in
validateAndCapExecutionFee.Resolution
GMX Team: Resolved.
-
L-25 Low Native Tokens Not Supported In bridgeIn Logical Error Resolved
Description
The
bridgeOutflow supports bridging native tokens using theStargatePoolNativecontract.However, there is no support to bridge these token in, as it will require a
receivefunction in theLayerZeroProvidercontract, and an alternate flow to wrap ETH and record it as WETH balance inmultichainVault.Recommendation
Consider adding a receive function and handle special case for
token = address(0), wrapping the ETH received and recording the WETH inmultichainVault.Resolution
GMX Team: Resolved.
No findings match.
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.
