Welcome to this comprehensive guide on building a Decentralized Exchange (DEX) on the Stellar network! Whether you're a blockchain novice or an experienced developer, this tutorial will walk you through creating a functional DEX from scratch.
Table of Contents
- Introduction to Decentralized Exchanges
- Understanding Stellar's Core Concepts
- Setting Up the Development Environment
- Designing the DEX Smart Contract
- Implementing the Smart Contract
- Creating Custom Tokens for Trading
- Deploying the Smart Contract
- Building the Frontend
- Testing the DEX
- Advanced Features and Optimizations
- Security Considerations
- Conclusion and Next Steps
Introduction to Decentralized Exchanges
A Decentralized Exchange (DEX) is a type of cryptocurrency exchange that operates without a central authority. Instead, it uses smart contracts to facilitate trades directly between users.
Key Terms:
- Decentralized Exchange (DEX): A peer-to-peer marketplace for cryptocurrencies without a central authority.
- Smart Contract: Self-executing code that automatically implements the terms of an agreement between parties.
- Liquidity: The ease with which an asset can be bought or sold without affecting its price significantly.
Understanding Stellar's Core Concepts
Before we dive into building our DEX, let's review some core Stellar concepts:
Accounts: Represent participants in the Stellar network. Each account has a public key and can hold balances in various assets.
Assets: Represent different types of value on the Stellar network. The native asset is called Lumens (XLM), but custom assets can also be created.
Operations: Individual commands that modify the ledger state, such as creating accounts, sending payments, or making offers.
Transactions: Groups of operations that are submitted to the network as a single unit.
Orderbook: A record of outstanding offers to buy or sell assets on the Stellar network.
Path Payments: Allows sending one type of asset and having the recipient receive another type, with the network automatically handling the conversion.
Setting Up the Development Environment
Let's set up our development environment:
- Install Node.js and npm (if not already installed)
- Install the Stellar SDK: ```bash
npm install stellar-sdk
3. Install Soroban CLI for smart contract development:
```bash
cargo install --locked --version 20.0.0-rc2 soroban-cli
- Set up a new project: ```bash
mkdir stellar-dex
cd stellar-dex
npm init -y
## Designing the DEX Smart Contract
Our DEX will need the following core functionalities:
1. Create and manage order books for different trading pairs
2. Place buy and sell orders
3. Match and execute orders
4. Manage user balances
5. Withdraw funds
Let's design our smart contract structure:
```rust
pub struct DEX {
order_books: Map<AssetPair, OrderBook>,
balances: Map<Address, Map<Asset, i128>>,
}
pub struct AssetPair {
base_asset: Asset,
quote_asset: Asset,
}
pub struct OrderBook {
buy_orders: Vec<Order>,
sell_orders: Vec<Order>,
}
pub struct Order {
user: Address,
amount: i128,
price: i128,
is_buy: bool,
}
pub struct Asset {
code: Symbol,
issuer: Option<Address>,
}
Implementing the Smart Contract
Now, let's implement our DEX smart contract. Create a new file src/lib.rs
:
#![no_std]
use soroban_sdk::{contractimpl, Address, Env, Map, Symbol, Vec};
mod types;
use types::{DEX, AssetPair, OrderBook, Order, Asset};
const PRECISION: i128 = 10000000; // 7 decimal places
#[contractimpl]
impl DEX {
pub fn init(env: Env) -> Self {
Self {
order_books: Map::new(&env),
balances: Map::new(&env),
}
}
pub fn deposit(&mut self, env: Env, user: Address, asset: Asset, amount: i128) {
user.require_auth();
let balance = self.balances.get(user).unwrap_or(Map::new(&env));
let new_balance = balance.get(asset).unwrap_or(0) + amount;
balance.set(asset, new_balance);
self.balances.set(user, balance);
}
pub fn withdraw(&mut self, env: Env, user: Address, asset: Asset, amount: i128) {
user.require_auth();
let mut balance = self.balances.get(user).unwrap();
let current_balance = balance.get(asset).unwrap();
if current_balance < amount {
panic!("Insufficient balance");
}
balance.set(asset, current_balance - amount);
self.balances.set(user, balance);
// Here we would typically initiate a Stellar transaction to send the assets
}
pub fn place_order(&mut self, env: Env, user: Address, pair: AssetPair, amount: i128, price: i128, is_buy: bool) {
user.require_auth();
let order = Order {
user: user.clone(),
amount,
price,
is_buy,
};
let mut order_book = self.order_books.get(pair).unwrap_or(OrderBook::new(&env));
if is_buy {
order_book.buy_orders.push_back(order);
order_book.buy_orders.sort_by(|a, b| b.price.cmp(&a.price));
} else {
order_book.sell_orders.push_back(order);
order_book.sell_orders.sort_by(|a, b| a.price.cmp(&b.price));
}
self.order_books.set(pair, order_book);
self.match_orders(env, pair);
}
fn match_orders(&mut self, env: Env, pair: AssetPair) {
let mut order_book = self.order_books.get(pair).unwrap();
while !order_book.buy_orders.is_empty() && !order_book.sell_orders.is_empty() {
let buy_order = order_book.buy_orders.first().unwrap();
let sell_order = order_book.sell_orders.first().unwrap();
if buy_order.price < sell_order.price {
break;
}
let trade_price = (buy_order.price + sell_order.price) / 2;
let trade_amount = buy_order.amount.min(sell_order.amount);
self.execute_trade(env, &pair, &buy_order.user, &sell_order.user, trade_amount, trade_price);
// Update or remove orders
if buy_order.amount > trade_amount {
order_book.buy_orders.set(0, Order {
amount: buy_order.amount - trade_amount,
..buy_order
});
} else {
order_book.buy_orders.remove(0);
}
if sell_order.amount > trade_amount {
order_book.sell_orders.set(0, Order {
amount: sell_order.amount - trade_amount,
..sell_order
});
} else {
order_book.sell_orders.remove(0);
}
}
self.order_books.set(pair, order_book);
}
fn execute_trade(&mut self, env: Env, pair: &AssetPair, buyer: &Address, seller: &Address, amount: i128, price: i128) {
let base_amount = amount;
let quote_amount = amount * price / PRECISION;
// Transfer base asset from seller to buyer
self.transfer(env, seller, buyer, pair.base_asset, base_amount);
// Transfer quote asset from buyer to seller
self.transfer(env, buyer, seller, pair.quote_asset, quote_amount);
}
fn transfer(&mut self, env: Env, from: &Address, to: &Address, asset: Asset, amount: i128) {
let mut from_balance = self.balances.get(from).unwrap();
let mut to_balance = self.balances.get(to).unwrap_or(Map::new(&env));
let from_amount = from_balance.get(asset).unwrap();
let to_amount = to_balance.get(asset).unwrap_or(0);
from_balance.set(asset, from_amount - amount);
to_balance.set(asset, to_amount + amount);
self.balances.set(from, from_balance);
self.balances.set(to, to_balance);
}
}
This implementation covers the core functionalities of our DEX:
- Depositing and withdrawing assets
- Placing buy and sell orders
- Matching and executing orders
- Managing user balances
Creating Custom Tokens for Trading
To create a more interesting trading environment, let's create two custom tokens:
- SpaceBucks (SPC)
- LunarCredits (LNC)
Create a new file src/tokens.rs
:
use soroban_sdk::{contractimpl, token, Address, Env, String};
pub struct Token;
#[contractimpl]
impl Token {
pub fn initialize(env: Env, admin: Address, decimal: u32, name: String, symbol: String) {
let token = token::Interface::new(&env, &env.current_contract_address());
token.initialize(&admin, &decimal, &name, &symbol);
}
pub fn mint(env: Env, to: Address, amount: i128) {
let token = token::Interface::new(&env, &env.current_contract_address());
token.mint(&to, &amount);
}
pub fn balance(env: Env, id: Address) -> i128 {
let token = token::Interface::new(&env, &env.current_contract_address());
token.balance(&id)
}
pub fn transfer(env: Env, from: Address, to: Address, amount: i128) {
let token = token::Interface::new(&env, &env.current_contract_address());
token.transfer(&from, &to, &amount);
}
}
Deploying the Smart Contract
Now, let's deploy our smart contracts to the Stellar testnet:
- Build the contracts: ```bash
soroban contract build
2. Create a Stellar account for deployment:
```bash
soroban config identity generate admin
soroban config network add testnet --rpc-url https://soroban-testnet.stellar.org
soroban config identity fund admin --network testnet
- Deploy the DEX contract: ```bash
soroban contract deploy --wasm target/wasm32-unknown-unknown/release/dex.wasm --source admin --network testnet
4. Deploy the token contracts:
```bash
soroban contract deploy --wasm target/wasm32-unknown-unknown/release/token.wasm --source admin --network testnet
soroban contract deploy --wasm target/wasm32-unknown-unknown/release/token.wasm --source admin --network testnet
Make note of the contract IDs returned for each deployment.
Building the Frontend
For our frontend, we'll use React with the Stellar SDK. Create a new React app:
npx create-react-app stellar-dex-frontend
cd stellar-dex-frontend
npm install stellar-sdk @stellar/freighter-api
Replace the content of src/App.js
with:
import React, { useState, useEffect } from 'react';
import { Server } from 'stellar-sdk';
import { isConnected, getPublicKey } from '@stellar/freighter-api';
const server = new Server('https://horizon-testnet.stellar.org');
const dexContractId = 'YOUR_DEX_CONTRACT_ID';
const spcTokenId = 'YOUR_SPC_TOKEN_CONTRACT_ID';
const lncTokenId = 'YOUR_LNC_TOKEN_CONTRACT_ID';
function App() {
const [account, setAccount] = useState(null);
const [spcBalance, setSpcBalance] = useState(0);
const [lncBalance, setLncBalance] = useState(0);
const [orderBooks, setOrderBooks] = useState({ buy: [], sell: [] });
useEffect(() => {
checkConnection();
}, []);
const checkConnection = async () => {
const connected = await isConnected();
if (connected) {
const publicKey = await getPublicKey();
setAccount(publicKey);
fetchBalances(publicKey);
fetchOrderBooks();
}
};
const fetchBalances = async (publicKey) => {
const spcBalance = await server.loadAccount(publicKey).then(account => {
return account.balances.find(b => b.asset_code === 'SPC')?.balance || '0';
});
const lncBalance = await server.loadAccount(publicKey).then(account => {
return account.balances.find(b => b.asset_code === 'LNC')?.balance || '0';
});
setSpcBalance(spcBalance);
setLncBalance(lncBalance);
};
const fetchOrderBooks = async () => {
// In a real implementation, you would fetch this data from your smart contract
setOrderBooks({
buy: [
{ price: 1.2, amount: 100 },
{ price: 1.1, amount: 200 },
],
sell: [
{ price: 1.3, amount: 150 },
{ price: 1.4, amount: 300 },
],
});
};
const placeOrder = async (isBuy, amount, price) => {
// Implementation of placing an order via smart contract
console.log(`Placing ${isBuy ? 'buy' : 'sell'} order: ${amount} @ ${price}`);
};
return (
<div className="App">
<h1>Stellar DEX</h1>
{account ? (
<>
<p>Connected: {account}</p>
<p>SPC Balance: {spcBalance}</p>
<p>LNC Balance: {lncBalance}</p>
<h2>Order Books</h2>
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<div>
<h3>Buy Orders</h3>
<ul>
{orderBooks.buy.map((order, index) => (
<li key={index}>
{order.amount} SPC @ {order.price} LNC
</li>
))}
</ul>
</div>
<div>
<h3>Sell Orders</h3>
<ul>
{orderBooks.sell.map((order, index) => (
<li key={index}>
{order.amount} SPC @ {order.price} LNC
</li>
))}
</ul>
</div>
</div>
<h2>Place Order</h2>
<form onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.target);
placeOrder(
formData.get('type') === 'buy',
parseFloat(formData.get('amount')),
parseFloat(formData.get('price'))
);
}}>
<select name="type">
<option value="buy">Buy</option>
<option value="sell">Sell</option>
</select>
<input type="number" name="amount" placeholder="Amount" step="0.01" required />
<input type="number" name="price" placeholder="Price" step="0.01" required />
<button type="submit">Place Order</button>
</form>
</>
) : (
<p>Please connect your Stellar wallet</p>
)}
</div>
);
}
export default App;
This completes our basic frontend implementation. It provides a user interface for viewing balances, order books, and placing orders.
Testing the DEX
To test our DEX, we'll need to:
- Deploy the smart contracts (if not already done)
- Create and fund test accounts
- Mint some test tokens
- Place orders and observe the matching process
Here's a script to help with testing (test_dex.js
):
const StellarSdk = require('stellar-sdk');
const { Contract, Server } = StellarSdk;
const server = new Server('https://horizon-testnet.stellar.org');
const dexContractId = 'YOUR_DEX_CONTRACT_ID';
const spcTokenId = 'YOUR_SPC_TOKEN_CONTRACT_ID';
const lncTokenId = 'YOUR_LNC_TOKEN_CONTRACT_ID';
async function createTestAccount() {
const pair = StellarSdk.Keypair.random();
await server.friendbot(pair.publicKey()).call();
return pair;
}
async function mintTestTokens(account, tokenId, amount) {
const contract = new Contract(tokenId);
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET
})
.addOperation(contract.call('mint', account.publicKey(), amount))
.setTimeout(30)
.build();
tx.sign(account);
await server.submitTransaction(tx);
}
async function placeOrder(account, isBuy, amount, price) {
const contract = new Contract(dexContractId);
const tx = new StellarSdk.TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: StellarSdk.Networks.TESTNET
})
.addOperation(contract.call('place_order', {
user: account.publicKey(),
pair: { base_asset: spcTokenId, quote_asset: lncTokenId },
amount: amount,
price: price,
is_buy: isBuy
}))
.setTimeout(30)
.build();
tx.sign(account);
await server.submitTransaction(tx);
}
async function runTest() {
const alice = await createTestAccount();
const bob = await createTestAccount();
await mintTestTokens(alice, spcTokenId, '1000');
await mintTestTokens(bob, lncTokenId, '1000');
await placeOrder(alice, true, '100', '1.2'); // Alice places a buy order
await placeOrder(bob, false, '100', '1.2'); // Bob places a matching sell order
console.log('Test completed successfully!');
}
runTest().catch(console.error);
Run this script with Node.js to test your DEX functionality.
Advanced Features and Optimizations
To take your DEX to the next level, consider implementing these advanced features:
Limit and Market Orders: Implement different order types to provide more trading options.
Order Book Optimization: Use a more efficient data structure (e.g., a binary heap) for faster order matching.
Fee System: Implement a small fee for trades to incentivize liquidity providers.
Liquidity Pools: Add automated market maker (AMM) functionality alongside the order book.
Cross-Asset Trades: Implement path payments to allow trading between any asset pairs.
Price Oracle: Integrate with external price feeds for more accurate pricing.
Trade History: Store and display recent trades for each asset pair.
Security Considerations
When building a DEX, security is paramount. Here are some key considerations:
Smart Contract Audits: Have your smart contracts audited by professional security researchers.
Rate Limiting: Implement rate limiting to prevent spam and potential DoS attacks.
Access Control: Ensure that only authorized users can perform sensitive operations.
Integer Overflow Protection: Use safe math operations to prevent integer overflows.
Reentrancy Guards: Implement checks to prevent reentrancy attacks.
Formal Verification: Consider using formal verification tools to mathematically prove the correctness of your smart contracts.
Upgrade Mechanism: Implement a secure upgrade mechanism to fix bugs and add features without compromising user funds.
Conclusion and Next Steps
Congratulations! You've built a basic decentralized exchange on Stellar. This project has introduced you to:
- Stellar's core concepts and smart contract platform (Soroban)
- Implementing complex financial logic in smart contracts
- Integrating Stellar operations with a web frontend
- Security considerations for decentralized finance applications
To continue your journey:
- Expand the frontend to include more user-friendly features like charts and detailed order history.
- Implement the advanced features mentioned above.
- Conduct thorough testing, including edge cases and stress tests.
- Consider the regulatory implications of running a DEX and consult with legal experts.
- Engage with the Stellar community for feedback and potential collaborations.
Remember, building a production-ready DEX requires extensive testing, auditing, and compliance considerations. This tutorial serves as a starting point for your exploration of decentralized finance on Stellar.
Happy coding, and welcome to the exciting world of decentralized exchanges!
Top comments (1)
Happy to read comments and take corrections on any error found in this article. Thank you