Skip to content
$1,000,000 in security audit grants are live now, Apply here →

Security review · May 2026

Token Delisting Procedure

for GMX

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

1 acknowledged · 10 reported · 13 pending

Findings 24

Main Review

13 findings · May 13 to 18, 2026
  1. H-01 High Full Impact Pool Withdrawal Step Will Likely DoS Itself DoS Pending
    Round
    Main Review

    Description

    In Phase 4 the remaining position impact pool is withdrawn. This is a 2-Step flow with a time delay and the withdrawFromPositionImpactPool.ts script is used to do so.

    This script is called with FULL_WITHDRAWAL=true for 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 positionImpactPoolDistributionRate is 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 the FULL_WITHDRAWAL=true flag 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 positionImpactPoolDistributionRate of the market to 0 before calling the withdrawFromPositionImpactPool.ts script.

  2. H-02 High Runbook Script Commands Do Not Match Actual Script Inputs Configuration Pending
    Round
    Main Review

    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.ts ignores MARKET; it uses a hardcoded market instead.
    • withdrawFromPositionImpactPool.ts ignores RECEIVER; it uses a hardcoded receiver address.
    • updateMarketPositionImpactDistributionConfig.ts ignores MARKET; it applies all pending impact-distribution config changes across configured markets.
    • updateMarketConfig.ts ignores SKIP_BORROWING_FEE_FOR_SMALLER_SIDE

    Recommendation

    Update the runbook and scripts so every documented environment variable is either implemented or removed.

  3. H-03 High Phase 2 Skip Borrowing-Fee Toggle Affects All Markets Configuration Pending
    Round
    Main Review

    Description

    In phase 2 SKIP_BORROWING_FEE_FOR_SMALLER_SIDE is 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
  4. H-04 High Zero-valued config changes skipped by script Error Pending
    Round
    Main Review

    Description

    In the market delisting playbook, Phase 1d says set maxPoolUsdForDeposit values to 0, and Phase 2 says set ABOVE_OPTIMAL_USAGE_BORROWING_FACTOR to 0. However, in updateMarketConfigUtils.ts (line 112)the addConfigItem returns 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 addConfigItem in updateMarketConfigUtils.ts so it only skips truly missing values, not valid zero values:

    if (value === undefined || value === null) {
    return;
    }
    
  5. M-01 Medium Static Prices Unusable When Ref Price Diverge DoS Pending
    Round
    Main Review

    Description

    StaticOracleProvider is 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. StaticOracleProvider explicitly reports that it is not a Chainlink on-chain provider:

    function isChainlinkOnChainProvider() external pure returns (bool) {
        return false;
    }
    

    As a result, Oracle._validatePrices applies 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_FACTOR of 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 with MaxRefPriceDeviationExceeded. 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 calling lastRoundData on the chainlink feed will lead to a revert.

    Recommendation

    Do not apply the live Chainlink reference-price deviation check to explicitly frozen static prices.

  6. M-02 Medium Wrong Role Defintions Access Control Pending
    Round
    Main Review

    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_KEEPER but in reality it’s only executable by the CONFIG_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.

  7. M-03 Medium GLV Market Removal DoS Griefing Attack DoS Pending
    Round
    Main Review

    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:

    1. GLV market is disabled
    2. Keeper creates a GLV shift for the full accounted GM balance
    3. Attacker transfers 1 wei GM directly to the GLV before executeGlvShift
    4. executeGlvShift transfers out the accounted GM amount
    5. StrictBank._afterTransferOut syncs tokenBalances[market] to the actual ERC20 balance, which is now the attacker’s 1 wei dust
    6. removeMarketFromGlv reverts with GlvNonZeroMarketBalance

    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.

  8. M-04 Medium Static Price Fallback Uses Incorrect DataStore Key Shape Configuration Pending
    Round
    Main Review

    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. StaticOracleProvider reads two separate uint values: Keys.staticOraclePriceKey(token, false) // min Keys.staticOraclePriceKey(token, true) // max The runbook also suggests switching the oracle provider via scripts/updateOracleConfigForTokens.ts, but that script only supports chainlink feeds.

    Recommendation

    Update the fallback instructions to match the actual storage layout and tooling.

  9. M-05 Medium Phase 2 Misstates How to Disable Borrowing Kink Model Configuration Pending
    Round
    Main Review

    Description

    Phase 2 says to “disable kink model” by setting ABOVE_OPTIMAL_USAGE_BORROWING_FACTOR to 0. This does not disable the kink model, the kink branch is selected whenever OPTIMAL_USAGE_FACTOR != 0. ABOVE_OPTIMAL_USAGE_BORROWING_FACTOR only controls the extra borrowing-rate slope after utilization exceeds OPTIMAL_USAGE_FACTOR. Setting it to 0 removes that above-optimal surcharge, but the rate still uses the kink-model base formula

    Recommendation

    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_FACTOR to 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.
  10. M-06 Medium LPs Can Game Remaining Delisting Impact Pool Distribution Gaming Pending
    Round
    Main Review

    Description

    In the market delisting playbook, Phase 1b starts draining the position impact pool by setting minPositionImpactPoolAmount = 0 and increasing positionImpactPoolDistributionRate. Phase 1d freezes deposits later by setting MAX_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.

  11. L-01 Low Residual Swap Impact Pool Funds May Be Stuck After Market Disable Rewards Pending
    Round
    Main Review

    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 because validateSwapMarket requires the market to be enabled.

    Recommendation

    Be aware about that and consider to implement a withdraw from swap impact pool function if needed.

  12. I-01 Informational Swaps Remain Open Informational Pending
    Round
    Main Review

    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.

  13. I-02 Informational Disabled Markets May Leave Stale Swap Virtual Inventory Informational Pending
    Round
    Main Review

    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
  1. M-01 Medium Runbook Incorrectly States priceFeedKey Cannot Be Cleared DoS Acknowledged
    Location
    Market Delisting Runbook
    Round
    Remediation Review

    Description

    The Market Delisting runbook states for Phase 3 (Option 1) that on-chain cleanup of priceFeedKey(token) has no current Config / TimelockConfig path and would require a contract upgrade. This is inaccurate for priceFeedKey. Config.setAddress cannot clear it because PRICE_FEED is not allowlisted in Config._initAllowedBaseKeys, but TimelockConfig.signalSetPriceFeed schedules a direct DataStore.setAddress(Keys.priceFeedKey(token), priceFeed) call and does not reject address(0). Clearing priceFeedKey(token) matters because ChainlinkPriceFeedUtils.getPriceFeedPrice returns (false, 0) when the configured feed address is zero. This skips Chainlink reference-price validation, which otherwise can make StaticOracleProvider unusable if the live feed diverges, becomes stale, or reverts. This should be distinguished from oracleProviderForToken: there is no safe dedicated clear path for that key. Config.setOracleProviderForToken requires the provider to be enabled, and enabling address(0) as an oracle provider would be an unsafe abuse of the provider allowlist.

    Recommendation

    Update the runbooks to state:

    • Config.setAddress cannot clear priceFeedKey(token).
    • TimelockConfig.signalSetPriceFeed(token, address(0), ...) can clear priceFeedKey(token).
  2. M-02 Medium Phase 3 Role Guidance Incorrectly Allows LIMITED_CONFIG_KEEPER DoS Reported
    Location
    Market Delisting Runbook
    Round
    Remediation Review

    Description

    The market delisting runbook correctly states in its prerequisites that Phase 3 requires CONFIG_KEEPER, but the Phase 3 forced-liquidation section later says raising minCollateralFactorForLiquidation can be executed by LIMITED_CONFIG_KEEPER or CONFIG_KEEPER. This regresses the earlier M-02 role-fix finding. MIN_COLLATERAL_FACTOR_FOR_LIQUIDATION is included in Config._initAllowedBaseKeys, but it is not included in Config._initAllowedLimitedBaseKeys. A LIMITED_CONFIG_KEEPER call through Config.setUint would therefore revert with InvalidBaseKey. 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 minCollateralFactorForLiquidation requires CONFIG_KEEPER only. Keep the prerequisite and per-step role guidance consistent.

  3. M-03 Medium Wrong Allowance Spender for Lent Impact Repayment Configuration Reported
    Location
    Market Delisting Runbook
    Round
    Remediation Review

    Description

    Phase 4a instructs fundingAccount to approve the market contract for both market.longToken and market.shortToken before executing reduceLentImpactAmount. This is the wrong spender. The timelock executes ConfigTimelockController.reduceLentImpactAmount, which calls PositionImpactPoolUtils.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 for ConfigTimelockController. 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 fundingAccount to approve ConfigTimelockController as the spender for both long and short tokens. If longToken == shortToken, approve enough for the combined long-token and short-token transfer amount.

  4. M-04 Medium GLV Removal Before Impact-Pool Distribution Can Misallocate Value Rewards Reported
    Round
    Remediation Review

    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.

  5. M-05 Medium Borrowing Incentive Guidance Can Raise an Ignored Parameter Configuration Reported
    Round
    Remediation Review

    Description

    The runbook states out to raise the BORROWING_FACTOR and/or BASE_BORROWING_FACTOR, then set the OPTIMAL_USAGE_FACTOR to zero to disable the kink model. However once OPTIMAL_USAGE_FACTOR is zero, the non-kink borrowing calculation is used and BASE_BORROWING_FACTOR is ignored. Therefore if the BASE_BORROWING_FACTOR is raised, the intended voluntary-exit incentive do not increase.

    Recommendation

    Increase BORROWING_FACTOR to the intended effective rate for both long and short sides. The BASE_BORROWING_FACTOR only applies while the kink model remains active.

  6. M-06 Medium Residual Long/Short Pool Tokens Are Not Handled Documentation Reported
    Location
    Market Delisting Runbook
    Round
    Remediation Review

    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 / shortToken pool balances from the market to treasury. As a result, treasury may compensate residual LPs using separate treasury funds while the corresponding longToken / shortToken backing 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 poolAmountKey values after disable. Recovery appears possible through privileged governance operations. ConfigTimelockController is granted CONTROLLER, and governance can schedule arbitrary calls directly through the underlying timelock controller, via a scheduled batch call

  7. L-01 Low Phase 1c GLV shift-out can be blocked DoS Reported
    Round
    Remediation Review

    Description

    Phase 1B of the market delisting playbook says "Multiple shift transactions may be required depending on balance size (choose different TO_MARKET values)." That's correct but only names one of four reasons a MARKET_TOKEN_AMOUNT=ALL shift can revert. The four constraints are:

    1. GlvMaxMarketTokenBalance{Amount,Usd}Exceeded: Full amount pushes the destination past its per-GLV cap (this is covered in playbook)
    2. GlvShiftMaxLossExceeded: Combined withdraw + deposit price impact exceeds glvShiftMaxLossFactor
    3. GlvShiftIntervalNotYetPassed: Wall-clock rate limit on shifts per GLV
    4. MaxPnlFactorExceeded: 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.

  8. L-02 Low Pending Target-Market Orders Can Revert After Final Disable DoS Reported
    Round
    Remediation Review

    Description

    The market delisting playbook reaches final disable in Phase 4d by setting isDisabled: true for 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 generic ExecuteOrder selected that order and reverted with DisabledMarket(0x324AA6331C60D0089e06c8Ec26112F339a56F398). The index-token delisting runbook notes that pending orders can be cancelled by users after terminal disable, because cancelOrder does 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.

  9. I-01 Informational Process Summary Retains Stale Bilateral Borrowing Reference Documentation Reported
    Location
    Market Delisting Runbook
    Round
    Remediation Review

    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.

  10. I-02 Informational Phase 3 Static Price Step References Removed Function Configuration Reported
    Location
    Market Delisting Runbook
    Round
    Remediation Review

    Description

    The market delisting runbook instructs operators to call:

    Config.setStaticPriceForToken(token, pricesParams)
    

    The latest v2.2c commit fd095157 does 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 to StaticOracleProvider.

    Recommendation

    Determine which commit the runbook should reference, and update the Phase 3 reference accordingly.

  11. I-03 Informational Overcollateralized Small Position Can Block 100% Liquidation Cleanup DoS Reported
    Round
    Remediation Review

    Description

    The runbook relies on setting the minCollateralFactorForLiquidation to 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.

More from GMX

All 52 reports
  1. Incentives Contracts

    17 findings 17 findings: 3 medium, 3 low, 11 informational
  2. Donation Contracts

    25 findings 25 findings: 1 medium, 6 low, 18 informational
  3. V2.3 Pre-Review

    7 findings 7 findings: 7 informational
  4. Risk Oracle

    13 findings 13 findings: 2 medium, 4 low, 7 informational

Put 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.

Get a quote