Zum Hauptinhalt springen
tsecurity.de LIVE
Echtzeit-Radar & Feeds
Alle RSS Feeds
👥 Community & Social
Sichere ProgrammierungLINQ GroupBy: The Operator Everyone Uses Wrong(23.09.2026 um 09:41 Uhr)
Sichere ProgrammierungIT Heard About the Acquisition Nine Days Before It Closed(23.09.2026 um 09:45 Uhr)
Sichere ProgrammierungThe Story Behind Building NuvyntraLabs(23.09.2026 um 09:45 Uhr)
Sichere ProgrammierungFive dashboards nobody was opening(23.09.2026 um 09:46 Uhr)
Sichere ProgrammierungThe Shift from AI Insights to AI Actions in Finance(23.09.2026 um 09:47 Uhr)
Sichere ProgrammierungGo WebAssembly Meets WebForms Core 2.1(23.09.2026 um 09:49 Uhr)
Sichere ProgrammierungJust One More Round: Scope Creep in the Age of AI Agents(23.09.2026 um 09:50 Uhr)
Sichere ProgrammierungThe Calls That Reach Us Now Are the Ones the Model Could Not Answer(23.09.2026 um 09:50 Uhr)
Sichere ProgrammierungOne Loop Made Four Hundred Round Trips(23.09.2026 um 09:52 Uhr)
Sichere ProgrammierungThe order was committed and nothing else ever heard about it(23.09.2026 um 09:53 Uhr)
Sichere ProgrammierungLINQ GroupBy: The Operator Everyone Uses Wrong(23.09.2026 um 09:41 Uhr)
Sichere ProgrammierungIT Heard About the Acquisition Nine Days Before It Closed(23.09.2026 um 09:45 Uhr)
Sichere ProgrammierungThe Story Behind Building NuvyntraLabs(23.09.2026 um 09:45 Uhr)
Sichere ProgrammierungFive dashboards nobody was opening(23.09.2026 um 09:46 Uhr)
Sichere ProgrammierungThe Shift from AI Insights to AI Actions in Finance(23.09.2026 um 09:47 Uhr)
Sichere ProgrammierungGo WebAssembly Meets WebForms Core 2.1(23.09.2026 um 09:49 Uhr)
Sichere ProgrammierungJust One More Round: Scope Creep in the Age of AI Agents(23.09.2026 um 09:50 Uhr)
Sichere ProgrammierungThe Calls That Reach Us Now Are the Ones the Model Could Not Answer(23.09.2026 um 09:50 Uhr)
Sichere ProgrammierungOne Loop Made Four Hundred Round Trips(23.09.2026 um 09:52 Uhr)
Sichere ProgrammierungThe order was committed and nothing else ever heard about it(23.09.2026 um 09:53 Uhr)
Intelligence View
⚡ tsecurity.de Intelligence

Building a Prediction Market on Solana with Anchor: Complete Rust Smart Contract Guide

TL;DR I built an open-source prediction market program on Solana using Anchor framework. Features include pot-based binary markets, PDA-based account management, SPL token integration, and proportional payout distribution. 48…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!




TL;DR



I built an open-source prediction market program on Solana using Anchor framework. Features include pot-based binary markets, PDA-based account management, SPL token integration, and proportional payout distribution. 48 comprehensive tests covering the full market lifecycle.



GitHub: https://github.com/SivaramPg/solana-simple-prediction-market-contract









Table of Contents




  1. Introduction

  2. Solana vs EVM: Key Differences

  3. Project Architecture

  4. Account Design with PDAs

  5. Instruction Implementation

  6. SPL Token Integration

  7. Error Handling

  8. Testing with Anchor

  9. Deployment Guide

  10. Conclusion









Introduction



After building a prediction market on EVM chains, I ported the same logic to Solana using the Anchor framework. This article covers the complete implementation, highlighting Solana-specific patterns like PDAs, CPIs, and the account model.






What We're Building





  • Binary prediction markets (YES/NO outcomes)


  • Pot-based parimutuel betting system


  • SPL Token integration (USDC, any SPL token)

  • Admin-controlled resolution

  • PDA-based state management






Tech Stack





  • Rust - Programming language


  • Anchor 0.32+ - Solana development framework


  • SPL Token - Token standard


  • TypeScript - Test suite


  • Mocha/Chai - Testing framework









Solana vs EVM: Key Differences



Before diving in, let's understand the paradigm shift:
















































Aspect EVM (Ethereum) Solana
State Storage Contract stores its own state Accounts are separate from programs
Account Model Single contract address Program + multiple data accounts
Execution Sequential Parallel (if accounts don't overlap)
Fees Gas (paid in ETH) Rent + transaction fees (paid in SOL)
Token Standard ERC20 (per-contract) SPL Token (unified program)
Randomness Block hash (weak) VRF or external
Composability Internal calls Cross-Program Invocations (CPI)





The Account Model



In Solana, programs are stateless. All state lives in accounts:




┌─────────────────┐     ┌─────────────────┐
│ Program │ │ Data Account │
│ (Executable) │────▶│ (State/Data) │
│ │ │ │
│ prediction_ │ │ Config PDA │
│ market.so │ │ Market PDA │
│ │ │ Position PDA │
│ │ │ Vault PDA │
└─────────────────┘ └─────────────────┘












Project Architecture






Directory Structure






programs/prediction_market/
├── src/
│ ├── lib.rs # Program entrypoint
│ ├── constants.rs # Global constants
│ ├── error.rs # Custom errors
│ ├── state/
│ │ ├── mod.rs
│ │ ├── config.rs # Config account
│ │ ├── market.rs # Market account
│ │ └── user_position.rs# User position account
│ └── instructions/
│ ├── mod.rs
│ ├── initialize.rs
│ ├── create_market.rs
│ ├── place_bet.rs
│ ├── resolve_market.rs
│ ├── cancel_market.rs
│ ├── claim_winnings.rs
│ ├── update_config.rs
│ └── pause.rs
├── Cargo.toml
└── Xargo.toml









State Machine






                    ┌──────────┐
│ Active │◀── create_market
└────┬─────┘

┌──────────────┴──────────────┐
│ │
▼ ▼
┌──────────┐ ┌───────────┐
│ Resolved │ │ Cancelled │
└────┬─────┘ └─────┬─────┘
│ │
└──────────┬──────────────────┘

claim_winnings












Account Design with PDAs






What are PDAs?



Program Derived Addresses (PDAs) are deterministic addresses derived from seeds and a program ID. They:




  • Don't have private keys (can't sign externally)

  • Can only be "signed" by the program that derived them

  • Are perfect for program-owned accounts






Config Account






// state/config.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct Config {
/// Admin who can resolve/cancel markets
pub admin: Pubkey,
/// Fee recipient address
pub fee_recipient: Pubkey,
/// SPL token mint for betting
pub token_mint: Pubkey,
/// Token decimals (cached for convenience)
pub token_decimals: u8,
/// Maximum fee in basis points (1000 = 10%)
pub max_fee_bps: u16,
/// Global market counter
pub market_counter: u64,
/// Pause flag
pub paused: bool,
/// PDA bump seed
pub bump: u8,
}

impl Config {
pub const SEED: &'static [u8] = b"config";
}






PDA Derivation:




seeds = [b"config"]









Market Account






// state/market.rs
use anchor_lang::prelude::*;

#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, InitSpace)]
pub enum MarketState {
Active,
Resolved,
Cancelled,
}

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

#[account]
#[derive(InitSpace)]
pub struct Market {
pub id: u64,
#[max_len(200)]
pub question: String,
pub resolution_time: i64,
pub state: MarketState,
pub winning_outcome: Outcome,
pub yes_pool: u64,
pub no_pool: u64,
pub creation_fee: u64,
pub creator: Pubkey,
pub created_at: i64,
/// Config snapshot - fee recipient at creation
pub config_fee_recipient: Pubkey,
/// Config snapshot - max fee at creation
pub config_max_fee_bps: u16,
pub bump: u8,
pub vault_bump: u8,
}

impl Market {
pub const SEED: &'static [u8] = b"market";
pub const VAULT_SEED: &'static [u8] = b"vault";
}






PDA Derivation:




// Market PDA
seeds = [b"market", market_id.to_le_bytes()]

// Vault PDA (token account owned by market)
seeds = [b"vault", market_id.to_le_bytes()]









User Position Account






// state/user_position.rs
use anchor_lang::prelude::*;

#[account]
#[derive(InitSpace)]
pub struct UserPosition {
pub market_id: u64,
pub user: Pubkey,
pub yes_bet: u64,
pub no_bet: u64,
pub claimed: bool,
pub bump: u8,
}

impl UserPosition {
pub const SEED: &'static [u8] = b"position";
}






PDA Derivation:




seeds = [b"position", market_id.to_le_bytes(), user.key()]












Instruction Implementation






Initialize Instruction






// instructions/initialize.rs
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use crate::constants::MAX_FEE_LIMIT;
use crate::error::PredictionMarketError;
use crate::state::Config;

#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub admin: Signer<'info>,

#[account(
init,
payer = admin,
space = 8 + Config::INIT_SPACE,
seeds = [Config::SEED],
bump
)]
pub config: Account<'info, Config>,

/// The SPL token mint for betting
pub token_mint: Account<'info, Mint>,

pub system_program: Program<'info, System>,
}

pub fn handler(
ctx: Context<Initialize>,
fee_recipient: Pubkey,
max_fee_bps: u16
) -> Result<()> {
require!(
max_fee_bps <= MAX_FEE_LIMIT,
PredictionMarketError::InvalidFee
);

let config = &mut ctx.accounts.config;
config.admin = ctx.accounts.admin.key();
config.fee_recipient = fee_recipient;
config.token_mint = ctx.accounts.token_mint.key();
config.token_decimals = ctx.accounts.token_mint.decimals;
config.max_fee_bps = max_fee_bps;
config.market_counter = 0;
config.paused = false;
config.bump = ctx.bumps.config;

msg!("Prediction Market initialized");
Ok(())
}









Create Market Instruction






// instructions/create_market.rs
#[derive(Accounts)]
pub struct CreateMarket<'info> {
#[account(mut)]
pub creator: Signer<'info>,

#[account(
mut,
seeds = [Config::SEED],
bump = config.bump,
constraint = !config.paused @ PredictionMarketError::Paused
)]
pub config: Account<'info, Config>,

#[account(
init,
payer = creator,
space = 8 + Market::INIT_SPACE,
seeds = [Market::SEED, (config.market_counter + 1).to_le_bytes().as_ref()],
bump
)]
pub market: Account<'info, Market>,

/// Market vault for holding tokens
#[account(
init,
payer = creator,
seeds = [Market::VAULT_SEED, (config.market_counter + 1).to_le_bytes().as_ref()],
bump,
token::mint = token_mint,
token::authority = market // Market PDA owns the vault
)]
pub market_vault: Account<'info, TokenAccount>,

#[account(constraint = token_mint.key() == config.token_mint)]
pub token_mint: Account<'info, Mint>,

#[account(
mut,
token::mint = token_mint,
token::authority = creator
)]
pub creator_token_account: Account<'info, TokenAccount>,

#[account(mut, token::mint = token_mint)]
pub fee_recipient_token_account: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}

pub fn handler(
ctx: Context<CreateMarket>,
question: String,
resolution_time: i64,
fee_amount: u64,
) -> Result<()> {
// Validations
require!(!question.is_empty(), PredictionMarketError::EmptyQuestion);
require!(
question.len() <= MAX_QUESTION_LENGTH,
PredictionMarketError::QuestionTooLong
);

let clock = Clock::get()?;
require!(
resolution_time > clock.unix_timestamp,
PredictionMarketError::InvalidResolutionTime
);

// Transfer fee if applicable
if fee_amount > 0 {
let cpi_accounts = Transfer {
from: ctx.accounts.creator_token_account.to_account_info(),
to: ctx.accounts.fee_recipient_token_account.to_account_info(),
authority: ctx.accounts.creator.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
transfer(cpi_ctx, fee_amount)?;
}

// Increment market counter
let config = &mut ctx.accounts.config;
config.market_counter = config.market_counter
.checked_add(1)
.ok_or(PredictionMarketError::Overflow)?;

// Initialize market
let market = &mut ctx.accounts.market;
market.id = config.market_counter;
market.question = question;
market.resolution_time = resolution_time;
market.state = MarketState::Active;
market.winning_outcome = Outcome::None;
market.yes_pool = 0;
market.no_pool = 0;
market.creation_fee = fee_amount;
market.creator = ctx.accounts.creator.key();
market.created_at = clock.unix_timestamp;
// Snapshot config at creation
market.config_fee_recipient = config.fee_recipient;
market.config_max_fee_bps = config.max_fee_bps;
market.bump = ctx.bumps.market;
market.vault_bump = ctx.bumps.market_vault;

msg!("Market created: {}", market.id);
Ok(())
}









Place Bet Instruction






// instructions/place_bet.rs
#[derive(Accounts)]
#[instruction(market_id: u64)]
pub struct PlaceBet<'info> {
#[account(mut)]
pub bettor: Signer<'info>,

#[account(
seeds = [Config::SEED],
bump = config.bump,
constraint = !config.paused @ PredictionMarketError::Paused
)]
pub config: Account<'info, Config>,

#[account(
mut,
seeds = [Market::SEED, market_id.to_le_bytes().as_ref()],
bump = market.bump,
constraint = market.state == MarketState::Active @ PredictionMarketError::MarketNotActive
)]
pub market: Account<'info, Market>,

#[account(
mut,
seeds = [Market::VAULT_SEED, market_id.to_le_bytes().as_ref()],
bump = market.vault_bump
)]
pub market_vault: Account<'info, TokenAccount>,

#[account(
init_if_needed, // Creates position on first bet
payer = bettor,
space = 8 + UserPosition::INIT_SPACE,
seeds = [UserPosition::SEED, market_id.to_le_bytes().as_ref(), bettor.key().as_ref()],
bump
)]
pub user_position: Account<'info, UserPosition>,

#[account(
mut,
token::mint = config.token_mint,
token::authority = bettor
)]
pub bettor_token_account: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}

pub fn handler(
ctx: Context<PlaceBet>,
market_id: u64,
outcome: Outcome,
amount: u64,
) -> Result<()> {
require!(amount > 0, PredictionMarketError::ZeroAmount);
require!(
outcome == Outcome::Yes || outcome == Outcome::No,
PredictionMarketError::InvalidOutcome
);

let clock = Clock::get()?;
require!(
clock.unix_timestamp < ctx.accounts.market.resolution_time,
PredictionMarketError::MarketExpired
);

// Transfer tokens to vault
let cpi_accounts = Transfer {
from: ctx.accounts.bettor_token_account.to_account_info(),
to: ctx.accounts.market_vault.to_account_info(),
authority: ctx.accounts.bettor.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts
);
transfer(cpi_ctx, amount)?;

// Update market pools
let market = &mut ctx.accounts.market;
if outcome == Outcome::Yes {
market.yes_pool = market.yes_pool
.checked_add(amount)
.ok_or(PredictionMarketError::Overflow)?;
} else {
market.no_pool = market.no_pool
.checked_add(amount)
.ok_or(PredictionMarketError::Overflow)?;
}

// Update user position
let position = &mut ctx.accounts.user_position;
if position.market_id == 0 {
// Initialize new position
position.market_id = market_id;
position.user = ctx.accounts.bettor.key();
position.yes_bet = 0;
position.no_bet = 0;
position.claimed = false;
position.bump = ctx.bumps.user_position;
}

if outcome == Outcome::Yes {
position.yes_bet = position.yes_bet
.checked_add(amount)
.ok_or(PredictionMarketError::Overflow)?;
} else {
position.no_bet = position.no_bet
.checked_add(amount)
.ok_or(PredictionMarketError::Overflow)?;
}

msg!("Bet placed: {} on {:?}", amount, outcome);
Ok(())
}









Claim Winnings Instruction






// instructions/claim_winnings.rs
#[derive(Accounts)]
#[instruction(market_id: u64)]
pub struct ClaimWinnings<'info> {
#[account(mut)]
pub user: Signer<'info>,

#[account(
seeds = [Market::SEED, market_id.to_le_bytes().as_ref()],
bump = market.bump,
constraint = market.state != MarketState::Active @ PredictionMarketError::MarketNotFinalized
)]
pub market: Account<'info, Market>,

#[account(
mut,
seeds = [Market::VAULT_SEED, market_id.to_le_bytes().as_ref()],
bump = market.vault_bump
)]
pub market_vault: Account<'info, TokenAccount>,

#[account(
mut,
seeds = [UserPosition::SEED, market_id.to_le_bytes().as_ref(), user.key().as_ref()],
bump = user_position.bump,
constraint = user_position.user == user.key() @ PredictionMarketError::InvalidAdmin,
constraint = !user_position.claimed @ PredictionMarketError::AlreadyClaimed
)]
pub user_position: Account<'info, UserPosition>,

#[account(mut, token::authority = user)]
pub user_token_account: Account<'info, TokenAccount>,

pub token_program: Program<'info, Token>,
}

pub fn handler(ctx: Context<ClaimWinnings>, market_id: u64) -> Result<()> {
let market = &ctx.accounts.market;
let position = &ctx.accounts.user_position;

require!(
position.yes_bet > 0 || position.no_bet > 0,
PredictionMarketError::NoPosition
);

// Calculate payout
let payout: u64 = if market.state == MarketState::Resolved {
if market.winning_outcome == Outcome::Yes && position.yes_bet > 0 {
// User bet on YES and YES won
let winning_pool = market.yes_pool;
let losing_pool = market.no_pool;
let share = (position.yes_bet as u128)
.checked_mul(losing_pool as u128)
.ok_or(PredictionMarketError::Overflow)?
.checked_div(winning_pool as u128)
.ok_or(PredictionMarketError::Overflow)?;
position.yes_bet
.checked_add(share as u64)
.ok_or(PredictionMarketError::Overflow)?
} else if market.winning_outcome == Outcome::No && position.no_bet > 0 {
// User bet on NO and NO won
let winning_pool = market.no_pool;
let losing_pool = market.yes_pool;
let share = (position.no_bet as u128)
.checked_mul(losing_pool as u128)
.ok_or(PredictionMarketError::Overflow)?
.checked_div(winning_pool as u128)
.ok_or(PredictionMarketError::Overflow)?;
position.no_bet
.checked_add(share as u64)
.ok_or(PredictionMarketError::Overflow)?
} else {
0 // Bet on losing side
}
} else {
// Cancelled - full refund
position.yes_bet
.checked_add(position.no_bet)
.ok_or(PredictionMarketError::Overflow)?
};

// Mark as claimed
ctx.accounts.user_position.claimed = true;

// Transfer payout using PDA signature
if payout > 0 {
let market_id_bytes = market_id.to_le_bytes();
let seeds = &[
Market::SEED,
market_id_bytes.as_ref(),
&[market.bump]
];
let signer_seeds = &[&seeds[..]];

let cpi_accounts = Transfer {
from: ctx.accounts.market_vault.to_account_info(),
to: ctx.accounts.user_token_account.to_account_info(),
authority: ctx.accounts.market.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
signer_seeds,
);
transfer(cpi_ctx, payout)?;
}

msg!("Claimed {} tokens", payout);
Ok(())
}












SPL Token Integration






Cross-Program Invocations (CPI)



Solana uses CPIs to interact with other programs. For SPL tokens:




use anchor_spl::token::{transfer, Token, TokenAccount, Transfer};

// Transfer FROM user TO vault (user signs)
let cpi_accounts = Transfer {
from: ctx.accounts.user_token_account.to_account_info(),
to: ctx.accounts.vault.to_account_info(),
authority: ctx.accounts.user.to_account_info(),
};
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
);
transfer(cpi_ctx, amount)?;

// Transfer FROM vault TO user (PDA signs)
let seeds = &[b"vault", market_id.as_ref(), &[bump]];
let signer = &[&seeds[..]];

let cpi_accounts = Transfer {
from: ctx.accounts.vault.to_account_info(),
to: ctx.accounts.user_token_account.to_account_info(),
authority: ctx.accounts.vault_authority.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
cpi_accounts,
signer,
);
transfer(cpi_ctx, amount)?;









Token Account Constraints



Anchor provides powerful constraints for token accounts:




#[account(
mut,
token::mint = expected_mint, // Verify mint
token::authority = expected_owner // Verify owner
)]
pub token_account: Account<'info, TokenAccount>,

#[account(
init,
payer = payer,
seeds = [b"vault", id.as_ref()],
bump,
token::mint = mint,
token::authority = vault_authority // PDA owns this account
)]
pub vault: Account<'info, TokenAccount>,












Error Handling






Custom Errors






// error.rs
use anchor_lang::prelude::*;

#[error_code]
pub enum PredictionMarketError {
#[msg("Invalid admin")]
InvalidAdmin,

#[msg("Contract is paused")]
Paused,

#[msg("Contract is not paused")]
NotPaused,

#[msg("Invalid fee")]
InvalidFee,

#[msg("Empty question")]
EmptyQuestion,

#[msg("Question too long")]
QuestionTooLong,

#[msg("Invalid resolution time")]
InvalidResolutionTime,

#[msg("Market not active")]
MarketNotActive,

#[msg("Market expired")]
MarketExpired,

#[msg("Market not expired")]
MarketNotExpired,

#[msg("Market already finalized")]
MarketAlreadyFinalized,

#[msg("Market not finalized")]
MarketNotFinalized,

#[msg("Invalid outcome")]
InvalidOutcome,

#[msg("No opposition")]
NoOpposition,

#[msg("Zero amount")]
ZeroAmount,

#[msg("No position")]
NoPosition,

#[msg("Already claimed")]
AlreadyClaimed,

#[msg("Arithmetic overflow")]
Overflow,
}









Using Errors






// With require! macro
require!(amount > 0, PredictionMarketError::ZeroAmount);

// With constraint
#[account(
constraint = !config.paused @ PredictionMarketError::Paused
)]
pub config: Account<'info, Config>,

// Manual return
if some_condition {
return Err(PredictionMarketError::InvalidAdmin.into());
}












Testing with Anchor






Test Setup






// tests/prediction_market.ts
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PredictionMarket } from "../target/types/prediction_market";
import {
Keypair,
PublicKey,
SystemProgram,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
import {
TOKEN_PROGRAM_ID,
createMint,
createAccount,
mintTo,
getAccount,
} from "@solana/spl-token";
import { assert } from "chai";

describe("prediction_market", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);

const program = anchor.workspace.PredictionMarket as Program<PredictionMarket>;

let admin: Keypair;
let alice: Keypair;
let bob: Keypair;
let tokenMint: PublicKey;
let configPda: PublicKey;

const DECIMALS = 6;
const usdc = (amount: number) => amount * 10 ** DECIMALS;

// Helper: derive market PDAs
const getMarketPdas = (marketId: number) => {
const marketIdBuffer = Buffer.alloc(8);
marketIdBuffer.writeBigUInt64LE(BigInt(marketId));

const [marketPda] = PublicKey.findProgramAddressSync(
[Buffer.from("market"), marketIdBuffer],
program.programId
);
const [vaultPda] = PublicKey.findProgramAddressSync(
[Buffer.from("vault"), marketIdBuffer],
program.programId
);
return { marketPda, vaultPda };
};

before(async () => {
// Setup accounts, mint tokens, etc.
});
});









Test Categories






describe("Market Creation", () => {
it("TC-MC-001: should create market successfully", async () => {
// ...
});

it("TC-MC-008: should reject empty question", async () => {
try {
await program.methods
.createMarket("", new anchor.BN(resolutionTime), new anchor.BN(0))
.accounts({ /* ... */ })
.signers([alice])
.rpc();
assert.fail("Should have thrown");
} catch (err: any) {
assert.include(err.message, "EmptyQuestion");
}
});
});

describe("Betting", () => {
it("TC-BT-001: should place YES bet", async () => {
const balanceBefore = (await getAccount(connection, aliceToken)).amount;

await placeBet(alice, marketId, { yes: {} }, usdc(100));

const balanceAfter = (await getAccount(connection, aliceToken)).amount;
assert.equal(balanceBefore - balanceAfter, BigInt(usdc(100)));
});
});

describe("Integration - Full Lifecycle", () => {
it("TC-IT-001: complete flow YES wins", async () => {
// 1. Create market
const marketId = await createMarket("Test?", 2, alice);

// 2. Place bets
await placeBet(alice, marketId, { yes: {} }, usdc(100));
await placeBet(bob, marketId, { no: {} }, usdc(50));

// 3. Wait for resolution time
await sleep(3000);

// 4. Resolve
await resolveMarket(marketId, { yes: {} });

// 5. Claim
const aliceBefore = (await getAccount(connection, aliceToken)).amount;
await claimWinnings(alice, marketId);
const aliceAfter = (await getAccount(connection, aliceToken)).amount;

// Alice: 100 + (100/100) * 50 = 150
assert.equal(aliceAfter - aliceBefore, BigInt(usdc(150)));
});
});









Running Tests






# Start local validator and run tests
anchor test

# Run specific test
anchor test -- --grep "should create market"

# Run with logs
anchor test -- --verbose









Test Results






  prediction_market
Initialize
✔ should initialize the program
Market Creation
✔ TC-MC-001: should create market successfully
✔ TC-MC-002: should create market with fee
✔ TC-MC-008: should reject empty question
✔ TC-MC-009: should reject when paused
Betting
✔ TC-BT-001: should place YES bet successfully
✔ TC-BT-002: should place NO bet successfully
✔ TC-BT-003: should allow multiple bets same side
✔ TC-BT-004: should allow hedged bets (both sides)
✔ TC-BT-005: should reject zero amount bet
Resolution
✔ TC-RS-001: should resolve with YES winning
✔ TC-RS-002: should resolve with NO winning
✔ TC-RS-004: should reject non-admin resolution
✔ TC-RS-007/008: should reject with no opposition
Cancellation
✔ TC-CN-001: should cancel market successfully
✔ TC-CN-003: should reject non-admin cancellation
Claiming
✔ TC-CL-001: should claim winnings (winning side)
✔ TC-CL-002: should claim winnings (losing side - zero)
✔ TC-CL-003: should claim refund (cancelled market)
✔ TC-CL-006: should reject double claim
Access Control
✔ TC-AC-001: should allow admin to update config
✔ TC-AC-003: should allow admin to pause
Integration
✔ TC-IT-001: complete flow YES wins
✔ TC-IT-002: complete flow NO wins
✔ TC-IT-003: complete flow cancelled

48 passing (2m)












Deployment Guide






Prerequisites






# Install Solana CLI
sh -c "$(curl -sSfL https://release.solana.com/v1.18.0/install)"

# Install Anchor
cargo install --git https://github.com/coral-xyz/anchor anchor-cli

# Create wallet
solana-keygen new









Build and Deploy






# Build
anchor build

# Get program ID
solana-keygen pubkey target/deploy/prediction_market-keypair.json

# Update declare_id! in lib.rs with the program ID
# Update Anchor.toml with the program ID

# Deploy to devnet
anchor deploy --provider.cluster devnet

# Deploy to mainnet
anchor deploy --provider.cluster mainnet









Anchor.toml Configuration






[toolchain]

[features]
resolution = true
skip-lint = false

[programs.localnet]
prediction_market = "YourProgramId..."

[programs.devnet]
prediction_market = "YourProgramId..."

[programs.mainnet]
prediction_market = "YourProgramId..."

[registry]
url = "https://api.apr.dev"

[provider]
cluster = "devnet"
wallet = "~/.config/solana/id.json"

[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"









Initialize After Deployment






// scripts/initialize.ts
const tx = await program.methods
.initialize(feeRecipient, 500) // 5% max fee
.accounts({
admin: wallet.publicKey,
config: configPda,
tokenMint: USDC_MINT,
systemProgram: SystemProgram.programId,
})
.rpc();

console.log("Initialized:", tx);












Conclusion



This Solana prediction market demonstrates:





  • PDA-based architecture for deterministic account addresses


  • SPL Token integration with CPIs


  • Anchor framework patterns and macros


  • Comprehensive testing with TypeScript


  • Production-ready error handling






Key Takeaways





  1. Accounts are separate from programs - think of them as database tables


  2. PDAs enable program-owned accounts - crucial for holding funds


  3. CPIs are how programs interact - like internal function calls in EVM


  4. Anchor simplifies everything - macros handle serialization, validation






What's Next?



For production, consider:





  1. Switchboard VRF for randomness


  2. Pyth/Switchboard oracles for price feeds


  3. Clockwork for automated resolution


  4. Compressed NFTs for position receipts


  5. Governance with Realms






Resources








Disclaimer: This code is for educational purposes only. It has not been audited and should not be used in production without proper security review.






Found this useful? Star the repo and follow for more Solana tutorials!






solana #rust #anchor #smartcontracts #defi #predictionmarket #web3 #blockchain #tutorial #opensource #spl #pda

Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Building a Prediction Market on Solana with Anchor: Complete Rust Smart Contract Guide

Thematisch verwandte Begriffe: Building, Prediction, Market, Solana · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-96258 | A vulnerability has been found in onSite internet GmbH Auktion NG Auktio…
Advisory →
TTS Reader • tsecurity.de Voice
tsecurity.de Icon
tsecurity.de App
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag
Themen-Radar & Intelligence Matrix
Echtzeit-Taxonomie nach Angriffsvektoren & Plattformen

tsecurity.de Live Threat Radar

🔴 LIVE RADAR
MONITORING
AKTIV
CVE-DATENBANK
LIVE
🔍
Community Radar & Live Chat
Sentinel Bot online • Live-Stream
Dein Cluster: Security Explorer
Match:
lädt…
Verbindung zum Community-Stream wird aufgebaut...
Bearbeitungsmodus — Senden überschreibt deine Nachricht
Community-Puls — was gerade passiert
lädt…
Aktivitäten deiner Analysten
lädt…
Neues Thema oder Eilmeldung einreichen

Reiche interessante Links, Zero-Days oder Debatten ein. Die Community entscheidet per Upvote über die Veröffentlichung.

Heiß diskutierte Einreichungen
🔖 Gespeicherte Artikel
📂 Keine gespeicherten Artikel vorhanden.
Zurück Ziehen Vor
Links: vorheriger Artikel Rechts: nächster Artikel unten: schließen
News NIS-2 Frühwarnung Tier-1 Intel ⏱️ 3 Min vor 10 Min
Artikeldaten werden geladen...

Zurück: vorheriger Vor: nächster
↗ Original-Quelle
Social Reaktionen Deine Reaktion zählt
Einstufung & Relevanz-Poll 0 Stimmen
In sozialen Netzwerken teilen 1-Klick