This repository contains a comprehensive implementation of an Automated Market Maker (AMM) DEX built on Sui Move. The project demonstrates core DeFi primitives including liquidity pools, token swaps, and fee collection mechanisms.
The DeFi Scaffold implements a Uniswap V2-like AMM with the following core components:
- TradingPair: The central object representing a liquidity pool for a pair of tokens
- PairRegistry: Maintains a registry of all trading pairs
- LiquidityToken: Represents ownership shares in a trading pair
- AdminCap: Capability object for administrative functions
The main module implementing the DEX functionality:
module defi_scaffold::dex_core;
This module contains the core trading pair implementation, pair registry, and all trading functions.
Contains mathematical helper functions for the DEX:
module defi_scaffold::dex_helper;
This module implements the constant product formula calculations, fee calculations, and other mathematical operations.
public struct TradingPair<phantom TokenA, phantom TokenB> has key {
id: UID,
reserve_a: Balance<TokenA>,
reserve_b: Balance<TokenB>,
liquidity_supply: Supply<LiquidityToken<TokenA, TokenB>>,
fee_rate_bps: u64,
protocol_fee_bps: u64,
collected_fees_a: Balance<TokenA>,
collected_fees_b: Balance<TokenB>,
}reserve_aandreserve_b: Token reserves for the trading pairliquidity_supply: Supply of liquidity tokensfee_rate_bps: Trading fee rate in basis points (1/100 of 1%)protocol_fee_bps: Percentage of trading fees allocated to protocol
public struct PairRegistry has key {
id: UID,
pairs: Table<PairKey, ID>,
}pairs: Table mapping token pair keys to trading pair IDs
public struct LiquidityToken<phantom TokenA, phantom TokenB> has drop {}- Represents ownership share in a trading pair
- Phantom type parameters ensure type safety
public fun create_pair<TokenA, TokenB>(
registry: &mut PairRegistry,
initial_a: Coin<TokenA>,
initial_b: Coin<TokenB>,
fee_rate_bps: u64,
ctx: &mut TxContext,
): Coin<LiquidityToken<TokenA, TokenB>>- Validates token order (A != B)
- Ensures pair doesn't already exist
- Validates fee rate is within limits
- Creates trading pair with initial liquidity
- Registers pair in registry
- Returns initial liquidity tokens
public fun add_liquidity<TokenA, TokenB>(
pair: &mut TradingPair<TokenA, TokenB>,
token_a: Coin<TokenA>,
token_b: Coin<TokenB>,
min_liquidity: u64,
ctx: &mut TxContext,
): (Coin<TokenA>, Coin<TokenB>, Coin<LiquidityToken<TokenA, TokenB>>)- Calculates optimal deposit amounts based on current ratio
- Adds tokens to reserves
- Calculates and mints liquidity tokens
- Returns unused tokens and new liquidity tokens
public fun remove_liquidity<TokenA, TokenB>(
pair: &mut TradingPair<TokenA, TokenB>,
liquidity_tokens: Coin<LiquidityToken<TokenA, TokenB>>,
min_amount_a: u64,
min_amount_b: u64,
ctx: &mut TxContext,
): (Coin<TokenA>, Coin<TokenB>)- Burns liquidity tokens
- Calculates withdrawal amounts proportionally
- Withdraws tokens from reserves
- Returns tokens to user
public fun swap_a_to_b<TokenA, TokenB>(
pair: &mut TradingPair<TokenA, TokenB>,
token_a: Coin<TokenA>,
min_amount_out: u64,
ctx: &mut TxContext,
): Coin<TokenB>- Calculates output amount using constant product formula
- Applies trading fee
- Splits fee between protocol and liquidity providers
- Updates reserves
- Returns output tokens
public fun collect_fees<TokenA, TokenB>(
pair: &mut TradingPair<TokenA, TokenB>,
_: &AdminCap,
ctx: &mut TxContext,
): (Coin<TokenA>, Coin<TokenB>)- Extracts accumulated protocol fees
- Returns fee tokens to admin
The AMM uses the constant product formula: x * y = k
For swaps, this means:
(reserve_a + amount_in * (1 - fee)) * reserve_b = k
Output amount is calculated as:
amount_out = reserve_b - (k / (reserve_a + amount_in * (1 - fee)))
Initial liquidity is calculated as:
initial_liquidity = sqrt(amount_a * amount_b)
For subsequent deposits:
liquidity_minted = min(
(amount_a * total_liquidity) / reserve_a,
(amount_b * total_liquidity) / reserve_b
)
The contract defines several error codes to handle invalid operations:
EInsufficientInput: Input amount is too smallESlippageExceeded: Output amount is below minimum thresholdEInvalidFee: Fee rate is outside allowed rangeEPairExists: Pair already exists in registryEInsufficientLiquidity: Not enough liquidity in poolEInvalidTokenOrder: Token types must be differentEZeroAmount: Amount cannot be zero
The contract emits events for key operations:
PairCreated: When a new trading pair is createdLiquidityAdded: When liquidity is added to a pairLiquidityRemoved: When liquidity is removed from a pairSwapExecuted: When a token swap is executed
- Slippage Protection: Users can specify minimum output amounts to protect against front-running
- Fee Mechanism: Fees are split between protocol and liquidity providers
- Type Safety: Phantom type parameters ensure type safety for trading pairs
- Capability-based Admin: Administrative functions require the AdminCap
The contract includes comprehensive tests covering:
-
Core functionality tests
- Pair creation
- Liquidity addition (proportional and unbalanced)
- Liquidity removal
- Token swaps (A→B and B→A)
- Fee collection
-
Error condition tests
- Duplicate pair creation
- Invalid token order
- Slippage protection
- Invalid fee rate
- Insufficient liquidity
-
Integration tests
- Complete trading cycle
- Multiple pairs interaction
-
Mathematical verification tests
- Constant product invariant
- Price calculation accuracy
let registry = test_scenario::take_shared<PairRegistry>(scenario);
let treasury_a = test_scenario::take_from_sender<TreasuryCap<TokenA>>(scenario);
let treasury_b = test_scenario::take_from_sender<TreasuryCap<TokenB>>(scenario);
let coin_a = coin::mint(&mut treasury_a, 1000, test_scenario::ctx(scenario));
let coin_b = coin::mint(&mut treasury_b, 2000, test_scenario::ctx(scenario));
let lp_tokens = dex_core::create_pair(
&mut registry,
coin_a,
coin_b,
300, // 3% fee
test_scenario::ctx(scenario)
);let pair = test_scenario::take_shared<TradingPair<TokenA, TokenB>>(scenario);
let coin_a = test_scenario::take_from_sender<Coin<TokenA>>(scenario);
let coin_b = test_scenario::take_from_sender<Coin<TokenB>>(scenario);
let (remaining_a, remaining_b, lp_tokens) = dex_core::add_liquidity(
&mut pair,
coin_a,
coin_b,
1, // Min liquidity
test_scenario::ctx(scenario)
);let pair = test_scenario::take_shared<TradingPair<TokenA, TokenB>>(scenario);
let coin_a = test_scenario::take_from_sender<Coin<TokenA>>(scenario);
let output_b = dex_core::swap_a_to_b(
&mut pair,
coin_a,
95, // Min amount out (slippage protection)
test_scenario::ctx(scenario)
);This DeFi scaffold provides a robust foundation for building decentralized exchange functionality on Sui Move. The implementation follows best practices for AMM design, including constant product formula, fee mechanisms, and comprehensive testing.


