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
- Market Creation: Create a market for a binary outcome (Yes/No)
- Outcome Tokens: Mint Yes and No tokens representing each outcome
- Trading: Users buy/sell outcome tokens based on their predictions
- Resolution: Oracle determines the outcome
- 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:
- Market making algorithms - LMSR and constant product curves
- Oracle integration - Using Pyth for price data
- Token economics - Outcome tokens and settlement
- Time-locked contracts - Resolution windows
- PDA patterns - Complex account relationships
Try It Yourself
- Set up Anchor project with the base program
- Implement LMSR pricing in the contract
- Build frontend with market creation UI
- Add Pyth oracle integration
- Test with devnet USDC
Previous: CEX Trading Platform