Ethernaut: 22. Dex - Writeups

thispost
5 min readAug 12, 2021

--

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.

Listing 1 - Player’s balance of token1.
Listing 2 - Player’s balance of token2.

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)));
}
Listing 3 - Swap token1 to token2 using 1:1 price ratio for the first time.

We can add liquidity using, for example, token1 for 10.

Listing 4 - Add token1 as liquidity 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.

Listing 5 - token2/token1 price after add token1 as liquidity.
Listing 6 - the current balance of contract each token after add token1 as liquidity.

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.

Listing 7 - Approve to Dex.

3. Add MAL token to the Dex contract via add_liquidity().

Listing 8 - Add MAL to the Dex contract.

4. Now, we can use 100 MAL to swap 100 token2 indicating from get_swap_price().

Listing 9 - 100 MAL can exchange for 100 token2.

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.

Listing 10 - swap 100 MAL for 100 token2.
Listing 11 - token2 is drained.

--

--