1. 문제

DexTwo 컨트랙트가 가지고 있는 token1과 token2의 잔고를 모두 탈취(drain) 해라

나는 token1과 token2를 각각 10개씩 소유하고 있으며,
DEX 컨트랙트는 각각 100개씩 가진 상태로 시작한다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import "openzeppelin-contracts-08/access/Ownable.sol";

contract DexTwo is Ownable {
    address public token1;
    address public token2;

    constructor() {}

    function setTokens(address _token1, address _token2) public onlyOwner {
        token1 = _token1;
        token2 = _token2;
    }

    function add_liquidity(address token_address, uint256 amount) public onlyOwner {
        IERC20(token_address).transferFrom(msg.sender, address(this), amount);
    }

    function swap(address from, address to, uint256 amount) public {
        require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
        uint256 swapAmount = getSwapAmount(from, to, amount);
        IERC20(from).transferFrom(msg.sender, address(this), amount);
        IERC20(to).approve(address(this), swapAmount);
        IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
    }

    function getSwapAmount(address from, address to, uint256 amount) public view returns (uint256) {
        return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
    }

    function approve(address spender, uint256 amount) public {
        SwappableTokenTwo(token1).approve(msg.sender, spender, amount);
        SwappableTokenTwo(token2).approve(msg.sender, spender, amount);
    }

    function balanceOf(address token, address account) public view returns (uint256) {
        return IERC20(token).balanceOf(account);
    }
}

contract SwappableTokenTwo is ERC20 {
    address private _dex;

    constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)
    ERC20(name, symbol)
    {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
    }

    function approve(address owner, address spender, uint256 amount) public {
        require(owner != _dex, "InvalidApprover");
        super._approve(owner, spender, amount);
    }
}

2. 문제 분석

function swap(address from, address to, uint256 amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint256 swapAmount = getSwapAmount(from, to, amount);
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
}
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");

Dex 문제와 다르게 DexTwo 문제에서는 swap() 함수는 위 require 코드가 빠져있다.

따라서 DexTwo 문제에서는 token1, token2 를 따로 체크하는 코드가 없기 때문에 임의의 Token을 만들어 swap() 함수를 동작시켜도 정상적으로 동작한다.

이를 이용해 새로운 ERC20 Token을 만들어 swap() 함수를 동작시키면 된다.

3. 문제 해결 과정

Step 1 - token1 → token2

나의 AttackToken 100개를 DEX에게 주고, DEX의 token1 100개를 나에게 전송한다. (AttackToken은 내가 만든 커스텀 토큰이고, 공급량은 충분함, 처음은 1000개라고 가정)