首页 公告中心 价格操纵的黑魔法:Balancer V2 不变量计算漏洞剖析

价格操纵的黑魔法:Balancer V2 不变量计算漏洞剖析

发布于:2026-02-10 11:31
火币 HTX 官方发布

编译:白话区块链

2025 年 11 月 3 日,Balancer V2 的组合型稳定池(Composable Stable Pools)以及多个链上的几个分叉项目遭受了一次协同攻击,导致总损失超过 1.25 亿美元。BlockSec 在第一时间发出了警报,并随后发布了初步分析。

这是一次高度复杂的攻击。我们的调查显示,根本原因是不变量(invariant)计算中的精度损失导致的价格操纵,进而扭曲了 BPT(Balancer 池Token)的价格计算。这种不变量操纵允许攻击者通过单次批量交换(batch swap)从一个特定的稳定池中获利。尽管一些研究人员提供了富有洞察力的分析,但某些解读存在误导性,其根本原因和攻击过程尚未完全阐明。本博客旨在对该事件进行全面而准确的技术分析。

 

关键要点 (TL;DR)

 

在以下部分中,我们将首先提供有关 Balancer V2 的关键背景信息,然后深入分析已发现的问题和相关攻击。

 

0x1 背景

 

 

1. Balancer V2 的组合型稳定池

 

本次攻击中受影响的组件是 Balancer V2 协议的组合型稳定池。这些池专为预期保持接近 1:1 锚定(或以已知汇率交易)的资产而设计,并允许以最小的价格影响进行大额交换,从而显著提高同类或相关资产之间的资本效率。每个池都有自己的 Balancer 池Token (BPT),代表流动性提供者在池中的份额,以及相应的底层资产。

该池采用了 Stable Math(基于 Curve 的 StableSwap 模型),其中不变量 D 代表了池的虚拟总价值。

BPT 价格可以近似为:

图像

从上述公式可以看出,如果 D 在账面上可以被改小(即使没有任何实际资金损失),BPT 价格就会显得更便宜。

 

2. batchSwap() 和 onSwap()

 

Balancer V2 提供了 batchSwap() 函数,该函数支持在 Vault内进行多跳交换(multi-hop swaps)。根据传递给此函数的参数,有两种交换类型:

通常,batchSwap() 由多个通过 onSwap() 函数执行的Token间交换组成。以下概述了当一个 SwapRequest被分配为 GIVEN_OUT 交换类型时的执行路径(请注意 ComposableStablePool 继承自 BaseGeneralPool):

图像

以下显示了 GIVEN_OUT 交换类型中 amount_in 的计算,这涉及不变量 D。

// inGivenOut token x for y - polynomial equation to solve
// ax = amount in to calculate                                     
// bx = balance token in                                                                 
// x = bx + ax (finalBalanceIn)                                                                
// D = invariant
// A = amplification coefficient
// n = number of tokens
// S = sum of final balances but x                                                             
// P = product of final balances but x                                                         

                   D                     D^(n+1)  
  x^2 + ( S - ----------  - D) * x -  ------------- = 0         
               (A * n^n)               A * n^2n * P

 

3. 缩放与舍入

 

为了在不同Token余额之间规范化计算,Balancer 执行以下两个操作:

显然,放大和缩小在理论上是成对的操作——分别是乘法和除法。然而,这两种操作的实现中存在不一致。具体来说,缩小操作有两个变体或方向:divUpdivDown。相比之下,放大操作只有一个方向,即 mulDown

这种不一致的原因尚不清楚。根据 _upscale() 函数中的注释,开发人员认为单向舍入的影响是微乎其微的。

// 放大(Upscale)舍入不一定会始终朝同一方向:例如,在一次交换中,

// 输入Token的余额应向上舍入,而输出Token的余额应向下舍入。这是我们对所有金额都进行同向舍入的唯一地方,

// 因为这种舍入的影响预计是最小的

// (并且除非 _scalingFactor() 被覆盖,否则没有舍入误差)。

 

0x2 漏洞分析

 

根本问题源于在 BaseGeneralPool._swapGivenOut() 函数中执行放大(upscaling)操作时所执行的向下舍入操作。特别是,_swapGivenOut() 通过 _upscale() 函数错误地将 swapRequest.amount 向下舍入。这个舍入后的值随后被用作 amountOut,在通过 _onSwapGivenOut() 计算 amountIn 时使用。这种行为与“舍入应以有利于协议的方式应用”的标准实践相矛盾。

图像

因此,对于给定的池(wstETH/rETH/cbETH),计算出的 amountIn 低估了实际所需的输入。这允许用户以较少数量的一种底层资产(例如 wstETH)交换另一种资产(例如 cbETH),从而由于有效流动性减少而导致不变量 D 减少。因此,相应的 BPT(wstETH/rETH/cbETH)的价格变得被人为压低(deflated),因为 $\text{BPT price} = \frac{D}{\text{totalSupply}}$。

 

0x3 攻击分析

 

攻击者执行了

两阶段攻击,可能是为了最小化被检测的风险:

第一阶段可以进一步分为两个阶段:参数计算和批量交换。下面,我们使用 Arbitrum 上的一个攻击交易(TX)示例(https://app.blocksec.com/explorer/tx/arbitrum/0x7da32ebc615d0f29a24cacf9d18254bea3a2c730084c690ee40238b1d8b55773)来说明这些阶段。

 

参数计算阶段

 

在此阶段,攻击者将链下计算与链上模拟相结合,根据组合型稳定池的当前状态(包括缩放因子、放大系数、BPT 汇率、交换费用和其他参数),精确调整下一阶段(批量交换)中每一跳的参数。有趣的是,攻击者还部署了一个辅助合约来协助这些计算,这可能是为了减少被“抢跑”(front-running)的风险。

首先,攻击者收集目标池的基本信息,包括每个Token的缩放因子、放大参数、BPT 汇率和交换费百分比。然后他们计算一个关键值 trickAmt,这是用于引发精度损失的被操纵的目标Token数量。

将目标Token的缩放因子(scaling factor)记为 sF,计算如下:

图像

为了确定下一阶段(批量交换)步骤 2 中使用的参数,攻击者使用以下 calldata 对辅助合约的 0x524c9e20 函数进行后续模拟调用:

uint256[] balances; // 池Token的余额(不包括 BPT)
uint256[] scalingFactors; // 每个池Token的缩放因子
uint tokenIn; // 此跳模拟的输入Token索引
uint tokenOut; // 此跳模拟的输出Token索引
uint256 amountOut; // 期望的输出Token数量
uint256 amp; // 池的放大参数
uint256 fee; // 池交换费百分比

返回数据为:

uint256[] balances; // 交换后池Token的余额(不包括 BPT)

具体来说,初始余额和迭代循环次数是链下计算的,并作为参数传递给攻击者的合约(报告分别为 100,000,000,000 和 25)。每次迭代执行三次交换:

  1. 交换 1:将目标Token的数量推至 trickAmt + 1,假设交换方向为 0 → 1。

  2. 交换 2:继续用 trickAmt 换出目标Token,这会触发 _upscale() 调用中的向下舍入。

  3. 交换 3:执行一次反向交换操作(1 → 0),要交换的数量是通过截断池中当前Token余额的两个最高有效十进制数字得出的,即向下舍入到最接近的 $10^{d-2}$ 的倍数,其中 $d$ 是十进制数字的位数。例如,324,816 -> 320,000。

请注意,由于 StableMath 计算中使用了牛顿-拉弗森(Newton–Raphson)方法,此步骤可能偶尔会失败。为了缓解这种情况,攻击者实现了两次重试尝试,每次使用原始值的 9/10 作为备用值。

攻击者的辅助合约派生自 Balancer V2 的 StableMath 库,这一点可以通过其包含“BAL”风格的自定义错误消息来证明。

 

批量交换阶段

 

然后,batchSwap() 操作可以分解为三个步骤:

  1. 步骤 1:攻击者将 BPT (wstETH/rETH/cbETH) 换成底层资产,以精确地将一个Token(cbETH)的余额调整到舍入边界的边缘(amount = 9)。这为下一步的精度损失创造了条件。

  2. 步骤 2:然后,攻击者使用一个精心设计的数量(= 8)在另一种底层资产(wstETH)和 cbETH 之间进行交换。由于在缩放Token数量时向下舍入,计算出的 Δx 变得略小(从 8.918 到 8),导致 Δy 被低估,从而使不变量(D,来自 Curve 的 StableSwap 模型)变小。由于 $\text{BPT price} = \frac{D}{\text{totalSupply}}$,BPT 价格被人为压低。

    图像

  3. 步骤 3:攻击者反向将底层资产换回 BPT,恢复平衡,同时从被压低的 BPT 价格中获利。

 

0x4: 攻击与损失

 

我们在下表中总结了这些攻击及其相应的损失,总损失超过 1.25 亿美元。

图像

 

0x5 结论

 

该事件涉及一系列针对 Balancer V2 协议及其分叉项目的攻击交易,导致了重大的财务损失。在最初的攻击之后,在多个链上观察到了大量后续和模仿交易。这次事件凸显了 DeFi 协议设计和安全的几个关键教训:

在保持运营和业务连续性的同时,行业参与者可以利用 BlockSec Phalcon 作为最后一道防线来保护他们的资产。BlockSec 专家团队随时准备为您的项目进行全面的安全评估。

返回公告列表