From 4dec087ea66d71306875bf2a6bae0ca512646d1f Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:37:05 +0800 Subject: [PATCH] fix(pumpfun): resolve creator_vault vs PDA(creator); validate fee recipient pool - Add resolve_creator_vault_for_ix: use gRPC creator_vault only when it matches PDA(['creator-vault', bonding_curve.creator]); on mismatch use derived PDA to avoid ConstraintSeeds (2006) from stale/wrong stream vaults - pump_fun_fee_recipient_meta: only use event fee_recipient if authorized for is_mayhem_mode (avoids 6000 NotAuthorized when standard AMM fee is used on Mayhem coins) - Align PumpFunParams::from_trade/from_dev_trade cached creator_vault with the same resolution logic Made-with: Cursor --- src/instruction/pumpfun.rs | 54 +++++++++----------------------- src/instruction/utils/pumpfun.rs | 40 +++++++++++++++++++++++ src/trading/core/params.rs | 24 +++++++------- 3 files changed, 65 insertions(+), 53 deletions(-) diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 54f5934..c739cfc 100755 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -8,9 +8,9 @@ use crate::{ }; use crate::{ instruction::utils::pumpfun::{ - 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, + accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, + get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta, + resolve_creator_vault_for_ix, global_constants::{self}, BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, }, @@ -43,19 +43,16 @@ impl InstructionBuilder for PumpFunInstructionBuilder { } let bonding_curve = &protocol_params.bonding_curve; - // 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. + // 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 = if protocol_params.creator_vault != Pubkey::default() { - protocol_params.creator_vault - } else { - get_creator_vault_pda(&creator).ok_or_else(|| { + let creator_vault_pda = resolve_creator_vault_for_ix(&creator, protocol_params.creator_vault) + .ok_or_else(|| { anyhow!( "creator_vault PDA derivation failed (creator={})", creator ) - })? - }; + })?; // ======================================== // Trade calculation and account address preparation @@ -156,19 +153,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder { buy_data[24..26].copy_from_slice(&track_volume); } - // Fee recipient: prefer gRPC/event pubkey (matches live trades); else @pump-fun/pump-sdk getFeeRecipient. + // Fee recipient: event hint only if allowed for `is_mayhem_mode` (else 6000 NotAuthorized). 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() - }; + 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) @@ -219,16 +206,13 @@ impl InstructionBuilder for PumpFunInstructionBuilder { let bonding_curve = &protocol_params.bonding_curve; 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(|| { + let creator_vault_pda = resolve_creator_vault_for_ix(&creator, protocol_params.creator_vault) + .ok_or_else(|| { anyhow!( "creator_vault PDA derivation failed (creator={})", creator ) - })? - }; + })?; // ======================================== // Trade calculation and account address preparation @@ -295,17 +279,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes()); 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() - }; + pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); 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 3624926..56089c9 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -255,6 +255,32 @@ pub fn get_standard_fee_recipient_meta_random() -> AccountMeta { } } +/// Use gRPC/event fee recipient only if authorized for this bonding curve mode; otherwise pick +/// randomly from the same pool as `@pump-fun/pump-sdk` `getFeeRecipient` (avoids 6000 NotAuthorized +/// when event fee is from a different mode, e.g. standard AMM fee on a Mayhem coin). +#[inline] +pub fn pump_fun_fee_recipient_meta(from_event: Pubkey, is_mayhem_mode: bool) -> AccountMeta { + if from_event != Pubkey::default() { + let authorized = if is_mayhem_mode { + is_mayhem_fee_recipient(&from_event) + } else { + from_event == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(&from_event) + }; + if authorized { + return AccountMeta { + pubkey: from_event, + is_signer: false, + is_writable: true, + }; + } + } + if is_mayhem_mode { + get_mayhem_fee_recipient_meta_random() + } else { + get_standard_fee_recipient_meta_random() + } +} + pub struct Symbol; impl Symbol { @@ -316,6 +342,20 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option { ) } +/// Creator vault for buy/sell must be `PDA(["creator-vault", bonding_curve.creator])` (same as on-chain). +/// Use gRPC `from_event` only when it equals that PDA; if it differs (wrong mint / stale parse), use derived. +#[inline] +pub fn resolve_creator_vault_for_ix(creator: &Pubkey, from_event: Pubkey) -> Option { + let derived = get_creator_vault_pda(creator)?; + if from_event == Pubkey::default() { + Some(derived) + } else if from_event == derived { + Some(from_event) + } else { + Some(derived) + } +} + #[inline] pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option { crate::common::fast_fn::get_cached_pda( diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index 8c03480..c4a05a9 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -127,7 +127,7 @@ impl std::fmt::Debug for SwapParams { pub struct PumpFunParams { pub bonding_curve: Arc, pub associated_bonding_curve: Pubkey, - /// From events/parsed ix when set; else derived from `bonding_curve.creator`. Buy/sell prefer non-default. + /// `PDA(["creator-vault", bonding_curve.creator])`; from_trade resolves when event vault mismatches. pub creator_vault: Pubkey, pub token_program: Pubkey, /// Whether to close token account when selling, only effective during sell operations @@ -182,12 +182,11 @@ 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() - }; + let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix( + &bonding_curve_account.creator, + creator_vault, + ) + .unwrap_or_default(); Self { bonding_curve: Arc::new(bonding_curve_account), associated_bonding_curve: associated_bonding_curve, @@ -230,12 +229,11 @@ 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() - }; + let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix( + &bonding_curve.creator, + creator_vault, + ) + .unwrap_or_default(); Self { bonding_curve: Arc::new(bonding_curve), associated_bonding_curve: associated_bonding_curve,