fix(pumpfun): prefer event creator_vault; align fees with pump-sdk

- Use protocol_params.creator_vault for buy/sell when non-default to avoid
  ConstraintSeeds (2006) when gRPC/event creator drifts from on-chain bonding curve
- Preserve event creator_vault in PumpFunParams::from_trade/from_dev_trade instead
  of always overwriting with PDA(creator)
- Fee recipient: prefer gRPC fee_recipient; else random from CURRENT_FEE_RECIPIENTS
  pool (standard) or MAYHEM pool, matching @pump-fun/pump-sdk fees.ts
- Add cold-path helpers: extend_bonding_curve_account_instruction, PUMP_BONDING_CURVE_MIN_DATA_LEN

Made-with: Cursor
This commit is contained in:
0xfnzero
2026-04-11 19:13:12 +08:00
parent 4b451af5ff
commit 9c9ecf3e5b
3 changed files with 139 additions and 30 deletions
+54 -18
View File
@@ -8,8 +8,9 @@ use crate::{
};
use crate::{
instruction::utils::pumpfun::{
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator,
get_mayhem_fee_recipient_meta_random, get_user_volume_accumulator_pda,
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator_vault_pda,
get_mayhem_fee_recipient_meta_random, get_standard_fee_recipient_meta_random,
get_user_volume_accumulator_pda,
global_constants::{self},
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
},
@@ -42,8 +43,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
}
let bonding_curve = &protocol_params.bonding_curve;
let creator_vault_pda = protocol_params.creator_vault;
let creator = get_creator(&creator_vault_pda);
// creator_vault must match PDA(["creator-vault", bonding_curve.creator_on_chain]). Events/gRPC
// sometimes have a stale `creator` but a correct `creator_vault` from parsed ix; prefer non-default.
let creator = bonding_curve.creator;
let creator_vault_pda = if protocol_params.creator_vault != Pubkey::default() {
protocol_params.creator_vault
} else {
get_creator_vault_pda(&creator).ok_or_else(|| {
anyhow!(
"creator_vault PDA derivation failed (creator={})",
creator
)
})?
};
// ========================================
// Trade calculation and account address preparation
@@ -106,6 +118,8 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
// ========================================
// 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
@@ -142,12 +156,19 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
buy_data[24..26].copy_from_slice(&track_volume);
}
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
let fee_recipient_meta = if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
global_constants::FEE_RECIPIENT_META
};
// Fee recipient: prefer gRPC/event pubkey (matches live trades); else @pump-fun/pump-sdk getFeeRecipient.
let fee_recipient_meta =
if protocol_params.fee_recipient != Pubkey::default() {
AccountMeta {
pubkey: protocol_params.fee_recipient,
is_signer: false,
is_writable: true,
}
} else if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
get_standard_fee_recipient_meta_random()
};
let bonding_curve_v2 = get_bonding_curve_v2_pda(&params.output_mint).ok_or_else(|| {
anyhow!("bonding_curve_v2 PDA derivation failed for mint {}", params.output_mint)
@@ -197,8 +218,17 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
};
let bonding_curve = &protocol_params.bonding_curve;
let creator_vault_pda = protocol_params.creator_vault;
let creator = get_creator(&creator_vault_pda);
let creator = bonding_curve.creator;
let creator_vault_pda = if protocol_params.creator_vault != Pubkey::default() {
protocol_params.creator_vault
} else {
get_creator_vault_pda(&creator).ok_or_else(|| {
anyhow!(
"creator_vault PDA derivation failed (creator={})",
creator
)
})?
};
// ========================================
// Trade calculation and account address preparation
@@ -264,12 +294,18 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
sell_data[8..16].copy_from_slice(&token_amount.to_le_bytes());
sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
// Determine fee recipient based on mayhem mode (pump-public-docs: 2nd account = Mayhem fee recipient; use any one randomly)
let fee_recipient_meta = if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
global_constants::FEE_RECIPIENT_META
};
let fee_recipient_meta =
if protocol_params.fee_recipient != Pubkey::default() {
AccountMeta {
pubkey: protocol_params.fee_recipient,
is_signer: false,
is_writable: true,
}
} else if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random()
} else {
get_standard_fee_recipient_meta_random()
};
let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META,
+58 -2
View File
@@ -1,9 +1,40 @@
use crate::common::{bonding_curve::BondingCurveAccount, SolanaRpcClient};
use anyhow::anyhow;
use rand::seq::IndexedRandom;
use solana_sdk::{instruction::AccountMeta, pubkey::Pubkey};
use solana_sdk::{
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
};
use std::sync::Arc;
// --- Aligned with official `@pump-fun/pump-sdk` (npm) ---
// - `src/fees.ts` `getFeeRecipient(global, mayhemMode)` — fee recipient pools
// - `src/bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient`
// - `src/sdk.ts` `BONDING_CURVE_NEW_SIZE` (151) + `extendAccountInstruction` — **not** called from the
// trade hot path here (no RPC in `PumpFunInstructionBuilder`); use these helpers from a cold path if needed.
/// Minimum bonding curve account data length after protocol upgrades (`sdk.ts` `BONDING_CURVE_NEW_SIZE`).
pub const PUMP_BONDING_CURVE_MIN_DATA_LEN: usize = 151;
/// Anchor discriminator for `extend_account` (`pump.json`); same as `PumpSdk.extendAccountInstruction`.
pub const EXTEND_ACCOUNT_DISCRIMINATOR: [u8; 8] = [234, 102, 194, 203, 150, 72, 62, 229];
/// Build `extend_account` for bonding curve (cold path / separate tx only — do not add RPC to hot-path builds).
#[inline]
pub fn extend_bonding_curve_account_instruction(bonding_curve: &Pubkey, user: &Pubkey) -> Instruction {
Instruction {
program_id: accounts::PUMPFUN,
accounts: vec![
AccountMeta::new(*bonding_curve, false),
AccountMeta::new(*user, true),
crate::constants::SYSTEM_PROGRAM_META,
accounts::EVENT_AUTHORITY_META,
accounts::PUMPFUN_META,
],
data: EXTEND_ACCOUNT_DISCRIMINATOR.to_vec(),
}
}
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
pub mod seeds {
/// Seed for bonding curve PDAs
@@ -190,7 +221,8 @@ pub fn is_amm_fee_recipient(pubkey: &Pubkey) -> bool {
|| pubkey == &global_constants::PUMPFUN_AMM_FEE_7
}
/// Returns a random Mayhem fee recipient AccountMeta (pump-public-docs: Bonding Curve 2nd account = Mayhem fee recipient; use any one randomly).
/// Mayhem: random among `Global.reservedFeeRecipient` + `Global.reservedFeeRecipients` (`fees.ts` `getFeeRecipient` when `mayhemMode === true`).
/// Uses hardcoded `MAYHEM_FEE_RECIPIENTS`; prefer gRPC/event `PumpFunParams.fee_recipient` when set.
#[inline]
pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
let recipient = *global_constants::MAYHEM_FEE_RECIPIENTS
@@ -199,6 +231,30 @@ pub fn get_mayhem_fee_recipient_meta_random() -> AccountMeta {
AccountMeta { pubkey: recipient, is_signer: false, is_writable: true }
}
/// Non-mayhem: random among `Global::fee_recipient` + `Global::fee_recipients[0..7]`.
/// Same pubkey set as `bondingCurve.ts` `CURRENT_FEE_RECIPIENTS` / `getStaticRandomFeeRecipient` and `fees.ts` `getFeeRecipient` when `mayhemMode === false`.
#[inline]
pub fn get_standard_fee_recipient_meta_random() -> AccountMeta {
const POOL: &[Pubkey] = &[
global_constants::FEE_RECIPIENT,
global_constants::PUMPFUN_AMM_FEE_1,
global_constants::PUMPFUN_AMM_FEE_2,
global_constants::PUMPFUN_AMM_FEE_3,
global_constants::PUMPFUN_AMM_FEE_4,
global_constants::PUMPFUN_AMM_FEE_5,
global_constants::PUMPFUN_AMM_FEE_6,
global_constants::PUMPFUN_AMM_FEE_7,
];
let recipient = *POOL
.choose(&mut rand::rng())
.unwrap_or(&global_constants::FEE_RECIPIENT);
AccountMeta {
pubkey: recipient,
is_signer: false,
is_writable: true,
}
}
pub struct Symbol;
impl Symbol {
+27 -10
View File
@@ -119,20 +119,22 @@ impl std::fmt::Debug for SwapParams {
/// PumpFun protocol specific parameters
/// Configuration parameters specific to PumpFun trading protocol.
///
/// **Creator Rewards Sharing**: Some coins use a dynamic `creator_vault` (fee-sharing config).
/// Always use the latest on-chain creator/vault when building params for **sell**; do not reuse
/// cached params from buy. Either fetch fresh data via RPC, or pass `creator_vault` from gRPC
/// using [`from_trade`](PumpFunParams::from_trade) / [`from_dev_trade`](PumpFunParams::from_dev_trade),
/// or override with [`with_creator_vault`](PumpFunParams::with_creator_vault).
/// **Creator vault**: Pump buy/sell instructions always pass `creator_vault` =
/// `PDA(["creator-vault", bonding_curve.creator])` derived from [`BondingCurveAccount::creator`].
/// Keep `bonding_curve.creator` in sync with chain (gRPC / RPC); stale `creator_vault` in this struct
/// does not affect ix building.
#[derive(Clone)]
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey,
/// Creator vault PDA. For Creator Rewards Sharing coins this can change; pass latest from gRPC when selling.
/// From events/parsed ix when set; else derived from `bonding_curve.creator`. Buy/sell prefer non-default.
pub creator_vault: Pubkey,
pub token_program: Pubkey,
/// Whether to close token account when selling, only effective during sell operations
pub close_token_account_when_sell: Option<bool>,
/// Fee recipient for buy/sell account #2. When set from gRPC (matches `@pump-fun/pump-sdk` `fees.ts` / observed trades), mirrors on-chain choice.
/// `Pubkey::default()` uses the same random pools as `getFeeRecipient` / `getStaticRandomFeeRecipient` in the npm SDK.
pub fee_recipient: Pubkey,
}
impl PumpFunParams {
@@ -147,6 +149,7 @@ impl PumpFunParams {
creator_vault: creator_vault,
token_program: token_program,
close_token_account_when_sell: Some(close_token_account_when_sell),
fee_recipient: Pubkey::default(),
}
}
@@ -179,12 +182,19 @@ impl PumpFunParams {
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = if creator_vault != Pubkey::default() {
creator_vault
} else {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator)
.unwrap_or_default()
};
Self {
bonding_curve: Arc::new(bonding_curve_account),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault,
creator_vault: creator_vault_resolved,
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
}
}
@@ -220,12 +230,19 @@ impl PumpFunParams {
is_mayhem_mode,
is_cashback_coin,
);
let creator_vault_resolved = if creator_vault != Pubkey::default() {
creator_vault
} else {
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator)
.unwrap_or_default()
};
Self {
bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve,
creator_vault: creator_vault,
creator_vault: creator_vault_resolved,
close_token_account_when_sell: close_token_account_when_sell,
token_program: token_program,
fee_recipient,
}
}
@@ -262,11 +279,11 @@ impl PumpFunParams {
creator_vault: creator_vault.unwrap(),
close_token_account_when_sell: None,
token_program: mint_account.owner,
fee_recipient: Pubkey::default(),
})
}
/// Override `creator_vault` with a value from gRPC/event (e.g. for Creator Rewards Sharing).
/// Use when selling so the instruction uses the latest on-chain vault and avoids "seeds constraint violated" (2006).
/// Updates the cached `creator_vault` field only. Buy/sell ix use [`BondingCurveAccount::creator`].
#[inline]
pub fn with_creator_vault(mut self, creator_vault: Pubkey) -> Self {
self.creator_vault = creator_vault;