Prediction Market

Build a decentralized prediction market on Solana where users can bet on future outcomes.

Project Overview

A prediction market allows users to trade on the outcome of future events. This project demonstrates advanced DeFi concepts including market making, oracle integration, and settlement mechanics.

Text
┌─────────────────────────────────────────────────────────────────┐
│                 Prediction Market Architecture                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─────────────┐    ┌─────────────────┐    ┌───────────────┐   │
│  │   Market    │───▶│   Outcome       │───▶│   Resolution  │   │
│  │   Creation  │    │   Tokens        │    │   & Payout    │   │
│  └─────────────┘    └─────────────────┘    └───────────────┘   │
│         │                   │                      │            │
│         ▼                   ▼                      ▼            │
│  ┌─────────────┐    ┌─────────────────┐    ┌───────────────┐   │
│  │   USDC      │    │   Trading/      │    │   Oracle      │   │
│  │   Collateral│    │   AMM Pool      │    │   (Pyth/etc)  │   │
│  └─────────────┘    └─────────────────┘    └───────────────┘   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

How Prediction Markets Work

  1. Market Creation: Create a market for a binary outcome (Yes/No)
  2. Outcome Tokens: Mint Yes and No tokens representing each outcome
  3. Trading: Users buy/sell outcome tokens based on their predictions
  4. Resolution: Oracle determines the outcome
  5. Settlement: Winning tokens redeemed for collateral

Program Architecture

Rust
use anchor_lang::prelude::*;

declare_id!("Pred111111111111111111111111111111111111111");

#[program]
pub mod prediction_market {
    use super::*;

    pub fn create_market(
        ctx: Context<CreateMarket>,
        question: String,
        resolution_time: i64,
        oracle: Pubkey,
    ) -> Result<()> {
        let market = &mut ctx.accounts.market;
        market.authority = ctx.accounts.authority.key();
        market.question = question;
        market.resolution_time = resolution_time;
        market.oracle = oracle;
        market.resolved = false;
        market.outcome = None;
        market.total_yes_shares = 0;
        market.total_no_shares = 0;
        market.collateral_pool = 0;
        market.bump = ctx.bumps.market;
        Ok(())
    }

    pub fn buy_shares(
        ctx: Context<BuyShares>,
        outcome: Outcome,
        amount: u64,
    ) -> Result<()> {
        let market = &mut ctx.accounts.market;
        require!(!market.resolved, ErrorCode::MarketResolved);
        require!(
            Clock::get()?.unix_timestamp < market.resolution_time,
            ErrorCode::MarketClosed
        );

        // Calculate share price using LMSR
        let shares = calculate_shares(market, outcome, amount)?;

        // Transfer collateral
        anchor_spl::token::transfer(
            CpiContext::new(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token::Transfer {
                    from: ctx.accounts.user_collateral.to_account_info(),
                    to: ctx.accounts.market_collateral.to_account_info(),
                    authority: ctx.accounts.user.to_account_info(),
                },
            ),
            amount,
        )?;

        // Mint outcome tokens
        match outcome {
            Outcome::Yes => {
                market.total_yes_shares += shares;
                ctx.accounts.user_position.yes_shares += shares;
            }
            Outcome::No => {
                market.total_no_shares += shares;
                ctx.accounts.user_position.no_shares += shares;
            }
        }

        market.collateral_pool += amount;

        Ok(())
    }

    pub fn resolve_market(
        ctx: Context<ResolveMarket>,
        outcome: Outcome,
    ) -> Result<()> {
        let market = &mut ctx.accounts.market;
        
        require!(!market.resolved, ErrorCode::AlreadyResolved);
        require!(
            Clock::get()?.unix_timestamp >= market.resolution_time,
            ErrorCode::TooEarly
        );
        require!(
            ctx.accounts.oracle.key() == market.oracle,
            ErrorCode::InvalidOracle
        );

        market.resolved = true;
        market.outcome = Some(outcome);

        Ok(())
    }

    pub fn claim_winnings(ctx: Context<ClaimWinnings>) -> Result<()> {
        let market = &ctx.accounts.market;
        let position = &mut ctx.accounts.user_position;

        require!(market.resolved, ErrorCode::NotResolved);

        let winning_shares = match market.outcome.unwrap() {
            Outcome::Yes => position.yes_shares,
            Outcome::No => position.no_shares,
        };

        require!(winning_shares > 0, ErrorCode::NoWinnings);

        // Calculate payout
        let total_winning_shares = match market.outcome.unwrap() {
            Outcome::Yes => market.total_yes_shares,
            Outcome::No => market.total_no_shares,
        };

        let payout = (market.collateral_pool as u128)
            .checked_mul(winning_shares as u128)
            .unwrap()
            .checked_div(total_winning_shares as u128)
            .unwrap() as u64;

        // Transfer winnings
        let seeds = &[
            b"market",
            market.question.as_bytes(),
            &[market.bump],
        ];
        let signer = &[&seeds[..]];

        anchor_spl::token::transfer(
            CpiContext::new_with_signer(
                ctx.accounts.token_program.to_account_info(),
                anchor_spl::token::Transfer {
                    from: ctx.accounts.market_collateral.to_account_info(),
                    to: ctx.accounts.user_collateral.to_account_info(),
                    authority: ctx.accounts.market.to_account_info(),
                },
                signer,
            ),
            payout,
        )?;

        // Clear position
        position.yes_shares = 0;
        position.no_shares = 0;

        Ok(())
    }
}

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
    Yes,
    No,
}

#[account]
pub struct Market {
    pub authority: Pubkey,
    pub question: String,
    pub resolution_time: i64,
    pub oracle: Pubkey,
    pub resolved: bool,
    pub outcome: Option<Outcome>,
    pub total_yes_shares: u64,
    pub total_no_shares: u64,
    pub collateral_pool: u64,
    pub bump: u8,
}

#[account]
pub struct UserPosition {
    pub user: Pubkey,
    pub market: Pubkey,
    pub yes_shares: u64,
    pub no_shares: u64,
}

#[error_code]
pub enum ErrorCode {
    #[msg("Market has already been resolved")]
    MarketResolved,
    #[msg("Market is closed for trading")]
    MarketClosed,
    #[msg("Market has already been resolved")]
    AlreadyResolved,
    #[msg("Too early to resolve")]
    TooEarly,
    #[msg("Invalid oracle")]
    InvalidOracle,
    #[msg("Market not yet resolved")]
    NotResolved,
    #[msg("No winnings to claim")]
    NoWinnings,
}

LMSR Market Maker

The Logarithmic Market Scoring Rule (LMSR) provides automated liquidity:

Rust
use std::f64::consts::E;

/// Calculate the cost of buying shares using LMSR
fn calculate_cost(
    yes_shares: u64,
    no_shares: u64,
    shares_to_buy: u64,
    outcome: Outcome,
    liquidity: f64,
) -> u64 {
    let current_cost = lmsr_cost(yes_shares as f64, no_shares as f64, liquidity);
    
    let (new_yes, new_no) = match outcome {
        Outcome::Yes => (yes_shares + shares_to_buy, no_shares),
        Outcome::No => (yes_shares, no_shares + shares_to_buy),
    };
    
    let new_cost = lmsr_cost(new_yes as f64, new_no as f64, liquidity);
    
    ((new_cost - current_cost) * 1e6) as u64 // Scale to USDC decimals
}

/// LMSR cost function
fn lmsr_cost(yes: f64, no: f64, b: f64) -> f64 {
    b * (E.powf(yes / b) + E.powf(no / b)).ln()
}

/// Calculate current probability of Yes outcome
fn get_yes_probability(yes_shares: u64, no_shares: u64, liquidity: f64) -> f64 {
    let yes = yes_shares as f64;
    let no = no_shares as f64;
    let b = liquidity;
    
    E.powf(yes / b) / (E.powf(yes / b) + E.powf(no / b))
}

Frontend Integration

TypeScript
import { Connection, PublicKey } from "@solana/web3.js";
import { Program, AnchorProvider } from "@coral-xyz/anchor";
import { PredictionMarket, IDL } from "./idl/prediction_market";

const PROGRAM_ID = new PublicKey("Pred111111111111111111111111111111111111111");

export class PredictionMarketClient {
  private program: Program<PredictionMarket>;
  
  constructor(provider: AnchorProvider) {
    this.program = new Program(IDL, PROGRAM_ID, provider);
  }
  
  async createMarket(
    question: string,
    resolutionTime: number,
    oracle: PublicKey
  ) {
    const [marketPda] = PublicKey.findProgramAddressSync(
      [Buffer.from("market"), Buffer.from(question)],
      PROGRAM_ID
    );
    
    return this.program.methods
      .createMarket(question, resolutionTime, oracle)
      .accounts({
        market: marketPda,
        authority: this.program.provider.publicKey,
        systemProgram: SystemProgram.programId,
      })
      .rpc();
  }
  
  async buyShares(
    marketPda: PublicKey,
    outcome: "yes" | "no",
    amount: number
  ) {
    const [positionPda] = PublicKey.findProgramAddressSync(
      [
        Buffer.from("position"),
        marketPda.toBuffer(),
        this.program.provider.publicKey.toBuffer(),
      ],
      PROGRAM_ID
    );
    
    return this.program.methods
      .buyShares(
        outcome === "yes" ? { yes: {} } : { no: {} },
        new BN(amount * 1e6)
      )
      .accounts({
        market: marketPda,
        userPosition: positionPda,
        user: this.program.provider.publicKey,
        // ... other accounts
      })
      .rpc();
  }
  
  async getMarketProbability(marketPda: PublicKey): Promise<number> {
    const market = await this.program.account.market.fetch(marketPda);
    const liquidity = 1000; // LMSR liquidity parameter
    
    const yes = market.totalYesShares.toNumber();
    const no = market.totalNoShares.toNumber();
    
    return Math.exp(yes / liquidity) / 
           (Math.exp(yes / liquidity) + Math.exp(no / liquidity));
  }
}

React Components

TSX
import { useState, useEffect } from "react";
import { useWallet } from "@solana/wallet-adapter-react";

function MarketCard({ market }: { market: Market }) {
  const { publicKey } = useWallet();
  const [probability, setProbability] = useState(0.5);
  const [betAmount, setBetAmount] = useState("");
  
  useEffect(() => {
    // Fetch current probability
    async function fetchProbability() {
      const prob = await client.getMarketProbability(market.publicKey);
      setProbability(prob);
    }
    fetchProbability();
  }, [market]);
  
  const handleBet = async (outcome: "yes" | "no") => {
    if (!publicKey || !betAmount) return;
    
    await client.buyShares(
      market.publicKey,
      outcome,
      parseFloat(betAmount)
    );
  };
  
  return (
    <div className="border rounded-lg p-6">
      <h3 className="text-xl font-bold">{market.question}</h3>
      
      <div className="my-4">
        <div className="flex justify-between mb-2">
          <span>Yes: {(probability * 100).toFixed(1)}%</span>
          <span>No: {((1 - probability) * 100).toFixed(1)}%</span>
        </div>
        <div className="h-4 bg-gray-200 rounded-full overflow-hidden">
          <div 
            className="h-full bg-green-500"
            style={{ width: `${probability * 100}%` }}
          />
        </div>
      </div>
      
      <input
        type="number"
        value={betAmount}
        onChange={(e) => setBetAmount(e.target.value)}
        placeholder="Amount (USDC)"
        className="w-full p-2 border rounded mb-4"
      />
      
      <div className="flex gap-4">
        <button
          onClick={() => handleBet("yes")}
          className="flex-1 py-2 bg-green-500 text-white rounded"
        >
          Buy Yes
        </button>
        <button
          onClick={() => handleBet("no")}
          className="flex-1 py-2 bg-red-500 text-white rounded"
        >
          Buy No
        </button>
      </div>
    </div>
  );
}

Oracle Integration

Rust
use pyth_solana_receiver_sdk::price_update::PriceUpdateV2;

pub fn resolve_with_pyth(
    ctx: Context<ResolveWithPyth>,
    price_feed_id: [u8; 32],
    target_price: i64,
) -> Result<()> {
    let price_update = &ctx.accounts.price_update;
    
    // Get the price from Pyth
    let price = price_update.get_price_no_older_than(
        &Clock::get()?,
        60, // Max age in seconds
        &price_feed_id,
    )?;
    
    // Determine outcome based on price
    let outcome = if price.price >= target_price {
        Outcome::Yes
    } else {
        Outcome::No
    };
    
    // Resolve the market
    let market = &mut ctx.accounts.market;
    market.resolved = true;
    market.outcome = Some(outcome);
    
    Ok(())
}

Learning Outcomes

After completing this project, you will understand:

  1. Market making algorithms - LMSR and constant product curves
  2. Oracle integration - Using Pyth for price data
  3. Token economics - Outcome tokens and settlement
  4. Time-locked contracts - Resolution windows
  5. PDA patterns - Complex account relationships

Try It Yourself

  1. Set up Anchor project with the base program
  2. Implement LMSR pricing in the contract
  3. Build frontend with market creation UI
  4. Add Pyth oracle integration
  5. Test with devnet USDC

Previous: CEX Trading Platform