How to create your own Uniswap

In this tutorial we’re going to build a very basic decentralized exchange (DEX) like Uniswap or PancakeSwap.

Our project will consist of 2 smart contracts: Exchange.sol and ExchangePool.sol.

Full source code can be found here: https://github.com/ryzhak/dex-demo

Exchange contract has the following features:

  • – Exchange owner can create a new pool of a pair of ERC20 tokens
  • – Any user can add liquidity (stake a pair of ERC20 tokens) to the pool. When a user adds liquidity to the pool he receives LP (liquidity provider) tokens which can be later used to remove liquidity (unstake a pair of ERC20 tokens) or for liquidity mining (not implemented in this tutorial). Normally the more liquidity a user adds in a single pool the more fee a user gets when somebody makes a swap/trade in the pool (again swap fees are not implemented in this tutorial).
  • – Any user can remove liquidity (unstake a pair of ERC20 tokens) from the pool to get his ERC20 tokens back. When a user removes liquidity his LP tokens are burned.
  • – Any user can make a swap/trade/sell/buy in the pool.

Notice that this project is not production ready as the following features are not implemented:

  • – Swap trading fees
  • Slippage protection
  • – Many validation steps are missed
  • – Reentrancy protection
  • – ETH => ERC20 and ERC20 => ETH swaps are not supported. Exchange pool consists of 2 ERC20 tokens. But ETH is not ERC20 compliant. That is why if we were to implement such a feature we would have to convert ETH to WETH inside the contract and operate with WETH because it is ERC20 compliant. So when a user sells ETH then ETH is converted to WETH inside the smart contract and sent to the pool. When a user buys ETH then WETH is converted to ETH in the smart contract and sent to the user.

AMM

Centralized exchanges use order book to match sellers and buyers. So if a user wants to buy ETH but nobody sells it then order will not be fulfilled. On the contrary, with a decentralized exchange user will always fulfill the order. Uniswap and other decentralized exchanges use different automated market maker models (AMM). Uniswap uses constant product k = a * b formula where a is the amount of the 1st token in the liquidity pool and b is the amount of the 2nd token in the liquidity pool. K is a constant that means total assets liquidity in the pool has to remain the same. So when a user sells/swaps B token to buy A token then A price goes up as there becomes less A in the pool and B price goes down as there becomes more B in the pool.

Example:

  1. 1. User1 adds 10 CAT tokens and 100 DOG tokens to the liquidity pool.
  2. 2. User1 gets 10 * 100 = 1000 LP (liquidity providers) tokens.
  3. 3. User2 wants to sell 1 CAT token to buy as many DOG tokens as possible.
  4. 4. Amount of CAT tokens after the swap: 11
  5. 5. K (constant product) should always remain the same so the amount of DOG tokens after the swap: K / 11 = 1000 / 11 = ~90.91

Amount of DOG tokens that user2 will get for selling 1 CAT token: total amount of DOG token in the pool before the swap – amount of DOG token in the pool after the swap = 100 – 90.91 = 9.09

Init project

Requirements:

  • – Truffle
  • – Solidity
  • – NodeJS
  • – Ganache

Create a new project folder and run truffle init to initialize an empty truffle project. Next install openzeppelin contracts via npm install @openzeppelin/contracts –save. Then create an empty ExchangePool contract via truffle create contract ExchangePool. And finally create an empty Exchange contract via truffle create contract Exchange.

Creating a pool contract

Add the following code to the ExchangePool.sol file:

Exchange contract will have a list of all available pools (ExchangePool contract). Only Exchange contract can create new pools so Exchange will always be the owner of the ExchangePool contract.

ExchangePool is ERC20 token itself because it maintains LP (liquidity provider) tokens of users who added liquidity to the pool.

ExchangePool contract has 2 ERC20 token addresses which define the pool. Notice that token addresses in the pool are always sorted so tokenAddress0 < tokenAddress1.

Owner method approvePoolTokenAmount() approves the owner (exchange) to spend tokens from the pool’s address.

Owner method mint() creates new LP (liquidity provider) tokens when a user adds liquidity to the pool.

Owner method burn() deletes LP tokens when a user removes liquidity from the pool.

Creating an exchange contract

Add the following code to the Exchange.sol file:

Owner method createPool() creates a new liquidity pool. Token addresses can be passed in any order as they are sorted inside.

Public method getPoolAddress() returns a pool address by 2 provided ERC20 token addresses.

Public method sortAddresses() sorts 2 addresses as strings.

Public method addLiquidity() adds 2 ERC20 token amounts to the liquidity pool. 

How it works:

  1. 1. User approves an exchange contract to spend his CAT and DOG tokens.
  2. 2. User calls addLiquidity() method.
  3. 3. Exchange transfers user’s CAT and DOG tokens to the liquidity pool address.
  4. 4. Exchange (via pool) mints user LP tokens for provided liquidity.

Public method removeLiquidity() burns user’s LP tokens and sends ERC20 tokens from the pool to the user.

How it works:

  1. 1. User calls removeLiquidity() and provides the amount of LP tokens he wants to burn.
  2. 2. Exchange (via pool) burns user’s LP tokens.
  3. 3. Exchange asks pool approval to transfer tokens from the pool address.
  4. 4. Exchange transfers a pair of ERC20 tokens from the pool address to the user address.

Public method swap() sells the provided amount of token A to buy a maximum amount of token B.

How it works:

  1. 1. User approves exchange to transfer 1 CAT token from user address
  2. 2. User sells 1 CAT token to get a maximum amount of DOG token
  3. 3. Exchange calculates amount of DOG token that user will get for 1 CAT token
  4. 4. Exchange asks pool contract to approve transfer of DOG tokens from the pool address
  5. 5. Exchange transfers 1 CAT token from user address to the pool address
  6. 6. Exchange transfers a calculated amount of DOG token form the pool address to the user address.

Now run truffle compile to check that there are no errors:

Creating a migration

Open a new console window and run ganache to start a development blockchain.

Add ganache config to the truffle-config.js file:

In the migrations folder create a new file 2_deploy_exchange.js with the following content:

Now run truffle migrate to check that migration to blockchain works:

Writing tests

In the test folder create a new file Exchange.test.js with the following content:

Here we added 2 helper methods for convenience. Also before each test we’re going to create a new exchange contract, a new instance of the CAT token and a new instance of the DOG token.

We’re going to cover only basic methods. All tests can be found here: https://github.com/ryzhak/dex-demo/blob/master/test/Exchange.test.js

Let’s add some liquidity:

Run truffle test:

Now let’s make a swap:

Again run truffle test:

And finally let’s remove liquidity:

Run truffle test:

Summary

In this tutorial we learned how decentralized exchanges work, learned what AMM is, created a basic DEX with add/remove liquidity and swap features, and wrote tests to check that everything works as expected. Now you should have a basic understanding of how DEX works.

Leave a Reply

Your email address will not be published. Required fields are marked *