1use 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
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;
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
86pub 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
122pub 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
158pub 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
172pub 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
188pub 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
206pub 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}