更新于 2023 年 9 月 15 日:Balancer 发布了 官方事后复盘,该复盘详细描述了此次事件的完整经过,包括经验教训。这篇引人入胜的事后复盘,叙述详尽且精彩,绝对值得您花时间阅读。
从安全角度来看,此次事后复盘揭示了存在两个错误。第一个是我们报告中讨论的向下取整错误,第二个是报告中描述的发生在攻击步骤 3.6 和 3.7 的“归零供应重置费率”。Balancer 将第二个错误视为最关键的问题,第一个错误是助推因素。然而,我们认为两者对于盈利性攻击都同等重要:
-
第一个错误用于推高代币费率,是利润的根本原因。没有它,盈利将不可行。
-
第二个错误通过平衡 bb-a-代币的债务来实现攻击。如果没有它,由于 bb-a-代币的流动性差,攻击将失败,因为没有其他途径可以获取这些代币(除非攻击者以某种方式设法获得它们)。
2023 年 8 月 22 日,Balancer 公开宣布 存在影响多个增强型资金池的关键漏洞,并敦促用户立即从受影响的资金池中提取 LP。Balancer 已启动紧急缓解措施以保护大部分 TVL,但仍有部分资金面临风险。不幸的是,在 8 月 27 日,也就是五天后,我们注意到了野外发生的数起攻击。自那时以来,已有超过 212 万美元的资产被盗。
在撰写本报告时(公告发布三个多月后,我们认为此时发布是安全的),Balancer 尚未发布对该漏洞的深入分析。本报告旨在提供一个全面的分析,主要基于 其中一笔攻击交易。
主要亮点(TL;DR)
- 我们的调查表明,根本原因源于***
linear资金池中向下取整逻辑导致的价格操纵。这进而会不当地影响相应boosted资金池使用的缓存代币费率***。 - 此事件强调了及时通知已从易受攻击源分叉的项目的重要性,这确实对整个社区构成了重大挑战。
- 持续不断的攻击凸显了主动预防威胁的必要性,这可能有助于减轻未来损失。
在接下来的部分,我们将首先提供关于 Balancer 的一些基本背景信息。之后,我们将对漏洞和相关攻击进行全面分析。最后,我们将简要总结我们迄今为止观察到的攻击及其相应的利润。
0x1 Balancer 背景
Balancer V2 [1] 是一个去中心化的自动做市商 (AMM) 协议,代表着可编程流动性的灵活构建模块。与其他将代币会计与资金池逻辑相结合的 AMM 不同,Balancer 将代币会计和管理与资金池逻辑分开,从而可以通过减少大量的代币转移来提高交易效率。
Balancer 支持各种类型的资金池。每个资金池都与一个名为 BPT (即 Balancer Pool Token) 的 LP 代币相关联。基本上,BPT 的价值基于所有底层代币的总价值计算。
Balancer 支持多跳交易,也称为 batchSwap,它利用注册到 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:
Linear Pool BPT可以嵌套在另一个资金池中。这会在基础资产和外部资金池中的代币之间创建一个简单的batchSwap路径,因为交易者可以从BPT交易到Linear Pool的其中一种底层代币。 -
Composable Stable Pools:Composable Stable Pools [3] 专为预期以接近平价或已知汇率稳定交易的资产而设计。Composable Stable Pools 使用稳定数学,允许在遇到显著价格影响之前进行大额交易,从而大大提高同类或相关类交易的资本效率。
当一个资金池允许与其自身的 LP 代币进行交易时,它就是可组合的。将其 LP 代币放入其他资金池(或“嵌套”)可以轻松地从嵌套资金池代币与外部资金池代币进行
batchSwap。 -
Boosted Pools:
Boosted资金池 [4] 旨在提高大型资金池闲置流动性的资本效率。Boosted资金池实际上是其他资金池的一个子类。例如,一个boosted资金池可以构建在linear资金池之上。Boosted Pools 的设计宗旨是通过使用户能够为常用代币提供交易流动性,同时将闲置代币转发给外部协议,从而实现高资本效率。这使得流动性提供者除了收取交易费用外,还能获得 Aave 等协议的好处。
0x1.2 易受攻击的增强型资金池的具体示例:Balancer Boosted Aave USD
Balancer Boosted Aave USD(符号:bb-a-USD)是一个 Composable Stable Pool,它促进三种稳定币(即 USDC、USDT 和 DAI)之间的交易,同时将闲置流动性发送到 Aave。底层 linear 资金池为:
bb-a-USDC(由 USDC 和包装的 aUSDC 组成)bb-a-USDT(由 USDT 和包装的 aUSDT 组成)bb-a-DAI(由 DAI 和包装的 aDAI 组成)
具体来说,bb-a-USD 是一个 Composable Stable Pool 的集合,其中包含三个不同 linear 资金池的资金池代币,每个 linear 资金池都有一个关联的稳定币:DAI、USDC 和 USDT。下图由官方文档 [5] 提供,展示了 bb-a-USD 的结构:

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

计算可以总结如下:

这里的 tokenRate 使用以下公式计算:

_INITIAL_BPT_SUPPLY是一个常量值:。
在上述公式中,分子可以简化为 main 代币余额和 wrapped 代币余额之和,而分母是预定值(即 _INITIAL_BPT_SUPPLY)与 BPT 余额之差。
值得注意的是,所有涉及的代币余额都需要在计算之前进行名义化,因为不同代币可能具有不同的精度。具体来说,给定代币的原始余额将乘以一个相应的比例因子,该因子由 _scalingFactors 函数确定。
(1) Linear 资金池的比例因子
BPT 和 main 代币都具有常规的、恒定的比例因子。

(2) 像 bb-a-USD 这样的 Boosted 资金池的比例因子
boosted 资金池的计算有点复杂。具体来说,返回的比例因子是原始比例因子(例如 1e18)与代币费率的乘积,该费率是从缓存的代币费率(如果存在)获得的。

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

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

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

此外,通过 onSwap 函数的路径在更新时会进行过期检查:

0x2 漏洞分析
根本原因在于 linear 资金池的 onSwap 函数中的向下取整逻辑造成的价格操纵。这反过来又不当地影响了 boosted 资金池使用的缓存代币费率。
具体来说,当调用 _downscaleDown 函数时,amountOut 会被向下取整。因此,如果 amountOut 和 scalingFactors[indexOut] 之间存在显著的幅度差异,_downscaleDown 函数的返回值可能会为零。

例如,如果我们在 bb-a-USDC 资金池中使用 bb-a-USDC(作为 BPT)交易 USDC(作为 main 代币),当 amountOut 小于 1,000,000,000,000 时,返回值将始终向下取整为零。这将增加 bb-a-USDC 的余额,因为它会被视为单方面增加了 bb-a-USDC 的流动性。
因此,如果 BPT 是交易使用的代币,则根据计算费率的公式,其费率将上升,因为分子保持不变而分母减小。这个错误可能被利用导致(巨大的)价格差异。
0x3 攻击分析
攻击交易 包括以下攻击步骤:
- 通过 Aave 的 Flashloan 借入 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 代币兑换成相应的底层稳定币:
- 在
bb-a-DAI资金池中 139,431bb-a-DAI-> 141,127DAI - 在
bb-a-USDC资金池中 15,628bb-a-USDC-> 15,685USDC - 在
bb-a-USDT资金池中 248,868bb-a-USDT-> 253,461USDT
- 在
-
偿还闪电贷,最终利润为:
- 114,324
DAI - 253,461
USDT - 0.970495
aUSDC
- 114,324
值得注意的是,攻击者在步骤 2 中从 bb-a-USDC 资金池中消耗了 USDC 和 aUSDC,这将使步骤 3 中的价格操纵更容易,即攻击者只需要关注 USDC 和 bb-a-USDC。
这里的步骤 3 起着关键作用。现在让我们深入研究此步骤的细节,以弄清楚攻击者为何能够获利。具体来说:
- 步骤 3.1 用于从
bb-a-USDC资金池中消耗USDC并获得bb-a-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 的公式。由于步骤 2 中已耗尽 aUSDC 的余额,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 中增加,然后在步骤 3.7 中 bptSupply 变为零。通过这样做,可以使用接近 1:1 的汇率将 USDC 兑换成 bb-a-USDC。

0x4 攻击和利润摘要
截至本文撰写时,我们已观察到野外发生了数十起攻击,造成损失超过 212 万美元。总而言之,这些攻击是由三个不同的账户执行的,如下所示:

Balancer 由于此漏洞损失总计约 100 万美元。在 Balancer 首次遭受攻击不到 12 小时后,其分叉协议Beethoven X 也遭受了类似的攻击,估计损失约 110 万美元。**Beethoven X 的损失甚至比 Balancer 更大!此次安全事件造成的累计损失总计约 212 万美元。
所有攻击交易的完整列表已收集在我们准备的文档中。请参考该文档以获取更详细的信息。
关于攻击者的一些观察
通过分析每个网络的交易,我们发现Fantom 上的攻击交易痕迹与以太坊和 Optimism 上的交易痕迹存在显著差异。
具体来说,除了关键函数中的明显差异外,Fantom 上的攻击者还利用了两种独特的技巧来避免被 MEV 机器人抢跑。此外,Fantom 攻击中使用的资金是在攻击发生前 163 天准备好的。
从上述观察中,我们可以推断:
- 至少有两位不同的攻击者参与其中。
- Fantom 上的攻击者是一位经验丰富的惯犯。
0x5 结论
总而言之,这是一个微妙的漏洞,根源在于向下取整逻辑。然而,利用此漏洞并非易事。具体来说,攻击者通过利用 linear 资金池中的向下取整问题,成功地提高了缓存代币费率,从而操纵了相应 boosted 资金池中的代币价格。
此次事件也强调了及时通知那些从易受攻击源分叉的项目的重要性。尽管 Balancer 已经发出警告,但针对分叉协议的攻击仍在继续,这凸显了这些分叉项目需要及时了解其源项目安全更新的必要性。然而,确保这些分叉项目及时收到通知仍然是社区面临的一个持续挑战。
此外,持续不断的攻击系列凸显了主动预防威胁的重要性,这可以有效地帮助减轻潜在损失。
参考
- [1] https://docs.balancer.fi/concepts/overview/basics.html
- [2] Linear Pools: https://docs.balancer.fi/concepts/pools/linear.html
- [3] Composable Stable Pools
- [4] Boosted Pools
- [5] https://docs.balancer.fi/concepts/pools/boosted.html#example
- [6] https://docs.balancer.fi/reference/math/linear-math.html
- [7] https://docs.balancer.fi/reference/math/stable-math.html



