Decentralized Exchange In NodeJS
In this YouTube lecture, Harkirat provides an in-depth explanation of building a decentralized exchange (DEX) using Node.js. He contrasts traditional centralized exchanges with DEXs.
How do we Buy and Store Crypto
There are two main ways to buy and store cryptocurrencies:
Centralized Exchanges (CEXs): You can buy cryptocurrencies on centralized exchanges like Binance, Coinbase, WazirX, etc. These platforms allow you to track your balances and holdings in a user interface. However, the exchange essentially holds your crypto assets, and you don't have direct control over the private keys.
Cold Storage Wallets: These are hardware wallets (like the one shown) or software wallets (browser extensions) that store your private keys, giving you complete ownership and control over your crypto assets. As long as you have access to the wallet, no one can take your money away or block your access.
Storing your crypto in a cold storage wallet is considered more secure as you directly own and control your assets, unlike centralized exchanges where the exchange holds your funds.
How Centralized Exchanges (CEXs) Work
A centralized exchange like Binance acts as an intermediary, facilitating the trading of cryptocurrencies between buyers and sellers. There are two main types of participants on a CEX:
Market Makers: These are companies or traders that continuously place buy and sell orders for various cryptocurrencies based on algorithms that determine the fair price. They aim to buy at slightly lower prices and sell at slightly higher prices to make a profit.
Retail Traders: These are individual traders who place buy or sell orders at the available market prices, without necessarily considering the fair price.
The CEX's goal is to match the buy and sell orders from these two groups of participants and facilitate the exchange of assets, charging a small fee for each transaction.
Here's how a typical transaction works on a CEX:
A retail trader sends their cryptocurrency (e.g., 1 ETH) from their personal wallet to the exchange's wallet address.
The exchange holds this 1 ETH in its wallet.
The retail trader can then use the exchange's interface to trade their 1 ETH for another cryptocurrency (e.g., USDC) or fiat currency (e.g., USD) at the current market rate.
The exchange matches the trader's order with available buy/sell orders from market makers or other traders.
The exchange facilitates the trade, taking a small fee from both parties involved in the transaction.
The main advantage of using a CEX is the ability to easily buy, sell, and trade cryptocurrencies, as well as the option to convert between cryptocurrencies and fiat currencies. However, users don't have direct control over their assets, as the exchange holds the private keys.
How Prices are Determined in a Market
The price of an asset or commodity in a market is primarily determined by the interplay of supply and demand forces. The key factors that influence price determination are:
Number of Buyers and Sellers: The more buyers there are for a particular asset, the higher the demand, which drives up the price. Conversely, if there are more sellers than buyers, the supply exceeds demand, leading to a decrease in price.
Willingness to Buy or Sell: The price is also influenced by the eagerness or desperation of buyers and sellers. If buyers are more willing to pay a higher price, it will drive up the price. Similarly, if sellers are more desperate to sell quickly, they may accept a lower price.
Competition: The presence of competition among sellers can lead to lower prices as they try to undercut each other to attract buyers. On the other hand, limited competition or a monopolistic market can result in higher prices.
The process of price determination can be illustrated with an example of a housing market:
Suppose there are 10 houses for sale in a neighborhood, and multiple potential buyers are interested.
Initially, sellers may list their houses at different prices, say $400,000, $380,000, and $420,000.
Buyers will evaluate these prices based on their willingness to pay and make offers accordingly.
If there are more interested buyers than available houses, buyers may start bidding higher prices to secure a house, driving up the prices.
Conversely, if there are more houses for sale than interested buyers, sellers may lower their prices to attract buyers, leading to a decrease in prices.
The equilibrium price is reached when the number of buyers willing to pay a certain price matches the number of sellers willing to sell at that price.
The price of an asset or commodity in a market is determined by the dynamic interaction of supply (number of sellers and their willingness to sell) and demand (number of buyers and their willingness to buy), as well as the level of competition among sellers.
Some Jargons
1] Broker and Order Book
In traditional financial markets, there is typically a broker who acts as an intermediary between buyers and sellers. The broker maintains an order book, which is a list of buy and sell orders from various market participants, along with the quantities and prices they are willing to trade at.
2] Web2 vs Web3 and Gas Fees
In a Web2 (traditional centralized) system, maintaining an order book is feasible as there is a central authority (the exchange) that can process and match orders efficiently. However, in Web3 (decentralized blockchain-based systems), maintaining a traditional order book on-chain becomes impractical due to the gas fees associated with each transaction.
3] Liquidity Pools and Automated Market Makers (AMMs)
To address this challenge, the concept of liquidity pools and automated market makers (AMMs) was introduced in decentralized finance (DeFi). Instead of an order book, liquidity is provided by participants who deposit pairs of assets (e.g., ETH and USDC) into a shared pool.
The AMM algorithm determines the price of an asset based on the ratio of the assets in the pool, following the constant product formula: x * y = k, where x and y are the quantities of the two assets, and k is a constant.
Example
Consider a liquidity pool with 10 houses and 20 crores (CR) of cash. The initial constant product is 10 * 20 CR = 200 CR.
If someone wants to buy a house, they must deposit cash into the pool, and the price of a house is determined by the constant product formula.
For example, if the first person deposits 2.22 CR, they receive 1 house, and the pool becomes 9 houses and 18 CR (9 * 18 CR = 162 CR).
The next person must deposit more cash to receive a house, as the constant product must be maintained (e.g., 2.78 CR for the next house, leaving 8 houses and 20.78 CR in the pool).
As more assets are traded, the price of the remaining assets increases due to the constant product formula, incentivizing liquidity providers to add more assets to the pool.
Summary
Automated market makers and liquidity pools enable decentralized trading without the need for a traditional order book or market makers. The constant product formula determines the price of assets based on the ratio of assets in the pool, allowing for efficient and gas-efficient trading on blockchain networks.
The more liquidity (assets) in the pool, the smaller the price impact of individual trades, providing better pricing for users. This innovative approach addresses the challenges of maintaining order books on-chain and facilitates decentralized trading in DeFi ecosystems
Difference between CEX and DEX
1] Centralized Exchanges (CEXs)
Use traditional order books to match buy and sell orders
Rely on market makers to provide liquidity
Operated by a central authority that holds user funds
More efficient for high trading volumes
Users face counterparty risk as they don't control their assets
2] Decentralized Exchanges (DEXs)
No order books due to high gas fees for on-chain transactions
Use automated market makers (AMMs) and liquidity pools
Liquidity providers deposit asset pairs into shared pools
Prices determined by the constant product formula (x * y = k)
Decentralized, without a central authority controlling trades
Users maintain self-custody of their assets
May face liquidity challenges for less popular trading pairs
Technical Implementation
With the theoretical understanding of how automated market makers and liquidity pools work for decentralized exchanges, the next step is to implement this concept in code. The key is to create a server that supports the necessary operations for users to interact with the liquidity pool.
To bring the decentralized exchange to life, we need to develop an HTTP server that exposes the following endpoints:
Add Liquidity: This endpoint allows liquidity providers to deposit pairs of assets (e.g., ETH and USDC) into the shared liquidity pool, initializing or contributing to the constant product formula.
Buy Asset: Users can call this endpoint to purchase an asset (e.g., a house) from the liquidity pool by providing the required amount of the other asset (e.g., USDC) based on the constant product formula.
Sell Asset: Conversely, users can sell an asset they hold by calling this endpoint, receiving the corresponding amount of the other asset from the liquidity pool.
Get Quote: Before executing a trade, users can call this endpoint to obtain a quote for the amount of assets they would receive or need to provide for a specific trade, based on the current state of the liquidity pool.
By implementing these four core operations as HTTP endpoints, users and liquidity providers can seamlessly interact with the decentralized exchange, facilitating the trading of assets through the automated market maker mechanism
Setting up a Node.js Express Backend in TypeScript
To begin implementing the decentralized exchange (DEX) using automated market makers (AMMs) and liquidity pools, we'll set up a Node.js backend with Express and TypeScript. Here's how you can get started:
Initialize a new Node.js project:
mkdir dex-project
cd dex-project
npm init -
Install required dependencies:
npm install express body-parser
npm install --save-dev typescript @types/node @types/expres
We're installing
express
for the web server,body-parser
to parse request bodies,typescript
for TypeScript support, and the corresponding type definitions for Node.js and Express.Configure TypeScript:
Create a
tsconfig.json
file in the project root with the following content:
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"outDir": "dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src/**/*"]
}
This configuration sets up TypeScript to compile our code to ES6 JavaScript, with strict type checking and support for ES6 module interoperability.
Create the entry point:
Create a
src
directory in the project root.Inside the
src
directory, create anindex.ts
file with the following content:
import express, { Request, Response } from 'express';
import bodyParser from 'body-parser';
const app = express();
const port = 3000;
app.use(bodyParser.json());
app.get('/', (req: Request, res: Response) => {
res.send('Hello, DEX!');
});
app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
This code sets up a basic Express server with a root route that responds with "Hello, DEX!".
Build and run the server:
Open
package.json
and add the following scripts:
"scripts": {
"build": "tsc",
"start": "node dist/index.js"
}
Run the build script to compile the TypeScript code:
npm run build
Start the server:
npm start
You should see the "Server is running on port 3000" message in the console.
Now you have a basic Node.js Express backend set up with TypeScript support. You can access the root route by visiting
http://localhost:3000
in your web browser.
Core Backend Functionality
Now that we have out index.ts
file initialized, lets jump in to write the code for handling our backend routes of the DEX.
Importing Dependencies:
import express from "express";
The code starts by importing the
express
module, which is a popular web application framework for Node.js.Setting up Express:
const app = express();
app.use(express.json());
An instance of the Express application is created using
express()
, and theapp.use(express.json())
middleware is added to parse incoming JSON request bodies.Initializing Balances and Constant Product:
let ETH_BALANCE = 200;
let USDC_BALANCE = 700000;
const CONSTANT_PRODUCT = ETH_BALANCE * USDC_BALANCE; // 200 * 700000 = 140000000
The initial balances of ETH and USDC in the liquidity pool are set to 200 and 700000, respectively. The
CONSTANT_PRODUCT
is calculated by multiplying these two values, which is 140000000. This constant product will be used to determine the prices of assets in the liquidity pool.Buy Asset Endpoint:
app.post("/buy-asset", (req, res) => {
const quantity = req.body.quantity; // Quantity of ETH to buy
const updatedEthQuantity = ETH_BALANCE - quantity;
const updatedUsdcBalance = CONSTANT_PRODUCT / updatedEthQuantity;
const paidAmount = USDC_BALANCE - updatedUsdcBalance;
ETH_BALANCE = updatedEthQuantity;
USDC_BALANCE = updatedUsdcBalance;
res.json({
message: `You paid ${paidAmount} USDC for ${quantity} ETH`
});
});
The
/buy-asset
endpoint is defined to handle requests for buying ETH from the liquidity pool.The quantity of ETH to buy is extracted from the request body (
req.body.quantity
).The updated ETH quantity (
updatedEthQuantity
) is calculated by subtracting the requested quantity from the currentETH_BALANCE
.The updated USDC balance (
updatedUsdcBalance
) is calculated using the constant product formula:CONSTANT_PRODUCT / updatedEthQuantity
.The amount of USDC paid (
paidAmount
) is calculated by subtracting theupdatedUsdcBalance
from the currentUSDC_BALANCE
.The
ETH_BALANCE
andUSDC_BALANCE
are updated with the new values.A JSON response is sent back with a message indicating the amount of USDC paid and the quantity of ETH received.
Sell Asset Endpoint:
app.post("/sell-asset", (req, res) => {
const quantity = req.body.quantity; // Quantity of ETH to sell
const updatedEthQuantity = ETH_BALANCE + quantity;
const updatedUsdcBalance = CONSTANT_PRODUCT / updatedEthQuantity;
const gottenUsdc = USDC_BALANCE - updatedUsdcBalance;
ETH_BALANCE = updatedEthQuantity;
USDC_BALANCE = updatedUsdcBalance;
res.json({
message: `You got ${gottenUsdc} USDC for ${quantity} ETH`
});
});
The
/sell-asset
endpoint is defined to handle requests for selling ETH to the liquidity pool.The quantity of ETH to sell is extracted from the request body (
req.body.quantity
).The updated ETH quantity (
updatedEthQuantity
) is calculated by adding the requested quantity to the currentETH_BALANCE
.The updated USDC balance (
updatedUsdcBalance
) is calculated using the constant product formula:CONSTANT_PRODUCT / updatedEthQuantity
.The amount of USDC received (
gottenUsdc
) is calculated by subtracting theupdatedUsdcBalance
from the currentUSDC_BALANCE
.The
ETH_BALANCE
andUSDC_BALANCE
are updated with the new values.A JSON response is sent back with a message indicating the amount of USDC received and the quantity of ETH sold.
Starting the Server:
app.listen(3000, () => {
console.log("Server is running on port 3000");
});
The
app.listen(3000, ...)
method is called to start the server and listen for incoming requests on port 3000. A callback function is provided to log a message when the server starts.
This implementation follows the constant product formula for automated market makers (AMMs) to determine the prices of assets in the liquidity pool. As assets are bought or sold, the balances of ETH and USDC are updated accordingly, and the prices adjust based on the constant product formula.
Working
Here's how the constant product formula works:
The initial constant product is calculated as
ETH_BALANCE * USDC_BALANCE
, which is 140000000 in this case.When a user wants to buy ETH, the quantity is subtracted from the current
ETH_BALANCE
, and the updated USDC balance is calculated asCONSTANT_PRODUCT / updatedEthQuantity
.When a user wants to sell ETH, the quantity is added to the current
ETH_BALANCE
, and the updated USDC balance is calculated asCONSTANT_PRODUCT / updatedEthQuantity
.
The constant product formula ensures that as one asset is bought or sold, the price of the other asset adjusts accordingly, maintaining the constant product value. This mechanism allows for decentralized trading without the need for traditional order books or market makers.
Testing
As we see in Postman, the dynamic pricing behavior of the automated market maker (AMM) implementation is demonstrated.
When the user repeatedly sold ETH to the liquidity pool, the amount of USDC received decreased with each subsequent sale, reflecting the changing ratio of ETH to USDC in the pool based on the constant product formula.
Conversely, if another user were to start buying ETH, the price would increase due to the reduced supply of ETH in the pool. This real-time adjustment of prices based on supply and demand is a core feature of AMMs, enabling decentralized trading without the need for traditional order books or market makers.
The testing in Postman showcased how the constant product formula automatically rebalances asset prices in the liquidity pool as assets are bought and sold, maintaining an equilibrium through the market-making mechanism.
Further Reference
You can also refer to the Drift Protocol, an open-source decentralized exchange (DEX) built on the Solana blockchain. It combines different mechanisms like an order book, liquidity pools, and potentially other components to facilitate trading and swapping of assets. Despite being a DEX, it still incorporates an order book mechanism, which is more feasible on a high-throughput blockchain like Solana compared to Ethereum due to lower transaction costs.
🔥