From 02d939b3cfe389047fb759f88824544122211fed Mon Sep 17 00:00:00 2001 From: 0xfnzero <0xfnzero@users.noreply.github.com> Date: Sun, 3 May 2026 14:50:26 +0800 Subject: [PATCH] fix(pumpswap): align remaining accounts with official SDK for buyback - Append pool-v2 in buy/sell remaining accounts only when pool coin_creator is not default, matching @pump-fun/pump-swap-sdk. Always appending pool-v2 shifted buyback pubkey/ATA and caused BuybackFeeRecipientNotAuthorized (6053). - Add coin_creator to PumpSwapParams (filled from decoded Pool / from_trade). Update pumpswap_trading example to pass pool_data.coin_creator. - Document buyback fee recipient constants against GlobalConfig.buyback_fee_recipients. - Extend PumpSwap quote math to include cashback_fee_basis_points in creator-side fees. Co-authored-by: Cursor --- examples/pumpswap_trading/src/main.rs | 4 +++ src/instruction/pumpswap.rs | 30 +++++++++++++------ src/instruction/utils/pumpswap.rs | 4 ++- src/trading/core/params.rs | 19 ++++++++++++ src/utils/calc/pumpswap.rs | 43 +++++++++++++++++++-------- 5 files changed, 78 insertions(+), 22 deletions(-) diff --git a/examples/pumpswap_trading/src/main.rs b/examples/pumpswap_trading/src/main.rs index d3e78bf..cfbad3e 100644 --- a/examples/pumpswap_trading/src/main.rs +++ b/examples/pumpswap_trading/src/main.rs @@ -162,7 +162,9 @@ async fn pumpswap_trade_with_grpc_buy_event(trade_info: PumpSwapBuyEvent) -> Any trade_info.base_token_program, trade_info.quote_token_program, trade_info.protocol_fee_recipient, + pool_data.coin_creator, pool_data.is_cashback_coin, + trade_info.cashback_fee_basis_points, ); let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT @@ -191,7 +193,9 @@ async fn pumpswap_trade_with_grpc_sell_event(trade_info: PumpSwapSellEvent) -> A trade_info.base_token_program, trade_info.quote_token_program, trade_info.protocol_fee_recipient, + pool_data.coin_creator, pool_data.is_cashback_coin, + trade_info.cashback_fee_basis_points, ); let mint = if trade_info.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || trade_info.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT diff --git a/src/instruction/pumpswap.rs b/src/instruction/pumpswap.rs index f12f15b..d3e691f 100755 --- a/src/instruction/pumpswap.rs +++ b/src/instruction/pumpswap.rs @@ -76,6 +76,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY { creator = params_coin_creator_vault_authority; } + let cashback_fee_bps = protocol_params.cashback_fee_basis_points; let (mut token_amount, sol_amount) = if quote_is_wsol_or_usdc { let result = buy_quote_input_internal( @@ -84,6 +85,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { pool_base_token_reserves, pool_quote_token_reserves, &creator, + cashback_fee_bps, ) .unwrap(); // base_amount_out, max_quote_amount_in @@ -95,6 +97,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { pool_base_token_reserves, pool_quote_token_reserves, &creator, + cashback_fee_bps, ) .unwrap(); // min_quote_amount_out, base_amount_in @@ -202,11 +205,15 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { accounts.push(AccountMeta::new(wsol_ata, false)); } } - // remainingAccounts: @pump-fun/pump-swap-sdk 要求末尾传 poolV2Pda(baseMint),勿删 - let pool_v2 = get_pool_v2_pda(&base_mint) - .ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?; - accounts.push(AccountMeta::new_readonly(pool_v2, false)); - // Apr 2026: protocol fee recipient + quote ATA (after pool-v2) + // `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts); + // 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized(6053)。 + if protocol_params.coin_creator != Pubkey::default() { + let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| { + anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint) + })?; + accounts.push(AccountMeta::new_readonly(pool_v2, false)); + } + // Trailing accounts: GlobalConfig.buyback_fee_recipients 中任 pubkey + quote ATA(与 pump-swap-sdk 静态池对齐;轮换时需查链上)。 let protocol_extra = get_protocol_extra_fee_recipient_random(); accounts.push(AccountMeta::new_readonly(protocol_extra, false)); accounts.push(AccountMeta::new( @@ -299,6 +306,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { if params_coin_creator_vault_authority != accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY { creator = params_coin_creator_vault_authority; } + let cashback_fee_bps = protocol_params.cashback_fee_basis_points; let (token_amount, mut sol_amount) = if quote_is_wsol_or_usdc { let result = sell_base_input_internal( @@ -307,6 +315,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { pool_base_token_reserves, pool_quote_token_reserves, &creator, + cashback_fee_bps, ) .unwrap(); // base_amount_in, min_quote_amount_out @@ -318,6 +327,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { pool_base_token_reserves, pool_quote_token_reserves, &creator, + cashback_fee_bps, ) .unwrap(); // max_quote_amount_in, base_amount_out @@ -410,10 +420,12 @@ impl InstructionBuilder for PumpSwapInstructionBuilder { accounts.push(AccountMeta::new(accumulator, false)); } } - // remainingAccounts: @pump-fun/pump-swap-sdk sell 要求末尾传 poolV2Pda(baseMint),勿删 - let pool_v2 = get_pool_v2_pda(&base_mint) - .ok_or_else(|| anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint))?; - accounts.push(AccountMeta::new_readonly(pool_v2, false)); + if protocol_params.coin_creator != Pubkey::default() { + let pool_v2 = get_pool_v2_pda(&base_mint).ok_or_else(|| { + anyhow!("pool_v2 PDA derivation failed for base_mint {}", base_mint) + })?; + accounts.push(AccountMeta::new_readonly(pool_v2, false)); + } let protocol_extra = get_protocol_extra_fee_recipient_random(); accounts.push(AccountMeta::new_readonly(protocol_extra, false)); accounts.push(AccountMeta::new( diff --git a/src/instruction/utils/pumpswap.rs b/src/instruction/utils/pumpswap.rs index c426b39..6ec9eb0 100644 --- a/src/instruction/utils/pumpswap.rs +++ b/src/instruction/utils/pumpswap.rs @@ -91,7 +91,9 @@ pub mod accounts { /// Default Mayhem fee recipient (first of MAYHEM_FEE_RECIPIENTS) pub const MAYHEM_FEE_RECIPIENT: Pubkey = MAYHEM_FEE_RECIPIENTS[0]; - /// Protocol extra fee recipients (Apr 2026 breaking upgrade). After `pool-v2`: recipient (readonly), then quote ATA (writable). + /// Buyback trailing fee recipients (`GlobalConfig.buyback_fee_recipients` on Pump AMM). + /// Must match one of these for the pubkey passed after optional `pool-v2` (`@pump-fun/pump-swap-sdk` `getBuybackFeeRecipient`). + /// Static mirror of pump-public-docs; if protocol rotates configs, decode global_config from RPC. pub const PROTOCOL_EXTRA_FEE_RECIPIENTS: [Pubkey; 8] = [ pubkey!("5YxQFdt3Tr9zJLvkFccqXVUwhdTWJQc1fFg2YPbxvxeD"), pubkey!("9M4giFFMxmFGXtc3feFzRai56WbBqehoSeRE5GK7gf7"), diff --git a/src/trading/core/params.rs b/src/trading/core/params.rs index 348faae..167d154 100755 --- a/src/trading/core/params.rs +++ b/src/trading/core/params.rs @@ -338,8 +338,17 @@ pub struct PumpSwapParams { pub quote_token_program: Pubkey, /// Whether the pool is in mayhem mode pub is_mayhem_mode: bool, + /// Pool [`Pool::coin_creator`](crate::instruction::utils::pumpswap_types::Pool). Used for PumpSwap + /// `remaining_accounts`: **`pool-v2` is appended only when this is not `Pubkey::default()` + /// (matches `@pump-fun/pump-swap-sdk`); wrong flag causes buys to revert with buyback recipient errors (e.g. 6053). + pub coin_creator: Pubkey, /// Whether the pool's coin has cashback enabled pub is_cashback_coin: bool, + /// Cashback fee in basis points (from trade events / sol-parser-sdk). For quote-in buy and base-in sell + /// math, this is summed with [`COIN_CREATOR_FEE_BASIS_POINTS`](crate::instruction::utils::pumpswap::accounts::COIN_CREATOR_FEE_BASIS_POINTS) + /// when a creator vault applies — matching on-chain treating creator + cashback as one fee bucket. + /// Use `0` when unknown (e.g. RPC-only pool decode has no per-mint cashback bps). + pub cashback_fee_basis_points: u64, } impl PumpSwapParams { @@ -356,7 +365,9 @@ impl PumpSwapParams { base_token_program: Pubkey, quote_token_program: Pubkey, fee_recipient: Pubkey, + coin_creator: Pubkey, is_cashback_coin: bool, + cashback_fee_basis_points: u64, ) -> Self { let is_mayhem_mode = fee_recipient == MAYHEM_FEE_RECIPIENT_SWAP; Self { @@ -372,7 +383,9 @@ impl PumpSwapParams { base_token_program, quote_token_program, is_mayhem_mode, + coin_creator, is_cashback_coin, + cashback_fee_basis_points, } } @@ -398,7 +411,9 @@ impl PumpSwapParams { base_token_program: Pubkey, quote_token_program: Pubkey, fee_recipient: Pubkey, + coin_creator: Pubkey, is_cashback_coin: bool, + cashback_fee_basis_points: u64, ) -> Self { Self::new( pool, @@ -413,7 +428,9 @@ impl PumpSwapParams { base_token_program, quote_token_program, fee_recipient, + coin_creator, is_cashback_coin, + cashback_fee_basis_points, ) } @@ -495,6 +512,8 @@ impl PumpSwapParams { crate::constants::TOKEN_PROGRAM_2022 }, is_mayhem_mode: pool_data.is_mayhem_mode, + coin_creator: pool_data.coin_creator, + cashback_fee_basis_points: 0, }) } } diff --git a/src/utils/calc/pumpswap.rs b/src/utils/calc/pumpswap.rs index ed6dc99..f0c99a5 100644 --- a/src/utils/calc/pumpswap.rs +++ b/src/utils/calc/pumpswap.rs @@ -6,6 +6,21 @@ use crate::instruction::utils::pumpswap::accounts::{ }; use solana_sdk::pubkey::Pubkey; +/// Creator-side fee bps: fixed coin-creator fee when a creator vault applies, plus optional +/// cashback fee bps for cashback-enabled coins (see Pump AMM / parser event field). +#[inline] +pub(crate) fn creator_side_fee_basis_points( + coin_creator: &Pubkey, + cashback_fee_basis_points: u64, +) -> u64 { + let creator_bps = if *coin_creator == Pubkey::default() { + 0 + } else { + COIN_CREATOR_FEE_BASIS_POINTS + }; + creator_bps.saturating_add(cashback_fee_basis_points) +} + /// Result for buying base tokens with base amount input #[derive(Clone, Debug)] pub struct BuyBaseInputResult { @@ -58,6 +73,7 @@ pub struct SellQuoteInputResult { /// * `base_reserve` - Base token reserves in the pool /// * `quote_reserve` - Quote token reserves in the pool /// * `coin_creator` - Token creator address +/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown /// /// # Returns /// * `BuyBaseInputResult` containing quote amounts and slippage calculations @@ -67,6 +83,7 @@ pub fn buy_base_input_internal( base_reserve: u64, quote_reserve: u64, coin_creator: &Pubkey, + cashback_fee_basis_points: u64, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -89,11 +106,9 @@ pub fn buy_base_input_internal( let lp_fee = compute_fee(quote_amount_in as u128, LP_FEE_BASIS_POINTS as u128) as u64; let protocol_fee = compute_fee(quote_amount_in as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64; - let coin_creator_fee = if *coin_creator == Pubkey::default() { - 0 - } else { - compute_fee(quote_amount_in as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64 - }; + let creator_bps = + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128; + let coin_creator_fee = compute_fee(quote_amount_in as u128, creator_bps) as u64; let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee; // Calculate max quote with slippage @@ -114,6 +129,7 @@ pub fn buy_base_input_internal( /// * `base_reserve` - Base token reserves in the pool /// * `quote_reserve` - Quote token reserves in the pool /// * `coin_creator` - Token creator address +/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown /// /// # Returns /// * `BuyQuoteInputResult` containing base amount and slippage calculations @@ -123,6 +139,7 @@ pub fn buy_quote_input_internal( base_reserve: u64, quote_reserve: u64, coin_creator: &Pubkey, + cashback_fee_basis_points: u64, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -131,7 +148,7 @@ pub fn buy_quote_input_internal( // Calculate total fee basis points let total_fee_bps = LP_FEE_BASIS_POINTS + PROTOCOL_FEE_BASIS_POINTS - + if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS }; + + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points); let denominator = 10_000 + total_fee_bps; // Calculate effective quote amount after fees @@ -165,6 +182,7 @@ pub fn buy_quote_input_internal( /// * `base_reserve` - Base token reserves in the pool /// * `quote_reserve` - Quote token reserves in the pool /// * `coin_creator` - Token creator address +/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown /// /// # Returns /// * `SellBaseInputResult` containing quote amounts and slippage calculations @@ -174,6 +192,7 @@ pub fn sell_base_input_internal( base_reserve: u64, quote_reserve: u64, coin_creator: &Pubkey, + cashback_fee_basis_points: u64, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -187,11 +206,9 @@ pub fn sell_base_input_internal( let lp_fee = compute_fee(quote_amount_out as u128, LP_FEE_BASIS_POINTS as u128) as u64; let protocol_fee = compute_fee(quote_amount_out as u128, PROTOCOL_FEE_BASIS_POINTS as u128) as u64; - let coin_creator_fee = if *coin_creator == Pubkey::default() { - 0 - } else { - compute_fee(quote_amount_out as u128, COIN_CREATOR_FEE_BASIS_POINTS as u128) as u64 - }; + let creator_bps = + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points) as u128; + let coin_creator_fee = compute_fee(quote_amount_out as u128, creator_bps) as u64; // Calculate final quote after fees let total_fees = lp_fee + protocol_fee + coin_creator_fee; @@ -234,6 +251,7 @@ fn calculate_quote_amount_out( /// * `base_reserve` - Base token reserves in the pool /// * `quote_reserve` - Quote token reserves in the pool /// * `coin_creator` - Token creator address +/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown /// /// # Returns /// * `SellQuoteInputResult` containing base amount and slippage calculations @@ -243,6 +261,7 @@ pub fn sell_quote_input_internal( base_reserve: u64, quote_reserve: u64, coin_creator: &Pubkey, + cashback_fee_basis_points: u64, ) -> Result { if base_reserve == 0 || quote_reserve == 0 { return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string()); @@ -256,7 +275,7 @@ pub fn sell_quote_input_internal( quote, LP_FEE_BASIS_POINTS, PROTOCOL_FEE_BASIS_POINTS, - if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS }, + creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points), ); // Calculate base amount needed using inverse constant product formula