fix(pumpfun): trust explicit ix creator_vault over creator-derived PDA
- resolve_creator_vault_for_ix_with_fee_sharing uses non-placeholder ix vault as-is. - When ix vault missing, fall back to fee_sharing_creator_vault_if_active then PDA(creator). - Add Pump troubleshooting CN doc; extend PumpFunParams field documentation. - Bump to v4.0.7. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Executable → Regular
+66
-74
@@ -1,3 +1,5 @@
|
||||
//! Pump.fun bonding-curve swap ix assembly ([`SwapParams`](crate::trading::core::params::SwapParams)).
|
||||
|
||||
use crate::{
|
||||
common::spl_token::close_account,
|
||||
constants::{trade::trade::DEFAULT_SLIPPAGE, TOKEN_PROGRAM_2022},
|
||||
@@ -26,30 +28,37 @@ use anyhow::{anyhow, Result};
|
||||
use solana_sdk::instruction::AccountMeta;
|
||||
use solana_sdk::{instruction::Instruction, pubkey::Pubkey, signer::Signer};
|
||||
|
||||
/// Instruction builder for PumpFun protocol
|
||||
#[inline]
|
||||
fn effective_pump_mint_token_program(protocol_params: &PumpFunParams) -> Pubkey {
|
||||
let tp = protocol_params.token_program;
|
||||
if tp == Pubkey::default() {
|
||||
TOKEN_PROGRAM_2022
|
||||
} else {
|
||||
tp
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PumpFunInstructionBuilder;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
.as_any()
|
||||
.downcast_ref::<PumpFunParams>()
|
||||
.ok_or_else(|| anyhow!("Invalid protocol params for PumpFun"))?;
|
||||
|
||||
if params.input_amount.unwrap_or(0) == 0 {
|
||||
let lamports_in = params.input_amount.unwrap_or(0);
|
||||
if lamports_in == 0 {
|
||||
return Err(anyhow!("Amount cannot be zero"));
|
||||
}
|
||||
|
||||
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
// creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived;
|
||||
// if stream sends a mismatched vault (wrong token / stale), fall back to derived.
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
protocol_params.creator_vault,
|
||||
¶ms.output_mint,
|
||||
@@ -62,9 +71,6 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
)
|
||||
})?;
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let buy_token_amount = match params.fixed_output_amount {
|
||||
Some(amount) => amount,
|
||||
None => get_buy_token_amount_from_sol_amount(
|
||||
@@ -72,25 +78,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
bonding_curve.real_token_reserves as u128,
|
||||
creator,
|
||||
params.input_amount.unwrap_or(0),
|
||||
lamports_in,
|
||||
),
|
||||
};
|
||||
|
||||
let max_sol_cost = calculate_with_slippage_buy(
|
||||
params.input_amount.unwrap_or(0),
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let max_sol_cost = calculate_with_slippage_buy(lamports_in, slippage_bp);
|
||||
|
||||
// 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子,
|
||||
// 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
|
||||
// Determine token program based on mayhem mode
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = protocol_params.token_program;
|
||||
let token_program_meta = if protocol_params.token_program == TOKEN_PROGRAM_2022 {
|
||||
let token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_META
|
||||
@@ -114,14 +114,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
let user_volume_accumulator = get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
// Hot path: no RPC here (latency). For legacy curves <151 bytes, use
|
||||
// `extend_bonding_curve_account_instruction` from a cold path or separate tx.
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
|
||||
// Create associated token account
|
||||
if params.create_output_mint_ata {
|
||||
instructions.extend(
|
||||
crate::common::fast_fn::create_associated_token_account_idempotent_fast_use_seed(
|
||||
@@ -135,12 +129,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
}
|
||||
|
||||
let buy_data = if params.use_exact_sol_amount.unwrap_or(true) {
|
||||
let min_tokens_out = calculate_with_slippage_sell(
|
||||
buy_token_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
);
|
||||
let min_tokens_out = calculate_with_slippage_sell(buy_token_amount, slippage_bp);
|
||||
encode_pumpfun_buy_exact_sol_in_ix_data(
|
||||
params.input_amount.unwrap_or(0),
|
||||
lamports_in,
|
||||
min_tokens_out,
|
||||
TRACK_VOLUME_TRUE,
|
||||
)
|
||||
@@ -148,14 +139,13 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
encode_pumpfun_buy_ix_data(buy_token_amount, max_sol_cost, TRACK_VOLUME_TRUE)
|
||||
};
|
||||
|
||||
// Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
|
||||
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.output_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
|
||||
})?;
|
||||
let mut accounts: Vec<AccountMeta> = vec![
|
||||
let mut metas: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
fee_recipient_meta,
|
||||
AccountMeta::new_readonly(params.output_mint, false),
|
||||
@@ -165,7 +155,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
AccountMeta::new(params.payer.pubkey(), true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
token_program_meta,
|
||||
AccountMeta::new(creator_vault_pda, false),
|
||||
AccountMeta::new(creator_vault_account, false),
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
accounts::GLOBAL_VOLUME_ACCUMULATOR_META,
|
||||
@@ -173,19 +163,22 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
accounts::FEE_CONFIG_META,
|
||||
accounts::FEE_PROGRAM_META,
|
||||
];
|
||||
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false)); // remainingAccounts: @pump-fun/pump-sdk 要求末尾传 bondingCurveV2Pda(mint),勿删
|
||||
// Apr 2026: extra protocol fee recipient after bonding-curve-v2 (writable)
|
||||
accounts.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
||||
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new(
|
||||
get_protocol_extra_fee_recipient_random(),
|
||||
false,
|
||||
));
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &buy_data, accounts));
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&buy_data,
|
||||
metas,
|
||||
));
|
||||
|
||||
Ok(instructions)
|
||||
}
|
||||
|
||||
async fn build_sell_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
|
||||
// ========================================
|
||||
// Parameter validation and basic data preparation
|
||||
// ========================================
|
||||
let protocol_params = params
|
||||
.protocol_params
|
||||
.as_any()
|
||||
@@ -201,9 +194,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
return Err(anyhow!("Amount token is required"));
|
||||
};
|
||||
|
||||
let slippage_bp = params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE);
|
||||
|
||||
let bonding_curve = &protocol_params.bonding_curve;
|
||||
let creator = bonding_curve.creator;
|
||||
let creator_vault_pda = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
let creator = protocol_params.effective_creator_for_trade();
|
||||
let creator_vault_account = resolve_creator_vault_for_ix_with_fee_sharing(
|
||||
&creator,
|
||||
protocol_params.creator_vault,
|
||||
¶ms.input_mint,
|
||||
@@ -216,9 +211,6 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
)
|
||||
})?;
|
||||
|
||||
// ========================================
|
||||
// Trade calculation and account address preparation
|
||||
// ========================================
|
||||
let sol_amount = get_sell_sol_amount_from_token_amount(
|
||||
bonding_curve.virtual_token_reserves as u128,
|
||||
bonding_curve.virtual_sol_reserves as u128,
|
||||
@@ -228,20 +220,16 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
|
||||
let min_sol_output = match params.fixed_output_amount {
|
||||
Some(fixed) => fixed,
|
||||
None => calculate_with_slippage_sell(
|
||||
sol_amount,
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
),
|
||||
None => calculate_with_slippage_sell(sol_amount, slippage_bp),
|
||||
};
|
||||
|
||||
let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
|
||||
})?;
|
||||
|
||||
// Determine token program based on mayhem mode
|
||||
let is_mayhem_mode = bonding_curve.is_mayhem_mode;
|
||||
let token_program = protocol_params.token_program;
|
||||
let token_program_meta = if protocol_params.token_program == TOKEN_PROGRAM_2022 {
|
||||
let token_program = effective_pump_mint_token_program(protocol_params);
|
||||
let token_program_meta = if token_program == TOKEN_PROGRAM_2022 {
|
||||
crate::constants::TOKEN_PROGRAM_2022_META
|
||||
} else {
|
||||
crate::constants::TOKEN_PROGRAM_META
|
||||
@@ -262,17 +250,12 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
params.open_seed_optimize,
|
||||
);
|
||||
|
||||
// ========================================
|
||||
// Build instructions
|
||||
// ========================================
|
||||
let mut instructions = Vec::with_capacity(2);
|
||||
|
||||
let sell_data = encode_pumpfun_sell_ix_data(token_amount, min_sol_output);
|
||||
|
||||
let fee_recipient_meta =
|
||||
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
|
||||
|
||||
let mut accounts: Vec<AccountMeta> = vec![
|
||||
let mut metas: Vec<AccountMeta> = vec![
|
||||
global_constants::GLOBAL_ACCOUNT_META,
|
||||
fee_recipient_meta,
|
||||
AccountMeta::new_readonly(params.input_mint, false),
|
||||
@@ -281,7 +264,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
AccountMeta::new(user_token_account, false),
|
||||
AccountMeta::new(params.payer.pubkey(), true),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
AccountMeta::new(creator_vault_pda, false),
|
||||
AccountMeta::new(creator_vault_account, false),
|
||||
token_program_meta,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
@@ -289,23 +272,28 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
accounts::FEE_PROGRAM_META,
|
||||
];
|
||||
|
||||
// Cashback: Bonding Curve Sell expects UserVolumeAccumulator PDA at 0th remaining account (writable)
|
||||
if bonding_curve.is_cashback_coin {
|
||||
let user_volume_accumulator =
|
||||
get_user_volume_accumulator_pda(¶ms.payer.pubkey())
|
||||
.ok_or_else(|| anyhow!("user_volume_accumulator PDA derivation failed"))?;
|
||||
accounts.push(AccountMeta::new(user_volume_accumulator, false));
|
||||
metas.push(AccountMeta::new(user_volume_accumulator, false));
|
||||
}
|
||||
// remainingAccounts: @pump-fun/pump-sdk sell 要求末尾传 bondingCurveV2Pda(mint)(cashback 时在 user_volume_accumulator 之后),勿删
|
||||
|
||||
let bonding_curve_v2 = get_bonding_curve_v2_pda(¶ms.input_mint).ok_or_else(|| {
|
||||
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.input_mint)
|
||||
})?;
|
||||
accounts.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
accounts.push(AccountMeta::new(get_protocol_extra_fee_recipient_random(), false));
|
||||
metas.push(AccountMeta::new_readonly(bonding_curve_v2, false));
|
||||
metas.push(AccountMeta::new(
|
||||
get_protocol_extra_fee_recipient_random(),
|
||||
false,
|
||||
));
|
||||
|
||||
instructions.push(Instruction::new_with_bytes(accounts::PUMPFUN, &sell_data, accounts));
|
||||
instructions.push(Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&sell_data,
|
||||
metas,
|
||||
));
|
||||
|
||||
// Optional: Close token account
|
||||
if protocol_params.close_token_account_when_sell.unwrap_or(false)
|
||||
|| params.close_input_mint_ata
|
||||
{
|
||||
@@ -322,16 +310,20 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/// Claim cashback for Bonding Curve (Pump program). Transfers native lamports from UserVolumeAccumulator to user.
|
||||
/// Claim cashback (UserVolumeAccumulator → user lamports).
|
||||
pub fn claim_cashback_pumpfun_instruction(payer: &Pubkey) -> Option<Instruction> {
|
||||
const CLAIM_CASHBACK_DISCRIMINATOR: [u8; 8] = [37, 58, 35, 126, 190, 53, 228, 197];
|
||||
let user_volume_accumulator = get_user_volume_accumulator_pda(payer)?;
|
||||
let accounts = vec![
|
||||
AccountMeta::new(*payer, true), // user (signer, writable)
|
||||
AccountMeta::new(user_volume_accumulator, false), // user_volume_accumulator (writable, not signer)
|
||||
let ix_accounts = vec![
|
||||
AccountMeta::new(*payer, true),
|
||||
AccountMeta::new(user_volume_accumulator, false),
|
||||
crate::constants::SYSTEM_PROGRAM_META,
|
||||
accounts::EVENT_AUTHORITY_META,
|
||||
accounts::PUMPFUN_META,
|
||||
];
|
||||
Some(Instruction::new_with_bytes(accounts::PUMPFUN, &CLAIM_CASHBACK_DISCRIMINATOR, accounts))
|
||||
Some(Instruction::new_with_bytes(
|
||||
accounts::PUMPFUN,
|
||||
&CLAIM_CASHBACK_DISCRIMINATOR,
|
||||
ix_accounts,
|
||||
))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user