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
This commit is contained in:
0xfnzero
2026-04-11 19:37:05 +08:00
parent 9c9ecf3e5b
commit 4dec087ea6
3 changed files with 65 additions and 53 deletions
+14 -40
View File
@@ -8,9 +8,9 @@ use crate::{
}; };
use crate::{ use crate::{
instruction::utils::pumpfun::{ instruction::utils::pumpfun::{
accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda, get_creator_vault_pda, accounts, get_bonding_curve_pda, get_bonding_curve_v2_pda,
get_mayhem_fee_recipient_meta_random, get_standard_fee_recipient_meta_random, get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta,
get_user_volume_accumulator_pda, resolve_creator_vault_for_ix,
global_constants::{self}, global_constants::{self},
BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR, BUY_DISCRIMINATOR, BUY_EXACT_SOL_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
}, },
@@ -43,19 +43,16 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
} }
let bonding_curve = &protocol_params.bonding_curve; let bonding_curve = &protocol_params.bonding_curve;
// creator_vault must match PDA(["creator-vault", bonding_curve.creator_on_chain]). Events/gRPC // creator_vault must be PDA(creator) per bonding curve. Event vault: use only if == derived;
// sometimes have a stale `creator` but a correct `creator_vault` from parsed ix; prefer non-default. // if stream sends a mismatched vault (wrong token / stale), fall back to derived.
let creator = bonding_curve.creator; let creator = bonding_curve.creator;
let creator_vault_pda = if protocol_params.creator_vault != Pubkey::default() { let creator_vault_pda = resolve_creator_vault_for_ix(&creator, protocol_params.creator_vault)
protocol_params.creator_vault .ok_or_else(|| {
} else {
get_creator_vault_pda(&creator).ok_or_else(|| {
anyhow!( anyhow!(
"creator_vault PDA derivation failed (creator={})", "creator_vault PDA derivation failed (creator={})",
creator creator
) )
})? })?;
};
// ======================================== // ========================================
// Trade calculation and account address preparation // Trade calculation and account address preparation
@@ -156,19 +153,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
buy_data[24..26].copy_from_slice(&track_volume); 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 = let fee_recipient_meta =
if protocol_params.fee_recipient != Pubkey::default() { pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
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(|| { 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) 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 bonding_curve = &protocol_params.bonding_curve;
let creator = bonding_curve.creator; let creator = bonding_curve.creator;
let creator_vault_pda = if protocol_params.creator_vault != Pubkey::default() { let creator_vault_pda = resolve_creator_vault_for_ix(&creator, protocol_params.creator_vault)
protocol_params.creator_vault .ok_or_else(|| {
} else {
get_creator_vault_pda(&creator).ok_or_else(|| {
anyhow!( anyhow!(
"creator_vault PDA derivation failed (creator={})", "creator_vault PDA derivation failed (creator={})",
creator creator
) )
})? })?;
};
// ======================================== // ========================================
// Trade calculation and account address preparation // 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()); sell_data[16..24].copy_from_slice(&min_sol_output.to_le_bytes());
let fee_recipient_meta = let fee_recipient_meta =
if protocol_params.fee_recipient != Pubkey::default() { pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode);
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![ let mut accounts: Vec<AccountMeta> = vec![
global_constants::GLOBAL_ACCOUNT_META, global_constants::GLOBAL_ACCOUNT_META,
+40
View File
@@ -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; pub struct Symbol;
impl Symbol { impl Symbol {
@@ -316,6 +342,20 @@ pub fn get_creator_vault_pda(creator: &Pubkey) -> Option<Pubkey> {
) )
} }
/// 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<Pubkey> {
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] #[inline]
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> { pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
crate::common::fast_fn::get_cached_pda( crate::common::fast_fn::get_cached_pda(
+11 -13
View File
@@ -127,7 +127,7 @@ impl std::fmt::Debug for SwapParams {
pub struct PumpFunParams { pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>, pub bonding_curve: Arc<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey, 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 creator_vault: Pubkey,
pub token_program: Pubkey, pub token_program: Pubkey,
/// Whether to close token account when selling, only effective during sell operations /// Whether to close token account when selling, only effective during sell operations
@@ -182,12 +182,11 @@ impl PumpFunParams {
is_mayhem_mode, is_mayhem_mode,
is_cashback_coin, is_cashback_coin,
); );
let creator_vault_resolved = if creator_vault != Pubkey::default() { let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
creator_vault &bonding_curve_account.creator,
} else { creator_vault,
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator) )
.unwrap_or_default() .unwrap_or_default();
};
Self { Self {
bonding_curve: Arc::new(bonding_curve_account), bonding_curve: Arc::new(bonding_curve_account),
associated_bonding_curve: associated_bonding_curve, associated_bonding_curve: associated_bonding_curve,
@@ -230,12 +229,11 @@ impl PumpFunParams {
is_mayhem_mode, is_mayhem_mode,
is_cashback_coin, is_cashback_coin,
); );
let creator_vault_resolved = if creator_vault != Pubkey::default() { let creator_vault_resolved = crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix(
creator_vault &bonding_curve.creator,
} else { creator_vault,
crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator) )
.unwrap_or_default() .unwrap_or_default();
};
Self { Self {
bonding_curve: Arc::new(bonding_curve), bonding_curve: Arc::new(bonding_curve),
associated_bonding_curve: associated_bonding_curve, associated_bonding_curve: associated_bonding_curve,