Skip to content
nadiroLaunch a coin

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.