返回博客

#3:KyberSwap事件:通过极其精妙的计算,巧妙利用舍入误差

Code Auditing
2024年2月10日
阅读约 7 分钟

2023年11月23日,KyberSwap 遭遇了一系列攻击,总损失超过4800万美元。根本问题源于 KyberSwap 再投资过程中错误的舍入方向,这导致了不正确的 Tick 计算,并最终导致流动性重复计算。

我们发布了一份详尽的报告:“又一起精度损失的悲剧:KyberSwap 事件深度分析”,其中深入探讨了此次事件的细节。为了更深入地理解,我们鼓励您阅读完整的分析。下面,我们对此次事件进行一个简要的介绍,并将其列为 2023 年十大安全事件之一。

背景

KyberSwap 是一个去中心化的自动做市商(CLAMM)平台。为了满足集中流动性的市场需求,KyberSwap Elastic 在 Uniswap V3 的基础上推出,并进行了一些改进,包括再投资曲线,以实现流动性提供收益的自动复投。

1. Tick 和平方根价格

Uniswap V3 类 CLAMM 中的 Tick 用于离散地标记价格,以便 LP 可以在固定范围内提供流动性,而不是整个范围(因此称为“集中”)。

流动性可以放置在任意两个 Tick 之间(不一定是相邻的),即一对 Tick 索引(一个下限 Tick 和一个上限 Tick)。具体来说,每个 Tick 的价格(在整数索引 i 处)定义如下:

实际上,使用的是 平方根价格(表示为 sqrtPsqrtPrice):

也可以根据当前的平方根价格计算当前的 Tick:

显然,对于给定的 Tick,只有一个平方根价格被计算出来,但多个平方根价格可能指向同一个 Tick。更详细的解释,请参考 Uniswap V3 和 KyberSwap 的文档。

2. 再投资曲线

基于 Uniswap V3 的 CLAMM 在 LP 费用的池子利用率和再投资所需的高昂 Gas 费用方面存在不足。因此,KyberSwap 采用了 再投资曲线 来解决这个问题。

再投资曲线的关键在于,每次互换所收集的费用作为“再投资流动性”累积到池子中,范围是无限的。再投资代币会被铸造给 LP,并相应地分配累积的再投资流动性。此外,再投资流动性也参与互换和价格计算过程。

对应上述计算的代码显示在以下代码片段的 computeSwapStep 函数中,对应于 pool

需要注意的是,由于再投资流动性的存在,该函数中的 liquidity 是两个组成部分的和:baseL 表示基础流动性,reinvestL 表示累积的用于再投资的流动性。

3. KyberSwap 中的互换

前面讨论的 KyberSwap 池的 swap 函数的实现可以抽象为下图:

与 Tick 计算相关的关键逻辑位于互换的 while 循环内,如蓝色矩形所示。具体来说,主要逻辑涉及 computeSwapStep 函数和 _updateLiquidityAndCrossTick 函数。前者计算给定互换的输入和输出金额以及 nextSqrtP 等关键状态,后者处理 穿越 Tick 的情况。

传统上,当价格上涨时,我们称之为 Tick 向右/向上移动;否则,我们称 Tick 向左/向下移动。

为了更好地理解稍后将讨论的漏洞,探索 computeSwapStep 函数的相关代码逻辑至关重要,如下所示:

具体而言,calcReachAmount 函数用于计算目标价格 targetSqrtP 所需的输入代币(第 50-57 行)。如果 usedAmount 大于 specifiedAmount,则 Tick 未被穿越,并且 nextSqrtPdeltaL(即 delta 流动性,第 59-62 行)计算得出。deltaL 使用 estimateIncrementalLiquidity 函数确定,最终价格 nextSqrtP 使用 calcFinalPrice 函数计算(第 70-79 行)。如果需要较少的输入,nextSqrtP 会被设置为下一个 Tick 的价格,但此情况在攻击中未使用。

上述步骤清楚地表明,如果 Tick 未被穿越,computeSwapStep 返回的 nextSqrtP 不应大于下一个 Tick 的 sqrtP。然而,由于价格对流动性(基础流动性和 delta 流动性)和精度损失的依赖性,攻击者能够操纵 nextSqrtP 使其变大,而 Tick 未被穿越。

漏洞分析

根本原因在于 SwapMath 合约(由 computeSwapStep 函数调用)的 delta 流动性计算(即 estimateIncrementalLiquidity 函数)中错误的舍入方向导致了错误的 Tick 计算。这反过来又会不正确地影响后续的 Tick 计算。

有趣的是,在检查第 188 行的注释(由蓝色矩形突出显示)时,我们发现 deltaL 本应向上舍入,以便向下舍入 nextSqrtP。然而,由于在第 189 行使用了 mulDivFloor 函数,deltaL 被错误地向下舍入。因此,nextSqrtP 被不准确地向上舍入。

攻击分析

攻击者发起了一系列攻击交易,每笔交易都耗尽了多个池子。为简单起见,以下讨论基于 攻击交易 中的第一次攻击。

核心攻击逻辑由以下六个步骤组成:

  1. 通过 AAVE 的闪电贷借入 2000 WETH。

  2. 在受害者池子 0xfd7b 中,用 6.850 WETH 兑换 6.371 frxETH。此步骤用于将当前 Tick 和 currentSqrtP 推入当前没有流动性的位置。

  • currentSqrtP 似乎是攻击者随机选择的,并且互换恰好在该价格停止。
  • 此步骤后,基础流动性(baseL)为零,但再投资流动性(reinvestL)非零。
  1. 向池子添加流动性,然后移除部分流动性。此步骤用于将范围和总流动性控制到所需的数量。
  • Tick 范围是根据 currentSqrtP 选择的。
  • 攻击所需的流动性可以从 Tick 范围推导出来,尽管相应的计算逻辑需要进一步探讨。
  1. 在池子中用 387.170 WETH 兑换 0.06 frxETH。此步骤用于操纵当前 Tick,使 nextTick == currentTick。具体来说,步骤 4 中的互换巧妙地欺骗池子,使其认为 Tick 111,310 未被穿越。然而,实际上,currentSqrtP 确实大于 Tick 111,310 的 sqrtP
  1. 在池子中用 0.06 frxETH 兑换 396.244 WETH。注意,互换方向与上一步相反。在此步骤中,流动性被双重计算,使互换有利可图,并最终耗尽池子。
  1. 偿还闪电贷,并收获 6.364 WETH 和 1.117 frxETH。

如需深入研究,请参阅我们的 全面分析,其中包含更多计算和图示。

总结

此次事件的根本问题源于 KyberSwap 再投资过程中不正确的舍入,导致了不准确的 Tick 计算,并最终导致流动性重复计算。此次事件凸显了 DeFi 协议中精度损失问题的复杂性和隐蔽性,对整个社区构成了严峻挑战。

这次 2023 年的攻击因其复杂性而脱颖而出,具有异常精细的计算,并成为许多精度相关安全事件的典型例子,这些事件严重考验了社区。此外,经过与当局的广泛协商,攻击者发表了一份挑衅性的公开声明,要求完全控制该协议。

阅读本系列的其余文章:

订阅最新动态
约940万美元损失:Injective、Aquifer遭利用攻击 | BlockSec Weekly
Security Insights

约940万美元损失:Injective、Aquifer遭利用攻击 | BlockSec Weekly

过去一周(2026/08/31-2026/09/06),Injective、Solana、Ethereum和Flow EVM上发生四起安全事件,损失约940万美元。最大的是Injective漏洞,保险基金标识符与二元期权市场标识符冲突,结算路径未比较其面额,损失约480万美元;Solana上的Aquifer因交换路径调用未验证的调用者提供的Token Program,损失约247万美元;Ethereum上的Notional Finance V1因未检查的uint128转换将债务估值为零,损失约173万美元。Flow EVM上的Ankr FLOW因质押入口跳过暂停保护并按过时比率铸币,损失约41万美元。

超越智能合约:Web3中的域名与DNS运营安全
Security Insights

超越智能合约:Web3中的域名与DNS运营安全

合约审计只审合约本身。我们对DefiLlama TVL前100协议背后的100个域名进行了8项基于SEAL的DNS和注册商检查,共800项检查,只有一个域名全部通过。以下是大多数项目缺失的四项关键控制措施,以及它们在用户入口处为何至关重要。

Web3攻击面:渗透测试概览

Web3攻击面:渗透测试概览

加密机构承载所有传统攻击面,并叠加资金处理链。本文提供系统的实用抽象:应用、授权与签名、区块链交互、基础设施四组件模型,说明各自职责、代表实现及继承的攻击面,并将web3特有覆盖细分为五大攻击面:生产与自动化运维、签名意图、审批与提现链、资金逻辑及链上交易与已部署合约。

Web3 最佳安全审计方

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

BlockSec 审计