fix(pumpswap): validate virtual reserve quotes

This commit is contained in:
0xfnzero
2026-07-17 02:15:20 +08:00
parent dd41dd4f87
commit fb1ff176d0
13 changed files with 914 additions and 111 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2021"
[dependencies]
sol-trade-sdk = { path = "../.." }
solana-streamer-sdk = "0.5.0"
solana-streamer-sdk = { version = "2.0.0", git = "https://github.com/0xfnzero/solana-streamer", rev = "85c6cc901ad3f1bf8fe8010d79b92ecdd0be02b4" }
solana-sdk = "3.0.0"
solana-commitment-config = { version = "3.0.0", features = ["serde"] }
tokio = { version = "1", features = ["full"] }
+1 -1
View File
@@ -21,7 +21,7 @@ cargo run --release --package pumpswap_trading
- The buy uses `BuyAmount::WithMaxInput`, which applies slippage to maximum quote cost and is appropriate when fill priority matters.
- Buy parameters use post-trade reserves and LP/protocol/creator fee bps from the event.
- `solana-streamer-sdk 0.5.0` predates the appended `virtual_quote_reserves` event field, so this compatibility example reads that Pool field once before quoting. When the parser exposes the field, pass it directly to `PumpSwapParams::from_trade_with_fee_basis_points` or keep it in a Pool account cache to remove this RPC from the hot path.
- The event's raw and virtual quote reserves come from the same transaction snapshot. The hot path does not fetch the Pool account, avoiding both added latency and mixed-slot quotes.
- The example records the pre-buy balance and sells only the confirmed balance increase. It refreshes pool state and blockhash before selling.
- Use `BuyAmount::ExactInput` when the quote spend must be exact. That mode protects minimum output and can fail more often in an active pool.
+1 -1
View File
@@ -19,7 +19,7 @@ cargo run --release --package pumpswap_trading
- 买入使用 `BuyAmount::WithMaxInput`,适合优先成交的跟单/狙击场景,滑点限制最大 quote 成本。
- 买入参数使用事件中的成交后储备和 LP/protocol/creator fee bps。
- `solana-streamer-sdk 0.5.0` 尚未暴露追加的 `virtual_quote_reserves` 事件字段,因此该兼容示例会在报价前读取一次 Pool 字段。解析器暴露该字段后,应直接传给 `PumpSwapParams::from_trade_with_fee_basis_points`,或维护 Pool 账户缓存,以移除热路径中的这次 RPC
- 原始 quote 储备和虚拟 quote 储备均来自同一笔交易的事件快照;热路径不再查询 Pool 账户,避免额外延迟和跨 slot 混合报价
- 示例记录买前余额,只卖出确认后的余额增量;卖出前重新获取池状态和 blockhash。
- 若业务必须精确花费 quote,应改用 `BuyAmount::ExactInput`。这会启用最小输出保护,在活跃池中更容易因状态变化而失败。
+64 -74
View File
@@ -1,5 +1,4 @@
use sol_trade_sdk::common::{clock::now_micros, SolanaRpcClient, TradeConfig};
use sol_trade_sdk::instruction::utils::pumpswap::fetch_pool;
use sol_trade_sdk::TradeTokenType;
use sol_trade_sdk::{
common::AnyResult,
@@ -12,18 +11,16 @@ use sol_trade_sdk::{
};
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{hash::Hash, pubkey::Pubkey};
use solana_streamer_sdk::streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID;
use solana_streamer_sdk::streaming::event_parser::{
common::filter::EventTypeFilter, protocols::pumpswap::PumpSwapBuyEvent,
};
use solana_streamer_sdk::streaming::event_parser::{
common::EventType, protocols::pumpswap::PumpSwapSellEvent,
};
use solana_streamer_sdk::streaming::event_parser::{Protocol, UnifiedEvent};
use solana_streamer_sdk::streaming::event_parser::{DexEvent, Protocol};
use solana_streamer_sdk::streaming::yellowstone_grpc::{AccountFilter, TransactionFilter};
use solana_streamer_sdk::streaming::YellowstoneGrpc;
use solana_streamer_sdk::{
match_event, streaming::event_parser::protocols::pumpswap::parser::PUMPSWAP_PROGRAM_ID,
};
use std::str::FromStr;
use std::sync::{
atomic::{AtomicBool, Ordering},
@@ -170,8 +167,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let account_filter = AccountFilter { account: vec![], owner: vec![], filters: vec![] };
// listen to specific event type
let event_type_filter =
EventTypeFilter { include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell] };
let event_type_filter = EventTypeFilter {
include: vec![EventType::PumpSwapBuy, EventType::PumpSwapSell],
..Default::default()
};
grpc.subscribe_events_immediate(
protocols,
@@ -194,62 +193,59 @@ fn create_event_callback(
client: Arc<SolanaTrade>,
blockhash_cache: BlockhashCache,
selection: EventSelection,
) -> impl Fn(Box<dyn UnifiedEvent>) {
move |event: Box<dyn UnifiedEvent>| {
match_event!(event, {
PumpSwapBuyEvent => |e: PumpSwapBuyEvent| {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
tokio::spawn(async move {
if let Err(err) = pumpswap_trade_with_grpc_buy_event(
client,
blockhash_cache,
event_clone,
).await {
eprintln!("Error in trade: {:?}", err);
std::process::exit(1);
}
});
}
},
PumpSwapSellEvent => |e: PumpSwapSellEvent| {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT || e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let event_clone = e.clone();
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
tokio::spawn(async move {
if let Err(err) = pumpswap_trade_with_grpc_sell_event(
client,
blockhash_cache,
event_clone,
).await {
eprintln!("Error in trade: {:?}", err);
std::process::exit(1);
}
});
}
) -> impl Fn(DexEvent) {
move |event: DexEvent| match event {
DexEvent::PumpSwapBuyEvent(e) => {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
});
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
tokio::spawn(async move {
if let Err(err) =
pumpswap_trade_with_grpc_buy_event(client, blockhash_cache, e).await
{
eprintln!("Error in trade: {:?}", err);
std::process::exit(1);
}
});
}
}
DexEvent::PumpSwapSellEvent(e) => {
let is_wsol = e.base_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::WSOL_TOKEN_ACCOUNT;
let is_usdc = e.base_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT
|| e.quote_mint == sol_trade_sdk::constants::USDC_TOKEN_ACCOUNT;
if !is_wsol && !is_usdc {
return;
}
if !selection.matches(e.pool, e.base_mint, e.quote_mint, e.metadata.recv_us) {
return;
}
// Test code, only test one transaction
if !ALREADY_EXECUTED.swap(true, Ordering::SeqCst) {
let client = client.clone();
let blockhash_cache = blockhash_cache.clone();
tokio::spawn(async move {
if let Err(err) =
pumpswap_trade_with_grpc_sell_event(client, blockhash_cache, e).await
{
eprintln!("Error in trade: {:?}", err);
std::process::exit(1);
}
});
}
}
_ => {}
}
}
@@ -280,10 +276,6 @@ async fn pumpswap_trade_with_grpc_buy_event(
blockhash_cache: BlockhashCache,
trade_info: PumpSwapBuyEvent,
) -> AnyResult<()> {
// solana-streamer-sdk 0.5.0 predates the appended event field. Read the
// Pool value so this compatibility example still prices effective reserves.
let virtual_quote_reserves =
fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?.virtual_quote_reserves;
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
@@ -292,7 +284,7 @@ async fn pumpswap_trade_with_grpc_buy_event(
trade_info.pool_quote_token_account,
trade_info.pool_base_token_reserves,
trade_info.pool_quote_token_reserves,
virtual_quote_reserves,
trade_info.virtual_quote_reserves,
trade_info.coin_creator_vault_ata,
trade_info.coin_creator_vault_authority,
trade_info.base_token_program,
@@ -300,8 +292,8 @@ async fn pumpswap_trade_with_grpc_buy_event(
trade_info.protocol_fee_recipient,
Pubkey::default(),
trade_info.coin_creator,
false,
0,
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
trade_info.cashback_fee_basis_points,
trade_info.lp_fee_basis_points,
trade_info.protocol_fee_basis_points,
trade_info.coin_creator_fee_basis_points,
@@ -323,8 +315,6 @@ async fn pumpswap_trade_with_grpc_sell_event(
blockhash_cache: BlockhashCache,
trade_info: PumpSwapSellEvent,
) -> AnyResult<()> {
let virtual_quote_reserves =
fetch_pool(&client.infrastructure.rpc, &trade_info.pool).await?.virtual_quote_reserves;
let params = PumpSwapParams::from_trade_with_fee_basis_points(
trade_info.pool,
trade_info.base_mint,
@@ -333,7 +323,7 @@ async fn pumpswap_trade_with_grpc_sell_event(
trade_info.pool_quote_token_account,
trade_info.pool_base_token_reserves,
trade_info.pool_quote_token_reserves,
virtual_quote_reserves,
trade_info.virtual_quote_reserves,
trade_info.coin_creator_vault_ata,
trade_info.coin_creator_vault_authority,
trade_info.base_token_program,
@@ -341,8 +331,8 @@ async fn pumpswap_trade_with_grpc_sell_event(
trade_info.protocol_fee_recipient,
Pubkey::default(),
trade_info.coin_creator,
false,
0,
trade_info.cashback_fee_basis_points != 0 || trade_info.cashback != 0,
trade_info.cashback_fee_basis_points,
trade_info.lp_fee_basis_points,
trade_info.protocol_fee_basis_points,
trade_info.coin_creator_fee_basis_points,