GMX V1 Arbitrum OrderBook Refund Cross-Contract Reentrancy / GLP Price Manipulation (2025-07-09)

  • Name: GMX V1 Arbitrum OrderBook refund cross-contract reentrancy / GLP price manipulation
  • Date: 2025-07-09
  • Protocol: GMX V1
  • Chain: Arbitrum
  • Category: perpetual DEX accounting desynchronization / cross-contract reentrancy / temporary-privilege-window abuse / GLP AUM manipulation
  • Loss: about 40.1Mrecovered into stablecoins, about0.9Mof segregated V1 fees, and an implied shortfall of roughly5M` bounty
  • Summary: GMX V1 split short-accounting responsibility across two layers. ShortsTracker.updateGlobalShortData() updated globalShortAveragePrices in the keeper-controlled order-execution path, while Vault.increasePosition() still updated globalShortSizes lower in the stack. During OrderBook.executeDecreaseOrder() for a WETH position, GMX refunded ETH to the user with _transferOutETH(), which handed control to attacker contract 0x7D3BD50336f64b7A473C51f54e7f0Bd6771cc355 through its fallback function while the temporary isLeverageEnabled window was still open. The attacker used that callback to call Vault.increasePosition() directly, growing WBTC short size without the corresponding ShortsTracker average-price update. After repeating that skew cycle several times, the attacker used a 7.538M USDC flash loan to mint GLP cheaply, opened one more huge direct short to make AUM explode, and redeemed GLP at the manipulated price to pull about $42M from the Arbitrum GLP pool.
  • Trigger / entry point:
    • GMX V1 keepers executed user orders through PositionManager / OrderBook, temporarily opening a leverage window via Timelock.enableLeverage(_vault).
    • For WETH decrease orders on the orderbook path, OrderBook.executeDecreaseOrder() eventually called _transferOutETH() and sent ETH to the order owner.
    • GMX treated _account / receiver as if it were a passive user address, but the attacker made it a contract with a malicious fallback function.
    • That callback fired before the leverage window closed, creating a path from keeper-mediated order execution into direct Vault.increasePosition() calls.
  • Root cause:
    • GMX’s reentrancy assumptions were local while the dangerous state was cross-contract.
    • OrderBook’s refund path yielded execution to attacker code, but the protocol relied on a temporary isLeverageEnabled flag rather than a fully sealed end-to-end execution context.
    • Vault.increasePosition() could therefore be reached during the callback and would update globalShortSizes without the matching ShortsTracker.updateGlobalShortData() call that normally keeps globalShortAveragePrices in sync.
    • That split accounting let the attacker manufacture a state where GLP AUM treated WBTC shorts as if a much larger short book had been opened at a much lower average price than reality.
    • Multiple post-incident analyses tied the exploitable split to GMX’s 2022 response to the earlier Collider-disclosed GLP bug: short-average-price updates were moved upward into ShortsTracker, but short-size mutations still happened lower in Vault, leaving a new cross-layer desynchronization surface.
  • Exploit path:
    1. The attacker deployed contract 0x7D3BD50336f64b7A473C51f54e7f0Bd6771cc355, opened a small WETH long position through GMX’s ordinary order flow, and then created a matching decrease order using that contract as the receiving account.
    2. When a GMX keeper executed the decrease order, OrderBook.executeDecreaseOrder() closed the WETH position and _transferOutETH() sent ETH to the attacker contract.
    3. The fallback function executed while isLeverageEnabled was still true, so the attacker directly called Vault.increasePosition() to open a WBTC short without going through PositionManager / ShortsTracker.
    4. The attacker then created a normal decrease order for that WBTC short. When a keeper later executed the close through the ordinary router path, ShortsTracker updated globalShortAveragePrices against a short book that had been opened without the corresponding earlier average-price sync.
    5. Repeating this loop five times pushed WBTC globalShortAveragePrices from roughly 1.087e35 down to about 1.913e33, which public postmortems summarized as a collapse from about $109,505.77 to $1,913.70.
    6. In the final profit transaction 0x03182d3f0956a91c4e4c8f225bbc7975f9434fab042228c7acdc5ec9a32626ef, the same callback window was used again. The attacker flash-loaned about 7.538M USDC, minted and staked GLP with 6M USDC, then used the remaining 1.538M USDC to open one more oversized direct WBTC short through Vault.increasePosition().
    7. Because GLP AUM combines Vault short size with ShortsTracker average price, this final direct short made AUM jump violently while the average price stayed stale. BlockSec’s reconstruction put AUM at roughly 4.69e37 before the final step and about 9.18e38 after it.
    8. The attacker then called unstakeAndRedeemGlp() repeatedly, redeeming GLP at the inflated price for vault assets across USDC, ETH, and WBTC, closed the manipulated short, repaid the flash loan, and kept the extracted assets.
    9. After GMX’s onchain negotiation message and bounty offer, the attacker returned most of the funds and kept a 5M white-hat-style reward.
  • What the attacker needed to be true:
    • GMX had to allow a contract account, not just an EOA, to receive ETH refunds during keeper order execution.
    • The ETH refund had to happen before Timelock.disableLeverage(_vault) closed the temporary privilege window.
    • Vault.increasePosition() had to remain reachable as long as isLeverageEnabled was true, even if the call bypassed the expected PositionManager / ShortsTracker route.
    • GLP pricing had to mix short-size data from Vault with short-average-price data from ShortsTracker, so partial updates produced monetizable AUM fiction.
    • The Arbitrum GLP pool had to hold enough liquid assets for inflated GLP redemptions to cash out into real vault inventory.
  • Technical anatomy worth remembering:
    • This was not just missing nonReentrant. OrderBook and Vault each had their own local assumptions, and the profit emerged from the gap between them.
    • The dangerous callback surface was an ETH refund in the keeper path, not ERC-777 hooks, ERC-721 receivers, or arbitrary user-supplied tokens.
    • The economically decisive bug was state split across layers: globalShortSizes in Vault, globalShortAveragePrices in ShortsTracker, and GLP AUM trusting both as if they were updated atomically.
    • GMX’s temporary leverage window acted like a capability that was supposed to be exercised only by the intended router path. Reentrancy turned that temporary permission into a general-purpose direct entrypoint.
    • The exploit is a good reminder that modularization can turn a previously local invariant into an inter-contract invariant. Once that happens, single-contract reentrancy guards stop being the whole story.
  • Representative transactions and addresses:
    • Main profit transaction: 0x03182d3f0956a91c4e4c8f225bbc7975f9434fab042228c7acdc5ec9a32626ef
    • Attack-contract deployment: 0xa4ece5a7f106f2fa62dbd0d03441183aeb650d8f587e5c1706e1e5488cd4c93f
    • First skewing decrease-order execution: 0x1f00da742318ad1807b6ea8283bfe22b4a8ab0bc98fe428fbfe443746a4a7353
    • GMX onchain bounty message: 0x92a39e66e54aff033cd7b41b468de7891cf459593495d68d78099cc889547380
    • Refund transactions: 0x62b8450259ee85838c7be3335e659a65be66268a1a5c6792ca731df199211841, 0x255d0af85ae52c22e0206cad52d22f8620c002b8e23efcc3162916d8559321b3, 0xceafc389d50eb1dea3df64f1565a0c39df206f05848f02ea7a72babda6313b22
    • Attacker EOA: 0xDF3340A436c27655bA62F8281565C9925C3a5221
    • Attack contract: 0x7D3BD50336f64b7A473C51f54e7f0Bd6771cc355
    • GMX Vault: 0x489ee077994B6658eAfA855C308275EAd8097C4A
    • GMX OrderBook: 0x09f77E8A13De9a35a7231028187e9fD5DB8a2ACB
    • GMX ShortsTracker: 0xf58eEc83Ba28ddd79390B9e90C4d3EbfF1d434da
    • GMX GlpManager: 0x3963FfC9dff443c2A94f21b129D429891E32ec18
  • Response / mitigation:
    • GMX suspended V1 trading, minting, and redemption on Arbitrum and Avalanche while investigating.
    • The team publicly stated V2 was unaffected.
    • GMX posted an onchain message offering a 10% bounty in exchange for the return of funds and no legal action.
    • Most funds were returned on 2025-07-11 and moved into the GMX security multisig; later DAO accounting described about $40.1M held in stablecoins, about $3.0M of residual value still inside GLP, and about $0.9M of segregated V1 fees against about $46.0M of Arbitrum GLP value at incident time.
    • In the follow-on governance discussion GMX contributors said V1 remained paused on both Arbitrum and Avalanche and that sunsetting V1 was the most probable outcome rather than attempting a full restart.
    • The incident triggered renewed scrutiny of GMX V1 forks because the vulnerable execution and accounting pattern was inherited code, not a one-off deployment mistake.

Pattern tags

  • keeper-mediated native-asset refund left a broad leverage capability live across external delivery
  • Vault short size and ShortsTracker average price were one GLP AUM truth object, but GMX let them update on different paths
  • direct privileged entry into Vault bypassed the accounting companion module that the normal router / orderbook path was supposed to enforce
  • earlier GLP hardening moved one invariant upward but did not re-bind every lower-layer state mutation that GLP pricing still trusted

Why the exploit worked

  • GMX assumed the order-execution frame stayed coherent because keepers opened isLeverageEnabled, routed through OrderBook / PositionManager, and would later close the privilege window. The ETH refund in _transferOutETH() broke that assumption by handing control to attacker code before the capability narrowed again.
  • During that callback the attacker did not need to reenter the same function. They only needed some still-authorized path into Vault.increasePosition(), and the still-open leverage window gave them exactly that.
  • Once Vault could grow globalShortSizes without the paired ShortsTracker.updateGlobalShortData() call, GLP AUM stopped being one canonical short-book object. GlpManager still priced GLP as if size and average price described the same book, so stale average-price state became spendable LP value.
  • The exploit therefore monetized two linked design failures, not one isolated callback bug: a temporary capability that survived external delivery, and a cross-module accounting split where partial updates could still publish money-good AUM truth.
  • GMX’s older Collider-era fix is worth preserving as part of the lesson. The protocol had already learned that GLP short accounting needed more care, but moving average-price maintenance into ShortsTracker without making every short-size mutation consume the same invariant created a new desynchronization rail.

Detection and audit pivots

  • Inventory every temporary privilege window (enableLeverage, keeper-only execution toggles, flash-loan modes, liquidation modes, migration flags) and then ask the sharper question: can any external delivery edge — native-asset refund, token transfer, callback, helper self-call — happen before that window fully closes?
  • Treat native ETH refund to arbitrary receiver as a first-class callback surface, not a safer cousin of token transfers. If a refund happens while a privileged execution frame is still live, model the receiver exactly like hostile callback code.
  • Map every state variable that one pricing or solvency formula consumes together. If AUM or collateral power is derived from multiple modules (Vault size plus ShortsTracker average price here), review whether any privileged path can mutate one component without the others.
  • When a patch moves invariant maintenance up or down the stack, re-audit every remaining lower-layer mutator. The regression question is not just did the new helper run? but can any still-authorized old path mutate state that the helper was supposed to keep in sync?
  • Search for direct entrypoints that become reachable whenever a broad feature flag is true. If Vault.increasePosition() was only meant to be safe inside router-managed flows, the code should machine-check that narrower caller/context object instead of trusting a coarse global isLeverageEnabled bit.

Failure-prevention checklist

  • Bind keeper execution to one explicit frame object: order key, intended account / receiver, permitted next call surface, short-accounting side effects, and the exact point at which the leverage window is consumed.
  • Close broad capability windows before any external value delivery. Native-asset refunds, fee rebates, residual payout sends, and withdrawETH helpers are callback edges even when the surrounding flow feels keeper-controlled.
  • Keep size and average price mutations on one money-good path. If GLP AUM trusts both as one short-book object, no direct entrypoint should be able to mutate either half without consuming the same invariant-maintenance routine.
  • Replace coarse global execution flags like isLeverageEnabled with narrower machine-checkable authorization that binds caller, stage, and allowed verb. Some privileged flow is active is too broad if Vault.increasePosition() is otherwise callable.
  • Treat direct-to-vault mutators as dangerous even when the intended UX goes through routers, order books, or position managers. Low-level entrypoints should either be impossible during higher-level flows or independently re-verify the same accounting preconditions.
  • When fixing one accounting bug by moving logic across modules, re-audit every remaining mutator that still touches the conserved state. A patch that relocates one half of an invariant can create a new split-accounting exploit surface.
  • Quarantine LP mint / redeem authority from unsettled perp-accounting transitions. If AUM depends on recently mutated short-book state, same-transaction GLP minting and redemption should not be able to cash out partially updated truth.
  • Adversarially test refund callback -> direct vault entry -> later normal close -> LP mint / redeem chains, not just same-function recursion or obvious token-hook loops.

Audit prompts

  • What exact object proves that a keeper-mediated order execution may still call Vault.increasePosition() right now: the intended order / router frame, or only a broad isLeverageEnabled bit?
  • Can any ETH refund, token send, plugin call, callback target, or helper payout happen before that privilege window is fully closed?
  • Which state variables does GLP AUM consume together, and is there any path that can mutate one of them without synchronizing the others in the same authenticated frame?
  • If a user or keeper bypasses the normal PositionManager / OrderBook route and hits Vault directly, what concrete check proves the missing companion updates have already happened?
  • Did an earlier mitigation move part of the invariant up or down the stack? If so, which legacy mutators or newly reachable paths still write the old half of the same security object?
  • Are native-asset refunds treated as passive value delivery in review, even though they hand control to arbitrary receiver code with the same economic power as a token hook?
  • Can GLP minting, staking, redemption, or other AUM-sensitive actions occur in the same transaction that leaves short-book accounting temporarily desynchronized?
  • What property tests prove that no sequence of partial short-state updates can make GLP mint cheap and redeem expensive against the same underlying pool inventory?

Lessons that should already be standard

  • Keeper flow is not a security boundary. Once a keeper-controlled path sends ETH to an arbitrary receiver, the receiver is part of the control plane.
  • Temporary privilege flags are dangerous by default. If the protocol cannot name the exact actor, stage, and allowed next action that the flag authorizes, it is probably broader than reviewers think.
  • Split accounting only stays safe when every mutator consumes the same invariant. GMX is the reminder that moving average price logic into a helper does not help if size can still change elsewhere under spendable downstream pricing.
  • Refund paths deserve the same paranoia as swaps and borrow transfers. The profitable callback edge here was not an exotic token standard; it was plain ETH delivery in an order-execution path.
  • Patch regressions often come from relocating invariants rather than deleting them. Teams should ask not only did the bug get fixed? but what other path still writes the conserved state from the old layer?
  • LP pricing modules are downstream consumers of upstream execution truth. If AUM reads a half-updated perp book, the LP token becomes the cash-out surface for the earlier control-flow mistake.
  • Coarse global toggles age badly in modular systems. As soon as multiple contracts participate in one economic invariant, enabled right now is usually weaker than this exact state transition is authorized.

Why this incident matters

  • It preserves a mechanically distinct reentrancy cousin: the callback was a keeper-mediated ETH refund, but the money was made by corrupting accounting in another contract family and then cashing out through LP share pricing.
  • It shows how temporary permission flags (isLeverageEnabled) can become exploitable capability leaks when any external call happens before the window fully closes.
  • It is also a valuable patch-regression case. GMX’s earlier fix improved one GLP pricing bug but left short accounting split between Vault and ShortsTracker, so the protocol stopped being wrong in one way and became exploitable in another.
  • More broadly, it is a strong reminder that refund path, still-authorized execution frame, and AUM accounting truth can be one joined security object even when they live in different contracts and were reviewed by different mental models.

Internal linkages

  • Core recurring pattern: callback-frame-authority-reuse.
  • Parent family: reentrancy.
  • Sharper positive-design companion: callback-frame-truth-security-object — GMX needed one authenticated keeper-execution frame binding refund callback source, affected order, temporary leverage privilege, allowed resume path, and the short-accounting state that GLP AUM would later trust.
  • Ordering companion: temporary-capability-closure-before-external-delivery — the broad keeper / leverage capability should have been extinguished before _transferOutETH() handed control to the receiver at all.
  • End-to-end routing companion: route-commitment-across-callback-frame-and-settlement-envelope — useful for the broader design question of how GMX should have kept one keeper callback frame, one short-accounting update path, and one later GLP-settlement object pinned across the same route lifecycle instead of letting refund-time control flow reopen a half-authenticated execution state.

Control surface