背景

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

[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 代表的是总体空头仓位的平均价格,也就是说当现货价格与平均价格相等时,则到达了不亏不赚的成本价。

  1. 如果正常进行开仓操作,更新globalShortAveragePrices 的值,会往现货价格 Price 的值靠拢。(比如现货价格高于平均价格,那么采用现货价格开空时,会抬高平均价格)
  2. 而当进行减仓操作时,如果获利,则上调 globalShortAveragePrices 的值,如果亏损,则下调 globalShortAveragePrices 的值。(比如在现货价格高于平均价格时减仓,首先仓位的亏损金额不会变,剩余仓位需要到达更低的价格才能填补上减仓部分的亏损)

正常情况下, increasePosition 需要 Keeper 调用 PositionManager.executeIncreaseOrder() 作为入口,此时会执行 ShortsTracker.updateGlobalShortData() 更新 ShortsTracker.globalShortAveragePrices 数据。

而攻击者通过重入绕过 TimelockgetIncreaseOrder 直接调用 Vault.increasePosition() ,则不会更新 ShortsTracker.globalShortAveragePrices 的值,维持 globalShortAveragePrices108757 没有向现货价格 109394 靠拢。

而在 TX 5 中,当 Keeper 执行 Position Router.executeDecreasePosition() 的时候会更新 ShortsTracker.globalShortAveragePrices 的值

  1. 开仓时缺失了一次更新,使得所采用的值会比实际值要小。
  2. 加上是亏损的减仓操作,所以 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,并以其他各种代币的形式进行提取。

  1. 以提取 WBTC 的调用为例,攻击者只移除了 386498 GLP,经过计算得出这部分的价值为 9731948 USDG,等价于 88 WBTC。

  1. WETH:移除 341596 GLP,赎回价值 8601309 USDG 的 3205 WETH
  2. USDC:移除 7503 GLP,赎回价值 188930 USDG 的 187343 USDC
  3. LINK:移除 13453 GLP,赎回价值 338759 USDG 的 23800 LINK
  4. UNI:移除 21422 GLP,赎回价值 539419 USDG 的 65479 UNI
  5. USDT:移除 53812 GLP,赎回价值 1354 USDG 的 1343 USDT
  6. FRAX:移除 450568 GLP,赎回价值 11345197 USDG 的 11249897 FRAX
  7. 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

非稳定币的资产总价值计算涉及以下方面:

  1. 空头仓位数量:size

  2. 空头仓位获利/亏损数量:delta

  3. 多头垫付资金:guaranteedUsd

    guaranteedUsd = size - collateral

    多头仓位收益/亏损 = size - guaranteedUsd

  4. 可用流动性: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 也过了一遍了,希望这篇文章也能够给你带来收获。

20250709 - GMX V1 攻击事件: 重入漏洞导致的总体仓位价值操纵的更多相关文章

  1. 使用timer定时器,防止事件重入

    首先简单介绍一下timer,这里所说的timer是指的System.Timers.timer,顾名思义,就是可以在指定的间隔是引发事件.官方介绍在这里,摘抄如下: 1 2 Timer 组件是基于服务器 ...

  2. ACE handle_timeout 事件重入

    当多线程运行反应器事件时, 注意handle_timeout会重入,单独线程不存在下列问题! 1. 一个timer事件 // test_ace_timer.cpp : Defines the entr ...

  3. Delphi主线程重入而导致程序卡死的解决方案

    Delphi的线程可以通过调用AThread.Synchronize(AProc),可以将Proc放入主线程中同步运行,此时AThread将挂起,直到主线程执行完AProc. 如果有BThread,调 ...

  4. C#中Timer使用及解决重入问题

    C#中Timer使用及解决重入问题 ★介绍 首先简单介绍一下timer,这里所说的timer是指的System.Timers.timer,顾名思义,就是可以在指定的间隔是引发事件.官方介绍在这里,摘抄 ...

  5. Use Reentrant Functions for Safer Signal Handling(译:使用可重入函数进行更安全的信号处理)

    Use Reentrant Functions for Safer Signal Handling 使用可重入函数进行更安全的信号处理 How and when to employ reentranc ...

  6. [转]C#中Timer使用及解决重入问题

    本文转自:http://www.cnblogs.com/hdkn235/archive/2014/12/27/4187925.html ★前言 打开久违的Live Writer,又已经好久没写博客了, ...

  7. golang RWMutex RLock重入导致死锁

    现象 一个组件实现了raft分布式协议,在分布式部署环境中来进行选主,在某客户现场突然发生文件句柄泄露,在打印某些错误日志后,几个小时内没有日志打印,然后某个协程突然报无可用的文件句柄. 分析 经过代 ...

  8. linux可重入、异步信号安全和线程安全

    一 可重入函数 当一个被捕获的信号被一个进程处理时,进程执行的普通的指令序列会被一个信号处理器暂时地中断.它首先执行该信号处理程序中的指令.如果从信号处理程序返回(例如没有调用exit或longjmp ...

  9. QThread 与 QObject的关系(QObject可以用于多线程,可以发送信号调用存在于其他线程的slot函数,但GUI类不可重入)

    QThread 继承 QObject..它可以发送started和finished信号,也提供了一些slot函数. QObject.可以用于多线程,可以发送信号调用存在于其他线程的slot函数,也可以 ...

  10. UNIX高级环境编程(13)信号 - 概念、signal函数、可重入函数

    信号就是软中断. 信号提供了异步处理事件的一种方式.例如,用户在终端按下结束进程键,使一个进程提前终止.   1 信号的概念 每一个信号都有一个名字,它们的名字都以SIG打头.例如,每当进程调用了ab ...

随机推荐

  1. win7激活,2023年亲测可用 ,win7激活密钥,激活码

    还在找激活密钥,激活win7吗,试了无数个都激活不了? 直接用这个工具激活吧,亲测可用,用过的都知道. WIN7Chew-WGA0.9.exe 阿里云盘:https://www.aliyundrive ...

  2. 2025dsfz集训Day10:区间、树形DP

    Day10:区间.树形DP 区间DP 区间类型动态规划是线性动态规划的拓展,它在分阶段划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系.(例:\(f[i][j]=f[i][ ...

  3. ragflow k8s部署详细过程

    一.概述 ragflow官方提供的安装方式是docker-compose方式部署的,单机运行. k8s部署方式,暂未提供. 不过我们可以通过工具,结合docker-compose.yaml,来推演出对 ...

  4. WebAssembly:开启新时代的跨平台

    @charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...

  5. C语言:高级语言怎样抽象执行逻辑

    平时我们做编程的时候,底层 CPU 如何执行指令已经被封装好了,因此你很少会想到把底层和语言编译联系在一起.但从我自己学习各种编程语言的经历看,从这样一个全新视角重新剖析 C 语言,有助于加深你对它的 ...

  6. B1086 就不告诉你

    描述 做作业的时候,邻座的小盆友问你:"五乘以七等于多少?"你应该不失礼貌地围笑着告诉他:"五十三."本题就要求你,对任何一对给定的正整数,倒着输出它们的乘积. ...

  7. git 初始化项目、创建本地分支、本地分支与远程分支关联

    在远程没有项目的场景下,可以通过如下步骤创建和关联远程分支: 在Git官网上点击New repository新建项目: 在本地新建一个同名文件(以demo为例),并初始化项目: 在demo目录打开gi ...

  8. Java 多个线程之间共享数据

    线程执行的代码相同    如果每个线程执行的代码相同,可以使用同一个Runnable对象,在这个Runnable对象中定义共享数据即可,例如,卖票系统就可以这么做. public class Sell ...

  9. FastAPI权限迷宫:RBAC与多层级依赖的魔法通关秘籍

    title: FastAPI权限迷宫:RBAC与多层级依赖的魔法通关秘籍 date: 2025/06/04 21:17:50 updated: 2025/06/04 21:17:50 author: ...

  10. Springboot笔记<6>Rest的使用和请求参数注解@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody

    Rest的使用和原理 Rest风格支持(使用HTTP请求方式动词来表示对资源的操作) • 以前:/getUser 获取用户 /deleteUser 删除用户 /editUser 修改用户 /saveU ...