Skip to main content

sendfun_sdk/math/
amm.rs

1use std::fmt;
2
3const BPS_DIVISOR: u128 = 10_000;
4
5/// Bps-scale denominators only. The `+ denominator` step can overflow.
6fn ceil_div(numerator: u128, denominator: u128) -> Option<u128> {
7	numerator
8		.checked_add(denominator)?
9		.checked_sub(1)?
10		.checked_div(denominator)
11}
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub struct AmmInput {
15	pub quote_reserves: u64,
16	pub base_reserves: u64,
17	pub amount: u64,
18	pub fee_bps: u16,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub struct TradeQuote {
23	pub base_amount: u64,
24	pub quote_amount: u64,
25	/// Platform fee, in quote units.
26	pub fee: u64,
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum AmmError {
31	InsufficientLiquidity,
32	InvalidAmount,
33	Overflow,
34}
35
36impl fmt::Display for AmmError {
37	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38		match self {
39			Self::InsufficientLiquidity => write!(f, "Insufficient liquidity"),
40			Self::InvalidAmount => write!(f, "Invalid amount"),
41			Self::Overflow => write!(f, "Arithmetic overflow"),
42		}
43	}
44}
45
46impl std::error::Error for AmmError {}
47
48/// A Token-2022 transfer fee for one epoch. Use the fee of the landing epoch.
49/// A stale fee gives a wrong price.
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct MintFee {
52	/// 0 to `10_000`.
53	pub bps: u16,
54	pub maximum_fee: u64,
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum QuoteError {
59	Amm(AmmError),
60	InvalidTransferFee,
61	/// No transfer amount lands exactly the requested amount.
62	TransferFeeNotSettleable,
63}
64
65impl From<AmmError> for QuoteError {
66	fn from(error: AmmError) -> Self {
67		Self::Amm(error)
68	}
69}
70
71impl fmt::Display for QuoteError {
72	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73		match self {
74			Self::Amm(error) => error.fmt(f),
75			Self::InvalidTransferFee => {
76				write!(f, "Transfer fee rate above 10000 bps")
77			}
78			Self::TransferFeeNotSettleable => {
79				write!(f, "Transfer fee not settleable")
80			}
81		}
82	}
83}
84
85impl std::error::Error for QuoteError {}
86
87fn validate_mint_fee(fee: MintFee) -> Result<(), QuoteError> {
88	if u128::from(fee.bps) > BPS_DIVISOR {
89		return Err(QuoteError::InvalidTransferFee);
90	}
91	Ok(())
92}
93
94/// The transfer fee on `amount`, as SPL calculates it: rounds up, then caps at
95/// `maximum_fee`. `0` for `None`.
96pub fn fee_on(amount: u64, fee: Option<MintFee>) -> Result<u64, QuoteError> {
97	let Some(fee) = fee else {
98		return Ok(0);
99	};
100	validate_mint_fee(fee)?;
101	if amount == 0 || fee.bps == 0 {
102		return Ok(0);
103	}
104
105	let numerator = u128::from(amount)
106		.checked_mul(u128::from(fee.bps))
107		.ok_or(AmmError::Overflow)?;
108	let raw = ceil_div(numerator, BPS_DIVISOR).ok_or(AmmError::Overflow)?;
109	let raw = u64::try_from(raw).map_err(|_| AmmError::Overflow)?;
110
111	Ok(raw.min(fee.maximum_fee))
112}
113
114pub fn amount_after_fee(
115	amount: u64,
116	fee: Option<MintFee>,
117) -> Result<u64, QuoteError> {
118	amount
119		.checked_sub(fee_on(amount, fee)?)
120		.ok_or_else(|| AmmError::Overflow.into())
121}
122
123/// Mirrors SPL's `TransferFee::calculate_pre_fee_amount` line for line.
124fn pre_fee_amount(amount: u64, fee: MintFee) -> Option<u64> {
125	let bps = u128::from(fee.bps);
126	match (bps, amount) {
127		(0, _) => Some(amount),
128		// `gross_up` never gets here. Kept to match SPL.
129		(_, 0) => Some(0),
130		(BPS_DIVISOR, _) => amount.checked_add(fee.maximum_fee),
131		_ => {
132			let numerator = u128::from(amount).checked_mul(BPS_DIVISOR)?;
133			let denominator = BPS_DIVISOR.checked_sub(bps)?;
134			let raw_pre_fee_amount = ceil_div(numerator, denominator)?;
135
136			if raw_pre_fee_amount.checked_sub(u128::from(amount))?
137				>= u128::from(fee.maximum_fee)
138			{
139				amount.checked_add(fee.maximum_fee)
140			} else {
141				u64::try_from(raw_pre_fee_amount).ok()
142			}
143		}
144	}
145}
146
147/// The amount to send so that exactly `amount` lands after the transfer fee.
148/// Fails with `TransferFeeNotSettleable` when no sent amount lands exactly
149/// `amount`.
150pub fn gross_up(amount: u64, fee: Option<MintFee>) -> Result<u64, QuoteError> {
151	let Some(schedule) = fee else {
152		return Ok(amount);
153	};
154	validate_mint_fee(schedule)?;
155	if amount == 0 {
156		return Ok(0);
157	}
158
159	let pre_fee = pre_fee_amount(amount, schedule)
160		.ok_or(QuoteError::TransferFeeNotSettleable)?;
161	let implied_fee = fee_on(pre_fee, fee)?;
162	let gross = amount
163		.checked_add(implied_fee)
164		.ok_or(QuoteError::TransferFeeNotSettleable)?;
165
166	if fee_on(gross, fee)? != implied_fee {
167		return Err(QuoteError::TransferFeeNotSettleable);
168	}
169
170	Ok(gross)
171}
172
173/// `amm.amount` is the user's amount: the quote that the user sends for
174/// [`buy_exact_in_with_fees`], the base that the user receives for
175/// [`buy_exact_out_with_fees`].
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub struct BuyArgs {
178	pub amm: AmmInput,
179	pub quote_fee: Option<MintFee>,
180	pub base_fee: Option<MintFee>,
181	/// Launchpad: `bonding_curve.real_base_reserves`. `None` for a DEX pool.
182	pub base_reserve_cap: Option<u64>,
183}
184
185/// `amm.amount` is the user's amount: the base that the user sends for
186/// [`sell_exact_in_with_fees`], the quote that the user receives for
187/// [`sell_exact_out_with_fees`].
188#[derive(Debug, Clone, Copy, PartialEq, Eq)]
189pub struct SellArgs {
190	pub amm: AmmInput,
191	pub quote_fee: Option<MintFee>,
192	pub base_fee: Option<MintFee>,
193}
194
195/// `base_amount` and `quote_amount` are the transfer amounts. For the user's
196/// amounts, read `base_to_user` and `quote_from_user`. Do not calculate them
197/// from the transfer amounts.
198#[derive(Debug, Clone, Copy, PartialEq, Eq)]
199pub struct BuyQuote {
200	pub base_amount: u64,
201	pub quote_amount: u64,
202	/// Platform fee, in quote units, on the quote that reaches the vault.
203	pub fee: u64,
204	pub base_transfer_fee: u64,
205	pub quote_transfer_fee: u64,
206	/// The base that the buyer receives, after the base mint's transfer fee.
207	pub base_to_user: u64,
208	/// The quote that the buyer sends, with the transfer fee. Equals
209	/// `quote_amount`.
210	pub quote_from_user: u64,
211}
212
213/// Same field rules as [`BuyQuote`].
214#[derive(Debug, Clone, Copy, PartialEq, Eq)]
215pub struct SellQuote {
216	pub base_amount: u64,
217	pub quote_amount: u64,
218	/// Platform fee, in quote units, on the quote output of the AMM before the
219	/// fee.
220	pub fee: u64,
221	pub base_transfer_fee: u64,
222	pub quote_transfer_fee: u64,
223	/// The base that the seller sends, with the transfer fee. Equals
224	/// `base_amount`.
225	pub base_from_user: u64,
226	/// The quote that the seller receives, after the quote mint's transfer fee.
227	pub quote_to_user: u64,
228}
229
230struct BuyLegs {
231	base_amount: u64,
232	quote_amount: u64,
233	fee: u64,
234	quote_transfer_fee: u64,
235	base_fee: Option<MintFee>,
236}
237
238fn buy_quote(legs: BuyLegs) -> Result<BuyQuote, QuoteError> {
239	let base_transfer_fee = fee_on(legs.base_amount, legs.base_fee)?;
240	Ok(BuyQuote {
241		base_amount: legs.base_amount,
242		quote_amount: legs.quote_amount,
243		fee: legs.fee,
244		base_transfer_fee,
245		quote_transfer_fee: legs.quote_transfer_fee,
246		base_to_user: legs
247			.base_amount
248			.checked_sub(base_transfer_fee)
249			.ok_or(AmmError::Overflow)?,
250		quote_from_user: legs.quote_amount,
251	})
252}
253
254/// Set `min_amount_out` to `calculate_slippage_down(q.base_to_user, bps)`.
255pub fn buy_exact_in_with_fees(args: BuyArgs) -> Result<BuyQuote, QuoteError> {
256	let quote_from_user = args.amm.amount;
257	if quote_from_user == 0 {
258		return Err(AmmError::InvalidAmount.into());
259	}
260
261	let quote_into_vault = amount_after_fee(quote_from_user, args.quote_fee)?;
262	if quote_into_vault == 0 {
263		return Err(AmmError::InvalidAmount.into());
264	}
265
266	let uncapped = buy_exact_in(AmmInput {
267		amount: quote_into_vault,
268		..args.amm
269	})?;
270
271	// Above the cap, price `cap` as exact-out. The user pays only for `cap`, at
272	// or below the offer.
273	let Some(cap) = args
274		.base_reserve_cap
275		.filter(|cap| uncapped.base_amount > *cap)
276	else {
277		return buy_quote(BuyLegs {
278			base_amount: uncapped.base_amount,
279			quote_amount: quote_from_user,
280			fee: uncapped.fee,
281			quote_transfer_fee: quote_from_user
282				.checked_sub(quote_into_vault)
283				.ok_or(AmmError::Overflow)?,
284			base_fee: args.base_fee,
285		});
286	};
287
288	let capped = buy_exact_out(AmmInput {
289		amount: cap,
290		..args.amm
291	})?;
292	let quote_from_user = gross_up(capped.quote_amount, args.quote_fee)?;
293
294	buy_quote(BuyLegs {
295		base_amount: cap,
296		quote_amount: quote_from_user,
297		fee: capped.fee,
298		quote_transfer_fee: quote_from_user
299			.checked_sub(capped.quote_amount)
300			.ok_or(AmmError::Overflow)?,
301		base_fee: args.base_fee,
302	})
303}
304
305/// Set `max_amount_in` to `calculate_slippage_up(q.quote_from_user, bps)`.
306pub fn buy_exact_out_with_fees(args: BuyArgs) -> Result<BuyQuote, QuoteError> {
307	let base_to_user = args.amm.amount;
308	if base_to_user == 0 {
309		return Err(AmmError::InvalidAmount.into());
310	}
311
312	let base_out_of_vault = gross_up(base_to_user, args.base_fee)?;
313	let base_amount = match args.base_reserve_cap {
314		// The program grosses the buyer's net up again. That gross can be
315		// below the cap.
316		Some(cap) if base_out_of_vault > cap => {
317			gross_up(amount_after_fee(cap, args.base_fee)?, args.base_fee)?
318		}
319		_ => base_out_of_vault,
320	};
321
322	let bought = buy_exact_out(AmmInput {
323		amount: base_amount,
324		..args.amm
325	})?;
326	let quote_from_user = gross_up(bought.quote_amount, args.quote_fee)?;
327
328	buy_quote(BuyLegs {
329		base_amount,
330		quote_amount: quote_from_user,
331		fee: bought.fee,
332		quote_transfer_fee: quote_from_user
333			.checked_sub(bought.quote_amount)
334			.ok_or(AmmError::Overflow)?,
335		base_fee: args.base_fee,
336	})
337}
338
339/// Set `min_amount_out` to `calculate_slippage_down(q.quote_to_user, bps)`.
340pub fn sell_exact_in_with_fees(
341	args: SellArgs,
342) -> Result<SellQuote, QuoteError> {
343	let base_from_user = args.amm.amount;
344	if base_from_user == 0 {
345		return Err(AmmError::InvalidAmount.into());
346	}
347
348	let base_into_vault = amount_after_fee(base_from_user, args.base_fee)?;
349	if base_into_vault == 0 {
350		return Err(AmmError::InvalidAmount.into());
351	}
352
353	let sold = sell_exact_in(AmmInput {
354		amount: base_into_vault,
355		..args.amm
356	})?;
357	let quote_transfer_fee = fee_on(sold.quote_amount, args.quote_fee)?;
358
359	Ok(SellQuote {
360		base_amount: base_from_user,
361		quote_amount: sold.quote_amount,
362		fee: sold.fee,
363		base_transfer_fee: base_from_user
364			.checked_sub(base_into_vault)
365			.ok_or(AmmError::Overflow)?,
366		quote_transfer_fee,
367		base_from_user,
368		quote_to_user: sold
369			.quote_amount
370			.checked_sub(quote_transfer_fee)
371			.ok_or(AmmError::Overflow)?,
372	})
373}
374
375/// Set `max_amount_in` to `calculate_slippage_up(q.base_from_user, bps)`.
376pub fn sell_exact_out_with_fees(
377	args: SellArgs,
378) -> Result<SellQuote, QuoteError> {
379	let quote_to_user = args.amm.amount;
380	if quote_to_user == 0 {
381		return Err(AmmError::InvalidAmount.into());
382	}
383
384	let quote_out_of_vault = gross_up(quote_to_user, args.quote_fee)?;
385	let sold = sell_exact_out(AmmInput {
386		amount: quote_out_of_vault,
387		..args.amm
388	})?;
389	let base_from_user = gross_up(sold.base_amount, args.base_fee)?;
390
391	Ok(SellQuote {
392		base_amount: base_from_user,
393		quote_amount: quote_out_of_vault,
394		fee: sold.fee,
395		base_transfer_fee: base_from_user
396			.checked_sub(sold.base_amount)
397			.ok_or(AmmError::Overflow)?,
398		quote_transfer_fee: quote_out_of_vault
399			.checked_sub(quote_to_user)
400			.ok_or(AmmError::Overflow)?,
401		base_from_user,
402		quote_to_user,
403	})
404}
405
406/// Prices a buy of exactly `input.amount` base units. Set `max_amount_in` to
407/// `calculate_slippage_up(q.quote_amount, bps)`.
408pub fn buy_exact_out(input: AmmInput) -> Result<TradeQuote, AmmError> {
409	let base_amount = input.amount;
410	let quote_before_fee = calculate_input_for_output(
411		input.quote_reserves,
412		input.base_reserves,
413		base_amount,
414	)?;
415
416	// The gross quote rounds up. The buyer pays the remainder.
417	let fee_bps = u128::from(input.fee_bps);
418	let quote = u128::from(quote_before_fee);
419
420	let divisor = BPS_DIVISOR
421		.checked_sub(fee_bps)
422		.ok_or(AmmError::InvalidAmount)?;
423	if divisor == 0 {
424		return Err(AmmError::InvalidAmount);
425	}
426
427	let numerator = quote.checked_mul(BPS_DIVISOR).ok_or(AmmError::Overflow)?;
428	let total_quote = ceil_div(numerator, divisor).ok_or(AmmError::Overflow)?;
429
430	let fee_amount =
431		total_quote.checked_sub(quote).ok_or(AmmError::Overflow)?;
432
433	let total_quote =
434		u64::try_from(total_quote).map_err(|_| AmmError::Overflow)?;
435	let fee_amount =
436		u64::try_from(fee_amount).map_err(|_| AmmError::Overflow)?;
437
438	Ok(TradeQuote {
439		base_amount,
440		quote_amount: total_quote,
441		fee: fee_amount,
442	})
443}
444
445/// Prices a buy that spends exactly `input.amount` quote units. Set
446/// `min_amount_out` to `calculate_slippage_down(q.base_amount, bps)`.
447pub fn buy_exact_in(input: AmmInput) -> Result<TradeQuote, AmmError> {
448	let quote_amount = input.amount;
449	if quote_amount == 0 {
450		return Err(AmmError::InvalidAmount);
451	}
452
453	// The net quote rounds down. The remainder is the fee.
454	let fee_bps_128 = u128::from(input.fee_bps);
455	let quote_128 = u128::from(quote_amount);
456
457	let net_factor = BPS_DIVISOR
458		.checked_sub(fee_bps_128)
459		.ok_or(AmmError::InvalidAmount)?;
460	if net_factor == 0 {
461		return Err(AmmError::InvalidAmount);
462	}
463
464	let net_quote_128 = quote_128
465		.checked_mul(net_factor)
466		.ok_or(AmmError::Overflow)?
467		.checked_div(BPS_DIVISOR)
468		.ok_or(AmmError::Overflow)?;
469	let net_quote =
470		u64::try_from(net_quote_128).map_err(|_| AmmError::Overflow)?;
471	if net_quote == 0 {
472		return Err(AmmError::InvalidAmount);
473	}
474
475	let fee = quote_amount
476		.checked_sub(net_quote)
477		.ok_or(AmmError::Overflow)?;
478
479	let base_amount =
480		calculate_output(input.quote_reserves, input.base_reserves, net_quote)?;
481
482	Ok(TradeQuote {
483		base_amount,
484		quote_amount,
485		fee,
486	})
487}
488
489/// Prices a sale of exactly `input.amount` base units. Set `min_amount_out` to
490/// `calculate_slippage_down(q.quote_amount, bps)`.
491pub fn sell_exact_in(input: AmmInput) -> Result<TradeQuote, AmmError> {
492	if u128::from(input.fee_bps) > BPS_DIVISOR {
493		return Err(AmmError::InvalidAmount);
494	}
495
496	let base_amount = input.amount;
497	let quote_before_fee = calculate_output(
498		input.base_reserves,
499		input.quote_reserves,
500		base_amount,
501	)?;
502
503	// The fee rounds up.
504	let numerator = u128::from(quote_before_fee)
505		.checked_mul(u128::from(input.fee_bps))
506		.ok_or(AmmError::Overflow)?;
507	let fee_amount =
508		ceil_div(numerator, BPS_DIVISOR).ok_or(AmmError::Overflow)?;
509	let fee_amount =
510		u64::try_from(fee_amount).map_err(|_| AmmError::Overflow)?;
511	let quote_after_fee = quote_before_fee
512		.checked_sub(fee_amount)
513		.ok_or(AmmError::Overflow)?;
514
515	Ok(TradeQuote {
516		base_amount,
517		quote_amount: quote_after_fee,
518		fee: fee_amount,
519	})
520}
521
522/// Prices a sale that gives exactly `input.amount` quote units after the fee.
523/// Set `max_amount_in` to `calculate_slippage_up(q.base_amount, bps)`.
524pub fn sell_exact_out(input: AmmInput) -> Result<TradeQuote, AmmError> {
525	let quote_amount = input.amount;
526	if quote_amount == 0 {
527		return Err(AmmError::InvalidAmount);
528	}
529
530	// The gross quote rounds up. The seller pays the remainder.
531	let fee_bps_128 = u128::from(input.fee_bps);
532	let quote_128 = u128::from(quote_amount);
533
534	let divisor = BPS_DIVISOR
535		.checked_sub(fee_bps_128)
536		.ok_or(AmmError::InvalidAmount)?;
537	if divisor == 0 {
538		return Err(AmmError::InvalidAmount);
539	}
540
541	let numerator = quote_128
542		.checked_mul(BPS_DIVISOR)
543		.ok_or(AmmError::Overflow)?;
544	let quote_before_fee =
545		ceil_div(numerator, divisor).ok_or(AmmError::Overflow)?;
546
547	let fee = quote_before_fee
548		.checked_sub(quote_128)
549		.ok_or(AmmError::Overflow)?;
550
551	let quote_before_fee =
552		u64::try_from(quote_before_fee).map_err(|_| AmmError::Overflow)?;
553	let fee = u64::try_from(fee).map_err(|_| AmmError::Overflow)?;
554
555	let base_amount = calculate_input_for_output(
556		input.base_reserves,
557		input.quote_reserves,
558		quote_before_fee,
559	)?;
560
561	Ok(TradeQuote {
562		base_amount,
563		quote_amount,
564		fee,
565	})
566}
567
568/// `amount` plus `slippage_bps`, rounded up. Use it for `max_amount_in`.
569pub fn calculate_slippage_up(
570	amount: u64,
571	slippage_bps: u16,
572) -> Result<u64, AmmError> {
573	let numerator = u128::from(amount)
574		.checked_mul(
575			BPS_DIVISOR
576				.checked_add(u128::from(slippage_bps))
577				.ok_or(AmmError::Overflow)?,
578		)
579		.ok_or(AmmError::Overflow)?;
580	let result = ceil_div(numerator, BPS_DIVISOR).ok_or(AmmError::Overflow)?;
581	u64::try_from(result).map_err(|_| AmmError::Overflow)
582}
583
584/// `amount` minus `slippage_bps`, rounded down. Use it for `min_amount_out`.
585pub fn calculate_slippage_down(
586	amount: u64,
587	slippage_bps: u16,
588) -> Result<u64, AmmError> {
589	let factor = BPS_DIVISOR
590		.checked_sub(u128::from(slippage_bps))
591		.ok_or(AmmError::InvalidAmount)?;
592	let numerator = u128::from(amount)
593		.checked_mul(factor)
594		.ok_or(AmmError::Overflow)?;
595	let result = numerator
596		.checked_div(BPS_DIVISOR)
597		.ok_or(AmmError::Overflow)?;
598	u64::try_from(result).map_err(|_| AmmError::Overflow)
599}
600
601/// Constant-product output for `amount_in`. Rounds down.
602pub fn calculate_output(
603	reserve_in: u64,
604	reserve_out: u64,
605	amount_in: u64,
606) -> Result<u64, AmmError> {
607	if reserve_in == 0 || reserve_out == 0 {
608		return Err(AmmError::InsufficientLiquidity);
609	}
610	if amount_in == 0 {
611		return Err(AmmError::InvalidAmount);
612	}
613
614	let k = u128::from(reserve_in)
615		.checked_mul(u128::from(reserve_out))
616		.ok_or(AmmError::Overflow)?;
617	let new_reserve_in = u128::from(reserve_in)
618		.checked_add(u128::from(amount_in))
619		.ok_or(AmmError::Overflow)?;
620
621	// Not `ceil_div`: `k + reserve_in + amount_in` reaches exactly `u128::MAX`
622	// with u64 inputs. A wider input makes the pre-add overflow.
623	let mut new_reserve_out =
624		k.checked_div(new_reserve_in).ok_or(AmmError::Overflow)?;
625	let remainder = k.checked_rem(new_reserve_in).ok_or(AmmError::Overflow)?;
626	if remainder > 0 {
627		new_reserve_out =
628			new_reserve_out.checked_add(1).ok_or(AmmError::Overflow)?;
629	}
630
631	let amount_out = u128::from(reserve_out)
632		.checked_sub(new_reserve_out)
633		.ok_or(AmmError::InsufficientLiquidity)?;
634	if amount_out == 0 {
635		return Err(AmmError::InsufficientLiquidity);
636	}
637
638	u64::try_from(amount_out).map_err(|_| AmmError::Overflow)
639}
640
641/// Constant-product input for `amount_out`. Rounds up.
642pub fn calculate_input_for_output(
643	reserve_in: u64,
644	reserve_out: u64,
645	amount_out: u64,
646) -> Result<u64, AmmError> {
647	if reserve_in == 0 || reserve_out == 0 {
648		return Err(AmmError::InsufficientLiquidity);
649	}
650	if amount_out == 0 || amount_out >= reserve_out {
651		return Err(AmmError::InvalidAmount);
652	}
653
654	let k = u128::from(reserve_in)
655		.checked_mul(u128::from(reserve_out))
656		.ok_or(AmmError::Overflow)?;
657	let new_reserve_out = u128::from(reserve_out)
658		.checked_sub(u128::from(amount_out))
659		.ok_or(AmmError::Overflow)?;
660
661	// Not `ceil_div`: same u64-width margin as `calculate_output`.
662	let mut new_reserve_in =
663		k.checked_div(new_reserve_out).ok_or(AmmError::Overflow)?;
664	let remainder = k.checked_rem(new_reserve_out).ok_or(AmmError::Overflow)?;
665	if remainder > 0 {
666		new_reserve_in =
667			new_reserve_in.checked_add(1).ok_or(AmmError::Overflow)?;
668	}
669
670	let amount_in = new_reserve_in
671		.checked_sub(u128::from(reserve_in))
672		.ok_or(AmmError::InsufficientLiquidity)?;
673	if amount_in == 0 {
674		return Err(AmmError::InsufficientLiquidity);
675	}
676
677	u64::try_from(amount_in).map_err(|_| AmmError::Overflow)
678}
679
680/// The TypeScript SDK tests use the same vectors. Change an expected value in
681/// both.
682#[cfg(test)]
683mod transfer_fee_tests {
684	use super::*;
685
686	const LAUNCH_QUOTE_RESERVES: u64 = 30_000_000_000;
687	const LAUNCH_BASE_RESERVES: u64 = 1_000_000_000_000_000;
688	const FEE_BPS: u16 = 100;
689
690	const QUOTE_20_BPS: MintFee = MintFee {
691		bps: 20,
692		maximum_fee: u64::MAX,
693	};
694
695	const BASE_100_BPS: MintFee = MintFee {
696		bps: 100,
697		maximum_fee: u64::MAX,
698	};
699
700	fn amm(amount: u64) -> AmmInput {
701		AmmInput {
702			quote_reserves: LAUNCH_QUOTE_RESERVES,
703			base_reserves: LAUNCH_BASE_RESERVES,
704			amount,
705			fee_bps: FEE_BPS,
706		}
707	}
708
709	#[track_caller]
710	fn assert_buy(quote: &BuyQuote, expected: [u64; 7]) {
711		assert_eq!(
712			[
713				quote.base_amount,
714				quote.quote_amount,
715				quote.fee,
716				quote.base_transfer_fee,
717				quote.quote_transfer_fee,
718				quote.base_to_user,
719				quote.quote_from_user,
720			],
721			expected,
722			"[base_amount, quote_amount, fee, base_transfer_fee, \
723			 quote_transfer_fee, base_to_user, quote_from_user]"
724		);
725	}
726
727	#[track_caller]
728	fn assert_sell(quote: &SellQuote, expected: [u64; 7]) {
729		assert_eq!(
730			[
731				quote.base_amount,
732				quote.quote_amount,
733				quote.fee,
734				quote.base_transfer_fee,
735				quote.quote_transfer_fee,
736				quote.base_from_user,
737				quote.quote_to_user,
738			],
739			expected,
740			"[base_amount, quote_amount, fee, base_transfer_fee, \
741			 quote_transfer_fee, base_from_user, quote_to_user]"
742		);
743	}
744
745	#[test]
746	fn a_zero_rate_never_moves_an_amount() {
747		let fee = Some(MintFee {
748			bps: 0,
749			maximum_fee: 0,
750		});
751		assert_eq!(fee_on(1_000_000, fee).unwrap(), 0);
752		assert_eq!(gross_up(1_000_000, fee).unwrap(), 1_000_000);
753	}
754
755	#[test]
756	fn the_fee_rounds_up() {
757		let fee = Some(QUOTE_20_BPS);
758		assert_eq!(fee_on(1, fee).unwrap(), 1);
759		assert_eq!(fee_on(10_000, fee).unwrap(), 20);
760		assert_eq!(fee_on(10_001, fee).unwrap(), 21);
761	}
762
763	#[test]
764	fn the_fee_is_zero_on_a_zero_amount() {
765		let fee = Some(MintFee {
766			bps: 10_000,
767			maximum_fee: u64::MAX,
768		});
769		assert_eq!(fee_on(0, fee).unwrap(), 0);
770	}
771
772	#[test]
773	fn the_maximum_fee_caps_after_rounding() {
774		let fee = Some(MintFee {
775			bps: 100,
776			maximum_fee: 500,
777		});
778		assert_eq!(fee_on(1_000_000, fee).unwrap(), 500);
779	}
780
781	#[test]
782	fn the_fee_stays_superadditive_across_a_split_at_the_cap() {
783		// 300 + 400 >= 500: a split booking can only collect more.
784		let fee = Some(MintFee {
785			bps: 100,
786			maximum_fee: 500,
787		});
788		assert_eq!(fee_on(30_000, fee).unwrap(), 300);
789		assert_eq!(fee_on(40_000, fee).unwrap(), 400);
790		assert_eq!(fee_on(70_000, fee).unwrap(), 500);
791	}
792
793	#[test]
794	fn a_mint_with_no_config_is_the_identity() {
795		assert_eq!(fee_on(1_000_000, None).unwrap(), 0);
796		assert_eq!(amount_after_fee(1_000_000, None).unwrap(), 1_000_000);
797		assert_eq!(gross_up(1_000_000, None).unwrap(), 1_000_000);
798	}
799
800	#[test]
801	fn an_out_of_range_rate_is_rejected() {
802		let fee = Some(MintFee {
803			bps: 10_001,
804			maximum_fee: 0,
805		});
806		assert_eq!(
807			fee_on(1_000, fee).unwrap_err(),
808			QuoteError::InvalidTransferFee
809		);
810	}
811
812	#[test]
813	fn amount_after_fee_deducts_what_fee_on_charges() {
814		assert_eq!(
815			amount_after_fee(1_000_000_000, Some(QUOTE_20_BPS)).unwrap(),
816			998_000_000
817		);
818		assert_eq!(
819			amount_after_fee(10_000_000_000_000, Some(BASE_100_BPS)).unwrap(),
820			9_900_000_000_000
821		);
822	}
823
824	#[test]
825	fn gross_up_is_a_no_op_at_zero() {
826		let fee = Some(MintFee {
827			bps: 100,
828			maximum_fee: u64::MAX,
829		});
830		assert_eq!(gross_up(0, fee).unwrap(), 0);
831	}
832
833	#[test]
834	fn gross_up_lands_the_exact_amount() {
835		for bps in [1_u16, 20, 100, 500, 3_333, 9_999] {
836			let fee = Some(MintFee {
837				bps,
838				maximum_fee: u64::MAX,
839			});
840			for amount in [1_u64, 7, 1_000, 999_983, 1_000_000_000] {
841				let gross = gross_up(amount, fee).unwrap();
842				let landed = gross - fee_on(gross, fee).unwrap();
843				assert_eq!(
844					landed, amount,
845					"bps {bps} amount {amount} grossed to {gross}"
846				);
847			}
848		}
849	}
850
851	#[test]
852	fn the_maximum_fee_clamps_the_gross_up() {
853		let fee = Some(MintFee {
854			bps: 100,
855			maximum_fee: 500,
856		});
857		assert_eq!(fee_on(1_000_000, fee).unwrap(), 500);
858		let gross = gross_up(1_000_000, fee).unwrap();
859		assert_eq!(gross, 1_000_500);
860		assert_eq!(gross - fee_on(gross, fee).unwrap(), 1_000_000);
861	}
862
863	#[test]
864	fn a_full_rate_with_a_finite_cap_still_settles() {
865		let fee = Some(MintFee {
866			bps: 10_000,
867			maximum_fee: 1_000,
868		});
869		let gross = gross_up(4_200, fee).unwrap();
870		assert_eq!(gross, 5_200);
871		assert_eq!(gross - fee_on(gross, fee).unwrap(), 4_200);
872	}
873
874	#[test]
875	fn a_full_rate_without_a_cap_is_rejected() {
876		let fee = Some(MintFee {
877			bps: 10_000,
878			maximum_fee: u64::MAX,
879		});
880		assert_eq!(
881			gross_up(1_000, fee).unwrap_err(),
882			QuoteError::TransferFeeNotSettleable
883		);
884	}
885
886	#[test]
887	fn a_gross_up_past_u64_is_rejected() {
888		let fee = Some(MintFee {
889			bps: 5_000,
890			maximum_fee: u64::MAX,
891		});
892		assert_eq!(
893			gross_up(u64::MAX, fee).unwrap_err(),
894			QuoteError::TransferFeeNotSettleable
895		);
896	}
897
898	#[test]
899	fn buy_exact_in_pins_the_users_leg_at_what_they_sent() {
900		let quote = buy_exact_in_with_fees(BuyArgs {
901			amm: amm(1_000_000_000),
902			quote_fee: None,
903			base_fee: None,
904			base_reserve_cap: None,
905		})
906		.unwrap();
907		assert_buy(
908			&quote,
909			[
910				31_945_788_964_181,
911				1_000_000_000,
912				10_000_000,
913				0,
914				0,
915				31_945_788_964_181,
916				1_000_000_000,
917			],
918		);
919	}
920
921	#[test]
922	fn buy_exact_in_prices_only_what_reached_the_vault() {
923		let quote = buy_exact_in_with_fees(BuyArgs {
924			amm: amm(1_000_000_000),
925			quote_fee: Some(QUOTE_20_BPS),
926			base_fee: None,
927			base_reserve_cap: None,
928		})
929		.unwrap();
930		assert_buy(
931			&quote,
932			[
933				31_883_934_501_139,
934				1_000_000_000,
935				9_980_000,
936				0,
937				2_000_000,
938				31_883_934_501_139,
939				1_000_000_000,
940			],
941		);
942	}
943
944	#[test]
945	fn buy_exact_in_nets_the_base_leg_down_for_the_buyer() {
946		let quote = buy_exact_in_with_fees(BuyArgs {
947			amm: amm(1_000_000_000),
948			quote_fee: None,
949			base_fee: Some(BASE_100_BPS),
950			base_reserve_cap: None,
951		})
952		.unwrap();
953		assert_buy(
954			&quote,
955			[
956				31_945_788_964_181,
957				1_000_000_000,
958				10_000_000,
959				319_457_889_642,
960				0,
961				31_626_331_074_539,
962				1_000_000_000,
963			],
964		);
965	}
966
967	#[test]
968	fn an_uncapped_fill_ignores_a_cap_it_stays_under() {
969		let quote = buy_exact_in_with_fees(BuyArgs {
970			amm: amm(1_000_000_000),
971			quote_fee: None,
972			base_fee: None,
973			base_reserve_cap: Some(u64::MAX),
974		})
975		.unwrap();
976		assert_buy(
977			&quote,
978			[
979				31_945_788_964_181,
980				1_000_000_000,
981				10_000_000,
982				0,
983				0,
984				31_945_788_964_181,
985				1_000_000_000,
986			],
987		);
988	}
989
990	#[test]
991	fn a_capped_fill_grosses_the_users_leg_up_from_the_curves() {
992		let plain = buy_exact_in_with_fees(BuyArgs {
993			amm: amm(1_000_000_000),
994			quote_fee: None,
995			base_fee: None,
996			base_reserve_cap: Some(1_000_000_000_000),
997		})
998		.unwrap();
999		assert_buy(
1000			&plain,
1001			[
1002				1_000_000_000_000,
1003				30_333_365,
1004				303_334,
1005				0,
1006				0,
1007				1_000_000_000_000,
1008				30_333_365,
1009			],
1010		);
1011
1012		let charged = buy_exact_in_with_fees(BuyArgs {
1013			amm: amm(1_000_000_000),
1014			quote_fee: Some(QUOTE_20_BPS),
1015			base_fee: None,
1016			base_reserve_cap: Some(1_000_000_000_000),
1017		})
1018		.unwrap();
1019		// The curve leg stays 30_333_365. Only the user's leg grows.
1020		assert_buy(
1021			&charged,
1022			[
1023				1_000_000_000_000,
1024				30_394_154,
1025				303_334,
1026				0,
1027				60_789,
1028				1_000_000_000_000,
1029				30_394_154,
1030			],
1031		);
1032	}
1033
1034	#[test]
1035	fn buy_exact_in_rejects_a_leg_the_mint_eats_whole() {
1036		let err = buy_exact_in_with_fees(BuyArgs {
1037			amm: amm(100),
1038			quote_fee: Some(MintFee {
1039				bps: 10_000,
1040				maximum_fee: u64::MAX,
1041			}),
1042			base_fee: None,
1043			base_reserve_cap: None,
1044		})
1045		.unwrap_err();
1046		assert_eq!(err, QuoteError::Amm(AmmError::InvalidAmount));
1047	}
1048
1049	#[test]
1050	fn buy_exact_out_moves_exactly_what_the_user_asked_for() {
1051		let quote = buy_exact_out_with_fees(BuyArgs {
1052			amm: amm(10_000_000_000_000),
1053			quote_fee: None,
1054			base_fee: None,
1055			base_reserve_cap: None,
1056		})
1057		.unwrap();
1058		assert_buy(
1059			&quote,
1060			[
1061				10_000_000_000_000,
1062				306_091_217,
1063				3_060_913,
1064				0,
1065				0,
1066				10_000_000_000_000,
1067				306_091_217,
1068			],
1069		);
1070	}
1071
1072	#[test]
1073	fn buy_exact_out_grosses_both_legs_up() {
1074		let quote = buy_exact_out_with_fees(BuyArgs {
1075			amm: amm(10_000_000_000_000),
1076			quote_fee: Some(QUOTE_20_BPS),
1077			base_fee: Some(BASE_100_BPS),
1078			base_reserve_cap: None,
1079		})
1080		.unwrap();
1081		assert_buy(
1082			&quote,
1083			[
1084				10_101_010_101_011,
1085				309_834_264,
1086				3_092_146,
1087				101_010_101_011,
1088				619_669,
1089				10_000_000_000_000,
1090				309_834_264,
1091			],
1092		);
1093	}
1094
1095	#[test]
1096	fn buy_exact_out_fills_only_up_to_the_cap() {
1097		let quote = buy_exact_out_with_fees(BuyArgs {
1098			amm: amm(10_000_000_000_000),
1099			quote_fee: None,
1100			base_fee: None,
1101			base_reserve_cap: Some(1_000_000_000_000),
1102		})
1103		.unwrap();
1104		assert_buy(
1105			&quote,
1106			[
1107				1_000_000_000_000,
1108				30_333_365,
1109				303_334,
1110				0,
1111				0,
1112				1_000_000_000_000,
1113				30_333_365,
1114			],
1115		);
1116	}
1117
1118	/// The cap and one unit less net the same amount.
1119	#[test]
1120	fn buy_exact_out_over_the_cap_sends_the_gross_of_what_the_cap_nets() {
1121		let quote = buy_exact_out_with_fees(BuyArgs {
1122			amm: amm(20_000_000_000_000),
1123			quote_fee: None,
1124			base_fee: Some(BASE_100_BPS),
1125			base_reserve_cap: Some(10_000_000_000_001),
1126		})
1127		.unwrap();
1128		assert_buy(
1129			&quote,
1130			[
1131				10_000_000_000_000,
1132				306_091_217,
1133				3_060_913,
1134				100_000_000_000,
1135				0,
1136				9_900_000_000_000,
1137				306_091_217,
1138			],
1139		);
1140	}
1141
1142	#[test]
1143	fn buy_exact_out_nets_the_capped_fill_down_through_the_base_mint() {
1144		let quote = buy_exact_out_with_fees(BuyArgs {
1145			amm: amm(10_000_000_000_000),
1146			quote_fee: None,
1147			base_fee: Some(BASE_100_BPS),
1148			base_reserve_cap: Some(1_000_000_000_000),
1149		})
1150		.unwrap();
1151		assert_buy(
1152			&quote,
1153			[
1154				1_000_000_000_000,
1155				30_333_365,
1156				303_334,
1157				10_000_000_000,
1158				0,
1159				990_000_000_000,
1160				30_333_365,
1161			],
1162		);
1163	}
1164
1165	#[test]
1166	fn sell_exact_in_pays_the_user_the_net() {
1167		let quote = sell_exact_in_with_fees(SellArgs {
1168			amm: amm(10_000_000_000_000),
1169			quote_fee: None,
1170			base_fee: None,
1171		})
1172		.unwrap();
1173		assert_sell(
1174			&quote,
1175			[
1176				10_000_000_000_000,
1177				294_059_404,
1178				2_970_298,
1179				0,
1180				0,
1181				10_000_000_000_000,
1182				294_059_404,
1183			],
1184		);
1185	}
1186
1187	#[test]
1188	fn sell_exact_in_books_only_the_base_that_landed() {
1189		let quote = sell_exact_in_with_fees(SellArgs {
1190			amm: amm(10_000_000_000_000),
1191			quote_fee: None,
1192			base_fee: Some(BASE_100_BPS),
1193		})
1194		.unwrap();
1195		assert_sell(
1196			&quote,
1197			[
1198				10_000_000_000_000,
1199				291_147_637,
1200				2_940_886,
1201				100_000_000_000,
1202				0,
1203				10_000_000_000_000,
1204				291_147_637,
1205			],
1206		);
1207	}
1208
1209	#[test]
1210	fn sell_exact_in_takes_the_quote_mints_cut_off_the_outbound_leg() {
1211		let quote = sell_exact_in_with_fees(SellArgs {
1212			amm: amm(10_000_000_000_000),
1213			quote_fee: Some(QUOTE_20_BPS),
1214			base_fee: None,
1215		})
1216		.unwrap();
1217		// No gross-up: the mint takes its fee from the transfer.
1218		assert_sell(
1219			&quote,
1220			[
1221				10_000_000_000_000,
1222				294_059_404,
1223				2_970_298,
1224				0,
1225				588_119,
1226				10_000_000_000_000,
1227				293_471_285,
1228			],
1229		);
1230	}
1231
1232	#[test]
1233	fn sell_exact_in_rejects_a_leg_the_mint_eats_whole() {
1234		let err = sell_exact_in_with_fees(SellArgs {
1235			amm: amm(100),
1236			quote_fee: None,
1237			base_fee: Some(MintFee {
1238				bps: 10_000,
1239				maximum_fee: u64::MAX,
1240			}),
1241		})
1242		.unwrap_err();
1243		assert_eq!(err, QuoteError::Amm(AmmError::InvalidAmount));
1244	}
1245
1246	#[test]
1247	fn sell_exact_out_lands_exactly_what_the_user_asked_for() {
1248		let quote = sell_exact_out_with_fees(SellArgs {
1249			amm: amm(100_000_000),
1250			quote_fee: None,
1251			base_fee: None,
1252		})
1253		.unwrap();
1254		assert_sell(
1255			&quote,
1256			[
1257				3_378_378_411_599,
1258				100_000_000,
1259				1_010_102,
1260				0,
1261				0,
1262				3_378_378_411_599,
1263				100_000_000,
1264			],
1265		);
1266	}
1267
1268	#[test]
1269	fn sell_exact_out_grosses_both_legs_up() {
1270		let quote = sell_exact_out_with_fees(SellArgs {
1271			amm: amm(100_000_000),
1272			quote_fee: Some(QUOTE_20_BPS),
1273			base_fee: Some(BASE_100_BPS),
1274		})
1275		.unwrap();
1276		assert_sell(
1277			&quote,
1278			[
1279				3_419_365_278_607,
1280				100_200_401,
1281				1_012_126,
1282				34_193_652_787,
1283				200_401,
1284				3_419_365_278_607,
1285				100_000_000,
1286			],
1287		);
1288	}
1289
1290	#[test]
1291	fn sell_exact_out_surfaces_an_unsettleable_quote_leg() {
1292		let err = sell_exact_out_with_fees(SellArgs {
1293			amm: amm(100_000_000),
1294			quote_fee: Some(MintFee {
1295				bps: 10_000,
1296				maximum_fee: u64::MAX,
1297			}),
1298			base_fee: None,
1299		})
1300		.unwrap_err();
1301		assert_eq!(err, QuoteError::TransferFeeNotSettleable);
1302	}
1303}
1304
1305#[cfg(test)]
1306mod tests {
1307	use super::*;
1308
1309	const INITIAL_VIRTUAL_QUOTE: u64 = 30_000_000_000; // 30 SOL
1310	const INITIAL_VIRTUAL_BASE: u64 = 1_000_000_000_000_000; // 1B tokens (6 decimals)
1311	const FEE_BPS: u16 = 100;
1312
1313	const BASE_INPUT: AmmInput = AmmInput {
1314		quote_reserves: INITIAL_VIRTUAL_QUOTE,
1315		base_reserves: INITIAL_VIRTUAL_BASE,
1316		amount: 0,
1317		fee_bps: FEE_BPS,
1318	};
1319
1320	#[test]
1321	fn test_ceil_div_edges() {
1322		assert_eq!(ceil_div(0, BPS_DIVISOR), Some(0));
1323		assert_eq!(ceil_div(1, BPS_DIVISOR), Some(1));
1324		assert_eq!(ceil_div(BPS_DIVISOR, BPS_DIVISOR), Some(1));
1325		assert_eq!(ceil_div(BPS_DIVISOR + 1, BPS_DIVISOR), Some(2));
1326		assert_eq!(ceil_div(u128::MAX - BPS_DIVISOR + 1, BPS_DIVISOR), None);
1327		assert_eq!(ceil_div(0, 0), None);
1328		assert_eq!(ceil_div(1, 0), None);
1329	}
1330
1331	#[test]
1332	fn test_calculate_output_basic() {
1333		let output = calculate_output(
1334			INITIAL_VIRTUAL_QUOTE,
1335			INITIAL_VIRTUAL_BASE,
1336			1_000_000_000,
1337		)
1338		.unwrap();
1339		assert_eq!(output, 32_258_064_516_129);
1340	}
1341
1342	#[test]
1343	fn test_calculate_output_ceiling_protects_reserves() {
1344		let output = calculate_output(1000, 1000, 10).unwrap();
1345		assert_eq!(output, 9);
1346	}
1347
1348	#[test]
1349	fn test_roundtrip_favors_protocol() {
1350		let initial_quote = 100_000_000_000u64;
1351		let initial_base = 1_000_000_000u64;
1352
1353		let tokens_out =
1354			calculate_output(initial_quote, initial_base, 10_000_000_000)
1355				.unwrap();
1356
1357		let new_quote = initial_quote + 10_000_000_000;
1358		let new_base = initial_base - tokens_out;
1359
1360		let sol_out =
1361			calculate_output(new_base, new_quote, tokens_out).unwrap();
1362		assert_eq!(sol_out, 9_999_999_900);
1363	}
1364
1365	#[test]
1366	fn test_calculate_input_for_output() {
1367		let quote_needed = calculate_input_for_output(
1368			100_000_000_000,
1369			1_000_000_000,
1370			100_000_000,
1371		)
1372		.unwrap();
1373
1374		let actual_out =
1375			calculate_output(100_000_000_000, 1_000_000_000, quote_needed)
1376				.unwrap();
1377		assert_eq!(actual_out, 100_000_000);
1378	}
1379
1380	#[test]
1381	fn test_zero_amount_fails() {
1382		assert_eq!(
1383			calculate_output(100, 100, 0).unwrap_err(),
1384			AmmError::InvalidAmount
1385		);
1386	}
1387
1388	#[test]
1389	fn test_output_exceeds_reserves() {
1390		assert_eq!(
1391			calculate_input_for_output(
1392				100_000_000_000,
1393				1_000_000_000,
1394				2_000_000_000
1395			)
1396			.unwrap_err(),
1397			AmmError::InvalidAmount
1398		);
1399	}
1400
1401	#[test]
1402	fn test_buy_exact_out_with_fee() {
1403		let quote = buy_exact_out(AmmInput {
1404			amount: 10_000_000_000_000,
1405			..BASE_INPUT
1406		})
1407		.unwrap();
1408
1409		assert_eq!(quote.base_amount, 10_000_000_000_000);
1410		assert_eq!(quote.quote_amount, 306_091_217);
1411		assert_eq!(quote.fee, 3_060_913);
1412	}
1413
1414	#[test]
1415	fn test_buy_exact_out_small_amounts_ceiling() {
1416		let quote = buy_exact_out(AmmInput {
1417			quote_reserves: 10_000,
1418			base_reserves: 10_000,
1419			amount: 99,
1420			fee_bps: FEE_BPS,
1421		})
1422		.unwrap();
1423
1424		assert_eq!(quote.quote_amount, 102);
1425		assert_eq!(quote.fee, 2);
1426	}
1427
1428	#[test]
1429	fn test_buy_exact_out_zero_fee() {
1430		let quote = buy_exact_out(AmmInput {
1431			amount: 10_000_000_000_000,
1432			fee_bps: 0,
1433			..BASE_INPUT
1434		})
1435		.unwrap();
1436
1437		assert_eq!(quote.fee, 0);
1438		let raw_cost = calculate_input_for_output(
1439			INITIAL_VIRTUAL_QUOTE,
1440			INITIAL_VIRTUAL_BASE,
1441			10_000_000_000_000,
1442		)
1443		.unwrap();
1444		assert_eq!(quote.quote_amount, raw_cost);
1445	}
1446
1447	#[test]
1448	fn test_buy_exact_in_basic() {
1449		let quote = buy_exact_in(AmmInput {
1450			amount: 1_000_000_000,
1451			..BASE_INPUT
1452		})
1453		.unwrap();
1454
1455		assert_eq!(quote.quote_amount, 1_000_000_000);
1456		assert_eq!(quote.fee, 10_000_000);
1457		assert_eq!(quote.base_amount, 31_945_788_964_181);
1458	}
1459
1460	#[test]
1461	fn test_buy_exact_in_zero_amount() {
1462		assert_eq!(
1463			buy_exact_in(AmmInput {
1464				amount: 0,
1465				..BASE_INPUT
1466			})
1467			.unwrap_err(),
1468			AmmError::InvalidAmount
1469		);
1470	}
1471
1472	#[test]
1473	fn test_buy_exact_in_zero_fee() {
1474		let quote = buy_exact_in(AmmInput {
1475			amount: 1_000_000_000,
1476			fee_bps: 0,
1477			..BASE_INPUT
1478		})
1479		.unwrap();
1480
1481		assert_eq!(quote.fee, 0);
1482		assert_eq!(quote.quote_amount, 1_000_000_000);
1483	}
1484
1485	#[test]
1486	fn test_sell_exact_in_with_fee() {
1487		let quote = sell_exact_in(AmmInput {
1488			amount: 10_000_000_000_000,
1489			..BASE_INPUT
1490		})
1491		.unwrap();
1492
1493		assert_eq!(quote.base_amount, 10_000_000_000_000);
1494		assert_eq!(quote.quote_amount, 294_059_404);
1495		assert_eq!(quote.fee, 2_970_298);
1496	}
1497
1498	#[test]
1499	fn test_sell_exact_in_fee_ceiling() {
1500		let quote = sell_exact_in(AmmInput {
1501			quote_reserves: 1_000_000_000_000,
1502			base_reserves: 1_000_000_000_000,
1503			amount: 9_999_999,
1504			fee_bps: FEE_BPS,
1505		})
1506		.unwrap();
1507
1508		assert_eq!(quote.fee, 99_999);
1509		assert_eq!(quote.quote_amount, 9_899_900);
1510	}
1511
1512	#[test]
1513	fn test_sell_exact_in_rejects_fee_above_full_bps() {
1514		assert_eq!(
1515			sell_exact_in(AmmInput {
1516				amount: 10_000_000_000_000,
1517				fee_bps: 10_001,
1518				..BASE_INPUT
1519			})
1520			.unwrap_err(),
1521			AmmError::InvalidAmount
1522		);
1523	}
1524
1525	/// Keep `fee_bps == 10_000` valid for TypeScript parity.
1526	#[test]
1527	fn test_sell_exact_in_full_fee_is_allowed() {
1528		let quote = sell_exact_in(AmmInput {
1529			quote_reserves: 1_000_000,
1530			base_reserves: 1_000_000,
1531			amount: 1_000,
1532			fee_bps: 10_000,
1533		})
1534		.unwrap();
1535
1536		assert_eq!(quote.base_amount, 1_000);
1537		assert_eq!(quote.quote_amount, 0);
1538		assert_eq!(quote.fee, 999);
1539	}
1540
1541	#[test]
1542	fn test_sell_exact_in_zero_fee() {
1543		let quote = sell_exact_in(AmmInput {
1544			amount: 10_000_000_000_000,
1545			fee_bps: 0,
1546			..BASE_INPUT
1547		})
1548		.unwrap();
1549
1550		assert_eq!(quote.fee, 0);
1551		let raw_output = calculate_output(
1552			INITIAL_VIRTUAL_BASE,
1553			INITIAL_VIRTUAL_QUOTE,
1554			10_000_000_000_000,
1555		)
1556		.unwrap();
1557		assert_eq!(quote.quote_amount, raw_output);
1558	}
1559
1560	#[test]
1561	fn test_sell_exact_out_roundtrip_with_sell_exact_in() {
1562		let sell = sell_exact_in(AmmInput {
1563			amount: 10_000_000_000_000,
1564			..BASE_INPUT
1565		})
1566		.unwrap();
1567
1568		let target = sell_exact_out(AmmInput {
1569			amount: sell.quote_amount,
1570			..BASE_INPUT
1571		})
1572		.unwrap();
1573
1574		assert_eq!(target.quote_amount, sell.quote_amount);
1575		assert_eq!(target.base_amount, 9_999_999_967_007);
1576
1577		let verify = sell_exact_in(AmmInput {
1578			amount: target.base_amount,
1579			..BASE_INPUT
1580		})
1581		.unwrap();
1582		assert_eq!(verify.quote_amount, 294_059_404);
1583	}
1584
1585	#[test]
1586	fn test_sell_exact_out_zero_fee() {
1587		let quote = sell_exact_out(AmmInput {
1588			amount: 1_000_000_000,
1589			fee_bps: 0,
1590			..BASE_INPUT
1591		})
1592		.unwrap();
1593
1594		assert_eq!(quote.fee, 0);
1595		assert_eq!(quote.quote_amount, 1_000_000_000);
1596		let raw = calculate_input_for_output(
1597			INITIAL_VIRTUAL_BASE,
1598			INITIAL_VIRTUAL_QUOTE,
1599			1_000_000_000,
1600		)
1601		.unwrap();
1602		assert_eq!(quote.base_amount, raw);
1603	}
1604
1605	#[test]
1606	fn test_sell_exact_out_fee_reversal() {
1607		let quote = sell_exact_out(AmmInput {
1608			amount: 1_000_000_000,
1609			..BASE_INPUT
1610		})
1611		.unwrap();
1612
1613		assert_eq!(quote.quote_amount, 1_000_000_000);
1614		assert_eq!(quote.fee, 10_101_011);
1615		let actual_gross = calculate_output(
1616			INITIAL_VIRTUAL_BASE,
1617			INITIAL_VIRTUAL_QUOTE,
1618			quote.base_amount,
1619		)
1620		.unwrap();
1621		assert_eq!(actual_gross, 1_010_101_011);
1622	}
1623
1624	#[test]
1625	fn test_sell_exact_out_hardcoded() {
1626		let quote = sell_exact_out(AmmInput {
1627			amount: 1_000_000_000,
1628			..BASE_INPUT
1629		})
1630		.unwrap();
1631
1632		assert_eq!(quote.quote_amount, 1_000_000_000);
1633		assert_eq!(quote.fee, 10_101_011);
1634		assert_eq!(quote.base_amount, 34_843_205_607_004);
1635	}
1636
1637	#[test]
1638	fn test_sell_exact_out_zero_amount() {
1639		assert_eq!(
1640			sell_exact_out(AmmInput {
1641				amount: 0,
1642				..BASE_INPUT
1643			})
1644			.unwrap_err(),
1645			AmmError::InvalidAmount
1646		);
1647	}
1648
1649	#[test]
1650	fn test_sell_exact_out_cross_consistency_with_slippage() {
1651		let slippage_bps = 50;
1652
1653		let quote = sell_exact_out(AmmInput {
1654			amount: 1_000_000_000,
1655			..BASE_INPUT
1656		})
1657		.unwrap();
1658
1659		let max_base =
1660			calculate_slippage_up(quote.base_amount, slippage_bps).unwrap();
1661		assert_eq!(max_base, 35_017_421_635_040);
1662
1663		let verify = sell_exact_in(AmmInput {
1664			amount: max_base,
1665			..BASE_INPUT
1666		})
1667		.unwrap();
1668		assert_eq!(verify.quote_amount, 1_004_830_836);
1669	}
1670
1671	#[test]
1672	fn test_calculate_slippage_up_basic() {
1673		let result = calculate_slippage_up(1_000_000_000, 100).unwrap();
1674		assert_eq!(result, 1_010_000_000);
1675	}
1676
1677	#[test]
1678	fn test_calculate_slippage_up_ceiling() {
1679		let result = calculate_slippage_up(101, 50).unwrap();
1680		assert_eq!(result, 102);
1681	}
1682
1683	#[test]
1684	fn test_calculate_slippage_up_zero_slippage() {
1685		let result = calculate_slippage_up(1_000_000_000, 0).unwrap();
1686		assert_eq!(result, 1_000_000_000);
1687	}
1688
1689	#[test]
1690	fn test_calculate_slippage_down_basic() {
1691		let result = calculate_slippage_down(1_000_000_000, 100).unwrap();
1692		assert_eq!(result, 990_000_000);
1693	}
1694
1695	#[test]
1696	fn test_calculate_slippage_down_floor() {
1697		let result = calculate_slippage_down(101, 50).unwrap();
1698		assert_eq!(result, 100);
1699	}
1700
1701	#[test]
1702	fn test_calculate_slippage_down_zero_slippage() {
1703		let result = calculate_slippage_down(1_000_000_000, 0).unwrap();
1704		assert_eq!(result, 1_000_000_000);
1705	}
1706
1707	#[test]
1708	fn test_calculate_slippage_down_100_percent_slippage() {
1709		let result = calculate_slippage_down(1_000_000_000, 10_000).unwrap();
1710		assert_eq!(result, 0);
1711	}
1712
1713	#[test]
1714	fn test_buy_sell_roundtrip() {
1715		let buy = buy_exact_out(AmmInput {
1716			amount: 10_000_000_000_000,
1717			..BASE_INPUT
1718		})
1719		.unwrap();
1720
1721		let new_quote = INITIAL_VIRTUAL_QUOTE + buy.quote_amount - buy.fee;
1722		let new_base = INITIAL_VIRTUAL_BASE - buy.base_amount;
1723
1724		let sell = sell_exact_in(AmmInput {
1725			quote_reserves: new_quote,
1726			base_reserves: new_base,
1727			amount: buy.base_amount,
1728			fee_bps: FEE_BPS,
1729		})
1730		.unwrap();
1731
1732		// Fees and rounding make the round trip return less.
1733		assert!(sell.quote_amount < buy.quote_amount);
1734	}
1735
1736	#[test]
1737	fn test_buy_exact_in_roundtrip_with_buy_exact_out() {
1738		let buy = buy_exact_in(AmmInput {
1739			amount: 1_000_000_000,
1740			..BASE_INPUT
1741		})
1742		.unwrap();
1743
1744		assert_eq!(buy.base_amount, 31_945_788_964_181);
1745		assert_eq!(buy.fee, 10_000_000);
1746
1747		let target = buy_exact_out(AmmInput {
1748			amount: buy.base_amount,
1749			..BASE_INPUT
1750		})
1751		.unwrap();
1752
1753		assert_eq!(target.base_amount, buy.base_amount);
1754		assert_eq!(target.quote_amount, 1_000_000_000);
1755		assert_eq!(target.fee, 10_000_000);
1756	}
1757}