Balancer V2 攻击事件深度分析

CN
4小时前

撰文:BlockSec

2025 年 11 月 3 日,Balancer V2 的 Composable Stable Pools 以及多个跨链的分叉项目遭受攻击,造成了超过 1.25 亿美元的总损失。BlockSec 在第一时间发出了预警 [1],并随后发布了初步分析报告 [2]。这是一场高度复杂的攻击。我们的调查显示,根本原因在于不变量(invariant)计算中的精度损失所引发的价格操纵,进而扭曲了 BPT(Balancer Pool Token)的价格计算。这种不变量操纵使攻击者能够通过一次批量交换(batch swap)从特定的稳定池中获利。

尽管部分研究者提供了有价值的分析,但也存在一些误导性的解读,而且目前对根本原因和攻击过程的细节仍未得到完全阐明。本报告旨在提供对此事件的全面且准确的技术分析。

关键要点 (TL;DR)

  • 根本原因:舍入不一致与精度损失

  • Upscaling(放大)操作采用单向舍入(向下取整),而downscaling(缩小)操作采用双向舍入(向上与向下取整)。

  • 这种不一致会造成精度损失,当攻击者通过精心设计的交换路径加以利用时,就会违背「舍入应始终有利于协议」的标准原则。

  • 攻击执行过程

  • 攻击者精心构造参数(包括迭代次数与输入值),以最大化精度损失的影响。

  • 攻击者采用两阶段攻击策略以规避检测:第一阶段在单笔交易中执行核心攻击但不立即获利,第二阶段通过独立交易提取资产获利。

  • 协议无法暂停,导致攻击影响加剧

  • 由于某些限制,协议无法暂停运行 [3]。这种无法中止操作的状况加剧了攻击的影响,并促成了多次后续及模仿攻击。

在接下来的章节中,我们将首先介绍 Balancer V2 的关键背景信息,然后对已识别的问题及相关攻击进行深入的技术分析。

0x1 背景知识

1.1 Balancer V2's Composable Stable Pool

此次攻击中受影响的组件是 Balancer V2 协议的 Composable Stable Pool [4]。这些池子专为预期保持接近 1:1 价格平价(或以已知汇率交易)的资产设计,允许在价格影响极小的情况下进行大额交换,从而显著提升同类或高度相关资产之间的资本效率。每个池都有自己对应的 Balancer Pool Token(BPT),用于表示流动性提供者在池中的份额,并与池内的相应底层资产绑定。

  • 该池采用 Stable Math(基于 Curve 的 StableSwap 模型),其中的不变量(invariant) D 表示池子的虚拟总价值(virtual total value)。

  • BPT 的价格可近似表示为

上述公式表明,如果在计算上使 D 的值变小(即使实际上并未造成资金损失),BPT 的价格也会更便宜。

1.2 batchSwap() and onSwap()

Balancer V2 提供了batchSwap()函数,使用户能够在 Vault [5] 内执行多跳(multi-hop)交换。

该函数通过传入的参数来确定两种交换类型:

  • GIVEN_IN(「给定输入」):调用者指定精确的输入代币数量,由池子计算出对应的输出代币数量。

  • GIVEN_OUT(「给定输出」):调用者指定期望获得的输出代币数量,由池子计算出所需的输入代币数量。

通常,一次 batchSwap() 操作由多个代币之间的交换组成,这些交换通过 onSwap() 函数执行。以下描述了当 SwapRequest 被指定为 GIVEN_OUT 类型时的执行路径(注意:ComposableStablePool 继承自 BaseGeneralPool):

以下展示了 GIVEN_OUT 类型交换中 amount_in 的计算过程,该计算涉及不变量 D。

1.3 Scaling and Rounding

为了统一不同代币余额之间的计算精度,Balancer 执行了以下两步操作:

  • Upscaling(放大): 在进行计算前,将代币余额和数量按统一的内部精度进行放大。

  • Downscaling(缩小): 在计算完成后,将结果转换回其原始精度,并应用有方向的舍入(例如,为防止池子少收费用,输入金额通常向上取整,而输出金额则通常向下取整)。

显然,upscaling和downscaling在理论上是一对相互对应的操作——分别对应乘法(mul)与除法(div)。然而,在具体实现中,这两种操作存在不一致之处。具体而言,downscaling 有两种方向或变体:divUp 和 divDown;而 upscaling 仅有一个方向,即 mulDown。

造成这种不一致的原因尚不明确。根据 _upscale() 函数中的注释,开发者认为单方向舍入带来的影响可以忽略不计。

// Upscale rounding wouldn't necessarily always go in the same direction: in a swap for example the balance of

// token in should be rounded up, and that of token out rounded down. This is the only place where we round in

// the same direction for all amounts, as the impact of this rounding is expected to be minimal (and there's no

// rounding error unless `_scalingFactor()` is overriden).

0x2 漏洞分析

根本问题源于 BaseGeneralPool._swapGivenOut() 函数中在upscaling过程中执行的向下取整(rounding-down)操作。具体来说,_swapGivenOut() 通过调用 _upscale() 函数对 swapRequest.amount 进行了错误的向下取整。随后,该被取整的数值被作为 amountOut,在调用 _onSwapGivenOut() 计算 amountIn 时被使用。这种行为事实上违反了通用的安全实践,即舍入方向应始终使协议受益。

因此,对于给定的池(例如 wstETH/rETH/cbETH),计算得到的 amountIn 低估了实际所需的输入量。这使得用户可以用更少数量的某一底层资产(例如 wstETH)去交换另一种资产(例如 cbETH),从而由于有效流动性减少导致不变量 D 降低。结果,对应的 BPT(wstETH/rETH/cbETH)价格 也会被低估(通缩),因为BPT price = D / totalSupply。

0x3 攻击分析

攻击者实施了一个精心设计的两阶段攻击,其目的可能是为了降低被检测的风险:

  • 阶段一:在单笔交易中执行核心攻击,但未立即获利。

  • 阶段二:通过单独的交易提取资产,实现最终获利。

第一阶段又可以细分为两个子阶段:参数计算和批量交换。以下我们将通过在 Arbitrum 上的一笔攻击交易示例来说明这两个子阶段。

3.1 参数计算

在这一子阶段中,攻击者结合了链下计算与链上模拟,以根据当前 Composable Stable Pool 的状态(包括缩放因子、放大系数、BPT 汇率、兑换手续费等参数),精确调整下一子阶段(批量交换阶段)中每一跳的参数。有趣的是,攻击者还部署了一个辅助合约来协助这些计算,这可能是为了降低被抢跑(front-running)攻击的风险。

在开始时,攻击者首先收集目标池的基本信息,包括每个代币的缩放因子、放大参数、BPT 汇率以及手续费百分比等。随后,他们计算出一个关键数值,称为 trickAmt,即用于引发精度损失的目标代币操纵数量。

若将目标代币的缩放因子记作 sF,其计算方式如下:

为了确定在下一个子阶段(批量交换)的第 2 步中使用的参数,攻击者向辅助合约的 0x524c9e20 函数发起了一系列后续模拟调用,调用数据(calldata)如下:

uint256[] balances; // Balances of pool tokens (excluding BPT)

uint256[] scalingFactors; // Scaling factors for each pool token

uint tokenIn; // Index of the input token for this hop's simulation

uint tokenOut; // Index of the output token for this hop's simulation

uint256 amountOut; // Desired output token amount

uint256 amp; // Amplification parameter of the pool

uint256 fee; // Pool swap fee percentage

返回数据如下:

uint256[] balances; // Pool token balances (excluding BPT) after the swap

具体来说,初始余额和迭代次数在链下计算得出并作为参数传入攻击者的合约(据报分别为 100,000,000,000 与 25)。每次迭代执行三次交换:

  • Swap 1: 将目标代币的数量推至 trickAmt + 1,假设交换方向为 0 → 1。

  • Swap 2: 继续用 trickAmt 交换出目标代币,这会在 _upscale() 调用中触发向下舍入。

  • Swap 3: 执行回拨操作(1 → 0),本次要交换的数量由池中当前代币余额确定,方法是截断最重要的两个十进制位,即向下舍入到最近的 10{d-2} 的倍数,其中 d 为该值的十进制位数。例如:324,816 → 320,000。

  • 注意:由于 StableMath 计算中使用了牛顿-拉夫森(Newton–Raphson)方法,该步骤有时会失败。为此,攻击者设置了两次重试机制,每次重试采用原始值的 9/10 作为回退值。

攻击者的辅助合约源自 Balancer V2 的 StableMath 库,这从合约中出现的类似 "BAL" 的自定义错误信息可以看出。

3.2 批量交换

批量交换通过batchSwap() 操作实现,可分为三个步骤:

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

  • 步骤 2: 攻击者随后使用精心构造的量(= 8)在另一个底层资产(wstETH)与 cbETH 之间进行交换。由于在缩放代币数量时发生向下舍入,计算得到的 Δx 略微变小(例如从 8.918 变为 8),从而导致 Δy 被低估,进而使不变量 D(基于 Curve 的 StableSwap 模型)变小。由于 BPT price = D / totalSupply,BPT 价格因此被人为压低。

  • 步骤 3: 攻击者将底层资产反向换回 BPT,利用被压低的 BPT 价格获利,同时恢复池内余额。

0x4 攻击及损失汇总

我们在下表中汇总了各次攻击及其对应的损失,总损失超过 1.25 亿美元。

0x5 结语

本次事件涉及一系列针对 Balancer V2 协议及其分叉项目的攻击交易,造成了重大经济损失。在首次攻击之后,多个区块链上陆续出现了后续攻击和模仿攻击。此事件为 DeFi 协议的设计与安全 提供了若干重要启示:

  • 舍入行为与精度损失:在放大(upscaling)操作中使用单向舍入(向下取整),而在缩小(downscaling)操作中则采用双向舍入(向上与向下取整)。为防止类似漏洞,协议应采用更高精度的算术运算并实现严格的验证机制。 必须坚持**「舍入方向应始终有利于协议」**的标准原则。

  • 攻击演化过程:攻击者实施了一个高度复杂的两阶段攻击,以规避检测。第一阶段在单笔交易中执行核心攻击但未立即获利;第二阶段通过单独交易提取资产并实现利润。此事件再次凸显了安全研究者与攻击者之间持续的攻防博弈。

  • 运营安全感知与威胁响应:此事件强调了初始化与运行状态的及时告警,以及主动威胁检测与防御机制的重要性,以减轻持续或模仿攻击可能带来的潜在损失。

在保持业务连续性和运营稳定的同时,行业参与者可以将 BlockSec Phalcon 作为资产防护的最后一道防线。BlockSec 专业团队随时准备为您的项目提供全面的安全评估服务。

注:在我们发布报告后,Balancer也公布了其官方初步分析报告[6],确认了我们的分析结论。

Reference

[1] https://x.com/Phalcon_xyz/status/1985262010347696312

[2] https://x.com/Phalcon_xyz/status/1985302779263643915

[3] https://x.com/Balancer/status/1985390307245244573

[4] https://docs-v2.balancer.fi/concepts/pools/composable-stable.html

[5] https://docs-v2.balancer.fi/reference/swaps/batch-swaps.html

[6] https://x.com/balancer/status/1986104426667401241

免责声明:本文章仅代表作者个人观点,不代表本平台的立场和观点。本文章仅供信息分享,不构成对任何人的任何投资建议。用户与作者之间的任何争议,与本平台无关。如网页中刊载的文章或图片涉及侵权,请提供相关的权利证明和身份证明发送邮件到support@aicoin.com,本平台相关工作人员将会进行核查。

分享至:
APP下载

X

Telegram

Facebook

Reddit

复制链接