Author: Jiujiu & Kong & Lisa
Editor: Liz
Background
On July 9, 2025, according to the monitoring of the SlowMist MistEye security monitoring system, the well-known decentralized trading platform GMX (@GMX_IO) was attacked and lost assets worth more than 42 million US dollars. The SlowMist security team immediately analyzed the incident and summarized the results as follows:
(https://x.com/SlowMist_Team/status/1942949653231841352)
Related information
Attacker address:
https://arbiscan.io/address/0xdf3340a436c27655ba62f8281565c9925c3a5221
Attack contract address:
https://arbiscan.io/address/0x7d3bd50336f64b7a473 c51f54e7f0bd6771cc355
Address of the vulnerable contract:
https://arbiscan.io/address/0x3963ffc9dff443c2a94f21b129d429891e32ec18
Attack transaction:
https://arbiscan.io/tx/0x03182d3f0956a91c4e4c8f225bbc7975f9434fab042228c7acdc5ec9a32626ef
Root cause
There are two main root causes for this attack:
First, GMX v1 will update the value of the global short average price (globalShortAveragePrices) when creating a short position, but will not update it when closing a short position. Second, GMX v1 will immediately increase the global short position size (globalShortSizes) when processing a short position.
These two factors directly affect the calculation of the total asset size (AUM), which in turn leads to the manipulation of the GLP token price.
The attacker took advantage of this design flaw and successfully created a large short position by reentry through the feature of Keeper that enables `timelock.enableLeverage` when executing orders (a necessary condition for creating large short orders), manipulated the global average price and the global short position size, artificially raised the price of GLP in a single transaction, and profited through redemption operations.
Attack preparation
1. The attacker first uses two pre-transactions to create a long position for the attack contract, and then creates a position reduction order that can be executed by Keeper.
2. After receiving the position reduction order, Keeper calls the executeDecreaseOrder function of the PositionManager contract to perform the position reduction operation for the attack contract. The enableLeverage function of the Timelock contract will be called first to enable _isLeverageEnabled in the Vault contract to be true, which is a necessary condition for directly creating a short position in the subsequent attack process.
After that, the executeDecreaseOrder function of the OrderBook contract will be called to execute the specific logic of reducing the position. After updating the position of the attack contract, the collateral token obtained by reducing the position will be transferred to the attack contract. Because the collateral token is WETH, WETH will be exchanged back to ETH and then transferred to the attack contract, which triggers the fallback function constructed in the attack contract for subsequent reentry operations.
3. The fallback function of the attack contract will first transfer 3001 USDC to the Vault contract, and call the increasePosition function of the Vault contract to open a 30-fold WBTC short order. Then create a corresponding closing order for the position so that Keeper can continue to call it later.
In the increasePosition function, the internal validate function is called first to check whether isLeverageEnabled is true. Only when the Keeper executes the order will isLeverageEnabled be enabled first, so that the check in this step passes. If the function is called directly, the position cannot be opened successfully. This shows that the purpose of the attacker to create a reduction order is to allow the Keeper to re-enter the increasePosition function through the fallback function while executing the reduction order, and directly create a short position.
In addition, since the value of globalShortAveragePrices is updated when opening a position through the increasePosition function, the value of globalShortAveragePrices is not updated when reducing or closing a position through the decreasePosition function. Therefore, the attacker can continuously create short orders with 3,000 USDC and then immediately let the Keeper close the short position, thereby manipulating the value of globalShortAveragePrices.This eventually made the value about 57 times lower than the normal WBTC price (note that all short and liquidation operations on WBTC here are completed by re-entering when Keeper executes the position reduction order). The global average short price (GlobalShortAveragePrice) when shorting WBTC for the first time by re-entering was 108757787000274036210359376021024492, and the global average short price (GlobalShortAveragePrice) when shorting WBTC for the last time before launching the formal attack transaction was 1913705482286167437447414747675542, a difference of about 57 times. Formal attack steps
1. After receiving the reduction order created during the last reentry, Keeper will call the executeDecreaseOrder function of the OrderBook contract to execute the reduction, and the fallback function will be triggered when the ETH obtained from the reduction is transferred to the attack contract.
2. It will first borrow 7.538 million USDC from Uniswap flash loan, and then call the mintAndStakeGlp function of the RewardRouterV2 contract, and use 6 million USDC to mint 4.129 million GLP tokens and pledge them.
The AumInUsdg value obtained during minting is 46942248263037264990037614.
The global short position size (globalShortSizes) and global average short price (getGlobalShortAveragePrice) of WBTC are 153730611140929591070000000000000000 and 1913705482286167437447414747675542 respectively.
3. Immediately afterwards, the attack contract calls the increasePosition function of the Vault contract, transfers USDC to the Vault and creates a large WBTC short position worth 15.385 million.
At the end of the increasePosition function, this part of the newly opened large short position will be used to update the global short position size, so that the global short position size (globalShortSizes) is immediately increased.
4. Then, after the attack contract completed the large short position, it immediately called the unstakeAndRedeemGlp function to unstake and redeem the GLP tokens.
However, it can be seen here that only 386,000 GLP were redeemed, but 9.731 million USDG tokens were burned, and 88 WBTC were finally transferred to the attack contract. Why is this? Let's continue to follow up in the _removeLiquidity function of the GlpManager contract:
When the user performs the operation of redeeming GLP tokens, the function will use the following formula to calculate the number of USDG tokens to be burned: usdgAmount = _glpAmount * aumInUsdg / glpSupply. These USDG will then be transferred to the Vault and sold in exchange for the assets (WBTC) that the user needs to redeem. The calculation method of aum is roughly as follows:
aum = ((totalPoolAmounts - totalReservedAmounts) * price) + totalGuaranteedUsd + GlobalShortLoss (global short position loss) - GlobalShortProfits (global short position profit) - aumDeduction
Since a large short position was created in the previous step, the scale of the global short position was increased, and since the global average price (getGlobalShortAveragePrice) was manipulated to be far lower than the normal price before, this part of the short position is a loss (that is, hasProfit is false), which increases GlobalShortLoss (global short position loss) by hundreds of times, resulting in the manipulation of AUM (aum + delta). In the end, the attacker used the manipulated AUM to redeem assets beyond the normal amount.
6. Finally, the attacker continues to use the manipulated AUM to call the unstakeAndRedeemGlp function to redeem other assets in the Vault to make a profit.
MistTrack analysis
According to the on-chain anti-money laundering and tracking tool MistTrack, the initial attacker address (0xdf3340a436c27655ba62f8281565c9925c3a5221) made a profit of more than 42 million US dollars, including:
The fund transfer situation is roughly summarized as follows:
1. After the initial attacker address made a profit on Arbitrum, it quickly transferred assets such as WETH, WBTC, and DAI to the transfer address (0x99cdeb84064c2bc63de0cea7c6978e272d0f2dae), and used CoW Swap, Across Protocol, Stargate Finance, Mayan Finance and other DEX and cross-chain bridges to exchange assets and transfer them across chains to Ethereum
2. The attacker mainly exchanged USDC to DAI and then to ETH through CoW Swap.
3. A large number of assets were eventually exchanged for ETH, and a total of 11,700 ETH have flowed into the address (0x6acc60b11217a1fd0e68b0ecaee7122d34a784c1).
It is worth noting that the attacker's initial funds came from 2 ETH transferred from Tornado Cash on July 7, and then 2 ETH was transferred across the chain to Arbitrum through Mayan Finance to provide initial Gas for the entire attack process.
As of now, the balances are as follows:
Arbitrum address 0xdf3340a436c27655ba62f8281565c9925c3a5221, balance 10,494,796 Legacy Frax Dollar and 1.07 ETH; Ethereum address 0xa33fcbe3b84fb8393690d1e994b6a6adc256d8a3, balance 3000 ETH; Ethereum address 0xe9ad5a0f2697a3cf75ffa7328bda93dbaef7f7e7, balance 3000 ETH; Ethereum address 0x69c965e164fa60e37a851aa5cd82b13ae39c1d95, balance 3000 ETH; Ethereum Address 0x639cd2fc24ec06be64aaf94eb89392bea98a6605, balance 2700 ETH.
We will continue to monitor the funds.
Summary
The core of this attack is that the attacker took advantage of the two features of the Keeper system: leverage is enabled when executing orders, and the global average price is updated when shorting, but not when closing shorts. Through reentrancy attacks, large short positions are created, and the values of the global average short price and the global short position size are manipulated, thereby directly amplifying the GLP price to redeem profits.
The SlowMist security team recommends that project parties should add reentry lock protection to core functions based on their own business logic, and strictly limit the direct impact of a single factor on the price. In addition, audits and security tests should be strengthened on the project's contract code