Guardian's review of Token Delisting Procedure for GMX, published May 2026. The report records 24 findings across 2 review rounds, including 4 high and 12 medium.
- Published
- Review window
- May 13 to 21, 2026
- Rounds
- Main Review, Remediation Review
- Language
- Solidity
- Chains
- Arbitrum, Avalanche
- Sector
- Perpetuals
- 0 Critical
- 4 High
- 12 Medium
- 3 Low
- 5 Informational
Findings 24
Main Review
13 findings · May 13 to 18, 2026-
H-01 High Full Impact Pool Withdrawal Step Will Likely DoS Itself DoS Pending
Description
In Phase 4 the remaining position impact pool is withdrawn. This is a 2-Step flow with a time delay and the
withdrawFromPositionImpactPool.tsscript is used to do so.This script is called with
FULL_WITHDRAWAL=truefor both creating the action and executing it. Therefore it will try to withdraw everything in the pool thats available at the exact time the script is called.If during creation and execution of this action the
positionImpactPoolDistributionRateis positive (which is the case after Phase 1b) than this value will no longer be correct at execution time and a wrong signature is created as theFULL_WITHDRAWAL=trueflag is used during execution and even if the right signature is created the contract would revert as not enough funds are in the pool.Recommendation
Set the
positionImpactPoolDistributionRateof the market to 0 before calling thewithdrawFromPositionImpactPool.tsscript. -
H-02 High Runbook Script Commands Do Not Match Actual Script Inputs Configuration Pending
Description
Several runbook commands pass environment variables or describe script behavior that is not implemented by the referenced scripts. This can cause operators to believe they are applying targeted delisting actions while the script either ignores the input, applies changes more broadly than expected, or uses hardcoded/default behavior, which could have dangerous unexpected outcomes.+ For example:
withdrawFromPositionImpactPool.tsignoresMARKET; it uses a hardcoded market instead.withdrawFromPositionImpactPool.tsignoresRECEIVER; it uses a hardcoded receiver address.updateMarketPositionImpactDistributionConfig.tsignoresMARKET; it applies all pending impact-distribution config changes across configured markets.updateMarketConfig.tsignoresSKIP_BORROWING_FEE_FOR_SMALLER_SIDE
Recommendation
Update the runbook and scripts so every documented environment variable is either implemented or removed.
-
H-03 High Phase 2 Skip Borrowing-Fee Toggle Affects All Markets Configuration Pending
Description
In phase 2
SKIP_BORROWING_FEE_FOR_SMALLER_SIDEis update to false as part of the target market’s delisting incentives. However, this setting is a global DataStore key, not a market-scoped parameter. As a result, changing this flag makes the smaller open-interest side pay borrowing fees across every market, not only the market being delisted. This can change position economics for unrelated traders and may incentivize liquidity or position exits from unaffected GMX markets.A even worse impact regarding that is a stepwise drop in the health factor of many positions, as the system does not automatically update borrowing of all the markets before changing this parameter. Therefore the more stale a market’s cumulative borrowing factor is (the longer it is untouched) the more pressure is put up on all the positions of the smaller side with a stepwise drop. This can lead to many positions being suddenly liquidatable.
Recommendation
Consider one of the following actions:
- Remove this step from the runbook
- Update this parameter so that it acts per market and also consider to always accrue funding before updating it (to avoid the mentioned step wise drops)
- Document this behavior and analyze the market before updating this param to handle it with care
-
H-04 High Zero-valued config changes skipped by script Error Pending
Description
In the market delisting playbook, Phase 1d says set
maxPoolUsdForDepositvalues to 0, and Phase 2 says setABOVE_OPTIMAL_USAGE_BORROWING_FACTORto 0. However, inupdateMarketConfigUtils.ts (line 112)theaddConfigItemreturns early for falsy values, so 0 is never added to the multicall.As a result, the operator runs the script, it returns 0 (the exit code, not the config value), the changeset PR is merged, and the market remains open for deposits and unchanged on the borrowing curve.
Recommendation
Change
addConfigIteminupdateMarketConfigUtils.tsso it only skips truly missing values, not valid zero values:if (value === undefined || value === null) { return; } -
M-01 Medium Static Prices Unusable When Ref Price Diverge DoS Pending
Description
StaticOracleProvideris intended to support frozen pricing for markets that are being closed. However, static prices are still validated against the live Chainlink reference price when a reference feed exists for the token.StaticOracleProviderexplicitly reports that it is not a Chainlink on-chain provider:function isChainlinkOnChainProvider() external pure returns (bool) { return false; }As a result,
Oracle._validatePricesapplies the reference-price deviation check to static prices:if (!provider.isChainlinkOnChainProvider()) { (bool hasRefPrice, uint256 refPrice) = ChainlinkPriceFeedUtils.getPriceFeedPrice(dataStore, token); if (hasRefPrice) { _validateRefPrice(token, validatedPrice.min, refPrice, maxRefPriceDeviationFactor); _validateRefPrice(token, validatedPrice.max, refPrice, maxRefPriceDeviationFactor); } }This means a frozen token price remains executable only while it stays within
MAX_ORACLE_REF_PRICE_DEVIATION_FACTORof the current Chainlink feed. If the live Chainlink price moves far enough away from the frozen price, every non-atomic execution requiring that static token price reverts withMaxRefPriceDeviationExceeded. This can leave positions and liquidity stuck or force governance/operators to change oracle configuration again under emergency conditions. This will also lead to reverts if the reason behind shutting down the market is a deprecated chainlink feed as in that case callinglastRoundDataon the chainlink feed will lead to a revert.Recommendation
Do not apply the live Chainlink reference-price deviation check to explicitly frozen static prices.
-
M-02 Medium Wrong Role Defintions Access Control Pending
Description
The defined roles that are needed to execute certain actions are wrong in multiple places, as it often says that an action can also be executed by the
LIMITED_CONFIG_KEEPERbut in reality it’s only executable by theCONFIG_KEEPER, this is for example the case for the following actions:- updating the position impact pool distribution rate in Phase 1b
- Borrowing parameter changes in Phase 2
- Raising min collateral factor for liquidation in Phase 3
- Final market disable
Recommendation
Recheck the roles in the runbook and update them accordingly to avoid running into access control issues.
-
M-03 Medium GLV Market Removal DoS Griefing Attack DoS Pending
Description
During Phase 1c, the runbook requires shifting all GM balance out of the affected market and then calling
removeMarketFromGlv. Removal requires the GLV market to be disabled and the GLV’s accounted GM balance for that market to be exactly zero. A malicious actor can DoS this process by transferring a dust amount of GM tokens to the GLV market so that zero balance check reverts. The runbook would then require to repeat this process, but the attacker could front run again:- GLV market is disabled
- Keeper creates a GLV shift for the full accounted GM balance
- Attacker transfers 1 wei GM directly to the GLV before
executeGlvShift executeGlvShifttransfers out the accounted GM amountStrictBank._afterTransferOutsyncstokenBalances[market]to the actual ERC20 balance, which is now the attacker’s 1 wei dustremoveMarketFromGlvreverts withGlvNonZeroMarketBalance
Recommendation
The safest practice would be to add a
CONFIG_KEEPER-only disabled-market cleanup function that atomically shifts any remaining GM and removes the market from the GLV in the same transaction. -
M-04 Medium Static Price Fallback Uses Incorrect DataStore Key Shape Configuration Pending
Description
Phase 3 describes a fallback for cases where the token price feed is completely unavailable:
dataStore.setUintArray(Keys.staticOraclePriceKey(token), [min, max])This does not match the implementation.StaticOracleProviderreads two separate uint values:Keys.staticOraclePriceKey(token, false) // minKeys.staticOraclePriceKey(token, true) // maxThe runbook also suggests switching the oracle provider viascripts/updateOracleConfigForTokens.ts, but that script only supports chainlink feeds.Recommendation
Update the fallback instructions to match the actual storage layout and tooling.
-
M-05 Medium Phase 2 Misstates How to Disable Borrowing Kink Model Configuration Pending
Description
Phase 2 says to “disable kink model” by setting
ABOVE_OPTIMAL_USAGE_BORROWING_FACTORto 0. This does not disable the kink model, the kink branch is selected wheneverOPTIMAL_USAGE_FACTOR != 0.ABOVE_OPTIMAL_USAGE_BORROWING_FACTORonly controls the extra borrowing-rate slope after utilization exceedsOPTIMAL_USAGE_FACTOR. Setting it to 0 removes that above-optimal surcharge, but the rate still uses the kink-model base formulaRecommendation
Clarify the intended behavior and update the runbook accordingly:
- If the sentence is correct and the goal is to disable the kink model entirely, set
OPTIMAL_USAGE_FACTORto 0 for both long and short sides. - If the goal is only to remove the extra above-optimal surcharge, rewrite the sentence to say that explicitly.
- If the sentence is correct and the goal is to disable the kink model entirely, set
-
M-06 Medium LPs Can Game Remaining Delisting Impact Pool Distribution Gaming Pending
Description
In the market delisting playbook, Phase 1b starts draining the position impact pool by setting
minPositionImpactPoolAmount = 0and increasingpositionImpactPoolDistributionRate. Phase 1d freezes deposits later by settingMAX_POOL_USD_FOR_DEPOSIT = 0. This ordering leaves a window where public deposits are still enabled while the remaining impact-pool distribution is known and scheduled. An opportunistic LP can deposit between Phase 1b and Phase 1d and capture a pro-rata share of the remaining accelerated impact-pool release. Existing LPs are diluted because their share of the remaining scheduled distribution decreases after the new GM is minted. This creates an avoidable value transfer during delisting.Recommendation
Add any required treasury liquidity, then freeze public deposits before starting accelerated impact-pool distribution.
-
L-01 Low Residual Swap Impact Pool Funds May Be Stuck After Market Disable Rewards Pending
Description
The market delisting runbook withdraws the position impact pool but does not clear the
swapImpactPoolAmount. If this value is positive when the market is finally disabled, normal swaps can no longer consume it becausevalidateSwapMarketrequires the market to be enabled.Recommendation
Be aware about that and consider to implement a withdraw from swap impact pool function if needed.
-
I-01 Informational Swaps Remain Open Informational Pending
Description
Deposits and OI is frozen during this flow but swaps can still be performed.
Recommendation
Think about if this is intended and if not consider to disable swaps with this market during the flow.
-
I-02 Informational Disabled Markets May Leave Stale Swap Virtual Inventory Informational Pending
Description
During market delisting, the final disable step requires open interest to be zero, but does not require the market’s pool balances to be zero. Since swap virtual inventory is derived from pool amount updates, a disabled market with residual long/short pool balances may continue to influence swap price impact for active markets sharing the same virtual market ID.
Recommendation
Be aware about that and consider to add a note in the runbook to check that this value will not be too big to have any real influence on other markets.
Remediation Review
11 findings · May 19 to 21, 2026-
M-01 Medium Runbook Incorrectly States
priceFeedKeyCannot Be Cleared DoS AcknowledgedDescription
The Market Delisting runbook states for Phase 3 (Option 1) that on-chain cleanup of
priceFeedKey(token)has no currentConfig/TimelockConfigpath and would require a contract upgrade. This is inaccurate forpriceFeedKey.Config.setAddresscannot clear it becausePRICE_FEEDis not allowlisted inConfig._initAllowedBaseKeys, butTimelockConfig.signalSetPriceFeedschedules a directDataStore.setAddress(Keys.priceFeedKey(token), priceFeed)call and does not rejectaddress(0). ClearingpriceFeedKey(token)matters becauseChainlinkPriceFeedUtils.getPriceFeedPricereturns(false, 0)when the configured feed address is zero. This skips Chainlink reference-price validation, which otherwise can makeStaticOracleProviderunusable if the live feed diverges, becomes stale, or reverts. This should be distinguished fromoracleProviderForToken: there is no safe dedicated clear path for that key.Config.setOracleProviderForTokenrequires the provider to be enabled, and enablingaddress(0)as an oracle provider would be an unsafe abuse of the provider allowlist.Recommendation
Update the runbooks to state:
Config.setAddresscannot clearpriceFeedKey(token).TimelockConfig.signalSetPriceFeed(token, address(0), ...)can clearpriceFeedKey(token).
-
M-02 Medium Phase 3 Role Guidance Incorrectly Allows
LIMITED_CONFIG_KEEPERDoS ReportedDescription
The market delisting runbook correctly states in its prerequisites that Phase 3 requires
CONFIG_KEEPER, but the Phase 3 forced-liquidation section later says raisingminCollateralFactorForLiquidationcan be executed byLIMITED_CONFIG_KEEPERorCONFIG_KEEPER. This regresses the earlier M-02 role-fix finding.MIN_COLLATERAL_FACTOR_FOR_LIQUIDATIONis included inConfig._initAllowedBaseKeys, but it is not included inConfig._initAllowedLimitedBaseKeys. ALIMITED_CONFIG_KEEPERcall throughConfig.setUintwould therefore revert withInvalidBaseKey. Operators may attempt the Phase 3 liquidation-threshold update with the wrong signer, causing an avoidable failed transaction or delaying forced cleanup during a time-sensitive delisting.Recommendation
Update Phase 3 to state that changing
minCollateralFactorForLiquidationrequiresCONFIG_KEEPERonly. Keep the prerequisite and per-step role guidance consistent. -
M-03 Medium Wrong Allowance Spender for Lent Impact Repayment Configuration Reported
Description
Phase 4a instructs
fundingAccountto approve the market contract for bothmarket.longTokenandmarket.shortTokenbefore executingreduceLentImpactAmount. This is the wrong spender. The timelock executesConfigTimelockController.reduceLentImpactAmount, which callsPositionImpactPoolUtils.reduceLentAmount. The token pulls are performed by the executing controller context:IERC20(marketProps.longToken).safeTransferFrom(fundingAccount, market, longTokenAmount); IERC20(marketProps.shortToken).safeTransferFrom(fundingAccount, market, shortTokenAmount);The existing test approves
configTimelockController, not the market token, before executing the repayment. If operators follow the runbook and approve the market contract, the timelock execution will revert due to missing allowance forConfigTimelockController. This can delay final cleanup and may be especially problematic if the index token's oracle feed is close to expiry or outage.Recommendation
Update Phase 4a to instruct
fundingAccountto approveConfigTimelockControlleras the spender for both long and short tokens. IflongToken == shortToken, approve enough for the combined long-token and short-token transfer amount. -
M-04 Medium GLV Removal Before Impact-Pool Distribution Can Misallocate Value Rewards Reported
Description
The runbook shifts/removes the affected market from GLV before starting accelerated position impact-pool distribution. This can transfer value away from GLV LPs. GM pool value subtracts the undistributed position impact pool from market value, so a GLV shift-out values the GLV's GM balance before that impact-pool value has been released. Later, when the impact pool is distributed, the GM value increase accrues only to holders that still own the affected market's GM tokens.
Recommendation
Document this behavior or consider to distribute the impact pool twice for a more fair GLV wind down.
-
M-05 Medium Borrowing Incentive Guidance Can Raise an Ignored Parameter Configuration Reported
Description
The runbook states out to raise the
BORROWING_FACTORand/orBASE_BORROWING_FACTOR, then set theOPTIMAL_USAGE_FACTORto zero to disable the kink model. However onceOPTIMAL_USAGE_FACTORis zero, the non-kink borrowing calculation is used andBASE_BORROWING_FACTORis ignored. Therefore if theBASE_BORROWING_FACTORis raised, the intended voluntary-exit incentive do not increase.Recommendation
Increase
BORROWING_FACTORto the intended effective rate for both long and short sides. TheBASE_BORROWING_FACTORonly applies while the kink model remains active. -
M-06 Medium Residual Long/Short Pool Tokens Are Not Handled Documentation Reported
Description
In Phase 4e, residual LPs can redeem off-chain by sending GM tokens to the treasury, with the treasury paying them in underlying assets. It then says the treasury-held GM is retained for future handling, such as "admin redemption after a protocol upgrade". The runbook does not include a step that transfers the remaining
longToken/shortTokenpool balances from the market to treasury. As a result, treasury may compensate residual LPs using separate treasury funds while the correspondinglongToken/shortTokenbacking remains in the disabled market. If residual GM balances are non-trivial, the delisting process can leave material pool assets trapped. This makes Phase 4e operationally ambiguous: operators can complete the user-facing treasury swap, but the runbook does not specify how to settle the associated residual pool assets that Phase 0 instructed them to track.Recommendation
Document a reviewed timelock-controller batch procedure for residual recovery, including how to calculate transfer amounts, whether to burn treasury-held GM, and whether to update
poolAmountKeyvalues after disable. Recovery appears possible through privileged governance operations.ConfigTimelockControlleris grantedCONTROLLER, and governance can schedule arbitrary calls directly through the underlying timelock controller, via a scheduled batch call -
L-01 Low Phase 1c GLV shift-out can be blocked DoS Reported
Description
Phase 1B of the market delisting playbook says "Multiple shift transactions may be required depending on balance size (choose different
TO_MARKETvalues)." That's correct but only names one of four reasons aMARKET_TOKEN_AMOUNT=ALLshift can revert. The four constraints are:GlvMaxMarketTokenBalance{Amount,Usd}Exceeded: Full amount pushes the destination past its per-GLV cap (this is covered in playbook)GlvShiftMaxLossExceeded: Combined withdraw + deposit price impact exceedsglvShiftMaxLossFactorGlvShiftIntervalNotYetPassed: Wall-clock rate limit on shifts per GLVMaxPnlFactorExceeded: Destination's pending PnL near its deposit cap
Recommendation
Document these behavior in the runbook, be aware and consider updating the config values (e.g. max loss factor) if needed.
-
L-02 Low Pending Target-Market Orders Can Revert After Final Disable DoS Reported
Description
The market delisting playbook reaches final disable in Phase 4d by setting
isDisabled: truefor the target market. The playbook requires open interest to be zero before Phase 4, but it does not require the target market's pending order queues to be empty. This leaves an execution gap for already-created target-market orders, including frozen orders. In the guided delist fuzz repro, a frozen SOL-market order remained queued when the SOL market was disabled. A later genericExecuteOrderselected that order and reverted withDisabledMarket(0x324AA6331C60D0089e06c8Ec26112F339a56F398). The index-token delisting runbook notes that pending orders can be cancelled by users after terminal disable, becausecancelOrderdoes not need the oracle. Until those orders are cancelled, execution attempts against the stale queue can continue to fail.Recommendation
Add a pending-order drain before Phase 4d final disable.
-
I-01 Informational Process Summary Retains Stale Bilateral Borrowing Reference Documentation Reported
Description
The market delisting v2 runbook removed the Phase 2 instruction to change
SKIP_BORROWING_FEE_FOR_SMALLER_SIDE, resolving H-03's concern that this is a global setting affecting all markets. However, the Process Summary still describes Phase 2 as using "high borrowing fees, bilateral charges, disable kink". The phrase "bilateral charges" can lead operators to infer that the global bilateral borrowing toggle should still be changed during delisting.Recommendation
Remove "bilateral charges" from the Process Summary so it matches the corrected Phase 2 body.
-
I-02 Informational Phase 3 Static Price Step References Removed Function Configuration Reported
Description
The market delisting runbook instructs operators to call:
Config.setStaticPriceForToken(token, pricesParams)The latest v2.2c commit
fd095157does not expose this function. Static price freezing in that commit is performed through:Config.setStaticPriceForMarketIndexToken(market, pricesParams)This function accepts the market token address, loads the market, verifies it exists, derives the
indexToken, stores the current oracle min/max as static prices, and switches that index token toStaticOracleProvider.Recommendation
Determine which commit the runbook should reference, and update the Phase 3 reference accordingly.
-
I-03 Informational Overcollateralized Small Position Can Block 100% Liquidation Cleanup DoS Reported
Description
The runbook relies on setting the
minCollateralFactorForLiquidationto 100% to make all remaining positions liquidatable. However this is not the case for overcollateralized position. This enables an attack vector to DoS / slow down the delisting runbook as a malicious actor could create a very small, but heavily overcollateralized position before the delisting flow starts.Recommendation
Document this behavior and consider to increase the factor further in that case.
No findings match.
More from GMX
All 52 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.
