Balancer v2 的 Composable Stable Pools 遭受攻击,损失超过 1.2 亿美元。
最近 Balancer v2 Composable Stable Pool 遭受的攻击,导致超过 1.2 亿美元的损失,这无疑震撼了区块链社区。OpenZeppelin 曾对 Balancer v2 代码库的早期版本进行过安全审计,当时攻击向量尚未出现,而该向量后来被利用。 我们现在分享我们对此次攻击的技术分析,以帮助社区了解这种复杂的攻击,并推动关于在我们快速发展的行业中采取有效安全最佳实践的讨论。 这篇文章分为两个部分:
2025 年 11 月 3 日 UTC 时间大约 7:40,特定的 Balancer v2 稳定池受到了复杂的攻击,导致 Balancer 及其分叉的总损失超过 1.2 亿美元。
这次攻击利用了以下几个情况:
ComposableStablePool 合约的实例。这是一个特殊的池子,专为预计以接近平价或已知汇率持续交换的资产而设计。Vault合约的batchSwap 功能,它允许瞬时交换在需要结算未偿还差额之前发生。_upscale 函数中的舍入行为。攻击向量于 2021 年 7 月 16 日首次引入,当时 MetaStablePool 在 commit 059284e 中覆盖了 _scalingFactors 函数。 2021 年 9 月 1 日,在 commit 4e9e70a 中,对线性池应用了相同的更改,然后在 2021 年 9 月 20 日,在 commit f450760 中,对 StablePhantomPool 进行了相同的更改,后来重命名为 ComposableStablePool。OpenZeppelin 之前的审计没有涵盖任何呈现此攻击向量的池子。
在下面的问题描述中,我们将解释为什么引入此覆盖是攻击的核心。
Vault 合约的 batchSwap 函数是此攻击的入口点。 攻击者精心制作了一个调用,目标是 ComposableStablePool 的实例。 Vault 合约中将导致与 ComposableStablePool 交互的相关逻辑由此流程定义:
batchSwaps → 内部 调用_swapWithPools → 内部 调用 在_swapWithPool处为每次交换调用一个池子 _→ _processGeneralPoolSwapRequest → 最终_ 调用onSwap on theComposableStablePool .
一旦进入 ComposableStablePool,执行就会在 onSwap hook 中继续进行,其中会发生两件事:
_scalingFactors函数。_swapGivenOut,如上面提供的示例交易中定义的那样。 请注意,攻击者可能已经决定采用 _swapGivenIn 模式,但攻击交易中提供的输入清楚地表明了选择。 此函数将调用_upscale 函数。让我们看一下这两个函数。
_scalingFactorsfunction _scalingFactors() internal view virtual override returns(uint256[] memory) {
uint256 totalTokens = _getTotalTokens();
uint256[] memory scalingFactors = new uint256[](totalTokens);
for (uint256 i = 0; i < totalTokens; ++i) {
scalingFactors[i] = _getScalingFactor(i).mulDown(_getTokenRate(i));
}
return scalingFactors;
}
这个函数做两件事:
_upscalefunction _upscale(uint256 amount, uint256 scalingFactor) pure returns (uint256) {
/* 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. */
return FixedPoint.mulDown(amount, scalingFactor);
}
它只是执行金额乘以其缩放因子的乘法,最后除以 1e18。
问题在于两个事实:
mulDown),与交换的方向无关。scalingFactors 小几个数量级,则精度损失变得不可忽略。 在攻击者的交易中,金额和缩放因子的典型值为:金额:“17”
缩放因子:“1058132408689971699”
如果我们执行 amount*scalingFactor / 1e18,我们将获得 17,98,但在 Solidity 中,这将截断为 17,导致 _upscale 函数中 0% 的净变化。 相反,如果我们现在想象一个金额为 17000000000000000000(一个具有 18 个小数位的 token),我们将获得 17988250950000000000,这表示 5.8% 的增长(汇率确实如此)。 同样,如果我们执行一个具有 6 个小数位的 token(如大多数稳定币)17000000,我们将获得 17988250,这仍然大致是百分比的相同正增量。
通过使 amount*scalingFactor mod 1e18 尽可能大来 最大化截断,因此该乘积在除以 1e18 时会损失最大精度。
例如,在下表中,金额=17 和 金额=50 都产生大约 0.90 的绝对舍入损失。 然而,误差占金额的百分比 差异很大,并且 损失的增加百分比 差异更大,因为相对于 50 而言,相同的绝对损失相对于 17 要大得多。

表 1. 按因子 1.058132408689971699 缩放时的向下取整误差示例
_swapGivenOut 函数稍后将使用这些舍入的值来计算交换后攻击者欠池的金额。 这个值被人为地降低,使交换更便宜。 _swapGivenOut 的内部机制很复杂,它们还证明了为什么可以在可以随意获得低流动性状态的池中实现这一点。 这是由于一个不变量检查,该检查确保在一定程度上收敛。 在高流动性情况下,由于相同的不变量保护,余额的巨大变化会失败。
最终,经过大量迭代后,batchSwap 之后的增量会膨胀,将池中的大部分资金记入攻击者的账户。
此外,关于 _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. */
此注释的先前版本有一个额外的注释:
/* …as the impact of this rounding is expected to be minimal (and there's no rounding error unless `_scalingFactor()` is overriden). */
这很重要,因为 StablePhantomPool 于 2021 年 9 月 20 日引入,后来重命名为 ComposableStablePool,它完全实现了我们在上面讨论的 _scalingFactor 中的覆盖。 以前,在旧版本的 StablePool 中,_scalingFactor 函数仅考虑 token 小数位的差异,并且如已看到的那样,现在它也考虑了 exchange 汇率。 这从根本上是不同的,因为正如 _upscale 注释所表明的那样,代码从单一缩放因子(即 1e12)变为非单一汇率。 这种转变是 _upscale 警告的内容,为舍入误差以及攻击向量打开了大门。
虽然启用舍入误差是根本原因,但利用它需要利用额外的协议机制和特定的攻击步骤。
Balancer 的 batchSwap 允许 瞬态内部余额,这些余额仅在批处理结束时进行净结算。 感谢这一点,攻击者有效地在批处理中“借用”了 BPT 来操纵池,而无需在持有 BPT 的情况下结束交易。
BPT(Balancer 池 token)是一种 ERC-20,它代表 Balancer 池的按比例份额。 在某些池设计中,包括目标 ComposableStablePools,BPT 也可以作为池资产出现并且可以交易/交换。
第一阶段通过反复将 BPT 交换为 token₁ 和 BPT 交换为 token₂,将池 token 余额(例如,WETH / osETH)推至 非常低的水平(≲ 10 万)。
随着余额变小,定点舍入 变得占主导地位。 目标是 最大化向下取整算法中丢弃的小数部分,即最大化 amount*scalingFactor mod 1e18,以便该乘积在除以 1e18 时损失尽可能多的精度。 在余额足够低的情况下,整个预期增量可能会被截断,这就是达到最大值的地方,也是黑客寻找的东西。
漏洞利用以 重复的三元组交换 运行:
如下面的跟踪所示,关键交换通常使用 amount=17 来对抗 18 的 vault 余额。 你可以在每个三元组的 第二次交换 中看到重复的 17 金额。

图 1. BatchSwap 跟踪:重复的三元组。来源。
我们认为提供关于 OpenZeppelin 对 Balancer v2 早期版本的审计的具体细节非常重要,因为媒体上对我们的参与和一般安全审计存在误解。
攻击向量存在于 OpenZeppelin 审计结束后添加的代码(ComposableStablePool 和 Linear 池)中,并且不属于我们的审计范围。 OpenZeppelin 团队审计的合约(StablePool 和 WeightedPool)不包含返回非单位值的 _scalingFactors 覆盖,并且不会启用此漏洞利用。
OpenZeppelin 对 Balancer v2 进行了两次审计:
首次审计(2021 年 2 月 8 日 - 3 月 15 日)
在这里,_upscale 函数提出了引用的评论,该评论假定只要不对 _scalingFactors 函数进行任何覆盖,安全性就是安全的。
第二次审计(2021 年 8 月 23 日 - 9 月 10 日)
关键的细节是我们的第二次审计期间和之后发生的事情。 新的合约已添加到存储库中,但不在我们的参与范围内:
这些合约引入了关键的 _scalingFactors 覆盖,该覆盖考虑了汇率,从而使返回值不再是单位值,从而启用了攻击向量。 我们审计的 StablePool 和 WeightedPool 合约维护了原始的 _scalingFactors 实现,该实现仅处理小数位归一化 - 它们不包括使舍入行为可利用的汇率计算。
我们审计的池永远没有覆盖 _scalingFactors 以包括汇率。 正如原始代码注释警告的那样 _"预计这种舍入的影响将是最小的(除非 `_scalingFactor() \ 被覆盖,否则不会出现舍入错误)"_, _upscale 中的舍入方法是安全的 _"除非 `_scalingFactor``()_ 被覆盖", 这在我们审查的合约中并没有发生。 攻击向量仅在以下情况下显现:
_scalingFactor 函数被覆盖以包含非单位汇率(如 ComposableStablePool 中所示)_upscale 舍入行为交互此事件引发了关于智能合约安全审计有效性的辩论,一些评论员质疑它们是否能提供价值。 虽然这种情绪在沮丧的时刻是可以理解的,但它未能认识到为什么安全审计已成为行业最佳实践,以及安全审计实践在过去十年中对降低风险和保护用户产生的巨大影响。
安全公司每年共同防止数百个潜在的漏洞利用。 仅 OpenZeppelin 就已在所有审计中在它们投入生产之前识别出超过 700 个严重和高危漏洞。 这些预防的灾难不会成为头条新闻,但它们代表着数十亿美元的受保护价值和无数用户免受损失。 高质量的安全审计还有助于开发团队提高其在整个开发生命周期中的安全态势,从而使区块链应用程序中进一步引入错误的可能性呈指数级降低。
真正的教训不是审计无效 - 而是行业的审计实践尚未赶上保护重要价值的复杂协议的发展速度。 由于审计通常被限定为孤立的审查,而不是持续的参与,因此随着代码库的发展,上下文可能会丢失。 改变这一点需要集体转变:协议团队投资于持续安全,审计员构建支持它的框架。
Balancer v2 漏洞利用说明了为什么我们一直倡导行业如何处理安全性的根本转变。 自从 2016 年开创智能合约审计实践以来,我们已经看到最成功的安全结果来自持续的安全合作伙伴关系,这些合作伙伴关系涵盖整个协议代码库及其所有随时间的变化,而不是对主要升级进行的临时代码审查,这些审查通常范围有限,仅限于这些变化。
当安全研究人员与协议持续合作时,他们会对协议的架构、工程流程以及做出特定设计决策的原因有深入的了解。 与临时审计相比,这种深刻的理解显着降低了风险。
尽管 Balancer v2 代码库已由四家独立的审计公司审查,但每家公司都专注于协议的不同范围。 聘请多个审计师有助于减少错过漏洞的机会,但保持至少一个长期的安全合作伙伴关系可以更深入、持续地了解代码库如何发展以及新变化如何与现有逻辑交互。
通过我们十年的经验,OpenZeppelin 一直致力于在整个行业中建立和提升安全标准。 OpenZeppelin 合约已成为构建安全智能合约的事实标准,目前为整个生态系统中 超过 30 万亿美元的总转移价值 提供支持。 正如这些合约为安全开发奠定了基础一样,我们一直在积极努力,为安全评估和持续协议保护建立更强大的标准。
除了与 我们客户的长期关系 外,我们还在积极努力在行业和监管层面塑造这些标准。 例如,OpenZeppelin 贡献于行业标准制定机构,例如区块链安全标准委员会、企业以太坊联盟和国际标准组织 (ISO),以确保我们的团队和社区帮助开创的安全最佳实践可供整个行业使用。
我们还与全球的监管机构和政策制定者合作,以确保在相关的监管制度中推广区块链安全最佳实践。 特别是,OpenZeppelin 已与美国财政部、美国证券交易委员会、英国金融行为监管局以及法国 ACPR 和 银行监管机构 (AMF) 合作,讨论定期进行全面安全审计的好处。 此外,我们还探索了创建专门的自律组织的潜力,该组织类似于其他领域的审计师,我们认为这有助于正式化区块链技术的安全审计标准和方法,为审计师设定质量和道德要求,并管理认证以认证合格的审计师,这可以改善安全结果并提高消费者信心。
每个安全事件都提供了宝贵的见解,这些见解推动了我们整个行业的改进。 Balancer v2 漏洞利用加强了随着协议变得更有价值和复杂,协议团队必须投资于持续安全,以及审计公司构建支持它的框架,这一点变得至关重要。 我们可以共同创建与他们保护的技术一样创新和强大的安全实践。
- 原文链接: openzeppelin.com/news/un...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!