DEV Community

CryptoLoom
CryptoLoom

Posted on • Originally published at cryptoloom.xyz on

How to Create Your Own Decentralized Exchange (DEX) in Solidity & HardHat

Decentralized Exchanges (DEXes) play a crucial part in the world of decentralized finance (DeFi), offering users complete control over their assets while simultaneously providing a platform for the trustless transaction of cryptocurrencies. Traditional centralized exchanges (CEXes) require users to deposit assets on the platform and relinquish their private keys, creating a centralized point of failure.

Are you interested in creating your own decentralized exchange on the Ethereum blockchain? Look no further! This article will guide you through the process, providing code examples in Solidity and setting up a development environment using HardHat.

Decentralized Exchange Functionality

A basic decentralized exchange should allow users to:

  1. Deposit tokens or ether.
  2. Trade tokens or ether in a trustless manner.
  3. Withdraw tokens or ether.

To achieve this functionality, we’ll build our DEX using Ethereum’s most popular smart contract programming language, Solidity. To make our development process streamlined and efficient, we’ll use the HardHat development environment.

Setting Up the Development Environment

First, let’s set up our development environment using HardHat. Follow these simple steps:

  1. Install Node.js if you haven’t done so already. You can download it here.
  2. Create a new directory for your project and navigate to it in your terminal or command prompt.
  3. In the project directory, run the command npm init to initialize a new Node.js project.
  4. Install HardHat by executing npm install --save-dev hardhat.
  5. Run npx hardhat to generate a sample HardHat config file, and choose to create a sample project when prompted.
  6. Delete the sample contracts (Greeter.sol and Token.sol) as well as the test file (sample-test.js).

Now, you have a blank HardHat project set up, and you’re ready to start writing your own decentralized exchange from scratch.

Creating a Token for Our Exchange

First, we’ll create an ERC20 token for our demonstration. An ERC20 token is a widely accepted token standard on the Ethereum blockchain, allowing for seamless interoperability between different decentralized applications.

In your project directory, create a new directory called contracts. Within the contracts directory, create a new file called MyToken.sol. The code for our simple ERC20 token is as follows:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken is ERC20 {
    constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
        _mint(msg.sender, initialSupply);
    }
}

Enter fullscreen mode Exit fullscreen mode

Here, we make use of the ERC20 contract provided by the OpenZeppelin library. It takes care of the nitty-gritty of ERC20 token implementation, so we can simply provide an initial supply and our token’s name and symbol.

Building the Decentralized Exchange Contract

Next, create a new file called DEX.sol in the contracts directory. This is where we’ll develop our decentralized exchange’s smart contract.

Initialization

Let’s start by defining the basic structure of our contract and initializing it:

pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";

contract DEX {
    using SafeERC20 for ERC20; // Use OpenZeppelin's SafeERC20 to deal with tokens securely
    address public owner; // The owner of the DEX

    constructor() {
        owner = msg.sender;
    }
}

Enter fullscreen mode Exit fullscreen mode

Here, we import both the ERC20 and SafeERC20 contracts from the OpenZeppelin library. We also set the DEX owner to the address that deploys the contract.

Adding Token Support

Now, let’s build the functionality for adding new tokens to our DEX:

struct Token {
    ERC20 tokenContract;
    uint256 totalLiquidity;
}

mapping(address => Token) public tokens; // Address of token contract to Token struct

function addToken(address tokenAddress) public {
    require(msg.sender == owner, "Only owner can add tokens.");
    ERC20 token = ERC20(tokenAddress);
    tokens[tokenAddress] = Token({tokenContract: token, totalLiquidity: 0});
}

Enter fullscreen mode Exit fullscreen mode

We define a new struct Token, which holds an instance of the token’s contract and the token’s total liquidity on our DEX. We create a mapping from the token’s contract address to the Token struct. Then, we add a function addToken that allows the owner of the DEX to add new tokens by providing the token’s contract address.

Notice the require statement to ensure that only the owner can add tokens: it checks if the function caller is the owner, and if not, the transaction is reverted with a custom error message.

Deposit and Withdraw Functions

Next, we’ll implement the deposit and withdraw functions. Users should be able to deposit and withdraw tokens as well as ether. For token deposits and withdrawals, we’ll make use of the transfer and transferFrom functions provided by the ERC20 contract.

Create the following functions in the DEX.sol file:

mapping(address => mapping(address => uint256)) public tokenBalances; // User address -> token address -> token balance

function depositToken(address tokenAddress, uint256 amount) public {
    require(tokenAddress != address(0), "Cannot deposit zero address token."); // Safety check
    require(tokens[tokenAddress].tokenContract != ERC20(address(0)), "Token not supported by DEX.");
    tokens[tokenAddress].tokenContract.safeTransferFrom(msg.sender, address(this), amount);
    tokenBalances[msg.sender][tokenAddress] += amount;
}

function deposit() payable public {
    require(msg.value > 0, "Cannot deposit zero value."); // Safety check
    tokenBalances[msg.sender][address(0)] += msg.value; // ETH is stored as zero address
}

mapping(address => mapping(address => uint256)) public etherBalances; // User address -> token address -> ether balance

function withdrawToken(address tokenAddress, uint256 amount) public {
    require(tokenAddress != address(0), "Cannot withdraw zero address token.");
    require(tokens[tokenAddress].tokenContract != ERC20(address(0)), "Token not supported by DEX.");
    tokenBalances[msg.sender][tokenAddress] -= amount;
    tokens[tokenAddress].tokenContract.safeTransfer(msg.sender, amount);
}

function withdraw(uint256 amount) public {
    require(etherBalances[msg.sender][address(0)] >= amount, "Insufficient ether balance.");
    etherBalances[msg.sender][address(0)] -= amount;
    payable(msg.sender).transfer(amount);
}

Enter fullscreen mode Exit fullscreen mode

With these four functions, users can deposit and withdraw both tokens and ether, and we update their balances accordingly.

Trading

Now for the most important part: the trading functionality!

First, create a struct for an order:

struct Order {
    address trader;
    address token;
    uint256 tokensTotal;
    uint256 tokensLeft;
    uint256 etherAmount;
    uint256 filled;
}

Enter fullscreen mode Exit fullscreen mode

Then, define an array to store open orders and a mapping to store order history. We also implement an event to emit when a new order is created:

Order[] public openOrders;
mapping(address => Order[]) public orderHistories;

event OrderPlaced(uint256 orderId, address indexed trader, address indexed token, uint256 tokensTotal, uint256 etherAmount);

Enter fullscreen mode Exit fullscreen mode

We can now create the function for placing a new order:

function placeOrder(address token, uint256 tokensTotal, uint256 etherAmount) public {
    require(tokens[token].tokenContract != ERC20(address(0)), "Token not supported by DEX.");
    require(tokenBalances[msg.sender][token] >= tokensTotal, "Insufficient token balance.");

    Order memory newOrder = Order({
        trader: msg.sender,
        token: token,
        tokensTotal: tokensTotal,
        tokensLeft: tokensTotal,
        etherAmount: etherAmount,
        filled: 0
    });

    openOrders.push(newOrder);
    tokenBalances[msg.sender][token] -= tokensTotal;

    emit OrderPlaced(openOrders.length-1, msg.sender, token, tokensTotal, etherAmount);
}

Enter fullscreen mode Exit fullscreen mode

Finally, let’s implement the fillOrder function, allowing users to complete open orders:

event OrderFilled(uint256 orderId, address indexed trader, uint256 tokensFilled, uint256 etherTransferred);

function fillOrder(uint256 orderId) public {
    Order storage order = openOrders[orderId];
    uint256 tokensToFill = order.tokensLeft;
    uint256 etherToFill = (tokensToFill * order.etherAmount) / order.tokensTotal;

    require(etherBalances[msg.sender][address(0)] >= etherToFill, "Insufficient ether balance.");

    etherBalances[order.trader][address(0)] += etherToFill;
    etherBalances[msg.sender][address(0)] -= etherToFill;
    tokenBalances[msg.sender][order.token] += tokensToFill;
    order.tokensLeft = 0;
    order.filled += tokensToFill;

    orderHistories[msg.sender].push(order);
    if (order.trader != msg.sender) orderHistories[order.trader].push(order);
    delete openOrders[orderId];

    emit OrderFilled(orderId, order.trader, tokensToFill, etherToFill);
}

Enter fullscreen mode Exit fullscreen mode

With the fillOrder function, users can complete orders, exchanging their ether for tokens. The order is then removed from the open orders list and added to each party’s order history.

Testing and Deployment

With our DEX contract completed, you can now deploy it to a local Ethereum blockchain using HardHat, test your code, or even deploy it to a public Ethereum test network.

Well done! You’ve now created your own decentralized exchange on the Ethereum blockchain. From here, you can extend the functionality of your DEX, add more trading pairs, or improve the user interface by implementing a frontend application.

References

  1. Ethereum.org: Solidity
  2. OpenZeppelin: Smart Contract Library
  3. HardHat: Ethereum Development Environment

The post How to Create Your Own Decentralized Exchange (DEX) in Solidity & HardHat appeared first on CryptoLoom.

Top comments (0)