GMX engaged Guardian to review the security of its dynamic funding fees and position impact distribution updates for the GMX V2 system. From the 1st of September to the 15th of September, a team of 2 auditors reviewed the source code in scope.
- Published
- Review window
- September 1 to 15, 2023
- Language
- Solidity
- Chains
- Arbitrum, Avalanche
- Sector
- Perpetuals
- 1 Critical
- 1 High
- 10 Medium
- 10 Low
- 0 Informational
Scope
Overview
GMX engaged Guardian to review the security of its dynamic funding fees and position impact distribution updates for the GMX V2 system. From the 1st of September to the 15th of September, a team of 2 auditors reviewed the source code in scope.
Findings 22
-
CALC-1 Critical boundMagnitude Fails To Bound Magnitude Logical Error Pending
Description
Proof of concept: PoC
When computing the sign of the resulting bounded value, the original value is divided by the magnitude.
However the magnitude has already been adjusted to be within the min or max bounds, therefore when the magnitude has been adjusted the resulting sign variable value is no longer a unit vector indicating sign.
This results in the bounded value, in this case the
nextSavedFundingFactorPerSecond, being in fact unbounded.For example:
- value = 800
- Min = 250
- Max = 400
- Magnitude is capped to 400
- Sign = 800 / 400 = 2
The returned value is 400 * 800 / 400 = 800 which is outside of the defined max As a result, dynamic funding fees cannot be capped, leading to any market being entirely bricked within hours of the dynamic funding fees being activated.
Recommendation
If the value is < 0 return
-magnitude.toInt256()and if the value is >=0 returnmagnitude.toInt256() -
MKTU-1 High Funding Factor Per Second Open To Manipulation Protocol Manipulation Pending
Description
Proof of concept: Video
The adaptive funding rate mechanism is prone to strategic gaming as the resulting
fundingFactorPerSecondthat a market experiences is affected by the amount of time that passes before the funding fees are updated. There are several ways the funding fees may be gamed as a result.Firstly, consider a trader, Bob, who holds a short position in the following scenario:
- Longs currently pay shorts, long OI > short OI, savedFundingFactorPerSecond > 0.
- Long OI becomes < short OI and the OI diff is within the
NoChangerange. - The
fundingFactorPerSecondwill begin decreasing, as the skew has changed sides. - As soon as the
fundingFactorPerSecondcrosses the threshold from positive to negative and becomes-1, Bob updates his order by removing 1 wei of collateral. - Now the
fundingFactorPerSecondsticks to that minimum magnitude rate, dependent on when Bob updated his position.
The terminal
fundingFactorPerSecondthat the market ends up experiencing is dependent on when the funding fees are updated, therefore Bob updates the funding fees at a time that is most advantageous to him and results in minimal funding paid.Secondly, consider the following occurs from where we left off with Bob:
- Alice is a long trader, she notices that the funding rate was stopped at the minimum magnitude, so she attempts to increase it.
- Alice opens another temporary long position just large enough to flip the skew back towards longs > shorts, as soon as the
fundingFactorPerSecondcrosses the threshold and becomes1, Alice closes her temporary long position. - Now the
fundingFactorPerSecondbegins decreasing again. Alice is hoping that Bob, or anyone else, will not update the funding — ideally until thefundingFactorPerSecondreaches the maximum magnitude.
In a perfectly competitive GMX V2 market, traders on both the long and short side would out-maneuver each other such that the minimum amount of funding fees is paid to the other side at all times. In reality, some traders will abuse these behaviors against uninformed traders to gain an unfair advantage and benefit from substantially less funding fees paid and substantially more funding fees collected.
Recommendation
Consider assigning a
fundingFactoPerSecondthat is halfway between theminandmaxmagnitude upon switching skews and allowing that middle value to remain, increase, or decrease based on the OI diff.Additionally, Instead of allowing minute changes to the open interest (e.g. 1 wei) to impact the rate, consider setting a threshold for OI changes to adjust the funding rate. Furthermore, carefully monitor the market for funding fee manipulation and adjust funding factors appropriately.
-
CALC-2 Medium boundMagnitude Function Cannot Bound 0 Logical Error Pending
Description
In the
boundMagnitudefunction, when the provided value is 0 the provided min fails to give a lower bound to the resulting value.- value = 0, min = 10
- sign = value / magnitude = 0 / 10
- Returned value = magnitude * sign = 10 * 0 = 0
Recommendation
When the value is 0 return the min with a sign informed by a boolean parameter. When the
FundingRateChangeTypeis Increase, the sign of the resulting min value should be whichever direction the increase is heading in. -
MKTU-2 Medium minFundingFactorPerSecond != 0 Undesired behaviors Logical Error Pending
Description
The funding factor per second returned from the
getNextFundingFactorPerSecondfunction may be unable to cross from negative to positive or from positive to negative in the event that theminFundingFactorPerSecondis set > 0 and orders are executed often.- Original
savedFundingFactorPerSecond= 10 minFundingFactorPerSecond= 10nextSavedFundingFactorPerSecond= 7- Bounded
nextSavedFundingFactorPerSecond= 10
The
nextSavedFundingFactorPerSecondcannot flip signs and continue to make progress to reach the funding factor it ought to be on the other side unless it can make a large enough jump to cross the gap from[0, minFundingFactorPerSecond], which may be unlikely if orders are consistently updating thesecondsSinceFundingUpdated.Additionally, if the gap from
[0, minFundingFactorPerSecond]is crossed successfully, the resultingnextSavedFundingFactorPerSecondwill be increased in magnitude to theminFundingFactorPerSecond, resulting as-minFundingFactorPerSecond. This jump will go above the configuredfundingIncreaseFactorPerSecondrate and may cause unexpected results.Recommendation
Be wary when configuring the
minFundingFactorPerSecondto be != 0, if theminFundingFactoris ever != 0 it should have a minimal magnitude to limit these behaviors. - Original
-
MKTU-3 Medium nextSavedFundingFactorPerSecond Cycling Logical Error Pending
Description
The
nextSavedFundingFactorPerSecondcan be decreased to 0 in the event that thecache.savedFundingFactorPerSecondMagnitude == decreaseValue. This can result in the funding side increasing rather than decreasing since it isn’t set to 1 or -1:- Long OI < short OI
- Original
nextSavedFundingFactorPerSecond= -10 - Original
longsPayShorts= false decreaseValue= 10nextSavedFundingFactorPerSecond= 0- Long OI < Short OI
nextSavedFundingFactorPerSecond= 0isSkewTheSameDirectionAsFunding= falseincreaseValue= 13nextSavedFundingFactorPerSecond= -13
Therefore the
fundingFactorPerSecondwas meant to decrease in magnitude but increased instead.Recommendation
Avoid this cycling by assigning the
nextSavedFundingFactorPerSecondto 1 or -1 in theifcase on line 1316 by changing thecache.savedFundingFactorPerSecondMagnitude < decreaseValuetocache.savedFundingFactorPerSecondMagnitude <= decreaseValue. -
CON-1 Medium Lack Of Parameter Validation Validation Pending
Description
There is no validation that the
THRESHOLD_FOR_DECREASE_FUNDINGis less thanTHRESHOLD_FOR_STABLE_FUNDING. If the config keeper were to invert the thresholds, the funding factor may be decreasing when it should be stable or increasing when it should be decreasing.Furthermore, there is no validation that the
MIN_FUNDING_FACTOR_PER_SECONDis less than theMAX_FUNDING_FACTOR_PER_SECOND. As a result,boundMagnitudewill bound the value incorrectly.Recommendation
Add validation in the Config to ensure
THRESHOLD_FOR_DECREASE_FUNDINGis less thanTHRESHOLD_FOR_STABLE_FUNDING.Add validation in the Config to ensure
MIN_FUNDING_FACTOR_PER_SECONDis less thanMAX_FUNDING_FACTOR_PER_SECOND. A check for the min and max bounds may be included inboundMagnitudeas well or the documentation should mention that the validation is done elsewhere. -
GSU-1 Medium tx.gasprice != 0 Is Fallible Validation Pending
Description
tx.gasprice != 0is not a bulletproof means of filtering out non-estimateGas calls. The Keeper can assign a non-zerogasPriceand there has been discussion about getting thetx.gaspriceto be the basefee even when thegasPrice = 0in anestimateGascall.Recommendation
Use
tx.originas no one can transact from the zero address. -
OCL-1 Medium Current Ref Price Compared With Earlier Price Validation Pending
Description
A keeper may have to use prices from several blocks ago to execute an order, regardless of if realtime feeds or the default oracle system is being used. In such a case, the
validateRefPricewould be comparing the latest Chainlink aggregator oracle price against an earlier price.Depending on how large the
MAX_ORACLE_REF_PRICE_DEVIATION_FACTORis and how volatile the asset, the execution with these earlier block numbers/prices may revert, preventing them from being used.Recommendation
Carefully assign the
MAX_ORACLE_REF_PRICE_DEVIATION_FACTORconsidering that it might be necessary in some cases to allow prices from blocks previous to when thelatestAnswerin the Chainlink aggregator was updated. -
GLOBAL-1 Medium Execution Gas Validated Too Early Validation Pending
Description
Based on the
startingGasand theestimatedGasLimit, the execution gas is validated to ensure thestartingGasis greater than theestimatedGasLimitand some variable, additional gas for execution. After callingGasUtils.validateExecutionGas(dataStore, startingGas, estimatedGasLimit), the gas for execution is presumed to be enough for execution of the order as it has been validated.However,
uint256 executionGas = GasUtils.getExecutionGas(dataStore, startingGas)is called right afterwards which reduces thestartingGasby the gas needed for error handling. The amount of gas validated for execution is different than the amount given for execution, which may now be insufficient.Recommendation
Consider making the gas validation more restrictive by calling
validateExecutionGas()on the result ofgetExecutionGas()or ensure the theminAdditionalGasForExecutionis large enough to cover the gas forwarded to handle the execution error. -
MKTU-4 Medium Users Paid Funding Fees When They Should Pay Incentives Pending
Description
Proof of concept: PoC
It is possible for shorts to get paid when longs should pay shorts (long OI > short OI) and vice versa.
This is because it may take a certain duration until the
savedFundingFactorPerSecondflips payment sides and accurately represents payment direction. Ultimately this functionality misaligns incentives during this period, where traders who imbalance the pool aren’t discouraged by funding fee payments.Recommendation
Consider resetting the
fundingFactorPerSecondentirely when the OI imbalance direction changes. -
MKTU-5 Medium Funding Factor Spikes To Max Logical Error Pending
Description
Because the
increaseValueis dependent ondurationInSeconds, when a period of time goes by without updates thecache.nextSavedFundingFactorPerSecondcan spike to the maximum bound. This can occur when the skew changes, causing an increase to a largefundingFactoPerSecondregardless of the new OI diff.The situation also may arise if the
savedFundingFactorPerSecondis at 0 and the duration is large, regardless of whether the previousfundingRateChangeTypewas an increase or no change.Recommendation
It's crucial to track the min/max limits and make adjustments as needed. Additionally, consider refactoring the funding such that these spikes do not occur when the skew is switching sides or the
savedFundingFactorPerSecondis 0. -
MKTU-6 Medium Unequal Funding fees Over Equal Durations Logical Error Pending
Description
Proof of concept: PoC
Traders don't experience an incremental increase in the
fundingFactorPerSecond. Instead, they receive funding based on the final funding factor applicable for the entire duration of their position. Consequently, a user who updates their position after X seconds will receive funding fees based on the rate at that specific moment.This could lead to a scenario where updating a position at the end of X seconds may yield more in funding fees than updating midway at X/2 and closing out another X/2 later, even though the total duration is the same.
Recommendation
Clearly document this behavior so that traders are aware how frequent updates can affect funding fee payments.
-
MKTU-7 Low Funding Fees Used To Brick Market Logical Error Pending
Description
Proof of concept: PoC
The
claimableFundingFeeAmountis asserted to be less than the balance of theMarketTokencontract at all times, however this may not always be the case and can lead to the halting of liquidations, orders, ADLs, and withdrawals as a result.Firstly, users may decrease the collateral backing their position to 0 given that the
willPositionCollateralBeSufficientvalidation ignores fees and price impact. All funding fees accumulated by positions with 0 collateral will not have corresponding ERC20 tokens backing that funding fee amount. Therefore the funding fees will grow without a corresponding backing value in the balance of theMarketToken.A malicious actor may then swap all (or most) of the backing
poolAmountto the other token in the market. This way the attacker can reduce the amount of ERC20 tokens that can count towards thebalancein thevalidateMarketTokenBalancevalidation to just over theclaimableFundingFeeAmount. The funding fees will then continue to increase and surpass the balance of the contract. Thereby causing orders for positions that would collect these funding fees as aclaimableFundingFeeAmountto revert.This exploit is unlikely as it requires a market with low open interest and would require time to execute as well as favorable funding rates. Additionally it could be resolved by manually plugging the hole by sending an amount of ERC20 tokens directly to the
MarketToken. However it could be leveraged by an attacker for a short period of time to cause grief to other users in the same market before tokens are swapped back.Recommendation
Consider removing the
claimableFundingFeeAmountvalidation in thevalidateMarketTokenBalancefunction entirely. Otherwise carefully monitor for any such manipulation. -
POSU-1 Low Not All LiquidatablePosition Args Set Events Pending
Description
If a position is deemed liquidatable before validating against
info.minCollateralUsdForLeverage, then anyLiquidatablePositionevents will emit 0 as theminCollateralUsdForLeveragesinceinfo.minCollateralUsdForLeveragehas yet to be set.Recommendation
Document such behavior or calculate the
minCollateralUsdForLeverageif needed. -
MKTU-8 Low Transition From Default To Dynamic Funding Unexpected Behaviour Pending
Description
In the
getNextFundingFactorPerSecondfunction, when thefundingIncreaseFactorPerSecondis not configured thesavedFundingFactorPerSecondis assigned to 0.However for markets where the default funding is used before activating the dynamic funding it may serve as a smoother transition to assign the
savedFundingFactorPerSecondto the current resultingfundingFactorPerSecond.Recommendation
Provide the resulting
fundingFactorPerSecondas the returnednextSavedFundingFactorPerSecondvalue on line 1268. -
MKTU-9 Low fundingDecreaseFactorPerSecond Not Checked Validation Pending
Description
In the
getNextFundingFactorPerSecondfunction, onlyconfigCache.fundingIncreaseFactorPerSecondis validated against inif (configCache.fundingIncreaseFactorPerSecond == 0).It is possible for the
fundingIncreaseFactorPerSecondto be non-zero, but thefundingDecreaseFactorPerSecondto be zero. As a result, thedecreaseValuewould be be zero and the saved funding factor would not be adjusted -- mimicking a stable behavior when it should really decrease.This could return a different result than simply calculating the funding factor per second using
Precision.applyFactor(cache.diffUsdToOpenInterestFactor, cache.fundingFactor).Recommendation
Validate the funding factor depending on the adjustment direction and/or document this behavior.
-
MKTU-10 Low Useful Event Data Events Pending
Description
In the
distributePositionImpactPoolfunction thedistributionAmountis emitted with the“PositionImpactPoolDistributed”event.However it may be helpful for consumers of the
“PositionImpactPoolDistributed”event to have access to the resulting value of the position impact pool as well.Recommendation
Consider adding the
nextPositionImpactPoolAmountto the information emitted with the“PositionImpactPoolDistributed”event. -
BOH-1 Low Outdated NatSpec Documentation Pending
Description
The current NatSpec does not include the new
Order.Props memory orderparameter.Recommendation
Update the documentation.
-
GLOBAL-2 Low Orders/Deposits/Withdrawals Not Passed As Params Optimization Pending
Description
During cancellation, the Order/Deposit/Withdrawal is read from the store just to validate the request cancellation, and then the appropriate cancel function is called in a Utils library. In Utils, the Order/Deposit/Withdrawal is read once again from the store.
Recommendation
Consider passing the objects as parameter which matches the updates in the rest of the codebase.
-
GLOBAL-3 Low Impact Pool Distribution Dampers Incentives Incentives Pending
Description
Positive position impact is a key incentive for actors to rebalance the long and short open interest of a market. However positive position impact is capped by the amount of virtual index tokens in the position impact pool.
When the position impact pool is distributed, the maximum net cap for positive impact is reduced. If the
minPositionImpactPoolAmountis not configured high enough it could lead to lacking incentives for users to rebalance the open interest.Recommendation
Ensure that the
minPositionImpactPoolAmountis configured to a reasonable amount, allowing for sufficient incentivization for aeros to balance the open interest. -
GSU-2 Low OOG Check Fallible Validation Pending
Description
In the
validateExecutionErrorGasfunction, the revertreasonBytesis required to be of length 0 to initiate the validation. However the length of thereasonBytesmay be 0 in many circumstances other than an out of gas error. For example if an emptyrevert()orrequire()is used without a revert string, or another type of error such as anINVALIDopcode.Recommendation
Be aware that the
reasonBytes.length == 0check does not ensure that the execution reverted with an out of gas error and monitor for any potential manipulations because of this assumption. -
KEY-1 Low Naming Conventions Ignored Naming Pending
Description
The
minMarketTokensForFirstDepositfunction does not follow the established naming convention for functions used to retrieve keys. The expected name for this function would beminMarketTokensForFirstDepositKey.Recommendation
Rename the
minMarketTokensForFirstDepositfunction tominMarketTokensForFirstDepositKey.
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.
