From c75ca4b034390a457c15234944cbebedaba6ac0d Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:34:44 +0800 Subject: [PATCH] fix(pumpfun): canonical BC PDAs, trust stream fee recipient, harden creator vault - Always derive bonding curve and associated bonding vault from mint (avoids stale cached PDAs / wrong pool). - Use non-default fee_recipient from parser/stream as account #2 without extra authorization filtering (addresses Pump 6000 NotAuthorized when event fee was rejected). - Rework resolve_creator_vault_for_ix: default-creator cases, phantom default PDA, fee-sharing vault; add unit tests. - PumpFunParams: respect explicit mayhem_mode from events; fallback creator_vault via get_creator_vault_pda when resolve returns None. - Remove unused lazy_static tip-cache stub and lazy_static dependency. Made-with: Cursor --- Cargo.toml | 1 - src/instruction/pumpfun.rs | 54 +++++-------- src/instruction/utils/pumpfun.rs | 132 +++++++++++++++++++++++-------- src/swqos/mod.rs | 8 +- src/trading/core/params.rs | 24 ++++-- 5 files changed, 140 insertions(+), 79 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 395d2af..af38b1a 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,7 +89,6 @@ regex = "1" tracing = "0.1.41" thiserror = "2.0.11" async-trait = "0.1.86" -lazy_static = "1.5.0" once_cell = "1.20.3" prost = "0.13" prost-types = "0.13" diff --git a/src/instruction/pumpfun.rs b/src/instruction/pumpfun.rs index 764fdf6..f2e9df6 100755 --- a/src/instruction/pumpfun.rs +++ b/src/instruction/pumpfun.rs @@ -8,7 +8,7 @@ use crate::{ }; use crate::{ instruction::utils::pumpfun::{ - accounts, get_bonding_curve_pda, get_bonding_curve_v2_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}, @@ -77,13 +77,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder { params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), ); - let bonding_curve_addr = if bonding_curve.account == Pubkey::default() { - get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| { - anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint) - })? - } else { - bonding_curve.account - }; + // 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子, + // 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。 + let bonding_curve_addr = get_bonding_curve_pda(¶ms.output_mint).ok_or_else(|| { + anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint) + })?; // Determine token program based on mayhem mode let is_mayhem_mode = bonding_curve.is_mayhem_mode; @@ -95,15 +93,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder { }; let associated_bonding_curve = - if protocol_params.associated_bonding_curve == Pubkey::default() { - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &bonding_curve_addr, - ¶ms.output_mint, - &token_program, - ) - } else { - protocol_params.associated_bonding_curve - }; + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + ¶ms.output_mint, + &token_program, + ); let user_token_account = crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( @@ -157,7 +151,7 @@ impl InstructionBuilder for PumpFunInstructionBuilder { buy_data[24..26].copy_from_slice(&track_volume); } - // Fee recipient: event hint only if allowed for `is_mayhem_mode` (else 6000 NotAuthorized). + // Fee recipient: gRPC/ShredStream 填入的 `PumpFunParams.fee_recipient`(同笔 create_v2+buy 或 trade 日志)优先;热路径无 RPC。 let fee_recipient_meta = pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); @@ -240,13 +234,9 @@ impl InstructionBuilder for PumpFunInstructionBuilder { ), }; - let bonding_curve_addr = if bonding_curve.account == Pubkey::default() { - get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| { - anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) - })? - } else { - bonding_curve.account - }; + let bonding_curve_addr = get_bonding_curve_pda(¶ms.input_mint).ok_or_else(|| { + anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) + })?; // Determine token program based on mayhem mode let is_mayhem_mode = bonding_curve.is_mayhem_mode; @@ -258,15 +248,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder { }; let associated_bonding_curve = - if protocol_params.associated_bonding_curve == Pubkey::default() { - crate::common::fast_fn::get_associated_token_address_with_program_id_fast( - &bonding_curve_addr, - ¶ms.input_mint, - &token_program, - ) - } else { - protocol_params.associated_bonding_curve - }; + crate::common::fast_fn::get_associated_token_address_with_program_id_fast( + &bonding_curve_addr, + ¶ms.input_mint, + &token_program, + ); let user_token_account = crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( diff --git a/src/instruction/utils/pumpfun.rs b/src/instruction/utils/pumpfun.rs index 94f99f4..b324c2b 100644 --- a/src/instruction/utils/pumpfun.rs +++ b/src/instruction/utils/pumpfun.rs @@ -258,26 +258,16 @@ 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). +/// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。 #[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, - }; +pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta { + if from_stream != Pubkey::default() { + AccountMeta { + pubkey: from_stream, + is_signer: false, + is_writable: true, } - } - if is_mayhem_mode { + } else if is_mayhem_mode { get_mayhem_fee_recipient_meta_random() } else { get_standard_fee_recipient_meta_random() @@ -355,29 +345,64 @@ pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option { .map(|(p, _)| p) } -/// Creator vault for buy/sell must be `PDA(["creator-vault", bonding_curve.creator])` (IDL `bonding_curve.creator`). -/// After fee-sharing migration, on-chain `creator` may be [`get_fee_sharing_config_pda`]`(mint)` while gRPC -/// still sends the wallet — accept `from_event` when it matches either `PDA(wallet)` or `PDA(sharing_cfg)`. +/// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when +/// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006. +#[inline] +pub fn phantom_default_creator_vault() -> Pubkey { + solana_sdk::pubkey!("2DR3iqRPVThyRLVJnwjPW1qiGWrp8RUFfHVjMbZyhdNc") +} + +#[inline] +pub fn is_phantom_default_creator_vault(pk: &Pubkey) -> bool { + *pk == phantom_default_creator_vault() +} + +/// Resolve `creator_vault` for Pump buy/sell account #10. +/// +/// - If `creator` is **missing** in the outer trade-event borsh (`Pubkey::default()`) but +/// `creator_vault` was filled from **instruction accounts** (e.g. `fill_trade_accounts` index 9), +/// **trust that vault** — unless it equals [`phantom_default_creator_vault`] (bad derivation / cache). +/// - If event `creator_vault` is **missing** → [`get_creator_vault_pda`]`(creator)` (never `PDA(default)`). +/// - If it **matches** `PDA(creator)` or `PDA(fee_sharing_config(mint))` → use it (fast path, matches ix). +/// - If it **does not match** either (e.g. stale vault but `creator` from tradeEvent is correct) → use +/// [`get_creator_vault_pda`]`(creator)` so seeds match on-chain bonding curve (fixes 2006 Left≠Right). #[inline] pub fn resolve_creator_vault_for_ix( creator: &Pubkey, - from_event: Pubkey, + creator_vault_from_event: Pubkey, mint: &Pubkey, ) -> Option { - let v_creator = get_creator_vault_pda(creator)?; - let sharing = get_fee_sharing_config_pda(mint)?; - let v_sharing = get_creator_vault_pda(&sharing)?; + let phantom = phantom_default_creator_vault(); - if from_event == Pubkey::default() { - return Some(v_creator); + if *creator == Pubkey::default() { + if creator_vault_from_event == Pubkey::default() { + return None; + } + if creator_vault_from_event == phantom { + return None; + } + return Some(creator_vault_from_event); } - if from_event == v_creator || from_event == v_sharing { - return Some(from_event); + + // Real creator: poisoned cache may hold phantom vault — always remap to PDA(creator). + if creator_vault_from_event == phantom { + return get_creator_vault_pda(creator); } - if v_creator == v_sharing { - return Some(v_creator); + + let v_derived = get_creator_vault_pda(creator)?; + if creator_vault_from_event == Pubkey::default() { + return Some(v_derived); } - Some(v_creator) + if creator_vault_from_event == v_derived { + return Some(creator_vault_from_event); + } + if let Some(sharing) = get_fee_sharing_config_pda(mint) { + let v_sharing = get_creator_vault_pda(&sharing)?; + if creator_vault_from_event == v_sharing { + return Some(creator_vault_from_event); + } + } + Some(v_derived) } #[inline] @@ -468,4 +493,47 @@ mod tests { let b = get_fee_sharing_config_pda(&mint).unwrap(); assert_eq!(a, b); } + + #[test] + fn default_creator_yields_fixed_creator_vault() { + let v = get_creator_vault_pda(&Pubkey::default()).unwrap(); + assert_eq!(v, phantom_default_creator_vault(), "phantom vault constant must match PDA(default creator)"); + } + + #[test] + fn resolve_uses_ix_vault_when_creator_borsh_is_default() { + let mint = Pubkey::new_unique(); + let ix_vault = Pubkey::new_unique(); + let resolved = resolve_creator_vault_for_ix(&Pubkey::default(), ix_vault, &mint); + assert_eq!(resolved, Some(ix_vault)); + } + + #[test] + fn resolve_returns_none_when_creator_and_vault_missing() { + let mint = Pubkey::new_unique(); + assert_eq!( + resolve_creator_vault_for_ix(&Pubkey::default(), Pubkey::default(), &mint), + None + ); + } + + #[test] + fn resolve_rejects_phantom_vault_when_creator_borsh_is_default() { + let mint = Pubkey::new_unique(); + assert_eq!( + resolve_creator_vault_for_ix(&Pubkey::default(), phantom_default_creator_vault(), &mint), + None + ); + } + + #[test] + fn resolve_remaps_phantom_vault_when_creator_known() { + let creator = Pubkey::new_unique(); + let mint = Pubkey::new_unique(); + let expected = get_creator_vault_pda(&creator).unwrap(); + assert_eq!( + resolve_creator_vault_for_ix(&creator, phantom_default_creator_vault(), &mint), + Some(expected) + ); + } } diff --git a/src/swqos/mod.rs b/src/swqos/mod.rs index 6b530a1..2ec2d05 100755 --- a/src/swqos/mod.rs +++ b/src/swqos/mod.rs @@ -22,7 +22,6 @@ use std::sync::Arc; use solana_commitment_config::CommitmentConfig; use solana_sdk::transaction::VersionedTransaction; -use tokio::sync::RwLock; use anyhow::Result; @@ -52,11 +51,8 @@ use crate::{ }, }; -lazy_static::lazy_static! { - /// Reserved for future per-SWQOS tip account caching (currently unused). - #[allow(dead_code)] - static ref TIP_ACCOUNT_CACHE: RwLock> = RwLock::new(Vec::new()); -} +// Tip 账户:`SwqosClient::get_tip_account()` 在各实现里多为静态常量;同一批多路提交时, +// 在 `trading::core::async_executor::execute_parallel` 内用局部 `tip_cache`(按 client 指针)去重解析。 /// SWQOS provider blacklist configuration /// Providers added here will be disabled even if configured by user diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index 6fea43c..611bb46 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -127,13 +127,13 @@ impl std::fmt::Debug for SwapParams { pub struct PumpFunParams { pub bonding_curve: Arc, pub associated_bonding_curve: Pubkey, - /// `PDA(["creator-vault", bonding_curve.creator])`; from_trade resolves when event vault mismatches. + /// Resolved by [`resolve_creator_vault_for_ix`](crate::instruction::utils::pumpfun::resolve_creator_vault_for_ix): use ix vault when it matches `PDA(creator)` or fee-sharing vault; else `PDA(creator)`. 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. + /// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。 + /// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。 pub fee_recipient: Pubkey, } @@ -187,6 +187,9 @@ impl PumpFunParams { creator_vault, &mint, ) + .or_else(|| { + crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve_account.creator) + }) .unwrap_or_default(); Self { bonding_curve: Arc::new(bonding_curve_account), @@ -200,7 +203,11 @@ impl PumpFunParams { /// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event /// so that sell instructions include the correct remaining accounts for cashback. - /// `mayhem_mode`: `Some` when known from Create/Trade event. `None` → infer from `fee_recipient` (Mayhem list only). + /// + /// `mayhem_mode`: + /// - **`Some(v)`**(推荐):显式使用链上事件中的值。gRPC 日志解析对应 Explorer 的 `tradeEvent.mayhemMode`; + /// **不会**再用 `fee_recipient` 覆盖。 + /// - **`None`**:无该字段时(例如 ShredStream 仅解外层指令、或冷路径),才用 `fee_recipient` 是否落在 Mayhem 静态列表上推断。 pub fn from_trade( bonding_curve: Pubkey, associated_bonding_curve: Pubkey, @@ -217,8 +224,10 @@ impl PumpFunParams { is_cashback_coin: bool, mayhem_mode: Option, ) -> Self { - let is_mayhem_mode = - mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient)); + let is_mayhem_mode = match mayhem_mode { + Some(v) => v, + None => is_mayhem_fee_recipient(&fee_recipient), + }; let bonding_curve = BondingCurveAccount::from_trade( bonding_curve, mint, @@ -235,6 +244,9 @@ impl PumpFunParams { creator_vault, &mint, ) + .or_else(|| { + crate::instruction::utils::pumpfun::get_creator_vault_pda(&bonding_curve.creator) + }) .unwrap_or_default(); Self { bonding_curve: Arc::new(bonding_curve),