//! wenbond: a yes/no market on whether a pump.fun coin bonds before a deadline. //! //! Anyone opens a market on a coin that is still on its bonding curve, with one of three deadlines //! (1 h, 6 h, 24 h). Bets go into two pools held by the market account itself. If the coin's //! bonding curve `complete` byte reads 1 before the deadline, anyone can `mark` the market and YES wins. //! If the deadline passes unmarked, anyone can `expire` it and NO wins. Winners split the losing pool //! pro rata, minus 2% to the house. If either pool is empty, everyone gets their stake back, no fee. //! Nobody can move the pools before settlement: the only lamports that leave a market are claims, //! and a sweep of leftover dust 30 days after resolution. use anchor_lang::prelude::*; use anchor_lang::system_program; declare_id!("G1H5BrYax8JqkR42xtEpfSfUsrdxMq2B4EWYb8QQX23Y"); pub const PUMP: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); /// only the deploy key can open the house, so nobody can front-run init and make themselves admin pub const DEPLOYER: Pubkey = pubkey!("CTNkYr8FmCqiPsyx3Xt59mL4sLryJmFDM36wvg5e6kPP"); /// byte offset of `complete` in pump.fun's BondingCurve account (8 disc + 5 u64) pub const COMPLETE_AT: usize = 48; pub const FEE_BPS: u64 = 200; pub const MIN_BET: u64 = 10_000_000; // 0.01 SOL pub const DURATIONS: [i64; 3] = [3_600, 21_600, 86_400]; pub const SWEEP_AFTER: i64 = 30 * 86_400; pub const OPEN: u8 = 0; pub const YES: u8 = 1; pub const NO: u8 = 2; #[program] pub mod wenbond { use super::*; pub fn init_house(ctx: Context) -> Result<()> { let h = &mut ctx.accounts.house; h.admin = ctx.accounts.admin.key(); h.bump = ctx.bumps.house; Ok(()) } pub fn open_market(ctx: Context, nonce: u32, kind: u8) -> Result<()> { require!((kind as usize) < DURATIONS.len(), Err::BadDeadline); let mint = ctx.accounts.mint.key(); let complete = curve_complete(&ctx.accounts.curve, &mint)?; require!(!complete, Err::AlreadyBonded); let now = Clock::get()?.unix_timestamp; let m = &mut ctx.accounts.market; m.mint = mint; m.curve = ctx.accounts.curve.key(); m.opener = ctx.accounts.opener.key(); m.opened_at = now; m.deadline = now + DURATIONS[kind as usize]; m.nonce = nonce; m.kind = kind; m.state = OPEN; m.bump = ctx.bumps.market; emit!(MarketOpened { market: m.key(), mint, deadline: m.deadline, opener: m.opener }); Ok(()) } pub fn bet(ctx: Context, side: u8, amount: u64) -> Result<()> { require!(side == YES || side == NO, Err::BadSide); require!(amount >= MIN_BET, Err::BetTooSmall); let now = Clock::get()?.unix_timestamp; let m = &ctx.accounts.market; require!(m.state == OPEN, Err::NotOpen); require!(now < m.deadline, Err::PastDeadline); // no betting on a coin that has already bonded and is just waiting to be marked require!(!curve_complete(&ctx.accounts.curve, &m.mint)?, Err::AlreadyBonded); system_program::transfer( CpiContext::new(ctx.accounts.system_program.to_account_info(), system_program::Transfer { from: ctx.accounts.user.to_account_info(), to: ctx.accounts.market.to_account_info(), }), amount, )?; let b = &mut ctx.accounts.bet; if b.user == Pubkey::default() { b.user = ctx.accounts.user.key(); b.market = ctx.accounts.market.key(); b.bump = ctx.bumps.bet; } let m = &mut ctx.accounts.market; if side == YES { b.yes = b.yes.checked_add(amount).ok_or(Err::Overflow)?; m.yes_pool = m.yes_pool.checked_add(amount).ok_or(Err::Overflow)?; } else { b.no = b.no.checked_add(amount).ok_or(Err::Overflow)?; m.no_pool = m.no_pool.checked_add(amount).ok_or(Err::Overflow)?; } emit!(BetPlaced { market: m.key(), user: b.user, side, amount, yes_pool: m.yes_pool, no_pool: m.no_pool }); Ok(()) } /// The coin bonded before the deadline: YES wins. Anyone can call it. pub fn mark(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let m = &mut ctx.accounts.market; require!(m.state == OPEN, Err::NotOpen); require!(now < m.deadline, Err::PastDeadline); require_keys_eq!(ctx.accounts.curve.key(), m.curve, Err::WrongCurve); require!(curve_complete(&ctx.accounts.curve, &m.mint)?, Err::NotBonded); m.state = YES; m.resolved_at = now; emit!(Resolved { market: m.key(), state: YES, yes_pool: m.yes_pool, no_pool: m.no_pool }); Ok(()) } /// The deadline passed unmarked: NO wins. Anyone can call it. pub fn expire(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let m = &mut ctx.accounts.market; require!(m.state == OPEN, Err::NotOpen); require!(now >= m.deadline, Err::BeforeDeadline); m.state = NO; m.resolved_at = now; emit!(Resolved { market: m.key(), state: NO, yes_pool: m.yes_pool, no_pool: m.no_pool }); Ok(()) } /// Pays a bet out and closes it (its rent goes back to the bettor). Losing bets close with nothing. pub fn claim(ctx: Context) -> Result<()> { let m = &ctx.accounts.market; require!(m.state == YES || m.state == NO, Err::NotResolved); let b = &ctx.accounts.bet; let (stake, win_pool, lose_pool) = if m.state == YES { (b.yes, m.yes_pool, m.no_pool) } else { (b.no, m.no_pool, m.yes_pool) }; let (payout, fee) = if win_pool == 0 || lose_pool == 0 { (b.yes + b.no, 0u64) // one side empty: everyone gets their stake back } else { let gross = (lose_pool as u128) * (stake as u128) / (win_pool as u128); let fee = gross * (FEE_BPS as u128) / 10_000; ((stake as u128 + gross - fee) as u64, fee as u64) }; let out = payout.checked_add(fee).ok_or(Err::Overflow)?; if out > 0 { let market = ctx.accounts.market.to_account_info(); let keep = Rent::get()?.minimum_balance(market.data_len()); require!(market.lamports().saturating_sub(keep) >= out, Err::Insolvent); **market.try_borrow_mut_lamports()? -= out; **ctx.accounts.user.to_account_info().try_borrow_mut_lamports()? += payout; **ctx.accounts.house.to_account_info().try_borrow_mut_lamports()? += fee; } let m = &mut ctx.accounts.market; m.paid = m.paid.checked_add(payout).ok_or(Err::Overflow)?; m.fees = m.fees.checked_add(fee).ok_or(Err::Overflow)?; emit!(Claimed { market: m.key(), user: ctx.accounts.user.key(), payout, fee }); Ok(()) } /// 30 days after resolution, rounding dust and unclaimed winnings go to the house. pub fn sweep(ctx: Context) -> Result<()> { let m = &ctx.accounts.market; require!(m.state == YES || m.state == NO, Err::NotResolved); require!(Clock::get()?.unix_timestamp >= m.resolved_at + SWEEP_AFTER, Err::TooEarly); let market = ctx.accounts.market.to_account_info(); let keep = Rent::get()?.minimum_balance(market.data_len()); let extra = market.lamports().saturating_sub(keep); **market.try_borrow_mut_lamports()? -= extra; **ctx.accounts.house.to_account_info().try_borrow_mut_lamports()? += extra; Ok(()) } /// The admin moves house fees out. It cannot touch any market. pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { let house = ctx.accounts.house.to_account_info(); let keep = Rent::get()?.minimum_balance(house.data_len()); require!(house.lamports().saturating_sub(keep) >= amount, Err::Insolvent); **house.try_borrow_mut_lamports()? -= amount; **ctx.accounts.to.try_borrow_mut_lamports()? += amount; Ok(()) } pub fn set_admin(ctx: Context, admin: Pubkey) -> Result<()> { ctx.accounts.house.admin = admin; Ok(()) } } /// Reads `complete` from the coin's pump.fun bonding curve after checking it really is that account. fn curve_complete(curve: &UncheckedAccount, mint: &Pubkey) -> Result { let (expect, _) = Pubkey::find_program_address(&[b"bonding-curve", mint.as_ref()], &PUMP); require_keys_eq!(curve.key(), expect, Err::WrongCurve); require_keys_eq!(*curve.owner, PUMP, Err::WrongCurve); let data = curve.try_borrow_data()?; require!(data.len() > COMPLETE_AT, Err::WrongCurve); Ok(data[COMPLETE_AT] != 0) } #[account] #[derive(InitSpace)] pub struct House { pub admin: Pubkey, pub bump: u8, } #[account] #[derive(InitSpace)] pub struct Market { pub mint: Pubkey, pub curve: Pubkey, pub opener: Pubkey, pub opened_at: i64, pub deadline: i64, pub resolved_at: i64, pub yes_pool: u64, pub no_pool: u64, pub paid: u64, pub fees: u64, pub nonce: u32, pub kind: u8, pub state: u8, pub bump: u8, } #[account] #[derive(InitSpace)] pub struct Bet { pub user: Pubkey, pub market: Pubkey, pub yes: u64, pub no: u64, pub bump: u8, } #[derive(Accounts)] pub struct InitHouse<'info> { #[account(init, payer = admin, space = 8 + House::INIT_SPACE, seeds = [b"house"], bump)] pub house: Account<'info, House>, #[account(mut, address = DEPLOYER)] pub admin: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] #[instruction(nonce: u32)] pub struct OpenMarket<'info> { #[account(init, payer = opener, space = 8 + Market::INIT_SPACE, seeds = [b"market", mint.key().as_ref(), &nonce.to_le_bytes()], bump)] pub market: Account<'info, Market>, /// CHECK: any mint; the curve check below ties it to a pump.fun coin pub mint: UncheckedAccount<'info>, /// CHECK: verified in curve_complete (PDA of pump.fun, owned by pump.fun) pub curve: UncheckedAccount<'info>, #[account(mut)] pub opener: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct PlaceBet<'info> { #[account(mut)] pub market: Account<'info, Market>, #[account(init_if_needed, payer = user, space = 8 + Bet::INIT_SPACE, seeds = [b"bet", market.key().as_ref(), user.key().as_ref()], bump)] pub bet: Account<'info, Bet>, /// CHECK: verified in curve_complete pub curve: UncheckedAccount<'info>, #[account(mut)] pub user: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Resolve<'info> { #[account(mut)] pub market: Account<'info, Market>, /// CHECK: must equal market.curve, verified in curve_complete pub curve: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct Expire<'info> { #[account(mut)] pub market: Account<'info, Market>, } #[derive(Accounts)] pub struct Claim<'info> { #[account(mut)] pub market: Account<'info, Market>, #[account(mut, close = user, has_one = user, has_one = market, seeds = [b"bet", market.key().as_ref(), user.key().as_ref()], bump = bet.bump)] pub bet: Account<'info, Bet>, #[account(mut, seeds = [b"house"], bump = house.bump)] pub house: Account<'info, House>, #[account(mut)] pub user: Signer<'info>, } #[derive(Accounts)] pub struct Sweep<'info> { #[account(mut)] pub market: Account<'info, Market>, #[account(mut, seeds = [b"house"], bump = house.bump)] pub house: Account<'info, House>, } #[derive(Accounts)] pub struct Withdraw<'info> { #[account(mut, seeds = [b"house"], bump = house.bump, has_one = admin)] pub house: Account<'info, House>, pub admin: Signer<'info>, /// CHECK: any destination the admin picks #[account(mut)] pub to: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct SetAdmin<'info> { #[account(mut, seeds = [b"house"], bump = house.bump, has_one = admin)] pub house: Account<'info, House>, pub admin: Signer<'info>, } #[event] pub struct MarketOpened { pub market: Pubkey, pub mint: Pubkey, pub deadline: i64, pub opener: Pubkey } #[event] pub struct BetPlaced { pub market: Pubkey, pub user: Pubkey, pub side: u8, pub amount: u64, pub yes_pool: u64, pub no_pool: u64 } #[event] pub struct Resolved { pub market: Pubkey, pub state: u8, pub yes_pool: u64, pub no_pool: u64 } #[event] pub struct Claimed { pub market: Pubkey, pub user: Pubkey, pub payout: u64, pub fee: u64 } #[error_code] pub enum Err { #[msg("deadline must be 0 (1h), 1 (6h) or 2 (24h)")] BadDeadline, #[msg("side must be 1 (yes) or 2 (no)")] BadSide, #[msg("minimum bet is 0.01 SOL")] BetTooSmall, #[msg("the coin has already bonded")] AlreadyBonded, #[msg("the coin has not bonded")] NotBonded, #[msg("market is not open")] NotOpen, #[msg("market is not resolved")] NotResolved, #[msg("the deadline has passed")] PastDeadline, #[msg("the deadline has not passed")] BeforeDeadline, #[msg("not this coin's pump.fun bonding curve")] WrongCurve, #[msg("too early")] TooEarly, #[msg("not enough lamports")] Insolvent, #[msg("overflow")] Overflow, }