Back

Dex

Dex

The goal of this level is for you to hack the basic DEX contract below and steal the funds by price manipulation. You will start with 10 tokens of token1 and 10 of token2. The DEX contract starts with 100 of each token. 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.

What is a dex?

A DEX(decentralized exchange) enables the exchange of digital assets in a decentralized way, without the need of intermediaries such as centralized exchanges.

Vulnerable Contract

  • Expand to see Dex.sol
    // SPDX-License-Identifier: MIT
    pragma solidity ^0.8.0;
    
    import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
    import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
    import "@openzeppelin/contracts/access/Ownable.sol";
    
    contract Dex is Ownable {
      address public token1;
      address public token2;
      constructor() {}
    
      function setTokens(address _token1, address _token2) public onlyOwner {
        token1 = _token1;
        token2 = _token2;
      }
      
      //Owner set's liquidity for the tokens
      //1. From the token address + amount get's send to the DEX contract (this)
      function addLiquidity(address token_address, uint amount) public onlyOwner {
        IERC20(token_address).transferFrom(msg.sender, address(this), amount);
      }
    
      //People can swap the tokens one addresss to the other
      function swap(address from, address to, uint amount) public {
        require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); //The swap has to be the same amount
        require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); //Requires the token balance of the msg.sender to be => than amount
        uint swapAmount = getSwapPrice(from, to, amount); //price get's fetched and set to swapAmount
        IERC20(from).transferFrom(msg.sender, address(this), amount);
        IERC20(to).approve(address(this), swapAmount);
        IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
      }
    
      function getSwapPrice(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 {
        SwappableToken(token1).approve(msg.sender, spender, amount);
        SwappableToken(token2).approve(msg.sender, spender, amount);
      }
    
      function balanceOf(address token, address account) public view returns (uint){
        return IERC20(token).balanceOf(account);
      }
    }
    
    contract SwappableToken is ERC20 {
      address private _dex;
      constructor(address dexInstance, string memory name, string memory symbol, uint256 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);
      }
    }
    

AMM (Automated Market Makers)

  • In AMM there are no ‘order books’

X * Y = K

=

amount token1 * amount token2 = Total Amount

This is the formula that drives decentralized exchanges. Where X = token1, Y = token2 and K = price of the pair.

In this case only the owner can add liquidity into the smartcontract. In UniswapV2 people can add liquidity to earn money.

Swapping:

In this smartcontract people don’t trade with eachother, they trade with the DEX. They swap token1 with token2. So if you have in our case 10 token1, and in the current liquidity there are 100 tokens of token2

dex-pic1.png

If we want to swap 10 of our token2 for token1 we use the swap() function:

  • address from = address token2
  • address to = address token1
  • uint amount = 10
  function swap(address from, address to, uint amount) public {
    require((from == token1 && to == token2) || (from == token2 && to == token1), "Invalid tokens"); //The swap has to be the same amount
    require(IERC20(from).balanceOf(msg.sender) >= amount, "Not enough to swap"); //Requires the token balance of the msg.sender to be => than amount
    uint swapAmount = getSwapPrice(from, to, amount); //price get's fetched and set to swapAmount
    IERC20(from).transferFrom(msg.sender, address(this), amount);
    IERC20(to).approve(address(this), swapAmount);
    IERC20(to).transferFrom(address(this), msg.sender, swapAmount);
  }

the swapAmount get’s fetched from the getSwapPrice() function.

function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

the formula will be:

(amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this));

( 10 * DEX balance of token1) / Dex balance of token2 ==

( 10 * 100 ) / 100 = 10

At this moment we get 10 tokens1 for 10 tokens2

The vulnerability

The problem is that the price is determined on the / IERC20.balanceOf, which can be manipulated, at the beginning is only a small difference but over a couple of transactions the difference is huge.

function getSwapPrice(address from, address to, uint amount) public view returns(uint){
    return((amount * IERC20(to).balanceOf(address(this)))/IERC20(from).balanceOf(address(this)));
  }

POC (Proof of Exploit)

address from = token2

address to = token1

amount = 20 (token2’s)

swapPrice() = (20 * 110) / 90 = 24,4= 24 (tokens1)

So we get in this swap 24 (token1’s) for only 20 (token2’s)

Do it again:

address from = token1

address to = token2

amount = 24 (token1’s)

swapPrice() = (24 * 110)  / 86 = 30.7= 30 (tokens2)

Do this again and again until you eventually have 65 tokens of one of the two:

dex-pic2.png

Now you can do the last swap to get everything out of the contract.

dex-pic3.png

Now we have a balance of 110 and we completed the challenge.

dex-pic4.png