更新于 2025 年 11 月 6 日:Balancer 发布了其官方初步报告 [6],证实了我们分析中确定的根本原因。
2025 年 11 月 3 日,Balancer V2 的 Composable Stable Pools 以及跨多个链的几个分叉项目遭受了协调攻击,导致总损失超过 1.25 亿美元。BlockSec 尽早发布了警报 [1],随后发布了初步分析 [2]。
这是一次高度复杂的攻击。我们的调查显示,根本原因是由不变量计算中的精度损失引起的价格操纵,进而扭曲了 BPT(Balancer Pool Token)的价格计算。这种不变量操纵使攻击者能够通过一次批量兑换从特定稳定池中获利。尽管一些研究人员提供了有见地的分析,但某些解释存在误导性,根本原因和攻击过程尚未完全阐明。本文旨在对该事件进行全面准确的技术分析。
主要发现 (TL;DR)
根本原因:舍入不一致和精度损失
- 放大操作使用单向舍入(向下舍入),而缩小操作使用双向舍入(向上和向下舍入)。
- 这种不一致性导致精度损失,当通过精心设计的兑换路径加以利用时,就会违反舍入应始终有利于协议的标准原则。
攻击执行
- 攻击者故意构造了参数,包括迭代次数和输入值,以最大化精度损失的影响。
- 攻击者采用了两阶段方法来规避检测:首先在单个交易中执行核心攻击,但没有立即获利,然后通过单独的交易提取资产来实现利润。
运营影响和放大
- 由于某些限制 [3],协议无法暂停。这种无法停止运营的能力加剧了攻击的影响,并促成了众多后续或模仿攻击。
在接下来的章节中,我们将首先提供 Balancer V2 的关键背景信息,然后深入分析已识别的问题及相关攻击。
0x1 背景
Balancer V2 的 Composable Stable Pool
在此次攻击中受影响的组件是 Balancer V2 协议的 Composable Stable Pool [4]。这些池专为预期保持接近 1:1 比例(或按已知汇率交易)的资产而设计,并允许进行大额兑换,同时对价格影响最小,从而显著提高同类或相关资产之间的资本效率。每个池都有自己的 Balancer Pool Token (BPT),代表流动性提供者在该池中的份额,以及相应的底层资产。
- 该池采用 Stable Math(基于 Curve 的 StableSwap 模型),其中不变量 D 代表池的虚拟总值。
- BPT 价格可近似为:
从上述公式可以看出,如果 D 在账面上可以变小(即使没有实际资金损失),BPT 价格也会显得更便宜。
batchSwap() 和 onSwap()
Balancer V2 提供了 batchSwap() 函数,该函数支持 Vault [5] 内的多跳兑换。此函数通过一个参数确定两种兑换类型:
- GIVEN_IN(“给定输入”):调用者指定输入的准确数量,池计算相应的输出数量。
- GIVEN_OUT(“给定输出”):调用者指定期望的输出数量,池计算所需的输入数量。
通常,batchSwap() 由通过 onSwap() 函数执行的多个代币到代币兑换组成。以下概述了当 SwapRequest 被分配为 GIVEN_OUT 兑换类型时(请注意,ComposableStablePool 继承自 BaseGeneralPool)的执行路径:
以下显示了 GIVEN_OUT 兑换类型的 amount_in 计算,其中涉及不变量 D。
缩放和舍入
为了规范不同代币余额之间的计算,Balancer 执行以下两个操作:
- 放大(Upscaling):在执行计算之前,将余额和金额放大到统一的内部精度。
- 缩小(Downscaling):将结果转换回其本机精度,应用方向性舍入(例如,输入金额通常向上舍入,以确保池不收取过低的费用,而输出金额通常向下舍入)。
这种不一致性的原因尚不清楚。根据 _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() 函数中放大操作期间执行的向下舍入操作。特别是,_swapGivenOut() 通过 _upscale() 函数错误地将 swapRequest.amount 向下舍入。结果的舍入值随后在通过 _onSwapGivenOut() 计算 amountIn 时用作 amountOut。此行为与标准做法相矛盾,即舍入应以有利于协议的方式应用。
因此,对于给定的池(wstETH/rETH/cbETH),计算出的 amountIn 低估了实际所需的输入。这使得用户能够以较少的数量的一种底层资产(例如 wstETH)兑换另一种资产(例如 cbETH),从而由于有效流动性降低而导致不变量 D 降低。因此,相应 BPT(wstETH/rETH/cbETH)的价格会人为地被压低,因为 BPT 价格 = D / totalSupply。
0x3 攻击分析
攻击者执行了两阶段攻击,可能是为了最大限度地降低检测风险:
- 第一阶段,核心攻击在一次交易中完成,没有立即获利。
- 第二阶段,攻击者通过单独的交易提取资产来实现利润。
第一阶段可进一步细分为两个阶段:参数计算和批量兑换。下面,我们使用一个示例 Arbitrum 上的攻击交易 (TX) 来说明这些阶段。
参数计算阶段
在此阶段,攻击者结合了链下计算和链上模拟,以根据 Composable Stable Pool 的当前状态(包括缩放因子、放大系数、BPT 费率、兑换费用和其他参数)精确调整下一阶段(批量兑换)的每个跳跃参数。有趣的是,攻击者还部署了一个辅助合约来协助这些计算,这可能旨在减少被抢跑的风险。
首先,攻击者收集目标池的基本信息,包括每种代币的缩放因子、放大参数、BPT 费率和兑换费百分比。然后,他们计算一个称为 trickAmt 的关键值,这是用于诱导精度损失的目标代币的操纵数量。
令目标代币的缩放因子为 sF,计算公式为:
为了确定下一步(批量兑换)第 2 步中使用的参数,攻击者使用以下 calldata 对辅助合约的 0x524c9e20 函数进行了后续模拟调用:
uint256[] balances; // 池代币余额(不包括 BPT) uint256[] scalingFactors; // 每个池代币的缩放因子 uint tokenIn; // 此跳跃模拟的输入代币索引 uint tokenOut; // 此跳跃模拟的输出代币索引 uint256 amountOut; // 期望的输出代币数量 uint256 amp; // 池的放大参数 uint256 fee; // 池兑换费百分比
返回数据为:
uint256[] balances; // 兑换后的池代币余额(不包括 BPT)
具体来说,初始余额和迭代循环次数是在链下计算的,并作为参数传递给攻击者的合约(报告为 100,000,000,000 和 25,分别)。每次迭代执行三次兑换:
- 兑换 1:假设兑换方向为 0 → 1,将目标代币的数量推至 trickAmt + 1。
- 兑换 2:继续以 trickAmt 兑出目标代币,这会触发 _upscale() 的调用中的向下舍入。
- 兑换 3:执行反向兑换操作(1 → 0),兑换金额来自池中当前代币余额,通过截断最高有效位的两个十进制数字(即向下舍入到最接近的 $10^{d-2}$ 的倍数,其中 d 是小数位数)。例如,324,816 -> 320,000。
- 注意,由于 StableMath 计算中使用的牛顿-拉夫逊方法,此步骤有时可能会失败。为缓解此问题,攻击者实现了两次重试尝试,每次使用原始值的 9/10 作为回退。 攻击者的辅助合约源自 Balancer V2 的 StableMath 库,这一点可以从包含“BAL”风格的自定义错误消息中得到证明。
批量兑换阶段
然后,batchSwap() 操作可以分为三个步骤:
-
第一步:攻击者将 BPT(wstETH/rETH/cbETH)兑换成底层资产,以精确地将一种代币(cbETH)的余额调整到舍入边界的边缘(金额 = 9)。这为下一步的精度损失创造了条件。
-
第二步:攻击者使用构造的金额(= 8)在另一种底层资产(wstETH)和 cbETH 之间进行兑换。由于缩放代币金额时向下舍入,计算出的 Δx 会略小(从 8.918 变为 8),从而导致不变量(来自 Curve 的 StableSwap 模型 D)被低估。由于 BPT 价格 = D / totalSupply,BPT 价格被人为地压低。
- 第三步:攻击者将底层资产反向兑换回 BPT,恢复了平衡,同时从被压低的 BPT 价格中获利。
0x4 攻击和损失
我们在下表中总结了攻击及其相应的损失,总损失超过 1.25 亿美元。
0x5 结论
此次事件涉及一系列针对 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 [6] https://x.com/balancer/status/1986104426667401241



