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