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::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
26pub 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
49pub 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
85pub 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
121pub 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
157pub 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
171pub 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
187pub 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
205pub 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}