Program
AiM3e4SvyeeoaBv9kbJNpr9UHhczqbh3T7KJLVYbSJd3
solana mainnet · anchor 0.31 · 769 lines · upgradeable by CTNk…6kPP until frozen · no verified build yet · solscan ↗
programs/nadiro/src/lib.rs · 769 lines · scrolls
//! NADIRO - every time the price falls 20% from its high, the coin's fees buy it. //! //! A coin launched here has the `pool` PDA as its pump.fun creator, so its creator fees land in the //! pool (`collect_curve` on the bonding curve, `collect_amm` after graduation; anyone can call both). //! `observe` reads the price from the coin's own reserves (the bonding curve, or the canonical //! PumpSwap pool after graduation) and raises the recorded high when the price is above it. //! When the price is at least 20% under the high, anyone can call `buy_dip_curve` or `buy_dip_amm`: //! the pool spends what it holds on the coin, at most 2% of the SOL side of the reserves per buy. The tokens go to the `holder` //! PDA and stay there: there is no instruction that sells or moves them. After a buy the high is //! reset to the dip price, so the next buy needs a new 20% fall from a new high. //! //! No fee to us and no admin instruction. The program is upgradeable by its deploy key until frozen. use anchor_lang::prelude::*; use anchor_lang::solana_program::instruction::{AccountMeta, Instruction}; use anchor_lang::solana_program::program::{invoke, invoke_signed}; declare_id!("AiM3e4SvyeeoaBv9kbJNpr9UHhczqbh3T7KJLVYbSJd3"); pub const PUMP_PROGRAM_ID: Pubkey = anchor_lang::pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"); pub const PUMP_AMM_PROGRAM_ID: Pubkey = anchor_lang::pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA"); pub const TOKEN_PROGRAM_ID: Pubkey = anchor_lang::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); pub const ATA_PROGRAM_ID: Pubkey = anchor_lang::pubkey!("ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL"); pub const WSOL_MINT: Pubkey = anchor_lang::pubkey!("So11111111111111111111111111111111111111112"); pub const PUMP_CREATE_DISC: [u8; 8] = [24, 30, 200, 40, 5, 28, 7, 119]; pub const PUMP_BUY_EXACT_SOL_DISC: [u8; 8] = [56, 252, 116, 8, 158, 223, 205, 95]; // buy_exact_sol_in pub const AMM_BUY_EXACT_QUOTE_DISC: [u8; 8] = [198, 46, 21, 82, 180, 217, 232, 112]; // buy_exact_quote_in pub const PUMP_COLLECT_DISC: [u8; 8] = [20, 22, 86, 123, 198, 28, 219, 132]; pub const AMM_COLLECT_DISC: [u8; 8] = [160, 57, 89, 42, 181, 139, 43, 66]; pub const TOKEN_ACCOUNT_LEN: usize = 165; pub const POOL_SEED: &[u8] = b"pool"; pub const HOLD_SEED: &[u8] = b"hold"; pub const MAX_NAME: usize = 32; pub const MAX_SYMBOL: usize = 10; pub const MAX_URI: usize = 200; pub const DROP_BPS: u128 = 2_000; // 20% under the high pub const PRICE_SCALE: u128 = 1_000_000_000_000; pub const MIN_BUY: u64 = 10_000_000; // 0.01 SOL pub const MAX_BUY: u64 = 10_000_000_000; // 10 SOL per dip pub const MIN_OUT_BPS: u128 = 9_000; // take no less than 90% of what the reserves give, on the curve and on PumpSwap pub const RESERVE_CAP_BPS: u128 = 200; // one dip spends at most 2% of the SOL side (virtual included), which bounds its price impact pub const HOLDER_FLOOR: u64 = 900_000; // a data-less account stays above rent exemption (890_880) pub const SETUP_RESERVE: u64 = 2 * 2_039_280 + 3_000_000; // token accounts + pump volume accumulator + the holder's floor, first buy only pub const SOURCE_CURVE: u8 = 0; pub const SOURCE_AMM: u8 = 1; #[program] pub mod nadiro { use super::*; /// Create the coin on pump.fun with the pool PDA as its creator. pub fn launch(ctx: Context<Launch>, name: String, symbol: String, uri: String) -> Result<()> { require!(!name.is_empty() && name.len() <= MAX_NAME, NadiroError::BadText); require!(!symbol.is_empty() && symbol.len() <= MAX_SYMBOL, NadiroError::BadText); require!(!uri.is_empty() && uri.len() <= MAX_URI, NadiroError::BadText); let a = &ctx.accounts; require!(a.mint.is_signer, NadiroError::MintMustSign); let pool_key = a.pool.key(); let mut data = Vec::with_capacity(8 + 4 + MAX_NAME + 4 + MAX_SYMBOL + 4 + MAX_URI + 32); data.extend_from_slice(&PUMP_CREATE_DISC); put_str(&mut data, &name); put_str(&mut data, &symbol); put_str(&mut data, &uri); data.extend_from_slice(pool_key.as_ref()); invoke( &Instruction { program_id: PUMP_PROGRAM_ID, accounts: vec![ AccountMeta::new(a.mint.key(), true), AccountMeta::new_readonly(a.mint_authority.key(), false), AccountMeta::new(a.bonding_curve.key(), false), AccountMeta::new(a.curve_ata.key(), false), AccountMeta::new_readonly(a.global.key(), false), AccountMeta::new_readonly(a.mpl.key(), false), AccountMeta::new(a.metadata.key(), false), AccountMeta::new(a.launcher.key(), true), AccountMeta::new_readonly(a.system_program.key(), false), AccountMeta::new_readonly(a.token_program.key(), false), AccountMeta::new_readonly(a.ata_program.key(), false), AccountMeta::new_readonly(a.rent.key(), false), AccountMeta::new_readonly(a.event_authority.key(), false), AccountMeta::new_readonly(a.pump_program.key(), false), ], data, }, &[ a.mint.to_account_info(), a.mint_authority.to_account_info(), a.bonding_curve.to_account_info(), a.curve_ata.to_account_info(), a.global.to_account_info(), a.mpl.to_account_info(), a.metadata.to_account_info(), a.launcher.to_account_info(), a.system_program.to_account_info(), a.token_program.to_account_info(), a.ata_program.to_account_info(), a.rent.to_account_info(), a.event_authority.to_account_info(), a.pump_program.to_account_info(), ], )?; let now = Clock::get()?.unix_timestamp; let p = &mut ctx.accounts.pool; p.mint = ctx.accounts.mint.key(); p.launcher = ctx.accounts.launcher.key(); p.high = 0; p.high_ts = 0; p.buys = 0; p.sol_spent = 0; p.tokens_bought = 0; p.collected_total = 0; p.created_ts = now; p.bump = ctx.bumps.pool; p.hold_bump = Pubkey::find_program_address(&[HOLD_SEED, p.mint.as_ref()], &crate::ID).1; emit!(Launched { pool: pool_key, mint: p.mint, launcher: p.launcher, ts: now }); Ok(()) } /// Anyone: read the price from the coin's reserves and raise the high if the price is above it. pub fn observe(ctx: Context<Observe>) -> Result<()> { let a = &ctx.accounts; let (price, _) = read_price(&a.pool.mint, &a.bonding_curve.to_account_info(), &a.amm_pool.to_account_info(), &a.amm_base_vault.to_account_info(), &a.amm_quote_vault.to_account_info())?; let now = Clock::get()?.unix_timestamp; let p = &mut ctx.accounts.pool; if price > p.high { p.high = price; p.high_ts = now; emit!(HighSet { pool: p.key(), price, ts: now }); } Ok(()) } /// Anyone, while the coin is on the bonding curve and 20% under its high: the pool buys. pub fn buy_dip_curve(ctx: Context<BuyDipCurve>) -> Result<()> { let a = &ctx.accounts; let curve_info = a.bonding_curve.to_account_info(); let (vtr, vsr, complete) = read_curve(&curve_info)?; require!(!complete, NadiroError::Graduated); let price = vsr * PRICE_SCALE / vtr.max(1); require_dip(a.pool.high, price)?; let spend = spendable(&a.pool.to_account_info())?.min((vsr * RESERVE_CAP_BPS / 10_000) as u64); require!(spend >= MIN_BUY, NadiroError::Empty); // fund the holder: the spend, plus its account rent the first time it buys let mut need = spend; if a.holder_ata.to_account_info().data_is_empty() || a.user_volume.to_account_info().data_is_empty() { need += SETUP_RESERVE; } let pool_info = a.pool.to_account_info(); require!(need <= above_rent(&pool_info)?, NadiroError::Empty); **pool_info.try_borrow_mut_lamports()? -= need; **a.holder.to_account_info().try_borrow_mut_lamports()? += need; let mint_key = a.pool.mint; let hseeds: &[&[u8]] = &[HOLD_SEED, mint_key.as_ref(), &[a.pool.hold_bump]]; sync(&a.holder.to_account_info(), &pool_info, &a.system_program.to_account_info(), hseeds)?; ata_create(&a.holder.to_account_info(), &a.holder_ata.to_account_info(), &a.holder.to_account_info(), &a.mint.to_account_info(), &a.system_program.to_account_info(), &a.token_program.to_account_info(), &a.ata_program.to_account_info(), hseeds)?; // spend exactly `spend`; refuse to take less than 90% of what the reserves give before fees let expected = vtr - (vtr * vsr) / (vsr + spend as u128); let min_out = (expected * MIN_OUT_BPS / 10_000) as u64; require!(min_out > 0, NadiroError::Empty); let tokens_before = token_amount(&a.holder_ata.to_account_info())?; let holder_before = a.holder.lamports(); let mut data = Vec::with_capacity(8 + 8 + 8 + 1); data.extend_from_slice(&PUMP_BUY_EXACT_SOL_DISC); data.extend_from_slice(&spend.to_le_bytes()); data.extend_from_slice(&min_out.to_le_bytes()); data.push(0); invoke_signed( &Instruction { program_id: PUMP_PROGRAM_ID, accounts: vec![ AccountMeta::new_readonly(a.global.key(), false), AccountMeta::new(a.fee_recipient.key(), false), AccountMeta::new_readonly(a.mint.key(), false), AccountMeta::new(a.bonding_curve.key(), false), AccountMeta::new(a.curve_ata.key(), false), AccountMeta::new(a.holder_ata.key(), false), AccountMeta::new(a.holder.key(), true), AccountMeta::new_readonly(a.system_program.key(), false), AccountMeta::new_readonly(a.token_program.key(), false), AccountMeta::new(a.creator_vault.key(), false), AccountMeta::new_readonly(a.event_authority.key(), false), AccountMeta::new_readonly(a.pump_program.key(), false), AccountMeta::new_readonly(a.global_volume.key(), false), AccountMeta::new(a.user_volume.key(), false), AccountMeta::new_readonly(a.fee_config.key(), false), AccountMeta::new_readonly(a.fee_program.key(), false), AccountMeta::new_readonly(a.bonding_curve_v2.key(), false), AccountMeta::new(a.buyback.key(), false), ], data, }, &[ a.global.to_account_info(), a.fee_recipient.to_account_info(), a.mint.to_account_info(), a.bonding_curve.to_account_info(), a.curve_ata.to_account_info(), a.holder_ata.to_account_info(), a.holder.to_account_info(), a.system_program.to_account_info(), a.token_program.to_account_info(), a.creator_vault.to_account_info(), a.event_authority.to_account_info(), a.pump_program.to_account_info(), a.global_volume.to_account_info(), a.user_volume.to_account_info(), a.fee_config.to_account_info(), a.fee_program.to_account_info(), a.bonding_curve_v2.to_account_info(), a.buyback.to_account_info(), ], &[hseeds], )?; let spent = holder_before.saturating_sub(a.holder.lamports()); let tokens = token_amount(&a.holder_ata.to_account_info())?.saturating_sub(tokens_before); // what the curve did not take goes back to the pool; the holder keeps only its floor let back = a.holder.lamports().saturating_sub(HOLDER_FLOOR); if back > 0 { invoke_signed( &anchor_lang::solana_program::system_instruction::transfer(&a.holder.key(), &pool_info.key(), back), &[a.holder.to_account_info(), pool_info.clone(), a.system_program.to_account_info()], &[hseeds], )?; } finish_buy(&mut ctx.accounts.pool, price, spent, tokens, SOURCE_CURVE) } /// Anyone, after graduation and 20% under the high: the pool buys on the canonical PumpSwap pool. pub fn buy_dip_amm<'info>(ctx: Context<'_, '_, '_, 'info, BuyDipAmm<'info>>) -> Result<()> { let a = &ctx.accounts; let (_, _, complete) = read_curve(&a.bonding_curve.to_account_info())?; require!(complete, NadiroError::NotGraduated); let (price, base_res, quote_res) = read_amm(&a.pool.mint, &a.amm_pool.to_account_info(), &a.pool_base.to_account_info(), &a.pool_quote.to_account_info())?; require_dip(a.pool.high, price)?; let spend = spendable(&a.pool.to_account_info())?.min((quote_res * RESERVE_CAP_BPS / 10_000) as u64); require!(spend >= MIN_BUY, NadiroError::Empty); let pool_info = a.pool.to_account_info(); let mint_key = a.pool.mint; let hseeds: &[&[u8]] = &[HOLD_SEED, mint_key.as_ref(), &[a.pool.hold_bump]]; // the holder's token accounts are paid for by the pool the first time; one move, then a sync CPI let setup = a.holder_ata.to_account_info().data_is_empty() || a.holder_wsol.to_account_info().data_is_empty() || a.user_volume.to_account_info().data_is_empty(); let total = spend.checked_add(if setup { SETUP_RESERVE } else { 0 }).ok_or(NadiroError::Math)?; require!(total <= above_rent(&pool_info)?, NadiroError::Empty); **pool_info.try_borrow_mut_lamports()? -= total; **a.holder.to_account_info().try_borrow_mut_lamports()? += total; sync(&a.holder.to_account_info(), &pool_info, &a.system_program.to_account_info(), hseeds)?; ata_create(&a.holder.to_account_info(), &a.holder_ata.to_account_info(), &a.holder.to_account_info(), &a.mint.to_account_info(), &a.system_program.to_account_info(), &a.token_program.to_account_info(), &a.ata_program.to_account_info(), hseeds)?; ata_create(&a.holder.to_account_info(), &a.holder_wsol.to_account_info(), &a.holder.to_account_info(), &a.wsol_mint.to_account_info(), &a.system_program.to_account_info(), &a.token_program.to_account_info(), &a.ata_program.to_account_info(), hseeds)?; // wrap: the holder sends the spend into its WSOL account, then SyncNative invoke_signed( &anchor_lang::solana_program::system_instruction::transfer(&a.holder.key(), &a.holder_wsol.key(), spend), &[a.holder.to_account_info(), a.holder_wsol.to_account_info(), a.system_program.to_account_info()], &[hseeds], )?; invoke( &Instruction { program_id: TOKEN_PROGRAM_ID, accounts: vec![AccountMeta::new(a.holder_wsol.key(), false)], data: vec![17] }, &[a.holder_wsol.to_account_info(), a.token_program.to_account_info()], )?; // spend an exact amount; take at least 90% of what the effective reserves give (PumpSwap fees are a few %) let expected = base_res * spend as u128 / (quote_res + spend as u128); let min_out = ((expected * MIN_OUT_BPS / 10_000) as u64).max(1); let tokens_before = token_amount(&a.holder_ata.to_account_info())?; let wsol_before = token_amount(&a.holder_wsol.to_account_info())?; let mut data = Vec::with_capacity(8 + 8 + 8 + 2); data.extend_from_slice(&AMM_BUY_EXACT_QUOTE_DISC); data.extend_from_slice(&spend.to_le_bytes()); data.extend_from_slice(&min_out.to_le_bytes()); // min_base_amount_out data.push(0); // track_volume: OptionBool(false) let mut metas = vec![ AccountMeta::new(a.amm_pool.key(), false), AccountMeta::new(a.holder.key(), true), AccountMeta::new_readonly(a.global_config.key(), false), AccountMeta::new_readonly(a.mint.key(), false), AccountMeta::new_readonly(a.wsol_mint.key(), false), AccountMeta::new(a.holder_ata.key(), false), AccountMeta::new(a.holder_wsol.key(), false), AccountMeta::new(a.pool_base.key(), false), AccountMeta::new(a.pool_quote.key(), false), AccountMeta::new_readonly(a.protocol_fee_recipient.key(), false), AccountMeta::new(a.protocol_fee_recipient_ata.key(), false), AccountMeta::new_readonly(a.token_program.key(), false), AccountMeta::new_readonly(a.token_program.key(), false), AccountMeta::new_readonly(a.system_program.key(), false), AccountMeta::new_readonly(a.ata_program.key(), false), AccountMeta::new_readonly(a.amm_event_authority.key(), false), AccountMeta::new_readonly(a.amm_program.key(), false), AccountMeta::new(a.creator_vault_ata.key(), false), AccountMeta::new_readonly(a.creator_vault_authority.key(), false), AccountMeta::new_readonly(a.global_volume.key(), false), AccountMeta::new(a.user_volume.key(), false), AccountMeta::new_readonly(a.fee_config.key(), false), AccountMeta::new_readonly(a.fee_program.key(), false), ]; let mut infos = vec![ a.amm_pool.to_account_info(), a.holder.to_account_info(), a.global_config.to_account_info(), a.mint.to_account_info(), a.wsol_mint.to_account_info(), a.holder_ata.to_account_info(), a.holder_wsol.to_account_info(), a.pool_base.to_account_info(), a.pool_quote.to_account_info(), a.protocol_fee_recipient.to_account_info(), a.protocol_fee_recipient_ata.to_account_info(), a.token_program.to_account_info(), a.system_program.to_account_info(), a.ata_program.to_account_info(), a.amm_event_authority.to_account_info(), a.amm_program.to_account_info(), a.creator_vault_ata.to_account_info(), a.creator_vault_authority.to_account_info(), a.global_volume.to_account_info(), a.user_volume.to_account_info(), a.fee_config.to_account_info(), a.fee_program.to_account_info(), ]; // PumpSwap has added accounts at the end of `buy` before; pass any extra ones through untouched for r in ctx.remaining_accounts.iter() { metas.push(if r.is_writable { AccountMeta::new(r.key(), false) } else { AccountMeta::new_readonly(r.key(), false) }); infos.push(r.clone()); } invoke_signed(&Instruction { program_id: PUMP_AMM_PROGRAM_ID, accounts: metas, data }, &infos, &[hseeds])?; let spent = wsol_before.saturating_sub(token_amount(&a.holder_wsol.to_account_info())?); let tokens = token_amount(&a.holder_ata.to_account_info())?.saturating_sub(tokens_before); finish_buy(&mut ctx.accounts.pool, price, spent, tokens, SOURCE_AMM) } /// Anyone: move the coin's bonding-curve creator fees into the pool. pub fn collect_curve(ctx: Context<CollectCurve>) -> Result<()> { let a = &ctx.accounts; let info = a.pool.to_account_info(); let before = info.lamports(); invoke( &Instruction { program_id: PUMP_PROGRAM_ID, accounts: vec![ AccountMeta::new(info.key(), false), AccountMeta::new(a.creator_vault.key(), false), AccountMeta::new_readonly(a.system_program.key(), false), AccountMeta::new_readonly(a.event_authority.key(), false), AccountMeta::new_readonly(a.pump_program.key(), false), ], data: PUMP_COLLECT_DISC.to_vec(), }, &[info.clone(), a.creator_vault.to_account_info(), a.system_program.to_account_info(), a.event_authority.to_account_info(), a.pump_program.to_account_info()], )?; let got = info.lamports().checked_sub(before).ok_or(NadiroError::Math)?; require!(got > 0, NadiroError::Nothing); let p = &mut ctx.accounts.pool; p.collected_total = p.collected_total.checked_add(got).ok_or(NadiroError::Math)?; emit!(Collected { pool: info.key(), source: SOURCE_CURVE, lamports: got, ts: Clock::get()?.unix_timestamp }); Ok(()) } /// Anyone: move the coin's PumpSwap creator fees into the pool as SOL. If the caller had to /// create the pool's WSOL account, its rent goes back to the caller. pub fn collect_amm(ctx: Context<CollectAmm>) -> Result<()> { let a = &ctx.accounts; let info = a.pool.to_account_info(); let pool_key = info.key(); let (expected, _) = Pubkey::find_program_address(&[pool_key.as_ref(), TOKEN_PROGRAM_ID.as_ref(), WSOL_MINT.as_ref()], &ATA_PROGRAM_ID); require_keys_eq!(a.pool_wsol.key(), expected, NadiroError::BadAccount); let created_here = a.pool_wsol.to_account_info().data_is_empty(); if created_here { invoke( &Instruction { program_id: ATA_PROGRAM_ID, accounts: vec![ AccountMeta::new(a.payer.key(), true), AccountMeta::new(a.pool_wsol.key(), false), AccountMeta::new_readonly(pool_key, false), AccountMeta::new_readonly(a.wsol_mint.key(), false), AccountMeta::new_readonly(a.system_program.key(), false), AccountMeta::new_readonly(a.token_program.key(), false), ], data: vec![1], }, &[a.payer.to_account_info(), a.pool_wsol.to_account_info(), info.clone(), a.wsol_mint.to_account_info(), a.system_program.to_account_info(), a.token_program.to_account_info(), a.ata_program.to_account_info()], )?; } invoke( &Instruction { program_id: PUMP_AMM_PROGRAM_ID, accounts: vec![ AccountMeta::new_readonly(a.wsol_mint.key(), false), AccountMeta::new_readonly(a.token_program.key(), false), AccountMeta::new_readonly(pool_key, false), AccountMeta::new_readonly(a.vault_authority.key(), false), AccountMeta::new(a.vault_ata.key(), false), AccountMeta::new(a.pool_wsol.key(), false), AccountMeta::new_readonly(a.amm_event_authority.key(), false), AccountMeta::new_readonly(a.amm_program.key(), false), ], data: AMM_COLLECT_DISC.to_vec(), }, &[a.wsol_mint.to_account_info(), a.token_program.to_account_info(), info.clone(), a.vault_authority.to_account_info(), a.vault_ata.to_account_info(), a.pool_wsol.to_account_info(), a.amm_event_authority.to_account_info(), a.amm_program.to_account_info()], )?; let before = info.lamports(); let mint_key = a.pool.mint; let seeds: &[&[u8]] = &[POOL_SEED, mint_key.as_ref(), &[a.pool.bump]]; invoke_signed( &Instruction { program_id: TOKEN_PROGRAM_ID, accounts: vec![AccountMeta::new(a.pool_wsol.key(), false), AccountMeta::new(pool_key, false), AccountMeta::new_readonly(pool_key, true)], data: vec![9], }, &[a.pool_wsol.to_account_info(), info.clone(), a.token_program.to_account_info()], &[seeds], )?; let landed = info.lamports().checked_sub(before).ok_or(NadiroError::Math)?; let rent = Rent::get()?.minimum_balance(TOKEN_ACCOUNT_LEN); let refund = if created_here { rent.min(landed) } else { 0 }; if refund > 0 { **info.try_borrow_mut_lamports()? -= refund; **a.payer.to_account_info().try_borrow_mut_lamports()? += refund; } let got = landed - refund; require!(got > 0, NadiroError::Nothing); let p = &mut ctx.accounts.pool; p.collected_total = p.collected_total.checked_add(got).ok_or(NadiroError::Math)?; emit!(Collected { pool: pool_key, source: SOURCE_AMM, lamports: got, ts: Clock::get()?.unix_timestamp }); Ok(()) } } fn finish_buy(p: &mut Account<Pool>, price: u128, spent: u64, tokens: u64, source: u8) -> Result<()> { require!(tokens > 0, NadiroError::Empty); let now = Clock::get()?.unix_timestamp; let high_was = p.high; p.buys += 1; p.sol_spent = p.sol_spent.checked_add(spent).ok_or(NadiroError::Math)?; p.tokens_bought = p.tokens_bought.checked_add(tokens).ok_or(NadiroError::Math)?; // the next buy needs a new 20% fall from a new high p.high = price; p.high_ts = now; emit!(Dipped { pool: p.key(), source, n: p.buys, high_was, price, lamports: spent, tokens, ts: now }); Ok(()) } fn require_dip(high: u128, price: u128) -> Result<()> { require!(high > 0, NadiroError::NoHigh); require!(price * 10_000 <= high * (10_000 - DROP_BPS), NadiroError::NotADip); Ok(()) } /// what one dip can spend: everything above the pool's rent, capped fn spendable(pool: &AccountInfo) -> Result<u64> { let s = above_rent(pool)?.saturating_sub(SETUP_RESERVE).min(MAX_BUY); require!(s >= MIN_BUY, NadiroError::Empty); Ok(s) } fn read_curve(curve: &AccountInfo) -> Result<(u128, u128, bool)> { require_keys_eq!(*curve.owner, PUMP_PROGRAM_ID, NadiroError::BadAccount); let d = curve.try_borrow_data()?; require!(d.len() >= 49, NadiroError::BadAccount); let vtr = u64::from_le_bytes(d[8..16].try_into().unwrap()) as u128; let vsr = u64::from_le_bytes(d[16..24].try_into().unwrap()) as u128; Ok((vtr, vsr, d[48] != 0)) } /// the canonical PumpSwap pool of a graduated pump.fun coin: ["pool", 0u16, pool-authority, mint, wsol] fn read_amm(mint: &Pubkey, pool: &AccountInfo, base: &AccountInfo, quote: &AccountInfo) -> Result<(u128, u128, u128)> { let (authority, _) = Pubkey::find_program_address(&[b"pool-authority", mint.as_ref()], &PUMP_PROGRAM_ID); let (canonical, _) = Pubkey::find_program_address(&[b"pool", &0u16.to_le_bytes(), authority.as_ref(), mint.as_ref(), WSOL_MINT.as_ref()], &PUMP_AMM_PROGRAM_ID); require_keys_eq!(pool.key(), canonical, NadiroError::BadAccount); require_keys_eq!(*pool.owner, PUMP_AMM_PROGRAM_ID, NadiroError::BadAccount); let d = pool.try_borrow_data()?; require!(d.len() >= 261, NadiroError::BadAccount); require_keys_eq!(Pubkey::new_from_array(d[139..171].try_into().unwrap()), base.key(), NadiroError::BadAccount); require_keys_eq!(Pubkey::new_from_array(d[171..203].try_into().unwrap()), quote.key(), NadiroError::BadAccount); // PumpSwap prices graduated pump coins on the quote vault plus a virtual quote reserve set at migration let virtual_quote = i128::from_le_bytes(d[245..261].try_into().unwrap()); drop(d); let b = token_amount(base)? as u128; let q = token_amount(quote)? as i128; let eq = q.checked_add(virtual_quote).ok_or(NadiroError::Math)?; require!(b > 0 && eq > 0, NadiroError::BadAccount); let eq = eq as u128; Ok((eq * PRICE_SCALE / b, b, eq)) } /// price on the curve while it trades there, on the canonical PumpSwap pool after graduation fn read_price(mint: &Pubkey, curve: &AccountInfo, pool: &AccountInfo, base: &AccountInfo, quote: &AccountInfo) -> Result<(u128, bool)> { let (vtr, vsr, complete) = read_curve(curve)?; if !complete { return Ok((vsr * PRICE_SCALE / vtr.max(1), false)); } let (p, _, _) = read_amm(mint, pool, base, quote)?; Ok((p, true)) } /// Lamports moved by hand between the pool and the holder are only seen by the runtime for accounts /// that take part in the next CPI. A zero transfer between the two makes both moves visible at once. fn sync<'a>(holder: &AccountInfo<'a>, pool: &AccountInfo<'a>, system: &AccountInfo<'a>, seeds: &[&[u8]]) -> Result<()> { invoke_signed( &anchor_lang::solana_program::system_instruction::transfer(&holder.key(), &pool.key(), 0), &[holder.clone(), pool.clone(), system.clone()], &[seeds], )?; Ok(()) } fn ata_create<'a>(payer: &AccountInfo<'a>, ata: &AccountInfo<'a>, owner: &AccountInfo<'a>, mint: &AccountInfo<'a>, system: &AccountInfo<'a>, token: &AccountInfo<'a>, ata_prog: &AccountInfo<'a>, seeds: &[&[u8]]) -> Result<()> { if !ata.data_is_empty() { return Ok(()); } invoke_signed( &Instruction { program_id: ATA_PROGRAM_ID, accounts: vec![ AccountMeta::new(payer.key(), true), AccountMeta::new(ata.key(), false), AccountMeta::new_readonly(owner.key(), false), AccountMeta::new_readonly(mint.key(), false), AccountMeta::new_readonly(system.key(), false), AccountMeta::new_readonly(token.key(), false), ], data: vec![1], }, &[payer.clone(), ata.clone(), owner.clone(), mint.clone(), system.clone(), token.clone(), ata_prog.clone()], &[seeds], )?; Ok(()) } fn put_str(v: &mut Vec<u8>, s: &str) { v.extend_from_slice(&(s.len() as u32).to_le_bytes()); v.extend_from_slice(s.as_bytes()); } fn above_rent(info: &AccountInfo) -> Result<u64> { let rent = Rent::get()?.minimum_balance(info.data_len()); Ok(info.lamports().saturating_sub(rent)) } fn token_amount(info: &AccountInfo) -> Result<u64> { if info.data_is_empty() { return Ok(0); } require_keys_eq!(*info.owner, TOKEN_PROGRAM_ID, NadiroError::BadAccount); let d = info.try_borrow_data()?; require!(d.len() >= 72, NadiroError::BadAccount); Ok(u64::from_le_bytes(d[64..72].try_into().unwrap())) } #[account] #[derive(InitSpace)] pub struct Pool { pub mint: Pubkey, pub launcher: Pubkey, pub high: u128, pub high_ts: i64, pub buys: u64, pub sol_spent: u64, pub tokens_bought: u64, pub collected_total: u64, pub created_ts: i64, pub bump: u8, pub hold_bump: u8, } #[derive(Accounts)] pub struct Launch<'info> { #[account(init, payer = launcher, space = 8 + Pool::INIT_SPACE, seeds = [POOL_SEED, mint.key().as_ref()], bump)] pub pool: Account<'info, Pool>, #[account(mut)] pub launcher: Signer<'info>, /// CHECK: new mint, must sign #[account(mut)] pub mint: UncheckedAccount<'info>, /// CHECK: pump pda pub mint_authority: UncheckedAccount<'info>, /// CHECK: pump pda #[account(mut)] pub bonding_curve: UncheckedAccount<'info>, /// CHECK: pump ata #[account(mut)] pub curve_ata: UncheckedAccount<'info>, /// CHECK: pump global pub global: UncheckedAccount<'info>, /// CHECK: metaplex pub mpl: UncheckedAccount<'info>, /// CHECK: metadata pda #[account(mut)] pub metadata: UncheckedAccount<'info>, pub system_program: Program<'info, System>, /// CHECK: spl token #[account(address = TOKEN_PROGRAM_ID)] pub token_program: UncheckedAccount<'info>, /// CHECK: ata program #[account(address = ATA_PROGRAM_ID)] pub ata_program: UncheckedAccount<'info>, /// CHECK: rent sysvar pub rent: UncheckedAccount<'info>, /// CHECK: pump event authority pub event_authority: UncheckedAccount<'info>, /// CHECK: pump program #[account(address = PUMP_PROGRAM_ID)] pub pump_program: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct Observe<'info> { #[account(mut, seeds = [POOL_SEED, pool.mint.as_ref()], bump = pool.bump)] pub pool: Account<'info, Pool>, /// CHECK: the coin's bonding curve #[account(seeds = [b"bonding-curve", pool.mint.as_ref()], bump, seeds::program = PUMP_PROGRAM_ID)] pub bonding_curve: UncheckedAccount<'info>, /// CHECK: the canonical PumpSwap pool, checked in read_amm when the curve is complete pub amm_pool: UncheckedAccount<'info>, /// CHECK: checked against the PumpSwap pool pub amm_base_vault: UncheckedAccount<'info>, /// CHECK: checked against the PumpSwap pool pub amm_quote_vault: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct BuyDipCurve<'info> { #[account(mut, seeds = [POOL_SEED, pool.mint.as_ref()], bump = pool.bump, has_one = mint)] pub pool: Account<'info, Pool>, /// CHECK: the data-less PDA that holds the bought tokens forever #[account(mut, seeds = [HOLD_SEED, pool.mint.as_ref()], bump = pool.hold_bump)] pub holder: UncheckedAccount<'info>, /// CHECK: the holder's token account, created on the first buy #[account(mut)] pub holder_ata: UncheckedAccount<'info>, /// CHECK: the coin pub mint: UncheckedAccount<'info>, /// CHECK: the coin's bonding curve #[account(mut, seeds = [b"bonding-curve", pool.mint.as_ref()], bump, seeds::program = PUMP_PROGRAM_ID)] pub bonding_curve: UncheckedAccount<'info>, /// CHECK: pump checks #[account(mut)] pub curve_ata: UncheckedAccount<'info>, /// CHECK: pump checks pub global: UncheckedAccount<'info>, /// CHECK: pump checks #[account(mut)] pub fee_recipient: UncheckedAccount<'info>, /// CHECK: pump's creator vault for this pool #[account(mut, seeds = [b"creator-vault", pool.key().as_ref()], bump, seeds::program = PUMP_PROGRAM_ID)] pub creator_vault: UncheckedAccount<'info>, /// CHECK: pump checks pub event_authority: UncheckedAccount<'info>, /// CHECK: pump checks pub global_volume: UncheckedAccount<'info>, /// CHECK: pump checks #[account(mut)] pub user_volume: UncheckedAccount<'info>, /// CHECK: pump checks pub fee_config: UncheckedAccount<'info>, /// CHECK: pump checks pub fee_program: UncheckedAccount<'info>, /// CHECK: pump checks pub bonding_curve_v2: UncheckedAccount<'info>, /// CHECK: pump checks #[account(mut)] pub buyback: UncheckedAccount<'info>, pub system_program: Program<'info, System>, /// CHECK: spl token #[account(address = TOKEN_PROGRAM_ID)] pub token_program: UncheckedAccount<'info>, /// CHECK: ata program #[account(address = ATA_PROGRAM_ID)] pub ata_program: UncheckedAccount<'info>, /// CHECK: pump program #[account(address = PUMP_PROGRAM_ID)] pub pump_program: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct BuyDipAmm<'info> { #[account(mut, seeds = [POOL_SEED, pool.mint.as_ref()], bump = pool.bump, has_one = mint)] pub pool: Account<'info, Pool>, /// CHECK: the data-less PDA that holds the bought tokens forever #[account(mut, seeds = [HOLD_SEED, pool.mint.as_ref()], bump = pool.hold_bump)] pub holder: UncheckedAccount<'info>, /// CHECK: the holder's token account #[account(mut)] pub holder_ata: UncheckedAccount<'info>, /// CHECK: the holder's WSOL account #[account(mut)] pub holder_wsol: UncheckedAccount<'info>, /// CHECK: the coin pub mint: UncheckedAccount<'info>, /// CHECK: wrapped SOL #[account(address = WSOL_MINT)] pub wsol_mint: UncheckedAccount<'info>, /// CHECK: the coin's bonding curve, read for the complete flag #[account(seeds = [b"bonding-curve", pool.mint.as_ref()], bump, seeds::program = PUMP_PROGRAM_ID)] pub bonding_curve: UncheckedAccount<'info>, /// CHECK: the canonical PumpSwap pool, checked in read_amm #[account(mut)] pub amm_pool: UncheckedAccount<'info>, /// CHECK: checked against the PumpSwap pool #[account(mut)] pub pool_base: UncheckedAccount<'info>, /// CHECK: checked against the PumpSwap pool #[account(mut)] pub pool_quote: UncheckedAccount<'info>, /// CHECK: PumpSwap checks pub global_config: UncheckedAccount<'info>, /// CHECK: PumpSwap checks pub protocol_fee_recipient: UncheckedAccount<'info>, /// CHECK: PumpSwap checks #[account(mut)] pub protocol_fee_recipient_ata: UncheckedAccount<'info>, /// CHECK: PumpSwap checks #[account(mut)] pub creator_vault_ata: UncheckedAccount<'info>, /// CHECK: PumpSwap checks pub creator_vault_authority: UncheckedAccount<'info>, /// CHECK: PumpSwap checks pub global_volume: UncheckedAccount<'info>, /// CHECK: PumpSwap checks #[account(mut)] pub user_volume: UncheckedAccount<'info>, /// CHECK: PumpSwap checks pub fee_config: UncheckedAccount<'info>, /// CHECK: PumpSwap checks pub fee_program: UncheckedAccount<'info>, /// CHECK: PumpSwap event authority pub amm_event_authority: UncheckedAccount<'info>, pub system_program: Program<'info, System>, /// CHECK: spl token #[account(address = TOKEN_PROGRAM_ID)] pub token_program: UncheckedAccount<'info>, /// CHECK: ata program #[account(address = ATA_PROGRAM_ID)] pub ata_program: UncheckedAccount<'info>, /// CHECK: PumpSwap #[account(address = PUMP_AMM_PROGRAM_ID)] pub amm_program: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct CollectCurve<'info> { #[account(mut, seeds = [POOL_SEED, pool.mint.as_ref()], bump = pool.bump)] pub pool: Account<'info, Pool>, /// CHECK: pump.fun's creator vault for this pool #[account(mut, seeds = [b"creator-vault", pool.key().as_ref()], bump, seeds::program = PUMP_PROGRAM_ID)] pub creator_vault: UncheckedAccount<'info>, pub system_program: Program<'info, System>, /// CHECK: pump.fun event authority #[account(seeds = [b"__event_authority"], bump, seeds::program = PUMP_PROGRAM_ID)] pub event_authority: UncheckedAccount<'info>, /// CHECK: pump.fun #[account(address = PUMP_PROGRAM_ID)] pub pump_program: UncheckedAccount<'info>, } #[derive(Accounts)] pub struct CollectAmm<'info> { #[account(mut, seeds = [POOL_SEED, pool.mint.as_ref()], bump = pool.bump)] pub pool: Account<'info, Pool>, /// pays the pool's WSOL account rent if it has to be created, and gets it back #[account(mut)] pub payer: Signer<'info>, /// CHECK: the pool's WSOL associated token account, checked in the handler #[account(mut)] pub pool_wsol: UncheckedAccount<'info>, /// CHECK: PumpSwap's creator vault authority for this pool #[account(seeds = [b"creator_vault", pool.key().as_ref()], bump, seeds::program = PUMP_AMM_PROGRAM_ID)] pub vault_authority: UncheckedAccount<'info>, /// CHECK: the vault authority's WSOL account, PumpSwap checks it #[account(mut)] pub vault_ata: UncheckedAccount<'info>, /// CHECK: wrapped SOL #[account(address = WSOL_MINT)] pub wsol_mint: UncheckedAccount<'info>, /// CHECK: spl token #[account(address = TOKEN_PROGRAM_ID)] pub token_program: UncheckedAccount<'info>, /// CHECK: ata program #[account(address = ATA_PROGRAM_ID)] pub ata_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, /// CHECK: PumpSwap event authority #[account(seeds = [b"__event_authority"], bump, seeds::program = PUMP_AMM_PROGRAM_ID)] pub amm_event_authority: UncheckedAccount<'info>, /// CHECK: PumpSwap #[account(address = PUMP_AMM_PROGRAM_ID)] pub amm_program: UncheckedAccount<'info>, } #[event] pub struct Launched { pub pool: Pubkey, pub mint: Pubkey, pub launcher: Pubkey, pub ts: i64 } #[event] pub struct HighSet { pub pool: Pubkey, pub price: u128, pub ts: i64 } #[event] pub struct Dipped { pub pool: Pubkey, pub source: u8, pub n: u64, pub high_was: u128, pub price: u128, pub lamports: u64, pub tokens: u64, pub ts: i64 } #[event] pub struct Collected { pub pool: Pubkey, pub source: u8, pub lamports: u64, pub ts: i64 } #[error_code] pub enum NadiroError { #[msg("text out of range")] BadText, #[msg("the mint keypair must sign")] MintMustSign, #[msg("nothing to move")] Nothing, #[msg("no high recorded yet: call observe first")] NoHigh, #[msg("the price is not 20% under the high")] NotADip, #[msg("the pool does not hold enough for a buy")] Empty, #[msg("the coin has graduated: use buy_dip_amm")] Graduated, #[msg("the coin is still on the curve: use buy_dip_curve")] NotGraduated, #[msg("wrong account")] BadAccount, #[msg("math")] Math, }
Accounts, in order
- launch(name, symbol, uri)pool (PDA "pool", mint) · launcher, signer · mint, signer · pump mint authority · bonding curve · curve ATA · pump global · metaplex · metadata · system · token · associated token · rent · pump event authority · pump
- observe()pool · bonding curve · canonical PumpSwap pool · its base vault · its quote vault (on the curve the last three are not read)
- buy_dip_curve()pool · holder (PDA "hold", mint) · holder ATA · mint · bonding curve · curve ATA · pump global · fee recipient · creator vault · event authority · global volume · holder volume · fee config · fee program · bonding curve v2 · buyback recipient · system · token · associated token · pump
- buy_dip_amm()pool · holder · holder ATA · holder WSOL ATA · mint · WSOL · bonding curve · PumpSwap pool · base vault · quote vault · global config · protocol fee recipient · its WSOL ATA · creator vault WSOL ATA · creator vault authority · global volume · holder volume · fee config · fee program · event authority · system · token · associated token · PumpSwap; then pool-v2, a buyback fee recipient and its WSOL ATA, passed through to PumpSwap
- collect_curve()pool · pump creator vault (PDA "creator-vault", pool) · system · pump event authority · pump
- collect_amm()pool · payer, signer · pool WSOL ATA · PumpSwap vault authority (PDA "creator_vault", pool) · its WSOL ATA · WSOL · token · associated token · system · PumpSwap event authority · PumpSwap
Instruction data is Anchor's: the first 8 bytes of sha256("global:<name>"), then the arguments. Events are "Program data:" lines with sha256("event:<Name>") as the first 8 bytes.