feat(pumpswap): support virtual quote reserves

This commit is contained in:
0xfnzero
2026-07-16 20:58:56 +08:00
parent 8bef655abb
commit dd41dd4f87
11 changed files with 976 additions and 36 deletions
+31 -5
View File
@@ -54,7 +54,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let base_mint = protocol_params.base_mint;
let quote_mint = protocol_params.quote_mint;
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?;
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
let params_coin_creator_vault_authority = protocol_params.coin_creator_vault_authority;
let create_input_ata = params.create_input_mint_ata;
@@ -283,7 +283,7 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
let base_mint = protocol_params.base_mint;
let quote_mint = protocol_params.quote_mint;
let pool_base_token_reserves = protocol_params.pool_base_token_reserves;
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
let pool_quote_token_reserves = protocol_params.effective_quote_reserves()?;
let pool_base_token_account = protocol_params.pool_base_token_account;
let pool_quote_token_account = protocol_params.pool_quote_token_account;
let params_coin_creator_vault_ata = protocol_params.coin_creator_vault_ata;
@@ -535,6 +535,7 @@ mod tests {
pk(4),
1_000_000_000,
2_000_000_000,
0,
pk(5),
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
crate::constants::TOKEN_PROGRAM,
@@ -627,6 +628,7 @@ mod tests {
pk(4),
1_000_000_000,
2_000_000_000,
0,
pk(5),
accounts::DEFAULT_COIN_CREATOR_VAULT_AUTHORITY,
crate::constants::TOKEN_PROGRAM,
@@ -653,8 +655,9 @@ mod tests {
let mut params = swap_params(TradeType::Buy, None);
params.input_amount = Some(1_000_000);
params.use_exact_sol_amount = Some(false);
params.protocol_params =
DexParamEnum::PumpSwap(pumpswap_params().with_fee_basis_points(20, 5, 75));
let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 75);
protocol_params.virtual_quote_reserves = 500_000_000;
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
let instructions =
PumpSwapInstructionBuilder.build_buy_instructions(&params).await.unwrap();
@@ -667,10 +670,33 @@ mod tests {
1_000_000,
100,
1_000_000_000,
2_000_000_000,
2_500_000_000,
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
)
.unwrap();
assert_eq!(base_amount_out, expected.base);
}
#[tokio::test]
async fn pumpswap_sell_prices_with_effective_quote_reserves() {
let mut params = swap_params(TradeType::Sell, None);
let mut protocol_params = pumpswap_params().with_fee_basis_points(20, 5, 0);
protocol_params.virtual_quote_reserves = 500_000_000;
params.protocol_params = DexParamEnum::PumpSwap(protocol_params);
let instructions =
PumpSwapInstructionBuilder.build_sell_instructions(&params).await.unwrap();
let ix = instructions.last().unwrap();
let min_quote_amount_out = u64::from_le_bytes(ix.data[16..24].try_into().unwrap());
let expected = crate::utils::calc::pumpswap::sell_base_input_internal_with_fees(
100_000,
100,
1_000_000_000,
2_500_000_000,
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
)
.unwrap();
assert_eq!(min_quote_amount_out, expected.min_quote);
}
}
+17 -13
View File
@@ -18,7 +18,7 @@ use std::sync::{
use std::time::{Duration, Instant};
use tracing::warn;
// Pool account sizes moved to find_by_base_mint/find_by_quote_mint (POOL_DATA_LEN_SPL, POOL_DATA_LEN_T22)
// Pool account sizes are handled by find_by_base_mint/find_by_quote_mint.
/// Constants used as seeds for deriving PDAs (Program Derived Addresses)
pub mod seeds {
@@ -740,12 +740,14 @@ pub async fn fetch_pool(
Ok(pool)
}
/// Known pool account sizes: 252 (SPL Token) and 643 (Token2022)
const POOL_DATA_LEN_SPL: u64 = 8 + 244;
const POOL_DATA_LEN_T22: u64 = 643;
/// Known allocated Pool account sizes. The July 2026 layout carrying
/// `virtual_quote_reserves` is allocated to 300 bytes on-chain.
const POOL_DATA_LEN_LEGACY: u64 = 8 + 244;
const POOL_DATA_LEN_CURRENT: u64 = 300;
const POOL_DATA_LEN_EXTENDED: u64 = 643;
/// Run getProgramAccounts with a Memcmp filter, querying both pool sizes in parallel.
async fn get_program_accounts_both_sizes(
/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel.
async fn get_program_accounts_known_sizes(
rpc: &SolanaRpcClient,
memcmp_offset: usize,
mint: &Pubkey,
@@ -768,12 +770,14 @@ async fn get_program_accounts_both_sizes(
};
let program_id = accounts::AMM_PROGRAM;
#[allow(deprecated)]
let (spl_result, t22_result) = tokio::join!(
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_SPL)),
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_T22)),
let (legacy_result, current_result, extended_result) = tokio::join!(
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_LEGACY)),
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_CURRENT)),
rpc.get_program_accounts_with_config(&program_id, make_config(POOL_DATA_LEN_EXTENDED)),
);
let mut all = spl_result.unwrap_or_default();
all.extend(t22_result.unwrap_or_default());
let mut all = legacy_result.unwrap_or_default();
all.extend(current_result.unwrap_or_default());
all.extend(extended_result.unwrap_or_default());
Ok(all)
}
@@ -797,7 +801,7 @@ pub async fn find_by_base_mint(
base_mint: &Pubkey,
) -> Result<(Pubkey, Pool), anyhow::Error> {
// base_mint offset: 8(discriminator) + 1(bump) + 2(index) + 32(creator) = 43
let accounts = get_program_accounts_both_sizes(rpc, 43, base_mint).await?;
let accounts = get_program_accounts_known_sizes(rpc, 43, base_mint).await?;
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", base_mint));
}
@@ -814,7 +818,7 @@ pub async fn find_by_quote_mint(
quote_mint: &Pubkey,
) -> Result<(Pubkey, Pool), anyhow::Error> {
// quote_mint offset: 8 + 1 + 2 + 32 + 32 = 75
let accounts = get_program_accounts_both_sizes(rpc, 75, quote_mint).await?;
let accounts = get_program_accounts_known_sizes(rpc, 75, quote_mint).await?;
if accounts.is_empty() {
return Err(anyhow!("No pool found for mint {}", quote_mint));
}
+112 -7
View File
@@ -17,16 +17,121 @@ pub struct Pool {
pub is_mayhem_mode: bool,
/// Whether this pool's coin has cashback enabled
pub is_cashback_coin: bool,
/// Reserved for future fields (pump-public-docs: pool structure = 244 bytes total)
pub _reserved: [u8; 7],
/// Virtual quote reserves appended to the Pool account.
///
/// Quotes must use `quote_vault_balance + virtual_quote_reserves`.
pub virtual_quote_reserves: i128,
}
/// Borsh 解码用的 Pool 长度。链上池为 244 字节(pump-public-docs Breaking Change),与 POOL_SIZE 一致。
pub const POOL_SIZE: usize = 244;
/// 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;
#[derive(BorshDeserialize)]
struct LegacyPool {
pool_bump: u8,
index: u16,
creator: Pubkey,
base_mint: Pubkey,
quote_mint: Pubkey,
lp_mint: Pubkey,
pool_base_token_account: Pubkey,
pool_quote_token_account: Pubkey,
lp_supply: u64,
coin_creator: Pubkey,
is_mayhem_mode: bool,
is_cashback_coin: bool,
}
impl From<LegacyPool> for Pool {
fn from(pool: LegacyPool) -> Self {
Self {
pool_bump: pool.pool_bump,
index: pool.index,
creator: pool.creator,
base_mint: pool.base_mint,
quote_mint: pool.quote_mint,
lp_mint: pool.lp_mint,
pool_base_token_account: pool.pool_base_token_account,
pool_quote_token_account: pool.pool_quote_token_account,
lp_supply: pool.lp_supply,
coin_creator: pool.coin_creator,
is_mayhem_mode: pool.is_mayhem_mode,
is_cashback_coin: pool.is_cashback_coin,
virtual_quote_reserves: 0,
}
}
}
pub fn pool_decode(data: &[u8]) -> Option<Pool> {
if data.len() < POOL_SIZE {
return None;
if data.len() >= POOL_SIZE {
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);
}
None
}
/// Compute the quote reserves used by PumpSwap pricing.
///
/// Returns `None` when the signed sum is negative or cannot fit in a `u64`.
#[inline]
pub fn effective_quote_reserves(
quote_vault_balance: u64,
virtual_quote_reserves: i128,
) -> Option<u64> {
i128::from(quote_vault_balance)
.checked_add(virtual_quote_reserves)
.and_then(|reserves| u64::try_from(reserves).ok())
}
#[cfg(test)]
mod tests {
use super::*;
fn pool_payload(virtual_quote_reserves: i128) -> Vec<u8> {
let mut data = Vec::with_capacity(POOL_SIZE);
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());
data
}
#[test]
fn decodes_current_pool_virtual_quote_reserves() {
let pool = pool_decode(&pool_payload(987_654_321)).unwrap();
assert_eq!(pool.virtual_quote_reserves, 987_654_321);
assert!(pool.is_mayhem_mode);
assert!(!pool.is_cashback_coin);
}
#[test]
fn decodes_legacy_pool_with_zero_virtual_quote_reserves() {
let mut data = pool_payload(0);
data.truncate(LEGACY_POOL_SIZE);
data.extend_from_slice(&[0; 7]);
let pool = pool_decode(&data).unwrap();
assert_eq!(pool.virtual_quote_reserves, 0);
}
#[test]
fn effective_reserves_support_signed_virtual_amounts_and_reject_invalid_sums() {
assert_eq!(effective_quote_reserves(1_000, 250), Some(1_250));
assert_eq!(effective_quote_reserves(1_000, -250), Some(750));
assert_eq!(effective_quote_reserves(100, -101), None);
assert_eq!(effective_quote_reserves(u64::MAX, 1), None);
}
borsh::from_slice::<Pool>(&data[..POOL_SIZE]).ok()
}
+38 -2
View File
@@ -32,8 +32,10 @@ pub struct PumpSwapParams {
pub pool_quote_token_account: Pubkey,
/// Base token reserves in the pool
pub pool_base_token_reserves: u64,
/// Quote token reserves in the pool
/// Raw quote-vault token balance. Pricing uses this plus [`Self::virtual_quote_reserves`].
pub pool_quote_token_reserves: u64,
/// Signed virtual quote reserves from the PumpSwap Pool account or trade event.
pub virtual_quote_reserves: i128,
/// Coin creator vault ATA
pub coin_creator_vault_ata: Pubkey,
/// Coin creator vault authority
@@ -75,6 +77,7 @@ impl PumpSwapParams {
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
virtual_quote_reserves: i128,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
@@ -99,6 +102,7 @@ impl PumpSwapParams {
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
virtual_quote_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
@@ -127,6 +131,21 @@ impl PumpSwapParams {
self
}
/// Quote reserves used by PumpSwap pricing and fee-tier selection.
pub fn effective_quote_reserves(&self) -> Result<u64, anyhow::Error> {
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
self.pool_quote_token_reserves,
self.virtual_quote_reserves,
)
.ok_or_else(|| {
anyhow::anyhow!(
"Invalid PumpSwap effective quote reserves: vault={} virtual={}",
self.pool_quote_token_reserves,
self.virtual_quote_reserves
)
})
}
pub fn with_fee_basis_points(
mut self,
lp_fee_basis_points: u64,
@@ -161,6 +180,7 @@ impl PumpSwapParams {
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
virtual_quote_reserves: i128,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
@@ -178,6 +198,7 @@ impl PumpSwapParams {
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
virtual_quote_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
@@ -202,6 +223,7 @@ impl PumpSwapParams {
pool_quote_token_account: Pubkey,
pool_base_token_reserves: u64,
pool_quote_token_reserves: u64,
virtual_quote_reserves: i128,
coin_creator_vault_ata: Pubkey,
coin_creator_vault_authority: Pubkey,
base_token_program: Pubkey,
@@ -223,6 +245,7 @@ impl PumpSwapParams {
pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
virtual_quote_reserves,
coin_creator_vault_ata,
coin_creator_vault_authority,
base_token_program,
@@ -277,6 +300,18 @@ impl PumpSwapParams {
) -> 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 effective_quote_token_reserves =
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
pool_quote_token_reserves,
pool_data.virtual_quote_reserves,
)
.ok_or_else(|| {
anyhow::anyhow!(
"Invalid PumpSwap effective quote reserves: vault={} virtual={}",
pool_quote_token_reserves,
pool_data.virtual_quote_reserves
)
})?;
let base_mint_supply = fetch_mint_supply(rpc, &pool_data.base_mint).await.ok();
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(
@@ -285,7 +320,7 @@ impl PumpSwapParams {
pool_data.base_mint,
base_mint_supply,
pool_base_token_reserves,
pool_quote_token_reserves,
effective_quote_token_reserves,
);
let creator_fee_basis_points = if pool_data.coin_creator == Pubkey::default() {
0
@@ -319,6 +354,7 @@ impl PumpSwapParams {
pool_quote_token_account: pool_data.pool_quote_token_account,
pool_base_token_reserves,
pool_quote_token_reserves,
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 {