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" tracing = "0.1.41"
thiserror = "2.0.11" thiserror = "2.0.11"
async-trait = "0.1.86" async-trait = "0.1.86"
lazy_static = "1.5.0"
once_cell = "1.20.3" once_cell = "1.20.3"
prost = "0.13" prost = "0.13"
prost-types = "0.13" prost-types = "0.13"
+20 -34
View File
@@ -8,7 +8,7 @@ use crate::{
}; };
use crate::{ use crate::{
instruction::utils::pumpfun::{ 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, get_user_volume_accumulator_pda, pump_fun_fee_recipient_meta,
resolve_creator_vault_for_ix, resolve_creator_vault_for_ix,
global_constants::{self}, global_constants::{self},
@@ -77,13 +77,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE), params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
); );
let bonding_curve_addr = if bonding_curve.account == Pubkey::default() { // 始终用 mint 推导 canonical bonding curve PDA。缓存里的 `bonding_curve.account` 可能指向其它池子,
get_bonding_curve_pda(&params.output_mint).ok_or_else(|| { // 会导致链上读到错误 `creator`,从而 creator_vault seeds 与传入的 vault 不一致(Anchor 2006)。
anyhow!("bonding_curve PDA derivation failed for mint {}", params.output_mint) 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)
} else { })?;
bonding_curve.account
};
// Determine token program based on mayhem mode // Determine token program based on mayhem mode
let is_mayhem_mode = bonding_curve.is_mayhem_mode; let is_mayhem_mode = bonding_curve.is_mayhem_mode;
@@ -95,15 +93,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
}; };
let associated_bonding_curve = let associated_bonding_curve =
if protocol_params.associated_bonding_curve == Pubkey::default() { crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
crate::common::fast_fn::get_associated_token_address_with_program_id_fast( &bonding_curve_addr,
&bonding_curve_addr, &params.output_mint,
&params.output_mint, &token_program,
&token_program, );
)
} else {
protocol_params.associated_bonding_curve
};
let user_token_account = let user_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( 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); 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 = let fee_recipient_meta =
pump_fun_fee_recipient_meta(protocol_params.fee_recipient, is_mayhem_mode); 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() { let bonding_curve_addr = get_bonding_curve_pda(&params.input_mint).ok_or_else(|| {
get_bonding_curve_pda(&params.input_mint).ok_or_else(|| { anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint)
anyhow!("bonding_curve PDA derivation failed for mint {}", params.input_mint) })?;
})?
} else {
bonding_curve.account
};
// Determine token program based on mayhem mode // Determine token program based on mayhem mode
let is_mayhem_mode = bonding_curve.is_mayhem_mode; let is_mayhem_mode = bonding_curve.is_mayhem_mode;
@@ -258,15 +248,11 @@ impl InstructionBuilder for PumpFunInstructionBuilder {
}; };
let associated_bonding_curve = let associated_bonding_curve =
if protocol_params.associated_bonding_curve == Pubkey::default() { crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
crate::common::fast_fn::get_associated_token_address_with_program_id_fast( &bonding_curve_addr,
&bonding_curve_addr, &params.input_mint,
&params.input_mint, &token_program,
&token_program, );
)
} else {
protocol_params.associated_bonding_curve
};
let user_token_account = let user_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed( 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 /// 账户 #2 fee recipient:优先使用 gRPC/ShredStream 解析值(同笔 create_v2+buy 的 `observed_fee_recipient` 或 `tradeEvent.feeRecipient`);未提供时按 mayhem 从静态池随机。
/// 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] #[inline]
pub fn pump_fun_fee_recipient_meta(from_event: Pubkey, is_mayhem_mode: bool) -> AccountMeta { pub fn pump_fun_fee_recipient_meta(from_stream: Pubkey, is_mayhem_mode: bool) -> AccountMeta {
if from_event != Pubkey::default() { if from_stream != Pubkey::default() {
let authorized = if is_mayhem_mode { AccountMeta {
is_mayhem_fee_recipient(&from_event) pubkey: from_stream,
} else { is_signer: false,
from_event == global_constants::FEE_RECIPIENT || is_amm_fee_recipient(&from_event) is_writable: true,
};
if authorized {
return AccountMeta {
pubkey: from_event,
is_signer: false,
is_writable: true,
};
} }
} } else if is_mayhem_mode {
if is_mayhem_mode {
get_mayhem_fee_recipient_meta_random() get_mayhem_fee_recipient_meta_random()
} else { } else {
get_standard_fee_recipient_meta_random() get_standard_fee_recipient_meta_random()
@@ -355,29 +345,64 @@ pub fn get_fee_sharing_config_pda(mint: &Pubkey) -> Option<Pubkey> {
.map(|(p, _)| p) .map(|(p, _)| p)
} }
/// Creator vault for buy/sell must be `PDA(["creator-vault", bonding_curve.creator])` (IDL `bonding_curve.creator`). /// PDA of `["creator-vault", Pubkey::default()]`. Never use as a real vault — it is only produced when
/// After fee-sharing migration, on-chain `creator` may be [`get_fee_sharing_config_pda`]`(mint)` while gRPC /// `creator` was missing and code incorrectly derived a vault; on-chain this fails with Anchor 2006.
/// still sends the wallet — accept `from_event` when it matches either `PDA(wallet)` or `PDA(sharing_cfg)`. #[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] #[inline]
pub fn resolve_creator_vault_for_ix( pub fn resolve_creator_vault_for_ix(
creator: &Pubkey, creator: &Pubkey,
from_event: Pubkey, creator_vault_from_event: Pubkey,
mint: &Pubkey, mint: &Pubkey,
) -> Option<Pubkey> { ) -> Option<Pubkey> {
let v_creator = get_creator_vault_pda(creator)?; let phantom = phantom_default_creator_vault();
let sharing = get_fee_sharing_config_pda(mint)?;
let v_sharing = get_creator_vault_pda(&sharing)?;
if from_event == Pubkey::default() { if *creator == Pubkey::default() {
return Some(v_creator); 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] #[inline]
@@ -468,4 +493,47 @@ mod tests {
let b = get_fee_sharing_config_pda(&mint).unwrap(); let b = get_fee_sharing_config_pda(&mint).unwrap();
assert_eq!(a, b); 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_commitment_config::CommitmentConfig;
use solana_sdk::transaction::VersionedTransaction; use solana_sdk::transaction::VersionedTransaction;
use tokio::sync::RwLock;
use anyhow::Result; use anyhow::Result;
@@ -52,11 +51,8 @@ use crate::{
}, },
}; };
lazy_static::lazy_static! { // Tip 账户:`SwqosClient::get_tip_account()` 在各实现里多为静态常量;同一批多路提交时,
/// Reserved for future per-SWQOS tip account caching (currently unused). // 在 `trading::core::async_executor::execute_parallel` 内用局部 `tip_cache`(按 client 指针)去重解析。
#[allow(dead_code)]
static ref TIP_ACCOUNT_CACHE: RwLock<Vec<String>> = RwLock::new(Vec::new());
}
/// SWQOS provider blacklist configuration /// SWQOS provider blacklist configuration
/// Providers added here will be disabled even if configured by user /// 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 struct PumpFunParams {
pub bonding_curve: Arc<BondingCurveAccount>, pub bonding_curve: Arc<BondingCurveAccount>,
pub associated_bonding_curve: Pubkey, 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 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
pub close_token_account_when_sell: Option<bool>, 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. /// Fee recipient for buy/sell account #2. Set from sol-parser-sdk (`tradeEvent.feeRecipient` / 同笔 create_v2+buy 回填的 `observed_fee_recipient`);热路径不查 RPC。
/// `Pubkey::default()` uses the same random pools as `getFeeRecipient` / `getStaticRandomFeeRecipient` in the npm SDK. /// `Pubkey::default()` 时按 mayhem 从静态池随机(与 npm 静态池一致,可能落后于主网 Global)。
pub fee_recipient: Pubkey, pub fee_recipient: Pubkey,
} }
@@ -187,6 +187,9 @@ impl PumpFunParams {
creator_vault, creator_vault,
&mint, &mint,
) )
.or_else(|| {
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),
@@ -200,7 +203,11 @@ impl PumpFunParams {
/// When building from event/parser (e.g. sol-parser-sdk), pass `is_cashback_coin` from the event /// 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. /// 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( pub fn from_trade(
bonding_curve: Pubkey, bonding_curve: Pubkey,
associated_bonding_curve: Pubkey, associated_bonding_curve: Pubkey,
@@ -217,8 +224,10 @@ impl PumpFunParams {
is_cashback_coin: bool, is_cashback_coin: bool,
mayhem_mode: Option<bool>, mayhem_mode: Option<bool>,
) -> Self { ) -> Self {
let is_mayhem_mode = let is_mayhem_mode = match mayhem_mode {
mayhem_mode.unwrap_or_else(|| is_mayhem_fee_recipient(&fee_recipient)); Some(v) => v,
None => is_mayhem_fee_recipient(&fee_recipient),
};
let bonding_curve = BondingCurveAccount::from_trade( let bonding_curve = BondingCurveAccount::from_trade(
bonding_curve, bonding_curve,
mint, mint,
@@ -235,6 +244,9 @@ impl PumpFunParams {
creator_vault, creator_vault,
&mint, &mint,
) )
.or_else(|| {
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),