Constant-product Automated Market Maker (AMM) smart contract written in Rust for Stellar Soroban.
This contract implements a Uniswap V2-style AMM on Stellar Soroban. It holds reserves for two SEP-41 Stellar tokens, executes atomic swaps, and issues LP tokens representing each provider's proportional share.
┌─────────────────────────────────────┐
│ NodusAmm │
│ │
│ reserve_0 ──── reserve_1 │
│ \ / │
│ k = x * y (invariant) │
│ │
│ add_liquidity() → mint LP tokens │
│ remove_liquidity() → burn LP tokens│
│ swap() │
│ sync() (drift correction) │
│ │
│ TWAP price accumulators │
│ (price_0_cumulative_last, …) │
└─────────────────────────────────────┘
LP tokens are tracked internally in the pool's persistent storage — no separate token contract is required.
src/
lib.rs Contract entry point — all public functions
liquidity_pool.rs Pool math: optimal amounts, K-invariant, LP mint/burn
lp_token.rs Internal LP ledger: mint, burn, transfer, approve, allowance
math.rs AMM formulas: get_amount_out, get_amount_in, sqrt
storage.rs DataKey enum for all instance + persistent storage keys
events.rs Soroban event wrappers: Mint, Burn, Swap, Sync
errors.rs Stable #[contracterror] enum
traits.rs IAmmPool interface definition
tests/
unit_tests.rs Pure math + liquidity-pool unit tests (no Soroban env)
integration_tests.rs Soroban testenv contract interaction tests
fuzz_tests.rs Property tests: k-invariant, sqrt floor, fee monotonicity
| Function | Auth | Description |
|---|---|---|
initialize(token_0, token_1) |
— | One-time setup. Stores token addresses. |
sync() |
— | Reconcile reserves with actual contract token balances. |
| Function | Auth | Description |
|---|---|---|
add_liquidity(from, to, amount_0_desired, amount_1_desired, amount_0_min, amount_1_min, deadline) |
from |
Deposit tokens, receive LP tokens. |
remove_liquidity(from, to, liquidity, amount_0_min, amount_1_min, deadline) |
from |
Burn LP tokens, receive underlying tokens. |
| Function | Auth | Description |
|---|---|---|
swap(to, amount_0_out, amount_1_out) |
— | Low-level swap. Caller must transfer tokens in before calling; K-invariant enforced post-swap. |
get_amount_out(amount_in, reserve_in, reserve_out) |
— | Quote output for a given input (0.3% fee). |
get_amount_in(amount_out, reserve_in, reserve_out) |
— | Quote input required to receive a given output. |
| Function | Auth | Description |
|---|---|---|
lp_balance_of(owner) |
— | Return LP token balance. |
lp_total_supply() |
— | Return total LP tokens in circulation. |
transfer_lp(from, to, amount) |
from |
Transfer LP tokens directly. |
approve_lp(owner, spender, amount) |
owner |
Approve spender to transfer up to amount LP tokens. |
lp_allowance(owner, spender) |
— | Return remaining approved LP amount. |
transfer_lp_from(spender, from, to, amount) |
spender |
Transfer LP tokens using an existing allowance. |
| Function | Description |
|---|---|
get_reserves() |
Returns (reserve_0, reserve_1, timestamp_last). |
get_price_cumulative() |
Returns (price_0_cumulative_last, price_1_cumulative_last) for TWAP. |
token_0() / token_1() |
Return the configured token contract addresses. |
# Install Stellar CLI
cargo install --locked stellar-cli --features opt
# Build (produces optimised WASM)
make build
# or: stellar contract build
# Run tests
make test
# Lint
make lint# Testnet
STELLAR_SECRET_KEY=S... TOKEN_0=C... TOKEN_1=C... make deploy-testnet
# Mainnet
STELLAR_SECRET_KEY=S... TOKEN_0=C... TOKEN_1=C... make deploy-mainnetThe deploy script uploads the WASM, deploys a new contract instance, and calls initialize.
The AMM uses the constant-product formula k = x * y with a 0.3% fee (997/1000):
amount_out = (amount_in * 997 * reserve_out) / (reserve_in * 1000 + amount_in * 997)
The minimum liquidity constant (MINIMUM_LIQUIDITY = 1000) is permanently locked in the dead address on first deposit to prevent price manipulation attacks on empty pools.
TWAP price accumulators use Q32.32 fixed-point: price_cumulative += (reserve_1 << 32) / reserve_0 * time_elapsed.
MIT