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::pump_amm::{FeeConfig, GlobalConfig};
10
11pub struct BuyQuoteInputResult {
12    pub base_amount_out: u64,
13    pub effective_quote: u64,
14}
15
16pub struct BuyBaseInputResult {
17    pub total_quote_in: u64,
18    pub raw_quote_in: u64,
19}
20
21pub struct SellBaseInputResult {
22    pub final_quote_out: u64,
23    pub raw_quote_out: u64,
24}
25
26/// Common AMM trade context. `pool_creator` is the pool's anchor `creator`
27/// field; `coin_creator` is the per-coin creator that receives the
28/// coin-creator fee slice (set to `Pubkey::default()` to skip the slice).
29pub struct AmmContext<'a> {
30    pub global_config: &'a GlobalConfig,
31    pub fee_config: Option<&'a FeeConfig>,
32    pub base_mint: &'a Pubkey,
33    pub pool_creator: &'a Pubkey,
34    pub coin_creator: &'a Pubkey,
35    pub base_reserve: u64,
36    pub quote_reserve: u64,
37    pub base_mint_supply: u64,
38}
39
40impl AmmContext<'_> {
41    fn check_reserves(&self) -> QuoteResult<()> {
42        if self.base_reserve == 0 || self.quote_reserve == 0 {
43            return Err(QuoteError::EmptyReserves);
44        }
45        Ok(())
46    }
47}
48
49/// AMM buy: caller specifies SOL input, gets tokens out.
50pub fn buy_quote_input(ctx: &AmmContext<'_>, quote_in: u64) -> QuoteResult<BuyQuoteInputResult> {
51    ctx.check_reserves()?;
52
53    let AmmFeeBps {
54        lp_fee_bps,
55        protocol_fee_bps,
56        creator_fee_bps,
57    } = compute_amm_fee_bps(
58        ctx.global_config,
59        ctx.fee_config,
60        ctx.base_mint,
61        ctx.pool_creator,
62        ctx.base_mint_supply,
63        ctx.base_reserve,
64        ctx.quote_reserve,
65    );
66    let coin_creator_bps = if *ctx.coin_creator == Pubkey::default() {
67        0
68    } else {
69        creator_fee_bps
70    };
71
72    let total_fee_bps = lp_fee_bps + protocol_fee_bps + coin_creator_bps;
73    let denom = 10_000u128 + total_fee_bps as u128;
74
75    let effective_quote = (quote_in as u128) * 10_000 / denom;
76    let base_out = (ctx.base_reserve as u128) * effective_quote
77        / ((ctx.quote_reserve as u128) + effective_quote);
78
79    Ok(BuyQuoteInputResult {
80        base_amount_out: base_out as u64,
81        effective_quote: effective_quote as u64,
82    })
83}
84
85/// AMM buy: caller specifies desired tokens out, gets total SOL cost.
86pub fn buy_base_input(ctx: &AmmContext<'_>, base_out: u64) -> QuoteResult<BuyBaseInputResult> {
87    ctx.check_reserves()?;
88    if base_out >= ctx.base_reserve {
89        return Err(QuoteError::BaseOutExceedsReserve);
90    }
91
92    let numerator = (ctx.quote_reserve as u128) * (base_out as u128);
93    let denominator = (ctx.base_reserve as u128) - (base_out as u128);
94    let raw_quote = ceil_div(numerator, denominator);
95
96    let AmmFeeBps {
97        lp_fee_bps,
98        protocol_fee_bps,
99        creator_fee_bps,
100    } = compute_amm_fee_bps(
101        ctx.global_config,
102        ctx.fee_config,
103        ctx.base_mint,
104        ctx.pool_creator,
105        ctx.base_mint_supply,
106        ctx.base_reserve,
107        ctx.quote_reserve,
108    );
109
110    let lp = fee_amount(raw_quote, lp_fee_bps);
111    let protocol = fee_amount(raw_quote, protocol_fee_bps);
112    let coin_creator = creator_fee_amount(ctx.coin_creator, raw_quote, creator_fee_bps);
113    let total = raw_quote + lp + protocol + coin_creator;
114
115    Ok(BuyBaseInputResult {
116        total_quote_in: total as u64,
117        raw_quote_in: raw_quote as u64,
118    })
119}
120
121/// AMM sell: caller specifies tokens in, gets net SOL out.
122pub fn sell_base_input(ctx: &AmmContext<'_>, base_in: u64) -> QuoteResult<SellBaseInputResult> {
123    ctx.check_reserves()?;
124
125    let raw_quote = (ctx.quote_reserve as u128) * (base_in as u128)
126        / ((ctx.base_reserve as u128) + (base_in as u128));
127
128    let AmmFeeBps {
129        lp_fee_bps,
130        protocol_fee_bps,
131        creator_fee_bps,
132    } = compute_amm_fee_bps(
133        ctx.global_config,
134        ctx.fee_config,
135        ctx.base_mint,
136        ctx.pool_creator,
137        ctx.base_mint_supply,
138        ctx.base_reserve,
139        ctx.quote_reserve,
140    );
141
142    let lp = fee_amount(raw_quote, lp_fee_bps);
143    let protocol = fee_amount(raw_quote, protocol_fee_bps);
144    let coin_creator = creator_fee_amount(ctx.coin_creator, raw_quote, creator_fee_bps);
145    let total_fee = lp + protocol + coin_creator;
146    if raw_quote < total_fee {
147        return Err(QuoteError::FeesExceedOutput);
148    }
149    let final_quote = raw_quote - total_fee;
150
151    Ok(SellBaseInputResult {
152        final_quote_out: final_quote as u64,
153        raw_quote_out: raw_quote as u64,
154    })
155}
156
157/// Constant-product sell quote, fees not applied.
158/// `out = amount * pool_quote / (pool_base + amount)`.
159pub fn sell_quote(
160    pool_base_token_reserves: u64,
161    pool_quote_token_reserves: u64,
162    amount: u64,
163) -> QuoteResult<u128> {
164    let amount = u128::from(amount);
165    let v_quote = u128::from(pool_quote_token_reserves);
166    let v_base = u128::from(pool_base_token_reserves);
167    let denom = v_base.checked_add(amount).ok_or(QuoteError::MathOverflow)?;
168    mul_div_u128(amount, v_quote, denom)
169}
170
171/// Pure constant-product buy quote on an AMM pool, no fees applied.
172/// `out = sol_amount * pool_base / (pool_quote + sol_amount)`.
173pub fn buy_token_quote_with_sol(
174    pool_base_token_reserves: u64,
175    pool_quote_token_reserves: u64,
176    sol_amount: u64,
177) -> QuoteResult<u128> {
178    let sol_amount = u128::from(sol_amount);
179    let v_quote = u128::from(pool_quote_token_reserves);
180    let v_base = u128::from(pool_base_token_reserves);
181    let denom = v_quote
182        .checked_add(sol_amount)
183        .ok_or(QuoteError::MathOverflow)?;
184    mul_div_u128(sol_amount, v_base, denom)
185}
186
187/// Inverse of [`sell_quote`]: given a desired SOL output, how many tokens
188/// must be sold. `out = sol_amount * pool_base / (pool_quote - sol_amount)`.
189///
190/// Returns [`QuoteError::MathOverflow`] if `sol_amount >= pool_quote_token_reserves`.
191pub fn sell_token_quote_with_sol(
192    pool_base_token_reserves: u64,
193    pool_quote_token_reserves: u64,
194    sol_amount: u64,
195) -> QuoteResult<u128> {
196    let sol_amount = u128::from(sol_amount);
197    let v_quote = u128::from(pool_quote_token_reserves);
198    let v_base = u128::from(pool_base_token_reserves);
199    let denom = v_quote
200        .checked_sub(sol_amount)
201        .ok_or(QuoteError::MathOverflow)?;
202    mul_div_u128(sol_amount, v_base, denom)
203}
204
205/// Validate that the AMM pool's current market cap is within
206/// `target_market_cap ± slippage_bps`. Uses the fixed [`TOKEN_SUPPLY`] for
207/// market-cap derivation: `mcap = TOKEN_SUPPLY * pool_quote / pool_base`.
208pub fn validate_market_cap(
209    pool_base_token_reserves: u64,
210    pool_quote_token_reserves: u64,
211    target_market_cap: u128,
212    slippage_bps: u16,
213) -> QuoteResult<()> {
214    let v_quote = u128::from(pool_quote_token_reserves);
215    let v_base = u128::from(pool_base_token_reserves);
216
217    let current = mul_div_u128(TOKEN_SUPPLY, v_quote, v_base)?;
218
219    let (min, max) =
220        slippage_bounds(target_market_cap, slippage_bps).ok_or(QuoteError::MathOverflow)?;
221
222    if current < min || current > max {
223        return Err(QuoteError::SlippageExceeded);
224    }
225    Ok(())
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    const POOL_QUOTE: u64 = 100_000_000_000;
233    const POOL_BASE: u64 = 800_000_000_000_000;
234
235    #[test]
236    fn sell_quote_matches_constant_product() {
237        let amount: u64 = 1_000_000_000_000;
238        let out = sell_quote(POOL_BASE, POOL_QUOTE, amount).unwrap();
239        let expected =
240            (amount as u128) * (POOL_QUOTE as u128) / ((POOL_BASE as u128) + amount as u128);
241        assert_eq!(out, expected);
242    }
243
244    #[test]
245    fn buy_and_sell_token_quote_with_sol_use_correct_denominators() {
246        let sol_in: u64 = 1_000_000_000;
247        let bought = buy_token_quote_with_sol(POOL_BASE, POOL_QUOTE, sol_in).unwrap();
248        let expected =
249            (sol_in as u128) * (POOL_BASE as u128) / ((POOL_QUOTE as u128) + sol_in as u128);
250        assert_eq!(bought, expected);
251
252        let inv = sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, sol_in).unwrap();
253        let expected_inv =
254            (sol_in as u128) * (POOL_BASE as u128) / ((POOL_QUOTE as u128) - sol_in as u128);
255        assert_eq!(inv, expected_inv);
256    }
257
258    #[test]
259    fn sell_token_quote_overflow_when_sol_exceeds_reserve() {
260        assert_eq!(
261            sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, POOL_QUOTE),
262            Err(QuoteError::MathOverflow)
263        );
264        assert_eq!(
265            sell_token_quote_with_sol(POOL_BASE, POOL_QUOTE, POOL_QUOTE + 1),
266            Err(QuoteError::MathOverflow)
267        );
268    }
269
270    #[test]
271    fn validate_market_cap_passes_within_envelope() {
272        let current = TOKEN_SUPPLY * (POOL_QUOTE as u128) / (POOL_BASE as u128);
273        validate_market_cap(POOL_BASE, POOL_QUOTE, current, 0).unwrap();
274        validate_market_cap(POOL_BASE, POOL_QUOTE, current * 99 / 100, 200).unwrap();
275    }
276
277    #[test]
278    fn validate_market_cap_fails_outside_envelope() {
279        let current = TOKEN_SUPPLY * (POOL_QUOTE as u128) / (POOL_BASE as u128);
280        assert_eq!(
281            validate_market_cap(POOL_BASE, POOL_QUOTE, current * 95 / 100, 100),
282            Err(QuoteError::SlippageExceeded)
283        );
284    }
285}