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,
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
102pub 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
131fn 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 (_, 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
155pub 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)]
183pub struct BuyArgs {
184 pub amm: AmmInput,
185 pub quote_fee: Option<MintFee>,
186 pub base_fee: Option<MintFee>,
187 pub base_reserve_cap: Option<u64>,
189}
190
191#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
203pub struct BuyQuote {
204 pub base_amount: u64,
205 pub quote_amount: u64,
206 pub fee: u64,
208 pub base_transfer_fee: u64,
209 pub quote_transfer_fee: u64,
210 pub base_to_user: u64,
212 pub quote_from_user: u64,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub struct SellQuote {
219 pub base_amount: u64,
220 pub quote_amount: u64,
221 pub fee: u64,
223 pub base_transfer_fee: u64,
224 pub quote_transfer_fee: u64,
225 pub base_from_user: u64,
227 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
255pub 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 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
307pub 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
336pub 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
373pub 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
404pub 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 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
442pub 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 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
485pub 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 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
517pub 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 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
562pub 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
578pub 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
595pub 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 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
635pub 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 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#[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 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 "e,
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 "e,
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 "e,
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 "e,
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 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 "e,
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 "e,
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 "e,
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 "e,
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 "e,
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 "e,
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 assert_sell(
1190 "e,
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 "e,
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 "e,
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; const INITIAL_VIRTUAL_BASE: u64 = 1_000_000_000_000_000; 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 #[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 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}