fix(pumpswap): validate virtual reserve quotes
This commit is contained in:
+46
-10
@@ -54,7 +54,9 @@ 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.effective_quote_reserves()?;
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let virtual_quote_reserves = protocol_params.virtual_quote_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;
|
||||
@@ -97,9 +99,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// base_amount_out, max_quote_amount_in
|
||||
(result.base, result.max_quote)
|
||||
} else {
|
||||
@@ -108,9 +111,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// min_quote_amount_out, base_amount_in
|
||||
(result.min_quote, params.input_amount.unwrap_or(0))
|
||||
};
|
||||
@@ -283,7 +287,9 @@ 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.effective_quote_reserves()?;
|
||||
let pool_quote_token_reserves = protocol_params.pool_quote_token_reserves;
|
||||
let virtual_quote_reserves = protocol_params.virtual_quote_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;
|
||||
@@ -305,8 +311,8 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
return Err(anyhow!("Pool must contain WSOL or USDC"));
|
||||
}
|
||||
|
||||
if params.input_amount.is_none() {
|
||||
return Err(anyhow!("Token amount is not set"));
|
||||
if params.input_amount.unwrap_or_default() == 0 {
|
||||
return Err(anyhow!("Token amount must be greater than zero"));
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@@ -320,6 +326,9 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
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"));
|
||||
}
|
||||
(params.input_amount.unwrap(), output_amount)
|
||||
} else if quote_is_wsol_or_usdc {
|
||||
let result = sell_base_input_internal_with_fees(
|
||||
@@ -327,9 +336,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// base_amount_in, min_quote_amount_out
|
||||
(params.input_amount.unwrap(), result.min_quote)
|
||||
} else {
|
||||
@@ -338,9 +348,10 @@ impl InstructionBuilder for PumpSwapInstructionBuilder {
|
||||
params.slippage_basis_points.unwrap_or(DEFAULT_SLIPPAGE),
|
||||
pool_base_token_reserves,
|
||||
pool_quote_token_reserves,
|
||||
virtual_quote_reserves,
|
||||
&fee_basis_points,
|
||||
)
|
||||
.unwrap();
|
||||
.map_err(anyhow::Error::msg)?;
|
||||
// max_quote_amount_in, base_amount_out
|
||||
(result.max_quote, result.base)
|
||||
};
|
||||
@@ -617,6 +628,29 @@ mod tests {
|
||||
assert_eq!(u64::from_le_bytes(ix.data[16..24].try_into().unwrap()), 42);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_fixed_output_rejects_real_vault_overflow() {
|
||||
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;
|
||||
|
||||
let error = PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "Minimum quote output exceeds the real quote-vault balance");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_sell_rejects_zero_input() {
|
||||
let mut params = swap_params(TradeType::Sell, None);
|
||||
params.input_amount = Some(0);
|
||||
|
||||
let error = PumpSwapInstructionBuilder.build_sell_instructions(¶ms).await.unwrap_err();
|
||||
|
||||
assert_eq!(error.to_string(), "Token amount must be greater than zero");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn pumpswap_usdc_buy_create_input_builds_usdc_ata() {
|
||||
let mut params = swap_params(TradeType::Buy, Some(42));
|
||||
@@ -670,7 +704,8 @@ mod tests {
|
||||
1_000_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_500_000_000,
|
||||
2_000_000_000,
|
||||
500_000_000,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -693,7 +728,8 @@ mod tests {
|
||||
100_000,
|
||||
100,
|
||||
1_000_000_000,
|
||||
2_500_000_000,
|
||||
2_000_000_000,
|
||||
500_000_000,
|
||||
&crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints::new(20, 5, 0),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -740,10 +740,11 @@ pub async fn fetch_pool(
|
||||
Ok(pool)
|
||||
}
|
||||
|
||||
/// Known allocated Pool account sizes. The July 2026 layout carrying
|
||||
/// `virtual_quote_reserves` is allocated to 300 bytes on-chain.
|
||||
/// Known allocated Pool account sizes. Current accounts may be serialized to
|
||||
/// exactly 261 bytes or retain a larger historical allocation.
|
||||
const POOL_DATA_LEN_LEGACY: u64 = 8 + 244;
|
||||
const POOL_DATA_LEN_CURRENT: u64 = 300;
|
||||
const POOL_DATA_LEN_CURRENT: u64 = 8 + 253;
|
||||
const POOL_DATA_LEN_PADDED: u64 = 300;
|
||||
const POOL_DATA_LEN_EXTENDED: u64 = 643;
|
||||
|
||||
/// Run getProgramAccounts with a Memcmp filter, querying known Pool sizes in parallel.
|
||||
@@ -770,13 +771,15 @@ async fn get_program_accounts_known_sizes(
|
||||
};
|
||||
let program_id = accounts::AMM_PROGRAM;
|
||||
#[allow(deprecated)]
|
||||
let (legacy_result, current_result, extended_result) = tokio::join!(
|
||||
let (legacy_result, current_result, padded_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_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());
|
||||
Ok(all)
|
||||
}
|
||||
@@ -990,4 +993,12 @@ mod tests {
|
||||
);
|
||||
assert_eq!(fees, PumpSwapFeeBasisPoints::new(20, 5, 75));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pumpswap_pool_queries_cover_current_serialized_and_padded_sizes() {
|
||||
assert_eq!(POOL_DATA_LEN_LEGACY, 252);
|
||||
assert_eq!(POOL_DATA_LEN_CURRENT, 261);
|
||||
assert_eq!(POOL_DATA_LEN_PADDED, 300);
|
||||
assert_eq!(POOL_DATA_LEN_EXTENDED, 643);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ pub fn pool_decode(data: &[u8]) -> Option<Pool> {
|
||||
|
||||
/// Compute the quote reserves used by PumpSwap pricing.
|
||||
///
|
||||
/// Returns `None` when the signed sum is negative or cannot fit in a `u64`.
|
||||
/// Returns `None` when the signed sum is non-positive or cannot fit in a `u64`.
|
||||
#[inline]
|
||||
pub fn effective_quote_reserves(
|
||||
quote_vault_balance: u64,
|
||||
@@ -87,6 +87,7 @@ pub fn effective_quote_reserves(
|
||||
i128::from(quote_vault_balance)
|
||||
.checked_add(virtual_quote_reserves)
|
||||
.and_then(|reserves| u64::try_from(reserves).ok())
|
||||
.filter(|reserves| *reserves != 0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -131,6 +132,7 @@ mod tests {
|
||||
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(1_000, -1_000), None);
|
||||
assert_eq!(effective_quote_reserves(100, -101), None);
|
||||
assert_eq!(effective_quote_reserves(u64::MAX, 1), None);
|
||||
}
|
||||
|
||||
+163
-18
@@ -7,6 +7,23 @@ use crate::instruction::utils::pumpswap::accounts::{
|
||||
use crate::instruction::utils::pumpswap::PumpSwapFeeBasisPoints;
|
||||
use solana_sdk::pubkey::Pubkey;
|
||||
|
||||
#[inline]
|
||||
fn effective_quote_reserve(
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
) -> Result<u64, String> {
|
||||
crate::instruction::utils::pumpswap_types::effective_quote_reserves(
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
)
|
||||
.filter(|reserve| *reserve != 0)
|
||||
.ok_or_else(|| {
|
||||
format!(
|
||||
"Invalid effective quote reserves: raw={quote_reserve}, virtual={virtual_quote_reserves}."
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// 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]
|
||||
@@ -69,7 +86,8 @@ pub struct SellQuoteInputResult {
|
||||
/// * `base` - Amount of base tokens to buy
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins (from on-chain / events); use `0` if unknown
|
||||
///
|
||||
@@ -80,6 +98,7 @@ pub fn buy_base_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyBaseInputResult, String> {
|
||||
@@ -88,6 +107,7 @@ pub fn buy_base_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
@@ -101,17 +121,19 @@ pub fn buy_base_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<BuyBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
if base > base_reserve {
|
||||
return Err("Cannot buy more base tokens than the pool reserves.".to_string());
|
||||
}
|
||||
|
||||
// Calculate required quote amount using constant product formula
|
||||
let numerator = (quote_reserve as u128) * (base as u128);
|
||||
let numerator = (effective_quote_reserve as u128) * (base as u128);
|
||||
let denominator = base_reserve - base;
|
||||
|
||||
if denominator == 0 {
|
||||
@@ -148,7 +170,8 @@ pub fn buy_base_input_internal_with_fees(
|
||||
/// * `quote` - Amount of quote tokens to spend
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
@@ -159,6 +182,7 @@ pub fn buy_quote_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<BuyQuoteInputResult, String> {
|
||||
@@ -167,6 +191,7 @@ pub fn buy_quote_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
@@ -180,11 +205,13 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<BuyQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate total fee basis points
|
||||
let total_fee_bps = fee_basis_points
|
||||
@@ -208,7 +235,7 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
|
||||
// Calculate base amount out using constant product formula
|
||||
let numerator = (base_reserve as u128) * input_amount;
|
||||
let denominator_effective = (quote_reserve as u128) + input_amount;
|
||||
let denominator_effective = (effective_quote_reserve as u128) + input_amount;
|
||||
|
||||
if denominator_effective == 0 {
|
||||
return Err("Pool would be depleted; denominator is zero.".to_string());
|
||||
@@ -232,7 +259,8 @@ pub fn buy_quote_input_internal_with_fees(
|
||||
/// * `base` - Amount of base tokens to sell
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
@@ -243,6 +271,7 @@ pub fn sell_base_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellBaseInputResult, String> {
|
||||
@@ -251,6 +280,7 @@ pub fn sell_base_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
@@ -264,14 +294,16 @@ pub fn sell_base_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<SellBaseInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
return Err("Invalid input: 'baseReserve' or 'quoteReserve' cannot be zero.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate quote amount out using constant product formula
|
||||
let quote_amount_out = ((quote_reserve as u128) * (base as u128)
|
||||
let quote_amount_out = ((effective_quote_reserve as u128) * (base as u128)
|
||||
/ ((base_reserve as u128) + (base as u128))) as u64;
|
||||
|
||||
// Calculate fees
|
||||
@@ -290,6 +322,10 @@ pub fn sell_base_input_internal_with_fees(
|
||||
if total_fees > quote_amount_out {
|
||||
return Err("Fees exceed total output; final quote is negative.".to_string());
|
||||
}
|
||||
let quote_vault_outflow = quote_amount_out - lp_fee;
|
||||
if quote_vault_outflow > quote_reserve {
|
||||
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||
}
|
||||
let final_quote = quote_amount_out - total_fees;
|
||||
|
||||
// Calculate min quote with slippage
|
||||
@@ -310,12 +346,22 @@ fn calculate_quote_amount_out(
|
||||
lp_fee_basis_points: u64,
|
||||
protocol_fee_basis_points: u64,
|
||||
coin_creator_fee_basis_points: u64,
|
||||
) -> u64 {
|
||||
let total_fee_basis_points =
|
||||
lp_fee_basis_points + protocol_fee_basis_points + coin_creator_fee_basis_points;
|
||||
let denominator = MAX_FEE_BASIS_POINTS - total_fee_basis_points;
|
||||
ceil_div((user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128), denominator as u128)
|
||||
as u64
|
||||
) -> Result<u64, String> {
|
||||
let total_fee_basis_points = lp_fee_basis_points
|
||||
.checked_add(protocol_fee_basis_points)
|
||||
.and_then(|fees| fees.checked_add(coin_creator_fee_basis_points))
|
||||
.ok_or_else(|| "Fee basis points overflow.".to_string())?;
|
||||
let denominator = MAX_FEE_BASIS_POINTS
|
||||
.checked_sub(total_fee_basis_points)
|
||||
.ok_or_else(|| "Total fee basis points must be less than 10,000.".to_string())?;
|
||||
if denominator == 0 {
|
||||
return Err("Total fee basis points must be less than 10,000.".to_string());
|
||||
}
|
||||
let raw_quote = ceil_div(
|
||||
(user_quote_amount_out as u128) * (MAX_FEE_BASIS_POINTS as u128),
|
||||
denominator as u128,
|
||||
);
|
||||
u64::try_from(raw_quote).map_err(|_| "Calculated quote amount exceeds u64.".to_string())
|
||||
}
|
||||
|
||||
/// Calculate base tokens needed to receive a specific amount of quote tokens
|
||||
@@ -324,7 +370,8 @@ fn calculate_quote_amount_out(
|
||||
/// * `quote` - Desired amount of quote tokens to receive
|
||||
/// * `slippage_basis_points` - Slippage tolerance in basis points (100 = 1%)
|
||||
/// * `base_reserve` - Base token reserves in the pool
|
||||
/// * `quote_reserve` - Quote token reserves in the pool
|
||||
/// * `quote_reserve` - Raw quote-vault balance
|
||||
/// * `virtual_quote_reserves` - Signed virtual quote reserves from the same pool snapshot
|
||||
/// * `coin_creator` - Token creator address
|
||||
/// * `cashback_fee_basis_points` - Extra fee bps for cashback coins; use `0` if unknown
|
||||
///
|
||||
@@ -335,6 +382,7 @@ pub fn sell_quote_input_internal(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
coin_creator: &Pubkey,
|
||||
cashback_fee_basis_points: u64,
|
||||
) -> Result<SellQuoteInputResult, String> {
|
||||
@@ -343,6 +391,7 @@ pub fn sell_quote_input_internal(
|
||||
slippage_basis_points,
|
||||
base_reserve,
|
||||
quote_reserve,
|
||||
virtual_quote_reserves,
|
||||
&PumpSwapFeeBasisPoints::new(
|
||||
LP_FEE_BASIS_POINTS,
|
||||
PROTOCOL_FEE_BASIS_POINTS,
|
||||
@@ -356,6 +405,7 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
slippage_basis_points: u64,
|
||||
base_reserve: u64,
|
||||
quote_reserve: u64,
|
||||
virtual_quote_reserves: i128,
|
||||
fee_basis_points: &PumpSwapFeeBasisPoints,
|
||||
) -> Result<SellQuoteInputResult, String> {
|
||||
if base_reserve == 0 || quote_reserve == 0 {
|
||||
@@ -364,6 +414,7 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
if quote > quote_reserve {
|
||||
return Err("Cannot receive more quote tokens than the pool quote reserves.".to_string());
|
||||
}
|
||||
let effective_quote_reserve = effective_quote_reserve(quote_reserve, virtual_quote_reserves)?;
|
||||
|
||||
// Calculate raw quote amount including fees
|
||||
let raw_quote = calculate_quote_amount_out(
|
||||
@@ -371,19 +422,113 @@ pub fn sell_quote_input_internal_with_fees(
|
||||
fee_basis_points.lp_fee_basis_points,
|
||||
fee_basis_points.protocol_fee_basis_points,
|
||||
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 {
|
||||
return Err("Insufficient real quote reserves to cover the sell output.".to_string());
|
||||
}
|
||||
|
||||
// Calculate base amount needed using inverse constant product formula
|
||||
if raw_quote >= quote_reserve {
|
||||
if raw_quote >= effective_quote_reserve {
|
||||
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), (quote_reserve - raw_quote) as u128)
|
||||
as u64;
|
||||
let base_amount_in = ceil_div(
|
||||
(base_reserve as u128) * (raw_quote as u128),
|
||||
(effective_quote_reserve - raw_quote) as u128,
|
||||
) as u64;
|
||||
|
||||
// Calculate min quote with slippage
|
||||
let min_quote = calculate_with_slippage_sell(quote, slippage_basis_points);
|
||||
|
||||
Ok(SellQuoteInputResult { internal_raw_quote: raw_quote, base: base_amount_in, min_quote })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn fees() -> PumpSwapFeeBasisPoints {
|
||||
PumpSwapFeeBasisPoints::new(20, 5, 0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buy_uses_effective_quote_reserves() {
|
||||
let result =
|
||||
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 500_000, &fees())
|
||||
.unwrap();
|
||||
let without_virtual =
|
||||
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 0, &fees())
|
||||
.unwrap();
|
||||
|
||||
assert!(result.base < without_virtual.base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sell_rejects_output_not_covered_by_real_quote_vault() {
|
||||
let error = sell_base_input_internal_with_fees(
|
||||
1_000_000,
|
||||
100,
|
||||
1_000_000,
|
||||
1_000,
|
||||
1_000_000,
|
||||
&fees(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, "Insufficient real quote reserves to cover the sell output.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_quote_sell_uses_effective_reserve_for_denominator() {
|
||||
let result =
|
||||
sell_quote_input_internal_with_fees(500, 100, 1_000_000, 1_000, 1_000_000, &fees())
|
||||
.unwrap();
|
||||
|
||||
assert!(result.base < 1_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_quote_sell_rejects_output_above_real_quote_vault() {
|
||||
let error =
|
||||
sell_quote_input_internal_with_fees(1_001, 100, 1_000_000, 1_000, 1_000_000, &fees())
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, "Cannot receive more quote tokens than the pool quote reserves.");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_virtual_reserves_are_applied() {
|
||||
let result = buy_quote_input_internal_with_fees(
|
||||
10_000,
|
||||
100,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
-500_000,
|
||||
&fees(),
|
||||
)
|
||||
.unwrap();
|
||||
let without_virtual =
|
||||
buy_quote_input_internal_with_fees(10_000, 100, 1_000_000, 1_000_000, 0, &fees())
|
||||
.unwrap();
|
||||
|
||||
assert!(result.base > without_virtual.base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_effective_quote_reserves_are_rejected() {
|
||||
let error = buy_quote_input_internal_with_fees(
|
||||
10_000,
|
||||
100,
|
||||
1_000_000,
|
||||
1_000_000,
|
||||
-1_000_000,
|
||||
&fees(),
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(error, "Invalid effective quote reserves: raw=1000000, virtual=-1000000.");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user