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.1M
recovered 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()updatedglobalShortAveragePricesin the keeper-controlled order-execution path, whileVault.increasePosition()still updatedglobalShortSizeslower in the stack. DuringOrderBook.executeDecreaseOrder()for a WETH position, GMX refunded ETH to the user with_transferOutETH(), which handed control to attacker contract0x7D3BD50336f64b7A473C51f54e7f0Bd6771cc355through its fallback function while the temporaryisLeverageEnabledwindow was still open. The attacker used that callback to callVault.increasePosition()directly, growing WBTC short size without the correspondingShortsTrackeraverage-price update. After repeating that skew cycle several times, the attacker used a7.538M USDCflash 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$42Mfrom the Arbitrum GLP pool. - Trigger / entry point:
- GMX V1 keepers executed user orders through
PositionManager/OrderBook, temporarily opening a leverage window viaTimelock.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.
- GMX V1 keepers executed user orders through
- 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 temporaryisLeverageEnabledflag rather than a fully sealed end-to-end execution context.Vault.increasePosition()could therefore be reached during the callback and would updateglobalShortSizeswithout the matchingShortsTracker.updateGlobalShortData()call that normally keepsglobalShortAveragePricesin 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 inVault, leaving a new cross-layer desynchronization surface.
- Exploit path:
- 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. - When a GMX keeper executed the decrease order,
OrderBook.executeDecreaseOrder()closed the WETH position and_transferOutETH()sent ETH to the attacker contract. - The fallback function executed while
isLeverageEnabledwas stilltrue, so the attacker directly calledVault.increasePosition()to open a WBTC short without going throughPositionManager/ShortsTracker. - The attacker then created a normal decrease order for that WBTC short. When a keeper later executed the close through the ordinary router path,
ShortsTrackerupdatedglobalShortAveragePricesagainst a short book that had been opened without the corresponding earlier average-price sync. - Repeating this loop five times pushed WBTC
globalShortAveragePricesfrom roughly1.087e35down to about1.913e33, which public postmortems summarized as a collapse from about$109,505.77to$1,913.70. - In the final profit transaction
0x03182d3f0956a91c4e4c8f225bbc7975f9434fab042228c7acdc5ec9a32626ef, the same callback window was used again. The attacker flash-loaned about7.538M USDC, minted and staked GLP with6M USDC, then used the remaining1.538M USDCto open one more oversized direct WBTC short throughVault.increasePosition(). - Because GLP AUM combines
Vaultshort size withShortsTrackeraverage price, this final direct short made AUM jump violently while the average price stayed stale. BlockSec’s reconstruction put AUM at roughly4.69e37before the final step and about9.18e38after it. - 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. - After GMX’s onchain negotiation message and bounty offer, the attacker returned most of the funds and kept a
5Mwhite-hat-style reward.
- The attacker deployed contract
- 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 asisLeverageEnabledwas true, even if the call bypassed the expectedPositionManager/ShortsTrackerroute.- GLP pricing had to mix short-size data from
Vaultwith short-average-price data fromShortsTracker, 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.OrderBookandVaulteach 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:
globalShortSizesinVault,globalShortAveragePricesinShortsTracker, 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.
- This was not just
- 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
- Main profit transaction:
- 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.1Mheld in stablecoins, about$3.0Mof residual value still inside GLP, and about$0.9Mof segregated V1 fees against about$46.0Mof 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 deliveryVault short size and ShortsTracker average price were one GLP AUM truth object, but GMX let them update on different pathsdirect privileged entry into Vault bypassed the accounting companion module that the normal router / orderbook path was supposed to enforceearlier 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 throughOrderBook/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
Vaultcould growglobalShortSizeswithout the pairedShortsTracker.updateGlobalShortData()call, GLP AUM stopped being one canonical short-book object.GlpManagerstill priced GLP as ifsizeandaverage pricedescribed 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
ShortsTrackerwithout 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 receiveras 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
AUMor collateral power is derived from multiple modules (Vaultsize plusShortsTrackeraverage 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?butcan 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 globalisLeverageEnabledbit.
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
withdrawETHhelpers are callback edges even when the surrounding flow feels keeper-controlled. - Keep
sizeandaverage pricemutations 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
isLeverageEnabledwith narrower machine-checkable authorization that binds caller, stage, and allowed verb.Some privileged flow is activeis too broad ifVault.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 / redeemchains, 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 broadisLeverageEnabledbit? - 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/OrderBookroute and hitsVaultdirectly, 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 flowis 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 pricelogic into a helper does not help ifsizecan 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?butwhat 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 nowis usually weaker thanthis 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
VaultandShortsTracker, 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, andAUM accounting truthcan 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
-
The leverage sat in GMX’s keeper execution path, the temporary leverage window, and the accounting boundary between
Vault,ShortsTracker,OrderBook, andGlpManager. -
The exploit was not about perp trading complexity in the abstract. It was GMX allowing a callback inside a privileged order-execution window while the protocol’s notion of
global short statewas split across modules that were no longer updated atomically. -
Sources:
- https://x.com/GMX_IO/status/1942955807756165574
- https://x.com/GMX_IO/status/1943336664102756471
- https://docs.gmx.io/docs/archived/contracts-v1
- https://gov.gmx.io/t/discussion-resolve-the-distribution-plan-for-the-recovered-funds-from-arbitrum-glp/4678.json
- https://raw.githubusercontent.com/gmx-io/gmx-contracts/master/contracts/core/Vault.sol
- https://raw.githubusercontent.com/gmx-io/gmx-contracts/master/contracts/core/ShortsTracker.sol
- https://blocksec.com/blog/gmx-incident-cross-contract-reentrancy-bypasses-a-four-year-old-guard
- https://www.certik.com/blog/gmx-incident-analysis
- https://ackee.xyz/blog/gmx-hack-analysis-attack-scenarios-with-wake/
- https://rekt.news/gmx-rekt
- https://www.collider.vc/post/gmx-granted-million-dollar-bug-bounty-to-collider-the-bug-aftermath
-
Last reviewed: 2026-07-20 UTC