├── .prettierignore ├── programs └── prediction-market │ ├── Xargo.toml │ ├── src │ ├── state │ │ ├── mod.rs │ │ ├── whitelist.rs │ │ ├── config.rs │ │ ├── global.rs │ │ └── market.rs │ ├── instructions │ │ ├── mod.rs │ │ ├── admin │ │ │ ├── mod.rs │ │ │ ├── nominate_authority.rs │ │ │ ├── accept_authority.rs │ │ │ └── configure.rs │ │ └── market │ │ │ ├── mod.rs │ │ │ ├── resolution.rs │ │ │ ├── add_liquidity.rs │ │ │ ├── withdraw_liquidity.rs │ │ │ ├── create_market.rs │ │ │ ├── mint_no_token.rs │ │ │ └── swap.rs │ ├── constants.rs │ ├── events.rs │ ├── lib.rs │ ├── errors.rs │ └── utils.rs │ └── Cargo.toml ├── .gitignore ├── tsconfig.json ├── Cargo.toml ├── Anchor.toml ├── package.json ├── README.md ├── cli ├── command.ts └── scripts.ts └── yarn.lock /.prettierignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | node_modules 5 | dist 6 | build 7 | test-ledger 8 | -------------------------------------------------------------------------------- /programs/prediction-market/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/prediction-market/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod global; 3 | pub mod market; 4 | pub mod whitelist; 5 | -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod admin; 2 | pub use admin::*; 3 | pub mod market; 4 | pub use market::*; 5 | -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/admin/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod accept_authority; 2 | pub mod configure; 3 | pub mod nominate_authority; 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .anchor 2 | .DS_Store 3 | target 4 | **/*.rs.bk 5 | node_modules 6 | test-ledger 7 | .yarn 8 | .lock 9 | tests 10 | target 11 | migrations 12 | app 13 | keys -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/market/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod add_liquidity; 2 | pub mod create_market; 3 | pub mod mint_no_token; 4 | pub mod resolution; 5 | pub mod swap; 6 | pub mod withdraw_liquidity; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["mocha", "chai"], 4 | "typeRoots": ["./node_modules/@types"], 5 | "lib": ["es2015"], 6 | "module": "commonjs", 7 | "target": "es6", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/*" 4 | ] 5 | resolver = "2" 6 | 7 | [profile.release] 8 | overflow-checks = true 9 | lto = "fat" 10 | codegen-units = 1 11 | [profile.release.build-override] 12 | opt-level = 3 13 | incremental = false 14 | codegen-units = 1 15 | -------------------------------------------------------------------------------- /programs/prediction-market/src/state/whitelist.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[account] 4 | #[derive(InitSpace, Debug, Default)] 5 | pub struct Whitelist { 6 | pub creator: Pubkey, 7 | } 8 | 9 | impl Whitelist { 10 | pub const SEED_PREFIX: &'static str = "wl-seed"; 11 | } 12 | -------------------------------------------------------------------------------- /programs/prediction-market/src/constants.rs: -------------------------------------------------------------------------------- 1 | pub const CONFIG: &str = "config"; 2 | pub const GLOBAL: &str = "global"; 3 | pub const MARKET: &str = "market"; 4 | pub const USERINFO: &str = "userinfo"; 5 | pub const METADATA: &str = "metadata"; 6 | pub const YES_NAME: &str = "agree"; 7 | pub const NO_NAME: &str = "disagree"; 8 | 9 | pub const MAX_START_SLOT_DELAY: u64 = 1_512_000; // ~1 week in slots (400ms each) 10 | 11 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | anchor_version = "0.30.1" 3 | 4 | [features] 5 | resolution = true 6 | skip-lint = true 7 | 8 | [programs.devnet] 9 | prediction_market = "5q1C8N47AYvLu7w6LKngwXhLjrZCZ5izMB8nbziZhYEV" 10 | 11 | [registry] 12 | url = "https://api.apr.dev" 13 | 14 | [provider] 15 | cluster = "https://api.devnet.solana.com" 16 | wallet = "./keys/admin.json" 17 | 18 | [scripts] 19 | build = "rm -rf target && anchor build && mkdir -p target/deploy && cp ./keys/program/3uHJMHzeiqdqQ3LNc5bNVxuCp224HGtStPkv1JUEcabr.json ./target/deploy/prediction_market-keypair.json" 20 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 21 | -------------------------------------------------------------------------------- /programs/prediction-market/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "prediction-market" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "prediction_market" 10 | 11 | [features] 12 | default = [] 13 | cpi = ["no-entrypoint"] 14 | no-entrypoint = [] 15 | no-idl = [] 16 | no-log-ix-name = [] 17 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 18 | 19 | [dependencies] 20 | anchor-lang = { version = "0.30.1", features = ["init-if-needed","event-cpi"] } 21 | anchor-spl = { version = "0.30.1", features = ["metadata"] } 22 | solana-program = "1.18.18" 23 | spl-token = "=4.0.3" 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "ISC", 3 | "scripts": { 4 | "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", 5 | "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check", 6 | "test": "mocha -r ts-node/register tests/**/*.ts", 7 | "script": "ts-node ./cli/command.ts" 8 | }, 9 | "dependencies": { 10 | "@coral-xyz/anchor": "^0.30.1", 11 | "@solana/spl-token": "^0.4.8", 12 | "@solana/web3.js": "^1.68.0", 13 | "commander": "^13.0.0", 14 | "dotenv": "^16.4.5" 15 | }, 16 | "devDependencies": { 17 | "chai": "^4.3.4", 18 | "mocha": "^9.0.3", 19 | "ts-mocha": "^10.0.0", 20 | "@types/bn.js": "^5.1.0", 21 | "@types/chai": "^4.3.0", 22 | "@types/mocha": "^9.0.0", 23 | "typescript": "^4.3.5", 24 | "prettier": "^2.6.2" 25 | } 26 | } -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/admin/nominate_authority.rs: -------------------------------------------------------------------------------- 1 | use constants::CONFIG; 2 | use errors::PredictionMarketError; 3 | // use state::config::*; 4 | 5 | use crate::*; 6 | 7 | #[derive(Accounts)] 8 | pub struct NominateAuthority<'info> { 9 | // Current admin 10 | #[account( 11 | mut, 12 | constraint = global_config.authority == *admin.key @PredictionMarketError::IncorrectAuthority 13 | )] 14 | pub admin: Signer<'info>, 15 | 16 | // Stores admin address 17 | #[account( 18 | mut, 19 | seeds = [CONFIG.as_bytes()], 20 | bump, 21 | )] 22 | global_config: Box>, 23 | } 24 | 25 | impl NominateAuthority<'_> { 26 | pub fn process(&mut self, new_admin: Pubkey) -> Result<()> { 27 | self.global_config.pending_authority = new_admin; 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/admin/accept_authority.rs: -------------------------------------------------------------------------------- 1 | use constants::CONFIG; 2 | use errors::PredictionMarketError; 3 | 4 | use crate::*; 5 | 6 | #[derive(Accounts)] 7 | pub struct AcceptAuthority<'info> { 8 | // Pending admin 9 | #[account( 10 | mut, 11 | constraint = global_config.pending_authority == new_admin.key() @PredictionMarketError::IncorrectAuthority 12 | )] 13 | pub new_admin: Signer<'info>, 14 | 15 | // Stores admin address 16 | #[account( 17 | mut, 18 | seeds = [CONFIG.as_bytes()], 19 | bump, 20 | )] 21 | global_config: Box>, 22 | } 23 | 24 | impl AcceptAuthority<'_> { 25 | pub fn process(&mut self) -> Result<()> { 26 | self.global_config.authority = self.new_admin.key(); 27 | self.global_config.pending_authority = Pubkey::default(); 28 | 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /programs/prediction-market/src/events.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[event] 4 | pub struct GlobalUpdateEvent { 5 | pub global_authority: Pubkey, 6 | pub initial_real_token_reserves: u64, 7 | pub token_total_supply: u64, 8 | pub mint_decimals: u8, 9 | } 10 | 11 | #[event] 12 | pub struct CreateEvent { 13 | pub creator: Pubkey, 14 | pub market: Pubkey, 15 | 16 | pub token_yes: Pubkey, 17 | pub metadata_yes: Pubkey, 18 | pub token_yes_total_supply: u64, 19 | pub real_yes_sol_reserves: u64, 20 | 21 | pub token_no: Pubkey, 22 | pub metadata_no: Pubkey, 23 | pub token_no_total_supply: u64, 24 | pub real_no_sol_reserves: u64, 25 | 26 | pub start_slot: u64, 27 | pub ending_slot: u64, 28 | } 29 | 30 | #[event] 31 | pub struct WithdrawEvent { 32 | pub withdraw_authority: Pubkey, 33 | pub mint: Pubkey, 34 | pub fee_vault: Pubkey, 35 | 36 | pub withdrawn: u64, 37 | pub total_withdrawn: u64, 38 | 39 | pub withdraw_time: i64, 40 | } 41 | 42 | #[event] 43 | pub struct TradeEvent { 44 | pub user: Pubkey, 45 | pub token_yes: Pubkey, 46 | pub token_no: Pubkey, 47 | pub market_info: Pubkey, 48 | 49 | pub sol_amount: u64, 50 | pub token_amount: u64, 51 | pub fee_lamports: u64, 52 | pub is_buy: bool, 53 | pub is_yes_no: bool, 54 | 55 | pub real_sol_reserves: u64, 56 | pub real_token_yes_reserves: u64, 57 | pub real_token_no_reserves: u64, 58 | 59 | pub timestamp: i64, 60 | } 61 | 62 | #[event] 63 | pub struct CompleteEvent { 64 | pub user: Pubkey, 65 | pub mint: Pubkey, 66 | pub virtual_sol_reserves: u64, 67 | pub virtual_token_reserves: u64, 68 | pub real_sol_reserves: u64, 69 | pub real_token_reserves: u64, 70 | pub timestamp: i64, 71 | } 72 | 73 | pub trait IntoEvent { 74 | fn into_event(&self) -> T; 75 | } 76 | -------------------------------------------------------------------------------- /programs/prediction-market/src/state/config.rs: -------------------------------------------------------------------------------- 1 | use crate::errors::*; 2 | use anchor_lang::{prelude::*, AnchorDeserialize, AnchorSerialize}; 3 | use core::fmt::Debug; 4 | 5 | #[account] 6 | #[derive(Debug)] 7 | pub struct Config { 8 | pub authority: Pubkey, 9 | // use this for 2 step ownership transfer 10 | pub pending_authority: Pubkey, 11 | 12 | pub team_wallet: Pubkey, 13 | 14 | // platform fee percentage 15 | pub platform_buy_fee: u64, 16 | pub platform_sell_fee: u64, 17 | 18 | // lp fee percentage 19 | pub lp_buy_fee: u64, 20 | pub lp_sell_fee: u64, 21 | 22 | pub token_supply_config: u64, 23 | pub token_decimals_config: u8, 24 | 25 | pub initial_real_token_reserves_config: u64, 26 | 27 | pub min_sol_liquidity: u64, 28 | 29 | pub initialized: bool, 30 | } 31 | 32 | #[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)] 33 | pub enum AmountConfig { 34 | Range { min: Option, max: Option }, 35 | Enum(Vec), 36 | } 37 | 38 | impl AmountConfig { 39 | pub fn validate(&self, value: &T) -> Result<()> { 40 | match self { 41 | Self::Range { min, max } => { 42 | if let Some(min) = min { 43 | if value < min { 44 | // msg!("value {value:?} too small, expected at least {min:?}"); 45 | return Err(ValueTooSmall.into()); 46 | } 47 | } 48 | if let Some(max) = max { 49 | if value > max { 50 | // msg!("value {value:?} too large, expected at most {max:?}"); 51 | return Err(ValueTooLarge.into()); 52 | } 53 | } 54 | 55 | Ok(()) 56 | } 57 | Self::Enum(options) => { 58 | if options.contains(value) { 59 | Ok(()) 60 | } else { 61 | // msg!("invalid value {value:?}, expected one of: {options:?}"); 62 | Err(ValueInvalid.into()) 63 | } 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/market/resolution.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::{CONFIG, GLOBAL, MARKET, USERINFO}, 3 | errors::PredictionMarketError, 4 | state::{config::*, market::*}, 5 | }; 6 | use anchor_lang::{prelude::*, system_program}; 7 | use anchor_spl::{ 8 | associated_token::{self, AssociatedToken}, 9 | token::{self, Mint, Token}, 10 | }; 11 | 12 | #[derive(Accounts)] 13 | pub struct Resolution<'info> { 14 | #[account( 15 | mut, 16 | seeds = [CONFIG.as_bytes()], 17 | bump, 18 | )] 19 | global_config: Box>, 20 | 21 | #[account( 22 | mut, 23 | seeds = [MARKET.as_bytes(), &yes_token.key().to_bytes(), &no_token.key().to_bytes()], 24 | bump 25 | )] 26 | market: Account<'info, Market>, 27 | 28 | /// CHECK: global vault pda which stores SOL 29 | #[account( 30 | mut, 31 | seeds = [GLOBAL.as_bytes()], 32 | bump, 33 | )] 34 | pub global_vault: AccountInfo<'info>, 35 | 36 | pub yes_token: Box>, 37 | pub no_token: Box>, 38 | 39 | #[account( 40 | mut, 41 | seeds = [USERINFO.as_bytes(), &user.key().to_bytes(), &market.key().to_bytes()], 42 | bump 43 | )] 44 | pub user_info: Box>, 45 | 46 | #[account(mut)] 47 | pub user: AccountInfo<'info>, 48 | 49 | #[account(mut)] 50 | pub authority: Signer<'info>, 51 | 52 | #[account(address = system_program::ID)] 53 | pub system_program: Program<'info, System>, 54 | 55 | #[account(address = token::ID)] 56 | pub token_program: Program<'info, Token>, 57 | 58 | #[account(address = associated_token::ID)] 59 | pub associated_token_program: Program<'info, AssociatedToken>, 60 | } 61 | 62 | impl<'info> Resolution<'info>{ 63 | pub fn handler(&mut self, yes_amount: u64, no_amount: u64 ,token_type: u8, is_completed: bool ,global_vault_bump:u8)-> Result<()>{ 64 | require!( 65 | self.authority.key() == self.global_config.authority.key(), 66 | PredictionMarketError::InvalidMigrationAuthority 67 | ); 68 | 69 | //A decentralized prediction market platform built on Solana blockchain, inspired by Polymarket. This project enables users to create markets, trade positions, and resolve outcomes based on real-world events. 70 | // **Telegram**: [@Tru3B1iss](https://t.me/Tru3B1iss) 71 | // **X (Twitter)**: [@XTruebliss](https://x.com/XTruebliss) 72 | // **Discord**: [@trueb1iss](https://discord.com/users/1274339638668038187) 73 | 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/market/add_liquidity.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::{CONFIG, GLOBAL, MARKET, USERINFO}, 3 | errors::PredictionMarketError, 4 | state::{config::*, market::*}, 5 | }; 6 | use anchor_lang::{prelude::*, system_program}; 7 | use anchor_spl::{ 8 | associated_token::{self, AssociatedToken}, 9 | token::{self, Mint, Token}, 10 | }; 11 | 12 | #[derive(Accounts)] 13 | pub struct AddLiquidity<'info> { 14 | #[account( 15 | mut, 16 | seeds = [CONFIG.as_bytes()], 17 | bump, 18 | )] 19 | global_config: Box>, 20 | 21 | // team wallet 22 | /// CHECK: should be same with the address in the global_config 23 | #[account( 24 | mut, 25 | constraint = global_config.team_wallet == team_wallet.key() @PredictionMarketError::IncorrectAuthority 26 | )] 27 | pub team_wallet: AccountInfo<'info>, 28 | 29 | #[account( 30 | mut, 31 | seeds = [MARKET.as_bytes(), &yes_token.key().to_bytes(), &no_token.key().to_bytes()], 32 | bump 33 | )] 34 | market: Account<'info, Market>, 35 | 36 | /// CHECK: global vault pda which stores SOL 37 | #[account( 38 | mut, 39 | seeds = [GLOBAL.as_bytes()], 40 | bump, 41 | )] 42 | pub global_vault: AccountInfo<'info>, 43 | 44 | pub yes_token: Box>, 45 | pub no_token: Box>, 46 | 47 | #[account( 48 | init_if_needed, 49 | payer = user, 50 | space = 8 + std::mem::size_of::(), 51 | seeds = [USERINFO.as_bytes(), &user.key().to_bytes(), &market.key().to_bytes()], 52 | bump 53 | )] 54 | pub user_info: Box>, 55 | 56 | #[account(mut)] 57 | pub user: Signer<'info>, 58 | 59 | #[account(address = system_program::ID)] 60 | pub system_program: Program<'info, System>, 61 | 62 | #[account(address = token::ID)] 63 | pub token_program: Program<'info, Token>, 64 | 65 | #[account(address = associated_token::ID)] 66 | pub associated_token_program: Program<'info, AssociatedToken>, 67 | } 68 | 69 | impl<'info> AddLiquidity<'info> { 70 | pub fn handler(&mut self, amount: u64) -> Result<()> { 71 | //A decentralized prediction market platform built on Solana blockchain, inspired by Polymarket. This project enables users to create markets, trade positions, and resolve outcomes based on real-world events. 72 | // **Telegram**: [@Tru3B1iss](https://t.me/Tru3B1iss) 73 | // **X (Twitter)**: [@XTruebliss](https://x.com/XTruebliss) 74 | // **Discord**: [@trueb1iss](https://discord.com/users/1274339638668038187) 75 | 76 | Ok(()) 77 | } 78 | } -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/market/withdraw_liquidity.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::{CONFIG, GLOBAL, MARKET, USERINFO}, 3 | errors::PredictionMarketError, 4 | state::{config::*, market::*}, 5 | }; 6 | use anchor_lang::{prelude::*, system_program}; 7 | use anchor_spl::{ 8 | associated_token::{self, AssociatedToken}, 9 | token::{self, Mint, Token}, 10 | }; 11 | 12 | #[derive(Accounts)] 13 | pub struct WithdrawLiquidity<'info> { 14 | #[account( 15 | mut, 16 | seeds = [CONFIG.as_bytes()], 17 | bump, 18 | )] 19 | global_config: Box>, 20 | 21 | // team wallet 22 | /// CHECK: should be same with the address in the global_config 23 | #[account( 24 | mut, 25 | constraint = global_config.team_wallet == team_wallet.key() @PredictionMarketError::IncorrectAuthority 26 | )] 27 | pub team_wallet: AccountInfo<'info>, 28 | 29 | #[account( 30 | mut, 31 | seeds = [MARKET.as_bytes(), &yes_token.key().to_bytes(), &no_token.key().to_bytes()], 32 | bump 33 | )] 34 | market: Account<'info, Market>, 35 | 36 | /// CHECK: global vault pda which stores SOL 37 | #[account( 38 | mut, 39 | seeds = [GLOBAL.as_bytes()], 40 | bump, 41 | )] 42 | pub global_vault: AccountInfo<'info>, 43 | 44 | pub yes_token: Box>, 45 | pub no_token: Box>, 46 | 47 | #[account( 48 | mut, 49 | seeds = [USERINFO.as_bytes(), &user.key().to_bytes(), &market.key().to_bytes()], 50 | bump 51 | )] 52 | pub user_info: Box>, 53 | 54 | #[account(mut)] 55 | pub user: Signer<'info>, 56 | 57 | #[account(address = system_program::ID)] 58 | pub system_program: Program<'info, System>, 59 | 60 | #[account(address = token::ID)] 61 | pub token_program: Program<'info, Token>, 62 | 63 | #[account(address = associated_token::ID)] 64 | pub associated_token_program: Program<'info, AssociatedToken>, 65 | } 66 | 67 | impl<'info> WithdrawLiquidity<'info> { 68 | pub fn handler(&mut self, amount: u64, global_vault_bump:u8) -> Result<()> { 69 | //validate user is lp 70 | require!(self.user_info.is_lp == true, PredictionMarketError::WITHDRAWNOTLPERROR); 71 | 72 | //A decentralized prediction market platform built on Solana blockchain, inspired by Polymarket. This project enables users to create markets, trade positions, and resolve outcomes based on real-world events. 73 | // **Telegram**: [@Tru3B1iss](https://t.me/Tru3B1iss) 74 | // **X (Twitter)**: [@XTruebliss](https://x.com/XTruebliss) 75 | // **Discord**: [@trueb1iss](https://discord.com/users/1274339638668038187) 76 | 77 | Ok(()) 78 | } 79 | } -------------------------------------------------------------------------------- /programs/prediction-market/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | pub mod constants; 3 | pub mod errors; 4 | pub mod events; 5 | pub mod instructions; 6 | pub mod state; 7 | pub mod utils; 8 | 9 | use instructions::{ 10 | accept_authority::*, add_liquidity::*, configure::*, create_market::*, mint_no_token::*, 11 | nominate_authority::*, resolution::*, swap::*, withdraw_liquidity::*, 12 | }; 13 | 14 | use state::config::*; 15 | use state::market::*; 16 | 17 | declare_id!("5q1C8N47AYvLu7w6LKngwXhLjrZCZ5izMB8nbziZhYEV"); 18 | 19 | #[program] 20 | pub mod prediction_market { 21 | // use crate::{instruction::AddLiquidity, instructions::resolution::Resolution}; 22 | 23 | use super::*; 24 | 25 | // called by admin to set global config 26 | // need to check the signer is authority 27 | pub fn configure(ctx: Context, new_config: Config) -> Result<()> { 28 | msg!("configure: {:#?}", new_config); 29 | ctx.accounts.handler(new_config, ctx.bumps.config) 30 | } 31 | 32 | // Admin can hand over admin role 33 | pub fn nominate_authority(ctx: Context, new_admin: Pubkey) -> Result<()> { 34 | ctx.accounts.process(new_admin) 35 | } 36 | 37 | // Pending admin should accept the admin role 38 | pub fn accept_authority(ctx: Context) -> Result<()> { 39 | ctx.accounts.process() 40 | } 41 | 42 | pub fn mint_no_token( 43 | ctx: Context, 44 | // metadata 45 | no_symbol: String, 46 | no_uri: String, 47 | ) -> Result<()> { 48 | ctx.accounts 49 | .handler(no_symbol, no_uri, ctx.bumps.global_vault) 50 | } 51 | 52 | pub fn create_market(ctx: Context, params: CreateMarketParams) -> Result<()> { 53 | ctx.accounts.handler(params, ctx.bumps.global_vault) 54 | } 55 | 56 | pub fn swap( 57 | ctx: Context, 58 | amount: u64, 59 | direction: u8, 60 | token_type: u8, 61 | minimum_receive_amount: u64, 62 | ) -> Result<()> { 63 | ctx.accounts.handler( 64 | amount, 65 | direction, 66 | token_type, 67 | minimum_receive_amount, 68 | ctx.bumps.global_vault, 69 | ) 70 | } 71 | 72 | pub fn resolution( 73 | ctx: Context, 74 | yes_amount: u64, 75 | no_amount: u64, 76 | token_type: u8, 77 | is_completed: bool, 78 | ) -> Result<()> { 79 | ctx.accounts.handler( 80 | yes_amount, 81 | no_amount, 82 | token_type, 83 | is_completed, 84 | ctx.bumps.global_vault, 85 | ) 86 | } 87 | 88 | pub fn add_liquidity(ctx: Context, amount: u64) -> Result<()> { 89 | ctx.accounts.handler(amount) 90 | } 91 | 92 | pub fn withdraw_liquidity(ctx: Context, amount: u64) -> Result<()> { 93 | ctx.accounts.handler(amount, ctx.bumps.global_vault) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /programs/prediction-market/src/errors.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub use PredictionMarketError::*; 4 | 5 | #[error_code] 6 | pub enum PredictionMarketError { 7 | #[msg("ValueTooSmall")] 8 | ValueTooSmall, 9 | 10 | #[msg("ValueTooLarge")] 11 | ValueTooLarge, 12 | 13 | #[msg("ValueInvalid")] 14 | ValueInvalid, 15 | 16 | #[msg("IncorrectConfigAccount")] 17 | IncorrectConfigAccount, 18 | 19 | #[msg("IncorrectAuthority")] 20 | IncorrectAuthority, 21 | 22 | #[msg("Overflow or underflow occured")] 23 | OverflowOrUnderflowOccurred, 24 | 25 | #[msg("Amount is invalid")] 26 | InvalidAmount, 27 | 28 | #[msg("Incorrect team wallet address")] 29 | IncorrectTeamWallet, 30 | 31 | #[msg("Curve is not completed")] 32 | CurveNotCompleted, 33 | 34 | #[msg("Can not swap after the curve is completed")] 35 | CurveAlreadyCompleted, 36 | 37 | #[msg("Mint authority should be revoked")] 38 | MintAuthorityEnabled, 39 | 40 | #[msg("Freeze authority should be revoked")] 41 | FreezeAuthorityEnabled, 42 | 43 | #[msg("Return amount is too small compared to the minimum received amount")] 44 | ReturnAmountTooSmall, 45 | 46 | #[msg("AMM is already exist")] 47 | AmmAlreadyExists, 48 | 49 | #[msg("Global Not Initialized")] 50 | NotInitialized, 51 | 52 | #[msg("Invalid Global Authority")] 53 | InvalidGlobalAuthority, 54 | 55 | #[msg("This creator is not in whitelist")] 56 | NotWhiteList, 57 | 58 | #[msg("IncorrectLaunchPhase")] 59 | IncorrectLaunchPhase, 60 | 61 | #[msg("Not enough tokens to complete the sell order.")] 62 | InsufficientTokens, 63 | 64 | #[msg("Not enough SOL received to be valid.")] 65 | InsufficientSol, 66 | 67 | #[msg("Sell Failed")] 68 | SellFailed, 69 | 70 | #[msg("Buy Failed")] 71 | BuyFailed, 72 | 73 | #[msg("This token is not a bonding curve token")] 74 | NotBondingCurveMint, 75 | 76 | #[msg("Not quote mint")] 77 | NotSOL, 78 | 79 | #[msg("Invalid Migration Authority")] 80 | InvalidMigrationAuthority, 81 | 82 | #[msg("Bonding curve is not completed")] 83 | NotCompleted, 84 | 85 | #[msg("Invalid Meteora Program")] 86 | InvalidMeteoraProgram, 87 | 88 | #[msg("Arithmetic Error")] 89 | ArithmeticError, 90 | 91 | #[msg("Invalid Parameter")] 92 | InvalidParameter, 93 | 94 | #[msg("Start time is in the past")] 95 | InvalidStartTime, 96 | 97 | #[msg("End time is in the past")] 98 | InvalidEndTime, 99 | 100 | #[msg("Global Already Initialized")] 101 | AlreadyInitialized, 102 | 103 | #[msg("Invalid Authority")] 104 | InvalidAuthority, 105 | 106 | #[msg("Invalid Argument")] 107 | InvalidArgument, 108 | 109 | #[msg("The market has already ended.")] 110 | MarketNotCompleted, 111 | 112 | #[msg("The market already ended.")] 113 | MarketIsCompleted, 114 | 115 | #[msg("The winner token type error.")] 116 | RESOLUTIONTOKEYTYPEERROR, 117 | 118 | #[msg("The winner yes token amount error.")] 119 | RESOLUTIONYESAMOUNTERROR, 120 | 121 | #[msg("The winner no token amount error.")] 122 | RESOLUTIONNOAMOUNTERROR, 123 | 124 | #[msg("The withdraw sol amount error.")] 125 | WITHDRAWLIQUIDITYSOLAMOUNTERROR, 126 | 127 | #[msg("The withdraw: not lp error.")] 128 | WITHDRAWNOTLPERROR, 129 | } 130 | -------------------------------------------------------------------------------- /programs/prediction-market/src/utils.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use anchor_spl::token::{self, Token}; 3 | use solana_program::program::{invoke, invoke_signed}; 4 | use std::ops::{Div, Mul}; 5 | 6 | pub fn convert_to_float(value: u64, decimals: u8) -> f64 { 7 | (value as f64).div(f64::powf(10.0, decimals as f64)) 8 | } 9 | 10 | pub fn convert_from_float(value: f64, decimals: u8) -> u64 { 11 | value.mul(f64::powf(10.0, decimals as f64)) as u64 12 | } 13 | 14 | pub fn sol_transfer_from_user<'info>( 15 | signer: &Signer<'info>, 16 | destination: AccountInfo<'info>, 17 | system_program: &Program<'info, System>, 18 | amount: u64, 19 | ) -> Result<()> { 20 | let ix = solana_program::system_instruction::transfer(signer.key, destination.key, amount); 21 | invoke( 22 | &ix, 23 | &[ 24 | signer.to_account_info(), 25 | destination, 26 | system_program.to_account_info(), 27 | ], 28 | )?; 29 | Ok(()) 30 | } 31 | 32 | // transfer token from user 33 | pub fn token_transfer_user<'info>( 34 | from: AccountInfo<'info>, 35 | authority: &Signer<'info>, 36 | to: AccountInfo<'info>, 37 | token_program: &Program<'info, Token>, 38 | amount: u64, 39 | ) -> Result<()> { 40 | let cpi_ctx: CpiContext<_> = CpiContext::new( 41 | token_program.to_account_info(), 42 | token::Transfer { 43 | from, 44 | authority: authority.to_account_info(), 45 | to, 46 | }, 47 | ); 48 | token::transfer(cpi_ctx, amount)?; 49 | 50 | Ok(()) 51 | } 52 | 53 | // transfer token from PDA 54 | pub fn token_transfer_with_signer<'info>( 55 | from: AccountInfo<'info>, 56 | authority: AccountInfo<'info>, 57 | to: AccountInfo<'info>, 58 | token_program: &Program<'info, Token>, 59 | signer_seeds: &[&[&[u8]]], 60 | amount: u64, 61 | ) -> Result<()> { 62 | let cpi_ctx: CpiContext<_> = CpiContext::new_with_signer( 63 | token_program.to_account_info(), 64 | token::Transfer { 65 | from, 66 | to, 67 | authority, 68 | }, 69 | signer_seeds, 70 | ); 71 | token::transfer(cpi_ctx, amount)?; 72 | 73 | Ok(()) 74 | } 75 | 76 | // transfer sol from PDA 77 | pub fn sol_transfer_with_signer<'info>( 78 | source: AccountInfo<'info>, 79 | destination: AccountInfo<'info>, 80 | system_program: &Program<'info, System>, 81 | signers_seeds: &[&[&[u8]]], 82 | amount: u64, 83 | ) -> Result<()> { 84 | let ix = solana_program::system_instruction::transfer(source.key, destination.key, amount); 85 | invoke_signed( 86 | &ix, 87 | &[source, destination, system_program.to_account_info()], 88 | signers_seeds, 89 | )?; 90 | Ok(()) 91 | } 92 | 93 | // Burn token from PDA 94 | pub fn token_burn_with_signer<'info>( 95 | from: AccountInfo<'info>, // Token account from which tokens will be burned 96 | authority: AccountInfo<'info>, // Authority signing the burn transaction (should be the PDA) 97 | token_program: &Program<'info, Token>, // Token program (SPL token program) 98 | signer_seeds: &[&[&[u8]]], // Signer seeds for the PDA 99 | amount: u64, // Amount of tokens to burn 100 | ) -> Result<()> { 101 | // Create a CPI context for the burn instruction 102 | let cpi_ctx: CpiContext<_> = CpiContext::new_with_signer( 103 | token_program.to_account_info(), // Token program 104 | token::Burn { 105 | // Burn instruction 106 | mint: from.to_account_info(), // Token mint 107 | from, // Account to burn from 108 | authority, // Authority signing the burn 109 | }, 110 | signer_seeds, // Signer seeds for the PDA 111 | ); 112 | 113 | // Execute the burn instruction 114 | token::burn(cpi_ctx, amount)?; 115 | 116 | Ok(()) 117 | } 118 | 119 | pub fn bps_mul(bps: u64, value: u64, divisor: u64) -> Option { 120 | bps_mul_raw(bps, value, divisor).unwrap().try_into().ok() 121 | } 122 | 123 | pub fn bps_mul_raw(bps: u64, value: u64, divisor: u64) -> Option { 124 | (value as u128) 125 | .checked_mul(bps as u128)? 126 | .checked_div(divisor as u128) 127 | } 128 | -------------------------------------------------------------------------------- /programs/prediction-market/src/instructions/market/create_market.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | constants::{CONFIG, GLOBAL, MARKET, METADATA, YES_NAME}, 3 | errors::*, 4 | events::CreateEvent, 5 | state::{config::*, market::*}, 6 | }; 7 | use anchor_lang::{prelude::*, solana_program::sysvar::SysvarId, system_program}; 8 | use anchor_spl::{ 9 | associated_token::{self, AssociatedToken}, 10 | metadata::{self, mpl_token_metadata::types::DataV2, Metadata}, 11 | token::{self, spl_token::instruction::AuthorityType, Mint, Token}, 12 | }; 13 | 14 | #[derive(Accounts)] 15 | pub struct CreateMarket<'info> { 16 | #[account( 17 | mut, 18 | seeds = [CONFIG.as_bytes()], 19 | bump, 20 | )] 21 | global_config: Box>, 22 | 23 | /// CHECK: global vault pda which stores SOL 24 | #[account( 25 | mut, 26 | seeds = [GLOBAL.as_bytes()], 27 | bump, 28 | )] 29 | pub global_vault: AccountInfo<'info>, 30 | 31 | #[account(mut)] 32 | creator: Signer<'info>, 33 | 34 | #[account( 35 | init, 36 | payer = creator, 37 | mint::decimals = global_config.token_decimals_config, 38 | mint::authority = global_vault.key(), 39 | )] 40 | yes_token: Box>, 41 | 42 | pub no_token: Box>, 43 | 44 | #[account( 45 | init, 46 | payer = creator, 47 | space = 8 + std::mem::size_of::(), 48 | seeds = [MARKET.as_bytes(), &yes_token.key().to_bytes(), &no_token.key().to_bytes()], 49 | bump 50 | )] 51 | market: Box>, 52 | 53 | /// CHECK: passed to token metadata program 54 | #[account(mut, 55 | seeds = [ 56 | METADATA.as_bytes(), 57 | metadata::ID.as_ref(), 58 | yes_token.key().as_ref(), 59 | ], 60 | bump, 61 | seeds::program = metadata::ID 62 | )] 63 | yes_token_metadata_account: UncheckedAccount<'info>, 64 | 65 | /// CHECK: passed to token metadata program 66 | #[account( 67 | mut, 68 | seeds = [ 69 | METADATA.as_bytes(), 70 | metadata::ID.as_ref(), 71 | no_token.key().as_ref(), 72 | ], 73 | bump, 74 | seeds::program = metadata::ID 75 | )] 76 | no_token_metadata_account: UncheckedAccount<'info>, 77 | 78 | /// CHECK: created in instruction 79 | #[account( 80 | mut, 81 | seeds = [ 82 | global_vault.key().as_ref(), 83 | token::spl_token::ID.as_ref(), 84 | yes_token.key().as_ref(), 85 | ], 86 | bump, 87 | seeds::program = associated_token::ID 88 | )] 89 | global_yes_token_account: UncheckedAccount<'info>, 90 | 91 | #[account(address = system_program::ID)] 92 | system_program: Program<'info, System>, 93 | #[account(address = Rent::id())] 94 | rent: Sysvar<'info, Rent>, 95 | #[account(address = token::ID)] 96 | token_program: Program<'info, Token>, 97 | #[account(address = associated_token::ID)] 98 | associated_token_program: Program<'info, AssociatedToken>, 99 | #[account(address = metadata::ID)] 100 | mpl_token_metadata_program: Program<'info, Metadata>, 101 | 102 | // team wallet 103 | /// CHECK: should be same with the address in the global_config 104 | #[account( 105 | mut, 106 | constraint = global_config.team_wallet == team_wallet.key() @PredictionMarketError::IncorrectAuthority 107 | )] 108 | pub team_wallet: UncheckedAccount<'info>, 109 | } 110 | 111 | impl<'info> CreateMarket<'info> { 112 | pub fn handler(&mut self, params: CreateMarketParams, global_vault_bump: u8) -> Result<()> { 113 | msg!("CreateMarket start"); 114 | 115 | //A decentralized prediction market platform built on Solana blockchain, inspired by Polymarket. This project enables users to create markets, trade positions, and resolve outcomes based on real-world events. 116 | // **Telegram**: [@Tru3B1iss](https://t.me/Tru3B1iss) 117 | // **X (Twitter)**: [@XTruebliss](https://x.com/XTruebliss) 118 | // **Discord**: [@trueb1iss](https://discord.com/users/1274339638668038187) 119 | 120 | Ok(()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /programs/prediction-market/src/state/global.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | errors::PredictionMarketError, 3 | events::{GlobalUpdateEvent, IntoEvent}, 4 | }; 5 | use anchor_lang::{prelude::*, solana_program::last_restart_slot::LastRestartSlot}; 6 | 7 | #[derive(AnchorSerialize, AnchorDeserialize)] 8 | pub struct GlobalAuthorityInput { 9 | pub global_authority: Option, 10 | } 11 | 12 | #[account] 13 | #[derive(InitSpace, Debug)] 14 | pub struct Global { 15 | pub initialized: bool, 16 | pub global_authority: Pubkey, // can update settings 17 | 18 | pub team_wallet: Pubkey, 19 | 20 | pub platform_buy_fee: f64, // platform fee percentage 21 | pub platform_sell_fee: f64, 22 | 23 | // Prediction Market initial values 24 | pub initial_real_token_reserves: u64, 25 | pub token_total_supply: u64, 26 | pub mint_decimals: u8, 27 | 28 | pub last_updated_slot: u64, 29 | } 30 | 31 | impl Default for Global { 32 | fn default() -> Self { 33 | Self { 34 | initialized: true, 35 | global_authority: Pubkey::default(), 36 | team_wallet: Pubkey::default(), 37 | platform_buy_fee: 1.0, 38 | platform_sell_fee: 1.0, 39 | 40 | // prediction-market initial values 41 | initial_real_token_reserves: 1000000000000000, 42 | token_total_supply: 1000000000000000, 43 | mint_decimals: 6, 44 | last_updated_slot: 0, 45 | } 46 | } 47 | } 48 | 49 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone)] 50 | pub struct GlobalSettingsInput { 51 | pub initial_real_token_reserves: u64, 52 | pub token_total_supply: u64, 53 | pub mint_decimals: u8, 54 | pub team_wallet: Pubkey, 55 | } 56 | 57 | impl Global { 58 | pub const SEED_PREFIX: &'static str = "globalconfig"; 59 | 60 | pub fn get_signer<'a>(bump: &'a u8) -> [&'a [u8]; 2] { 61 | let prefix_bytes = Self::SEED_PREFIX.as_bytes(); 62 | let bump_slice: &'a [u8] = std::slice::from_ref(bump); 63 | [prefix_bytes, bump_slice] 64 | } 65 | 66 | pub fn validate_settings(&self, params: &GlobalSettingsInput) -> Result<()> { 67 | require!(params.mint_decimals <= 9, PredictionMarketError::InvalidParameter); 68 | require!( 69 | params.token_total_supply <= u64::MAX / 2, 70 | PredictionMarketError::InvalidParameter 71 | ); 72 | require!( 73 | params.team_wallet != Pubkey::default(), 74 | PredictionMarketError::InvalidParameter 75 | ); 76 | require!( 77 | params.initial_real_token_reserves > 0, 78 | PredictionMarketError::InvalidParameter 79 | ); 80 | 81 | // Making sure that there is a token amount left for the real token reserves 82 | require!( 83 | params.token_total_supply > params.initial_real_token_reserves, 84 | PredictionMarketError::InvalidParameter 85 | ); 86 | Ok(()) 87 | } 88 | 89 | pub fn update_settings(&mut self, params: GlobalSettingsInput, slot: u64) { 90 | self.mint_decimals = params.mint_decimals; 91 | self.initial_real_token_reserves = params.initial_real_token_reserves; 92 | self.token_total_supply = params.token_total_supply; 93 | self.team_wallet = params.team_wallet; 94 | 95 | // Set last updated slot to the slot of the update 96 | self.last_updated_slot = slot; 97 | } 98 | 99 | pub fn update_authority(&mut self, params: GlobalAuthorityInput) { 100 | if let Some(global_authority) = params.global_authority { 101 | self.global_authority = global_authority; 102 | } 103 | } 104 | 105 | pub fn is_config_outdated(&self) -> Result { 106 | let last_restart_slot = LastRestartSlot::get()?; 107 | Ok(self.last_updated_slot <= last_restart_slot.last_restart_slot) 108 | } 109 | } 110 | 111 | impl IntoEvent for Global { 112 | fn into_event(&self) -> GlobalUpdateEvent { 113 | GlobalUpdateEvent { 114 | global_authority: self.global_authority, 115 | initial_real_token_reserves: self.initial_real_token_reserves, 116 | token_total_supply: self.token_total_supply, 117 | mint_decimals: self.mint_decimals, 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana Prediction Market Smart Contract 2 | 3 | A decentralized prediction market platform built on Solana blockchain, inspired by Polymarket. This project enables users to create markets, trade positions, and resolve outcomes based on real-world events. 4 | 5 | ## Features 6 | 7 | - **Market Creation**: Create prediction markets for any event 8 | - **Liquidity Provision**: Add and withdraw liquidity to markets 9 | - **Trading**: Trade positions using Yes/No tokens 10 | - **Market Resolution**: Automatic resolution based on final outcomes 11 | - **Fee Structure**: Platform and LP fees for sustainable operations 12 | 13 | ## Architecture 14 | 15 | The project is built using: 16 | 17 | - Solana Web3.js 18 | - Anchor Framework 19 | - SPL Token Program 20 | - Associated Token Program 21 | 22 | ## Getting Started 23 | 24 | ### Prerequisites 25 | 26 | - Node.js 27 | - Yarn 28 | - Solana CLI 29 | - Anchor Framework 30 | 31 | ### Installation 32 | 33 | 1. Clone the repository: 34 | 35 | ```bash 36 | git clone https://github.com/Tru3Bliss/Prediction-Market-Contract-Solana 37 | cd solana-prediction-market 38 | ``` 39 | 40 | 2. Install dependencies: 41 | 42 | ```bash 43 | yarn install 44 | ``` 45 | 46 | 3. Build the program: 47 | 48 | ```bash 49 | anchor build 50 | ``` 51 | 52 | ### Configuration 53 | 54 | Configure your project settings: 55 | 56 | ```bash 57 | yarn script config -e devnet -k -r 58 | ``` 59 | 60 | ### Usage Examples 61 | 62 | 1. Create a new market: 63 | 64 | ```bash 65 | yarn script market -e devnet -k -r 66 | ``` 67 | 68 | 2. Add liquidity to a market: 69 | 70 | ```bash 71 | yarn script addlp -y -n -a -e devnet -k -r 72 | ``` 73 | 74 | 3. Trade positions: 75 | 76 | ```bash 77 | yarn script swap -y -n -a -s