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::{
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(&params.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<AccountMeta> = vec![
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;
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]
pub fn get_user_volume_accumulator_pda(user: &Pubkey) -> Option<Pubkey> {
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 bonding_curve: Arc<BondingCurveAccount>,
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,