From 9c9ecf3e5b8cd171e46c19043670e57adb4ce6f4 Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:13:12 +0800 Subject: [PATCH] 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 --- src/instruction/pumpfun.rs | 72 ++++++++++++++++++++++++-------- src/instruction/utils/pumpfun.rs | 60 +++++++++++++++++++++++++- src/trading/core/params.rs | 37 +++++++++++----- 3 files changed, 139 insertions(+), 30 deletions(-) diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index c223de5..54f5934 100755 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -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(¶ms.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 = vec![ global_constants::GLOBAL_ACCOUNT_META, diff --git a/src/instruction/utils/pumpfun.rs b/src/instruction/utils/pumpfun.rs index 1982df6..3624926 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -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 { diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index 8f5243e..8c03480 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -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, 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, + /// 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;