Introduction
Pump.fun, the largest meme coin launchpad on Solana, offers a powerful feature: when a token reaches a certain price threshold, it can be paired with SOL to create a Raydium pool. The process involves three simple steps:
- Create a meme token on the Pump.fun program
- Swap the meme token on the Pump.fun program
- Once the meme token's price reaches a target value, migrate the SOL and tokens from Pump.fun to create a Raydium pool, enabling trading on Raydium
The success of Pump.fun has inspired many developers to create their own forks. Steps 1 and 2 are straightforward since they occur entirely within your forked program. However, step 3 presents a challenge: migrating your assets to Raydium. There are two main difficulties:
- Building the correct Raydium instructions
- Understanding Solana's account management system
This article will walk through these challenges step by step and demonstrate how to overcome them.
How does Solana manage accounts
First, you need to understand the basics of Solana accounts. You can find detailed information in the official documentation. The content below focuses on what's needed for this article. If you're already familiar with Solana accounts, feel free to skip to the next section.
Everything in Solana is an account. An account contains the following information:
pub struct Account {
/// lamports in the account
pub lamports: u64,
/// data held in this account
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
pub data: Vec<u8>,
/// the program that owns this account. If executable, the program that loads this account.
pub owner: Pubkey,
/// this account's data contains a loaded program (and is now read-only)
pub executable: bool,
/// the epoch at which this account will next owe rent
pub rent_epoch: Epoch,
}
Explanation: This struct defines the basic structure of a Solana account.
The lamports field stores the account's balance, data holds the account's state, owner specifies which program controls the account, executable indicates if it contains executable code,
and rent_epoch tracks when rent is due.
Based on the executable field, we can divide Solana accounts into two categories:
Program account: An account withexecutable = true. This account contains executable code. Deployed smart contracts use this type.Data account: An account withexecutable = false. This account doesn't contain executable code and is used to store data for a program.
The owner is the program that owns the account. Only owner program can
modify the account's state. Based on the owner address, we can divide
Solana accounts into four categories:
Program state account: An account whoseowneris a user-deployed program. It contains information related to that program.System account: An account whoseowneris theSystem Program. This type of account can pay transaction fees or rent-fee.Sysvar account: An account whoseowneris theSysvar Program. These are predefined addresses that provide access to cluster state data.Other: An account whoseowneris built-in programs other thanSystem programorSysvar program. We just mention it here for completeness, it's not relate to our topic.
Each account is identified by a unique 32-byte ID called an address, typically displayed as a base58-encoded string. Based on how addresses are generated, Solana accounts fall into two categories:
Public key address: The account address is a public key from an Ed25519 keypair. Whoever holds the corresponding private key can sign for this account.Program derived address (PDA): The account address is derived deterministically using aprogram's addressand one or more optionalseeds. This account has no private key. The Solana runtime allows the program from which the PDA is derived to sign on its behalf.
Solana Account Classification Summary
| Classification | Category | Description | Key Characteristic |
|---|---|---|---|
By executable | Program Account | Contains executable code | executable = true |
| Data Account | Stores data for programs | executable = false | |
By owner | Program State Account | Program-specific data storage | owner = User Program |
| System Account | Can pay transaction fees | owner = System Program | |
| Sysvar Account | Cluster state data access | owner = Sysvar Program | |
| Other | Other built-in programs | owner = Other Built-in | |
| By Address | Public Key Address | Ed25519 keypair-based | Has private key |
| PDA | Deterministically derived | No private key |
This table summarizes the different ways to classify Solana accounts based on their characteristics.
Create an FPump program
Now let's address the main purpose of this article: creating a Solana program that can migrate assets and create a Raydium pool. This section includes code examples, iterative updates, and references to a GitHub repository demonstrating the final implementation.
This article assumes you're familiar with Anchor and how to build programs on Solana using Anchor. Let's prepare the prerequisites and initialize an Anchor program.
# Prerequisites
# Anchor v0.31.1
# Node v0.22.15
# Rustc ≥ 1.79.0
#. Solana 2.3.0
anchor init pumpfun-integrate-raydiumExplanation: This command initializes a new Anchor project named "pumpfun-integrate-raydium". Make sure you have the required versions installed: Anchor v0.31.1, Node v0.22.15, Rust ≥ 1.79.0, and Solana 2.3.0.
In our FPump (Forked Pump) program, we use simplified logic supporting two instructions:
create_token: Creates a new mint, mints 1,000,000 tokens, and transfers 10 SOL into the vault. These assets are used to create a Raydium pool instead of waiting for users to swap.migrate: Creates a Raydium pool with 1,000,000 tokens and 9 SOL. The remaining 1 SOL is used to pay the creation fee.
Create a new token
We create a new mint token with 6 decimals, initialize a Vault PDA that stores 1,000,000 of the newly created tokens,
and transfer 10 SOL into the vault.
Raydium requires wrapped SOL (wSOL) instead of native SOL when creating a liquidity pool.
Therefore, in our FPump program, we receive native SOL and convert it to wSOL using the sync_native instruction.
Please check the branch mint-token for more details.
use anchor_lang::{prelude::*, solana_program::native_token::LAMPORTS_PER_SOL};
use anchor_spl::{
associated_token::AssociatedToken,
token::{self, Mint, SyncNative, Token, TokenAccount, sync_native},
};
use anchor_lang::system_program::{transfer, Transfer};
pub const NATIVE_MINT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
#[derive(Accounts)]
pub struct CreateToken<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(
init,
payer = signer,
mint::decimals = 6,
mint::authority = vault,
)]
pub mint: Box<Account<'info, Mint>>,
#[account(address = NATIVE_MINT)]
pub wsol_mint: Box<Account<'info, Mint>>,
#[account(
init,
payer = signer,
space = 8 + Vault::INIT_SPACE,
seeds = [Vault::SEED_PREFIX.as_bytes(), mint.key().as_ref()],
bump,
)]
pub vault: Account<'info, Vault>,
#[account(
init,
payer = signer,
associated_token::mint = mint,
associated_token::authority = vault,
)]
pub vault_token_account: Account<'info, TokenAccount>,
#[account(
init_if_needed,
payer = signer,
associated_token::mint = wsol_mint,
associated_token::authority = vault
)]
pub wsol_vault_token_account: Box<Account<'info, TokenAccount>>,
pub token_program: Program<'info, Token>,
pub associated_token_program: Program<'info, AssociatedToken>,
pub system_program: Program<'info, System>,
}
#[account]
#[derive(InitSpace)]
pub struct Vault {}
impl Vault {
pub const SEED_PREFIX: &'static str = "vault";
}
pub fn handler(ctx: Context<CreateToken>) -> Result<()> {
let mint_key = ctx.accounts.mint.key();
let vault_seeds: &[&[&[u8]]] = &[&[
Vault::SEED_PREFIX.as_bytes(),
mint_key.as_ref(),
&[ctx.bumps.vault],
]];
token::mint_to(
CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
token::MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.vault_token_account.to_account_info(),
authority: ctx.accounts.vault.to_account_info(),
},
vault_seeds,
),
1000_000_000_000,
)?;
transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
Transfer {
from: ctx.accounts.signer.to_account_info(),
to: ctx.accounts.wsol_vault_token_account.to_account_info(),
},
),
10 * LAMPORTS_PER_SOL,
)?;
sync_native(CpiContext::new(
ctx.accounts.token_program.to_account_info(),
SyncNative {
account: ctx.accounts.wsol_vault_token_account.to_account_info(),
},
))?;
Ok(())
}
Explanation: This code creates a new token mint and initializes a vault to store liquidity. The handler mints 1,000,000 tokens to the vault, transfers 10 SOL (converted to wSOL), and stores both assets in preparation for creating a Raydium pool. The vault uses a PDA (Program Derived Address) for secure asset management.
Create a Raydium pool
So far so good. Now let's create a Raydium pool. There are several Raydium programs available for creating pools,
each corresponding to specific pool logic. In this article, we use the latest version of CPMM, which is optimized
for pools that pair SOL with a token.
First, you need to install CPMM SDK. Add this dependency to your Cargo.toml, then rebuild your program.
[dependencies]
raydium-cp-swap = { git = "https://github.com/raydium-io/raydium-cp-swap", features = [
"no-entrypoint",
"cpi",
] }Explanation: This dependency adds the Raydium CPMM (Constant Product Market Maker) swap program to your project. The "no-entrypoint" and "cpi" features enable Cross-Program Invocation (CPI) from your program to Raydium's pool creation logic.
migrate instruction in FPump that calls the initialize instruction to create a new pool in the CPMM program.
Please check the branch migrate for more detailsuse anchor_lang::{prelude::*, solana_program::native_token::LAMPORTS_PER_SOL};
use anchor_spl::{
associated_token::AssociatedToken,
token::Token,
token_interface::{Mint, TokenAccount, TokenInterface},
};
use raydium_cp_swap::{
cpi,
states::{OBSERVATION_SEED, POOL_LP_MINT_SEED, POOL_SEED, POOL_VAULT_SEED},
AUTH_SEED,
};
use crate::Vault;
pub const NATIVE_MINT: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
#[derive(Accounts)]
pub struct MigrateToken<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account()]
pub mint: Box<InterfaceAccount<'info, Mint>>,
#[account(address = NATIVE_MINT)]
pub wsol_mint: Box<InterfaceAccount<'info, Mint>>,
/// Token_0 mint, the key must smaller then token_1 mint.
#[account(
constraint = token_0_mint.key() < token_1_mint.key(),
mint::token_program = token_0_program
)]
pub token_0_mint: Box<InterfaceAccount<'info, Mint>>,
/// Token_1 mint, the key must grater then token_0 mint.
#[account(mint::token_program = token_1_program)]
pub token_1_mint: Box<InterfaceAccount<'info, Mint>>,
/// CHECK: pool lp mint, init by cp-swap
#[account(
mut,
seeds = [
POOL_LP_MINT_SEED.as_bytes(),
pool_state.key().as_ref(),
],
seeds::program = cp_swap_program,
bump,
)]
pub lp_mint: UncheckedAccount<'info>,
#[account(
mut,
seeds = [Vault::SEED_PREFIX.as_bytes(), mint.key().as_ref()],
bump,
)]
pub vault: SystemAccount<'info>,
#[account(
mut,
associated_token::mint = wsol_mint,
associated_token::authority = vault
)]
pub wsol_vault_token_account: Box<InterfaceAccount<'info, TokenAccount>>,
/// CHECK
#[account(
mut,
seeds = [
vault.key().as_ref(),
token_0_program.key().as_ref(),
token_0_mint.key().as_ref(),
],
bump,
seeds::program = associated_token_program,
)]
pub creator_token_0: UncheckedAccount<'info>,
/// CHECK
#[account(
mut,
seeds = [
vault.key().as_ref(),
token_1_program.key().as_ref(),
token_1_mint.key().as_ref(),
],
bump,
seeds::program = associated_token_program,
)]
pub creator_token_1: UncheckedAccount<'info>,
/// CHECK
#[account(
mut,
seeds = [
vault.key().as_ref(),
token_program.key().as_ref(),
lp_mint.key().as_ref(),
],
bump,
seeds::program = associated_token_program,
)]
pub creator_lp_token: UncheckedAccount<'info>,
/// CHECKED: amm config, passed to cp-swap, checked by the raydium initialize instruction
#[account(
owner = cp_swap_program.key(),
)]
pub amm_config: UncheckedAccount<'info>,
/// CHECK: create pool fee account, will be checked in the raydium initialize instruction
#[account(mut)]
pub create_pool_fee: UncheckedAccount<'info>,
/// CHECK: lp mint authority, passed to cp-swap
#[account(
seeds = [AUTH_SEED.as_bytes()],
seeds::program = cp_swap_program,
bump
)]
pub authority: UncheckedAccount<'info>,
/// CHECK: Initialize an account to store the pool state, init by cp-swap
#[account(
mut,
seeds = [
POOL_SEED.as_bytes(),
amm_config.key().as_ref(),
token_0_mint.key().as_ref(),
token_1_mint.key().as_ref(),
],
seeds::program = cp_swap_program,
bump,
)]
pub pool_state: UncheckedAccount<'info>,
/// CHECK: an account to store oracle observations, init by cp-swap
#[account(
mut,
seeds = [
OBSERVATION_SEED.as_bytes(),
pool_state.key().as_ref(),
],
seeds::program = cp_swap_program,
bump,
)]
pub observation_state: UncheckedAccount<'info>,
/// CHECK: Token_0 vault for the pool, init by cp-swap
#[account(
mut,
seeds = [
POOL_VAULT_SEED.as_bytes(),
pool_state.key().as_ref(),
token_0_mint.key().as_ref()
],
seeds::program = cp_swap_program,
bump,
)]
pub token_0_vault: UncheckedAccount<'info>,
/// CHECK: Token_1 vault for the pool, init by cp-swap
#[account(
mut,
seeds = [
POOL_VAULT_SEED.as_bytes(),
pool_state.key().as_ref(),
token_1_mint.key().as_ref()
],
seeds::program = cp_swap_program,
bump,
)]
pub token_1_vault: UncheckedAccount<'info>,
/// Sysvar for program account
pub rent: Sysvar<'info, Rent>,
/// CHECK: Raydium swap program
#[account(
address = raydium_cp_swap::program::RaydiumCpSwap::id(),
)]
pub cp_swap_program: UncheckedAccount<'info>,
pub associated_token_program: Program<'info, AssociatedToken>,
/// Spl token program or token program 2022
pub token_0_program: Interface<'info, TokenInterface>,
/// Spl token program or token program 2022
pub token_1_program: Interface<'info, TokenInterface>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
pub fn handler(ctx: Context<MigrateToken>) -> Result<()> {
let mint_key = ctx.accounts.mint.key();
let vault = ctx.accounts.vault.to_account_info();
let wsol_vault = ctx.accounts.wsol_vault_token_account.to_account_info();
let vault_seeds: &[&[&[u8]]] = &[&[
Vault::SEED_PREFIX.as_bytes(),
mint_key.as_ref(),
&[ctx.bumps.vault],
]];
let cpi_accounts = cpi::accounts::Initialize {
creator: vault,
amm_config: ctx.accounts.amm_config.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
pool_state: ctx.accounts.pool_state.to_account_info(),
token_0_mint: ctx.accounts.token_0_mint.to_account_info(),
token_1_mint: ctx.accounts.token_1_mint.to_account_info(),
lp_mint: ctx.accounts.lp_mint.to_account_info(),
creator_token_0: ctx.accounts.creator_token_0.to_account_info(),
creator_token_1: ctx.accounts.creator_token_1.to_account_info(),
creator_lp_token: ctx.accounts.creator_lp_token.to_account_info(),
token_0_vault: ctx.accounts.token_0_vault.to_account_info(),
token_1_vault: ctx.accounts.token_1_vault.to_account_info(),
create_pool_fee: ctx.accounts.create_pool_fee.to_account_info(),
observation_state: ctx.accounts.observation_state.to_account_info(),
token_program: ctx.accounts.token_program.to_account_info(),
token_0_program: ctx.accounts.token_0_program.to_account_info(),
token_1_program: ctx.accounts.token_1_program.to_account_info(),
associated_token_program: ctx.accounts.associated_token_program.to_account_info(),
system_program: ctx.accounts.system_program.to_account_info(),
rent: ctx.accounts.rent.to_account_info(),
};
let cpi_context = CpiContext::new_with_signer(
ctx.accounts.cp_swap_program.to_account_info(),
cpi_accounts,
vault_seeds,
);
let amount_0: u64;
let amount_1: u64;
if ctx.accounts.token_0_mint.key() == NATIVE_MINT {
amount_0 = 9 * LAMPORTS_PER_SOL;
amount_1 = 1_000_000_000;
} else {
amount_0 = 1_000_000_000;
amount_1 = 9 * LAMPORTS_PER_SOL;
}
cpi::initialize(
cpi_context,
amount_0,
amount_1,
Clock::get()?.unix_timestamp as u64,
)?;
Ok(())
}
Explanation: This migrate instruction performs a Cross-Program Invocation (CPI) to Raydium's CPMM program to create a new liquidity pool. It sets up all required accounts, calculates token amounts based on which token is SOL, and invokes the initialize function to atomically create the pool with the stored assets from the vault.
Everything appears ready. However, when we execute the migration instruction, we encounter below error. What went wrong?
AnchorError
Let's correct the error
The error message is clear: Account vault is not AccountNotSystemOwned.
The migrate instruction requires vault to be owned by the SystemProgram.
#[derive(Accounts)]
pub struct Initialize<'info> {
/// Address paying to create the pool. Can be anyone
#[account(mut)]
pub creator: Signer<'info>,
.....................................................
/// pool lp mint
#[account(
init,
seeds = [
POOL_LP_MINT_SEED.as_bytes(),
pool_state.key().as_ref(),
],
bump,
mint::decimals = 9,
mint::authority = authority,
payer = creator,
mint::token_program = token_program,
)]
pub lp_mint: Box<InterfaceAccount<'info, Mint>>,
.....................................................
}
Explanation: This snippet from Raydium's initialize instruction shows that the creator account (which is our vault) must be a Signer that pays for initializing the LP mint. Only SystemAccounts can pay
initialization fees, but our vault was already initialized as a Program State Account.
Why? vault is declared as creator in the initialize instruction,
and this creator is responsible for paying the creation fee for the lp_token. Only a SystemAccount can pay that fee.
However, vault is a Program State Account managed by the FPump program because we have already initialized it.
To fix this, we simply skip the init step. By default, all uninitialized accounts are owned by the SystemProgram.
With this change, everything works perfectly. You can check the master branch for the final code.
Please check the branch main for more details
#[derive(Accounts)]
pub struct CreateToken<'info> {
.....................................................
#[account(
mut,
seeds = [Vault::SEED_PREFIX.as_bytes(), mint.key().as_ref()],
bump,
)]
pub vault: SystemAccount<'info>,
.....................................................
}
Explanation: The fix is simple: change the vault account type from Account<'info, Vault> to SystemAccount<'info> and remove the init constraint. This keeps the vault as a
SystemAccount (owned by the System Program), allowing it to pay fees while still functioning as a PDA for signing operations.
Conclusion
Successfully migrating assets from a Pump.fun-style program to Raydium requires understanding two critical concepts: Solana's account ownership model and proper instruction construction. The key insight is recognizing that only SystemAccounts can pay transaction fees and rent fees—a requirement that necessitates careful account initialization.
By storing assets by a SystemAccount-owned PDA account and invoking Raydium's CPMM initialize instruction via CPI, we achieve atomic liquidity migration. This pattern enables automated market making without manual intervention, creating a seamless transition from bonding curve trading to full DEX liquidity.