返回博客

#8 Bunni 事件:反复小额提现将舍入误差放大成 840 万美元资金流失

Code Auditing
2026年2月12日
阅读约 8 分钟

2025年9月2日,Bunni V2 协议遭受了一次复杂的攻击 [1]。攻击者利用其流动性核算机制中的一个关键漏洞,从两个流动性池中提取了约840万美元:以太坊上的 USDC/USDT 池 [2]和 Unichain 上的 weETH/ETH 池 [3]。

根本原因是协议在移除流动性时更新闲置池余额时存在的一个舍入误差。该误差导致合约中总流动性的显著低估,从而在理论流动性与实际流动性之间产生了可被利用的差异。攻击者随后执行了一次精确的三明治攻击,从这一差异中获利。

此次事件直接给 Bunni 协议造成了严重的财务损失,该协议随后于2025年10月23日宣布破产 [4]。

背景

Bunni V2 是一个构建在 Uniswap V4 之上的自动化做市商(AMM)协议。它通过 hook 机制实现其核心逻辑,并在 Uniswap V3 的集中流动性算法基础上进行了创新,旨在为流动性提供者(LP)提供更高的资金效率 [5]。

具体而言,该协议主要通过再抵押(Rehypothecation)功能和再平衡(Rebalancing)机制来提升 LP 的收益。前者将流动性分配给外部产生收益的协议,在保障基础流动性的同时获取额外的外部收益。后者持续优化流动性在各价格区间的分布,提高资金的活跃利用率以增加手续费收入。这两种机制构成了该协议在基础集中流动性模型之上的核心创新。

再抵押(Rehypothecation)

为了提升流动性提供者的收益,Bunni V2 采用了再抵押策略。该策略将资金分配到不同的位置:

  • rawBalance(原始余额): 池中某代币储备的一部分直接存储在 Uniswap V4 的 contract PoolManager 中。这部分作为促成兑换的即时可用流动性。
  • reserves(储备): 剩余部分存入指定的 ERC4626 金库。这使用户能够在这些资产上获得额外的外部收益。

因此,一个池的总资产定义为:池资产 = rawBalance + reserves 的底层资产数量。

再平衡(Rebalancing)

为增加手续费收入,Bunni V2 实现了一种再平衡机制,该机制会监控时间加权平均价格。当价格变化超过某一阈值时,流动性会根据流动性分布函数(LDF)在不同价格区间内重新分配。

这种重新分配可能会改变 LDF 所要求的代币比例,从而使某一代币出现盈余。该盈余被定义为闲置余额(idle balance)。

因此,流动性被分为两部分:

  • 活跃余额(Active Balance):由 LDF 分配的、参与流动性计算的部分。
  • 闲置余额(Idle Balance):未用于活跃流动性的盈余部分。

因此,池资产 = 活跃余额 + 闲置余额。

关键函数:流动性计算与移除

此次攻击利用了两个关键函数:queryLDF() 和 withdraw()。函数 queryLDF() 用于计算兑换时池的流动性,而函数 withdraw() 允许用户按比例移除流动性。

函数 queryLDF()

由于再抵押策略的存在,底层资产的数量是动态变化的,Bunni V2 并不存储一个固定的“总流动性”数值。相反,该协议提供了函数 queryLDF(),用于在发生兑换时实时获取流动性 [6]。该函数的执行过程包含以下四个步骤:

  1. 查询流动性密度:

    1. 调用流动性密度函数 ldf.query(),获取当前价格区间之外的流动性密度。

    2. 调用 LiquidityAmounts.getAmountsForLiquidity(),获取当前区间内的密度。

    3. 计算 token0 和 token1 在两个方向上的总流动性密度,分别记为 totalDensity0 和 totalDensity1。

    值得注意的是,函数 LiquidityAmounts.getAmountsForLiquidity() 采用向上取整,以确保计算出的代币数量在保守情况下不低于理论值。

  2. 计算可用余额

    用于流动性计算的可用余额分别记为 balance0 和 balance1。闲置余额会从相应代币的总余额中扣除,从而排除不参与流动性计算的资金。

    在本次攻击中,池中的闲置资金由 token0 构成,其计算公式为:

    • balance0=rawBalance0+reserve0−idleBalancebalance0 = rawBalance0 + reserve0 - idleBalance

    • balance1=rawBalance1+reserve1balance1 = rawBalance1 + reserve1

  3. 估算有效流动性

    1. 根据每个代币的实际可用余额(balance0 或 balance1)以及计算出的总密度(totalDensity0 或 totalDensity1),估算每种代币所能支撑的流动性。

    2. 取两者估算值中较小的一个,作为最终有效的总流动性。

    公式如下:

    L=min(balance0totalDensity0,balance1totalDensity1)L= min(\frac{balance0}{totalDensity0},\frac{balance1}{totalDensity1})

  4. 计算活跃余额

根据确定的总流动性,协议计算出实际可用于交易的代币数量,即定义为活跃余额(Active Balance)。

函数 withdraw()

Bunni V2 提供函数 withdraw() 用于移除流动性。用户按其在池总资金中所占份额的比例移除流动性。协议会按相同比例更新 rawBalance、reserves 和 idleBalance。调整公式如下:

(rawBalance,reserves,idleBalance)=(rawBalance,reserves,idleBalance)×(1−sharestotalSupply)(rawBalance, reserves, idleBalance) \\= (rawBalance, reserves, idleBalance) \times (1-\frac{shares}{totalSupply})

其中:

  • shares 是用户移除的流动性份额数量;
  • totalSupply 是该池流动性代币的总供应量。

漏洞分析

该漏洞源于函数 withdraw() 在计算闲置余额调整数额时采用了向下取整(即舍去小数)的方式。这导致闲置余额被高估。

回顾可用余额公式,balance=rawBalance+reserve−idlebalancebalance = rawBalance + reserve - idle balance。闲置余额被高估会直接导致用于流动性计算的可用余额(balance0)被低估。因此,估算出的有效总流动性也随之被低估。根据 Bunni 的事故事后分析报告(Exploit Post Mortem)[7],在流动性计算中采用这种取整方向是有意为之的。较低的计算流动性数值会在兑换时导致更高的价格冲击。

这种设计依赖于一个关键假设:两种代币之间的余额比例保持相对平衡。在流动性充足的正常情况下,分别针对每种代币估算出的总流动性值通常较为接近,因此舍入误差的影响是有限的。然而,当携带闲置余额的代币的可用余额变得极低时,这一缺陷便会显现。在这种情况下,向下取整所产生的误差会被显著放大。

攻击者利用了这一漏洞,通过一系列小额提取操作,将 token0 的可用余额从28 wei 向下取整降至4 wei。这一降幅远远超过了实际销毁的流动性份额所对应的比例。同时,token1 的可用余额则维持在相对正常的水平。这种失衡创造了一个显著的套利窗口。下一章将提供详细的数值分析。

攻击分析

以以太坊上的交易 [2] 为例,攻击者执行了一次三阶段的攻击:

  • 在第一阶段,攻击者进行价格操纵,大幅消耗 USDC 的可用余额(token0)。这为放大后续的舍入误差创造了必要的初始条件。
  • 在第二阶段,攻击者通过一系列小额提取操作实施了核心漏洞利用,导致协议低估了池的实际流动性。
  • 在第三阶段,攻击者执行了两次方向相反的兑换,以套利协议低估的流动性与池实际流动性之间的差异,最终获取利润。

第一阶段:操纵价格并降低目标代币余额

攻击者执行了三笔兑换交易,操纵 USDC(token0)相对于 USDT(token1)的价格,将其从初始的 tick = -1 推动至 tick = 5000。其主要目的是消耗池中 USDC 的活跃余额,将其降低到28 wei 这一极低水平。这为在下一阶段放大随后的舍入误差创造了必要的初始条件。

第二阶段:利用提取操作放大流动性差异

攻击者通过函数 withdraw() 发起了44次小额提取操作。由于该函数在更新 idleBalance 时采用向下取整,协议的闲置余额被高估。这进一步导致函数 queryLDF() 中 USDC 的可用余额被低估。经过这些反复操作后,USDC 的可用余额被异常压低,从28 wei 降至4 wei。这一实际降幅达85.7%,远远超过所移除流动性份额对应的理论比例(即8.998105442969973e-07%)。此时,池中基于 USDC 估算的流动性被严重低估。

第三阶段:执行套利并实现利润

随后,攻击者执行了两次方向相反的兑换,构成了类似于三明治攻击的操作。

步骤1:攻击者用大量 USDT 兑换 USDC。此时,由于 USDC 余额被低估,内部流动性计算被严重低估。这笔大额兑换将价格推至极端水平,使 tick 从5,000移动到839,189。

步骤2: 在形成极端价格之后,攻击者立即进行反向操作,将一部分 USDC 换回 USDT。由于此时池的价格已严重偏离,函数 queryLDF() 针对 USDC 流动性密度返回的数值降至1。这使得基于 USDC 估算出的流动性值大于基于 USDT 估算出的值。

根据协议“取较小值”的逻辑,总流动性此时由 USDT 余额决定。这导致计算出的流动性瞬间从被低估的状态恢复到正常水平,出现骤增。攻击者利用了这一变化,用极少量的 USDC 兑换到大量的 USDT,从而完成套利并实现利润。

总结

此次事件最终是由移除流动性时调整闲置余额所产生的舍入误差引起的。尽管这种向下取整的设计最初是作为流动性计算中的一种安全策略而设计的,但它未能充分考虑关键的边界条件。具体而言,当代币余额严重失衡时,舍入误差会被非线性地放大。

此次事件揭示了复杂 DeFi 协议中多个模块之间存在的耦合风险。即使各个组件的取整规则在设计上是保守的,如果缺乏对整个系统一致的安全验证,仍可能在特定情况下产生可被利用的关键漏洞。

参考资料

  1. https://x.com/bunni_xyz/status/1962833866277744953

  2. https://etherscan.io/tx/0x1c27c4d625429acfc0f97e466eda725fd09ebdc77550e529ba4cbdbc33beb97b

  3. https://uniscan.xyz/tx/0x4776f31156501dd456664cd3c91662ac8acc78358b9d4fd79337211eb6a1d451

  4. https://x.com/bunni_xyz/status/1981160279871558114

  5. https://docs.bunni.xyz/docs/v2/overview

  6. https://github.com/Bunniapp/bunni-v2/blob/2b303b8c1b9f8afbb169d62ba52da93d6d2171fe/src/lib/QueryLDF.sol#L40

  7. https://blog.bunni.xyz/posts/exploit-post-mortem/


关于 BlockSec

BlockSec 是一家全栈区块链安全与加密合规服务提供商。我们打造的产品与服务能够帮助客户在协议与平台的全生命周期中进行代码审计(包括智能合约、区块链和钱包)、实时拦截攻击、分析事件、追踪非法资金,并满足反洗钱/反恐融资(AML/CFT)合规义务。

BlockSec 已在多个顶级会议上发表区块链安全相关论文,报告了多起 DeFi 应用的零日攻击,成功阻止了多起黑客攻击,挽回超过2000万美元的损失,并为价值数十亿美元的加密资产提供了安全保障。

订阅最新动态
损失约557万 USD:X100 Freezer、NEM 遭攻击|BlockSec周报
Security Insights

损失约557万 USD:X100 Freezer、NEM 遭攻击|BlockSec周报

本周报告涵盖 2026年10月19日–2026年10月25日 期间以太坊和 BNB Chain 上发生的两起安全事件。报告分析了利用闪电贷资金接管 X100 Freezer 治理权的事件,以及影响 BNB Chain 上一个 NEM 代币流动性池的税费结算缺陷。报告统计范围内的估计损失合计约 557万 USD:其中,具有代表性的 X100 交易造成约 ~475万 USD 的损失,NEM 流动性池事件造成约 ~82.0万 USD 的损失。该合计金额不包括另行报告的后续 X100 资金提取。

Autheo Network、UNCX 遭黑客攻击,损失约387万 USD | BlockSec 周报
Security Insights

Autheo Network、UNCX 遭黑客攻击,损失约387万 USD | BlockSec 周报

本周报告涵盖 2026年10月12日–2026年10月18日 期间的 3 起安全事件,估计总损失约为 387万 USD。Autheo Network 因私钥泄露损失约 176万 USD,Qubic Wallet 因种子生成机制薄弱损失约 130万 USD;UNCX Network 在 Ethereum、BNB Chain、Base 和 Robinhood Chain 上的 Smart Token Vesting V2 合约因会计核算错误损失约 80.7万 USD。重点分析的 UNCX 事件说明,有缺陷的总份额计算将按不同代币与份额兑换比率创建的归属批次混合计算,从而使人能够从共享资金池超额提取。

损失约54.2万美元:MakerDaoBot 漏洞利用事件|BlockSec 周报
Security Insights

损失约54.2万美元:MakerDaoBot 漏洞利用事件|BlockSec 周报

本报告涵盖2026/10/05至2026/10/11以太坊上的1起安全事件,估计损失约54.2万美元。攻击合约部署恶意适配器,并将其传入缺乏访问控制的MakerDaoBot函数,使机器人授予该适配器操作其内部MakerDAO余额的权限。适配器结算了4笔旧版ETH-A拍卖,将200单位ETH-A抵押品兑换为WETH;攻击合约随后将其解包,并向攻击者转出200 ETH。

Web3 最佳安全审计方

在上线之前验证设计、代码与业务逻辑,对标业内最高安全标准。

BlockSec 审计