A blazing fast, production-ready decentralized exchange (DEX) on Stacks (Bitcoin L2) implementing a constant-product AMM (x · y = k).
- Constant-Product AMM: Simple and proven x · y = k formula
- 0.30% Fee: Deducted from input, retained in pool
- Slippage Protection: User-defined minimum output
- Deadline Protection: Block height-based expiration
- Post-Conditions: Frontend adds safety checks for user protection
stacks-dex/
├── contracts/
│ ├── pool.clar # Main AMM pool contract
│ ├── traits/
│ │ └── sip-010-trait.clar # SIP-010 fungible token trait
│ └── test-tokens/
│ ├── token-x.clar # Test token X
│ └── token-y.clar # Test token Y
├── frontend/
│ ├── src/
│ │ ├── app.js # Main application
│ │ └── styles.css # Styles
│ ├── index.html # Entry point
│ └── package.json # Dependencies
└── Clarinet.toml # Clarinet configuration
The pool contract implements a single-direction swap (X → Y) with:
FEE_BPS = 30(0.30%)BPS_DENOM = 10000
get-reserves
(define-read-only (get-reserves)
{ x: uint, y: uint }
)quote-x-for-y (dx uint)
;; Calculates output amount with fee:
;; dx_after_fee = dx * 9970 / 10000
;; dy = (reserve_y * dx_after_fee) / (reserve_x + dx_after_fee)swap-x-for-y (token-x, token-y, dx, min-dy, recipient, deadline)
Executes swap with:
- Deadline validation (
block-height <= deadline) - Input validation (
dx > 0) - Fee calculation (0.30% deducted from input)
- AMM formula application
- Slippage check (
dy >= min-dy) - Token transfers
- Reserve updates
| Code | Description |
|---|---|
| u100 | Zero input |
| u101 | Zero reserves |
| u102 | Deadline expired |
| u103 | Slippage exceeded |
| u104 | Insufficient liquidity |
| u105 | Token X transfer failed |
| u106 | Token Y transfer failed |
REOWN AppKit does NOT provide a Stacks-specific SDK. Stacks support is achieved by combining:
DEX Frontend
|
| Wallet UI + sessions
v
REOWN AppKit (chain-agnostic transport + UX)
|
| WalletConnect v2 transport
v
WalletConnect Stacks JSON-RPC
|
| stx_* methods
v
Stacks Wallet (Hiro / Xverse / Leather)
Key Points:
- REOWN AppKit: Provides wallet connection UI/UX and WalletConnect v2 transport
- WalletConnect Universal Provider: Chain-agnostic session management
- Stacks JSON-RPC Methods:
stx_getAddresses,stx_signTransaction,stx_signMessage - Frontend builds transactions, wallet only signs them
- Vanilla JavaScript (ES Modules)
- Vite for bundling
- @reown/appkit for wallet UI
- @walletconnect/universal-provider for WalletConnect v2
- @stacks/transactions for transaction building
- Connect/disconnect wallet
- Real-time quote calculation
- Configurable slippage tolerance (default: 0.5%)
- Configurable deadline (default: 20 blocks)
- Fee display
- Price impact calculation
Get user's Stacks addresses:
const response = await provider.request({
method: 'stx_getAddresses',
params: {}
}, 'stacks:2147483648'); // testnet chain ID
// Response: { addresses: [{ address: 'ST...', publicKey: '...' }] }Sign a Stacks transaction:
const signedTx = await provider.request({
method: 'stx_signTransaction',
params: {
transaction: '0x...', // Serialized unsigned tx
network: 'testnet',
address: 'ST...'
}
}, 'stacks:2147483648');Sign an arbitrary message:
const signature = await provider.request({
method: 'stx_signMessage',
params: {
message: 'Hello Stacks!',
address: 'ST...'
}
}, 'stacks:2147483648');- Node.js 18+
- Clarinet (for contract testing)
- Leather or Xverse wallet browser extension
- Install Clarinet
curl -L https://get.clarinet.dev | bash- Test Contracts
cd stacks-dex
clarinet check
clarinet test- Run Frontend
cd frontend
npm install
npm run dev- Deploy to Testnet
clarinet deployments generate --testnet
clarinet deployments apply -p deployments/testnet.yaml- Update Frontend Config
Update
CONFIGinfrontend/src/app.jswith deployed contract addresses.
While REOWN AppKit doesn't currently support Stacks, the Stacks ecosystem is working on WalletConnect integration. When available:
// Future WalletConnect example
import { createAppKit } from '@reown/appkit';
import { StacksAdapter } from '@reown/appkit-adapter-stacks'; // Not yet available
const appKit = createAppKit({
projectId: 'YOUR_PROJECT_ID',
adapters: [new StacksAdapter()],
networks: [stacksMainnet]
});For now, use @stacks/connect for production Stacks dApps.
The frontend uses REOWN AppKit as the transport layer with WalletConnect v2:
import { createAppKit } from '@reown/appkit';
import UniversalProvider from '@walletconnect/universal-provider';
// 1. Initialize Universal Provider
const provider = await UniversalProvider.init({
projectId: 'YOUR_PROJECT_ID',
metadata: { name: 'Stacks DEX', ... }
});
// 2. Connect with Stacks namespace
const session = await provider.connect({
namespaces: {
stacks: {
methods: ['stx_getAddresses', 'stx_signTransaction', 'stx_signMessage'],
chains: ['stacks:2147483648'], // testnet
events: ['accountsChanged']
}
}
});
// 3. Use stx_* methods for Stacks operations
const addresses = await provider.request({
method: 'stx_getAddresses',
params: {}
}, 'stacks:2147483648');Given reserves
This ensures:
- Fee is deducted from input (stays in pool)
- Product invariant is maintained:
$(x + \Delta x_{fee})(y - \Delta y) \geq xy$
- No Admin Functions: Contract is immutable after deployment
- Slippage Protection: Users set minimum acceptable output
- Deadline: Prevents stale transactions
- Post-Conditions: Frontend adds additional safety checks
- Integer Math: All calculations use uint to prevent overflow/underflow
MIT