This prediction market protocol enables users to create binary markets, trade outcome tokens, and settle markets based on real-world events. Built using the Conditional Token Framework (CTF) model similar to Polymarket.
1. Market Creation → 2. Token Splitting → 3. Trading → 4. Settlement → 5. Redemption
- Deposit Collateral: Users deposit collateral (e.g., USDC) to mint equal amounts of Outcome A and Outcome B tokens
- Trade Outcomes: Outcome tokens can be traded on secondary markets based on event probability
- Market Settlement: Authority settles the market by declaring the winning outcome
- Claim Rewards: Winners redeem their tokens 1:1 for collateral
- Void Markets: If voided, users redeem matched pairs of both tokens for collateral
| Feature | Description |
|---|---|
| Binary Markets | Create markets with two possible outcomes (Yes/No, A/B) |
| Token Splitting | Convert collateral into equal outcome tokens (1 USDC → 1 YES + 1 NO) |
| Token Merging | Burn matched pairs to reclaim collateral before settlement |
| Flexible Settlement | Resolve to Outcome A, Outcome B, or Neither (void) |
| Security First | Built with Anchor's security features and comprehensive error handling |
| PDA Architecture | Secure account derivation using program-derived addresses |
sol-prediction-market/
├── programs/
│ └── prediction_market/
│ └── src/
│ ├── lib.rs # Program entrypoint
│ ├── error.rs # Custom error definitions
│ ├── state/
│ │ ├── mod.rs # State module exports
│ │ └── market.rs # Market account structure
│ └── instructions/
│ ├── mod.rs # Instruction module exports
│ ├── initialize_market.rs
│ ├── split_token.rs
│ ├── merge_token.rs
│ ├── set_winner.rs
│ └── claim_rewards.rs
├── tests/ # Integration tests
├── Anchor.toml # Anchor configuration
└── package.json # Dependencies
Market Account
pub struct Market {
pub authority: Pubkey, // Market creator/admin
pub market_id: u32, // Unique market identifier
pub settlement_deadline: i64, // Unix timestamp for settlement
pub outcome_a_mint: Pubkey, // Outcome A token mint
pub outcome_b_mint: Pubkey, // Outcome B token mint
pub collateral_mint: Pubkey, // Collateral token mint (e.g., USDC)
pub collateral_vault: Pubkey, // Vault holding collateral
pub is_settled: bool, // Settlement status
pub winning_outcome: Option<WinningOutcome>, // Final outcome
pub total_collateral_locked: u64, // Total collateral in vault
pub bump: u8, // PDA bump seed
}Winning Outcomes
pub enum WinningOutcome {
OutcomeA, // Outcome A wins
OutcomeB, // Outcome B wins
Neither, // Market voided (both sides refunded)
}1. Initialize Market
Creates a new prediction market with two outcome tokens.
Parameters:
market_id: Unique identifier for the marketsettlement_deadline: Unix timestamp after which the market can be settled
Accounts:
authority: Market creator (signer, payer)collateral_mint: Token to use as collateralmarket: Market PDAcollateral_vault: Token account to hold collateraloutcome_a_mint: Mint for Outcome A tokensoutcome_b_mint: Mint for Outcome B tokens
Example:
await program.methods
.initializeMarket(
1, // market_id
new anchor.BN(Date.now() / 1000 + 30 * 24 * 60 * 60) // 30 days from now
)
.accounts({
authority: wallet.publicKey,
collateralMint: usdcMint,
})
.rpc();2. Split Tokens
Convert collateral into equal amounts of both outcome tokens.
Parameters:
amount: Amount of collateral to split
Logic:
- Transfers collateral from user to vault
- Mints equal amounts of Outcome A and Outcome B tokens to user
Example:
await program.methods
.splitTokens(new anchor.BN(1_000_000)) // 1 USDC (6 decimals)
.accountsPartial({
user: user.publicKey,
userCollateral: userUsdcAccount,
collateralVault: vaultPda,
outcomeAMint: outcomeAMint,
outcomeBMint: outcomeBMint,
userOutcomeA: userOutcomeAAccount,
userOutcomeB: userOutcomeBAccount,
market: marketPda,
})
.signers([user])
.rpc();3. Merge Tokens
Burn matched pairs of outcome tokens to reclaim collateral (before settlement).
Parameters:
amount_a: Amount of Outcome A tokens to burnamount_b: Amount of Outcome B tokens to burn
Logic:
- Burns the minimum of
amount_aandamount_bfrom both token accounts - Returns equivalent collateral to user
Example:
await program.methods
.mergeTokens(
new anchor.BN(500_000), // 0.5 Outcome A
new anchor.BN(500_000) // 0.5 Outcome B
)
.accountsPartial({
user: user.publicKey,
userCollateral: userUsdcAccount,
collateralVault: vaultPda,
collateralMint: usdcMint,
outcomeAMint: outcomeAMint,
outcomeBMint: outcomeBMint,
userOutcomeA: userOutcomeAAccount,
userOutcomeB: userOutcomeBAccount,
market: marketPda,
})
.signers([user])
.rpc();4. Set Winning Side
Settles the market by declaring the winning outcome (admin only).
Parameters:
admin_outcome: The winning outcome (OutcomeA,OutcomeB, orNeither)
Example:
await program.methods
.setWinningSide({ outcomeA: {} })
.accountsPartial({
authority: authority.publicKey,
market: marketPda,
})
.signers([authority])
.rpc();5. Claim Rewards
Redeem winning tokens for collateral after settlement.
Parameters:
amount_a: Amount of Outcome A tokens to redeemamount_b: Amount of Outcome B tokens to redeem
Redemption Logic:
- Outcome A wins: Redeem Outcome A tokens 1:1 for collateral
- Outcome B wins: Redeem Outcome B tokens 1:1 for collateral
- Neither wins: Redeem matched pairs of both tokens 1:1 for collateral
Example:
// If Outcome A won
await program.methods
.claimRewards(
new anchor.BN(1_000_000), // 1 Outcome A token
new anchor.BN(0)
)
.accountsPartial({
user: user.publicKey,
userCollateral: userUsdcAccount,
collateralVault: vaultPda,
collateralMint: usdcMint,
outcomeAMint: outcomeAMint,
outcomeBMint: outcomeBMint,
userOutcomeA: userOutcomeAAccount,
userOutcomeB: userOutcomeBAccount,
market: marketPda,
})
.signers([user])
.rpc();| Code | Name | Description |
|---|---|---|
6000 |
InvalidSettlementDeadline |
Settlement deadline must be in the future |
6001 |
MarketAlreadySettled |
Cannot perform action on settled market |
6002 |
MarketExpired |
Market has passed settlement deadline |
6003 |
InvalidAmount |
Amount must be greater than zero |
6004 |
MathOverflow |
Arithmetic overflow occurred |
6005 |
MathUnderflow |
Arithmetic underflow occurred |
6006 |
MarketNotSettled |
Market must be settled before claiming |
6007 |
NoRedeemableTokens |
No matched pairs available for redemption |
6008 |
NoWinningTokens |
User has no winning tokens to claim |
| Account | Seeds | Example |
|---|---|---|
| Market | ["market", market_id.to_le_bytes()] |
findProgramAddressSync([Buffer.from("market"), marketIdBuffer]) |
| Collateral Vault | ["vault", market_id.to_le_bytes()] |
findProgramAddressSync([Buffer.from("vault"), marketIdBuffer]) |
| Outcome A Mint | ["outcome_a", market_id.to_le_bytes()] |
findProgramAddressSync([Buffer.from("outcome_a"), marketIdBuffer]) |
| Outcome B Mint | ["outcome_b", market_id.to_le_bytes()] |
findProgramAddressSync([Buffer.from("outcome_b"), marketIdBuffer]) |
| Tool | Version | Purpose |
|---|---|---|
| Rust | 1.70+ | Solana program development |
| Solana CLI | 1.18+ | Blockchain interaction |
| Anchor | 0.31.1 | Framework for Solana programs |
| Node.js | 16+ | Running tests and scripts |
| Yarn | Latest | Package management |
# 1. Clone the repository
git clone https://github.com/yourusername/sol-prediction-market.git
cd sol-prediction-market
# 2. Install dependencies
yarn install
# 3. Build the program
anchor build
# 4. Get the program ID
solana address -k target/deploy/prediction_market-keypair.json
# 5. Update the program ID in:
# - Anchor.toml
# - programs/prediction_market/src/lib.rs (in declare_id!())
# 6. Rebuild after updating program ID
anchor build# Start a local validator (in a separate terminal)
solana-test-validator
# Run the test suite
anchor test --skip-local-validator
# Or run tests with a fresh validator
anchor test✅ Market initialization ✅ Token splitting (collateral → outcome tokens) ✅ Token merging (outcome tokens → collateral) ✅ Market settlement (all three outcomes) ✅ Reward claiming for Outcome A winners ✅ Reward claiming for Outcome B winners ✅ Reward claiming for voided markets (Neither outcome)
# 1. Configure Solana CLI for devnet
solana config set --url devnet
# 2. Airdrop SOL for deployment
solana airdrop 2
# 3. Deploy the program
anchor deploy
# 4. Update Anchor.toml cluster setting to "devnet"
⚠️ Warning: Ensure thorough testing and security audits before mainnet deployment.
# 1. Configure for mainnet
solana config set --url mainnet-beta
# 2. Deploy (ensure you have sufficient SOL)
anchor deployCreating a Market
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { PredictionMarket } from "../target/types/prediction_market";
const provider = anchor.AnchorProvider.env();
const program = anchor.workspace.predictionMarket as Program<PredictionMarket>;
// Create market with 30-day settlement
const marketId = 1;
const settlementDeadline = Math.floor(Date.now() / 1000) + (30 * 24 * 60 * 60);
await program.methods
.initializeMarket(marketId, new anchor.BN(settlementDeadline))
.accounts({
authority: provider.wallet.publicKey,
collateralMint: usdcMintAddress,
})
.rpc();Complete Trading Flow
// 1. User deposits 100 USDC and gets 100 YES + 100 NO tokens
await program.methods
.splitTokens(new anchor.BN(100_000_000))
.accountsPartial({ /* accounts */ })
.rpc();
// 2. User trades tokens on secondary market (not included in this contract)
// 3. Market settles - admin declares winner
await program.methods
.setWinningSide({ outcomeA: {} })
.accountsPartial({ /* accounts */ })
.rpc();
// 4. Winner claims 1:1 collateral for winning tokens
await program.methods
.claimRewards(new anchor.BN(100_000_000), new anchor.BN(0))
.accountsPartial({ /* accounts */ })
.rpc();| Feature | Implementation |
|---|---|
| Authority Control | Only the market authority can settle markets |
| Timestamp Validation | Settlement deadline must be in the future |
| Overflow Protection | All math operations use checked arithmetic |
| Market State | Actions are restricted based on settlement status |
| Token Validation | All token accounts are validated against market PDAs |
| PDA Security | All accounts derived using secure PDAs |
⚠️ Manual settlement by authority (no oracle integration)⚠️ Binary markets only (two outcomes)⚠️ No automated market maker (AMM) for trading⚠️ No fees or revenue mechanism
- Oracle Integration: Automatic settlement using Pyth/Switchboard
- Multi-Outcome Markets: Support for more than 2 options
- Built-in AMM: Trading outcome tokens without external DEX
- Fee Mechanism: Creator fees and protocol revenue
- Market Creation Fees: Economic spam prevention
- Permissionless Markets: Allow anyone to create markets
- Dispute Resolution: Appeal system for contested outcomes
- TWAP Oracles: Time-weighted average price integration
- Cross-chain Support: Multi-chain collateral options
This project is licensed under the ISC License.
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Built with Anchor Framework
- Inspired by Polymarket and Gnosis Conditional Tokens
- Uses Solana's SPL Token program
- Community feedback and contributions
- Issues: GitHub Issues
- Discussions: GitHub Discussions
This is experimental software. Use at your own risk. Not audited for production use.
Made with ❤️ for the Solana ecosystem