黑客日记 - 第 42 篇 - ImmuneBytes

本文分析了 Euler Finance 于 2023 年 3 月 13 日遭受的攻击事件,攻击者利用 Euler Finance 在 donateToReserves() 函数中缺少流动性检查的漏洞,通过闪电贷和自清算,从 Euler Finance 获利约 1.97 亿美元。文章深入剖析了漏洞原理、攻击过程,并提出了防止类似事件再次发生的建议。

2025年6月16日

清算自己并全身而退——2023年3月13日 Euler Finance 被攻击事件

1. 场景设定:

  • 协议
    • Euler Finance 借贷市场 (主网)
  • 区块高度
    • 16 822 133
  • 主要攻击者 EOA
    • 0xb66cd966670d962c227b3eaba30a872dbfb995db
    • 0xb2698c2d99ad2c302a95a8db26b08d17a77cedd4
  • 主要漏洞利用合约
    • 攻击者: 0xeBC29199C817Dc47BA12E3F86102564D640CBf99
  • 漏洞利用交易
    • 0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d
  • 总收获: DAI、wBTC、wstETH、USDC 共计 $197 000 000
  • 完整 PoC: 如需查看,请参考 github post-martem

漏洞利用的黎明

UTC 时间 08:50:59,哈希值为 0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d 的交易

在区块 16 822 133 中落地。一个单独的外部所有账户 0xb66cd966670d962c227b3eaba30a872dbfb995db 从 Aave 借了 3000 万 DAI,直接将其导入 Euler 的借贷池,然后带着价值约 1.97 亿美元的抵押品走了出来。新闻媒体后来将其总结为闪电贷攻击。

有点正确,但这个标签掩盖了最重要的事实:小偷既没有利用预言机,也没有通过重入漏洞溜走。他们只是使用了 Euler 七个月前添加的公共函数 donateToReserves(),并依靠 Euler 自己的清算引擎来完成这项工作。

2. 漏洞剖析:

机器故障快速导览

Euler 允许任何人存入资产并获得 eToken(他们对池子的债权)。用户可以根据该抵押品进行借款,收到一个随着利息增长的 dToken。由于这两种代币都是 ERC-20,用户可以递归地借给自己:存款、借款、再存入借款,以此类推,大约可将风险敞口扩大 19 倍。Euler 的风险引擎会跟踪一个健康因子;如果它跌破 1,则该账户可以以很大的折扣进行清算。

2022 年 7 月,治理部门合并了一个 pull-request EIP-14。它引入了 donateToReserves(),旨在作为一个善意的按钮:鲸鱼可以燃烧掉它的一些抵押品,为其他所有人增加储备。该函数燃烧 eToken 并在内部储备计数器中添加相同的数量,但是,致命的是,它在燃烧后没有调用 checkLiquidity()。如果一个高杠杆用户按下该按钮,引擎会一直认为该账户有偿付能力,直到下一个指令,此时清算就变得合法了。Omniscia 的事后分析称之为“由疏忽造成的坏账”。

表面上看起来无辜且是一种善意的方法,donateToReserves() 在没有刷新捐赠者的健康因子的情况下燃烧了 eToken:

function donateToReserves(uint subAccountId, uint amount)
external nonReentrant
{
  _burn(msg.sender, amount); // eTokens destroyed
  reserves += amount; // protocol feels richer
  // ❌ Missing: checkLiquidity(msg.sender);
}
  • 通过让某人在债务完整的情况下删除抵押品,该函数可以在同一个 tx 中将他们的健康评分推到 < 1。
  • Euler 的软清算引擎很乐意允许任何人在高达 20 % 的折扣下清算一个抵押不足的地址。
  • 加上闪电贷,你就可以在一个区块中同时成为罪人和救世主。

3. 现场记录:

闪电贷掠夺者生活中的一个小时

以攻击者的身份写作,我那天早上的笔记本专注于区块编号 [16 822 133],(这里的时间是区块级别,而不是挂钟时间):

T – 00 s – 从 Aave V2 提取 30 000 000 DAI,如果我在同一个区块内结算,只需支付九个基点的费用。
T + 03 s – 将 20 000 000 DAI 存入 Euler;收到 19 568 124 eDAI。
T + 07 s – 循环:借入 195 681 244 DAI,再存入,再次借入,十次通过。健康因子浮动在 1.02——完全合法。
T + 10 s – 按下 donateToReserves(100 000 000 eDAI)。健康因子猛降至 0.77。警报静默;函数不检查。
T + 11 s – 从辅助合约 0xb2698c2d99ad2c302a95a8db26b08d17a77cedd4 触发清算,以 17% 的折扣购买我自己的坏账。
T + 20 s – 提取被扣押的抵押品(DAI、wBTC、wstETH、USDC),偿还 Aave 贷款加上费用,在这个资产上的净利润 ≈ 8 900 000 DAI;在矿工注意到之前,在其他四个池子上重复该模式。
T + 90 s – 将钱包余额清空到 Tornado Cash,然后等待新闻周期。

一次捐赠就打破了平衡;其他一切都是机械的清算和会计漂移。没有预言机尖峰,没有重入,没有治理延迟——只是协议逻辑被武器化来对付自己。BlockSec 的 Phalcon 仪表板在几分钟内就发出了漏洞利用标志,但交易已经完成。

4. 实验室重建:

目标:证明删除一个 require 允许清算你自己的债务以获得即时利润。

4.1 Fork 和账户

  • 在区块 16 822 130(攻击发生前三个区块)fork 主网,以便状态与攻击前匹配。
  • 模拟 EOA 0xb66cd966670d962c227b3eaba30a872dbfb995db 以保留链上许可和 nonce 顺序。

4.2 闪电贷存根

使用 Balancer 的 Vault,因为它的单资产闪电贷只需一次调用;我们只需要 wstETH 一次:

IERC20[] tokens;
tokens[0] = IERC20(DAI);uint256[] amounts;
amounts[0] = 30_000_000 ether;vault.flashLoan(this, tokens, amounts, "");

4.3 receiveFlashLoan 中的攻击流程

  1. 存入 20 000 000 DAI。
  2. Mint eToken::mint × 10 以达到 > 400 M eDAI。
  3. 通过 donateToReserves(易受攻击的调用)投放 100 000 000 eDAI。
  4. 通过从我们预先部署的辅助合约调用 Euler 的 liquidate() 来触发清算;它需要一个单独的地址,以便协议看到两个参与者。
  5. 提取被扣押的抵押品,
    1. 偿还闪电贷,
    2. 断言 DAI.balanceOf(attacker) > 8_000_000 ether。

4.4 断言集

assertGt( IERC20(DAI).balanceOf(address(this)),
8_000_000 ether, // 保守的下限
“profit too small – exploit failed”);
assertEq( EToken(eDAI).balanceOf(address(this)), 0,
“all collateral withdrawn”);

我们关心的不变量:盈利能力 ≥ 8 M DAI 且没有剩余债务——与链上满足的条件相同。

5. 为什么 PoC 选择这些确切的数字:

  • 20 M / 30 M 拆分 镜像了真实的闪电贷,因此健康评分计算与历史价格一致。
  • 10× mint 循环 是 Euler 记录的自抵押头寸的杠杆上限。
  • 100 M eDAI 捐赠 将健康评分推到略低于 1;捐赠更多会缩小随后的清算折扣。
  • 8 M DAI 断言 为本地 fork 中的 gas 成本留出了回旋余地(Foundry fork 有时会高估储备几个 wei)。

6. 应该阻止它的是:

  1. donateToReserves 内部的偿付能力重新检查

    1. 从字面上看,一个 require(health >= 1) 就会使捐赠失败。

    2. 不变量模糊测试 断言每次外部调用后 totalCollateral ≥ totalDebt。

    3. 一个 30 秒的 Echidna 运行会立即标记捐赠者路径。

  2. 动态杠杆限制 — 限制每个区块的递归 mint 深度;攻击者在捐赠前需要九个循环。
  3. 连接到监控的实时断路器(BlockSec Phalcon 在几分钟内发出警报),当坏账飙升 > x % 时暂停清算。

7. 结束页:

看起来无害的权限(“燃烧我自己的抵押品,帮助协议!”)可能比奇异的重入更致命。如果一个函数改变了用户的余额,总是在同一个调用的最后重新计算偿付能力——没有例外,没有神圣的奶牛。

参考文献

(全文引用的区块号、tx 哈希、代码库和分析)

日记关闭——直到下一次违规。

  • 原文链接: blog.immunebytes.com/202...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
ImmuneBytes
ImmuneBytes
Stay Ahead of the Security Curve.