The state account laid out byte by byte.
| offset | length | field | type |
|---|---|---|---|
| 0 | 8 | discriminator | [u8;8] |
| 8 | 32 | mint | Pubkey |
| 40 | 32 | vault | Pubkey |
| 72 | 8 | anchor_cap_micro | u64 |
| 80 | 8 | burned_raw | u64 |
| 88 | 8 | steps_fired | u64 |
| 96 | 8 | spent_lamports | u64 |
| 104 | 1 | bump | u8 |
| 105 | 1 | vault_bump | u8 |
The full program follows. It is the same code that is deployed.
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Burn, Mint, Token, TokenAccount};
declare_id!("REPLACE_WITH_DEPLOYED_PROGRAM_ID");
pub const PUMP_PROGRAM: Pubkey = pubkey!("6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P");
pub const PUMPSWAP_PROGRAM: Pubkey = pubkey!("pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA");
pub const RESERVE_FLOOR_LAMPORTS: u64 = 50_000_000;
#[program]
pub mod ecurve {
use super::*;
pub fn initialize(ctx: Context<Initialize>, anchor_cap_micro: u64) -> Result<()> {
let s = &mut ctx.accounts.state;
s.mint = ctx.accounts.mint.key();
s.vault = ctx.accounts.vault.key();
s.anchor_cap_micro = anchor_cap_micro;
s.burned_raw = 0;
s.steps_fired = 0;
s.spent_lamports = 0;
s.bump = ctx.bumps.state;
Ok(())
}
pub fn step(ctx: Context<Step>, max_lamports: u64, min_tokens_raw: u64, evaluation_id: u64) -> Result<()> {
let vault_balance = ctx.accounts.vault.lamports();
require!(vault_balance > RESERVE_FLOOR_LAMPORTS, EcurveError::BelowFloor);
let spend = max_lamports.min(vault_balance - RESERVE_FLOOR_LAMPORTS);
require!(spend > 0, EcurveError::NothingToSpend);
let venue = ctx.accounts.venue_program.key();
require!(venue == PUMP_PROGRAM || venue == PUMPSWAP_PROGRAM, EcurveError::WrongVenue);
let before = ctx.accounts.program_token.amount;
let mint_key = ctx.accounts.mint.key();
let seeds: &[&[u8]] = &[b"vault", mint_key.as_ref(), &[ctx.accounts.state.vault_bump]];
crate::venue::buy_with_vault(&ctx, spend, min_tokens_raw, &[seeds])?;
ctx.accounts.program_token.reload()?;
let received = ctx.accounts.program_token.amount.checked_sub(before).unwrap();
require!(received >= min_tokens_raw, EcurveError::TooFewTokens);
token::burn(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
Burn {
mint: ctx.accounts.mint.to_account_info(),
from: ctx.accounts.program_token.to_account_info(),
authority: ctx.accounts.state.to_account_info(),
},
&[&[b"state", mint_key.as_ref(), &[ctx.accounts.state.bump]]],
),
received,
)?;
let s = &mut ctx.accounts.state;
s.burned_raw = s.burned_raw.checked_add(received).unwrap();
s.steps_fired = s.steps_fired.checked_add(1).unwrap();
s.spent_lamports = s.spent_lamports.checked_add(spend).unwrap();
let r = &mut ctx.accounts.step_record;
r.evaluation_id = evaluation_id;
r.step_index = s.steps_fired;
r.spend_lamports = spend;
r.burned_raw = received;
Ok(())
}
pub fn record_fee(ctx: Context<RecordFee>) -> Result<()> {
msg!("vault {}", ctx.accounts.vault.lamports());
Ok(())
}
}
#[account]
pub struct State {
pub mint: Pubkey,
pub vault: Pubkey,
pub anchor_cap_micro: u64,
pub burned_raw: u64,
pub steps_fired: u64,
pub spent_lamports: u64,
pub bump: u8,
pub vault_bump: u8,
}
#[account]
pub struct StepRecord {
pub evaluation_id: u64,
pub step_index: u64,
pub spend_lamports: u64,
pub burned_raw: u64,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = authority, space = 8 + 32 + 32 + 8 + 8 + 8 + 8 + 1 + 1, seeds = [b"state", mint.key().as_ref()], bump)]
pub state: Account<'info, State>,
/// CHECK: system owned PDA that only receives lamports
#[account(seeds = [b"vault", mint.key().as_ref()], bump)]
pub vault: UncheckedAccount<'info>,
pub mint: Account<'info, Mint>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
#[instruction(max_lamports: u64, min_tokens_raw: u64, evaluation_id: u64)]
pub struct Step<'info> {
#[account(mut, seeds = [b"state", mint.key().as_ref()], bump = state.bump)]
pub state: Account<'info, State>,
/// CHECK: the reserve
#[account(mut, seeds = [b"vault", mint.key().as_ref()], bump = state.vault_bump)]
pub vault: UncheckedAccount<'info>,
#[account(mut)]
pub mint: Account<'info, Mint>,
#[account(mut, associated_token::mint = mint, associated_token::authority = state)]
pub program_token: Account<'info, TokenAccount>,
#[account(init, payer = keeper, space = 8 + 8 + 8 + 8 + 8, seeds = [b"step", mint.key().as_ref(), &evaluation_id.to_le_bytes()], bump)]
pub step_record: Account<'info, StepRecord>,
#[account(mut)]
pub keeper: Signer<'info>,
/// CHECK: checked against the two allowed venue ids in the handler
pub venue_program: UncheckedAccount<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct RecordFee<'info> {
/// CHECK: read only
pub vault: UncheckedAccount<'info>,
}
#[error_code]
pub enum EcurveError {
#[msg("reserve is at or below the floor")]
BelowFloor,
#[msg("nothing to spend")]
NothingToSpend,
#[msg("venue program not allowed")]
WrongVenue,
#[msg("received fewer tokens than required")]
TooFewTokens,
}The program must show its name on Solana explorers. These steps are part of the deployment and are not optional.
| value | source | refresh |
|---|---|---|
| market cap, price | Birdeye price endpoint through the edge function | 1 s |
| 24h volume, holders, liquidity | Birdeye token overview through the edge function | 5 s |
| trades | Birdeye token transactions through the edge function | 1 s |
| price history for the figure seed | Birdeye OHLCV, one minute | on load, then 60 s |
| holder list | Birdeye holder endpoint | 10 s |
| SOL price | Birdeye price endpoint for the wrapped SOL mint | 5 s |
| reserve, burned, steps fired, spent | the state account and vault, read by the evaluate function and written to the database | on every evaluation |
| steps, evaluations | database tables | realtime subscription |
Anything shown here can be checked without trusting this site. The state account can be read with any RPC. The vault balance is a system account balance. Each step's transaction is on Solscan with its burn instruction visible. The market cap is Birdeye's own figure.
| anchor cap | The market cap recorded at initialization. The base the target grows from. |
| band | Two percent either side of the target. Nothing fires inside it. |
| burned share | Burned tokens divided by the initial supply of one billion. |
| evaluation | One reading of the spread, triggered by one observed trade. |
| kernel | The rule that turns a breach of the band into a fraction of the reserve to spend. |
| reserve | The vault balance in SOL. Funded by creator fees only. |
| spread | Market cap over target, minus one. |
| state | One of below, inside, above. |
| step | One buy and burn transaction sent by the program. |
| target | Anchor cap times e to the eight times burned share. |
| vault | The program's PDA that receives creator fees and pays for steps. |
| venue | pump.fun before graduation, PumpSwap after. |