//! spendo: a coin's creator fees, split between its creator and its holders' gift cards. //! //! A coin's pump.fun creator registers it with a split from 0 to 50% for themselves; the split can never change after that. The //! coin's fees are pointed at its spendo account (pump.fun fee sharing). Anyone can book what arrived: the creator's //! share becomes claimable by the creator only, the rest goes to the holders' card pool. The operator (the server that //! buys gift cards on Bitrefill) can take from the card pool only, at most once every 20 hours, and every take names //! the Bitrefill order it paid for, on chain. Nobody can take the creator's share but the creator. use anchor_lang::prelude::*; declare_id!("Fpmg4VsFHa3fRriXd4Y6ZiYZKRbCKcaeX7FwF8YYGrY2"); pub const DEPLOYER: Pubkey = pubkey!("CTNkYr8FmCqiPsyx3Xt59mL4sLryJmFDM36wvg5e6kPP"); pub const PUMP: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); pub const MAX_CREATOR_BPS: u16 = 5_000; pub const SPEND_GAP: i64 = 20 * 3_600; pub const MAX_ORDER: usize = 64; #[program] pub mod spendo { use super::*; pub fn init(ctx: Context, operator: Pubkey) -> Result<()> { let c = &mut ctx.accounts.config; c.admin = ctx.accounts.admin.key(); c.operator = operator; c.bump = ctx.bumps.config; Ok(()) } pub fn set_operator(ctx: Context, operator: Pubkey) -> Result<()> { ctx.accounts.config.operator = operator; Ok(()) } pub fn set_admin(ctx: Context, admin: Pubkey) -> Result<()> { ctx.accounts.config.admin = admin; Ok(()) } /// The creator, once per coin: the split is fixed from here on. pub fn register(ctx: Context, creator_bps: u16) -> Result<()> { require!(creator_bps <= MAX_CREATOR_BPS, Err::SplitTooHigh); // only the coin's own pump.fun creator can register it, so nobody can squat someone else's coin let mint = ctx.accounts.mint.key(); let curve = &ctx.accounts.curve; let (expect, _) = Pubkey::find_program_address(&[b"bonding-curve", mint.as_ref()], &PUMP); require_keys_eq!(curve.key(), expect, Err::NotCreator); require_keys_eq!(*curve.owner, PUMP, Err::NotCreator); { let d = curve.try_borrow_data()?; require!(d.len() >= 81 && d[49..81] == ctx.accounts.creator.key().to_bytes(), Err::NotCreator); } let c = &mut ctx.accounts.coin; c.mint = ctx.accounts.mint.key(); c.creator = ctx.accounts.creator.key(); c.creator_bps = creator_bps; c.bump = ctx.bumps.coin; emit!(Registered { coin: c.key(), mint: c.mint, creator: c.creator, creator_bps }); Ok(()) } /// Anyone: split whatever fees arrived since the last booking. pub fn book(ctx: Context) -> Result<()> { book_new(&mut ctx.accounts.coin) } pub fn claim_creator(ctx: Context) -> Result<()> { book_new(&mut ctx.accounts.coin)?; let amount = ctx.accounts.coin.creator_owed; require!(amount > 0, Err::Nothing); let coin = ctx.accounts.coin.to_account_info(); **coin.try_borrow_mut_lamports()? -= amount; **ctx.accounts.creator.try_borrow_mut_lamports()? += amount; let c = &mut ctx.accounts.coin; c.creator_owed = 0; c.creator_paid = c.creator_paid.checked_add(amount).ok_or(Err::Overflow)?; emit!(CreatorPaid { coin: c.key(), amount }); Ok(()) } /// The operator: take from the holders' card pool to buy gift cards, naming the Bitrefill order. pub fn spend(ctx: Context, amount: u64, cards: u16, order: String) -> Result<()> { require!(!order.is_empty() && order.len() <= MAX_ORDER, Err::BadOrder); require!(cards > 0, Err::Nothing); book_new(&mut ctx.accounts.coin)?; let now = Clock::get()?.unix_timestamp; let c = &ctx.accounts.coin; require!(c.last_spend == 0 || now >= c.last_spend + SPEND_GAP, Err::TooSoon); require!(amount > 0 && amount <= c.card_pool, Err::OverPool); let coin = ctx.accounts.coin.to_account_info(); **coin.try_borrow_mut_lamports()? -= amount; **ctx.accounts.operator.try_borrow_mut_lamports()? += amount; let c = &mut ctx.accounts.coin; c.card_pool -= amount; c.spent = c.spent.checked_add(amount).ok_or(Err::Overflow)?; c.cards = c.cards.checked_add(cards as u64).ok_or(Err::Overflow)?; c.last_spend = now; emit!(Spent { coin: c.key(), amount, cards, order }); Ok(()) } /// The creator can hand the creator role (and the claim) to another wallet. The split stays. pub fn set_creator(ctx: Context, creator: Pubkey) -> Result<()> { ctx.accounts.coin.creator = creator; Ok(()) } } /// New lamports = balance above rent that is not already owed to the creator or sitting in the card pool. fn book_new(c: &mut Account) -> Result<()> { let info = c.to_account_info(); let keep = Rent::get()?.minimum_balance(info.data_len()); let tracked = keep.checked_add(c.creator_owed).and_then(|x| x.checked_add(c.card_pool)).ok_or(Err::Overflow)?; let new = info.lamports().saturating_sub(tracked); if new > 0 { let to_creator = ((new as u128) * (c.creator_bps as u128) / 10_000) as u64; c.creator_owed = c.creator_owed.checked_add(to_creator).ok_or(Err::Overflow)?; c.card_pool = c.card_pool.checked_add(new - to_creator).ok_or(Err::Overflow)?; c.booked = c.booked.checked_add(new).ok_or(Err::Overflow)?; emit!(Booked { coin: c.key(), lamports: new, to_creator }); } Ok(()) } #[account] #[derive(InitSpace)] pub struct Config { pub admin: Pubkey, pub operator: Pubkey, pub bump: u8, } #[account] #[derive(InitSpace)] pub struct Coin { pub mint: Pubkey, pub creator: Pubkey, pub creator_bps: u16, pub creator_owed: u64, pub creator_paid: u64, pub card_pool: u64, pub booked: u64, pub spent: u64, pub cards: u64, pub last_spend: i64, pub bump: u8, } #[derive(Accounts)] pub struct Init<'info> { #[account(init, payer = admin, space = 8 + Config::INIT_SPACE, seeds = [b"config"], bump)] pub config: Account<'info, Config>, #[account(mut, address = DEPLOYER)] pub admin: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct AdminOnly<'info> { #[account(mut, seeds = [b"config"], bump = config.bump, has_one = admin)] pub config: Account<'info, Config>, pub admin: Signer<'info>, } #[derive(Accounts)] pub struct Register<'info> { #[account(init, payer = creator, space = 8 + Coin::INIT_SPACE, seeds = [b"coin", mint.key().as_ref()], bump)] pub coin: Account<'info, Coin>, /// CHECK: the coin's mint pub mint: UncheckedAccount<'info>, /// CHECK: the coin's pump.fun bonding curve, checked in the handler pub curve: UncheckedAccount<'info>, #[account(mut)] pub creator: Signer<'info>, pub system_program: Program<'info, System>, } #[derive(Accounts)] pub struct Book<'info> { #[account(mut, seeds = [b"coin", coin.mint.as_ref()], bump = coin.bump)] pub coin: Account<'info, Coin>, } #[derive(Accounts)] pub struct ClaimCreator<'info> { #[account(mut, seeds = [b"coin", coin.mint.as_ref()], bump = coin.bump, has_one = creator)] pub coin: Account<'info, Coin>, #[account(mut)] pub creator: Signer<'info>, } #[derive(Accounts)] pub struct Spend<'info> { #[account(seeds = [b"config"], bump = config.bump, has_one = operator)] pub config: Account<'info, Config>, #[account(mut, seeds = [b"coin", coin.mint.as_ref()], bump = coin.bump)] pub coin: Account<'info, Coin>, #[account(mut)] pub operator: Signer<'info>, } #[derive(Accounts)] pub struct SetCreator<'info> { #[account(mut, seeds = [b"coin", coin.mint.as_ref()], bump = coin.bump, has_one = creator)] pub coin: Account<'info, Coin>, pub creator: Signer<'info>, } #[event] pub struct Registered { pub coin: Pubkey, pub mint: Pubkey, pub creator: Pubkey, pub creator_bps: u16 } #[event] pub struct Booked { pub coin: Pubkey, pub lamports: u64, pub to_creator: u64 } #[event] pub struct CreatorPaid { pub coin: Pubkey, pub amount: u64 } #[event] pub struct Spent { pub coin: Pubkey, pub amount: u64, pub cards: u16, pub order: String } #[error_code] pub enum Err { #[msg("the creator split is at most 50%")] SplitTooHigh, #[msg("nothing to pay")] Nothing, #[msg("the order id is empty or too long")] BadOrder, #[msg("one card buy per 20 hours")] TooSoon, #[msg("more than the card pool")] OverPool, #[msg("only the coin's pump.fun creator can register it")] NotCreator, #[msg("overflow")] Overflow, }