1use super::{
2 a_to_b, b_to_a, fee_from_post_fee_amount, fee_from_pre_fee_amount, next_band, prev_band,
3 skew_band, CoreError, SkewBand, SkewExponent, SkewVault, ARITHMETIC_OVERFLOW,
4 PARTIAL_FILL_NOT_ALLOWED, PER_M_DENOMINATOR, U128,
5};
6
7#[cfg(feature = "wasm")]
8use equanetwork_macros::wasm_expose;
9
10use ethnum::U256;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13#[cfg_attr(feature = "wasm", wasm_expose)]
14pub enum SwapMode {
15 ExactIn,
16 ExactOut,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20#[cfg_attr(feature = "wasm", wasm_expose)]
21pub struct SwapQuote {
22 pub amount_in: u64,
23 pub fee_in: u64,
24 pub amount_out: u64,
25 pub fee_out: u64,
26}
27
28#[derive(Debug, Clone, Copy)]
29#[cfg_attr(feature = "wasm", wasm_expose)]
30pub struct SwapVault {
31 pub swap_fee_per_m: u32,
32 pub inventory_limit_upper: u64,
33 pub inventory_limit_lower: u64,
34 pub max_swap_amount: u64,
35 pub positive_skew_per_m: u32,
36 pub negative_skew_per_m: u32,
37 pub lower_skew_offset: u64,
38 pub upper_skew_offset: u64,
39 pub skew_exponent: SkewExponent,
40}
41
42#[allow(clippy::too_many_arguments)]
43#[cfg_attr(feature = "wasm", wasm_expose)]
44pub fn swap_quote(
45 amount: u64,
46 swap_mode: SwapMode,
47 base_price: U128,
48 allow_partial_fill: bool,
49 input_vault: SwapVault,
50 input_vault_balance: u64,
51 output_vault: SwapVault,
52 output_vault_balance: u64,
53) -> Result<SwapQuote, CoreError> {
54 let base_price: u128 = base_price.into();
55 let max_amount_in = input_vault
56 .inventory_limit_upper
57 .checked_sub(input_vault_balance)
58 .ok_or(ARITHMETIC_OVERFLOW)?
59 .min(input_vault.max_swap_amount);
60
61 let max_amount_out = output_vault_balance
62 .checked_sub(output_vault.inventory_limit_lower)
63 .ok_or(ARITHMETIC_OVERFLOW)?
64 .min(output_vault.max_swap_amount);
65
66 match swap_mode {
67 SwapMode::ExactIn => {
68 let amount_in = amount.min(max_amount_in);
69 let mut quote = raw_quote(
70 amount_in,
71 SwapMode::ExactIn,
72 base_price.into(),
73 &input_vault,
74 input_vault_balance,
75 &output_vault,
76 output_vault_balance,
77 )?;
78 if quote.amount_out > max_amount_out {
79 quote = raw_quote(
80 max_amount_out,
81 SwapMode::ExactOut,
82 base_price.into(),
83 &input_vault,
84 input_vault_balance,
85 &output_vault,
86 output_vault_balance,
87 )?;
88 }
89 if allow_partial_fill || quote.amount_in == amount {
90 Ok(quote)
91 } else {
92 Err(PARTIAL_FILL_NOT_ALLOWED)
93 }
94 }
95 SwapMode::ExactOut => {
96 let amount_out = amount.min(max_amount_out);
97 let mut quote = raw_quote(
98 amount_out,
99 SwapMode::ExactOut,
100 base_price.into(),
101 &input_vault,
102 input_vault_balance,
103 &output_vault,
104 output_vault_balance,
105 )?;
106 if quote.amount_in > max_amount_in {
107 quote = raw_quote(
108 max_amount_in,
109 SwapMode::ExactIn,
110 base_price.into(),
111 &input_vault,
112 input_vault_balance,
113 &output_vault,
114 output_vault_balance,
115 )?;
116 }
117 if allow_partial_fill || quote.amount_out == amount {
118 Ok(quote)
119 } else {
120 Err(PARTIAL_FILL_NOT_ALLOWED)
121 }
122 }
123 }
124}
125
126fn raw_quote(
127 amount: u64,
128 swap_mode: SwapMode,
129 base_price: u128,
130 input_vault: &SwapVault,
131 input_vault_balance: u64,
132 output_vault: &SwapVault,
133 output_vault_balance: u64,
134) -> Result<SwapQuote, CoreError> {
135 let prelim_fee = match swap_mode {
136 SwapMode::ExactIn => fee_from_pre_fee_amount(amount, input_vault.swap_fee_per_m)?,
137 SwapMode::ExactOut => fee_from_post_fee_amount(amount, output_vault.swap_fee_per_m)?,
138 };
139
140 let consumable_amount = match swap_mode {
141 SwapMode::ExactIn => amount.checked_sub(prelim_fee).ok_or(ARITHMETIC_OVERFLOW)?,
142 SwapMode::ExactOut => amount.checked_add(prelim_fee).ok_or(ARITHMETIC_OVERFLOW)?,
143 };
144
145 let mut raw_amount_in = 0u64;
146 let mut raw_amount_out = 0u64;
147 let mut remaining_amount = consumable_amount;
148
149 let mut current_input_balance = input_vault_balance;
150 let mut current_output_balance = output_vault_balance;
151
152 let mut input_band = Some(skew_band(
153 input_vault_balance,
154 true,
155 SkewVault::from(*input_vault),
156 )?);
157 let mut output_band = Some(skew_band(
158 output_vault_balance,
159 false,
160 SkewVault::from(*output_vault),
161 )?);
162
163 while remaining_amount > 0 {
164 let (Some(current_input_band), Some(current_output_band)) = (&input_band, &output_band)
165 else {
166 break;
167 };
168
169 let (a_to_b_price, b_to_a_price) =
170 band_prices(base_price, current_input_band, current_output_band)?;
171 if a_to_b_price == 0 || b_to_a_price == 0 {
172 break;
173 }
174
175 let (input_amount, output_amount, move_input_vault, move_output_vault) = match swap_mode {
176 SwapMode::ExactIn => {
177 let input_amount_input_band = current_input_band
178 .upper_limit
179 .abs_diff(current_input_balance)
180 .min(remaining_amount);
181 let output_amount_output_band = current_output_band
182 .lower_limit
183 .abs_diff(current_output_balance);
184 let input_amount_output_band =
185 b_to_a(output_amount_output_band, b_to_a_price.into(), true)?
186 .min(remaining_amount);
187
188 let input_amount = input_amount_input_band.min(input_amount_output_band);
189 let output_amount = a_to_b(input_amount, a_to_b_price.into(), false)?;
190
191 (
192 input_amount,
193 output_amount,
194 input_amount_input_band < input_amount_output_band,
195 input_amount_input_band > input_amount_output_band,
196 )
197 }
198 SwapMode::ExactOut => {
199 let output_amount_output_band = current_output_band
200 .lower_limit
201 .abs_diff(current_output_balance)
202 .min(remaining_amount);
203 let input_amount_input_band = current_input_band
204 .upper_limit
205 .abs_diff(current_input_balance);
206 let output_amount_input_band =
207 a_to_b(input_amount_input_band, a_to_b_price.into(), false)?
208 .min(remaining_amount);
209
210 let output_amount = output_amount_input_band.min(output_amount_output_band);
211 let input_amount = b_to_a(output_amount, b_to_a_price.into(), true)?;
212
213 (
214 input_amount,
215 output_amount,
216 output_amount_input_band < output_amount_output_band,
217 output_amount_input_band > output_amount_output_band,
218 )
219 }
220 };
221
222 raw_amount_in = raw_amount_in
223 .checked_add(input_amount)
224 .ok_or(ARITHMETIC_OVERFLOW)?;
225 raw_amount_out = raw_amount_out
226 .checked_add(output_amount)
227 .ok_or(ARITHMETIC_OVERFLOW)?;
228
229 remaining_amount = match swap_mode {
230 SwapMode::ExactIn => remaining_amount
231 .checked_sub(input_amount)
232 .ok_or(ARITHMETIC_OVERFLOW)?,
233 SwapMode::ExactOut => remaining_amount
234 .checked_sub(output_amount)
235 .ok_or(ARITHMETIC_OVERFLOW)?,
236 };
237
238 current_input_balance = current_input_balance
239 .checked_add(input_amount)
240 .ok_or(ARITHMETIC_OVERFLOW)?;
241 current_output_balance = current_output_balance
242 .checked_sub(output_amount)
243 .ok_or(ARITHMETIC_OVERFLOW)?;
244
245 if move_input_vault {
246 input_band = next_band(current_input_band, &SkewVault::from(*input_vault))?;
247 }
248
249 if move_output_vault {
250 output_band = prev_band(current_output_band, &SkewVault::from(*output_vault))?;
251 }
252 }
253
254 let (fee_in, amount_in) =
255 if matches!(swap_mode, SwapMode::ExactIn) && consumable_amount == raw_amount_in {
256 (prelim_fee, amount)
257 } else {
258 let fee_in = fee_from_post_fee_amount(raw_amount_in, input_vault.swap_fee_per_m)?;
259 let amount_in = raw_amount_in
260 .checked_add(fee_in)
261 .ok_or(ARITHMETIC_OVERFLOW)?;
262 (fee_in, amount_in)
263 };
264
265 let (fee_out, amount_out) =
266 if matches!(swap_mode, SwapMode::ExactOut) && consumable_amount == raw_amount_out {
267 (prelim_fee, amount)
268 } else {
269 let fee_out = fee_from_pre_fee_amount(raw_amount_out, output_vault.swap_fee_per_m)?;
270 let amount_out = raw_amount_out
271 .checked_sub(fee_out)
272 .ok_or(ARITHMETIC_OVERFLOW)?;
273 (fee_out, amount_out)
274 };
275
276 Ok(SwapQuote {
277 amount_in,
278 fee_in,
279 amount_out,
280 fee_out,
281 })
282}
283
284fn band_prices(
285 base_price: u128,
286 input_band: &SkewBand,
287 output_band: &SkewBand,
288) -> Result<(u128, u128), CoreError> {
289 let price_skew_per_m = PER_M_DENOMINATOR
290 .checked_add(input_band.skew_per_m)
291 .ok_or(ARITHMETIC_OVERFLOW)?
292 .checked_add(output_band.skew_per_m)
293 .ok_or(ARITHMETIC_OVERFLOW)?
294 .clamp(0, PER_M_DENOMINATOR * 2);
295
296 let product = U256::from(base_price)
297 .checked_mul(U256::from(price_skew_per_m as u32))
298 .ok_or(ARITHMETIC_OVERFLOW)?;
299
300 let quotient = product
301 .checked_div(U256::from(PER_M_DENOMINATOR as u32))
302 .ok_or(ARITHMETIC_OVERFLOW)?;
303
304 let remainder = product
305 .checked_rem(U256::from(PER_M_DENOMINATOR as u32))
306 .ok_or(ARITHMETIC_OVERFLOW)?;
307
308 let result = if remainder > 0 {
309 quotient + 1
310 } else {
311 quotient
312 };
313
314 Ok((
315 quotient.try_into().map_err(|_| ARITHMETIC_OVERFLOW)?,
316 result.try_into().map_err(|_| ARITHMETIC_OVERFLOW)?,
317 ))
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323 use rstest::rstest;
324
325 fn stub_band(skew_per_m: i32) -> SkewBand {
326 SkewBand {
327 index: 0,
328 lower_limit: 0,
329 upper_limit: 0,
330 skew_per_m,
331 }
332 }
333
334 fn dead_zone_vault(swap_fee_per_m: u32, max_swap_amount: u64) -> SwapVault {
335 SwapVault {
336 swap_fee_per_m,
337 inventory_limit_upper: 100_000,
338 inventory_limit_lower: 0,
339 max_swap_amount,
340 positive_skew_per_m: 0,
341 negative_skew_per_m: 0,
342 lower_skew_offset: 0,
343 upper_skew_offset: 100_000,
344 skew_exponent: SkewExponent::Linear,
345 }
346 }
347
348 fn skew_vault(
349 positive_skew_per_m: u32,
350 negative_skew_per_m: u32,
351 skew_exponent: SkewExponent,
352 ) -> SwapVault {
353 SwapVault {
354 swap_fee_per_m: 0,
355 inventory_limit_upper: 64_000,
356 inventory_limit_lower: 0,
357 max_swap_amount: u64::MAX,
358 positive_skew_per_m,
359 negative_skew_per_m,
360 lower_skew_offset: 16_000,
361 upper_skew_offset: 48_000,
362 skew_exponent,
363 }
364 }
365
366 #[rstest]
367 #[case(0, 0, 1_000_000u128, 1_000_000u128)]
369 #[case(100_000, 0, 1_100_000, 1_100_000)]
370 #[case(0, -100_000, 900_000, 900_000)]
371 #[case(-600_000, -600_000, 0, 0)]
372 #[case(600_000, 600_000, 2_000_000, 2_000_000)]
373 fn test_band_prices(
374 #[case] input_skew_per_m: i32,
375 #[case] output_skew_per_m: i32,
376 #[case] expected_a_to_b: u128,
377 #[case] expected_b_to_a: u128,
378 ) {
379 let (a_to_b_price, b_to_a_price) = band_prices(
380 PER_M_DENOMINATOR as u128,
381 &stub_band(input_skew_per_m),
382 &stub_band(output_skew_per_m),
383 )
384 .unwrap();
385 assert_eq!(a_to_b_price, expected_a_to_b);
386 assert_eq!(b_to_a_price, expected_b_to_a);
387 }
388
389 #[rstest]
390 #[case(1, 0, 18_446_762_520_453_625_325, 18_446_762_520_453_625_326)]
392 fn test_band_prices_rounding(
393 #[case] input_skew_per_m: i32,
394 #[case] output_skew_per_m: i32,
395 #[case] expected_a_to_b: u128,
396 #[case] expected_b_to_a: u128,
397 ) {
398 let (a_to_b_price, b_to_a_price) = band_prices(
399 1u128 << 64,
400 &stub_band(input_skew_per_m),
401 &stub_band(output_skew_per_m),
402 )
403 .unwrap();
404 assert_eq!(a_to_b_price, expected_a_to_b);
405 assert_eq!(b_to_a_price, expected_b_to_a);
406 assert!(b_to_a_price > a_to_b_price);
407 }
408
409 #[rstest]
410 #[case(1_000, SwapMode::ExactIn)]
411 #[case(1_000, SwapMode::ExactOut)]
412 #[case(0, SwapMode::ExactIn)]
413 #[case(0, SwapMode::ExactOut)]
414 fn test_swap_quote_identity(#[case] amount: u64, #[case] swap_mode: SwapMode) {
415 let vault = dead_zone_vault(0, u64::MAX);
416 let quote = swap_quote(
417 amount,
418 swap_mode,
419 U128::from(1u128 << 64),
420 true,
421 vault,
422 50_000,
423 vault,
424 50_000,
425 )
426 .unwrap();
427 assert_eq!(
428 quote,
429 SwapQuote {
430 amount_in: amount,
431 fee_in: 0,
432 amount_out: amount,
433 fee_out: 0,
434 }
435 );
436 }
437
438 #[rstest]
439 #[case(SwapMode::ExactIn, 100_000, 0, 1_000, 1_000, 100, 900, 0)]
441 #[case(SwapMode::ExactOut, 0, 100_000, 900, 1_000, 0, 900, 100)]
443 #[case(SwapMode::ExactIn, 100_000, 100_000, 1_000, 1_000, 100, 810, 90)]
445 fn test_swap_quote_fees(
446 #[case] swap_mode: SwapMode,
447 #[case] input_fee_per_m: u32,
448 #[case] output_fee_per_m: u32,
449 #[case] amount: u64,
450 #[case] expected_in: u64,
451 #[case] expected_fee_in: u64,
452 #[case] expected_out: u64,
453 #[case] expected_fee_out: u64,
454 ) {
455 let quote = swap_quote(
456 amount,
457 swap_mode,
458 U128::from(1u128 << 64),
459 true,
460 dead_zone_vault(input_fee_per_m, u64::MAX),
461 50_000,
462 dead_zone_vault(output_fee_per_m, u64::MAX),
463 50_000,
464 )
465 .unwrap();
466 assert_eq!(quote.amount_in, expected_in);
467 assert_eq!(quote.fee_in, expected_fee_in);
468 assert_eq!(quote.amount_out, expected_out);
469 assert_eq!(quote.fee_out, expected_fee_out);
470 }
471
472 #[rstest]
473 #[case(9, 100, 9, 1, 8, 0)]
474 fn test_swap_quote_fees_rounding(
475 #[case] amount: u64,
476 #[case] input_fee_per_m: u32,
477 #[case] expected_in: u64,
478 #[case] expected_fee_in: u64,
479 #[case] expected_out: u64,
480 #[case] expected_fee_out: u64,
481 ) {
482 let quote = swap_quote(
483 amount,
484 SwapMode::ExactIn,
485 U128::from(1u128 << 64),
486 true,
487 dead_zone_vault(input_fee_per_m, u64::MAX),
488 50_000,
489 dead_zone_vault(0, u64::MAX),
490 50_000,
491 )
492 .unwrap();
493 assert_eq!(quote.amount_in, expected_in);
494 assert_eq!(quote.fee_in, expected_fee_in);
495 assert_eq!(quote.amount_out, expected_out);
496 assert_eq!(quote.fee_out, expected_fee_out);
497 }
498
499 #[rstest]
500 #[case(55_501, 32_000, true, 400, 400, 406)]
502 #[case(50_000, 8_000, false, 400, 400, 406)]
504 #[case(32_000, 32_000, true, 400, 400, 400)]
506 fn test_swap_quote_skew(
507 #[case] input_balance: u64,
508 #[case] output_balance: u64,
509 #[case] skew_on_input: bool,
510 #[case] amount: u64,
511 #[case] expected_in: u64,
512 #[case] expected_out: u64,
513 ) {
514 let (input_vault, output_vault) = if skew_on_input {
515 (
516 skew_vault(32_000, 0, SkewExponent::Linear),
517 dead_zone_vault(0, u64::MAX),
518 )
519 } else if input_balance == 32_000 && output_balance == 32_000 {
520 (dead_zone_vault(0, u64::MAX), dead_zone_vault(0, u64::MAX))
521 } else {
522 (
523 dead_zone_vault(0, u64::MAX),
524 skew_vault(32_000, 0, SkewExponent::Linear),
525 )
526 };
527 let quote = swap_quote(
528 amount,
529 SwapMode::ExactIn,
530 U128::from(1u128 << 64),
531 true,
532 input_vault,
533 input_balance,
534 output_vault,
535 output_balance,
536 )
537 .unwrap();
538 assert_eq!(quote.amount_in, expected_in);
539 assert_eq!(quote.fee_in, 0);
540 assert_eq!(quote.amount_out, expected_out);
541 assert_eq!(quote.fee_out, 0);
542 }
543
544 #[rstest]
545 #[case(48_001, 50_000, 10_000, 10_000, 10_023)]
547 fn test_swap_quote_skew_rounding(
548 #[case] input_balance: u64,
549 #[case] output_balance: u64,
550 #[case] positive_skew_per_m: u32,
551 #[case] amount: u64,
552 #[case] expected_out: u64,
553 ) {
554 let quote = swap_quote(
555 amount,
556 SwapMode::ExactIn,
557 U128::from(1u128 << 64),
558 true,
559 skew_vault(positive_skew_per_m, 0, SkewExponent::Linear),
560 input_balance,
561 dead_zone_vault(0, u64::MAX),
562 output_balance,
563 )
564 .unwrap();
565 assert_eq!(quote.amount_in, amount);
566 assert_eq!(quote.amount_out, expected_out);
567 }
568
569 #[rstest]
570 #[case(
572 SwapMode::ExactIn,
573 5_000,
574 9_000,
575 10_000,
576 50_000,
577 100_000,
578 u64::MAX,
579 1_000,
580 1_000
581 )]
582 #[case(
584 SwapMode::ExactOut,
585 5_000,
586 50_000,
587 100_000,
588 1_000,
589 100_000,
590 u64::MAX,
591 1_000,
592 1_000
593 )]
594 #[case(
596 SwapMode::ExactIn,
597 5_000,
598 50_000,
599 100_000,
600 50_000,
601 100_000,
602 500,
603 500,
604 500
605 )]
606 fn test_swap_quote_caps(
607 #[case] swap_mode: SwapMode,
608 #[case] amount: u64,
609 #[case] input_balance: u64,
610 #[case] input_upper: u64,
611 #[case] output_balance: u64,
612 #[case] output_upper: u64,
613 #[case] max_swap_amount: u64,
614 #[case] expected_in: u64,
615 #[case] expected_out: u64,
616 ) {
617 let input = SwapVault {
618 swap_fee_per_m: 0,
619 inventory_limit_upper: input_upper,
620 inventory_limit_lower: 0,
621 max_swap_amount,
622 positive_skew_per_m: 0,
623 negative_skew_per_m: 0,
624 lower_skew_offset: 0,
625 upper_skew_offset: input_upper,
626 skew_exponent: SkewExponent::Linear,
627 };
628 let output = SwapVault {
629 swap_fee_per_m: 0,
630 inventory_limit_upper: output_upper,
631 inventory_limit_lower: 0,
632 max_swap_amount,
633 positive_skew_per_m: 0,
634 negative_skew_per_m: 0,
635 lower_skew_offset: 0,
636 upper_skew_offset: output_upper,
637 skew_exponent: SkewExponent::Linear,
638 };
639 let quote = swap_quote(
640 amount,
641 swap_mode,
642 U128::from(1u128 << 64),
643 true,
644 input,
645 input_balance,
646 output,
647 output_balance,
648 )
649 .unwrap();
650 assert_eq!(quote.amount_in, expected_in);
651 assert_eq!(quote.amount_out, expected_out);
652 }
653
654 #[test]
655 fn test_swap_quote_caps_requote_on_output_room() {
656 let quote = swap_quote(
658 5_000,
659 SwapMode::ExactIn,
660 U128::from(1u128 << 64),
661 true,
662 dead_zone_vault(0, u64::MAX),
663 50_000,
664 dead_zone_vault(0, u64::MAX),
665 500,
666 )
667 .unwrap();
668 assert!(quote.amount_out <= 500);
669 assert_eq!(quote.amount_out, 500);
670 assert_eq!(quote.amount_in, 500);
671 }
672
673 #[rstest]
674 #[case(SwapMode::ExactIn, false, Err(PARTIAL_FILL_NOT_ALLOWED))]
675 #[case(SwapMode::ExactIn, true, Ok(()))]
676 #[case(SwapMode::ExactOut, false, Err(PARTIAL_FILL_NOT_ALLOWED))]
677 #[case(SwapMode::ExactOut, true, Ok(()))]
678 fn test_swap_quote_partial_fill(
679 #[case] swap_mode: SwapMode,
680 #[case] allow_partial_fill: bool,
681 #[case] expected: Result<(), CoreError>,
682 ) {
683 let input = SwapVault {
685 swap_fee_per_m: 0,
686 inventory_limit_upper: 10_000,
687 inventory_limit_lower: 0,
688 max_swap_amount: u64::MAX,
689 positive_skew_per_m: 0,
690 negative_skew_per_m: 0,
691 lower_skew_offset: 0,
692 upper_skew_offset: 10_000,
693 skew_exponent: SkewExponent::Linear,
694 };
695 let output = SwapVault {
696 swap_fee_per_m: 0,
697 inventory_limit_upper: 100_000,
698 inventory_limit_lower: 0,
699 max_swap_amount: u64::MAX,
700 positive_skew_per_m: 0,
701 negative_skew_per_m: 0,
702 lower_skew_offset: 0,
703 upper_skew_offset: 100_000,
704 skew_exponent: SkewExponent::Linear,
705 };
706 let result = swap_quote(
707 5_000,
708 swap_mode,
709 U128::from(1u128 << 64),
710 allow_partial_fill,
711 input,
712 9_000,
713 output,
714 1_000,
715 );
716 match expected {
717 Ok(()) => {
718 let quote = result.unwrap();
719 assert!(quote.amount_in < 5_000 || quote.amount_out < 5_000);
720 }
721 Err(e) => assert_eq!(result, Err(e)),
722 }
723 }
724
725 #[rstest]
726 #[case(SwapMode::ExactIn, 1_000)]
727 #[case(SwapMode::ExactOut, 1_000)]
728 fn test_swap_quote_multiband(#[case] swap_mode: SwapMode, #[case] amount: u64) {
729 let input = skew_vault(32_000, 0, SkewExponent::Linear);
731 let output = dead_zone_vault(0, u64::MAX);
732 let small = swap_quote(
733 100,
734 swap_mode,
735 U128::from(1u128 << 64),
736 true,
737 input,
738 48_001,
739 output,
740 50_000,
741 )
742 .unwrap();
743 let large = swap_quote(
744 amount,
745 swap_mode,
746 U128::from(1u128 << 64),
747 true,
748 input,
749 48_001,
750 output,
751 50_000,
752 )
753 .unwrap();
754 assert!(large.amount_in > 0 && large.amount_out > 0);
755 assert!(large.amount_in >= small.amount_in);
756 let small_rate = small.amount_out as u128 * 1_000_000 / small.amount_in.max(1) as u128;
758 let large_rate = large.amount_out as u128 * 1_000_000 / large.amount_in.max(1) as u128;
759 assert!(large_rate >= small_rate);
760 }
761
762 #[test]
763 fn test_swap_quote_capacity_overflow_input_above_upper() {
764 let vault = dead_zone_vault(0, u64::MAX);
766 let result = swap_quote(
767 1_000,
768 SwapMode::ExactIn,
769 U128::from(1u128 << 64),
770 true,
771 vault,
772 100_001,
773 vault,
774 50_000,
775 );
776 assert_eq!(result, Err(ARITHMETIC_OVERFLOW));
777 }
778
779 #[test]
780 fn test_swap_quote_capacity_overflow_output_below_lower() {
781 let input = dead_zone_vault(0, u64::MAX);
782 let output = SwapVault {
783 swap_fee_per_m: 0,
784 inventory_limit_upper: 100_000,
785 inventory_limit_lower: 1_000,
786 max_swap_amount: u64::MAX,
787 positive_skew_per_m: 0,
788 negative_skew_per_m: 0,
789 lower_skew_offset: 1_000,
790 upper_skew_offset: 100_000,
791 skew_exponent: SkewExponent::Linear,
792 };
793 let result = swap_quote(
794 1_000,
795 SwapMode::ExactOut,
796 U128::from(1u128 << 64),
797 true,
798 input,
799 50_000,
800 output,
801 500,
802 );
803 assert_eq!(result, Err(ARITHMETIC_OVERFLOW));
804 }
805
806 #[test]
807 fn test_swap_quote_zero_prices_exit_loop() {
808 let vault = skew_vault(0, 1_000_000, SkewExponent::Linear);
810 let quote = swap_quote(
811 400,
812 SwapMode::ExactIn,
813 U128::from(1u128 << 64),
814 true,
815 vault,
816 0,
817 vault,
818 64_000,
819 )
820 .unwrap();
821 assert_eq!(quote.amount_in, 0);
822 assert_eq!(quote.amount_out, 0);
823 assert_eq!(
824 swap_quote(
825 400,
826 SwapMode::ExactIn,
827 U128::from(1u128 << 64),
828 false,
829 vault,
830 0,
831 vault,
832 64_000,
833 ),
834 Err(PARTIAL_FILL_NOT_ALLOWED)
835 );
836 }
837
838 #[test]
839 fn test_swap_quote_both_away_stacks() {
840 let input = skew_vault(32_000, 0, SkewExponent::Linear);
841 let output = skew_vault(32_000, 0, SkewExponent::Linear);
842 let one_away = swap_quote(
843 400,
844 SwapMode::ExactIn,
845 U128::from(1u128 << 64),
846 true,
847 input,
848 55_501,
849 dead_zone_vault(0, u64::MAX),
850 32_000,
851 )
852 .unwrap();
853 let both_away = swap_quote(
854 400,
855 SwapMode::ExactIn,
856 U128::from(1u128 << 64),
857 true,
858 input,
859 55_501,
860 output,
861 8_000,
862 )
863 .unwrap();
864 assert_eq!(one_away.amount_out, 406);
865 assert!(both_away.amount_out > one_away.amount_out);
866 }
867}