This analysis examines the 42M attack on the GMX protocol. We provide a detailed technical breakdown of the vulnerability and include a working reproduction of the attack scenario for educational purposes in a forked environment.
The attack exploited a cross-contract reentrancy vulnerability that bypassed access controls during position increases. This resulted in GLP token price manipulation with a higher price, allowing attackers to redeem tokens at the manipulated price and extract profits from the protocol.

Reproducing with Wake
- Clone the repository
- Import GMX project dependencies:
$ npm i - Initialise Wake:
$ wake up - Get an Arbitrum fork URL from Alchemy or other providers and set it in
.envsimilar to.env.example. - Run the test:
$ wake test tests/test_attack_simple.py - Uncomment
print(tx.call_trace)to see the call trace.
Root cause
The vulnerability stems from a reentrancy issue. While the reentrancy itself is straightforward, its impact is significant.
The core issue: GLP token price calculation relies on the globalShortAveragePrices variable from ShortsTracker. This dependency creates an exploitable attack vector.
The vulnerability is cross-contract reentrancy. Multiple contracts were involved during the transaction. Each contract has a reentrancy guard; however, reentrancy occurred after it had already exited for one specific contract.
Entry point
The attack begins when a user increases their position:
- User calls
createIncreaseOrderto register the order - The orderkeeper bot calls
PositionManager.executeIncreaseOrderto execute it - Within
executeIncreaseOrder,ShortsTracker.updateGlobalShortDatais invoked
ShortsTracker.updateGlobalShortData stores the globalShortAveragePrice for the token — the average entry price of all short positions. This value directly influences GLP token price calculations.
contract PositionManager {
function executeIncreaseOrder(
address _account,
uint256 _orderIndex,
address payable _feeReceiver
) external onlyOrderKeeper {
//...
IShortsTracker(shortsTracker).updateGlobalShortData(_account, collateralToken, indexToken, isLong, sizeDelta, markPrice, true);
ITimelock(timelock).enableLeverage(_vault); // isLeverageEnabled <- True
IOrderBook(orderBook).executeIncreaseOrder(_account, _orderIndex, _feeReceiver);
ITimelock(timelock).disableLeverage(_vault); // isLeverageEnabled <- False
_emitDecreasePositionReferral(_account, sizeDelta);
}
}
External calls follow this path to reach the Vault:
OrderBook.executeIncreaseOrderRouter.pluginIncreasePositionVault.increasePosition
The decreasePosition flow follows a similar pattern.
The Vault.increasePosition function checks that isLeverageEnabled equals True to verify that the call occurs between Timelock.enableLeverage and Timelock.disableLeverage. This check proved insufficient.
contract Vault {
// function has no msg.sender check.
// Assumes caller transfers tokens or at least the caller is trusted.
function increasePosition(
address _account,
address _collateralToken,
address _indexToken,
uint256 _sizeDelta,
bool _isLong
) external override nonReentrant {
_validate(isLeverageEnabled, 28); // this will be bypassed
_validateGasPrice();
_validateRouter(_account);
...
...
}
}
During Vault.decreasePosition, the contract transfers collateral tokens for closed positions. When the collateral token is WETH, the system withdraws ETH and transfers it to the user’s account. Notably, these WETH operations occur outside the Vault contract.
The call flow proceeds as follows:
OrderBook.executeDecreaseOrderRouter.pluginDecreasePositionVault.decreasePosition- ReentrancyGuard set to
ENTERED Vaultcloses position- Sends WETH to OrderBook
- ReentrancyGuard set to
NOT_ENTERED
- ReentrancyGuard set to
OrderBookwithdraws ETH- Transfers ETH to user
User.receiveis triggeredVault.increasePosition(exploit)- ReentrancyGuard confirms
NOT_ENTERED - ReentrancyGuard is set to
ENTERED - Attack continues…
- ReentrancyGuard confirms
The reentrancy guard in Vault starts as NOT_ENTERED, but the reentrant call occurs after this status has been reset, bypassing the protection.
Attack escalation
The direct Vault.increasePosition call bypasses ShortsTracker.updateGlobalShortData, causing GlpManager.getAum to return inflated values and artificially increase the GLP token price.
Attack sequence:
- Re-enter through the open entry point
- Add liquidity to obtain GLP tokens
- Call
increasePositionto manipulate the GLP token price upward - Remove liquidity at the inflated GLP token price
Operational details
The attacker uses RewardRouterV2.mintAndStakeGlp because GLPManager.inPrivateMode is enabled, preventing direct calls to GLPManager.addLiquidity.
The attacker used a flash loan with USDC in fallback function to create a large WBTC short position.
Summary
The attack succeeded due to fragmented data responsibility across contracts. Critical state information was split between ShortsTracker and Vault, rendering the reentrancy guard ineffective. This architectural vulnerability allowed attackers to manipulate GLP token prices through carefully orchestrated re-entrant calls, enabling the multimillion exploit.