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
475 lines
14 KiB
Rust
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, ¶ms.token_a);
|
|
storage::set(keys::TOKEN_B, ¶ms.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()
|
|
}
|
|
}
|