DeFi Protocol
Build a complete DeFi protocol with liquidity pools, staking, and yield farming.
Overview
This example demonstrates how to create a full-featured DeFi protocol using ao-forge, including:
- Liquidity Pools - Provide and manage liquidity
- Staking System - Stake tokens and earn rewards
- Yield Farming - Farm yields from various strategies
- Token Swaps - Exchange tokens with minimal slippage
- Governance - Protocol governance and voting
- Analytics - Track protocol performance and metrics
Project Setup
Create the Project
# Create new DeFi protocol project
ao-forge init my-defi-protocol --template defi
# Or create manually
ao-forge init my-defi-protocol --framework nextjs
cd my-defi-protocol
Install Dependencies
# Install additional dependencies
npm install @solana/web3.js @solana/wallet-adapter-react
npm install @headlessui/react @heroicons/react
npm install recharts
npm install date-fns
AO Process Implementation
DeFi Contract (defi.lua)
-- DeFi Protocol with liquidity pools and staking
local defi = {}
-- State management
local function getPool(id)
return json.decode(AO.load("pool:" .. id) or "{}")
end
local function setPool(id, pool)
AO.store("pool:" .. id, json.encode(pool))
end
local function getStake(address, poolId)
return json.decode(AO.load("stake:" .. address .. ":" .. poolId) or "{}")
end
local function setStake(address, poolId, stake)
AO.store("stake:" .. address .. ":" .. poolId, json.encode(stake))
end
-- Create liquidity pool
Handlers.add("create_pool", function(msg)
local poolId = msg.PoolId or tostring(AO.load("pool_counter") or "0")
local pool = {
id = poolId,
tokenA = msg.TokenA,
tokenB = msg.TokenB,
reserveA = 0,
reserveB = 0,
totalSupply = 0,
fee = tonumber(msg.Fee) or 0.003, -- 0.3% default fee
created = msg.Timestamp,
creator = msg.From
}
setPool(poolId, pool)
-- Update pool counter
AO.store("pool_counter", tostring(tonumber(poolId) + 1))
return { success = true, pool = pool }
end)
-- Add liquidity
Handlers.add("add_liquidity", function(msg)
local poolId = msg.PoolId
local amountA = tonumber(msg.AmountA)
local amountB = tonumber(msg.AmountB)
local provider = msg.From
local pool = getPool(poolId)
if not pool then
return { error = "Pool not found" }
end
-- Calculate LP tokens to mint
local lpTokens = 0
if pool.totalSupply == 0 then
lpTokens = math.sqrt(amountA * amountB)
else
lpTokens = math.min(
(amountA * pool.totalSupply) / pool.reserveA,
(amountB * pool.totalSupply) / pool.reserveB
)
end
-- Update pool reserves
pool.reserveA = pool.reserveA + amountA
pool.reserveB = pool.reserveB + amountB
pool.totalSupply = pool.totalSupply + lpTokens
setPool(poolId, pool)
-- Record provider's LP tokens
local providerTokens = tonumber(AO.load("lp_tokens:" .. provider .. ":" .. poolId) or "0")
AO.store("lp_tokens:" .. provider .. ":" .. poolId, tostring(providerTokens + lpTokens))
return { success = true, lpTokens = lpTokens, pool = pool }
end)
-- Remove liquidity
Handlers.add("remove_liquidity", function(msg)
local poolId = msg.PoolId
local lpTokens = tonumber(msg.LPTokens)
local provider = msg.From
local pool = getPool(poolId)
if not pool then
return { error = "Pool not found" }
end
-- Check provider has enough LP tokens
local providerTokens = tonumber(AO.load("lp_tokens:" .. provider .. ":" .. poolId) or "0")
if providerTokens < lpTokens then
return { error = "Insufficient LP tokens" }
end
-- Calculate amounts to return
local amountA = (lpTokens * pool.reserveA) / pool.totalSupply
local amountB = (lpTokens * pool.reserveB) / pool.totalSupply
-- Update pool reserves
pool.reserveA = pool.reserveA - amountA
pool.reserveB = pool.reserveB - amountB
pool.totalSupply = pool.totalSupply - lpTokens
setPool(poolId, pool)
-- Update provider's LP tokens
AO.store("lp_tokens:" .. provider .. ":" .. poolId, tostring(providerTokens - lpTokens))
return { success = true, amountA = amountA, amountB = amountB, pool = pool }
end)
-- Swap tokens
Handlers.add("swap", function(msg)
local poolId = msg.PoolId
local tokenIn = msg.TokenIn
local amountIn = tonumber(msg.AmountIn)
local minAmountOut = tonumber(msg.MinAmountOut) or 0
local trader = msg.From
local pool = getPool(poolId)
if not pool then
return { error = "Pool not found" }
end
-- Determine swap direction
local reserveIn, reserveOut
if tokenIn == pool.tokenA then
reserveIn = pool.reserveA
reserveOut = pool.reserveB
else
reserveIn = pool.reserveB
reserveOut = pool.reserveA
end
-- Calculate output amount (constant product formula)
local amountInWithFee = amountIn * (1 - pool.fee)
local amountOut = (amountInWithFee * reserveOut) / (reserveIn + amountInWithFee)
if amountOut < minAmountOut then
return { error = "Insufficient output amount" }
end
-- Update pool reserves
if tokenIn == pool.tokenA then
pool.reserveA = pool.reserveA + amountIn
pool.reserveB = pool.reserveB - amountOut
else
pool.reserveB = pool.reserveB + amountIn
pool.reserveA = pool.reserveA - amountOut
end
setPool(poolId, pool)
return { success = true, amountOut = amountOut, pool = pool }
end)
-- Stake tokens
Handlers.add("stake", function(msg)
local poolId = msg.PoolId
local amount = tonumber(msg.Amount)
local staker = msg.From
local stake = getStake(staker, poolId)
if not stake or stake.amount == 0 then
stake = {
amount = 0,
startTime = msg.Timestamp,
rewards = 0
}
end
stake.amount = stake.amount + amount
stake.startTime = msg.Timestamp
setStake(staker, poolId, stake)
return { success = true, stake = stake }
end)
-- Unstake tokens
Handlers.add("unstake", function(msg)
local poolId = msg.PoolId
local amount = tonumber(msg.Amount)
local staker = msg.From
local stake = getStake(staker, poolId)
if not stake or stake.amount < amount then
return { error = "Insufficient staked amount" }
end
-- Calculate rewards
local timeStaked = msg.Timestamp - stake.startTime
local rewardRate = 0.01 -- 1% per day
local rewards = stake.amount * (timeStaked / 86400) * rewardRate
stake.amount = stake.amount - amount
stake.rewards = stake.rewards + rewards
stake.startTime = msg.Timestamp
setStake(staker, poolId, stake)
return { success = true, stake = stake, rewards = rewards }
end)
-- Get pool info
Handlers.add("get_pool", function(msg)
local poolId = msg.PoolId
local pool = getPool(poolId)
if not pool then
return { error = "Pool not found" }
end
return { success = true, pool = pool }
end)
-- List pools
Handlers.add("list_pools", function(msg)
local limit = tonumber(msg.Limit) or 10
local offset = tonumber(msg.Offset) or 0
-- This is a simplified implementation
-- In practice, you'd need to maintain indexes
return { success = true, pools = [] }
end)
Frontend Implementation
DeFi Dashboard Component
// components/DeFiDashboard.tsx
import React, { useState, useEffect } from 'react'
import { useWallet } from '@solana/wallet-adapter-react'
interface Pool {
id: string
tokenA: string
tokenB: string
reserveA: number
reserveB: number
totalSupply: number
fee: number
}
interface Stake {
amount: number
startTime: number
rewards: number
}
export default function DeFiDashboard() {
const { publicKey, connected } = useWallet()
const [pools, setPools] = useState<Pool[]>([])
const [stakes, setStakes] = useState<Record<string, Stake>>({})
const [loading, setLoading] = useState(true)
useEffect(() => {
if (connected) {
loadPools()
loadStakes()
}
}, [connected])
const loadPools = async () => {
try {
const response = await fetch('/api/defi/pools')
const data = await response.json()
setPools(data.pools || [])
} catch (error) {
console.error('Error loading pools:', error)
} finally {
setLoading(false)
}
}
const loadStakes = async () => {
if (!publicKey) return
try {
const response = await fetch(`/api/defi/stakes/${publicKey.toString()}`)
const data = await response.json()
setStakes(data.stakes || {})
} catch (error) {
console.error('Error loading stakes:', error)
}
}
const addLiquidity = async (poolId: string, amountA: number, amountB: number) => {
if (!connected || !publicKey) return
try {
const response = await fetch('/api/defi/add-liquidity', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
poolId,
amountA,
amountB,
provider: publicKey.toString()
})
})
if (response.ok) {
loadPools()
}
} catch (error) {
console.error('Error adding liquidity:', error)
}
}
const stake = async (poolId: string, amount: number) => {
if (!connected || !publicKey) return
try {
const response = await fetch('/api/defi/stake', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
poolId,
amount,
staker: publicKey.toString()
})
})
if (response.ok) {
loadStakes()
}
} catch (error) {
console.error('Error staking:', error)
}
}
if (loading) {
return <div>Loading DeFi dashboard...</div>
}
return (
<div className="max-w-7xl mx-auto p-6">
<h1 className="text-3xl font-bold mb-8">DeFi Protocol</h1>
{/* Liquidity Pools */}
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Liquidity Pools</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{pools.map((pool) => (
<PoolCard
key={pool.id}
pool={pool}
onAddLiquidity={addLiquidity}
onStake={stake}
userAddress={publicKey?.toString()}
/>
))}
</div>
</div>
{/* Staking */}
<div className="mb-8">
<h2 className="text-2xl font-semibold mb-4">Staking</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{Object.entries(stakes).map(([poolId, stake]) => (
<StakeCard
key={poolId}
poolId={poolId}
stake={stake}
onUnstake={() => {}}
/>
))}
</div>
</div>
</div>
)
}
Pool Card Component
// components/PoolCard.tsx
import React, { useState } from 'react'
interface PoolCardProps {
pool: Pool
onAddLiquidity: (poolId: string, amountA: number, amountB: number) => void
onStake: (poolId: string, amount: number) => void
userAddress?: string
}
export default function PoolCard({ pool, onAddLiquidity, onStake, userAddress }: PoolCardProps) {
const [showAddLiquidity, setShowAddLiquidity] = useState(false)
const [showStake, setShowStake] = useState(false)
const [amountA, setAmountA] = useState('')
const [amountB, setAmountB] = useState('')
const [stakeAmount, setStakeAmount] = useState('')
const handleAddLiquidity = () => {
if (amountA && amountB) {
onAddLiquidity(pool.id, parseFloat(amountA), parseFloat(amountB))
setAmountA('')
setAmountB('')
setShowAddLiquidity(false)
}
}
const handleStake = () => {
if (stakeAmount) {
onStake(pool.id, parseFloat(stakeAmount))
setStakeAmount('')
setShowStake(false)
}
}
const totalValue = pool.reserveA + pool.reserveB
const apy = 12.5 // This would be calculated from actual data
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-semibold">
{pool.tokenA}/{pool.tokenB}
</h3>
<span className="text-sm text-gray-500">APY: {apy}%</span>
</div>
<div className="space-y-2 mb-4">
<div className="flex justify-between">
<span className="text-sm text-gray-600">Total Value Locked</span>
<span className="font-medium">${totalValue.toLocaleString()}</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-600">Fee</span>
<span className="font-medium">{(pool.fee * 100).toFixed(2)}%</span>
</div>
<div className="flex justify-between">
<span className="text-sm text-gray-600">Reserves</span>
<span className="font-medium">
{pool.reserveA.toFixed(2)} {pool.tokenA} / {pool.reserveB.toFixed(2)} {pool.tokenB}
</span>
</div>
</div>
<div className="space-y-2">
<button
onClick={() => setShowAddLiquidity(!showAddLiquidity)}
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
>
Add Liquidity
</button>
<button
onClick={() => setShowStake(!showStake)}
className="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600"
>
Stake
</button>
</div>
{/* Add Liquidity Form */}
{showAddLiquidity && (
<div className="mt-4 p-4 bg-gray-50 rounded">
<h4 className="font-medium mb-2">Add Liquidity</h4>
<div className="space-y-2">
<input
type="number"
placeholder={`Amount of ${pool.tokenA}`}
value={amountA}
onChange={(e) => setAmountA(e.target.value)}
className="w-full border rounded px-3 py-2"
/>
<input
type="number"
placeholder={`Amount of ${pool.tokenB}`}
value={amountB}
onChange={(e) => setAmountB(e.target.value)}
className="w-full border rounded px-3 py-2"
/>
<button
onClick={handleAddLiquidity}
className="w-full bg-blue-500 text-white py-2 px-4 rounded hover:bg-blue-600"
>
Add Liquidity
</button>
</div>
</div>
)}
{/* Stake Form */}
{showStake && (
<div className="mt-4 p-4 bg-gray-50 rounded">
<h4 className="font-medium mb-2">Stake Tokens</h4>
<div className="space-y-2">
<input
type="number"
placeholder="Amount to stake"
value={stakeAmount}
onChange={(e) => setStakeAmount(e.target.value)}
className="w-full border rounded px-3 py-2"
/>
<button
onClick={handleStake}
className="w-full bg-green-500 text-white py-2 px-4 rounded hover:bg-green-600"
>
Stake
</button>
</div>
</div>
)}
</div>
)
}
Configuration
ao.config.yml
name: 'defi-protocol'
framework: 'nextjs'
packageManager: 'pnpm'
luaFiles: ['defi.lua']
autoStart: true
processName: 'defi-process'
ports:
dev: 3000
ao: 8080
Features
Liquidity Pools
- Automated Market Maker - Constant product formula
- Fee Collection - Configurable trading fees
- Liquidity Mining - Rewards for liquidity providers
- Impermanent Loss Protection - Risk management tools
Staking System
- Flexible Staking - Stake any amount for any duration
- Reward Calculation - Time-based reward distribution
- Compound Staking - Reinvest rewards automatically
- Unstaking - Flexible unstaking with penalties
Yield Farming
- Multiple Strategies - Various yield farming strategies
- Risk Assessment - Risk scoring for different farms
- Auto-compounding - Automatic reward reinvestment
- Performance Tracking - Track farming performance
Best Practices
- Security - Implement proper access controls and audits
- Risk Management - Provide clear risk warnings
- Transparency - Make all operations transparent
- User Experience - Make DeFi operations intuitive
- Performance - Optimize for gas efficiency
Next Steps
- Explore Token Contract example
- Learn about DAO Application implementation
- Check out NFT Marketplace example