Preface
The challenge comes with a basic Decentralized Exchange (DEX) which contains the standard ERC20 token swapping, liquidity adding, and the price calculation used to swap the token each other. There are not only 2 initial tokens that can be added as liquidity to the DEX’s pool, we can create our own token along with minting a large amount of token to ourselves in order to drain the initial token from the DEX.
***EDIT*** (6 Oct 2021): Ethernaut has revised this challenge by adding an additional require()
statement to receive only the token1 and the token2 address on swap()
.
function swap(address from, address to, uint amount) public {
require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens");
...(SNIP)...
This article’s solution is a part of 23. Dex Two, the new challenge that modifies 22.Dex (code is the same as old 22. Dex, but, the challenge’s goal is changed). So, we’ll split into the new writeups as following:
Writeups for modified 22. Dex
Writeups for 23. Dex Two
The challenge’s goal
You will be successful in this level if you manage to drain all of at least 1 of the 2 tokens from the contract, and allow the contract to report a “bad” price of the assets.
The initial token1 and token2 of the Dex contract are 100. So, we need to drain either token1 or token2 from the contract and let it has 0 amount.
Code analysis
We have 10 initial amounts per token.
The swapping price is calculated by get_swap_price()
and just using the division of the current balance from
and to
token in the contract. Suppose that on the first time, we can use 1:1 ratio for swapping token1 to token2 (or vice versa) since its initial balance is 100.
function get_swap_price(address from, address to, uint amount) public view returns(uint){
return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
}
We can add liquidity using, for example, token1 for 10.
The price is updated. 10 token1 can swap to 9 token2 since the current balance of token1 is 110 after add liquidity, but token2 is still 100. The ratio of token2:token1 is changed to 100:110 ~ 0.9. So, swap 10 token1 to token2 will receive 10*0.9 = 9 token2.
We can swap the token using swap()
. The contract will get the price from get_swap_price()
, then transfer our amount
of token to the contract.
function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swap_amount = get_swap_price(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swap_amount);
IERC20(to).transferFrom(address(this), msg.sender, swap_amount);
}
The token we need to exchange is transferred to our address at the end of the line.
function swap(address from, address to, uint amount) public {
require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap");
uint swap_amount = get_swap_price(from, to, amount);
IERC20(from).transferFrom(msg.sender, address(this), amount);
IERC20(to).approve(address(this), swap_amount);
IERC20(to).transferFrom(address(this), msg.sender, swap_amount);
}
Vulnerability analysis
Since the price of swapping is calculated from the division of the current amount of each token in the contract. We can create our own token, mint with a large amount to ourselves, and transfer it to the Dex contract via add_liquidity()
or the standard ERC20 transfer()
.
function add_liquidity(address token_address, uint amount) public {
IERC20(token_address).transferFrom(msg.sender, address(this), amount);
}
Suppose that the token we’ll create is the Malicious token (MAL), we mint a large amount of MAL to our address and transfer it to Dex contract for 100 or more and we can use our MAL to swap token1 or token2 of the Dex contract freely since we have an unlimited MAL.
Exploitation
The following scenario will be used,
- Create MAL token and mint 1M MAL to our address.
- We’ll use add_liquidity()
to transfer our MAL token to the Dex contract. So, approve an allowance of MAL for the Dex contract (spender) because it requires by transferFrom(
) in add_liquidity()
.
- Transfer MAL to the Dex contract for 100 MAL, we’ll drain token2. Currently, the Dex contract has 100 token2, the price ratio for MAL:token2 is 1:1.
- Drain token2 using swap()
.
Steps
1. Create MAL token by using Openzeppelin’s ERC20 and mint 1M of MAL to our address via _mint()
.
pragma solidity ^0.6.0;import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.1.0/contracts/token/ERC20/ERC20.sol";contract MaliciousToken is ERC20 {
constructor () public ERC20("Malicious", "MAL") {
_mint(msg.sender, 1000000 * (10 ** uint256(decimals())));
}
}
2. Approve the Dex contract.
3. Add MAL token to the Dex contract via add_liquidity()
.
4. Now, we can use 100 MAL to swap 100 token2 indicating from get_swap_price()
.
5. Swap 100 MAL for 100 token2 using swap()
. We successfully solve the challenge since the current balance of token2 in the Dex contract is 0.