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 <cursoragent@cursor.com>
This commit is contained in:
0xfnzero
2026-05-03 14:50:26 +08:00
co-authored by Cursor
parent e8ec9103ab
commit 02d939b3cf
5 changed files with 78 additions and 22 deletions
+4
View File
@@ -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
+21 -9
View File
@@ -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 错位,触发 BuybackFeeRecipientNotAuthorized6053)。
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(
+3 -1
View File
@@ -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"),
+19
View File
@@ -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,
})
}
}
+31 -12
View File
@@ -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<BuyBaseInputResult, String> {
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<BuyQuoteInputResult, String> {
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<SellBaseInputResult, String> {
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<SellQuoteInputResult, String> {
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