Skip to main content

pump_rust_client/math/
amm.rs

1//! AMM (pump-swap) quoting. Ports `buyQuoteInput`, `buyBaseInput`, and
2//! `sellBaseInput` from `@pump-fun/pump-swap-sdk/src/sdk/{buy,sell}.ts`.
3
4use solana_program::pubkey::Pubkey;
5
6use crate::math::bonding_curve::TOKEN_SUPPLY;
7use crate::math::fees::{ceil_div, compute_amm_fee_bps, fee_amount, AmmFeeBps};
8use crate::math::utils::slippage_bounds;
9use crate::math::{QuoteError, QuoteResult};
10use crate::state::pump_amm::{FeeConfig, GlobalConfig};
11
12pub struct BuyQuoteInputResult {
13    pub base_amount_out: u64,
14    pub effective_quote: u64,
15}
16
17pub struct BuyBaseInputResult {
18    pub total_quote_in: u64,
19    pub raw_quote_in: u64,
20}
21
22pub struct SellBaseInputResult {
23    pub final_quote_out: u64,
24    pub raw_quote_out: u64,
25}
26
27/// Common AMM trade context. `pool_creator` is the pool's anchor `creator`
28/// field; `coin_creator` is the per-coin creator that receives the
29/// coin-creator fee slice (set to `Pubkey::default()` to skip the slice).
30pub struct AmmContext<'a> {
31    pub global_config: &'a GlobalConfig,
32    pub fee_config: Option<&'a FeeConfig>,
33    pub base_mint: &'a Pubkey,
34    pub pool_creator: &'a Pubkey,
35    pub coin_creator: &'a Pubkey,
36    pub base_reserve: u64,
37    pub quote_reserve: u64,
38    pub base_mint_supply: u64,
39}
40
41impl AmmContext<'_> {
42    fn check_reserves(&self) -> QuoteResult<()> {
43        if self.base_reserve == 0 || self.quote_reserve == 0 {
44            return Err(QuoteError::EmptyReserves);
45        }
46        Ok(())
47    }
48}
49
50/// AMM buy: caller specifies SOL input, gets tokens out.
51pub fn buy_quote_input(ctx: &AmmContext<'_>, quote_in: u64) -> QuoteResult<BuyQuoteInputResult> {
52    ctx.check_reserves()?;
53
54    let AmmFeeBps {
55        lp_fee_bps,
56        protocol_fee_bps,
57        creator_fee_bps,
58    } = compute_amm_fee_bps(
59        ctx.global_config,
60        ctx.fee_config,
61        ctx.base_mint,
62        ctx.pool_creator,
63        ctx.base_mint_supply,
64        ctx.base_reserve,
65        ctx.quote_reserve,
66    );
67    let coin_creator_bps = if *ctx.coin_creator == Pubkey::default() {
68        0
69    } else {
70        creator_fee_bps
71    };
72
73    let total_fee_bps = lp_fee_bps + protocol_fee_bps + coin_creator_bps;
74    let denom = 10_000u128 + total_fee_bps as u128;
75
76    // Strip fees from the user's spend, then run constant-product on the
77    // pre-fee amount (matches Raydium-style CPAMM).
78    let effective_quote = (quote_in as u128) * 10_000 / denom;
79    let base_out = (ctx.base_reserve as u128) * effective_quote
80        / ((ctx.quote_reserve as u128) + effective_quote);
81
82    Ok(BuyQuoteInputResult {
83        base_amount_out: base_out as u64,
84        effective_quote: effective_quote as u64,
85    })
86}
87
88/// AMM buy: caller specifies desired tokens out, gets total SOL cost.
89pub fn buy_base_input(ctx: &AmmContext<'_>, base_out: u64) -> QuoteResult<BuyBaseInputResult> {
90    ctx.check_reserves()?;
91    if base_out >= ctx.base_reserve {
92        // `>=` covers both "more tokens than the pool holds" and the
93        // boundary case where the constant-product denominator would be 0.
94        return Err(QuoteError::BaseOutExceedsReserve);
95    }
96
97    let numerator = (ctx.quote_reserve as u128) * (base_out as u128);
98    let denominator = (ctx.base_reserve as u128) - (base_out as u128);
99    let raw_quote = ceil_div(numerator, denominator);
100
101    let AmmFeeBps {
102        lp_fee_bps,
103        protocol_fee_bps,
104        creator_fee_bps,
105    } = compute_amm_fee_bps(
106        ctx.global_config,
107        ctx.fee_config,
108        ctx.base_mint,
109        ctx.pool_creator,
110        ctx.base_mint_supply,
111        ctx.base_reserve,
112        ctx.quote_reserve,
113    );
114
115    let lp = fee_amount(raw_quote, lp_fee_bps);
116    let protocol = fee_amount(raw_quote, protocol_fee_bps);
117    let coin_creator = if *ctx.coin_creator == Pubkey::default() {
118        0
119    } else {
120        fee_amount(raw_quote, creator_fee_bps)
121    };
122    let total = raw_quote + lp + protocol + coin_creator;
123
124    Ok(BuyBaseInputResult {
125        total_quote_in: total as u64,
126        raw_quote_in: raw_quote as u64,
127    })
128}
129
130/// AMM sell: caller specifies tokens in, gets net SOL out.
131pub fn sell_base_input(ctx: &AmmContext<'_>, base_in: u64) -> QuoteResult<SellBaseInputResult> {
132    ctx.check_reserves()?;
133
134    let raw_quote = (ctx.quote_reserve as u128) * (base_in as u128)
135        / ((ctx.base_reserve as u128) + (base_in as u128));
136
137    let AmmFeeBps {
138        lp_fee_bps,
139        protocol_fee_bps,
140        creator_fee_bps,
141    } = compute_amm_fee_bps(
142        ctx.global_config,
143        ctx.fee_config,
144        ctx.base_mint,
145        ctx.pool_creator,
146        ctx.base_mint_supply,
147        ctx.base_reserve,
148        ctx.quote_reserve,
149    );
150
151    let lp = fee_amount(raw_quote, lp_fee_bps);
152    let protocol = fee_amount(raw_quote, protocol_fee_bps);
153    let coin_creator = if *ctx.coin_creator == Pubkey::default() {
154        0
155    } else {
156        fee_amount(raw_quote, creator_fee_bps)
157    };
158    let total_fee = lp + protocol + coin_creator;
159    if raw_quote < total_fee {
160        return Err(QuoteError::FeesExceedOutput);
161    }
162    let final_quote = raw_quote - total_fee;
163
164    Ok(SellBaseInputResult {
165        final_quote_out: final_quote as u64,
166        raw_quote_out: raw_quote as u64,
167    })
168}
169
170// ---------------------------------------------------------------------------
171// Fee-less constant-product primitives for pump-swap pools.
172//
173// Mirrors `mayhem-program::math::pump_swap_math` exactly: identical formulas
174// to the bonding-curve primitives with `pool_base_token_reserves ↔ vTokens`
175// and `pool_quote_token_reserves ↔ vSol`. The fee-aware functions above
176// layer LP / protocol / coin-creator fees on top of these same formulas.
177// ---------------------------------------------------------------------------
178
179/// Pure constant-product sell quote on an AMM pool, no fees applied.
180/// `out = amount * pool_quote / (pool_base + amount)`.
181pub fn sell_quote(
182    pool_base_token_reserves: u64,
183    pool_quote_token_reserves: u64,
184    amount: u64,
185) -> QuoteResult<u128> {
186    let amount = u128::from(amount);
187    let v_quote = u128::from(pool_quote_token_reserves);
188    let v_base = u128::from(pool_base_token_reserves);
189
190    amount
191        .checked_mul(v_quote)
192        .ok_or(QuoteError::MathOverflow)?
193        .checked_div(v_base.checked_add(amount).ok_or(QuoteError::MathOverflow)?)
194        .ok_or(QuoteError::MathOverflow)
195}
196
197/// Pure constant-product buy quote on an AMM pool, no fees applied.
198/// `out = sol_amount * pool_base / (pool_quote + sol_amount)`.
199pub fn buy_token_quote_with_sol(
200    pool_base_token_reserves: u64,
201    pool_quote_token_reserves: u64,
202    sol_amount: u64,
203) -> QuoteResult<u128> {
204    let sol_amount = u128::from(sol_amount);
205    let v_quote = u128::from(pool_quote_token_reserves);
206    let v_base = u128::from(pool_base_token_reserves);
207
208    sol_amount
209        .checked_mul(v_base)
210        .ok_or(QuoteError::MathOverflow)?
211        .checked_div(
212            v_quote
213                .checked_add(sol_amount)
214                .ok_or(QuoteError::MathOverflow)?,
215        )
216        .ok_or(QuoteError::MathOverflow)
217}
218
219/// Inverse of [`sell_quote`]: given a desired SOL output, how many tokens
220/// must be sold. `out = sol_amount * pool_base / (pool_quote - sol_amount)`.
221///
222/// Returns [`QuoteError::MathOverflow`] if `sol_amount >= pool_quote_token_reserves`.
223pub fn sell_token_quote_with_sol(
224    pool_base_token_reserves: u64,
225    pool_quote_token_reserves: u64,
226    sol_amount: u64,
227) -> QuoteResult<u128> {
228    let sol_amount = u128::from(sol_amount);
229    let v_quote = u128::from(pool_quote_token_reserves);
230    let v_base = u128::from(pool_base_token_reserves);
231
232    sol_amount
233        .checked_mul(v_base)
234        .ok_or(QuoteError::MathOverflow)?
235        .checked_div(
236            v_quote
237                .checked_sub(sol_amount)
238                .ok_or(QuoteError::MathOverflow)?,
239        )
240        .ok_or(QuoteError::MathOverflow)
241}
242
243/// Validate that the AMM pool's current market cap is within
244/// `target_market_cap ± slippage_bps`. Uses the fixed [`TOKEN_SUPPLY`] for
245/// market-cap derivation: `mcap = TOKEN_SUPPLY * pool_quote / pool_base`.
246pub fn validate_market_cap(
247    pool_base_token_reserves: u64,
248    pool_quote_token_reserves: u64,
249    target_market_cap: u128,
250    slippage_bps: u16,
251) -> QuoteResult<()> {
252    let v_quote = u128::from(pool_quote_token_reserves);
253    let v_base = u128::from(pool_base_token_reserves);
254
255    let current = TOKEN_SUPPLY
256        .checked_mul(v_quote)
257        .ok_or(QuoteError::MathOverflow)?
258        .checked_div(v_base)
259        .ok_or(QuoteError::MathOverflow)?;
260
261    let (min, max) =
262        slippage_bounds(target_market_cap, slippage_bps).ok_or(QuoteError::MathOverflow)?;
263
264    if current < min || current > max {
265        return Err(QuoteError::SlippageExceeded);
266    }
267    Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272    use super::*;
273
274    // Mid-pool fixture: pool holds 100 SOL of quote and 800M tokens of base.
275    const POOL_QUOTE: u64 = 100_000_000_000;
276    const POOL_BASE: u64 = 800_000_000_000_000;
277
278    #[test]
279    fn sell_quote_matches_constant_product() {
280        let amount: u64 = 1_000_000_000_000;
281        let out = sell_quote(POOL_BASE, POOL_QUOTE, amount).unwrap();
282        let expected =
283            (amount as u128) * (POOL_QUOTE as u128) / ((POOL_BASE as u128) + amount as u128);
284        assert_eq!(out, expected);
285    }
286
287    #[test]
288    fn buy_and_sell_token_quote_with_sol_use_correct_denominators() {
289        let sol_in: u64 = 1_000_000_000;
290        let bought = buy_token_quote_with_sol(POOL_BASE, POOL_QUOTE, sol_in).unwrap();
291        let expected =
292            (sol_in as u128) * (POOL_BASE as u128) / ((POOL_QUOTE as u128) + sol_in as u128);
293        assert_eq!(bought, expected);
294
295        let inv = sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, sol_in).unwrap();
296        let expected_inv =
297            (sol_in as u128) * (POOL_BASE as u128) / ((POOL_QUOTE as u128) - sol_in as u128);
298        assert_eq!(inv, expected_inv);
299    }
300
301    #[test]
302    fn sell_token_quote_overflow_when_sol_exceeds_reserve() {
303        assert_eq!(
304            sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, POOL_QUOTE),
305            Err(QuoteError::MathOverflow)
306        );
307        assert_eq!(
308            sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, POOL_QUOTE + 1),
309            Err(QuoteError::MathOverflow)
310        );
311    }
312
313    #[test]
314    fn validate_market_cap_passes_within_envelope() {
315        let current = TOKEN_SUPPLY * (POOL_QUOTE as u128) / (POOL_BASE as u128);
316        validate_market_cap(POOL_BASE, POOL_QUOTE, current, 0).unwrap();
317        validate_market_cap(POOL_BASE, POOL_QUOTE, current * 99 / 100, 200).unwrap();
318    }
319
320    #[test]
321    fn validate_market_cap_fails_outside_envelope() {
322        let current = TOKEN_SUPPLY * (POOL_QUOTE as u128) / (POOL_BASE as u128);
323        assert_eq!(
324            validate_market_cap(POOL_BASE, POOL_QUOTE, current * 95 / 100, 100),
325            Err(QuoteError::SlippageExceeded)
326        );
327    }
328}