20250709 - GMX V1 攻擊事件: 重入漏洞導致的總體倉位價值操縱
背景
2025 年 7 月 9 日,GMX V1 遭受黑客攻擊,損失約 4200 萬美元資產。攻擊者利用 executeDecreaseOrder 函數發送 ETH 的行為進行重入,繞過 enableLeverage 檢查和 globalShortAveragePrices 的更新進行開倉,從而操縱全局空頭平均價格(globalShortAveragePrices),抬高 GLP 代幣的價值。最后將 GLP 以池內資產(BTC、ETH、USDC 等)的形式贖回完成獲利。
GMX V1 是一個去中心化永續合約交易平臺,允許用戶以最高 30 倍杠桿交易加密資產(如 ETH、BTC)通過 GLP 池作為合約用戶對手方。流動性提供者(LP)通過存入資產(如 USDC、ETH)獲得 GLP 代幣。合約用戶可開多頭或空頭頭寸,盈虧以 USD 計價。平臺通過 Chainlink 預言機獲取價格,Keeper 自動化執行清算和限價單,確保效率和安全性。
整個攻擊事件涉及 14 筆交易,其中 1-13 筆是準備交易,第 14 筆是攻擊交易。
Prepare transaction [TX 1-13]
要把這些準備交易全部找出來排好序真的不容易啊,每筆交易的發起者是不同的,所調用的合約也不同的。所以只能夠通過各種 Key 和 Index 來排查每筆交易之間的順序關系,確保沒有遺漏掉相關的交易。
- positionKey 對應的是 position
- requestKey 對應的是 request
- increaseOrdersIndex 對應的是 order,從 0 開始
- decreasePositionsIndex 對應的是 request,從 1 開始
TX 1
Order Book.createIncreaseOrder(): 攻擊者創建了一個 WETH increase order ,這個倉位是后續多次進行重入的關鍵。[increaseOrdersIndex = 0]
TX 2
Order Book.executeIncreaseOrder(): Keeper 執行 TX 1 中的 order,創建 WETH long position [positionKey = 0x05d2]
TX 3
Order Book.createDecreaseOrder(): Hacker 創建了一個 WETH decrease order,這是利用重入漏洞的關鍵操作。[positionKey = 0x05d2, decreaseOrdersIndex = 0]
TX 4
Order Book.executeDecreaseOrder(): Keeper 執行 WETH decrease order,觸發重入漏洞。 [positionKey = 0x05d2, decreaseOrdersIndex = 0]- (In reentrancy)
Vault.increasePosition(): 繞過 enableLeverage 檢查和 globalShortAveragePrices 的更新,直接創建 WBTC short position(抵押品為 3001 USDC) [positionKey = 0x255b] - (In reentrancy)
Position Router.createDecreasePosition(): 創建 WBTC short position 的平倉 request [requestKey = 0xc239, decreasePositionsIndex = 1]


此時一些相關參數的值
price = 109469868000000000000000000000000000
In ShortsTracker:
[before]ShortsTracker.globalShortAveragePrices = 108757787000274036210359376021024492
繞過 globalShortAveragePrices 的更新會出現什么情況呢?
globalShortAveragePrices 代表的是總體空頭倉位的平均價格,也就是說當現貨價格與平均價格相等時,則到達了不虧不賺的成本價。
- 如果正常進行開倉操作,更新
globalShortAveragePrices的值,會往現貨價格 Price 的值靠攏。(比如現貨價格高于平均價格,那么采用現貨價格開空時,會抬高平均價格) - 而當進行減倉操作時,如果獲利,則上調
globalShortAveragePrices的值,如果虧損,則下調globalShortAveragePrices的值。(比如在現貨價格高于平均價格時減倉,首先倉位的虧損金額不會變,剩余倉位需要到達更低的價格才能填補上減倉部分的虧損)
正常情況下, increasePosition 需要 Keeper 調用 PositionManager.executeIncreaseOrder() 作為入口,此時會執行 ShortsTracker.updateGlobalShortData() 更新 ShortsTracker.globalShortAveragePrices 數據。

而攻擊者通過重入繞過 Timelock 和 getIncreaseOrder 直接調用 Vault.increasePosition() ,則不會更新 ShortsTracker.globalShortAveragePrices 的值,維持 globalShortAveragePrices 在 108757 沒有向現貨價格 109394 靠攏。
而在 TX 5 中,當 Keeper 執行 Position Router.executeDecreasePosition() 的時候會更新 ShortsTracker.globalShortAveragePrices 的值
- 開倉時缺失了一次更新,使得所采用的值會比實際值要小。
- 加上是虧損的減倉操作,所以
globalShortAveragePrices的值會進一步減小。
TX 5
Position Router.executeDecreasePosition(): Keeper 關閉 WBTC short position,贖回 2791 USDC [positionKey = 0x255b, requestKey = 0xc239] :gmxPositionCallback: 在 Callback 函數中調用Order Book.createDecreaseOrder()創建 WETH decrease order [positionKey = 0x05d2, decreaseOrdersIndex = 1]

此時一些相關參數的值,globalShortAveragePrices 已經被更新成了更小的值。
price = 109505774000000000000000000000000000
In ShortsTracker:
[beforeUpdate]ShortsTracker.globalShortAveragePrices = 108757787000274036210359376021024492
Position Router.executeDecreasePosition()
[afterUpdate] ShortsTracker.globalShortAveragePrices = 104766755156748843189540879601516878
隨后的 TX 6-7,8-9,10-11,12-13 都是在重復 TX 4-5 的操作,其目的就是通過反復多次的操作盡可能地縮小
globalShortAveragePrices的值
TX 6
Order Book.executeDecreaseOrder(): Keeper 執行 WETH decrease order,觸發重入漏洞。 [positionKey = 0x05d2, decreaseOrdersIndex = 1 ]- (in reentrancy)
Vault.increasePosition(): 繞過 enableLeverage 檢查和 globalShortAveragePrices 的更新,直接創建 WBTC short position(抵押品為 2791 USDC)[positionKey = 0x255b] - (in reentrancy)
Position Router.createDecreasePosition(): 創建 WBTC short position 的平倉 request [requestKey = 0x1489, decreasePositionsIndex = 2]
price = 109527370000000000000000000000000000
In ShortsTracker:
[before]ShortsTracker.globalShortAveragePrices = 104934381964999641338644145008879305
TX 7
Vault.decreasePosition(): Keeper 關閉 WBTC short position,贖回 2622 USDCgmxPositionCallback(): 在 Callback 函數中調用Order Book.createDecreaseOrder()創建 WETH decrease order [positionKey = 0x05d2, decreaseOrdersIndex = 2]
TX 8
Order Book.executeDecreaseOrder(): Keeper 執行 WETH decrease order,觸發重入漏洞 [positionKey = 0x05d2, decreaseOrdersIndex = 2]- (in reentrancy)
Vault.increasePosition(): 繞過 enableLeverage 檢查和 globalShortAveragePrices 的更新,直接創建 WBTC short position (抵押品為 2622 USDC) [positionKey = 0x255b] - (in reentrancy)
Position Router.createDecreasePosition(): 創建 WBTC short position 的平倉 request [requestKey = 0xe63c, decreasePositionsIndex = 3]
TX 9
Vault.decreasePosition(): Keeper 關閉 WBTC short position,贖回 2481 USDCgmxPositionCallback(): 在 Callback 函數中調用Order Book.createDecreaseOrder()創建 WETH decrease order [positionKey = 0x255b, decreaseOrdersIndex = 3]
TX 10
Order Book.executeDecreaseOrder(): Keeper 執行 WETH decrease order,觸發重入漏洞 [positionKey = 0x05d2, decreaseOrdersIndex = 3]- (in reentrancy)
Vault.increasePosition(): 繞過 enableLeverage 檢查和 globalShortAveragePrices 的更新,直接創建 WBTC short position (抵押品為 2481 USDC) [positionKey = 0x255b] - (in reentrancy)
Position Router.createDecreasePosition(): 創建 WBTC short position 的平倉 request [requestKey = 0xcc53, decreasePositionsIndex = 4]
TX 11
Vault.decreasePosition(): Keeper 關閉 WBTC short position,贖回 2345 USDCgmxPositionCallback(): 在 Callback 函數中調用Order Book.createDecreaseOrder()創建 WETH decrease order [positionKey = 0x255b, decreaseOrdersIndex = 4]
TX 12
Order Book.executeDecreaseOrder(): Keeper 執行 WETH decrease order,觸發重入漏洞 [positionKey = 0x05d2, decreaseOrdersIndex = 4]- (in reentrancy)
Vault.increasePosition(): 繞過 enableLeverage 檢查和 globalShortAveragePrices 的更新,直接創建 WBTC short position (抵押品為 2345 USDC)[positionKey = 0x255b] - (in reentrancy)
Position Router.createDecreasePosition(): 創建 WBTC short position 的平倉 request [requestKey = 0xf42a, decreasePositionsIndex = 5]
price = 109466220000000000000000000000000000
In ShortsTracker:
[before]ShortsTracker.globalShortAveragePrices = 9881613652623553707300056873939342
TX 13
Vault.decreasePosition(): Keeper 關閉 WBTC short position,贖回 2182 USDCgmxPositionCallback(): 在 Callback 函數中調用Order Book.createDecreaseOrder()創建 WETH decrease order [positionKey = 0x255b, decreaseOrdersIndex = 5]
price = 109505774000000000000000000000000000
In ShortsTracker:
[beforeUpdate]ShortsTracker.globalShortAveragePrices = 9881613652623553707300056873939342
Position Router.executeDecreasePosition()
[afterUpdate] ShortsTracker.globalShortAveragePrices = 1913705482286167437447414747675542
ShortsTracker.globalShortAveragePrices 的值變為原來的 1.76%
108757787000274036210359376021024492 -> 1913705482286167437447414747675542
Exploit transaction [TX 14]
TX 1-13 的目的,都是通過利用重入漏洞,繞過 ShortsTracker.globalShortAveragePrices 的更新進行開倉,從而達到降低 ShortsTracker.globalShortAveragePrices 值的目的。
TX 14 (攻擊交易)
重點分析重入后在 uniswapV3FlashCallback 中進行的操作
mintAndStakeGlp()
調用 mintAndStakeGlp() 鑄造并質押價值 6000000 USDC 的 GLP。通過 trace 可以看出扣除費用后價值 5997000 USDG。質押了 4129578 GLP

Vault.increasePosition()
調用 Vault.increasePosition() ,傳入 1538567 USDC 創建 WBTC short position

Reward Router V2.unstakeAndRedeemGlp() [Take profit]
取消質押 GLP,并以其他各種代幣的形式進行提取。

- 以提取 WBTC 的調用為例,攻擊者只移除了 386498 GLP,經過計算得出這部分的價值為 9731948 USDG,等價于 88 WBTC。

- WETH:移除 341596 GLP,贖回價值 8601309 USDG 的 3205 WETH
- USDC:移除 7503 GLP,贖回價值 188930 USDG 的 187343 USDC
- LINK:移除 13453 GLP,贖回價值 338759 USDG 的 23800 LINK
- UNI:移除 21422 GLP,贖回價值 539419 USDG 的 65479 UNI
- USDT:移除 53812 GLP,贖回價值 1354 USDG 的 1343 USDT
- FRAX:移除 450568 GLP,贖回價值 11345197 USDG 的 11249897 FRAX
- DAI:移除 53603 GLP,贖回價值 1349722 USDG 的 1338385 DAI
攻擊者在這個環節中共贖回了 1328455 GLP,剩余 2801123 GLP
超額的贖回價值是如何計算出來的呢?
在計算贖回 GLP 獲得的 WBTC 數量時,首先通過 _removeLiquidity() 計算等價的 USDG。其中 usdgAmount 的值需要根據 aumInUsdg 來計算,而 aumInUsdg 正是被攻擊者所操控的值。

AUM 的含義及計算方法
Assets Under Management (AUM)
AUM 代表 GMX 協議管理的所有資產的總價值
用途: GLP價格 = AUM / GLP總供應量
getAum() 函數計算 GMX 協議管理的所有資產的總價值,分為穩定幣和非穩定幣兩種計算方式。
https://github.com/gmx-io/gmx-contracts/blob/master/contracts/core/GlpManager.sol#L136
穩定幣的資產總價值計算方式較為簡單,代幣數量 * 代幣價格:poolAmount * price
非穩定幣的資產總價值計算涉及以下方面:
-
空頭倉位數量:size
-
空頭倉位獲利/虧損數量:delta
-
多頭墊付資金:guaranteedUsd
guaranteedUsd = size - collateral
多頭倉位收益/虧損 = size - guaranteedUsd
-
可用流動性:poolAmount - reservedAmount
計算公式:WBTC_AUM = guaranteedUsd + (poolAmount - reservedAmount) × price ± delta

其中 delta 通過 getGlobalShortDelta() 函數進行計算,其中 averagePrice 的值被攻擊者通過 TX 1-13 的操控后,變得遠小于實際值。使得最終計算得到的 delta要遠大于實際值。


globalShortAveragePrices = 1913705482286167437447414747675542(正常值的 1.76%)

delta:865836626141799337421744137507209211350
hasProfit:False
由于 hasProfit 為 false,代表空頭虧損,所以 WBTC_AUM 的計算公式需要加上被操控的 delta。
WBTC_AUM = guaranteedUsd + (poolAmount - reservedAmount) × price + delta
這也就導致了 aumInUsdg 的值比正常情況下大,計算得到的 usdgAmount 值也變大,所以攻擊者能夠贖回獲得超額的收益。
Vault.decreasePosition()
調用 Vault.decreasePosition() 關閉 WBTC short position,取回 1507796 USDC

Repeat to get more USDC
接下來黑客進行了 3 次操作去擴大收益,前面 2 次為了積累 GLP 代幣,為了在第 3 次贖回超額的 USDC。

第 1 次操作質押 FRAX 獲得了 16083241 GLP,贖回使用了 625160 GLP,剩余了 15458081 GLP。但同時又虧損了 149057 FRAX 和 2500 USDC。

(第 2 次操作與第 1 次類似)
第 3 次操作 tokenOut 選擇的是 USDC,贖回得到 15834169 USDC

Repay flashloan
歸還閃電貸

后記
這次的 GMX 攻擊事件分析可以說是我分析過的較為復雜的攻擊了(真的是看得身心疲憊啊),尤其是 GMX 里面涉及到了很多關于永續合約倉位和收益的計算。里面每個參數的含義,計算公式的含義還是比較難理解的。還有不得不說前面的 13 筆準備交易的收集也花費了大量的時間和精力,不過對 GMX 的了解也在理清楚準備交易的過程中慢慢加深了。托這次攻擊事件的福,我也是把一直沒看的 GMX 也過了一遍了,希望這篇文章也能夠給你帶來收獲。

浙公網安備 33010602011771號