1use 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
27pub 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
50pub 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;
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
88pub fn buy_base_input(ctx: &AmmContext<'_>, base_out: u64) -> QuoteResult<BuyBaseInputResult> {
90 ctx.check_reserves()?;
91 if base_out >= ctx.base_reserve {
92 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
130pub 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
170pub 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
197pub 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
219pub 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
243pub 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 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}