GMX engaged Guardian to review the security of updates to it’s synthetic assets exchange. From the 6th of May to the 27th of May, a team of 7 auditors reviewed the source code in scope.
- Published
- Review window
- May 6 to 27, 2024
- Language
- Solidity
- Chains
- Arbitrum, Avalanche
- Sector
- Perpetuals
- 3 Critical
- 9 High
- 9 Medium
- 24 Low
- 0 Informational
Scope
Overview
GMX engaged Guardian to review the security of updates to it’s synthetic assets exchange. From the 6th of May to the 27th of May, a team of 7 auditors reviewed the source code in scope.
Findings 45
-
C-01 Critical External Call Gas Adjustment DoS’s Liquidations Logical Error Resolved
Description
Proof of concept: PoC
In the
clearAutoCancelOrderfunction thecancelOrderfunction is invoked from within theOrderUtilslibrary, however thecancelOrderfunction will account for an external delegatecall being made as it subtractsgasleft() / 63from thestartingGasamount. No external call will be made as thecancelOrderfunction is being called from within the context of theOrderUtilsfile and therefore thecancelOrderfunction will be inlined in theclearAutoCancelOrderfunction.This errantly reduces the
startingGasused to measure the gas expenditure for the keeper while cancellingautoCancelorders. As a result any attempt to cancelautoCancelorders will revert as thestartingGashas been reduced such that it is now below thegasleft(). Therefore any positions withautoCancelorders cannot be closed as long as those orders exist, resulting in un-liquidatable positions.Recommendation
Add a
shouldAdjustStartingGasparameter to thecancelOrderfunction to account for when thecancelOrderfunction is invoked via delegatecall vs. function inlining.Resolution
GMX Team: Resolved.
-
C-02 Critical AutoCancellation Prevents Liquidations Logical Error Resolved
Description
During liquidation
OrderUtils.executeOrderwill be called. This function will check if this is a decrease order and ifsizeInUsdis 0 after the order execution, if so will callclearAutoCancelOrders. Indeed liquidation order is a decrease order andsizeInUsdafter liquidation will be 0, hence AutoCancellation mechanism will always be triggered with liquidations.AutoCancellation will check for all stop-loss/take-profit orders available for that position which can be as high as 10 in current configuration.
This cancellation can do possibly 10 callback calls with sending 2.000.000 gas for each. Additionally, for every cancellation the gas usage is around 600.000. When we also considered the fee refund mechanism's callback call which per call will send 500.000 gas, we can possibly reach total amount of 31.000.000 and more when considering liquidation's gas usage itself. So a liquidation order might require more than 31.000.000 gas which is more than block gas limit in avalanche and also can be possibly problematic in Arbitrum because it is expected from keepers to provide this amount of gas while it is not ensured the keeper provided sufficient amount of gas for all these actions.
Recommendation
Multiple steps are required to resolve the issue in its entirety:
- Either decrease the max auto cancel amount or further restrict gas usage for cancellation
callback. Combination of both can also be used. In the end, it is crucial to check maximum possible gas usage for AutoCancellation related actions.
- Include auto cancellation's gas usage when checking keeper's provided gas amount especially
when it is a liquidation order.
Resolution
GMX Team: Partially Resolved.
-
C-03 Critical shiftGasLimitKey Returns Incorrect Gas Limit Key Logical Error Resolved
Description
In the
Keys.solfile, theshiftGasLimitKeyfunction returns theWITHDRAWAL_GAS_LIMITkey instead of theSHIFT_GAS_LIMITkey.Therefore the estimated execution fee for shift actions will be significantly smaller than it ought to be, as a shift includes not only a withdrawal but a deposit action as well.
This will lead to the protocol keeper being unexpectedly drained of the native token, potentially stopping execution on the exchange for a period of time. The gas draining can occur due to regular exchange usage or can be easily leveraged by an attacker to maliciously drain the keeper.
Recommendation
Return the
SHIFT_GAS_LIMITkey in theshiftGasLimitKeyfunction.Resolution
GMX Team: Resolved.
-
H-01 High Borrowing Fees Increase Based On Incorrect Rate Logical Error Resolved
Description
When a position is updated the
updateFundingAndBorrowingStatefunction is called which updatesCUMULATIVE_BORROWING_FACTORbased on the recent rate as well as the time since the last update. The rate of which theCUMULATIVE_BORROWING_FACTORwill increase is dependent on what percentage of the pools liquidity is being borrowed. The higher the percentage the higher the rate. By updating theCUMULATIVE_BORROWING_FACTORbefore any changes to the state that could affect rate the borrowing fees can correctly be calculated.The issue however is that this is not the case everywhere. When a user withdraws or deposits they will change the rate. As they withdraw the rate will increase and as they deposit the rate will decrease. However because the
CUMULATIVE_BORROWING_FACTORis not updated before a deposit/withdraw, the next time it is updated the rate will use the new value not the value that was actually representative of the elapsed time. Leading to excessive fees being charged if a withdraw occurs, or insufficient fees being charged if a deposit occurs.With protocols integrating into GMX large deposits or withdraws will occur which will have a larger impact on the inaccuracy of the fees. The excessive fees being charged would lead to near liquidateable positions to become unexpectedly pushed to a liquidateable state. This step-wise jump in borrowing fees can also lead to arbitrage opportunities where attackers can profit off the inaccurate jump by making timely orders and deposits.
Recommendation
Update the Funding and Borrowing state early in the
executeDepositandexecuteWithdrawalfunctions.Resolution
GMX Team: Resolved.
-
H-02 High Atomic Providers Cannot Be Configured Logical Error Resolved
Description
In the
setAtomicOracleProviderAfterSignalfunction theisOracleProviderEnabledKeyis used instead of theisAtomicOracleProviderKey.As a result the
atomicOracleProvidervalue cannot be set, preventing any price feed provider from atomic withdrawal use, and disallowing the atomic withdrawal feature.Recommendation
Change the
isOracleProviderEnabledKeyto theisAtomicOracleProviderKey.Resolution
GMX Team: Resolved.
-
H-03 High Users Can Use Shift To Avoid Deposit Fees Logical Error Resolved
Description
Proof of concept: PoC
Users can avoid the
SwapPricingType.TwoStepfee on deposits by utilizing shift and its lack of fees. Inside_executeDeposit,SwapPricingUtils.getSwapFeescalculates the fee that users would pay for depositing into the exchange. Where there are 3 fees -TwoStep,Atomic, and 0 fee for shifts.Users can abuse the lack of fee for shifts by simply front-running the keeper and sending their desired tokens to the
shiftVault. When the keeper callsexecuteShift, these tokens would be accounted for as deposited from the shift whenrecordTransferInis executed. This way, users can avoid paying theSwapPricingType.TwoStepfee.For Example:
- User makes a deposit of 1 USDC into the USDC:WETH vault.
- User creates a shift for the same market.
- User sees keeper TX and front-runs with 10,000 USDC and 2 WETH.
- Keeper executes shift:
- In the middle of the shift after the withdrawal, the tokens are recorded from the ShiftVault.
- The recorded change is 10,001 USDC and 2 WETH.
- The shift deposits the USDC and WETH while avoiding the fee.
Recommendation
Call
shiftVault.recordTransferInfor the long and short tokens when startingexecuteShiftto account for any tokens sent directly to it.Resolution
GMX Team: Resolved.
-
H-04 High Cant set data stream through config Validation Resolved
Description
When the
setDataStreamfunction is called there is a check that ensures the dataStream is not already set. However the check incorrectly does not reference thedataStoreinstead the check only uses theKeys.dataStreamIdKey(token)key. Which will never return 0. Which means the check will always fail and Data Streams will not be able to be added via the config.Recommendation
Reference Data Store when checking if the stream has already been added.
Resolution
GMX Team: Resolved.
-
H-05 High GMOracleProvider Reverts Due To Incorrect Validation Logical Error Resolved
Description
GMOracleProviderget signed prices from Oracle keepers and tries to validate them withvalidateSigner(). What is expected from Order Keepers is providing both minPrices and maxPrices in ascending order for a token.The problem occur because these minPrices and maxPrices are validated against their corresponding index in the signers and signatures array. But it is not guaranteed and in most cases won't be possible to both sort minPrices and maxPrices while protecting their corresponding signatures in the correct index. Consider the following scenario:
- Oracle Keeper 1 signs minPrice = 1003, maxPrice = 1010
- Oracle Keeper 2 signs minPrice = 1004, maxPrice = 1011
- Oracle Keeper 3 signs minPrice = 1005, maxPrice = 1009
Here when Order Keeper sort both prices in ascending order they will be sorted as follows: minPrice =
[Keeper1 minPrice, Keeper2 minPrice, Keeper3 minPrice]maxPrice =[Keeper3 maxPrice, Keeper1 maxPrice, Keeper2 maxPrice]Hence in
validateSignercall, prices and respective signatures won't match and call will revert. If on the other hand Order Keeper does not sort the order as above, then sorting check will fail and transaction will again revert.Recommendation
Use old indexing system that matches min/maxPrice to their corresponding signers before validating signer.
Resolution
GMX Team: Resolved.
-
H-06 High Gas Validation Does Not Account For Callback Gas Logical Error Resolved
Description
When keeper performing the actions, if for any reason executions are failed, it is caught in their respective
_handleErrorfunctions. These functions before checking the reason for execution error and handling the cancellation logic, calls gasUtils'validateExecutionErrorGasfunction to check if execution trace can be followed till the end with currentgasLeft.The problem is, the variable that is checked against gasLeft is
MIN_HANDLE_EXECUTION_ERROR_GASand it is configured as 1.200.000 and did not take into account cancellation's callback gas usage. So this check can pass while gas provided by keeper can be fully used in the concurrent process and that can lead to forwarding less than enough gas to callback contracts which would create unexpected silent reverts for systems integrating with GMX V2, which in many cases could cause a loss of funds or protocol disruption for those integrators.The same situation also applies to
getExecutionGas()and it's corresponding variable:MIN_HANDLE_EXECUTION_ERROR_GAS_TO_FORWARDwhich is configured as 1.000.000 AdditionallyREFUND_EXECUTON_FEE_GAS_LIMITalso is not accounted which should be accounted similarly.Recommendation
When checking gas to see if it would be enough to handle executions, take into account the gas usage for callbacks.
Resolution
GMX Team: Resolved.
-
H-07 High MarketSwap Orders May Use Unexpected Prices Validation Partially resolved
Description
In the
processOrderfunction for swap orders, the price timestamp validation includes nomaxOracleTimestampvalidation forMarketSwaporders.As a result a keeper may accidentally or maliciously execute a
MarketSwaporder when it is far past its request expiration age, with prices that are significantly unfavorable for the user.Recommendation
When executing a
MarketSwaporder be sure to validate that themaxOracleTimestampis not above the order’srequestExpirationTime.Resolution
GMX Team: Partially Resolved.
-
H-08 High Atomic Withdrawal Feature Unsable Logical Error Resolved
Description
During
executeAtomicWithdrawalmodifierwithOraclePricesis used which when we follow the call trace we will reach the_validatePricesfunction inOracle.sol. In this function we check the provider for the given tokens to validate that it is indeed the same provider given by keeper.The problem is, the normal providers for tokens will be the
dataStreamProviderwhile for atomic withdrawals they will be thepriceFeedProvider. Hence keeper provided provider won't match this provider in dataStore which will lead to reverts for all atomic withdrawal calls.Recommendation
If the action is atomic withdrawal instead of comparing the provided provider with
oracleProviderForTokenKey, compare it withisAtomicOracleProviderKey.Resolution
GMX Team: Resolved.
-
H-09 High Migration Inconsistencies Logical Error Acknowledged
Description
According to documentation there is a time where both old contracts and new contracts are live and used by keepers. This can create massive inconsistency in the system such as:
- Borrowing fee calculations are different between systems which will lead to both different
borrowing fee's for users between systems and also different market token pricing's for the system which can have catastrophic effect.
- While old system continue to send excess fees to
account, new system will send them toreceiver.
Which can be especially problematic for integrators who changed their system according to new implementation and can't send excess fees to
receiveranymore.- Take profit and stop-loss orders that are opened with new contracts will be added to
autoCancelList but if the position is closed/liquidated with the old keeper, this lost won't be cleared. Which in turn, users can experience unexpected position openings in the future if they continue to use the system.
Recommendation
Try to not use both contracts at the same time, if they will be used, be sure to not set
optimalBorrowingFactoruntil old system abandoned and also inform users and integrations about inconsistencies that may arise because of this situation.Resolution
GMX Team: Acknowledged.
-
M-01 Medium makeExternalCalls Unexpected Funds Receiver Logical Error Acknowledged
Description
When
makeExternalCallsis used a batch of target contracts are called. This is used to allow users to interact with external contracts to perform a variety of operations.After the contracts are called
makeExternalCallswill loop through an array of refund tokens and send each token to a desired recipient.The issue is that in cases where two recipients are expected to receive the same token the first recipient will receive 100% of the tokens while the second recipient will receive nothing. This is because the amount sent to each recipient is based on the
balanceOffor the specific token, which ensures the entire balance will be used on the first recipient. Resulting in some address not receiving their expected funds.Recommendation
Iterate through
refundTokenand check that there are no duplicate addresses in the array.Resolution
GMX Team: Acknowledged.
-
M-02 Medium Attack can game price impact at users expense Logical Error Acknowledged
Description
Price impact is used to incentivize bringing markets to a balanced state. However, this can be gamed by bad actors who want to profit off other users.
An attacker can monitor transactions and when they see a deposit that is going to bring the market to a balanced state they can send multiple orders that do the same thing as the victim. The attacker can set the min output amount to a value greater than the input amount so that the only way the order will execute is if the order obtained the positive price impact, the rest would revert costing the user nothing more than a portion of the execution fee.
After the attackers order is executed the victims order will also execute, but they will experience negative price impact since their order is moving the price away from balanced. The attacker can then withdraw, bringing the pool back to balance again.
The reason this attack can be effective despite it not being certain that the attackers order will execute first is because the attacker can create many orders and only one needs to beat the victim. By sending more than one order the attacker is increasing the chance that it will be executed first and making this attack both likely to succeed and profitable in certain situations. By the end of the attack the attacker was able to obtain positive price impact twice where one of those times was at the expense of the other user.
Recommendation
Consider refunding less of the execution fee upon cancellation to make these type of attacks unprofitable.
Resolution
GMX Team: Acknowledged.
-
M-03 Medium MAX_AUTO_CANCEL_ORDERS Update Risk Unexpected Behavior Acknowledged
Description
When a position is completely closed (via user or by liquidation), user's orders in
autoCancelListwill be cancelled via providingMAX_AUTO_CANCEL_ORDERSas a max index to auto cancel list. If that variable changes to a variable that is less, then some orders will stay at the unreachable part of the autoCancel list.If user continues to use the system, these orders can be executed when they are not expecting. Since auto cancellation process is gas intensive,
MAX_AUTO_CANCEL_ORDERSis a variable that can be changed more than other variables to limit the gas usage of a single call. Hence it is very possible for this problem to occur in production.Recommendation
In the case of the aforementioned variable is changed, inform users so that they can cancel their orders.
Resolution
GMX Team: Acknowledged.
-
M-04 Medium uiFee is Taken Twice During Shift Logical Error Resolved
Description
During shift,
uiFeetaken twice both in withdrawal and also in deposit. Since both deposit and withdrawal done in a single action, it should be taken only once.Recommendation
Include the
uiFeeon either the deposit or the withdraw, but not both.Resolution
GMX Team: Resolved.
-
M-05 Medium Dangerous Atomic Withdrawal Invocation Pattern Unexpected Behavior Resolved
Description
The
executeAtomicWithdrawalfunction is intended to be called by users or integrators on theWithdrawalHandlerafter depositing market tokens into theWithdrawalVaultthrough theExchangeRouter. However users are not able to use a multicall on theExchangeRouterto do so seamlessly in a single transaction, as theexecuteAtomicWithdrawalfunction is only available through theWithdrawalHandlercontract. This promotes a dangerous pattern where users may send their market tokens to theWithdrawalVaultthrough theExchangeRouterand call theexecuteAtomicWithdrawalfunction on theWithdrawalHandlerin separate transactions. In this case the user is exposed to risk of total loss of funds if another actor frontruns their second transaction toexecuteAtomicWithdrawal, by malicious intent or on accident.Recommendation
Consider only exposing the
executeAtomicWithdrawalfunctionality through theExchangeRouter, this way it can be invoked through a multicall. Additionally, be sure to document this risk to users and integrators, advising them to use the multicall feature.Resolution
GMX Team: Resolved.
-
M-06 Medium Shifts Within A Virtual Inventory Unfairly Punished Unexpected Behavior Resolved
Description
When shifting between two markets in the same virtual inventory the shift will often experience negative impact from the virtual inventory even though the shift did not cause an imbalance in the virtual inventory token amounts.
Consider the following scenario:
- Market A has a longTokenUsd of 200 and a shortTokenUsd of 300
- Market B has a longTokenUsd of 505 and a shortTokenUsd of 500
- A virtual inventory is comprised of Market A and Market B, with an aggregate longTokenUsd of 705
and shortTokenUsd of 800
- Bob shifts 20% of the Market A marketToken supply to Market B, 40 longTokenUsd and 60
shortTokenUsd are shifted
- During the withdrawal the virtual inventory diff goes from 705 - 800 = -95 to 665 - 740 = -75
- During the deposit the MarketB diff goes from 505 - 500 = 5 to 545 - 560 = -15, while the virtual
inventory diff goes from 665 - 740 = -75 to 705 - 800 = -95
- The net virtual inventory diff stays the same from the start of the shift to the end of the shift,
however the user receives increased negative impact because the deposit creates a larger imbalance in the virtual inventory than in Market B.
In this scenario the user is not causing any imbalance to the virtual inventory and should therefore not be negatively impacted by the virtual inventory diff during deposit.
Recommendation
Consider ignoring price impact from the virtual inventory when shifting between two markets that are in the same virtual inventory, as this action will never cause a further imbalance in the virtual inventory. A more complete alternative would be to consider the net virtual inventory diff created by an entire shift action, this way the virtual inventory diff created by uiFees and other potential balance changes can be accounted for.
Resolution
GMX Team: Resolved.
-
M-07 Medium setFundingRate Unexpectedly Changes Funding Unexpected Behavior Acknowledged
Description
The
setFundingRatefunction allows a permissioned address to update themaxFundingRate, which potentially caps the existing funding rate for pending funding fees.This action would affect funding fees that have accumulated in the past causing unexpected funding changes for users, which could ultimately lead to accounts being subject to unexpected liquidation as a result of receiving less funding fees than expected.
Recommendation
Update the funding state before updating the
maxFundingRatesimilar to thesetPositionImpactDistributionRatefunction distributes the impact pool.Resolution
GMX Team: Acknowledged.
-
M-08 Medium MEV Bribes Allow Griefing Of Keepers Protocol Manipulation Acknowledged
Description
On Avalanche C-Chain a user can use providers such as flashbots or snowsight to set the gas price to the lowest acceptable gas price and still have their transaction executed via a bribe.
Inside of
validateExecutionFee(), the validation for the execution fee checks if execution fee is less thangasLimit*tx.gasprice. Setting a lowertx.gapricewill allow a user to pay less than the expected amount for an execution fee and force the keeper to draw on treasury reserves to subsidize the transaction.It may be possible for a malicious actor to submit many orders this way in order to grief the keepers. Or users may use this to pay less execution fees on the exchange consistently.
Recommendation
Set a value for the lowest acceptable gas price, and verify that the value of
tx.gaspriceis greater than this value invalidateExecutionFee().Resolution
GMX Team: Acknowledged.
-
M-09 Medium Refund gas limit is not accounted for Validation Acknowledged
Description
Proof of concept: PoC
The
payExecutionFeefunction refunds the leftover gas to the user **before** executing the external callback torefundExecutionFee. This gas refund doesn't take into account the fact thatrefundExecutionFeecan use up to 500k gas. This will enable users to grief keepers, making their TX unprofitable.For Example:
- User makes an order with 3m gas as execution fee.
- Keeper executes that order with 3m gas (equal execution fee).
- We reach
payExecutionFee, where up to now 2m gas is used. - Keeper is payed 2m and the user is refunded 1m.
- The
refundExecutionFeetriggers wasting 500k gas.
In the current scenario the user only paid 2m gas for his order, but costed the keeper 2.5m gas.
Recommendation
Increase
EXECUTION_GAS_FEE_BASE_AMOUNTin order foradjustGasUsageto calculate the keeper gas properly.Resolution
GMX Team: Acknowledged.
-
L-01 Low Kink Borrowing Yields Unexpected Rate Logical Error Resolved
Description
The kink borrowing factor is meant to increase the amount a user pays in fees when the usage exceeds the optimal threshold. Specifically the borrow rate changes from a base factor, to an above optimal factor. The issue here is that the base is applied to the entire
usageFactornot just the portion that is optimal. What this means is that when the above optimal factor is applied to the portion that is beyond the threshold, that portion is double charged. Once at the base factor and then again at the above optimal factor. The current kink charge may be an unexpected jump in borrowing fees for users.Recommendation
Consider only applying the base factor to the portion that is at or below the threshold. Then apply the above optimal threshold to the portion is above the threshold.
Resolution
GMX Team: Resolved.
-
L-02 Low Migration Causes Unexecutable Orders Unexpected Behavior Acknowledged
Description
TimestampInitializer.solwill set all orders that are inORDER_LISTand all positions that are inPOSITION_LIST's related timestamps (updatedAt, increasedAt, decreasedAt) to current timestamp if they have 0 as a value as a final migration step. This will make all order's that are created at the past and not executable(becauseREQUEST_EXPIRATION_TIMEhas passed since their latest update), and doesn't cancelled yet, executable.Hence users can experience unexpected order executions because against the expected workflow, orders with
REQUEST_EXPIRATION_TIMEpassed will be executed.Recommendation
Consider specifically avoiding these orders when updating the timestamps, if they will be updated, inform users beforehand.
Resolution
GMX Team: Acknowledged.
-
L-03 Low Lacking Configuration Validations Validation Resolved
Description
Newly allowed configuration variables including
OPTIMAL_USAGE_FACTOR,BASE_BORROWING_FACTOR, andABOVE_OPTIMAL_USAGE_BORROWING_FACTORhave not been added to function_validateRange().This directly contrasts with similar configuration variables such as
BORROWING_FACTORwhich is validated in_validateRangeto not be more than 100%. Consequently, invalid values may be set for such factors.Recommendation
Add the new factors to
_validateRangeto ensure they do not exceed 100%, similar toBORROWING_FACTORResolution
GMX Team: Resolved.
-
L-04 Low Stale Orders Allow For Short Term Risk Free Trades Unexpected Behavior Acknowledged
Description
With the new oracle pricing mechanism, market orders can only be executed with prices in the range [orderUpdatedAt, orderUpdatedAt + requestExpiration]. As a result, if a market order is not executed in a timely manner it can only be executed with outdated prices, which allows malicious actors to make risk free traders. Consider the following scenario based on current configurations:
- maxPriceAge is 5 minutes
- requestExpiration is 5 minutes
- orderUpdatedAt is at t = 100
- The request expiration is at t = 105
- The current time is t = 108
In this scenario the keeper may only execute the order with prices from the range [100, 105], meanwhile the current price is at t = 108. Additionally, since the market order has passed the request cancellation period, the user may cancel their order if price has not moved in a direction that benefits them.
The most straightforward application of this is a swapOrder with a swapPath which takes advantage of these outdated prices. The baseMarketConfig has a swapFeeFactorForPositiveImpact of 0.05% and a swapFeeFactorForNegativeImpact of 0.07%, assuming the swap receives the worse feeFactor, the swap would have to net > ~0.10% gain as a result of the outdated prices to be reasonably profitable. An analysis of minute candles for ETH/USD shows that ETH often moves by 0.10% or more in a single minute, given the price can be stale by up to 5 minutes it is possible that swaps could arbitrage a 0.20%+ gain for the user.
Recommendation
Consider restricting the execution of market orders, deposits, and withdrawals past their request expiration time. Otherwise consider introducing a stale order threshold time where an order can still be executed after the request expiration time, but not after the stale order time such that the prices do not have a chance to grow stale by a number of minutes. Currently the chainlink reference oracle will not prevent such an arbitrage as the maxRefPriceDeviationFactor is 50%, another solution could be to make this deviation factor much smaller — though this may introduce unnecessarily tight validation on prices for other pricing mechanisms.
Resolution
GMX Team: Acknowledged.
-
L-05 Low Timestamp Initialization Impacts Existing Orders Unexpected Behavior Acknowledged
Description
The
TimestampInitializercontract attempts to set the timestamp on all open orders and positions to the current Arbitrum timestamp. However, this may cause orders that would have executed prior to the timestamp transition to now fail in function validateOracleTimestamp() or be executed at worse prices.Consider the following scenario: 1. Bob increases their long position at block = 1; time = 1 at price $5000 2. Bob sees price is going downwards, so Bob submits a SL order at block = 5; time = 5 for price $4,500 3. Timestamps are initialized at time = 10 when price is $4,400. Both the orderUpdatedAtTime and positionDecreasedAtTime are now time=10. 4. The SL order cannot be executed at price $4,500 but at price $4,400, since the range when the $4,500 price was available for execution was effectively erased with the timestamp initialization
Recommendation
Rather than assigning the current block timestamp to outstanding orders and positions, consider converting the existing block numbers for
orderUpdatedAtBlock,positionIncreasedAtBlock, andpositionDecreasedAtBlockto their respectiveblock.timestampsby passing in a list of the correct timestamps for each order.Resolution
GMX Team: Acknowledged.
-
L-06 Low Lacking Deposit And Withdrawal Migrations Configuration Acknowledged
Description
While there exist migrations for orders and positions there are none for deposits and withdrawals. While deposits and withdrawals will not be waiting in their respective stores for long, it may be possible that some are left in flight while the contract upgrades are made.
These old deposits and withdrawals which do not have an
updatedAtTimecannot be executed as they will fail the price validations. They will not be cancelled by the keeper as the revert will be from an oracle error, therefore the deposits and withdrawals will continue to exist in their respective stores until they are cancelled manually.Some integrating systems, such as Umami’s GMI index, do not currently implement logic to call the
cancelDepositorcancelWithdrawalfunctions on theexchangeRouter. Thus if any deposits or withdrawals would be caught in this state they would at worst cause a loss of funds as the orders would not be cancellable and at best require a logic upgrade to be able to cancel these orders manually.Recommendation
Be sure to only conduct the contract upgrade while there are no pending deposits and withdrawals. Otherwise consider implementing migrations for deposits or withdrawals.
Resolution
GMX Team: Acknowledged.
-
L-07 Low Atomic Withdrawals Cannot Be Simulated Unexpected Behavior Acknowledged
Description
In the
WithdrawalHandler, thesimulateExecuteWithdrawalfunction has been updated to accept aswapPricingTypeparameter, presumably to be able to simulate normal two-step withdrawals as well as atomic withdrawals.However the
simulateExecuteWithdrawalfunction cannot be used to simulate atomic withdrawals as these withdrawals have not been created in theWithdrawalStoreand the function attempts to retrieve the withdrawal via awithdrawalKey.Recommendation
Consider making a separate simulation function to allow users and integrators to simulate atomic withdrawals.
Resolution
GMX Team: Acknowledged.
-
L-08 Low Longs Pay Higher Borrowing Fees As Price Increases Unexpected Behavior Resolved
Description
In markets where longs are backed by the same token as the index, or when the backing
longTokenis correlated with the index, as the index price and thereservedUsdincreases so does themaxReservedUsd.In fact the
maxReservedUsdwill increase more than thereservedUsddoes, as long as the reservedUsd is less than themaxReservedUsd.Therefore traders with longs will pay a lower borrowing rate as the price of the index increases. However with the new kink borrowing model, since the
openInterestLimitis capped at a fixed USDmaxOpenInterestvalue, when themaxOpenInterestis the constricting limit traders may pay a significantly higher borrowing rate as the index price increases.Recommendation
Consider if this is the expected behavior, if so document it so trader’s can be aware of these borrowing fee dynamics.
Resolution
GMX Team: Resolved.
-
L-09 Low Lacking timestampAdjustment Configurations Configuration Acknowledged
Description
dataStreamswill be configured with atimestampAdjustmentto account for price latency from differing sources, however in thesetDataStream,signalSetDataStream, andsetDataStreamAfterSignalfunctions there is no logic to allow the configuration of atimestampAdjustmentfor adataStream.While the
ORACLE_TIMESTAMP_ADJUSTMENTkey is an allowed base key, these functions may elect to offer accessibly configuration of the adjustment as necessary.Recommendation
Add configuration support for the
timestampAdjustmentin thesetDataStreamfunction.Resolution
GMX Team: Acknowledged.
-
L-10 Low Redundant priceFeed Checks Optimization Resolved
Description
In the
_validatePricesfunction the reference chainlink price feeds are validated for themaxRefPriceDeviationFactoreven when the price provider is theChainlinkPriceFeedProvider.Recommendation
Do not perform the
maxRefPriceDeviationFactorcheck when the price provider is theChainlinkPriceFeedProvideras this check is redundant.Resolution
GMX Team: Resolved.
-
L-11 Low Timestamp Adjustments DoS Atomic Withdrawals Unexpected Behavior Resolved
Description
The timestampAdjustment is not intended to be used with the
ChainlinkPriceFeedProvider. However if it is accidentally configured for aChainlinkPriceFeedProviderthen this provider cannot be used with atomic withdrawals as the price must have a timestamp of the currentblock.timestamp.Recommendation
Add validation such that the
timestampAdjustmentcannot be configured to be nonzero when the provider is aChainlinkPriceFeedProvider.Resolution
GMX Team: Resolved.
-
L-12 Low Shifts Are Allowed In The Same Market Unexpected Behavior Resolved
Description
When creating a shift there is no validation that prevents a user from shifting out of and back into the same market. While no high impact outcome has been identified, this action serves no purpose and increases the likelihood of a potentially unexpected state.
Recommendation
Out of an abundance of caution, consider validating that the from market is not the same as the to market when creating a shift.
Resolution
GMX Team: Resolved.
-
L-13 Low LPs May Avoid Losses With Atomic Withdrawals Unexpected Behavior Acknowledged
Description
In the event that an insolvent liquidation occurs, there is a stepwise decrease in the value of a
MarketToken, as the accounting system realizes that the liquidated position cannot cover the losses it had tracked in thegetPoolValueInfofunction.Informed depositors may observe this liquidation and frontrun it to avoid such losses with an
atomicWithdrawal. This way more of the value losses impact the other depositors who have not withdrawn, and the user avoids any losses due to the insolvency.Recommendation
Be aware of this manipulation and ensure the atomic withdrawal fee is maintained at a high rate to disincentivize this gaming.
Resolution
GMX Team: Acknowledged.
-
L-14 Low Outdated NatSpec Documentation Resolved
Description
The NatSpec for the
validateSignerfunction is outdated as it still references several parameters such as theblockHash,minOracleBlockNumber, andmaxOracleBlockNumberwhich are no longer used.Recommendation
Update the NatSpec for the
validateSignerfunction to match the current parameters.Resolution
GMX Team: Resolved.
-
L-15 Low GM Oracle Salt Optimization Optimization Resolved
Description
In the
getOraclePricefunction the_getSaltfunction is called in a loop for every signer, however the value will not change within the same transaction, therefore the result of_getSaltcan be cached outside of the signer loop.Recommendation
Cache the result of the
_getSaltfunction before the signer loop to save gas from needlessly re-computing it.Resolution
GMX Team: Resolved.
-
L-16 Low Unused Errors Optimization Resolved
Description
In the
Errors.solfile there are several errors which are no longer used in the GMX V2 codebase:CouldNotSendNativeTokenEmptyCompactedPriceEmptyCompactedBlockNumberEmptyCompactedTimestampUnsupportedOracleBlockNumberType
Recommendation
Remove these unused errors from the
Errors.solfile.Resolution
GMX Team: Resolved.
-
L-17 Low Use Of Lagging validFromTimestamp Unexpected Behavior Resolved
Description
The
ChainlinkDataStreamProvideruses thereport.validFromTimestampas the validated price timestamp. However this price is from the earliest time in the range, this is in contrast to theGmOracleProviderwhich correctly uses the timestamp where the price was aggregated from. For example consider the following prices reported by chainlink’s data stream:- t = 150, validFromTimestamp = 148, observationsTimestamp = 150
- t = 155, validFromTimestamp = 151, observationsTimestamp = 155
- t = 158, validFromTimestamp = 156, observationsTimestamp = 158
An order submitted at t = 153 should be allowed to use the price which is observed at t = 155, however it will not be usable as the
validFromTimestampof 151 is used.Recommendation
Use the
report.observationsTimestampfor the validated price timestamp instead of thereport.validFromTimestamp.Resolution
GMX Team: Resolved.
-
L-18 Low Users Can use Shift to Bypass Disabled Features Logical Error Acknowledged
Description
The validation for if a market is disabled to be withdrawn from or deposited into occurs in
WithdrawalHandler::_executeWithdrawal()andDepositHandler::_executeDeposit().When using shift it calls
ExecuteWithdrawalUtils::executeWithdrawal()andExecuteDepositUtils::executeDeposit(), which is past when those checks occur in the withdraw and deposit flow. This allows a user to withdraw and deposit from markets that do not allow withdrawals or deposits.Recommendation
Before calling
ExecuteWithdrawalUtils::executeWithdrawal()andExecuteDepositUtils::executeDeposit()inside ofShiftUtils::executeShift(), check if the withdraw and deposit feature is disabled for that market. Alternatively, verify that the shift feature is disabled if withdraw or deposit feature is disabled.Resolution
GMX Team: Acknowledged.
-
L-19 Low setPositionImpactDistributionRate Missing Validation Validation Acknowledged
Description
Unlike the other functions in the
configcontract. thesetPositionImpactDistributionRatefunction does not validate the provided parameters. Such aspositionImpactPoolDistributionRate.Recommendation
To ensure that there are no unexpected values set when calling the
setPositionImpactDistributionRateconsider adding checks for thepositionImpactPoolDistributionRate.Resolution
GMX Team: Acknowledged.
-
L-20 Low Inconsistent naming of function in key contract Logical Error Acknowledged
Description
In the
Keyscontract the function are used to retrieve specific keys. Each function follows a specific naming convention of using the name of key and then the word key Example:functionoracleProviderForTokenKey(address token).However there are two functions that do not follow this pattern
tokenTransferGasLimitandsavedCallbackContract.Recommendation
Consider changing the naming of these two functions to be consistent with the the rest of the functions in the contract
Resolution
GMX Team: Acknowledged.
-
L-21 Low Migrating Orders Resets Cancellation Cooldown Logical Error Acknowledged
Description
When a order gets migrated the timestamp will be updated. Because of this users who were previously able to cancel their orders will have to wait for an additional period of time to cancel.
Recommendation
Document that the cancelling order cool down will be reset upon migration.
Resolution
GMX Team: Acknowledged.
-
L-22 Low usageFactor Can Exceed 100% Documentation Acknowledged
Description
The
usageFactoringetKinkBorrowingFactorcan exceed 100% as the prices of collateral and index tokens fluctuates.Although
getOpenInterestLimitincludes a precaution to ensuremaxReservedUsdis at or below 100%, there is still a possibility that a sudden price movement, when the market is near full utilization, could causereservedUsdandopenInterestLimitto push theusageFactorabove 100%. Example:usageFactor - 80%- The pool is currently at 79%, but a drop in collateral price pushes utilization to 81%
getKinkBorrowingFactorwill then calculateusageFactorto be> 1e18
Recommendation
Consider documenting this behavior so that users are aware that the usage factor can go above 100% and result in higher than expected fees.
Resolution
GMX Team: Acknowledged.
-
L-23 Low Market Orders Can be Added to AutoCancel List Unexpected Behavior Resolved
Description
Documentation related to update specifies that take profits and stop-loss orders with
autoCancelflag will be added toautoCancelList.But it only checks if it is a decrease order before adding to the list. Which means MarketDecrease orders can be added to the list. This allows market orders to be cancelled within the
requestExpirationwindow.Recommendation
In
updateAutoCancelList()aside from checking if the order is decrease order, also check if it is a market order. If it is a market order, don't add to the autoCancelList.Resolution
GMX Team: Resolved.
-
L-24 Low Reference Price Check Bound Is Exceedingly Large Configuration Acknowledged
Description
All prices will be validated against the Chainlink
priceFeedPriceif they have address configured for priceFeeds. Currently this check allows price differences between oracles up to 50%.Chainlink priceFeeds have 0.5%-2% deviation threshold which if a price of the commodity changes at least as much as deviation threshold, price will be updated. So a lot lower threshold for
MAX_ORACLE_REF_PRICE_DEVIATION_FACTORcan be provided to be sure prices are in accepted ranges for different oracle usages.Recommendation
Decrease
MAX_ORACLE_REF_PRICE_DEVIATION_FACTORto a reasonable value such that it won't revert unnecesarily but it can catch malicious/stale prices.Resolution
GMX Team: Acknowledged.
No findings match.
Invariants 63
The review's fuzzing suite asserted 63 invariants. 46 held and 17 did not.
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 |
DEC-01 | Position size in USD should decrease after successful decrease 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 |
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 |
DEP-1 | Deposited market token amount should be equal to the amount after simulation. | Held |
DEP-2 | Market tokens total supply should increase after deposit. | Held |
WITHD-1 | The user should not be able to withdraw any long tokens with 0 LP tokens. | Held |
WITHD-2 | The user should not be able to withdraw any short tokens with 0 LP tokens. | Held |
WITHD-3 | Withdrawn long token amount should be equal to amount after simulation. | Held |
WITHD-4 | Withdrawn short token amount should be equal to amount after simulation. | Held |
WITHD-5 | Market tokens total supply should decrease after withdrawal. | Held |
SHFT-1 | User balance of from GM tokens decreases upon shift. | Held |
SHFT-2 | User balance of to market GM increases by shift.marketTokenAmount(). | Held |
SHFT-3 | User balance of from market GM decreases by shift.marketTokenAmount(). | Held |
SHFT-4 | Claimable fees for long token do not change upon shift. | Held |
SHFT-5 | Claimable fees for short token do not change upon shift. | Held |
SHFT-6 | Long token pool amount for from market should decrease if simulateLongTokenAmountWithdrawal > 0. | Held |
SHFT-7 | Short token pool amount for from market should decrease if simulateShortTokenAmountWithdrawal > 0. | Held |
SHFT-8 | Pool amount for to market long token should increase if simulateLongTokenAmountWithdrawal > 0. | Held |
SHFT-9 | Pool amount for to market short token should increase if simulateShortTokenAmountWithdrawal > 0. | Held |
SHFT-10 | Market token (GM) value for from market stays the same after shift execution. | Held |
SHFT-11 | Market token (GM) value for to market stays the same after shift execution. | Held |
LIQ-01 | Position count should decrease after liquidation. | Broken |
LIQ-02 | Auto cancel order list should be empty after liquidation. | Broken |
ADL-01 | Position size should be reduced exactly by delta | Broken |
CNCL-D | User market token amounts should stay | Broken |
EP-01 | unchanged | Held |
CNCL-D | User long token amounts after cancel should be less or equal balance before plus | Broken |
EP-02 | deposited amount | Held |
CNCL-D | User long token amounts should stay unchanged (stronger invariant than | Broken |
EP-03 | CNCL-DEP-02) | Held |
CNCL-D | Vault long token amounts should stay | Broken |
EP-04 | unchanged | Held |
CNCL-D | User short token amounts after cancel should be less or equal balance before plus | Broken |
EP-05 | deposited amount | Held |
CNCL-D | User short token amounts should stay unchanged (stronger invariant than | Broken |
EP-06 | CNCL-DEP-05) | Held |
CNCL-D | Vault short token amounts should stay | Broken |
EP-07 | unchanged | Held |
CNCL-O | User should receive the same amount of | Broken |
RD-1 | long tokens he sent to create an order | Held |
CNCL-O | User should receive the same amount of | Broken |
RD-2 | short tokens he sent to create an order | Held |
CNCL-S | Market from received amount should be | Broken |
HFT-01 | less or equal than before | Held |
CNCL-S | Market to balance should stay unchanged | Broken |
CNCL-WI | User should receive market tokens back | Broken |
TH-1 | after cancelling withdrawal | Held |
CNCL-WI | Market tokens total supply should stay the | Broken |
TH-2 | same | Held |
CNCL-W | Vault should refund market tokens | Broken |
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.
