2023 年 9 月 15 日更新:Balancer 已发布官方事后分析,详细描述了本次事件的完整经过,包括经验教训。这份事后分析叙述精妙且详尽,极具说服力,非常值得一读。
从安全角度来看,这份事后分析揭示了存在两个漏洞。第一个是我们报告中讨论的向下取整错误,第二个是“0 供应时重置费率”,正如我们在报告的攻击步骤 3.6 和 3.7 中所述。Balancer 的报告认为第二个问题最为关键,第一个问题起到了推波助澜的作用。然而,我们认为这两个漏洞对于实现获利性攻击同等重要:
第一个漏洞用于提升代币汇率(pumping the token rate),这是获利的根本原因。如果没有它,将无法获利。
第二个漏洞通过平衡 bb-a-tokens 的债务使攻击得以实现。如果没有它,由于 bb-a-tokens 的流动性较差,且缺乏获取这些代币的其他途径(除非攻击者能通过某种手段获取),攻击将会失败。
2023 年 8 月 22 日,Balancer 公开发布公告,称存在一个影响多个提升池(boosted pools)的关键漏洞,并敦促用户立即从受影响的资金池中撤出 LP。Balancer 启动了紧急缓解程序以保护大部分 TVL,但仍有一些资金面临风险。遗憾的是,五天后的 8 月 27 日,我们监测到了几起针对该漏洞的实际攻击。自那时起,已有多达 212 万美元 的资产被盗。
截至本文撰写之时(距离公告发布已超过三周,我们认为此时披露已相对安全),Balancer 尚未发布任何关于此漏洞的深度分析。在本报告中,我们的目标是基于其中一笔攻击交易,提供一份全面的分析。
关键摘要 (TL;DR)
- 我们的调查表明,根本原因在于线性池(
linearpool)中向下取整逻辑导致的代币价格操纵。这进而对相应的提升池(boostedpool)所使用的缓存代币汇率产生了不当影响。 - 此事件强调了及时通知基于易受攻击源进行分叉(fork)的项目至关重要,这对整个社区来说确实是一个重大挑战。
- 持续发生的多起攻击事件凸显了主动进行威胁预防的必要性,这将不可避免地有助于减少潜在损失。
在接下来的章节中,我们将首先提供有关 Balancer 的一些必要背景信息。随后,我们将对该漏洞及相关攻击进行全面分析。最后,我们将简要总结目前观察到的攻击及其各自的获利情况。
0x1 Balancer 的背景
Balancer V2 [1] 是一个去中心化的自动做市商(AMM)协议,代表了可编程流动性的灵活构建模块。不同于其他将代币核算与资金池逻辑捆绑在一起的 AMM,Balancer 将代币核算与管理从池逻辑中分离出来,这可以通过减少大量代币转移来提高交换效率。
Balancer 支持多种类型的资金池。每个资金池都关联一个名为 BPT (Balancer Pool Token) 的 LP 代币。基本上,BPT 的价值是根据所有底层代币的总价值计算得出的。
Balancer 支持多跳交换(multi-hop swaps),即 batch swaps,它利用了在 Vault 中注册的所有资金池的最佳价格。具体而言,Vault 提供了 batchSwap 函数来促进多跳交换。
Balancer 池中的 flash swap(闪电交换)无需持有传统交换所需的任何输入代币。相反,当你发现不平衡时,可以指示 Vault 执行交换,随后即可获得奖励。
0x1.1 Balancer 中的各类资金池
下面,我们简要介绍一些与此漏洞相关的资金池概念。
-
线性池 (Linear Pools):
Linear池 [2] 是 Balancer 提供的资金池,旨在以已知的兑换率促进资产与其生息封装代币之间的交换。顾名思义,Linear池使用线性数学。一个linear池持有三种代币,包括:- 两种资产,即具有相等潜在基础价值的
main(主)代币和wrapped(封装)代币; - 对应的
BPT(Balancer Pool Token)。请注意,BPT是 ERC-20 代币。
- 两种资产,即具有相等潜在基础价值的
-
嵌套线性池 (Nesting Linear Pools):线性池 BPT 可以嵌套在另一个池中。由于交换者可以从
BPT交换到线性池的底层代币之一,这就在基础资产和外部池中的代币之间建立了一条简单的batchSwap路径。 -
组合稳定池 (Composable Stable Pools):组合稳定池 [3] 专为预期以接近平价或已知汇率持续交换的资产而设计。组合稳定池使用稳定数学(Stable Math),允许在遇到显著价格波动前进行大规模交换,从而极大地提高了同类和相关类资产交换的资本效率。
当一个池允许与其自身的 LP 代币相互交换时,它就是可组合的。将其 LP 代币放入其他池(或“嵌套”)可以方便地从嵌套池代币交换到外部池中的代币。
-
提升池 (Boosted Pools):
Boosted池 [4] 旨在提高大型资金池闲置流动性的资本效率。Boosted池实际上是其他池的一个子类。例如,一个boosted池可以构建在linear池之上。提升池旨在通过使用户能够为常见代币提供交换流动性,同时将闲置代币转发给外部协议,从而提供高资本效率。这使得流动性提供者在除了从交换交易中收取的费用外,还能获得像 Aave 这类协议带来的收益。
0x1.2 易受攻击的提升池实例:Balancer Boosted Aave USD
Balancer Boosted Aave USD (代号: bb-a-USD) 是一个组合稳定池,旨在促进三种稳定币(即 USDC、USDT 和 DAI)之间的交换,同时将闲置流动性发送至 Aave。其底层的 linear 池包括:
bb-a-USDC(包含 USDC 和封装的 aUSDC)bb-a-USDT(包含 USDT 和封装的 aUSDT)bb-a-DAI(包含 DAI 和封装的 aDAI)
具体而言,bb-a-USD 是一个组合稳定池集合,其中包含三个不同 linear 池的代币,而每个 linear 池都关联一个稳定代币:DAI、USDC 和 USDT。下图由官方文档 [5] 提供,展示了 bb-a-USD 的结构:

0x1.3 如何计算 BPT 的价格
一个自然而然出现的重要问题是:当将特定数量(即 amountIn)的 BPT 交换为另一代币的一定数量(即 amountOut)时,如何确定 BPT 的价格?
Balancer 针对不同池采用的数学公式提供了详细的说明 [6, 7]。为了简单起见,我们在此抽象并总结最相关的概念。
以 linear 池为例,BPT 的价格是在 LinearPool 合约的 onSwap 函数中计算的。

计算公式可概括如下:

其中的 tokenRate 使用以下公式计算:

是一个常量:。
在上述公式中,分子可简化为 main 代币余额与 wrapped 代币余额之和,而分母是预定义值(即 _INITIAL_BPT_SUPPLY)与 BPT 余额之差。
值得注意的是,在进行计算之前,所有参与代币的余额都需要进行名义化(nominalized),因为不同代币可能具有不同的精度。具体而言,给定代币的原始余额将乘以相应的放大因子(upscale factor),该因子由 _scalingFactors 函数确定。
(1) 线性池 Linear Pools 的放大因子
BPT 和 main 代币都有一个固定的、常数的放大因子。

(2) 提升池 Boosted Pools(如 bb-a-USD)的放大因子
提升池的计算稍微复杂一些。具体来说,返回的放大因子是原始放大因子(例如 1e18)与代币汇率的乘积,该代币汇率从缓存的代币汇率(如有)中获取。

缓存的代币汇率从何而来?存在一个名为 _updateTokenRateCache 的私有函数。显然,该函数会首先通过调用相应代币的 getRate 函数来检索汇率,然后将其缓存。

同样,以 bb-a-USDC 为例,相应的 getRate 函数的核心逻辑遵循我们之前讨论的公式。

请注意,有三种可能的路径可以触发 _updateTokenRateCache 函数:

此外,通过 onSwap 函数执行更新路径时,会有过期检查:

0x2 漏洞分析
根本原因在于线性池 linear pool 中 onSwap 函数里的向下取整逻辑引发的价格操纵。这进而会不当地影响提升池 boosted pool 使用的缓存代币汇率。
具体而言,当调用 _downscaleDown 函数时,amountOut 被向下取整。因此,如果 amountOut 和 scalingFactors[indexOut] 之间存在显著的量级差异,_downscaleDown 函数的返回值可能会变为零。

例如,如果我们使用 bb-a-USDC (作为 BPT) 在 bb-a-USDC 池中交换 USDC (作为 main 代币),当 amountOut 小于 1,000,000,000,000 时,返回值将始终向下取整为零。这将增加 bb-a-USDC 的余额,因为它被视为单向增加了 bb-a-USDC 的流动性。
结果就是,如果 BPT 被用作交换代币,其汇率会上升,这符合汇率计算公式:在分子保持不变的情况下,分母减小。该漏洞可能被利用从而导致(巨大)的价格差异。
0x3 攻击分析
这笔攻击交易包含以下攻击步骤:
- 通过 Aave 的闪电贷借入 300,000 USDC。
- 在
bb-a-USDC池中,将 1.067753 USDC 交换为 0.970495 aUSDC。 - 在
bb-a-USDC和bb-a-USD池中执行batchSwap,即用 42,203 USDC 收割 15,628bb-a-USDC、139,431bb-a-DAI和 248,868bb-a-USDT。详细步骤总结在下表中(已包含小数位):

- 将 LP 代币交换为相应的底层稳定币:
- 139,431
bb-a-DAI-> 141,127DAI(在bb-a-DAI池中) - 15,628
bb-a-USDC-> 15,685USDC(在bb-a-USDC池中) - 248,868
bb-a-USDT-> 253,461USDT(在bb-a-USDT池中)
- 偿还闪电贷,最终利润为:
- 114,324
DAI - 253,461
USDT - 0.970495
aUSDC
值得注意的是,攻击者在第 2 步中利用 bb-a-USDC 池中的 USDC 抽干了 aUSDC,这将使第 3 步的价格操纵变得更加容易,即攻击者只需专注于 USDC 和 bb-a-USDC。
步骤 3 在其中发挥了关键作用。现在我们深入细节,查明攻击者为何能获利。具体而言:
- 步骤 3.1 用于通过
bb-a-USDC从bb-a-USDC池中抽取USDC; - 步骤 3.3 和 3.4 用于将
bb-a-USDC交换为bb-a-DAI,而步骤 3.5 用于将bb-a-USDC交换为bb-a-USDT。 - 步骤 3.7 用于通过
bb-a-USDC池将USDC交换为bb-a-USDC。
这里的步骤 3.2 和 3.6 由于上述的向下取整问题,并未交换回任何目标代币(即
USDC),因此交换后目标代币的余额保持不变,这可以被视为向bb-a-USDC池中添加了额外的bb-a-USDC流动性。
显然,异常交换主要发生在步骤 3.4、3.5 和 3.7。下面我们将依次探讨这些步骤的细节。
(1) bb-a-USDC -> bb-a-DAI
在此步骤 3.3 中,bb-a-USDC 和 bb-a-DAI 之间的汇率几乎为 1,而在步骤 3.4 中,汇率变为 19:
- 步骤 3.3: 1,000,339,378,515,783,699 / 1,000,000,000,000,000,000 = 1.00
- 步骤 3.4: 139,430,482,942,020,211,267,110 / 7,300,000,000,000,000,000,000 = 19.10
回顾我们之前讨论的代码逻辑,在步骤 3.3 中,在返回之前缓存的代币汇率以计算放大因子(1,012,181,365,780,643,700)后,它更新了汇率以计算出一个新值(40,240,000,000,000,000,000)。该更新后的值随后在步骤 3.4 中被用作新的放大因子。由于原始放大因子保持不变(即 1e18),这意味着新汇率比旧汇率高出约 40 倍。

那么,这种显著的增长来自何处呢?让我们重温计算 tokenRate 的公式。由于 aUSDC 的余额在第 2 步中已被耗尽,tokenRate 的计算可简化如下:


此处 nominalMainBalance 的实际值正是由于步骤 3.2 中的向下取整造成的。
(2) bb-a-USDC -> bb-a-USDT
步骤 3.5 使用相同的技巧来获取更多的 bb-a-USDT,且 bb-a-USDC 与 bb-a-USDT 之间的汇率超过了 12:
- 248,868,905,733,352,246,491,156 / 20,000,000,000,000,000,000,000 = 12.44
(3) USDC -> bb-a-USDC
此外,bptBalance 在步骤 3.6 中增加,然后 bptSupply 在步骤 3.7 中变为零。通过这样做,可以以接近 1:1 的汇率将 USDC 交换为 bb-a-USDC。

0x4 攻击与获利总结
截至撰写本文时,我们已在实际环境中观察到了数十起攻击,造成的损失超过 212 万美元。总结而言,这些攻击由三个不同的账户执行,如下所示:

Balancer 因该漏洞共遭受了约 100 万美元 的损失。在对 Balancer 进行首次攻击后的不到 12 小时内,其分叉协议 Beethoven X 也遭遇了类似的攻击,导致损失估计约为 110 万美元。**Beethoven X 的损失甚至比 Balancer 更大!**此次安全事件的总损失达到了约 212 万美元。
我们收集的攻击交易完整列表已整理在文档中。详情请参阅该文档。
关于攻击者的一些观察
通过分析每个网络发起的交易,我们发现 Fantom 上的攻击交易记录与在 Ethereum 和 Optimism 上的存在显著差异。
具体而言,除了关键函数中的显著差异外,Fantom 上的攻击者还利用了两种独特的技巧来避免被 MEV 机器人抢先交易。此外,用于 Fantom 攻击的资金在攻击前 163 天就已准备就绪。
通过上述细节观察,我们可以推断:
- 涉及了至少两名不同的攻击者。
- Fantom 上的攻击者是一名经验丰富的连环惯犯。
0x5 结论
总而言之,这是一个植根于向下取整逻辑的隐蔽漏洞。然而,利用此漏洞并非易事。具体来说,攻击者能够通过利用线性池中的向下取整问题来夸大缓存的代币汇率,从而操纵相应提升池中的代币价格。
此事件还强调了及时通知那些基于易受攻击源进行分叉的项目的必要性。尽管 Balancer 发出了警报,但针对分叉协议的攻击仍在继续,这突显了这些分叉项目需要随时了解其来源项目的安全更新。然而,确保这些分叉项目收到及时的通知,对社区而言仍然是一个持续存在的挑战。
此外,一系列持续的攻击进一步凸显了主动进行威胁预防的重要性,这可以有效地帮助减轻潜在的损失。
参考
- [1] https://docs-v2.balancer.fi/concepts/overview/basics.html
- [2] Linear Pools: https://docs-v2.balancer.fi/concepts/pools/linear.html
- [3] Composable Stable Pools
- [4] Boosted Pools
- [5] https://docs-v2.balancer.fi/concepts/pools/boosted.html#example
- [6] https://docs-v2.balancer.fi/reference/math/linear-math.html
- [7] https://docs-v2.balancer.fi/reference/math/stable-math.html



