20250709 - GMX V1 攻击事件中,重入漏洞如何导致总体仓位价值被操纵?

摘要:背景 2025 年 7 月 9 日,GMX V1 遭受黑客攻击,损失约 4200 万美元资产。攻击者利用 executeDecreaseOrder 函数发送 ETH 的行为进行重入,绕过 enableLeverage 检查和 globalS
背景 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 也过了一遍了,希望这篇文章也能够给你带来收获。