fix(pumpswap): harden quotes and low-latency examples

This commit is contained in:
0xfnzero
2026-07-17 03:22:52 +08:00
parent fb1ff176d0
commit dc3d2b8deb
34 changed files with 1133 additions and 972 deletions
+284 -48
View File
@@ -2,7 +2,7 @@ use crate::{
constants::trade::trade::DEFAULT_SLIPPAGE,
instruction::pumpswap_ix_data::{
encode_pumpswap_buy_exact_quote_in_ix_data, encode_pumpswap_buy_ix_data,
encode_pumpswap_buy_two_args, encode_pumpswap_sell_ix_data,
encode_pumpswap_sell_ix_data,
},
instruction::{
token_account_setup::{
@@ -34,6 +34,38 @@ use solana_sdk::{
/// Instruction builder for PumpSwap protocol
pub struct PumpSwapInstructionBuilder;
#[inline]
fn request_mint_matches_pool(actual: Pubkey, expected: Pubkey) -> bool {
actual == expected
|| (expected == crate::constants::WSOL_TOKEN_ACCOUNT
&& actual == crate::constants::SOL_TOKEN_ACCOUNT)
}
fn push_cashback_remaining_accounts(
accounts: &mut Vec<AccountMeta>,
user: &Pubkey,
quote_mint: &Pubkey,
quote_token_program: &Pubkey,
is_cashback_coin: bool,
is_buy_instruction: bool,
) -> Result<()> {
if !is_cashback_coin {
return Ok(());
}
let quote_ata = get_user_volume_accumulator_quote_ata(user, quote_mint, quote_token_program)
.ok_or_else(|| anyhow!("user volume accumulator quote ATA derivation failed"))?;
accounts.push(AccountMeta::new(quote_ata, false));
if !is_buy_instruction {
let accumulator = get_user_volume_accumulator_pda(user)
.ok_or_else(|| anyhow!("user volume accumulator PDA derivation failed"))?;
accounts.push(AccountMeta::new(accumulator, false));
}
Ok(())
}
#[async_trait::async_trait]
impl InstructionBuilder for PumpSwapInstructionBuilder {
async fn build_buy_instructions(&self, params: &SwapParams) -> Result<Vec<Instruction>> {
@@ -49,6 +81,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
if params.input_amount.unwrap_or(0) == 0 {
return Err(anyhow!("Amount cannot be zero"));
}
if params.fixed_output_amount == Some(0) {
return Err(anyhow!("Fixed output amount cannot be zero"));
}
let pool = protocol_params.pool;
let base_mint = protocol_params.base_mint;
@@ -83,15 +118,28 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
// ========================================
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
if params.fixed_output_amount.is_some() && !quote_is_wsol_or_usdc {
return Err(anyhow!(
"PumpSwap exact-output buy is unsupported when the pool requires a sell instruction"
));
}
let input_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
let input_stable_token_program =
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
let output_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
let output_trade_token_program =
if quote_is_wsol_or_usdc { base_token_program } else { quote_token_program };
if !request_mint_matches_pool(params.input_mint, input_stable_mint)
|| !request_mint_matches_pool(params.output_mint, output_trade_mint)
{
return Err(anyhow!("PumpSwap buy request mints do not match the supplied pool"));
}
let fee_basis_points = protocol_params.fee_basis_points;
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
if output_amount >= pool_base_token_reserves {
return Err(anyhow!("Exact base output must be below the pool base reserve"));
}
(output_amount, params.input_amount.unwrap_or(0))
} else if quote_is_wsol_or_usdc {
let result = buy_quote_input_internal_with_fees(
@@ -142,7 +190,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let recipient = get_protocol_fee_recipient_random();
(recipient, AccountMeta::new_readonly(recipient, false))
};
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint, quote_token_program);
// ========================================
// Build instructions
@@ -210,12 +258,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
}
accounts.push(accounts::FEE_CONFIG_META);
accounts.push(accounts::FEE_PROGRAM_META);
// Cashback: remaining_accounts[0] = WSOL ATA of UserVolumeAccumulator (after named accounts per IDL)
if protocol_params.is_cashback_coin {
if let Some(wsol_ata) = get_user_volume_accumulator_wsol_ata(&params.payer.pubkey()) {
accounts.push(AccountMeta::new(wsol_ata, false));
}
}
push_cashback_remaining_accounts(
&mut accounts,
&params.payer.pubkey(),
&quote_mint,
&quote_token_program,
protocol_params.is_cashback_coin,
quote_is_wsol_or_usdc,
)?;
// `pool-v2` only when coin_creator ≠ default (@pump-fun/pump-swap-sdk remainingAccounts)
// 否则多出的一格会把 buyback pubkey 错位,触发 BuybackFeeRecipientNotAuthorized6053)。
if protocol_params.coin_creator != Pubkey::default() {
@@ -228,12 +278,16 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let protocol_extra = get_protocol_extra_fee_recipient_random();
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
accounts.push(AccountMeta::new(
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
crate::instruction::utils::pumpswap::fee_recipient_ata(
protocol_extra,
quote_mint,
quote_token_program,
),
false,
));
// buy / buy_exact_quote_in:栈上 `[u8;25]` + `new_with_bytes`,避免每笔 `Vec` 堆分配。
let track_volume: u8 = if protocol_params.is_cashback_coin { 1 } else { 0 };
let track_volume = 1_u8;
if quote_is_wsol_or_usdc {
let ix_data = if params.fixed_output_amount.is_some() {
encode_pumpswap_buy_ix_data(token_amount, sol_amount, track_volume)
@@ -314,20 +368,34 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
if params.input_amount.unwrap_or_default() == 0 {
return Err(anyhow!("Token amount must be greater than zero"));
}
if params.fixed_output_amount == Some(0) {
return Err(anyhow!("Fixed output amount cannot be zero"));
}
// ========================================
// Trade calculation and account address preparation
// ========================================
let quote_is_wsol_or_usdc = quote_mint == crate::constants::WSOL_TOKEN_ACCOUNT
|| quote_mint == crate::constants::USDC_TOKEN_ACCOUNT;
if params.fixed_output_amount.is_some() && quote_is_wsol_or_usdc {
return Err(anyhow!(
"PumpSwap exact-output sell is unsupported when the pool requires a sell instruction"
));
}
let output_stable_mint = if quote_is_wsol_or_usdc { quote_mint } else { base_mint };
let output_stable_token_program =
if quote_is_wsol_or_usdc { quote_token_program } else { base_token_program };
let input_trade_mint = if quote_is_wsol_or_usdc { base_mint } else { quote_mint };
if !request_mint_matches_pool(params.input_mint, input_trade_mint)
|| !request_mint_matches_pool(params.output_mint, output_stable_mint)
{
return Err(anyhow!("PumpSwap sell request mints do not match the supplied pool"));
}
let fee_basis_points = protocol_params.fee_basis_points;
let (token_amount, sol_amount) = if let Some(output_amount) = params.fixed_output_amount {
if quote_is_wsol_or_usdc && output_amount > pool_quote_token_reserves {
return Err(anyhow!("Minimum quote output exceeds the real quote-vault balance"));
if output_amount >= pool_base_token_reserves {
return Err(anyhow!("Exact base output must be below the pool base reserve"));
}
(params.input_amount.unwrap(), output_amount)
} else if quote_is_wsol_or_usdc {
@@ -364,7 +432,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let recipient = get_protocol_fee_recipient_random();
(recipient, AccountMeta::new_readonly(recipient, false))
};
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint);
let fee_recipient_ata = fee_recipient_ata(fee_recipient, quote_mint, quote_token_program);
let user_base_token_account =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast_use_seed(
@@ -427,20 +495,14 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
}
accounts.push(accounts::FEE_CONFIG_META);
accounts.push(accounts::FEE_PROGRAM_META);
// Cashback sell: 官方 remainingAccounts = [accumulator 的 quote_mint ATA, accumulator PDA, poolV2](用 quote_mint 非固定 WSOL
if protocol_params.is_cashback_coin {
if let (Some(quote_ata), Some(accumulator)) = (
get_user_volume_accumulator_quote_ata(
&params.payer.pubkey(),
&quote_mint,
&quote_token_program,
),
get_user_volume_accumulator_pda(&params.payer.pubkey()),
) {
accounts.push(AccountMeta::new(quote_ata, false));
accounts.push(AccountMeta::new(accumulator, false));
}
}
push_cashback_remaining_accounts(
&mut accounts,
&params.payer.pubkey(),
&quote_mint,
&quote_token_program,
protocol_params.is_cashback_coin,
!quote_is_wsol_or_usdc,
)?;
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)
@@ -450,18 +512,46 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let protocol_extra = get_protocol_extra_fee_recipient_random();
accounts.push(AccountMeta::new_readonly(protocol_extra, false));
accounts.push(AccountMeta::new(
crate::instruction::utils::pumpswap::fee_recipient_ata(protocol_extra, quote_mint),
crate::instruction::utils::pumpswap::fee_recipient_ata(
protocol_extra,
quote_mint,
quote_token_program,
),
false,
));
// 栈数组 + `new_with_bytes`,避免 `data.to_vec()`。
let ix_data = if quote_is_wsol_or_usdc {
encode_pumpswap_sell_ix_data(token_amount, sol_amount)
let track_volume = 1_u8;
if quote_is_wsol_or_usdc {
let ix_data = encode_pumpswap_sell_ix_data(token_amount, sol_amount);
instructions.push(Instruction::new_with_bytes(
accounts::AMM_PROGRAM,
&ix_data,
accounts,
));
} else if params.fixed_output_amount.is_some() {
let ix_data = encode_pumpswap_buy_ix_data(sol_amount, token_amount, track_volume);
instructions.push(Instruction::new_with_bytes(
accounts::AMM_PROGRAM,
&ix_data,
accounts,
));
} else {
encode_pumpswap_buy_two_args(sol_amount, token_amount)
};
instructions.push(Instruction::new_with_bytes(accounts::AMM_PROGRAM, &ix_data, accounts));
let min_base_amount_out = crate::utils::calc::common::calculate_with_slippage_sell(
sol_amount,
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
);
let ix_data = encode_pumpswap_buy_exact_quote_in_ix_data(
params.input_amount.unwrap_or(0),
min_base_amount_out,
track_volume,
);
instructions.push(Instruction::new_with_bytes(
accounts::AMM_PROGRAM,
&ix_data,
accounts,
));
}
if close_wsol_ata {
push_close_wsol_if_needed(
@@ -558,6 +648,27 @@ mod tests {
)
}
fn reverse_pumpswap_params() -> PumpSwapParams {
PumpSwapParams::new(
pk(1),
crate::constants::USDC_TOKEN_ACCOUNT,
pk(2),
pk(3),
pk(4),
1_000_000_000,
2_000_000_000,
0,
pk(5),
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
crate::constants::TOKEN_PROGRAM,
crate::constants::TOKEN_PROGRAM,
accounts::PROTOCOL_FEE_RECIPIENT,
Pubkey::default(),
false,
0,
)
}
fn swap_params(trade_type: TradeType, fixed_output_amount: Option<u64>) -> SwapParams {
let (input_mint, output_mint) = if trade_type == TradeType::Sell {
(pk(2), crate::constants::WSOL_TOKEN_ACCOUNT)
@@ -616,29 +727,154 @@ mod tests {
}
#[tokio::test]
async fn pumpswap_sell_fixed_output_uses_min_quote_directly() {
let instructions = PumpSwapInstructionBuilder
async fn pumpswap_sell_fixed_output_rejects_unsupported_sell_instruction() {
let error = PumpSwapInstructionBuilder
.build_sell_instructions(&swap_params(TradeType::Sell, Some(42)))
.await
.unwrap();
let ix = instructions.last().unwrap();
.unwrap_err();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
assert_eq!(
error.to_string(),
"PumpSwap exact-output sell is unsupported when the pool requires a sell instruction"
);
}
#[tokio::test]
async fn pumpswap_sell_fixed_output_rejects_real_vault_overflow() {
async fn pumpswap_reverse_sell_fixed_output_uses_current_buy_layout() {
let mut params = swap_params(TradeType::Sell, Some(42));
let DexParamEnum::PumpSwap(protocol_params) = &mut params.protocol_params else {
unreachable!();
};
protocol_params.pool_quote_token_reserves = 41;
params.protocol_params = DexParamEnum::PumpSwap(reverse_pumpswap_params());
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
let error = PumpSwapInstructionBuilder.build_sell_instructions(&params).await.unwrap_err();
let instructions =
PumpSwapInstructionBuilder.build_sell_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(error.to_string(), "Minimum quote output exceeds the real quote-vault balance");
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::BUY_DISCRIMINATOR);
assert_eq!(ix.data.len(), 25);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 42);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 100_000);
assert_eq!(ix.data[24], 1);
}
#[tokio::test]
async fn pumpswap_reverse_sell_exact_input_never_increases_token_spend() {
let mut params = swap_params(TradeType::Sell, None);
params.protocol_params = DexParamEnum::PumpSwap(reverse_pumpswap_params());
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
let instructions =
PumpSwapInstructionBuilder.build_sell_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
assert_eq!(
&ix.data[..8],
crate::instruction::utils::pumpswap::BUY_EXACT_QUOTE_IN_DISCRIMINATOR
);
assert_eq!(ix.data.len(), 25);
assert_eq!(u64::from_le_bytes(ix.data[8..16].try_into().unwrap()), 100_000);
let quote = crate::utils::calc::pumpswap::buy_quote_input_internal_with_fees(
100_000,
100,
1_000_000_000,
2_000_000_000,
0,
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(25, 5, 0),
)
.unwrap();
let expected_min =
crate::utils::calc::common::calculate_with_slippage_sell(quote.base, 100);
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), expected_min);
assert_eq!(ix.data[24], 1);
}
#[tokio::test]
async fn pumpswap_buy_cashback_accounts_match_actual_instruction_direction() {
let mut params = swap_params(TradeType::Buy, None);
let mut protocol_params = pumpswap_params();
protocol_params.is_cashback_coin = true;
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
let instructions =
PumpSwapInstructionBuilder.build_buy_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
let expected_ata = get_user_volume_accumulator_quote_ata(
&params.payer.pubkey(),
&crate::constants::WSOL_TOKEN_ACCOUNT,
&crate::constants::TOKEN_PROGRAM,
)
.unwrap();
assert_eq!(
&ix.data[..8],
crate::instruction::utils::pumpswap::BUY_EXACT_QUOTE_IN_DISCRIMINATOR
);
assert_eq!(ix.accounts[23].pubkey, expected_ata);
assert_eq!(ix.data[24], 1);
}
#[tokio::test]
async fn pumpswap_reverse_buy_uses_sell_cashback_account_layout() {
let mut params = swap_params(TradeType::Buy, None);
let mut protocol_params = reverse_pumpswap_params();
protocol_params.is_cashback_coin = true;
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
params.input_mint = crate::constants::USDC_TOKEN_ACCOUNT;
let instructions =
PumpSwapInstructionBuilder.build_buy_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
let expected_ata = get_user_volume_accumulator_quote_ata(
&params.payer.pubkey(),
&pk(2),
&crate::constants::TOKEN_PROGRAM,
)
.unwrap();
let expected_accumulator = get_user_volume_accumulator_pda(&params.payer.pubkey()).unwrap();
assert_eq!(&ix.data[..8], crate::instruction::utils::pumpswap::SELL_DISCRIMINATOR);
assert_eq!(ix.accounts[21].pubkey, expected_ata);
assert_eq!(ix.accounts[22].pubkey, expected_accumulator);
}
#[tokio::test]
async fn pumpswap_fee_atas_use_the_quote_token_program() {
let quote_token_program = pk(99);
let mut params = swap_params(TradeType::Sell, None);
let mut protocol_params = reverse_pumpswap_params();
protocol_params.quote_token_program = quote_token_program;
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
params.output_mint = crate::constants::USDC_TOKEN_ACCOUNT;
let instructions =
PumpSwapInstructionBuilder.build_sell_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
let expected_fee_ata =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&ix.accounts[9].pubkey,
&pk(2),
&quote_token_program,
);
let buyback_recipient_index = ix.accounts.len() - 2;
let expected_buyback_ata =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&ix.accounts[buyback_recipient_index].pubkey,
&pk(2),
&quote_token_program,
);
assert_eq!(ix.accounts[10].pubkey, expected_fee_ata);
assert_eq!(ix.accounts[buyback_recipient_index + 1].pubkey, expected_buyback_ata);
}
#[tokio::test]
async fn pumpswap_rejects_request_mints_from_another_pool() {
let mut params = swap_params(TradeType::Buy, None);
params.output_mint = pk(88);
let error = PumpSwapInstructionBuilder.build_buy_instructions(&params).await.unwrap_err();
assert_eq!(error.to_string(), "PumpSwap buy request mints do not match the supplied pool");
}
#[tokio::test]
-9
View File
@@ -4,15 +4,6 @@ use crate::instruction::utils::pumpswap::{
BUY_DISCRIMINATOR, BUY_EXACT_QUOTE_IN_DISCRIMINATOR, SELL_DISCRIMINATOR,
};
#[inline(always)]
pub fn encode_pumpswap_buy_two_args(base_amount_out: u64, max_quote_amount_in: u64) -> [u8; 24] {
let mut d = [0u8; 24];
d[..8].copy_from_slice(&BUY_DISCRIMINATOR);
d[8..16].copy_from_slice(&base_amount_out.to_le_bytes());
d[16..24].copy_from_slice(&max_quote_amount_in.to_le_bytes());
d
}
#[inline(always)]
pub fn encode_pumpswap_buy_ix_data(
base_amount_out: u64,
+290 -27
View File
@@ -2,8 +2,8 @@ use crate::{
common::{
spl_associated_token_account::get_associated_token_address_with_program_id, SolanaRpcClient,
},
constants::{TOKEN_PROGRAM, WSOL_TOKEN_ACCOUNT},
instruction::utils::pumpswap_types::{pool_decode, Pool},
constants::WSOL_TOKEN_ACCOUNT,
instruction::utils::pumpswap_types::{pool_decode, Pool, POOL_DISCRIMINATOR},
};
use anyhow::anyhow;
use once_cell::sync::Lazy;
@@ -241,6 +241,8 @@ const U8_LEN: usize = 1;
const BOOL_LEN: usize = 1;
const GLOBAL_CONFIG_DISCRIMINATOR_LEN: usize = 8;
const FEE_CONFIG_DISCRIMINATOR_LEN: usize = 8;
const GLOBAL_CONFIG_DISCRIMINATOR: [u8; 8] = [149, 8, 156, 202, 160, 252, 176, 217];
const FEE_CONFIG_DISCRIMINATOR: [u8; 8] = [143, 52, 146, 187, 219, 123, 76, 155];
const FEE_CONFIG_BUMP_LEN: usize = 1;
const FEE_TIER_LEN: usize = 16 + U64_LEN * 3;
@@ -301,6 +303,9 @@ fn read_u32(data: &[u8], offset: usize) -> Option<u32> {
}
fn decode_global_config(data: &[u8]) -> Option<GlobalConfig> {
if data.get(..GLOBAL_CONFIG_DISCRIMINATOR_LEN)? != GLOBAL_CONFIG_DISCRIMINATOR {
return None;
}
let mut offset = GLOBAL_CONFIG_DISCRIMINATOR_LEN;
offset += PUBKEY_LEN; // admin
let lp_fee_basis_points = read_u64(data, offset)?;
@@ -364,6 +369,9 @@ fn decode_fee_tiers(data: &[u8], offset: &mut usize) -> Option<Vec<PumpSwapFeeTi
}
pub fn decode_fee_config(data: &[u8]) -> Option<PumpSwapFeeConfig> {
if data.get(..FEE_CONFIG_DISCRIMINATOR_LEN)? != FEE_CONFIG_DISCRIMINATOR {
return None;
}
let mut offset = FEE_CONFIG_DISCRIMINATOR_LEN;
offset += FEE_CONFIG_BUMP_LEN;
offset += PUBKEY_LEN; // admin
@@ -399,6 +407,14 @@ async fn refresh_global_config_once(rpc: &SolanaRpcClient) -> Option<GlobalConfi
}
};
if account.owner != accounts::AMM_PROGRAM {
warn!(
target: "pumpswap_global_config",
owner = %account.owner,
"PumpSwap GlobalConfig owner 无效"
);
return None;
}
let Some(config) = decode_global_config(&account.data) else {
warn!(
target: "pumpswap_global_config",
@@ -435,6 +451,14 @@ async fn refresh_fee_config_once(rpc: &SolanaRpcClient) -> Option<PumpSwapFeeCon
}
};
if account.owner != accounts::FEE_PROGRAM {
warn!(
target: "pumpswap_fee_config",
owner = %account.owner,
"PumpSwap FeeConfig owner 无效"
);
return None;
}
let Some(config) = decode_fee_config(&account.data) else {
warn!(
target: "pumpswap_fee_config",
@@ -665,22 +689,30 @@ pub(crate) fn coin_creator_vault_authority(coin_creator: Pubkey) -> Pubkey {
pump_pool_authority
}
pub(crate) fn coin_creator_vault_ata(coin_creator: Pubkey, quote_mint: Pubkey) -> Pubkey {
pub(crate) fn coin_creator_vault_ata(
coin_creator: Pubkey,
quote_mint: Pubkey,
quote_token_program: Pubkey,
) -> Pubkey {
let creator_vault_authority = coin_creator_vault_authority(coin_creator);
let associated_token_creator_vault_authority = get_associated_token_address_with_program_id(
&creator_vault_authority,
&quote_mint,
&TOKEN_PROGRAM,
&quote_token_program,
);
associated_token_creator_vault_authority
}
pub(crate) fn fee_recipient_ata(fee_recipient: Pubkey, quote_mint: Pubkey) -> Pubkey {
pub(crate) fn fee_recipient_ata(
fee_recipient: Pubkey,
quote_mint: Pubkey,
quote_token_program: Pubkey,
) -> Pubkey {
let associated_token_fee_recipient =
crate::common::fast_fn::get_associated_token_address_with_program_id_fast(
&fee_recipient,
&quote_mint,
&TOKEN_PROGRAM,
&quote_token_program,
);
associated_token_fee_recipient
}
@@ -733,11 +765,21 @@ pub async fn fetch_pool(
pool_address: &Pubkey,
) -> Result<Pool, anyhow::Error> {
let account = rpc.get_account(pool_address).await?;
decode_pool_account(&account).map_err(anyhow::Error::msg)
}
fn decode_pool_account(account: &solana_sdk::account::Account) -> Result<Pool, String> {
if account.owner != accounts::AMM_PROGRAM {
return Err(anyhow!("Account is not owned by PumpSwap program"));
return Err("Account is not owned by PumpSwap program".to_string());
}
let pool = pool_decode(&account.data[8..]).ok_or_else(|| anyhow!("Failed to decode pool"))?;
Ok(pool)
let discriminator = account
.data
.get(..8)
.ok_or_else(|| "Pool account is shorter than its discriminator".to_string())?;
if discriminator != POOL_DISCRIMINATOR {
return Err("Account discriminator is not PumpSwap Pool".to_string());
}
pool_decode(&account.data[8..]).ok_or_else(|| "Failed to decode pool".to_string())
}
/// Known allocated Pool account sizes. Current accounts may be serialized to
@@ -756,6 +798,9 @@ async fn get_program_accounts_known_sizes(
let make_config = |data_size: u64| solana_rpc_client_api::config::RpcProgramAccountsConfig {
filters: Some(vec![
solana_rpc_client_api::filter::RpcFilterType::DataSize(data_size),
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
solana_client::rpc_filter::Memcmp::new_base58_encoded(0, &POOL_DISCRIMINATOR),
),
solana_rpc_client_api::filter::RpcFilterType::Memcmp(
solana_client::rpc_filter::Memcmp::new_base58_encoded(memcmp_offset, mint.as_ref()),
),
@@ -777,10 +822,22 @@ async fn get_program_accounts_known_sizes(
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_PADDED)),
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)),
);
let mut all = legacy_result.unwrap_or_default();
all.extend(current_result.unwrap_or_default());
all.extend(padded_result.unwrap_or_default());
all.extend(extended_result.unwrap_or_default());
let results = [legacy_result, current_result, padded_result, extended_result];
let mut all = Vec::new();
let mut errors = Vec::new();
for (size, result) in
[POOL_DATA_LEN_LEGACY, POOL_DATA_LEN_CURRENT, POOL_DATA_LEN_PADDED, POOL_DATA_LEN_EXTENDED]
.into_iter()
.zip(results)
{
match result {
Ok(accounts) => all.extend(accounts),
Err(error) => errors.push(format!("dataSize={size}: {error}")),
}
}
if !errors.is_empty() {
return Err(anyhow!("Incomplete PumpSwap pool query: {}", errors.join("; ")));
}
Ok(all)
}
@@ -789,13 +846,7 @@ fn decode_pool_accounts(
) -> Vec<(Pubkey, Pool)> {
accounts
.into_iter()
.filter_map(|(addr, acc)| {
if acc.data.len() > 8 {
pool_decode(&acc.data[8..]).map(|pool| (addr, pool))
} else {
None
}
})
.filter_map(|(addr, acc)| decode_pool_account(&acc).ok().map(|pool| (addr, pool)))
.collect()
}
@@ -885,17 +936,111 @@ pub async fn find_by_mint(
Err(anyhow!("No pool found for mint {}. diag: {}", mint, diag_str))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PoolRpcSnapshot {
pub base_reserve: u64,
pub quote_reserve: u64,
pub base_token_program: Pubkey,
pub quote_token_program: Pubkey,
pub base_mint_supply: u64,
}
const TOKEN_ACCOUNT_MINT_END: usize = 32;
const TOKEN_ACCOUNT_AMOUNT_OFFSET: usize = 64;
const TOKEN_ACCOUNT_AMOUNT_END: usize = 72;
const TOKEN_ACCOUNT_STATE_OFFSET: usize = 108;
const MINT_SUPPLY_OFFSET: usize = 36;
const MINT_SUPPLY_END: usize = 44;
const MINT_INITIALIZED_OFFSET: usize = 45;
fn supported_token_program(program: &Pubkey) -> bool {
*program == crate::constants::TOKEN_PROGRAM || *program == crate::constants::TOKEN_PROGRAM_2022
}
fn decode_token_account_amount(
account: &solana_sdk::account::Account,
expected_mint: &Pubkey,
) -> Result<(u64, Pubkey), anyhow::Error> {
if !supported_token_program(&account.owner) {
return Err(anyhow!("Pool vault is not owned by a supported token program"));
}
let mint = account
.data
.get(..TOKEN_ACCOUNT_MINT_END)
.ok_or_else(|| anyhow!("Pool vault data is too short"))?;
if mint != expected_mint.as_ref() {
return Err(anyhow!("Pool vault mint does not match Pool account"));
}
if account.data.get(TOKEN_ACCOUNT_STATE_OFFSET).copied() != Some(1) {
return Err(anyhow!("Pool vault is not initialized"));
}
let amount = account
.data
.get(TOKEN_ACCOUNT_AMOUNT_OFFSET..TOKEN_ACCOUNT_AMOUNT_END)
.and_then(|bytes| bytes.try_into().ok())
.map(u64::from_le_bytes)
.ok_or_else(|| anyhow!("Pool vault amount is missing"))?;
Ok((amount, account.owner))
}
fn decode_mint_supply(
account: &solana_sdk::account::Account,
expected_token_program: &Pubkey,
) -> Result<u64, anyhow::Error> {
if account.owner != *expected_token_program {
return Err(anyhow!("Base mint and base vault use different token programs"));
}
if account.data.get(MINT_INITIALIZED_OFFSET).copied() != Some(1) {
return Err(anyhow!("Base mint is not initialized"));
}
account
.data
.get(MINT_SUPPLY_OFFSET..MINT_SUPPLY_END)
.and_then(|bytes| bytes.try_into().ok())
.map(u64::from_le_bytes)
.ok_or_else(|| anyhow!("Base mint supply is missing"))
}
pub async fn get_pool_rpc_snapshot(
pool: &Pool,
rpc: &SolanaRpcClient,
) -> Result<PoolRpcSnapshot, anyhow::Error> {
let addresses = [pool.pool_base_token_account, pool.pool_quote_token_account, pool.base_mint];
let accounts = rpc.get_multiple_accounts(&addresses).await?;
let base_vault = accounts
.first()
.and_then(Option::as_ref)
.ok_or_else(|| anyhow!("PumpSwap base vault account was not found"))?;
let quote_vault = accounts
.get(1)
.and_then(Option::as_ref)
.ok_or_else(|| anyhow!("PumpSwap quote vault account was not found"))?;
let base_mint = accounts
.get(2)
.and_then(Option::as_ref)
.ok_or_else(|| anyhow!("PumpSwap base mint account was not found"))?;
let (base_reserve, base_token_program) =
decode_token_account_amount(base_vault, &pool.base_mint)?;
let (quote_reserve, quote_token_program) =
decode_token_account_amount(quote_vault, &pool.quote_mint)?;
let base_mint_supply = decode_mint_supply(base_mint, &base_token_program)?;
Ok(PoolRpcSnapshot {
base_reserve,
quote_reserve,
base_token_program,
quote_token_program,
base_mint_supply,
})
}
pub async fn get_token_balances(
pool: &Pool,
rpc: &SolanaRpcClient,
) -> Result<(u64, u64), anyhow::Error> {
let base_balance = rpc.get_token_account_balance(&pool.pool_base_token_account).await?;
let quote_balance = rpc.get_token_account_balance(&pool.pool_quote_token_account).await?;
let snapshot = get_pool_rpc_snapshot(pool, rpc).await?;
let base_amount = base_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
let quote_amount = quote_balance.amount.parse::<u64>().map_err(|e| anyhow!(e))?;
Ok((base_amount, quote_amount))
Ok((snapshot.base_reserve, snapshot.quote_reserve))
}
#[inline]
@@ -909,7 +1054,40 @@ pub fn get_fee_config_pda() -> Option<Pubkey> {
#[cfg(test)]
mod tests {
use super::*;
use solana_sdk::pubkey::Pubkey;
use crate::instruction::utils::pumpswap_types;
use solana_sdk::{account::Account, pubkey::Pubkey};
fn pool_account(virtual_quote_reserves: i128) -> Account {
let mut data = Vec::with_capacity(8 + pumpswap_types::POOL_SIZE);
data.extend_from_slice(&POOL_DISCRIMINATOR);
data.push(7);
data.extend_from_slice(&42u16.to_le_bytes());
for seed in 1..=6 {
data.extend_from_slice(Pubkey::new_from_array([seed; 32]).as_ref());
}
data.extend_from_slice(&123_456u64.to_le_bytes());
data.extend_from_slice(Pubkey::new_from_array([7; 32]).as_ref());
data.push(1);
data.push(0);
data.extend_from_slice(&virtual_quote_reserves.to_le_bytes());
Account { data, owner: accounts::AMM_PROGRAM, ..Account::default() }
}
fn token_account(mint: Pubkey, owner: Pubkey, amount: u64) -> Account {
let mut data = vec![0; 165];
data[..32].copy_from_slice(mint.as_ref());
data[TOKEN_ACCOUNT_AMOUNT_OFFSET..TOKEN_ACCOUNT_AMOUNT_END]
.copy_from_slice(&amount.to_le_bytes());
data[TOKEN_ACCOUNT_STATE_OFFSET] = 1;
Account { data, owner, ..Account::default() }
}
fn mint_account(owner: Pubkey, supply: u64) -> Account {
let mut data = vec![0; 82];
data[MINT_SUPPLY_OFFSET..MINT_SUPPLY_END].copy_from_slice(&supply.to_le_bytes());
data[MINT_INITIALIZED_OFFSET] = 1;
Account { data, owner, ..Account::default() }
}
fn fee_config_fixture() -> PumpSwapFeeConfig {
PumpSwapFeeConfig {
@@ -936,6 +1114,25 @@ mod tests {
}
}
#[test]
fn config_decoders_require_official_account_discriminators() {
let global_len = 8 + 32 + 8 + 8 + 1 + 32 * 8 + 8 + 32 + 32 + 32 + 1 + 32 * 7 + 1 + 32 * 8;
let mut global_data = vec![0; global_len];
global_data[..8].copy_from_slice(&GLOBAL_CONFIG_DISCRIMINATOR);
assert!(decode_global_config(&global_data).is_some());
global_data[0] ^= 0xff;
assert!(decode_global_config(&global_data).is_none());
let mut fee_data = Vec::with_capacity(8 + 1 + 32 + 24 + 4 + 4);
fee_data.extend_from_slice(&FEE_CONFIG_DISCRIMINATOR);
fee_data.extend_from_slice(&[0; 1 + 32 + 24]);
fee_data.extend_from_slice(&0_u32.to_le_bytes());
fee_data.extend_from_slice(&0_u32.to_le_bytes());
assert!(decode_fee_config(&fee_data).is_some());
fee_data[0] ^= 0xff;
assert!(decode_fee_config(&fee_data).is_none());
}
#[test]
fn pumpswap_user_volume_accumulator_pda_deterministic() {
let user = Pubkey::new_unique();
@@ -1001,4 +1198,70 @@ mod tests {
assert_eq!(POOL_DATA_LEN_PADDED, 300);
assert_eq!(POOL_DATA_LEN_EXTENDED, 643);
}
#[test]
fn pool_account_validation_checks_owner_length_and_discriminator() {
let account = pool_account(-123_456);
assert_eq!(decode_pool_account(&account).unwrap().virtual_quote_reserves, -123_456);
let mut wrong_owner = account.clone();
wrong_owner.owner = Pubkey::new_unique();
assert_eq!(
decode_pool_account(&wrong_owner).unwrap_err(),
"Account is not owned by PumpSwap program"
);
let mut short = account.clone();
short.data.truncate(7);
assert_eq!(
decode_pool_account(&short).unwrap_err(),
"Pool account is shorter than its discriminator"
);
let mut wrong_discriminator = account;
wrong_discriminator.data[0] ^= 0xff;
assert_eq!(
decode_pool_account(&wrong_discriminator).unwrap_err(),
"Account discriminator is not PumpSwap Pool"
);
}
#[test]
fn pool_snapshot_decoders_validate_token_ownership_and_layout() {
let mint = Pubkey::new_unique();
let token_program = crate::constants::TOKEN_PROGRAM_2022;
let vault = token_account(mint, token_program, 987_654_321);
assert_eq!(
decode_token_account_amount(&vault, &mint).unwrap(),
(987_654_321, token_program)
);
assert_eq!(
decode_mint_supply(&mint_account(token_program, 42), &token_program).unwrap(),
42
);
let wrong_mint = Pubkey::new_unique();
assert_eq!(
decode_token_account_amount(&vault, &wrong_mint).unwrap_err().to_string(),
"Pool vault mint does not match Pool account"
);
let unsupported = token_account(mint, Pubkey::new_unique(), 1);
assert_eq!(
decode_token_account_amount(&unsupported, &mint).unwrap_err().to_string(),
"Pool vault is not owned by a supported token program"
);
}
#[test]
fn coin_creator_vault_ata_uses_quote_token_program() {
let creator = Pubkey::new_unique();
let mint = Pubkey::new_unique();
let token_program = crate::constants::TOKEN_PROGRAM_2022;
let authority = coin_creator_vault_authority(creator);
let expected =
get_associated_token_address_with_program_id(&authority, &mint, &token_program);
assert_eq!(coin_creator_vault_ata(creator, mint, token_program), expected);
}
}
+18 -4
View File
@@ -2,6 +2,8 @@ use borsh::BorshDeserialize;
use serde::{Deserialize, Serialize};
use solana_sdk::pubkey::Pubkey;
pub const POOL_DISCRIMINATOR: [u8; 8] = [241, 154, 109, 4, 17, 177, 109, 188];
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, BorshDeserialize)]
pub struct Pool {
pub pool_bump: u8,
@@ -26,7 +28,9 @@ pub struct Pool {
/// Minimum Borsh payload length for the current Pool layout, excluding the
/// 8-byte Anchor account discriminator.
pub const POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1 + 16;
const LEGACY_POOL_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
const LEGACY_POOL_FIELDS_SIZE: usize = 1 + 2 + 32 * 6 + 8 + 32 + 1 + 1;
/// Legacy Pool accounts were allocated with seven trailing padding bytes.
pub const LEGACY_POOL_SIZE: usize = LEGACY_POOL_FIELDS_SIZE + 7;
#[derive(BorshDeserialize)]
struct LegacyPool {
@@ -69,8 +73,10 @@ pub fn pool_decode(data: &[u8]) -> Option<Pool> {
return borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok();
}
if data.len() >= LEGACY_POOL_SIZE {
return borsh::from_slice::<LegacyPool>(&data[..LEGACY_POOL_SIZE]).ok().map(Into::into);
if data.len() == LEGACY_POOL_SIZE {
return borsh::from_slice::<LegacyPool>(&data[..LEGACY_POOL_FIELDS_SIZE])
.ok()
.map(Into::into);
}
None
@@ -121,13 +127,21 @@ mod tests {
#[test]
fn decodes_legacy_pool_with_zero_virtual_quote_reserves() {
let mut data = pool_payload(0);
data.truncate(LEGACY_POOL_SIZE);
data.truncate(LEGACY_POOL_FIELDS_SIZE);
data.extend_from_slice(&[0; 7]);
let pool = pool_decode(&data).unwrap();
assert_eq!(pool.virtual_quote_reserves, 0);
}
#[test]
fn rejects_partial_current_pool_layout() {
let current = pool_payload(987_654_321);
for len in (LEGACY_POOL_SIZE + 1)..POOL_SIZE {
assert!(pool_decode(&current[..len]).is_none(), "accepted body length {len}");
}
}
#[test]
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
+11 -49
View File
@@ -1,13 +1,9 @@
use crate::common::spl_associated_token_account::get_associated_token_address_with_program_id;
use crate::common::SolanaRpcClient;
use crate::instruction::utils::pumpswap::{
accounts::MAYHEM_FEE_RECIPIENT as MAYHEM_FEE_RECIPIENT_SWAP, PumpSwapFeeBasisPoints,
};
use solana_sdk::pubkey::Pubkey;
const SPL_MINT_SUPPLY_OFFSET: usize = 36;
const SPL_MINT_SUPPLY_LEN: usize = 8;
/// PumpSwap Protocol Specific Parameters
///
/// Parameters for configuring PumpSwap trading protocol, including liquidity pool information,
@@ -267,17 +263,9 @@ impl PumpSwapParams {
rpc: &SolanaRpcClient,
mint: &Pubkey,
) -> Result<Self, anyhow::Error> {
if let Ok((pool_address, _)) =
crate::instruction::utils::pumpswap::find_by_base_mint(rpc, mint).await
{
Self::from_pool_address_by_rpc(rpc, &pool_address).await
} else if let Ok((pool_address, _)) =
crate::instruction::utils::pumpswap::find_by_quote_mint(rpc, mint).await
{
Self::from_pool_address_by_rpc(rpc, &pool_address).await
} else {
return Err(anyhow::anyhow!("No pool found for mint"));
}
let (pool_address, pool) =
crate::instruction::utils::pumpswap::find_by_mint(rpc, mint).await?;
Self::from_pool_data(rpc, &pool_address, &pool).await
}
pub async fn from_pool_address_by_rpc(
@@ -298,8 +286,10 @@ impl PumpSwapParams {
pool_address: &Pubkey,
pool_data: &crate::instruction::utils::pumpswap_types::Pool,
) -> Result<Self, anyhow::Error> {
let (pool_base_token_reserves, pool_quote_token_reserves) =
crate::instruction::utils::pumpswap::get_token_balances(pool_data, rpc).await?;
let snapshot =
crate::instruction::utils::pumpswap::get_pool_rpc_snapshot(pool_data, rpc).await?;
let pool_base_token_reserves = snapshot.base_reserve;
let pool_quote_token_reserves = snapshot.quote_reserve;
let effective_quote_token_reserves =
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
pool_quote_token_reserves,
@@ -312,7 +302,7 @@ impl PumpSwapParams {
pool_data.virtual_quote_reserves
)
})?;
let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok();
let base_mint_supply = Some(snapshot.base_mint_supply);
let fee_config = crate::instruction::utils::pumpswap::fetch_fee_config(rpc).await;
let raw_fee_basis_points = crate::instruction::utils::pumpswap::compute_fee_basis_points(
fee_config.as_ref(),
@@ -331,21 +321,11 @@ impl PumpSwapParams {
let coin_creator_vault_ata = crate::instruction::utils::pumpswap::coin_creator_vault_ata(
creator,
pool_data.quote_mint,
snapshot.quote_token_program,
);
let coin_creator_vault_authority =
crate::instruction::utils::pumpswap::coin_creator_vault_authority(creator);
let base_token_program_ata = get_associated_token_address_with_program_id(
pool_address,
&pool_data.base_mint,
&crate::constants::TOKEN_PROGRAM,
);
let quote_token_program_ata = get_associated_token_address_with_program_id(
pool_address,
&pool_data.quote_mint,
&crate::constants::TOKEN_PROGRAM,
);
Ok(Self {
pool: *pool_address,
base_mint: pool_data.base_mint,
@@ -357,17 +337,9 @@ impl PumpSwapParams {
virtual_quote_reserves: pool_data.virtual_quote_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program: if pool_data.pool_base_token_account == base_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
crate::constants::TOKEN_PROGRAM_2022
},
base_token_program: snapshot.base_token_program,
is_cashback_coin: pool_data.is_cashback_coin,
quote_token_program: if pool_data.pool_quote_token_account == quote_token_program_ata {
crate::constants::TOKEN_PROGRAM
} else {
crate::constants::TOKEN_PROGRAM_2022
},
quote_token_program: snapshot.quote_token_program,
is_mayhem_mode: pool_data.is_mayhem_mode,
pool_creator: pool_data.creator,
coin_creator: pool_data.coin_creator,
@@ -381,13 +353,3 @@ impl PumpSwapParams {
})
}
}
fn decode_mint_supply(data: &[u8]) -> Option<u64> {
let bytes = data.get(SPL_MINT_SUPPLY_OFFSET..SPL_MINT_SUPPLY_OFFSET + SPL_MINT_SUPPLY_LEN)?;
Some(u64::from_le_bytes(bytes.try_into().ok()?))
}
async fn fetch_mint_supply(rpc: &SolanaRpcClient, mint: &Pubkey) -> Result<u64, anyhow::Error> {
let account = rpc.get_account(mint).await?;
decode_mint_supply(&account.data).ok_or_else(|| anyhow::anyhow!("Failed to decode mint supply"))
}
+53 -6
View File
@@ -11,7 +11,18 @@
/// * fee_basis_points = 100 -> 1% fee
#[inline(always)]
pub const fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 {
ceil_div(amount * fee_basis_points, 10_000)
let whole = match (amount / 10_000).checked_mul(fee_basis_points) {
Some(value) => value,
None => return u128::MAX,
};
let remainder_product = match (amount % 10_000).checked_mul(fee_basis_points) {
Some(value) => value,
None => return u128::MAX,
};
match whole.checked_add(ceil_div(remainder_product, 10_000)) {
Some(value) => value,
None => u128::MAX,
}
}
/// Ceiling division implementation
@@ -25,7 +36,12 @@ pub const fn compute_fee(amount: u128, fee_basis_points: u128) -> u128 {
/// Returns the ceiling result of a/b
#[inline(always)]
pub const fn ceil_div(a: u128, b: u128) -> u128 {
(a + b - 1) / b
let quotient = a / b;
if a % b == 0 {
quotient
} else {
quotient + 1
}
}
/// Maximum slippage in basis points (99.99% = 9999 bps)
@@ -55,7 +71,12 @@ pub const fn calculate_with_slippage_buy(amount: u64, basis_points: u64) -> u64
} else {
basis_points
};
amount + (amount * bps / 10000)
let result = amount as u128 + (amount as u128 * bps as u128 / 10_000);
if result > u64::MAX as u128 {
u64::MAX
} else {
result as u64
}
}
/// Calculate sell amount with slippage protection
@@ -75,9 +96,35 @@ pub const fn calculate_with_slippage_sell(amount: u64, basis_points: u64) -> u64
if amount == 0 {
return 0;
}
if amount <= basis_points / 10000 {
1
let bps = if basis_points > MAX_SLIPPAGE_BASIS_POINTS {
MAX_SLIPPAGE_BASIS_POINTS
} else {
amount - (amount * basis_points / 10000)
basis_points
};
amount - (amount as u128 * bps as u128 / 10_000) as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ceil_div_handles_u128_max_without_addition_overflow() {
assert_eq!(ceil_div(u128::MAX, u128::MAX), 1);
assert_eq!(ceil_div(u128::MAX, 2), u128::MAX / 2 + 1);
}
#[test]
fn compute_fee_handles_large_amounts_without_multiplication_overflow() {
assert_eq!(compute_fee(u128::MAX, 1), ceil_div(u128::MAX, 10_000));
assert_eq!(compute_fee(u128::MAX, 10_000), u128::MAX);
}
#[test]
fn slippage_helpers_are_deterministic_at_numeric_boundaries() {
assert_eq!(calculate_with_slippage_buy(u64::MAX, 100), u64::MAX);
assert_eq!(calculate_with_slippage_sell(u64::MAX, 100), 18_262_276_632_972_456_099);
assert_eq!(calculate_with_slippage_sell(10_000, u64::MAX), 1);
assert_eq!(calculate_with_slippage_sell(1, u64::MAX), 1);
}
}
+4 -2
View File
@@ -89,7 +89,9 @@ pub fn get_sell_sol_amount_from_token_amount(
let amount_128 = amount as u128;
// Calculate SOL amount received from selling tokens using constant product formula
let numerator = amount_128.checked_mul(virtual_sol_reserves).unwrap_or(0);
let Some(numerator) = amount_128.checked_mul(virtual_sol_reserves) else {
return u64::MAX;
};
let denominator = virtual_token_reserves.checked_add(amount_128).unwrap_or(1);
let sol_cost = numerator.checked_div(denominator).unwrap_or(0);
@@ -101,5 +103,5 @@ pub fn get_sell_sol_amount_from_token_amount(
// Calculate transaction fee
let fee = compute_fee(sol_cost, total_fee_basis_points_128);
sol_cost.saturating_sub(fee) as u64
sol_cost.saturating_sub(fee).min(u64::MAX as u128) as u64
}
+193 -42
View File
@@ -30,10 +30,22 @@ fn effective_quote_reserve(
pub(crate) fn creator_side_fee_basis_points(
coin_creator: &Pubkey,
cashback_fee_basis_points: u64,
) -> u64 {
) -> Result<u64, String> {
let creator_bps =
if *coin_creator == Pubkey::default() { 0 } else { COIN_CREATOR_FEE_BASIS_POINTS };
creator_bps.saturating_add(cashback_fee_basis_points)
creator_bps
.checked_add(cashback_fee_basis_points)
.ok_or_else(|| "Coin creator fee basis points overflow.".to_string())
}
#[inline]
fn checked_u64(value: u128, name: &str) -> Result<u64, String> {
u64::try_from(value).map_err(|_| format!("Calculated {name} exceeds u64."))
}
#[inline]
fn checked_fee(amount: u64, basis_points: u64, name: &str) -> Result<u64, String> {
checked_u64(compute_fee(amount as u128, basis_points as u128), name)
}
/// Result for buying base tokens with base amount input
@@ -111,7 +123,7 @@ pub fn buy_base_input_internal(
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
),
)
}
@@ -140,19 +152,23 @@ pub fn buy_base_input_internal_with_fees(
return Err("Pool would be depleted; denominator is zero.".to_string());
}
let quote_amount_in = ceil_div(numerator, denominator as u128) as u64;
let quote_amount_in =
checked_u64(ceil_div(numerator, denominator as u128), "raw quote amount")?;
// Calculate fees
let lp_fee =
compute_fee(quote_amount_in as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
let lp_fee = checked_fee(quote_amount_in, fee_basis_points.lp_fee_basis_points, "LP fee")?;
let protocol_fee =
compute_fee(quote_amount_in as u128, fee_basis_points.protocol_fee_basis_points as u128)
as u64;
let coin_creator_fee = compute_fee(
quote_amount_in as u128,
fee_basis_points.coin_creator_fee_basis_points as u128,
) as u64;
let total_quote = quote_amount_in + lp_fee + protocol_fee + coin_creator_fee;
checked_fee(quote_amount_in, fee_basis_points.protocol_fee_basis_points, "protocol fee")?;
let coin_creator_fee = checked_fee(
quote_amount_in,
fee_basis_points.coin_creator_fee_basis_points,
"coin creator fee",
)?;
let total_quote = quote_amount_in
.checked_add(lp_fee)
.and_then(|amount| amount.checked_add(protocol_fee))
.and_then(|amount| amount.checked_add(coin_creator_fee))
.ok_or_else(|| "Total quote amount exceeds u64.".to_string())?;
// Calculate max quote with slippage
let max_quote = calculate_with_slippage_buy(total_quote, slippage_basis_points);
@@ -195,7 +211,7 @@ pub fn buy_quote_input_internal(
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
),
)
}
@@ -216,9 +232,12 @@ pub fn buy_quote_input_internal_with_fees(
// Calculate total fee basis points
let total_fee_bps = fee_basis_points
.lp_fee_basis_points
.saturating_add(fee_basis_points.protocol_fee_basis_points)
.saturating_add(fee_basis_points.coin_creator_fee_basis_points);
let denominator = 10_000 + total_fee_bps;
.checked_add(fee_basis_points.protocol_fee_basis_points)
.and_then(|fees| fees.checked_add(fee_basis_points.coin_creator_fee_basis_points))
.ok_or_else(|| "Fee basis points overflow.".to_string())?;
let denominator = 10_000_u64
.checked_add(total_fee_bps)
.ok_or_else(|| "Fee denominator overflow.".to_string())?;
// Calculate effective quote amount after fees
let mut effective_quote = (quote as u128 * 10_000) / denominator as u128;
@@ -227,11 +246,19 @@ pub fn buy_quote_input_internal_with_fees(
compute_fee(effective_quote, fee_basis_points.protocol_fee_basis_points as u128);
let coin_creator_fee =
compute_fee(effective_quote, fee_basis_points.coin_creator_fee_basis_points as u128);
let total_with_fees = effective_quote + lp_fee + protocol_fee + coin_creator_fee;
let total_with_fees = effective_quote
.checked_add(lp_fee)
.and_then(|amount| amount.checked_add(protocol_fee))
.and_then(|amount| amount.checked_add(coin_creator_fee))
.ok_or_else(|| "Total quote amount exceeds u128.".to_string())?;
if total_with_fees > quote as u128 {
effective_quote = effective_quote.saturating_sub(total_with_fees - quote as u128);
effective_quote = effective_quote
.checked_sub(total_with_fees - quote as u128)
.ok_or_else(|| "Quote input is too small to cover fees.".to_string())?;
}
let input_amount = effective_quote.saturating_sub(1);
let input_amount = effective_quote
.checked_sub(1)
.ok_or_else(|| "Quote input is too small after fees.".to_string())?;
// Calculate base amount out using constant product formula
let numerator = (base_reserve as u128) * input_amount;
@@ -241,14 +268,14 @@ pub fn buy_quote_input_internal_with_fees(
return Err("Pool would be depleted; denominator is zero.".to_string());
}
let base_amount_out = (numerator / denominator_effective) as u64;
let base_amount_out = checked_u64(numerator / denominator_effective, "base amount")?;
// Calculate max quote with slippage
let max_quote = calculate_with_slippage_buy(quote, slippage_basis_points);
Ok(BuyQuoteInputResult {
base: base_amount_out,
internal_quote_without_fees: effective_quote as u64,
internal_quote_without_fees: checked_u64(effective_quote, "effective quote amount")?,
max_quote,
})
}
@@ -284,7 +311,7 @@ pub fn sell_base_input_internal(
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
),
)
}
@@ -303,22 +330,27 @@ pub fn sell_base_input_internal_with_fees(
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
// Calculate quote amount out using constant product formula
let quote_amount_out = ((effective_quote_reserve as u128) * (base as u128)
/ ((base_reserve as u128) + (base as u128))) as u64;
let quote_amount_out = checked_u64(
(effective_quote_reserve as u128) * (base as u128)
/ ((base_reserve as u128) + (base as u128)),
"raw quote amount",
)?;
// Calculate fees
let lp_fee =
compute_fee(quote_amount_out as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
let lp_fee = checked_fee(quote_amount_out, fee_basis_points.lp_fee_basis_points, "LP fee")?;
let protocol_fee =
compute_fee(quote_amount_out as u128, fee_basis_points.protocol_fee_basis_points as u128)
as u64;
let coin_creator_fee = compute_fee(
quote_amount_out as u128,
fee_basis_points.coin_creator_fee_basis_points as u128,
) as u64;
checked_fee(quote_amount_out, fee_basis_points.protocol_fee_basis_points, "protocol fee")?;
let coin_creator_fee = checked_fee(
quote_amount_out,
fee_basis_points.coin_creator_fee_basis_points,
"coin creator fee",
)?;
// Calculate final quote after fees
let total_fees = lp_fee + protocol_fee + coin_creator_fee;
let total_fees = lp_fee
.checked_add(protocol_fee)
.and_then(|fees| fees.checked_add(coin_creator_fee))
.ok_or_else(|| "Total fees exceed u64.".to_string())?;
if total_fees > quote_amount_out {
return Err("Fees exceed total output; final quote is negative.".to_string());
}
@@ -395,7 +427,7 @@ pub fn sell_quote_input_internal(
&PumpSwapFeeBasisPoints::new(
LP_FEE_BASIS_POINTS,
PROTOCOL_FEE_BASIS_POINTS,
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points),
creator_side_fee_basis_points(coin_creator, cashback_fee_basis_points)?,
),
)
}
@@ -424,9 +456,11 @@ pub fn sell_quote_input_internal_with_fees(
fee_basis_points.coin_creator_fee_basis_points,
)?;
let lp_fee =
compute_fee(raw_quote as u128, fee_basis_points.lp_fee_basis_points as u128) as u64;
if raw_quote.saturating_sub(lp_fee) > quote_reserve {
let lp_fee = checked_fee(raw_quote, fee_basis_points.lp_fee_basis_points, "LP fee")?;
let quote_vault_outflow = raw_quote
.checked_sub(lp_fee)
.ok_or_else(|| "LP fee exceeds raw quote output.".to_string())?;
if quote_vault_outflow > quote_reserve {
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
}
@@ -435,10 +469,13 @@ pub fn sell_quote_input_internal_with_fees(
return Err("Invalid input: Desired quote amount exceeds available reserve.".to_string());
}
let base_amount_in = ceil_div(
(base_reserve as u128) * (raw_quote as u128),
(effective_quote_reserve - raw_quote) as u128,
) as u64;
let base_amount_in = checked_u64(
ceil_div(
(base_reserve as u128) * (raw_quote as u128),
(effective_quote_reserve - raw_quote) as u128,
),
"base amount",
)?;
// Calculate min quote with slippage
let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points);
@@ -531,4 +568,118 @@ mod tests {
assert_eq!(error, "Invalid effective quote reserves: raw=1000000, virtual=-1000000.");
}
#[test]
fn quote_modes_match_official_integer_formulas() {
let fees = PumpSwapFeeBasisPoints::new(20, 5, 30);
let base_reserve = 800_000_000_000_000;
let quote_reserve = 100_000_000_000;
let virtual_quote_reserves = 5_000_000_000;
let slippage_basis_points = 125;
let buy_base = buy_base_input_internal_with_fees(
123_456_789_000,
slippage_basis_points,
base_reserve,
quote_reserve,
virtual_quote_reserves,
&fees,
)
.unwrap();
assert_eq!(buy_base.internal_quote_amount, 16_206_205);
assert_eq!(buy_base.ui_quote, 16_295_341);
assert_eq!(buy_base.max_quote, 16_499_032);
let buy_quote = buy_quote_input_internal_with_fees(
1_500_000_000,
slippage_basis_points,
base_reserve,
quote_reserve,
virtual_quote_reserves,
&fees,
)
.unwrap();
assert_eq!(buy_quote.internal_quote_without_fees, 1_491_795_125);
assert_eq!(buy_quote.base, 11_206_836_149_304);
assert_eq!(buy_quote.max_quote, 1_518_750_000);
let sell_base = sell_base_input_internal_with_fees(
123_456_789_000,
slippage_basis_points,
base_reserve,
quote_reserve,
virtual_quote_reserves,
&fees,
)
.unwrap();
assert_eq!(sell_base.internal_quote_amount_out, 16_201_203);
assert_eq!(sell_base.ui_quote, 16_112_095);
assert_eq!(sell_base.min_quote, 15_910_694);
let sell_quote = sell_quote_input_internal_with_fees(
500_000_000,
slippage_basis_points,
base_reserve,
quote_reserve,
virtual_quote_reserves,
&fees,
)
.unwrap();
assert_eq!(sell_quote.internal_raw_quote, 502_765_209);
assert_eq!(sell_quote.base, 3_849_022_110_532);
assert_eq!(sell_quote.min_quote, 493_750_000);
}
#[test]
fn oversized_quote_results_return_errors_instead_of_truncating() {
let no_fees = PumpSwapFeeBasisPoints::new(0, 0, 0);
let buy_error =
buy_base_input_internal_with_fees(u64::MAX - 1, 0, u64::MAX, u64::MAX, 0, &no_fees)
.unwrap_err();
assert_eq!(buy_error, "Calculated raw quote amount exceeds u64.");
let sell_error =
sell_quote_input_internal_with_fees(u64::MAX - 1, 0, u64::MAX, u64::MAX, 0, &no_fees)
.unwrap_err();
assert_eq!(sell_error, "Calculated base amount exceeds u64.");
}
#[test]
fn invalid_fee_boundaries_return_errors() {
let overflowing_fees = PumpSwapFeeBasisPoints::new(u64::MAX, 1, 0);
let error = buy_quote_input_internal_with_fees(
10_000,
0,
1_000_000,
1_000_000,
0,
&overflowing_fees,
)
.unwrap_err();
assert_eq!(error, "Fee basis points overflow.");
let oversized_fee = PumpSwapFeeBasisPoints::new(u64::MAX, 0, 0);
let error = sell_base_input_internal_with_fees(
1_000_000,
0,
1_000_000,
1_000_000,
0,
&oversized_fee,
)
.unwrap_err();
assert_eq!(error, "Calculated LP fee exceeds u64.");
let error = creator_side_fee_basis_points(&Pubkey::new_unique(), u64::MAX).unwrap_err();
assert_eq!(error, "Coin creator fee basis points overflow.");
}
#[test]
fn buy_quote_rejects_amount_too_small_after_fees() {
let error =
buy_quote_input_internal_with_fees(1, 0, 1_000_000, 1_000_000, 0, &fees()).unwrap_err();
assert_eq!(error, "Quote input is too small after fees.");
}
}