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