更新于 2025 年 11 月 6 日:Balancer 发布了官方初步报告[6],证实了我们分析中发现的根本原因。
2025 年 11 月 3 日,Balancer V2 的 Composable Stable Pools,以及多个链上的多个分叉项目,遭遇了一次协同利用,导致总损失超过 1.25 亿美元。BlockSec 在第一时间发布了警报[1],随后发布了初步分析[2]。
这是一次非常复杂的攻击。我们的调查显示,根本原因是不变量计算的精度损失导致的价格操纵,这反过来又扭曲了 BPT(平衡池代币)的价格计算。这种不变量操纵使攻击者可以通过一次批量交换从特定的稳定池中获利。虽然一些研究人员提供了有见地的分析,但某些解释具有误导性,根本原因和攻击过程尚未完全澄清。本博客旨在对该事件进行全面准确的技术分析。
关键启示(TL;DR)
根本原因:舍入不一致和精度损失
- 升频操作使用单向舍入(向下舍入),而降频操作使用双向舍入(向上和向下舍入)。
- 这种不一致性会造成精度损失,如果通过精心设计的交换路径加以利用,就会违反四舍五入应始终有利于协议的标准原则。
漏洞执行
- 攻击者故意制作参数,包括迭代次数和输入值,以最大限度地提高精度损失的效果。
- 攻击者使用两阶段方法逃避检测:首先在单笔交易中执行核心漏洞,但不会立即获利,然后在另一笔交易中通过提取资产实现获利。
运行影响和放大
- 由于某些限制[3],协议无法暂停。这种无法暂停操作的情况加剧了漏洞利用的影响,并使大量后续或模仿攻击成为可能。
在以下章节中,我们将首先提供有关 Balancer V2 的关键背景信息,然后深入分析已发现的问题和相关攻击。
0x1 背景
Balancer V2 的复合稳定池
此次攻击中受影响的组件是 Balancer V2 协议中的 Composable Stable Pool [4]。这些池专为预计将保持接近 1:1 平价(或以已知汇率交易)的资产而设计,允许在价格影响最小的情况下进行大额掉期,从而显著提高同类或相关资产之间的资本效率。每个池都有自己的平衡池代币(BPT),代表流动性提供者在池中的份额以及相应的基础资产。
- 该池采用稳定数学(基于 Curve 的 StableSwap 模型),其中不变式 D 代表池的虚拟总价值。
- BPT 价格近似为
batchSwap() 和 onSwap()
Balancer V2 提供了 batchSwap() 函数,可实现 Vault [5] 内的多跳交换。有两种交换类型由传递给该函数的参数决定:
- GIVEN_IN("Given In"):调用者指定输入令牌的确切金额,交换池计算相应的输出金额。
- GIVEN_OUT("Given Out"):调用者指定所需的输出量,池计算所需的输入量。
通常情况下,批量交换(batchSwap) 由多个通过 onSwap() 函数执行的令牌对令牌交换组成。下面概述了 SwapRequest 被指定为 GIVEN_OUT 掉期类型时的执行路径(注意 ComposableStablePool 继承自 BaseGeneralPool):
为了使不同代币余额的计算正常化,Balancer 会执行以下两种操作:
- 放大:在执行计算之前,将余额和金额缩放至统一的内部精度。
// 上调四舍五入不一定总是朝着同一个方向进行:例如,在交换中,"token in "的余额应该上调。 例如,在交换中,"入"//"出 "的余额应向上舍入,而 "出 "的余额应向下舍入。这是唯一一个四舍五入 // 所有金额都向同一方向舍入,因为舍入的影响预计会很小(而且不会出现舍入错误,除非 // 除非重写
_scalingFactor(),否则不会出现四舍五入错误)。
0x2 漏洞分析
底层问题源于 BaseGeneralPool._swapGivenOut() 函数在上调过程中执行的四舍五入操作。特别是,_swapGivenOut() 通过 _upscale() 函数错误地将 swapRequest.amount 向下舍入。在通过 _onSwapGivenOut() 计算 amountIn 时,四舍五入后的值被用作 amountOut。这种行为违背了四舍五入应有利于协议的标准做法。
0x3 攻击分析
攻击者实施了两阶段攻击,可能是为了将检测风险降至最低:
- 在第一阶段,核心漏洞利用是在单笔交易中进行的,不会立即产生收益。
- 在第二阶段,攻击者通过在单独交易中提取资产来实现利润。
第一阶段可进一步分为两个阶段:参数计算和批量交换。下面,我们以Arbitrum 上的攻击交易 (TX) 为例说明这两个阶段。
参数计算阶段
在这一阶段,攻击者将链外计算与链上模拟相结合,以根据 Composable Stable Pool 的当前状态(包括缩放因子、放大系数、BPT 率、交换费和其他参数),在下一阶段(批量交换)精确调整每个跳数的参数。有趣的是,攻击者还部署了一个辅助合约来协助这些计算,这可能是为了减少前置运行的风险。
一开始,攻击者会收集目标池的基本信息,包括每个代币的缩放因子、放大参数、BPT 利率和掉期费用百分比。然后,他们会计算一个名为 trickAmt 的关键值,即用于诱导精确损失的目标代币的操纵量。
将目标代币的缩放因子表示为 sF,计算公式为
uint256[] balances; // 池代币余额(不包括 BPT) uint256[] scalingFactors; // 每个池代币的缩放因子 uint tokenIn; // 该跳转模拟的输入令牌索引 uint tokenOut; // 该跳转模拟的输出令牌索引 uint256 amountOut; // 期望输出令牌数量 uint256 amp; // 池的放大参数 uint256 fee; // 池交换费百分比
返回数据是
uint256[] balances; // 池交换后的代币余额(不包括 BPT
具体来说,初始余额和迭代循环次数是在链外计算的,并作为参数传递给攻击者的合约(分别报告为 100,000,000,000 和 25)。每次迭代执行三次交换:
- 交换 1:将目标代币的金额推至 trickAmt + 1,假设交换方向为 0 → 1。
- 交换 2:继续将目标代币与 trickAmt 交换,这将触发 _upscale() 调用中的向下舍入。
- 交换 3:执行换回操作(1 → 0),其中要交换的金额是通过截去小数点后两位最重要的数字,即向下舍入到最接近的 10^{d-2}$ 的倍数得出的,erd 是小数点后的数字。例如,324,816 -> 320,000。
- 请注意,由于在 StableMath 计算中使用了牛顿-拉斐森方法,这一步骤有时可能会失败。为了缓解这种情况,攻击者会进行两次重试,每次重试都使用原值的 9/10 回退。 攻击者的辅助合约源自 Balancer V2 的 StableMath 库,包含 "BAL "风格的自定义错误信息就是证明。
批量交换阶段
然后,batchSwap() 操作可分为三个步骤:
-
第 1 步:攻击者用 BPT(wstETH/rETH/cbETH)交换基础资产,将一个代币(cbETH)的余额精确调整到四舍五入边界(amount = 9)的边缘。这为下一步的精确损失创造了条件。
-
第 2 步:然后,攻击者使用精心制作的金额(= 8)在另一个标的(wstETH)和 cbETH 之间进行交换。由于在缩放代币金额时四舍五入,计算出的Δx 会略微变小(从 8.918 到 8),从而导致低估的 Δy,因此不变式(Curve 的 StableSwap 模型中的 D)也会变小。由于 BPT 价格 = D / 总供应量,因此 BPT 价格被人为虚减。
0x4 攻击和损失
我们在下表中总结了攻击及其相应损失,总损失超过 .25 亿美元。
此次事件涉及一系列针对 Balancer V2 协议及其分叉项目的攻击交易,造成了重大经济损失。首次攻击发生后,在多个链上发现了大量后续和模仿交易。这一事件为 DeFi 协议的设计和安全性提供了若干重要启示:
-
舍入行为和精度损失:上调操作中使用的单向舍入(向下舍入)与下调操作中使用的双向舍入(向上和向下舍入)不同。为防止出现类似漏洞,协议应采用更高精度的运算,并实施稳健的验证检查。必须坚持四舍五入应始终有利于协议的标准原则。
-
漏洞利用的演变:攻击者实施了复杂的两阶段漏洞利用,旨在逃避检测。在第一阶段,攻击者在一次交易中执行了核心漏洞利用,但没有立即获利。在第二阶段,攻击者通过在单独交易中提取资产实现盈利。这一事件再次凸显了安全研究人员与攻击者之间正在进行的军备竞赛。
-
操作意识和威胁响应:这起事件凸显了有关初始化和运行状态的及时警报以及主动威胁检测和预防机制的重要性,以减少持续攻击或模仿攻击造成的潜在损失。
在保持运营和业务连续性的同时,行业参与者可以利用BlockSec Phalcon作为保护其资产的最后一道防线。BlockSec 专家团队随时准备为您的项目进行全面的安全评估。
- 🔗 BlockSec 审计
- 🔗 Phalcon 安全 APP
- 🔗 预约演示
参考资料
[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



