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
This commit is contained in:
0xfnzero
2026-04-12 00:34:44 +08:00
parent b13b4fda0a
commit c75ca4b034
5 changed files with 140 additions and 79 deletions
-1
View File
@@ -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"
+20 -34
View File
@@ -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(&params.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(&params.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,
&params.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,
&params.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(&params.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(&params.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,
&params.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,
&params.input_mint,
&token_program,
);
let user_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
+100 -32
View File
@@ -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<Pubkey> {
.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<Pubkey> {
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)
);
}
}
+2 -6
View File
@@ -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<Vec<String>> = 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
+18 -6
View File
@@ -127,13 +127,13 @@ impl std::fmt::Debug for SwapParams {
pub struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>,
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<bool>,
/// 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<bool>,
) -> 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),