synor/contracts/dex/src/lib.rs
Gulshan Yadav 48949ebb3f Initial commit: Synor blockchain monorepo
A complete blockchain implementation featuring:
- synord: Full node with GHOSTDAG consensus
- explorer-web: Modern React blockchain explorer with 3D DAG visualization
- CLI wallet and tools
- Smart contract SDK and example contracts (DEX, NFT, token)
- WASM crypto library for browser/mobile
2026-01-08 05:22:17 +05:30

475 lines
14 KiB
Rust

//! Simple AMM DEX Contract
//!
//! A Uniswap V2-style automated market maker for Synor.
//!
//! # Features
//! - Liquidity pools for token pairs
//! - Constant product formula (x * y = k)
//! - LP tokens for liquidity providers
//! - Swap with 0.3% fee
//!
//! # Methods
//! - `init(token_a, token_b)` - Create a new pair
//! - `add_liquidity(amount_a, amount_b, min_lp)` - Add liquidity
//! - `remove_liquidity(lp_amount, min_a, min_b)` - Remove liquidity
//! - `swap_a_for_b(amount_in, min_out)` - Swap token A for B
//! - `swap_b_for_a(amount_in, min_out)` - Swap token B for A
//! - `get_reserves() -> (u64, u64)` - Get current reserves
//! - `get_lp_balance(owner) -> u64` - Get LP token balance
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use borsh::BorshDeserialize;
use synor_sdk::prelude::*;
use synor_sdk::require;
// ==================== Constants ====================
/// Swap fee: 0.3% (30 basis points)
const SWAP_FEE_BPS: u64 = 30;
const BPS_DENOMINATOR: u64 = 10000;
/// Minimum liquidity locked forever (prevents division by zero attacks)
const MINIMUM_LIQUIDITY: u64 = 1000;
// ==================== Storage Keys ====================
mod keys {
pub const TOKEN_A: &[u8] = b"dex:token_a";
pub const TOKEN_B: &[u8] = b"dex:token_b";
pub const RESERVE_A: &[u8] = b"dex:reserve_a";
pub const RESERVE_B: &[u8] = b"dex:reserve_b";
pub const LP_TOTAL_SUPPLY: &[u8] = b"dex:lp_total";
pub const LP_BALANCES: &[u8] = b"dex:lp_balances";
pub const OWNER: &[u8] = b"dex:owner";
pub const INITIALIZED: &[u8] = b"dex:initialized";
}
// ==================== Storage Helpers ====================
fn lp_balances() -> storage::Map<[u8; 34], u64> {
storage::Map::new(keys::LP_BALANCES)
}
fn get_reserves() -> (u64, u64) {
let reserve_a: u64 = storage::get(keys::RESERVE_A).unwrap_or(0);
let reserve_b: u64 = storage::get(keys::RESERVE_B).unwrap_or(0);
(reserve_a, reserve_b)
}
fn set_reserves(reserve_a: u64, reserve_b: u64) {
storage::set(keys::RESERVE_A, &reserve_a);
storage::set(keys::RESERVE_B, &reserve_b);
}
fn get_lp_total_supply() -> u64 {
storage::get(keys::LP_TOTAL_SUPPLY).unwrap_or(0)
}
fn set_lp_total_supply(supply: u64) {
storage::set(keys::LP_TOTAL_SUPPLY, &supply);
}
fn mint_lp(to: &Address, amount: u64) {
let balance = lp_balances().get_or_default(&to.0);
lp_balances().set(&to.0, &(balance + amount));
set_lp_total_supply(get_lp_total_supply() + amount);
}
fn burn_lp(from: &Address, amount: u64) -> Result<()> {
let balance = lp_balances().get_or_default(&from.0);
require!(
balance >= amount,
Error::InsufficientBalance {
required: amount,
available: balance
}
);
lp_balances().set(&from.0, &(balance - amount));
set_lp_total_supply(get_lp_total_supply().saturating_sub(amount));
Ok(())
}
// ==================== Entry Points ====================
synor_sdk::entry_point!(init, call);
/// Initialize the DEX pair.
fn init(params: &[u8]) -> Result<()> {
#[derive(BorshDeserialize)]
struct InitParams {
token_a: Address,
token_b: Address,
}
let params = InitParams::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected token_a, token_b"))?;
require!(
params.token_a != params.token_b,
Error::invalid_args("Tokens must be different")
);
// Store token addresses
storage::set(keys::TOKEN_A, &params.token_a);
storage::set(keys::TOKEN_B, &params.token_b);
storage::set(keys::OWNER, &caller());
storage::set(keys::INITIALIZED, &true);
// Initialize reserves to zero
set_reserves(0, 0);
set_lp_total_supply(0);
emit(&PairCreated {
token_a: params.token_a,
token_b: params.token_b,
});
Ok(())
}
/// Handle contract calls.
fn call(selector: &[u8], params: &[u8]) -> Result<Vec<u8>> {
let add_liquidity_sel = synor_sdk::method_selector("add_liquidity");
let remove_liquidity_sel = synor_sdk::method_selector("remove_liquidity");
let swap_a_for_b_sel = synor_sdk::method_selector("swap_a_for_b");
let swap_b_for_a_sel = synor_sdk::method_selector("swap_b_for_a");
let get_reserves_sel = synor_sdk::method_selector("get_reserves");
let get_lp_balance_sel = synor_sdk::method_selector("get_lp_balance");
let get_tokens_sel = synor_sdk::method_selector("get_tokens");
let quote_sel = synor_sdk::method_selector("quote");
match selector {
s if s == add_liquidity_sel => {
#[derive(BorshDeserialize)]
struct Args {
amount_a: u64,
amount_b: u64,
min_lp: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected amount_a, amount_b, min_lp"))?;
let lp_minted = do_add_liquidity(args.amount_a, args.amount_b, args.min_lp)?;
Ok(borsh::to_vec(&lp_minted).unwrap())
}
s if s == remove_liquidity_sel => {
#[derive(BorshDeserialize)]
struct Args {
lp_amount: u64,
min_a: u64,
min_b: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected lp_amount, min_a, min_b"))?;
let (amount_a, amount_b) = do_remove_liquidity(args.lp_amount, args.min_a, args.min_b)?;
Ok(borsh::to_vec(&(amount_a, amount_b)).unwrap())
}
s if s == swap_a_for_b_sel => {
#[derive(BorshDeserialize)]
struct Args {
amount_in: u64,
min_out: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected amount_in, min_out"))?;
let amount_out = do_swap(args.amount_in, args.min_out, true)?;
Ok(borsh::to_vec(&amount_out).unwrap())
}
s if s == swap_b_for_a_sel => {
#[derive(BorshDeserialize)]
struct Args {
amount_in: u64,
min_out: u64,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected amount_in, min_out"))?;
let amount_out = do_swap(args.amount_in, args.min_out, false)?;
Ok(borsh::to_vec(&amount_out).unwrap())
}
s if s == get_reserves_sel => {
let reserves = get_reserves();
Ok(borsh::to_vec(&reserves).unwrap())
}
s if s == get_lp_balance_sel => {
#[derive(BorshDeserialize)]
struct Args {
owner: Address,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected owner"))?;
let balance = lp_balances().get_or_default(&args.owner.0);
Ok(borsh::to_vec(&balance).unwrap())
}
s if s == get_tokens_sel => {
let token_a: Address = storage::get(keys::TOKEN_A).unwrap_or(Address::zero());
let token_b: Address = storage::get(keys::TOKEN_B).unwrap_or(Address::zero());
Ok(borsh::to_vec(&(token_a, token_b)).unwrap())
}
s if s == quote_sel => {
#[derive(BorshDeserialize)]
struct Args {
amount_in: u64,
a_to_b: bool,
}
let args = Args::try_from_slice(params)
.map_err(|_| Error::invalid_args("Expected amount_in, a_to_b"))?;
let (reserve_a, reserve_b) = get_reserves();
let (reserve_in, reserve_out) = if args.a_to_b {
(reserve_a, reserve_b)
} else {
(reserve_b, reserve_a)
};
let amount_out = calculate_output(args.amount_in, reserve_in, reserve_out);
Ok(borsh::to_vec(&amount_out).unwrap())
}
_ => Err(Error::InvalidMethod),
}
}
// ==================== Internal Functions ====================
fn do_add_liquidity(amount_a: u64, amount_b: u64, min_lp: u64) -> Result<u64> {
require!(amount_a > 0 && amount_b > 0, Error::invalid_args("Amounts must be positive"));
let (reserve_a, reserve_b) = get_reserves();
let lp_supply = get_lp_total_supply();
let lp_to_mint = if lp_supply == 0 {
// First liquidity provider - use geometric mean
let liquidity = sqrt(amount_a as u128 * amount_b as u128) as u64;
require!(
liquidity > MINIMUM_LIQUIDITY,
Error::invalid_args("Initial liquidity too low")
);
// Lock minimum liquidity forever
mint_lp(&Address::zero(), MINIMUM_LIQUIDITY);
liquidity - MINIMUM_LIQUIDITY
} else {
// Proportional to existing liquidity
let lp_a = (amount_a as u128 * lp_supply as u128 / reserve_a as u128) as u64;
let lp_b = (amount_b as u128 * lp_supply as u128 / reserve_b as u128) as u64;
lp_a.min(lp_b)
};
require!(
lp_to_mint >= min_lp,
Error::invalid_args("Insufficient LP tokens minted")
);
// Update reserves
set_reserves(reserve_a + amount_a, reserve_b + amount_b);
// Mint LP tokens
let provider = caller();
mint_lp(&provider, lp_to_mint);
emit(&LiquidityAdded {
provider,
amount_a,
amount_b,
lp_minted: lp_to_mint,
});
Ok(lp_to_mint)
}
fn do_remove_liquidity(lp_amount: u64, min_a: u64, min_b: u64) -> Result<(u64, u64)> {
require!(lp_amount > 0, Error::invalid_args("Amount must be positive"));
let (reserve_a, reserve_b) = get_reserves();
let lp_supply = get_lp_total_supply();
require!(lp_supply > 0, Error::invalid_args("No liquidity"));
// Calculate proportional amounts
let amount_a = (lp_amount as u128 * reserve_a as u128 / lp_supply as u128) as u64;
let amount_b = (lp_amount as u128 * reserve_b as u128 / lp_supply as u128) as u64;
require!(
amount_a >= min_a && amount_b >= min_b,
Error::invalid_args("Slippage too high")
);
// Burn LP tokens
let provider = caller();
burn_lp(&provider, lp_amount)?;
// Update reserves
set_reserves(reserve_a - amount_a, reserve_b - amount_b);
emit(&LiquidityRemoved {
provider,
amount_a,
amount_b,
lp_burned: lp_amount,
});
Ok((amount_a, amount_b))
}
fn do_swap(amount_in: u64, min_out: u64, a_to_b: bool) -> Result<u64> {
require!(amount_in > 0, Error::invalid_args("Amount must be positive"));
let (reserve_a, reserve_b) = get_reserves();
let (reserve_in, reserve_out) = if a_to_b {
(reserve_a, reserve_b)
} else {
(reserve_b, reserve_a)
};
require!(reserve_in > 0 && reserve_out > 0, Error::invalid_args("No liquidity"));
let amount_out = calculate_output(amount_in, reserve_in, reserve_out);
require!(
amount_out >= min_out,
Error::invalid_args("Slippage too high")
);
// Update reserves
let (new_a, new_b) = if a_to_b {
(reserve_a + amount_in, reserve_b - amount_out)
} else {
(reserve_a - amount_out, reserve_b + amount_in)
};
set_reserves(new_a, new_b);
emit(&Swap {
trader: caller(),
amount_in,
amount_out,
a_to_b,
});
Ok(amount_out)
}
/// Calculate output amount using constant product formula with fee.
/// amount_out = (amount_in * fee_multiplier * reserve_out) / (reserve_in + amount_in * fee_multiplier)
fn calculate_output(amount_in: u64, reserve_in: u64, reserve_out: u64) -> u64 {
let amount_in_with_fee = amount_in as u128 * (BPS_DENOMINATOR - SWAP_FEE_BPS) as u128;
let numerator = amount_in_with_fee * reserve_out as u128;
let denominator = reserve_in as u128 * BPS_DENOMINATOR as u128 + amount_in_with_fee;
if denominator == 0 {
0
} else {
(numerator / denominator) as u64
}
}
/// Integer square root using Newton's method.
fn sqrt(n: u128) -> u128 {
if n == 0 {
return 0;
}
let mut x = n;
let mut y = (x + 1) / 2;
while y < x {
x = y;
y = (x + n / x) / 2;
}
x
}
// ==================== Events ====================
use synor_sdk::events::{Event, Topic};
#[derive(borsh::BorshSerialize)]
struct PairCreated {
token_a: Address,
token_b: Address,
}
impl Event for PairCreated {
fn topics(&self) -> alloc::vec::Vec<Topic> {
alloc::vec![
Topic::from_name("PairCreated"),
Topic::from(&self.token_a),
Topic::from(&self.token_b),
]
}
fn data(&self) -> alloc::vec::Vec<u8> {
alloc::vec::Vec::new()
}
}
#[derive(borsh::BorshSerialize)]
struct LiquidityAdded {
provider: Address,
amount_a: u64,
amount_b: u64,
lp_minted: u64,
}
impl Event for LiquidityAdded {
fn topics(&self) -> alloc::vec::Vec<Topic> {
alloc::vec![
Topic::from_name("LiquidityAdded"),
Topic::from(&self.provider),
]
}
fn data(&self) -> alloc::vec::Vec<u8> {
borsh::to_vec(self).unwrap_or_default()
}
}
#[derive(borsh::BorshSerialize)]
struct LiquidityRemoved {
provider: Address,
amount_a: u64,
amount_b: u64,
lp_burned: u64,
}
impl Event for LiquidityRemoved {
fn topics(&self) -> alloc::vec::Vec<Topic> {
alloc::vec![
Topic::from_name("LiquidityRemoved"),
Topic::from(&self.provider),
]
}
fn data(&self) -> alloc::vec::Vec<u8> {
borsh::to_vec(self).unwrap_or_default()
}
}
#[derive(borsh::BorshSerialize)]
struct Swap {
trader: Address,
amount_in: u64,
amount_out: u64,
a_to_b: bool,
}
impl Event for Swap {
fn topics(&self) -> alloc::vec::Vec<Topic> {
alloc::vec![
Topic::from_name("Swap"),
Topic::from(&self.trader),
]
}
fn data(&self) -> alloc::vec::Vec<u8> {
borsh::to_vec(self).unwrap_or_default()
}
}