How to Build a Decentralized Exchange (DEX)

How to Build a Decentralized Exchange (DEX)

In this guide, you'll learn how to create your own simple decentralized exchange (DEX) using thirdweb.

We'll build a DEX contract that allows users to swap between a native token (like MATIC on Polygon) and an ERC20 token of your choice.

Then we'll create a simple web application that interacts with the DEX contract.

Here's an overview of what we'll cover:

  1. Creating the DEX smart contract
  2. Building the DEX web application

Let's get started!

Video Tutorial

Prerequisites

Before diving in, make sure you have the following:

  • thirdweb account
  • Your thirdweb API key (get it from your account settings)
  • Node.js installed on your machine
  • Basic knowledge of React, Next.js, TypeScript

Step 1: Create the DEX Smart Contract

We'll start by creating the DEX smart contract using thirdweb's CLI tools.

Open your terminal and run:

npx thirdweb create contract

Name your project 'dex-contract' and choose Hardhat and TypeScript.

Once created, change into the new directory:

cd dex-contract

Open the project in your code editor. In the contracts/ folder, rename Contract.sol to DEX.sol.

Update the contract code in DEX.sol (you can find it in the Contracts GitHub repo):

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@thirdweb-dev/contracts/base/ERC20Base.sol";

contract DEX is ERC20Base {
    address public token;

    constructor (address _token, address _defaultAdmin, string memory _name, string memory _symbol)
        ERC20Base(_defaultAdmin, _name, _symbol)
    {
        token = _token;
    }

    function getTokensInContract() public view returns (uint256) {
        return ERC20Base(token).balanceOf(address(this));
    }

    function addLiquidity(uint256 _amount) public payable returns (uint256) {
        uint256 _liquidity;
        uint256 balanceInEth = address(this).balance;
        uint256 tokenReserve = getTokensInContract();
        ERC20Base _token = ERC20Base(token);

        if (tokenReserve == 0) {
            _token.transferFrom(msg.sender, address(this), _amount);
            _liquidity = balanceInEth;
            _mint(msg.sender, _amount);
        }
        else {
            uint256 reservedEth = balanceInEth - msg.value;
            require(
            _amount >= (msg.value * tokenReserve) / reservedEth,
            "Amount of tokens sent is less than the minimum tokens required"
            );
            _token.transferFrom(msg.sender, address(this), _amount);
        unchecked {
            _liquidity = (totalSupply() * msg.value) / reservedEth;
        }
        _mint(msg.sender, _liquidity);
        }
        return _liquidity;
    }

    function removeLiquidity(uint256 _amount) public returns (uint256, uint256) {
        require(
            _amount > 0, "Amount should be greater than zero"
        );
        uint256 _reservedEth = address(this).balance;
        uint256 _totalSupply = totalSupply();

        uint256 _ethAmount = (_reservedEth * _amount) / totalSupply();
        uint256 _tokenAmount = (getTokensInContract() * _amount) / _totalSupply;
        _burn(msg.sender, _amount);
        payable(msg.sender).transfer(_ethAmount);
        ERC20Base(token).transfer(msg.sender ,_tokenAmount);
        return (_ethAmount, _tokenAmount);
    }

    function getAmountOfTokens(
        uint256 inputAmount,
        uint256 inputReserve,
        uint256 outputReserve
    )
    public pure returns (uint256) 
    {
        require(inputReserve > 0 && outputReserve > 0, "Invalid Reserves");
        // We are charging a fee of `1%`
        // uint256 inputAmountWithFee = inputAmount * 99;
        uint256 inputAmountWithFee = inputAmount;
        uint256 numerator = inputAmountWithFee * outputReserve;
        uint256 denominator = (inputReserve * 100) + inputAmountWithFee;
        unchecked {
            return numerator / denominator;
        }
    }

    function swapEthTotoken() public payable {
        uint256 _reservedTokens = getTokensInContract();
        uint256 _tokensBought = getAmountOfTokens(
            msg.value, 
            address(this).balance, 
            _reservedTokens
        );
        ERC20Base(token).transfer(msg.sender, _tokensBought);
    }

    function swapTokenToEth(uint256 _tokensSold) public {
        uint256 _reservedTokens = getTokensInContract();
        uint256 ethBought = getAmountOfTokens(
            _tokensSold,
            _reservedTokens,
            address(this).balance
        );
        ERC20Base(token).transferFrom(
            msg.sender, 
            address(this), 
            _tokensSold
        );
        payable(msg.sender).transfer(ethBought);
    }
    
}

Once your contract code is complete, deploy it using:

npx thirdweb deploy

Select the network you want to deploy to (make sure it matches where your ERC20 token is deployed).

After deployment, copy the contract address. We'll need this when building the app.

Step 2: Build the DEX Application

Now let's build a simple web app that interacts with our DEX contract. We'll use Next.js and the thirdweb SDK.

From your terminal, run:

npx thirdweb create app

Name the project 'dex-app' and choose Next.js and TypeScript.

Navigate to the newly created directory:

cd dex-app

Open the project in your code editor and install the dependencies:

yarn

Configure the thirdweb provider

Open pages/_app.tsx and configure the thirdweb provider with your API key and network:

import { ThirdwebProvider } from '@thirdweb-dev/react';

function MyApp({ Component, pageProps }) {
  const API_KEY = process.env.NEXT_PUBLIC_API_KEY || '';
  const activeChain = 'mumbai';

  return (
    <ThirdwebProvider supportedChains={[activeChain]} apiKey={API_KEY}>
      <Component {...pageProps} />
    </ThirdwebProvider>
  );
}

export default MyApp;

Make sure to add your thirdweb API key to the .env file.

Build the swap components

Create a new component called SwapInput to handle the input fields:

export default function SwapInput() {
  // Component code here
}

This component will:

  • Allow the user to enter an amount to swap
  • Display token balances
  • Handle setting max amount

Next, update pages/index.tsx with the main swap functionality:

import { useContract } from '@thirdweb-dev/react';
import SwapInput from 'components/SwapInput';

const TOKEN_ADDRESS = '0x123...'; // ERC20 token address
const DEX_ADDRESS = '0x456...';   // DEX contract address

export default function Home() {
  const tokenContract = useContract(TOKEN_ADDRESS);
  const dexContract = useContract(DEX_ADDRESS);

  const [tokenBalance, setTokenBalance] = useState('0');
  const [nativeBalance, setNativeBalance] = useState('0');
  const [tokenSymbol, setTokenSymbol] = useState('');
  const [amountOut, setAmountOut] = useState(0);
  const [isLoading, setIsLoading] = useState(false);

  // useEffect hooks to fetch balances & allowance
  // ...

  async function executeSwap() {
    try {
      setIsLoading(true);

      // Approve DEX to spend token
      await tokenContract.call('approve', [DEX_ADDRESS, tokenAmount]);

      const tx = currentType === 'native'
        ? await dexContract.call('swapEthToToken', {
            value: toWei(nativeAmount),
          })
        : await dexContract.call('swapTokenToEth', [
            toWei(tokenAmount),
          ]);

      await tx.wait();
      alert(`Swap successful!`);
    } catch (err) {
      console.error(err);
      alert('An error occurred');
    } finally {
      setIsLoading(false);
    }
  }

  return (
    <div>
      <SwapInput
        symbol='MATIC' 
        type='native'
        balance={nativeBalance}
        amount={nativeAmount}
        onAmountChange={setNativeAmount}
      />
         
      <button onClick={() => setCurrentType(t => t === 'native' ? 'token' : 'native')}>
        Switch
      </button>

      <SwapInput
        symbol={tokenSymbol}
        type='token'
        balance={tokenBalance} 
        amount={tokenAmount}
        onAmountChange={setTokenAmount}
      />

      {amountOut > 0 && (
        <p>You will receive ~{formatUnits(amountOut)} tokens</p>  
      )}
      
      {address ? (
        <button onClick={executeSwap} disabled={isLoading}>
          {isLoading ? 'Swapping...' : 'Swap'}  
        </button>
      ) : (
        <p>Connect wallet to swap</p>  
      )}
    </div>
  )
}

This component handles:

  • Fetching token balances and symbol
  • Calculating amount of tokens out
  • Approving the DEX to spend tokens
  • Executing the swap

Test it out

Start the development server:

yarn dev

Open http://localhost:3000 in your browser to test out the DEX!

Connect your wallet, enter an amount, and click Swap to exchange between the native token and your ERC20 token.

Step 3: Deploy Your DEX App

Once you have tested your DEX locally and everything is working as expected, you can deploy it to share with others.

Deploy the Smart Contract

First, deploy your DEX smart contract to a live network using the thirdweb CLI. Make sure you have your wallet set up with the necessary funds for the network you are deploying to.

From the dex-contract directory, run:

npx thirdweb deploy

Select the network you want to deploy to and confirm the transaction in your wallet. Once deployed, you will see the live contract address printed out. Copy this as you will need it to configure your web app.

Configure the Web App

In your web app, open the .env file and add the following:

NEXT_PUBLIC_DEX_ADDRESS=<your-dex-contract-address>
NEXT_PUBLIC_TOKEN_ADDRESS=<your-token-contract-address>

Replace <your-dex-contract-address> with the address of your deployed DEX contract, and <your-token-contract-address> with the address of your ERC20 token contract.

Deploy the Web App

To deploy your Next.js app, you can use a service like Vercel.

Install the Vercel CLI:

npm i -g vercel

Then from your dex-app directory, run:

vercel

Follow the prompts to log in and deploy your app. Once deployed, you will get a live URL where people can access your DEX.

Wrapping Up

Congratulations, you just built your own decentralized exchange using thirdweb! Share the URL with others so they can start swapping tokens.

In this guide, we covered:

  • Creating a DEX smart contract to handle swapping between tokens
  • Building a web app to interact with the DEX contract
  • Calculating token amounts, fetching balances, and executing swaps

What's next?

There are many ways you can expand on this DEX implementation, such as:

  • Add a liquidity pool to earn fees on swaps
  • Implement limit orders
  • Support multiple token pairs
  • Integrate charting and price data
  • Allow users to create new token pairs

Use this guide as a starting point and continue building out your ideal decentralized exchange. Check out the thirdweb documentation to learn more about the available tools and features you can integrate.

Thanks for following this guide and good luck with your DEX! The complete code for this tutorial is available on GitHub — leave a star if you found it helpful.

If you have any questions or need help along the way, join the thirdweb Discord to connect with other builders and get support from the team.

Thanks for following this guide and good luck with your DEX!