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