zkLend 漏洞复盘:揭秘1000万美元闪电贷攻击的细节与澄清误解

本博客详细分析zkLend事件,以澄清误解。

zkLend 漏洞复盘:揭秘1000万美元闪电贷攻击的细节与澄清误解

2025年2月12日,StarkNet上的借贷协议zkLend[1]因操纵其累加器机制而遭到攻击,损失约1000万美元。攻击者利用闪电贷和四舍五入漏洞人为地夸大了抵押品价值,并从协议中借入其他资产以获利。

然而,从安全角度来看,仍然缺乏详细准确的技术分析。尽管其他安全研究人员已经提供了有价值的见解,但仍存在一些误解,尤其是在攻击分析方面。zkLend后来发布的官方事后报告[2]提供了一个简化的描述,但缺乏详细的技术分析。在本博客中,我们将提供全面的审查以阐明此次事件。

主要收获(TL;DR)

  • 此次事件的根本原因在于以下三项问题的结合

    • 空市场初始化允许任意资产的存入。
    • zkLend闪电贷中的特定捐赠机制使得操纵累加器成为可能。累加器是一个全局变量,用作缩放因子,动态调整用户的抵押品余额。
    • 因截断导致的精度损失。与经典的除法精度损失不同,分母从1开始,但被膨胀到一个非常大的值,导致在销毁份额代币时出现低估。
  • 攻击者并未从其他用户的wstETH存款中获利。相反,攻击者利用漏洞操纵了抵押品余额,使用少量wstETH作为初始资本,将抵押品余额增加到超过7,000个wstETH,从而能够从市场借入其他资产。

在接下来的章节中,我们将首先提供zkLend的一些关键背景信息。随后,我们将对问题和相关攻击进行深入分析。

0x1 背景:理解zkLend的核心协议

zkLend是StarkNet上的一个借贷项目,支持诸如抵押贷款和闪电贷等常见借贷协议。让我们深入了解这两种协议的实现细节。

0x1.1 抵押贷款

抵押贷款是指用户将特定资产存入协议作为抵押品,以换取借入其他资产的过程。抵押品的价值用于确定借款能力。值得注意的是,借贷协议通常不直接存储抵押品的资产价值;相反,它们使用以下公式计算:

collateral_balance = lending_accumulator * raw_balance

具体而言,lending_accumulator是一个缩放因子,动态调整每个用户的抵押品价值,而raw_balance代表用户在该市场中持有的实际份额。raw_balance是使用lending_accumulatorcollateral_balance派生出来的。

这种设计的目的是什么? 它使协议能够有效管理抵押品价值,同时激励用户存入资产。通过将协议收益的一部分分配给抵押品提供者,lending_accumulator会增加,从而按比例同时放大所有用户的抵押品价值。

0x1.2 zkLend上的闪电贷

闪电贷是一种无抵押贷款,用户可以在非常短的时间内(通常在一个交易内)从协议中借入资产。如果借款人未能偿还贷款或满足指定条件,整个交易将被回滚,贷款不会执行。

在zkLend的闪电贷实现中,有一个独特的捐赠机制。具体来说,当用户偿还资产时,他们不仅会返回所需的最低金额,还可以贡献额外资金作为捐赠。协议会跟踪这些捐赠的资金,并相应地更新lending_accumulator。此过程在thesettle_extra_reserve_balance()函数中实现。更新lending_accumulator的公式如下:

new_accumulator = (reserve_balance + totaldebt - amount_to_treasury) / ztoken_supply

  • reserve_balance:合约中持有的总基础代币(例如wstETH)数量,包括用户捐赠的代币数量。
  • totaldebt:所有借款用户的总债务。
  • amount_to_treasury:协议的收入金额。
  • ztoken_supply:份额代币(例如zwstETH)的总供应量。当用户存入wstETH时,zkLend ztoken合约会铸造等量的zwstETH。

在理解了zkLend的核心协议后,我们将正式解释攻击者如何通过操纵lending_accumulatorraw_balance变量来操纵其抵押品资产。

0x2 攻击分析

攻击者利用zkLend合约中的以下机制和漏洞来操纵抵押品价值:

  • 操纵lending_accumulator
    • 空市场:攻击发生前,zkLend的wstETH代币市场是空的,为操纵提供了理想条件。此外,zkLend Market合约允许任何人向空市场存入任意数量的资产。攻击者存入少量资产,显著夸大了lending_accumulator的值。
    • 捐赠机制:zkLend Market合约的flash_loan()函数具有独特的捐赠机制。具体来说,当用户偿还闪电贷时,Market合约会计算返回的多余资金,并增加全局lending_accumulator变量,从而放大合约中所有用户的抵押品价值。
  • 操纵raw_balance
    • 四舍五入行为:份额代币销毁过程中的除法运算采用截断(向下取整),导致用户在提取时raw_balance的变化被低估。

通过操纵这两个变量,攻击者能够将抵押品余额增加到7,000以上wstETH,并从市场借入其他资产以获利。

0x2.1 操纵lending_accumulator变量

0x2.1.1 空市场初始化

通过审查攻击发生前Market合约的交易记录,我们可以观察到攻击者最初向wstETH Market合约存入了1 wei的wstETH。通过审查此交易的内部调用,可以清楚地看到wstETH Market合约持有0 wstETH,zwstETH的总供应量也为0

因此,我们可以确认zkLend wstETH市场没有之前的存款或借款。reserve_balanceztoken_supply都处于初始值0,lending_accumulator的初始值为1。这种空市场情况为后续攻击创造了条件,允许攻击者用极少量的wstETH大幅放大lending_accumulator

0x2.1.2 通过闪电贷操纵lending_accumulator

接下来,在此交易中,攻击者调用flash_loan()函数,借入1 wei wstETH并偿还1000 wei wstETH。多余的999 wei被视为捐赠并记录到合约的reserve_balance中。

根据计算lending_accumulator的公式,此交易导致lending_accumulator1增加到851.0

0x2.1.3 重复执行flash_loan()

攻击者总共执行了10次flash_loan()调用,每次仅借入1 wei的wstETH,但偿还了更大的金额。结果,lending_accumulator急剧增加到4,069,297,906,051,644,020(4.069 × 10^18)的惊人值,这恰好与wstETH的十进制精度相符。

0x2.2 操纵raw_balance变量

在将lending_accumulator操纵到约4.069 × 10^18后,攻击者向Market合约的deposit()函数存入了4.069297906051644020 wstETH。根据最新的lending_accumulator值,攻击合约的raw_balance变为2

0x2.2.1 首次操纵raw_balance的交易

在此交易中,攻击者调用了攻击合约的callflashloandraaan()函数。虽然此合约并非开源,但根据内部调用跟踪,可以推测此函数逻辑包含一个循环,执行以下操作:

  • 存入:攻击者将一定数量的wstETH存入市场合约。
  • 提取:攻击者提取特定数量的wstETH。

代币转账记录分析 可以观察到,攻击者存入的wstETH数量始终是lending_accumulator的整数倍,例如2倍的值(例如8.13859)的lending_accumulator

然而,提取的wstETH数量是lending_accumulator的1.5倍(例如6.10394)。

通过计算,我们可以确定提取的wstETH数量超过了存入的数量。为什么会这样?

四舍五入行为 通过查看deposit()withdraw()方法的实现,我们可以看到这两个方法涉及zwstETH的铸造和销毁。工作原理如下:

Market合约中的`mint()`函数

Market合约中的`burn()`函数

mint()burn()过程都包含缩减逻辑。缩减逻辑涉及整数除法,并采用向下取整向最接近的整数舍去),这在漏洞利用中起着关键作用。

当攻击者销毁一定数量的zwstETH时,会应用缩减逻辑。由于操纵后的lending_accumulator值异常高(约4,069,297,906,051,644,020),此除法运算导致攻击者的raw_balance仅减少1个单位,尽管销毁了超过6个zwstETH。

攻击者的raw_balance变化总结如下表:

我们可以观察到,在此交易中,攻击者反复执行存入-提取逻辑,利用withdraw()函数中的精度损失,导致raw_balance差值的低估。最终,用户的raw_balance2增加到3,获得了额外的1个单位。

0x2.2.2 后续攻击流程

随后的攻击交易遵循了与第一次攻击相同的模式:攻击者反复进行存入-提取交易以获取wstETH。

获取的wstETH被重新存入市场,进一步增加了raw_balance,导致攻击者的抵押品价值不断上升。

示例说明 我们使用以下交易作为说明。

- 总共进行了30次存入,每次存入4.069 wstETH。 - 总共进行了30次提取,每次提取6.104 wstETH。 - 根据计算,在此周期之后,攻击者成功提取了61.39 wstETH。

此外,值得注意的是,在这些攻击交易之间,调用了几个increase()方法。这些方法用于将一定数量的wstETH从攻击者账户转移到攻击合约,然后由攻击合约提供资金进行后续存入市场合约。

这些操作提高了`raw_balance`的价值,使攻击者能够继续增加抵押品价值。最终,攻击者的`raw_balance`达到**1,724**,价值为**7,015.4** ***wstETH***,这足以从市场借入**其他资产**。

0x3 利润分析

0x3.1 借入其他类型的资金

在操纵了抵押品价值后,攻击者从市场借入了其他类型的资金,并进行了以下交易(摘录):

### 0x3.2 将借入的资金桥接到Layer1 通过检查[攻击者合约](https://voyager.online/contract/0x04d7191dc8eac499bac710dd368706e3ce76c9945da52535de770d06ce7d3b26#bridgeTxns?ps=100&p=1)的桥接交易,可以观察到攻击者将部分借入的资金桥接到了Layer 1。

0x4 结论

总而言之,此次对zkLend协议的攻击凸显了去中心化借贷协议设计和安全方面的几点重要启示:

  • 市场初始化和资产存入条件: 初始阶段的空市场允许攻击者存入少量wstETH并操纵lending_accumulator,为漏洞利用获得了杠杆。确保充足的流动性基础或限制早期市场阶段的资产捐赠有助于防止类似的攻击。
  • 累加器机制正常运行的重要性: 攻击者利用flash_loan()函数中的捐赠机制来操纵lending_accumulator,从而夸大了所有用户的抵押品价值。采用基于累加器的协议应防范缩放因子被轻易操纵。
  • 四舍五入行为和精度损失: zwstETH代币销毁过程中的四舍五入问题导致精度损失和raw_balance的低估,使攻击者能够操纵raw_balance。协议应使用更高的精度或验证检查来防止此类漏洞利用。

再次强调,此次事件凸显了及时通知有关初始化和运营状态的重要性,以及主动威胁预防以减轻潜在损失。

参考文献

[1] https://zklend.com/ [2] zkLend安全事件事后报告:https://drive.google.com/file/d/10i1dh_J89tPPw7KRcmFIVM6iNrJZAyfi/view

Sign up for the latest updates