A full-stack decentralized Automated Market Maker (AMM) built on Sui blockchain, featuring constant-product (x * y = k) invariant for token swaps and liquidity pools.
The system is divided into three main components that work together to provide a seamless decentralized trading experience:
- Smart Contracts (Move): On-chain logic for AMM operations
- Backend Server (Node.js/Express): Indexing and data aggregation layer
- Web Frontend (Next.js): User interface for interacting with the AMM
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Frontend │◄───►│ Backend │◄───►│ Sui │
│ (Next.js) │ │ (Node/Express)│ │ Blockchain │
│ │ │ │ │ (Move) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
The on-chain component that handles all AMM operations. Built using Move on the Sui blockchain.
- mini_amm.move: Main entry point and container for pools
- pool.move: Manages liquidity pools and LP tokens
- swap.move: Handles token swaps with constant-product formula
- treasury.move: Manages protocol fees and treasury operations
- admin.move: Administrative functions and access control
- Constant product formula (x * y = k) for price determination
- Multi-pool support for different token pairs
- Liquidity provider fee (0.3% per swap)
- Slippage protection
- Admin-controlled fee parameters
A service layer that provides additional functionality not suitable for on-chain execution.
-
API Endpoints:
/api/pools: Get all pools with current stats/api/transactions: Historical transaction data
-
Services:
- Event indexing and database synchronization
- Historical data aggregation
- Price and volume calculations
- Transaction simulation
-
Database (PostgreSQL):
- Stores indexed blockchain data
- Caches pool and token information
- Maintains transaction history
A modern, responsive web interface for users to interact with the AMM.
- Wallet connection (Sui Wallet, etc.)
- Pool creation and management
- Token swapping interface
- Liquidity provision and removal
- Transaction history
- Real-time price and pool statistics
PoolInterface.tsx: Main component for pool interactionsSwapInterface.tsx: Token swap interfaceTransactionsInterface.tsx: User transaction history
- Node.js 16+
- Sui CLI
- PostgreSQL (for backend)
- Yarn or npm
# Build contracts
cd contracts
sui move build
# Test contracts
sui move test
# Publish to testnet (example)
sui client publish # Install dependencies
cd server
npm install
# Set up environment variables
cp .env.example .env
# Edit .env with your configuration
# Run database migrations
npx prisma migrate dev
# Start development server
npm run dev# Install dependencies
cd client
npm install
# Set up environment variables
cp .env.local.example .env.local
# Edit .env.local with your configuration
# Start development server
npm start- Frontend calls
create_poolon the smart contract - Contract validates parameters and creates new pool
- Backend indexes the new pool and adds it to the database
- Frontend updates UI to show the new pool
- User selects tokens and amount in the frontend
- Frontend queries contract for expected output amount
- User approves transaction in their wallet
- Frontend submits transaction to the blockchain
- Backend indexes the swap event
- UI updates to reflect the new pool state
- All sensitive operations require wallet signatures
- Frontend validates all user inputs
- Backend implements rate limiting and request validation
Contributions are welcome! Please open an issue or submit a pull request.
Mini AMM is a concise AMM example to demonstrate:
- Writing Move smart contracts that implement liquidity pools and swaps
- Implementing the constant-product x * y = k AMM logic
- Providing functions for creating pools, adding/removing liquidity, and swapping
- A simple Next.js + TypeScript front-end showing how to discover wallet-owned coins and submit transactions using the Sui/DappKit SDK
The contracts live in contracts/sources/. The front-end is a Next.js app under client/.
The Move modules implement the AMM primitives. These files are in contracts/sources/.
Note: function names and signatures in the Move code may vary slightly; the README describes the high-level API and expected behavior.
This module is the main AMM entry point. It typically exposes:
create_pool<A, B>(container: &mut Container, coin_a: Coin<A>, coin_b: Coin<B>, ctx: &mut TxContext)- Creates a new liquidity pool for token types
AandB. - Accepts coin objects for initial liquidity (these are Move
Coin<T>objects). The Move call consumes the provided coins and mints a liquidity pool object and initial LP tokens.
- Creates a new liquidity pool for token types
- The module will reference a package-level
Containerobject that stores global state for the AMM (owner, admin settings, fee config).
Implements per-pool logic, likely including:
- A
LiquidityPool<A, B>struct which stores the reserves (amounts of A and B), fee parameters, and handles. add_liquidity(pool_id: &ObjectId, coin_a: Coin<A>, coin_b: Coin<B>)(or similar) — adds liquidity to an existing pool.remove_liquidity(pool_id: &ObjectId, lp_coin: Coin<LP<T>>, amount: u128)— burns LP tokens and returns underlying assets in proportion to pool reserves.
The implementation ensures that reserves are updated atomically and LP tokens are minted/burned accordingly.
Handles user swaps using the constant-product invariant. Typical function:
swap<A, B>(pool_id: &ObjectId, coin_in: Coin<A>, min_out: u128, ctx: &mut TxContext)- Consumes
coin_inof typeAand returns some amount ofBaccording to the AMM pricing. - Requires a
min_outto protect against excessive slippage. - Applies trading fees and updates reserves.
- Consumes
A simple module for collecting fees and distributing to a treasury object. Fees collected from swaps or liquidity operations may be sent here.
Administration helpers, package deployment helpers, or governance-like configuration (fee rates, owner change) may be here.
The AMM implemented here follows the Uniswap v2-like invariant:
- Let x and y be the reserves of token A and token B in the pool.
- The invariant is x * y = k (constant product).
A user who wants to swap Δx (an amount of token A) into the pool receives Δy of token B such that the new reserves x' and y' satisfy (x + Δx') * (y - Δy) = k, where Δx' is the effective amount after fees.
Solving for Δy (ignoring fees) yields:
Δy = y - k / (x + Δx)
Which simplifies to:
Δy = (Δx * y) / (x + Δx)
When fees are applied (e.g., a 0.3% fee), the effective amount of input used in the invariant is reduced (Δx' = Δx * (1 - fee_fraction)). The fee is typically subtracted from the input amount before calculating output.
Slippage is the difference between the expected price based on reserves and the price realized due to trade size. Large trades relative to reserves cause greater price impact.
Approximate instantaneous price (without fees) is:
price = y / x
A trade that moves the reserves changes the marginal price.
Most AMMs take a small fee on each swap (for example, 0.3%). Fees can be implemented by reducing the effective input used to calculate Δy and sending the fee portion to a treasury.
- Liquidity providers deposit token pairs (A and B) into the pool in proportion to the current reserves to avoid price changes on deposit.
- In return, LP tokens are minted to represent a share of the pool's reserves.
- When LPs later burn their LP tokens, they receive a proportional share of the reserves (plus any fees that were accrued to the pool while they were providing liquidity).
If a provider deposits amounts not perfectly in proportion to current reserves, the system commonly either rejects or performs an implied swap to balance ratios, depending on implementation. A minimal AMM usually expects deposits in existing ratio.
When you provide liquidity, the value of your deposited tokens vs. holding them can diverge due to price changes. This divergence is known as impermanent loss. It is "impermanent" because if prices return to original relative values, loss disappears; however, if you withdraw when prices have diverged, the loss becomes realized.
Fees earned by LPs can offset impermanent loss.
The front-end is a Next.js + TypeScript app located in client/. It contains components for:
- Listing pools and basic stats
- Creating a new pool
- Adding/removing liquidity
- Swapping tokens (if implemented)
Key front-end files:
client/app/components/PoolInterface.tsx- Lists pools discovered from on-chain package-owned objects.
- When creating a pool, it enumerates the connected wallet's coins, lets the user choose specific coin objects (object ids) for each token, and specify how much to deposit.
- Uses the DappKit/Sui SDK hooks:
useSignAndExecuteTransaction,useSuiClientto fetch owned objects and sign transactions. - Important UX details included in the client code:
- Friendly token symbol extraction from Move type strings (e.g., detect
SUI,USDC, or fallback to last segment of Move type). - Robust parsing of different RPC response shapes (different wallet clients may return different JSON shapes).
- Splitting coin objects in-transaction (via
splitCoins) to produce coin fragments of requested amounts, and passing those fragments to Movecreate_pooloradd_liquiditycalls.
- Friendly token symbol extraction from Move type strings (e.g., detect
Transaction pattern used by the front-end:
- Build a
Transaction(the client uses@mysten/sui/transactions). - Use
tx.splitCoins(coinObjectId, [amount])(or SDK equivalent) to create coin fragments. - Call
tx.moveCall(...)with the appropriate package/module/function and pass thetx.object(containerId)and the split coin references as arguments. - Sign and submit with
signAndExecuteTransaction({ transactionBlock: tx }).
This pattern ensures the on-chain Move function receives real Coin<T> values representing the exact amounts the user wants to deposit or swap.
- Node.js (16+ recommended)
- npm or yarn
- Rust and Move tools (if you want to build/publish Move packages)
- Sui client / localnet or testnet access and wallet extension (or
sui client) configured
From the repo root (or client/), install and run:
# from repo root
npm --prefix client install
npm --prefix client run dev
# or to build for production
npm --prefix client run buildOpen http://localhost:3000 (or the port Next prints) and connect a Sui-compatible wallet. The Pool UI should list pools created by the deployed package and allow creating pools using coins owned by the connected wallet.
If you modify the Move code and want to publish to local Sui devnet or testnet:
- Ensure
suiis installed and configured. - From
contracts/run:
# build and publish (example; your environment may vary)
cd contracts
sui move build
sui client publishPublishing returns the package id which the front-end must use (the TESTNET_PACKAGE_ID constant in client/app/constants.ts or similar). The front-end relies on the package id to fetch package-owned objects (pools, container) and call move functions.
If Move tests exist in contracts/tests/, run Move tests using the Sui toolchain or sui move test (depending on your setup). Example:
cd contracts
sui move testFront-end unit/integration tests (if present) can be run with Jest or the configured test runner (not included by default in this minimal project).
- User picks token A object id and token B object id (from wallet-owned objects).
- User enters amounts for token A and token B.
- Client builds transaction:
splitA = tx.splitCoins(coinAObjectId, [amountA])splitB = tx.splitCoins(coinBObjectId, [amountB])tx.moveCall({ target:${PACKAGE_ID}::mini_amm::create_pool, typeArguments: [typeA, typeB], arguments: [tx.object(containerId), splitA, splitB] })
- Sign & execute.
On-chain, create_pool consumes the provided coins, mints a LiquidityPool<A,B> object, and returns/creates LP tokens.
- Client splits coin objects to get the exact amounts.
- Calls
pool::add_liquidity(pool_id, coinAFragment, coinBFragment). - The contract updates reserves and mints LP tokens to the provider.
- Client calls
swap(pool_id, coin_in_fragment, min_out). - Contract applies fee, computes output using constant-product formula, updates reserves, and returns the output coin.
.
├── client/ # Next.js frontend application
│ ├── app/ # App router pages and components
│ │ ├── components/ # Reusable UI components
│ │ │ ├── PoolInterface.tsx # Pool management interface
│ │ │ ├── SwapInterface.tsx # Token swap interface
│ │ │ └── TransactionsInterface.tsx # Transaction history
│ │ └── ...
│ └── ...
│
├── server/ # Backend server (Node.js/Express)
│ ├── config/ # Configuration files
│ ├── crons/ # Scheduled tasks
│ ├── prisma/ # Database schema and migrations
│ ├── routes/ # API route handlers
│ │ ├── pools.ts # Pool-related endpoints
│ │ └── transactions.ts # Transaction endpoints
│ ├── services/ # Business logic
│ └── app.ts # Express application setup
│
└── contracts/ # Sui Move smart contracts
├── sources/ # Move source files
│ ├── mini_amm.move # Main AMM module
│ ├── pool.move # Pool management
│ ├── swap.move # Swap functionality
│ ├── treasury.move # Fee management
│ └── admin.move # Admin controls
├── tests/ # Move tests
└── Move.toml # Move package configuration
- Improve UI UX for token selection: show token icons and decimals, and fetch metadata.
- Replace Number-based math with BigInt or a bignumber library to safely handle u128 and decimals.
- Add inline validation and better error messages instead of
alert(). - Implement swap UI with slippage tolerances and estimated price preview.
- Add unit and integration tests for Move modules and front-end flows.
Contributions, bug fixes, and improvements are welcome. Please open issues or PRs describing the change and include tests where feasible.