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

  1. Security - Implement proper access controls and audits
  2. Risk Management - Provide clear risk warnings
  3. Transparency - Make all operations transparent
  4. User Experience - Make DeFi operations intuitive
  5. Performance - Optimize for gas efficiency

Next Steps