背景
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 自动化执行清算和限价单,确保效率和安全性。
- Attack contract:https://arbiscan.io/address/0x7d3bd50336f64b7a473c51f54e7f0bd6771cc355
整个攻击事件涉及 14 笔交易,其中 1-13 笔是准备交易,第 14 笔是攻击交易。
Prepare transaction [TX 1-13]
要把这些准备交易全部找出来排好序真的不容易啊,每笔交易的发起者是不同的,所调用的合约也不同的。所以只能够通过各种 Key 和 Index 来排查每笔交易之间的顺序关系,确保没有遗漏掉相关的交易。
- positionKey 对应的是 position
- requestKey 对应的是 request
- increaseOrdersIndex 对应的是 order,从 0 开始
- decreasePositionsIndex 对应的是 request,从 1 开始
TX 1
[355878385]https://app.blocksec.com/explorer/tx/arbitrum/0x0b8cd648fb585bc3d421fc02150013eab79e211ef8d1c68100f2820ce90a4712
- Order Book.createIncreaseOrder(): 攻击者创建了一个 WETH increase order ,这个仓位是后续多次进行重入的关键。[increaseOrdersIndex = 0]
TX 2
[355878605]https://app.blocksec.com/explorer/tx/arbitrum/0x28a000501ef8e3364b0e7f573256b04b87d9a8e8173410c869004b987bf0beef
- Order Book.executeIncreaseOrder(): Keeper 执行 TX 1 中的 order,创建 WETH long position [positionKey = 0x05d2]
TX 3
[355878984]https://app.blocksec.com/explorer/tx/arbitrum/0x20abfeff0206030986b05422080dc9e81dbb53a662fbc82461a47418decc49af
- Order Book.createDecreaseOrder(): Hacker 创建了一个 WETH decrease order,这是利用重入漏洞的关键操作。[positionKey = 0x05d2, decreaseOrdersIndex = 0]
TX 4
[355879148]https://app.blocksec.com/explorer/tx/arbitrum/0x1f00da742318ad1807b6ea8283bfe22b4a8ab0bc98fe428fbfe443746a4a7353
- 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
[355879171]https://app.blocksec.com/explorer/tx/arbitrum/0x222cdae82a8d28e53a2bddfb34ae5d1d823c94c53f8a7abc179d47a2c994464e
- 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
[355879337]https://app.blocksec.com/explorer/tx/arbitrum/0xc9a4692a4a297202a099144a59dc30497d47d20a0eef3a0f6dc2f017221293c2
- 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
[355879359]https://app.blocksec.com/explorer/tx/arbitrum/0x1cbf250b6b22a62e766e8cb7aa6c0b16d1d46777d3f5be53d5d80cd2d853943a
- Vault.decreasePosition(): Keeper 关闭 WBTC short position,赎回 2622 USDC
- gmxPositionCallback(): 在 Callback 函数中调用 Order Book.createDecreaseOrder() 创建 WETH decrease order [positionKey = 0x05d2, decreaseOrdersIndex = 2]
TX 8
[355879563]https://app.blocksec.com/explorer/tx/arbitrum/0xb58415cf40b03f7f3e3603646af0c0b6be6e22640459060a70b7ef803b4cfb0b
- 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
[355879585]https://app.blocksec.com/explorer/tx/arbitrum/0x5a37ff59323e70ba25560985ffaf20069f2c0ec53829e8aa639fef72cb59c3b7
- Vault.decreasePosition(): Keeper 关闭 WBTC short position,赎回 2481 USDC
- gmxPositionCallback(): 在 Callback 函数中调用 Order Book.createDecreaseOrder() 创建 WETH decrease order [positionKey = 0x255b, decreaseOrdersIndex = 3]
TX 10
[355879763]https://app.blocksec.com/explorer/tx/arbitrum/0xff6fe60a740fd5cab2ad5364949a7983f83eb82806b583834c9d4e90377bf108
- 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
[355879785]https://app.blocksec.com/explorer/tx/arbitrum/0xbd65d666e7f096255661747ead63128e7193efa5ed3cff255a1214e7e0187be6
- Vault.decreasePosition(): Keeper 关闭 WBTC short position,赎回 2345 USDC
- gmxPositionCallback(): 在 Callback 函数中调用 Order Book.createDecreaseOrder() 创建 WETH decrease order [positionKey = 0x255b, decreaseOrdersIndex = 4]
TX 12
[355879999]https://app.blocksec.com/explorer/tx/arbitrum/0x1052738769e80df1664049f37d715bc6200b01e38ba1123b841ce6c819fcdec6
- 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 USDC
- gmxPositionCallback(): 在 Callback 函数中调用 Order Book.createDecreaseOrder() 创建 WETH decrease order [positionKey = 0x255b, decreaseOrdersIndex = 5]
[355880022]https://app.blocksec.com/explorer/tx/arbitrum/0x0cdbacae0584e068dd9ba8f93c55df02630ee3481eeca8f2477cda7b84339fcc- 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 (攻击交易)
[355880237]https://app.blocksec.com/explorer/tx/arbitrum/0x03182d3f0956a91c4e4c8f225bbc7975f9434fab042228c7acdc5ec9a32626ef
重点分析重入后在 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 也过了一遍了,希望这篇文章也能够给你带来收获。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |