Skip to main content

pump_rust_client/math/
amm.rs

1//! AMM (pump-swap) quote helpers.
2
3use solana_program::pubkey::Pubkey;
4
5use crate::math::bonding_curve::TOKEN_SUPPLY;
6use crate::math::fees::{ceil_div, compute_amm_fee_bps, creator_fee_amount, fee_amount, AmmFeeBps};
7use crate::math::utils::{mul_div_u128, slippage_bounds};
8use crate::math::{QuoteError, QuoteResult};
9use crate::state::FeeConfig;
10use crate::state::pump_amm::{ 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    let effective_quote = (quote_in as u128) * 10_000 / denom;
77    let base_out = (ctx.base_reserve as u128) * effective_quote
78        / ((ctx.quote_reserve as u128) + effective_quote);
79
80    Ok(BuyQuoteInputResult {
81        base_amount_out: base_out as u64,
82        effective_quote: effective_quote as u64,
83    })
84}
85
86/// AMM buy: caller specifies desired tokens out, gets total SOL cost.
87pub fn buy_base_input(ctx: &AmmContext<'_>, base_out: u64) -> QuoteResult<BuyBaseInputResult> {
88    ctx.check_reserves()?;
89    if base_out >= ctx.base_reserve {
90        return Err(QuoteError::BaseOutExceedsReserve);
91    }
92
93    let numerator = (ctx.quote_reserve as u128) * (base_out as u128);
94    let denominator = (ctx.base_reserve as u128) - (base_out as u128);
95    let raw_quote = ceil_div(numerator, denominator);
96
97    let AmmFeeBps {
98        lp_fee_bps,
99        protocol_fee_bps,
100        creator_fee_bps,
101    } = compute_amm_fee_bps(
102        ctx.global_config,
103        ctx.fee_config,
104        ctx.base_mint,
105        ctx.pool_creator,
106        ctx.base_mint_supply,
107        ctx.base_reserve,
108        ctx.quote_reserve,
109    );
110
111    let lp = fee_amount(raw_quote, lp_fee_bps);
112    let protocol = fee_amount(raw_quote, protocol_fee_bps);
113    let coin_creator = creator_fee_amount(ctx.coin_creator, raw_quote, creator_fee_bps);
114    let total = raw_quote + lp + protocol + coin_creator;
115
116    Ok(BuyBaseInputResult {
117        total_quote_in: total as u64,
118        raw_quote_in: raw_quote as u64,
119    })
120}
121
122/// AMM sell: caller specifies tokens in, gets net SOL out.
123pub fn sell_base_input(ctx: &AmmContext<'_>, base_in: u64) -> QuoteResult<SellBaseInputResult> {
124    ctx.check_reserves()?;
125
126    let raw_quote = (ctx.quote_reserve as u128) * (base_in as u128)
127        / ((ctx.base_reserve as u128) + (base_in as u128));
128
129    let AmmFeeBps {
130        lp_fee_bps,
131        protocol_fee_bps,
132        creator_fee_bps,
133    } = compute_amm_fee_bps(
134        ctx.global_config,
135        ctx.fee_config,
136        ctx.base_mint,
137        ctx.pool_creator,
138        ctx.base_mint_supply,
139        ctx.base_reserve,
140        ctx.quote_reserve,
141    );
142
143    let lp = fee_amount(raw_quote, lp_fee_bps);
144    let protocol = fee_amount(raw_quote, protocol_fee_bps);
145    let coin_creator = creator_fee_amount(ctx.coin_creator, raw_quote, creator_fee_bps);
146    let total_fee = lp + protocol + coin_creator;
147    if raw_quote < total_fee {
148        return Err(QuoteError::FeesExceedOutput);
149    }
150    let final_quote = raw_quote - total_fee;
151
152    Ok(SellBaseInputResult {
153        final_quote_out: final_quote as u64,
154        raw_quote_out: raw_quote as u64,
155    })
156}
157
158/// Constant-product sell quote, fees not applied.
159/// `out = amount * pool_quote / (pool_base + amount)`.
160pub fn sell_quote(
161    pool_base_token_reserves: u64,
162    pool_quote_token_reserves: u64,
163    amount: u64,
164) -> QuoteResult<u128> {
165    let amount = u128::from(amount);
166    let v_quote = u128::from(pool_quote_token_reserves);
167    let v_base = u128::from(pool_base_token_reserves);
168    let denom = v_base.checked_add(amount).ok_or(QuoteError::MathOverflow)?;
169    mul_div_u128(amount, v_quote, denom)
170}
171
172/// Pure constant-product buy quote on an AMM pool, no fees applied.
173/// `out = sol_amount * pool_base / (pool_quote + sol_amount)`.
174pub fn buy_token_quote_with_sol(
175    pool_base_token_reserves: u64,
176    pool_quote_token_reserves: u64,
177    sol_amount: u64,
178) -> QuoteResult<u128> {
179    let sol_amount = u128::from(sol_amount);
180    let v_quote = u128::from(pool_quote_token_reserves);
181    let v_base = u128::from(pool_base_token_reserves);
182    let denom = v_quote
183        .checked_add(sol_amount)
184        .ok_or(QuoteError::MathOverflow)?;
185    mul_div_u128(sol_amount, v_base, denom)
186}
187
188/// Inverse of [`sell_quote`]: given a desired SOL output, how many tokens
189/// must be sold. `out = sol_amount * pool_base / (pool_quote - sol_amount)`.
190///
191/// Returns [`QuoteError::MathOverflow`] if `sol_amount >= pool_quote_token_reserves`.
192pub fn sell_token_quote_with_sol(
193    pool_base_token_reserves: u64,
194    pool_quote_token_reserves: u64,
195    sol_amount: u64,
196) -> QuoteResult<u128> {
197    let sol_amount = u128::from(sol_amount);
198    let v_quote = u128::from(pool_quote_token_reserves);
199    let v_base = u128::from(pool_base_token_reserves);
200    let denom = v_quote
201        .checked_sub(sol_amount)
202        .ok_or(QuoteError::MathOverflow)?;
203    mul_div_u128(sol_amount, v_base, denom)
204}
205
206/// Validate that the AMM pool's current market cap is within
207/// `target_market_cap ± slippage_bps`. Uses the fixed [`TOKEN_SUPPLY`] for
208/// market-cap derivation: `mcap = TOKEN_SUPPLY * pool_quote / pool_base`.
209pub fn validate_market_cap(
210    pool_base_token_reserves: u64,
211    pool_quote_token_reserves: u64,
212    target_market_cap: u128,
213    slippage_bps: u16,
214) -> QuoteResult<()> {
215    let v_quote = u128::from(pool_quote_token_reserves);
216    let v_base = u128::from(pool_base_token_reserves);
217
218    let current = mul_div_u128(TOKEN_SUPPLY, v_quote, v_base)?;
219
220    let (min, max) =
221        slippage_bounds(target_market_cap, slippage_bps).ok_or(QuoteError::MathOverflow)?;
222
223    if current < min || current > max {
224        return Err(QuoteError::SlippageExceeded);
225    }
226    Ok(())
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232
233    const POOL_QUOTE: u64 = 100_000_000_000;
234    const POOL_BASE: u64 = 800_000_000_000_000;
235
236    #[test]
237    fn sell_quote_matches_constant_product() {
238        let amount: u64 = 1_000_000_000_000;
239        let out = sell_quote(POOL_BASE, POOL_QUOTE, amount).unwrap();
240        let expected =
241            (amount as u128) * (POOL_QUOTE as u128) / ((POOL_BASE as u128) + amount as u128);
242        assert_eq!(out, expected);
243    }
244
245    #[test]
246    fn buy_and_sell_token_quote_with_sol_use_correct_denominators() {
247        let sol_in: u64 = 1_000_000_000;
248        let bought = buy_token_quote_with_sol(POOL_BASE, POOL_QUOTE, sol_in).unwrap();
249        let expected =
250            (sol_in as u128) * (POOL_BASE as u128) / ((POOL_QUOTE as u128) + sol_in as u128);
251        assert_eq!(bought, expected);
252
253        let inv = sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, sol_in).unwrap();
254        let expected_inv =
255            (sol_in as u128) * (POOL_BASE as u128) / ((POOL_QUOTE as u128) - sol_in as u128);
256        assert_eq!(inv, expected_inv);
257    }
258
259    #[test]
260    fn sell_token_quote_overflow_when_sol_exceeds_reserve() {
261        assert_eq!(
262            sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, POOL_QUOTE),
263            Err(QuoteError::MathOverflow)
264        );
265        assert_eq!(
266            sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, POOL_QUOTE + 1),
267            Err(QuoteError::MathOverflow)
268        );
269    }
270
271    #[test]
272    fn validate_market_cap_passes_within_envelope() {
273        let current = TOKEN_SUPPLY * (POOL_QUOTE as u128) / (POOL_BASE as u128);
274        validate_market_cap(POOL_BASE, POOL_QUOTE, current, 0).unwrap();
275        validate_market_cap(POOL_BASE, POOL_QUOTE, current * 99 / 100, 200).unwrap();
276    }
277
278    #[test]
279    fn validate_market_cap_fails_outside_envelope() {
280        let current = TOKEN_SUPPLY * (POOL_QUOTE as u128) / (POOL_BASE as u128);
281        assert_eq!(
282            validate_market_cap(POOL_BASE, POOL_QUOTE, current * 95 / 100, 100),
283            Err(QuoteError::SlippageExceeded)
284        );
285    }
286}