1use std::fmt;
2
3const BPS_DIVISOR: u128 = 10_000;
4
5fn 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct MintFee {
52 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 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
101pub 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
130fn 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 (_, 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
154pub 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#[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 pub base_reserve_cap: Option<u64>,
190}
191
192#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
206pub struct BuyQuote {
207 pub base_amount: u64,
208 pub quote_amount: u64,
209 pub fee: u64,
211 pub base_transfer_fee: u64,
212 pub quote_transfer_fee: u64,
213 pub base_to_user: u64,
215 pub quote_from_user: u64,
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub struct SellQuote {
223 pub base_amount: u64,
224 pub quote_amount: u64,
225 pub fee: u64,
228 pub base_transfer_fee: u64,
229 pub quote_transfer_fee: u64,
230 pub base_from_user: u64,
233 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
261pub 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 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
312pub 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
341pub 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
377pub 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
408pub 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 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
447pub 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 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
491pub 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 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
524pub 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 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
570pub 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
586pub 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
603pub 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 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
643pub 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 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#[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 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 "e,
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 "e,
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 "e,
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 "e,
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 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 "e,
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 "e,
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 "e,
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 "e,
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 "e,
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 "e,
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 assert_sell(
1197 "e,
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 "e,
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 "e,
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; const INITIAL_VIRTUAL_BASE: u64 = 1_000_000_000_000_000; 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 #[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 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}