//! ============================================================ //! lastcall — the LASTCALL settlement program. //! //! Holds the round state and the prize vault on-chain: //! //! * round one lasts `base_duration` (5 minutes); every round after //! lasts twice the one before — forever //! * the oracle (the round engine watching pump.fun trades) records //! each qualifying buy with `record_buy`; the freshest one holds //! the last-call spot //! * once the deadline passes, ANYONE can crank `settle`: the vault //! pays everything (minus rent) to the last buyer, the round //! advances, the clock doubles //! * creator fees are deposited into the vault with `fund` — by the //! engine, or by anyone who wants to sweeten the pot //! //! A round with no qualifying buyer rolls its vault into the next one. //! ============================================================ use anchor_lang::prelude::*; declare_id!("9QYZew72GPZjKJhGPRGAa7fEarE7WX1CG6cqWoDGRMdN"); pub const GAME_SEED: &[u8] = b"game"; pub const VAULT_SEED: &[u8] = b"vault"; #[program] pub mod lastcall { use super::*; /// Open the game. `base_duration_secs` is round one (300 = 5 minutes); /// buys below `min_buy_lamports` never take the spot. pub fn initialize( ctx: Context, base_duration_secs: i64, min_buy_lamports: u64, ) -> Result<()> { require!(base_duration_secs > 0, LastCallError::BadDuration); let game = &mut ctx.accounts.game; let now = Clock::get()?.unix_timestamp; game.authority = ctx.accounts.authority.key(); game.round = 1; game.duration = base_duration_secs; game.deadline = now + base_duration_secs; game.min_buy_lamports = min_buy_lamports; game.last_buyer = None; game.last_buy_at = 0; game.total_paid = 0; game.rounds_paid = 0; game.vault_bump = ctx.bumps.vault; emit!(RoundStarted { round: 1, duration: game.duration, deadline: game.deadline }); Ok(()) } /// Oracle-only: credit a qualifying buy to `buyer`. The engine calls this /// for every pump.fun buy of at least `min_buy_lamports` while the round /// is live. The freshest call holds the last-call spot. pub fn record_buy(ctx: Context, buyer: Pubkey, lamports_spent: u64) -> Result<()> { let game = &mut ctx.accounts.game; let now = Clock::get()?.unix_timestamp; require!(now < game.deadline, LastCallError::RoundOver); require!(lamports_spent >= game.min_buy_lamports, LastCallError::BuyTooSmall); game.last_buyer = Some(buyer); game.last_buy_at = now; emit!(LastCallTaken { round: game.round, buyer, lamports_spent }); Ok(()) } /// Deposit creator fees (or anything) into the prize vault. pub fn fund(ctx: Context, lamports: u64) -> Result<()> { let ix = anchor_lang::solana_program::system_instruction::transfer( &ctx.accounts.funder.key(), &ctx.accounts.vault.key(), lamports, ); anchor_lang::solana_program::program::invoke( &ix, &[ ctx.accounts.funder.to_account_info(), ctx.accounts.vault.to_account_info(), ], )?; emit!(Funded { round: ctx.accounts.game.round, lamports }); Ok(()) } /// Permissionless crank. After the deadline: pay the vault to the last /// buyer (or roll it over if nobody bought), advance the round, and /// DOUBLE the clock. pub fn settle(ctx: Context) -> Result<()> { let now = Clock::get()?.unix_timestamp; let game = &mut ctx.accounts.game; require!(now >= game.deadline, LastCallError::RoundStillLive); let ended = game.round; let winner = game.last_buyer; if let Some(winner_key) = winner { require_keys_eq!( ctx.accounts.winner.key(), winner_key, LastCallError::WrongWinnerAccount ); // pay everything above rent-exemption from the vault PDA. The vault // is system-owned, so the transfer is a system-program CPI with the // PDA itself signing via seeds. let rent = Rent::get()?.minimum_balance(0); let vault_lamports = ctx.accounts.vault.lamports(); let pot = vault_lamports.saturating_sub(rent); if pot > 0 { let ix = anchor_lang::solana_program::system_instruction::transfer( &ctx.accounts.vault.key(), &winner_key, pot, ); anchor_lang::solana_program::program::invoke_signed( &ix, &[ ctx.accounts.vault.to_account_info(), ctx.accounts.winner.to_account_info(), ], &[&[VAULT_SEED, &[game.vault_bump]]], )?; game.total_paid += pot; game.rounds_paid += 1; } emit!(RoundSettled { round: ended, winner: Some(winner_key), paid: pot }); } else { // no buyer: the vault rolls into the next round untouched emit!(RoundSettled { round: ended, winner: None, paid: 0 }); } game.last_buyer = None; game.last_buy_at = 0; game.round = ended + 1; game.duration = game.duration.saturating_mul(2); game.deadline += game.duration; emit!(RoundStarted { round: game.round, duration: game.duration, deadline: game.deadline }); Ok(()) } } /* ---------- accounts ---------- */ #[account] pub struct Game { pub authority: Pubkey, // the oracle allowed to record buys pub round: u64, // 1-based pub duration: i64, // seconds; doubles every round pub deadline: i64, // unix; settle() is open after this pub min_buy_lamports: u64, pub last_buyer: Option, pub last_buy_at: i64, pub total_paid: u64, pub rounds_paid: u64, pub vault_bump: u8, } impl Game { pub const SPACE: usize = 8 + 32 + 8 + 8 + 8 + 8 + (1 + 32) + 8 + 8 + 8 + 1; } #[derive(Accounts)] pub struct Initialize<'info> { #[account(init, payer = authority, space = Game::SPACE, seeds = [GAME_SEED], bump)] pub game: Account<'info, Game>, /// CHECK: SOL-only prize vault PDA; holds lamports, never data #[account(mut, seeds = [VAULT_SEED], bump)] pub vault: UncheckedAccount<'info>, #[account(mut)] pub authority: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct RecordBuy<'info> { #[account(mut, seeds = [GAME_SEED], bump, has_one = authority)] pub game: Account<'info, Game>, pub authority: Signer<'info>, } #[derive(Accounts)] pub struct Fund<'info> { #[account(seeds = [GAME_SEED], bump)] pub game: Account<'info, Game>, /// CHECK: SOL-only prize vault PDA #[account(mut, seeds = [VAULT_SEED], bump = game.vault_bump)] pub vault: UncheckedAccount<'info>, #[account(mut)] pub funder: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Settle<'info> { #[account(mut, seeds = [GAME_SEED], bump)] pub game: Account<'info, Game>, /// CHECK: SOL-only prize vault PDA (system-owned; debited via CPI + seeds) #[account(mut, seeds = [VAULT_SEED], bump = game.vault_bump)] pub vault: UncheckedAccount<'info>, /// CHECK: must match game.last_buyer; verified in the handler #[account(mut)] pub winner: UncheckedAccount<'info>, pub cranker: Signer<'info>, pub system_program: Program<'info, System>, } /* ---------- events + errors ---------- */ #[event] pub struct RoundStarted { pub round: u64, pub duration: i64, pub deadline: i64 } #[event] pub struct LastCallTaken { pub round: u64, pub buyer: Pubkey, pub lamports_spent: u64 } #[event] pub struct Funded { pub round: u64, pub lamports: u64 } #[event] pub struct RoundSettled { pub round: u64, pub winner: Option, pub paid: u64 } #[error_code] pub enum LastCallError { #[msg("duration must be positive")] BadDuration, #[msg("the round has already ended")] RoundOver, #[msg("the round is still live")] RoundStillLive, #[msg("buy is below the minimum")] BuyTooSmall, #[msg("winner account does not match the recorded last buyer")] WrongWinnerAccount, }