1use alloy_primitives::U256;
30
31use crate::evm::protocol::curve::{adapter::CurveVariant, math::Pool};
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum BuildError {
36 MissingField { variant: CurveVariant, field: &'static str },
38 WrongCoinCount { variant: CurveVariant, expected: usize, actual: usize },
40 DecimalsMismatch { balances_len: usize, decimals_len: usize },
42 DecimalsTooLarge { index: usize, decimals: u8, max: u8 },
44 DynamicRatesMismatch { balances_len: usize, rates_len: usize },
46 PriceScaleWrongLen { expected: usize, actual: usize },
48 MetaMissingVirtualPrice,
53}
54
55impl std::fmt::Display for BuildError {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Self::MissingField { variant, field } => {
59 write!(f, "{variant}: missing required field `{field}`")
60 }
61 Self::WrongCoinCount { variant, expected, actual } => {
62 write!(f, "{variant}: expected {expected} coins, got {actual} balances")
63 }
64 Self::DecimalsMismatch { balances_len, decimals_len } => write!(
65 f,
66 "token_decimals length ({decimals_len}) != balances length ({balances_len})"
67 ),
68 Self::DecimalsTooLarge { index, decimals, max } => {
69 write!(f, "token_decimals[{index}] = {decimals} exceeds maximum {max}")
70 }
71 Self::DynamicRatesMismatch { balances_len, rates_len } => {
72 write!(f, "dynamic_rates length ({rates_len}) != balances length ({balances_len})")
73 }
74 Self::PriceScaleWrongLen { expected, actual } => {
75 write!(f, "price_scale: expected {expected} elements, got {actual}")
76 }
77 Self::MetaMissingVirtualPrice => write!(
78 f,
79 "StableSwapMeta: dynamic_rates must provide an explicit rate for the last coin \
80 (base pool LP token virtual_price). Without it, swap calculations are incorrect."
81 ),
82 }
83 }
84}
85
86impl std::error::Error for BuildError {}
87
88#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
94pub struct RawPoolState {
95 pub variant: CurveVariant,
97
98 #[serde(default)]
100 pub admin_fee: Option<U256>,
101
102 pub balances: Vec<U256>,
116
117 pub token_decimals: Vec<u8>,
120
121 pub amp: U256,
166
167 pub fee: Option<U256>,
170
171 pub mid_fee: Option<U256>,
173
174 pub out_fee: Option<U256>,
176
177 pub fee_gamma: Option<U256>,
179
180 pub offpeg_fee_multiplier: Option<U256>,
182
183 pub price_scale: Option<Vec<U256>>,
187
188 pub d: Option<U256>,
190
191 pub gamma: Option<U256>,
195
196 pub dynamic_rates: Option<Vec<Option<U256>>>,
227
228 pub precisions: Option<Vec<U256>>,
239
240 pub eth_variant: Option<bool>,
252}
253
254impl Default for RawPoolState {
255 fn default() -> Self {
256 Self {
257 variant: CurveVariant::StableSwapV2,
258 balances: Vec::new(),
259 token_decimals: Vec::new(),
260 amp: U256::ZERO,
261 fee: None,
262 admin_fee: None,
263 mid_fee: None,
264 out_fee: None,
265 fee_gamma: None,
266 offpeg_fee_multiplier: None,
267 price_scale: None,
268 d: None,
269 gamma: None,
270 dynamic_rates: None,
271 precisions: None,
272 eth_variant: None,
273 }
274 }
275}
276
277pub fn interpolate_a(
303 initial_a: U256,
304 future_a: U256,
305 initial_a_time: u64,
306 future_a_time: u64,
307 block_timestamp: u64,
308) -> U256 {
309 if block_timestamp >= future_a_time {
310 return future_a;
311 }
312
313 let elapsed = U256::from(block_timestamp - initial_a_time);
316 let duration = U256::from(future_a_time - initial_a_time);
317
318 if future_a > initial_a {
319 initial_a + (future_a - initial_a) * elapsed / duration
320 } else {
321 initial_a - (initial_a - future_a) * elapsed / duration
322 }
323}
324
325fn compute_stableswap_rates(
334 token_decimals: &[u8],
335 dynamic_rates: &Option<Vec<Option<U256>>>,
336) -> Vec<U256> {
337 token_decimals
338 .iter()
339 .enumerate()
340 .map(|(i, &decimals)| {
341 if let Some(ref rates) = dynamic_rates {
343 if let Some(Some(rate)) = rates.get(i) {
344 return *rate;
345 }
346 }
347 U256::from(10u64).pow(U256::from(36 - decimals as u32))
349 })
350 .collect()
351}
352
353fn compute_crypto_precisions(token_decimals: &[u8]) -> Vec<U256> {
360 token_decimals
361 .iter()
362 .map(|&decimals| U256::from(10u64).pow(U256::from(18 - decimals as u32)))
363 .collect()
364}
365
366macro_rules! require {
368 ($state:expr, $field:ident) => {
369 $state
370 .$field
371 .ok_or(BuildError::MissingField {
372 variant: $state.variant,
373 field: stringify!($field),
374 })?
375 };
376}
377
378pub fn build_pool(state: &RawPoolState) -> Result<Pool, BuildError> {
387 if state.balances.len() != state.token_decimals.len() {
389 return Err(BuildError::DecimalsMismatch {
390 balances_len: state.balances.len(),
391 decimals_len: state.token_decimals.len(),
392 });
393 }
394
395 if let Some(ref dr) = state.dynamic_rates {
397 if dr.len() != state.balances.len() {
398 return Err(BuildError::DynamicRatesMismatch {
399 balances_len: state.balances.len(),
400 rates_len: dr.len(),
401 });
402 }
403 }
404
405 match state.variant {
406 CurveVariant::StableSwapV0 |
407 CurveVariant::StableSwapV1 |
408 CurveVariant::StableSwapV2 |
409 CurveVariant::StableSwapSTETH |
410 CurveVariant::StableSwapMeta => build_stableswap_plain(state),
411 CurveVariant::StableSwapNG => build_stableswap_ng(state),
412 CurveVariant::StableSwapALend => build_stableswap_alend(state),
413 CurveVariant::TwoCryptoV1 | CurveVariant::TwoCryptoNG | CurveVariant::TwoCryptoStable => {
414 build_twocrypto(state)
415 }
416 CurveVariant::TriCryptoV1 | CurveVariant::TriCryptoNG => build_tricrypto(state),
417 }
418}
419
420fn build_stableswap_plain(state: &RawPoolState) -> Result<Pool, BuildError> {
423 let fee = require!(state, fee);
424
425 for (i, &d) in state.token_decimals.iter().enumerate() {
426 if d > 36 {
427 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 36 });
428 }
429 }
430
431 if state.variant == CurveVariant::StableSwapMeta {
434 let n = state.balances.len();
435 let has_vp = state
436 .dynamic_rates
437 .as_ref()
438 .and_then(|dr| dr.get(n - 1))
439 .map(|r| r.is_some())
440 .unwrap_or(false);
441 if !has_vp {
442 return Err(BuildError::MetaMissingVirtualPrice);
443 }
444 }
445
446 let rates = compute_stableswap_rates(&state.token_decimals, &state.dynamic_rates);
447 let balances = state.balances.clone();
448 let amp = state.amp;
449
450 Ok(match state.variant {
451 CurveVariant::StableSwapV0 => Pool::StableSwapV0 { balances, rates, amp, fee },
452 CurveVariant::StableSwapV1 => Pool::StableSwapV1 { balances, rates, amp, fee },
453 CurveVariant::StableSwapV2 => Pool::StableSwapV2 { balances, rates, amp, fee },
454 CurveVariant::StableSwapSTETH => Pool::StableSwapSTETH { balances, rates, amp, fee },
455 CurveVariant::StableSwapMeta => Pool::StableSwapMeta { balances, rates, amp, fee },
456 _ => unreachable!(),
457 })
458}
459
460fn build_stableswap_ng(state: &RawPoolState) -> Result<Pool, BuildError> {
461 let fee = require!(state, fee);
462 let offpeg = state
467 .offpeg_fee_multiplier
468 .unwrap_or(U256::from(10_000_000_000u64));
469
470 for (i, &d) in state.token_decimals.iter().enumerate() {
471 if d > 36 {
472 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 36 });
473 }
474 }
475
476 let rates = compute_stableswap_rates(&state.token_decimals, &state.dynamic_rates);
477
478 Ok(Pool::StableSwapNG {
479 balances: state.balances.clone(),
480 rates,
481 amp: state.amp,
482 fee,
483 offpeg_fee_multiplier: offpeg,
484 })
485}
486
487fn build_stableswap_alend(state: &RawPoolState) -> Result<Pool, BuildError> {
488 let fee = require!(state, fee);
489 let offpeg = require!(state, offpeg_fee_multiplier);
490
491 for (i, &d) in state.token_decimals.iter().enumerate() {
492 if d > 18 {
493 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
494 }
495 }
496
497 let precision_mul = compute_crypto_precisions(&state.token_decimals);
498
499 Ok(Pool::StableSwapALend {
500 balances: state.balances.clone(),
501 precision_mul,
502 amp: state.amp,
503 fee,
504 offpeg_fee_multiplier: offpeg,
505 })
506}
507
508fn build_twocrypto(state: &RawPoolState) -> Result<Pool, BuildError> {
509 if state.balances.len() != 2 {
510 return Err(BuildError::WrongCoinCount {
511 variant: state.variant,
512 expected: 2,
513 actual: state.balances.len(),
514 });
515 }
516
517 for (i, &d) in state.token_decimals.iter().enumerate() {
518 if d > 18 {
519 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
520 }
521 }
522
523 let mid_fee = require!(state, mid_fee);
524 let out_fee = require!(state, out_fee);
525 let fee_gamma = require!(state, fee_gamma);
526 let d = require!(state, d);
527 let ann = state.amp;
528
529 let price_scale_vec = state
530 .price_scale
531 .as_ref()
532 .ok_or(BuildError::MissingField { variant: state.variant, field: "price_scale" })?;
533 if price_scale_vec.len() != 1 {
534 return Err(BuildError::PriceScaleWrongLen { expected: 1, actual: price_scale_vec.len() });
535 }
536 let price_scale = price_scale_vec[0];
537
538 let default_precs = compute_crypto_precisions(&state.token_decimals);
539 let precisions = state
540 .precisions
541 .as_deref()
542 .unwrap_or(&default_precs);
543 let balances: [U256; 2] = [state.balances[0], state.balances[1]];
544 let prec_arr: [U256; 2] = [precisions[0], precisions[1]];
545
546 match state.variant {
547 CurveVariant::TwoCryptoV1 => {
548 let gamma = require!(state, gamma);
549 let eth_variant = require!(state, eth_variant);
552 Ok(Pool::TwoCryptoV1 {
553 balances,
554 precisions: prec_arr,
555 price_scale,
556 d,
557 ann,
558 gamma,
559 mid_fee,
560 out_fee,
561 fee_gamma,
562 eth_variant,
563 })
564 }
565 CurveVariant::TwoCryptoNG => {
566 let gamma = require!(state, gamma);
567 Ok(Pool::TwoCryptoNG {
568 balances,
569 precisions: prec_arr,
570 price_scale,
571 d,
572 ann,
573 gamma,
574 mid_fee,
575 out_fee,
576 fee_gamma,
577 })
578 }
579 CurveVariant::TwoCryptoStable => Ok(Pool::TwoCryptoStable {
580 balances,
581 precisions: prec_arr,
582 price_scale,
583 d,
584 ann,
585 mid_fee,
586 out_fee,
587 fee_gamma,
588 }),
589 _ => unreachable!("build_twocrypto called for non-twocrypto variant"),
590 }
591}
592
593fn build_tricrypto(state: &RawPoolState) -> Result<Pool, BuildError> {
594 if state.balances.len() != 3 {
595 return Err(BuildError::WrongCoinCount {
596 variant: state.variant,
597 expected: 3,
598 actual: state.balances.len(),
599 });
600 }
601
602 for (i, &d) in state.token_decimals.iter().enumerate() {
603 if d > 18 {
604 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
605 }
606 }
607
608 let mid_fee = require!(state, mid_fee);
609 let out_fee = require!(state, out_fee);
610 let fee_gamma = require!(state, fee_gamma);
611 let d = require!(state, d);
612 let gamma = require!(state, gamma);
613 let ann = state.amp;
614
615 let price_scale_vec = state
616 .price_scale
617 .as_ref()
618 .ok_or(BuildError::MissingField { variant: state.variant, field: "price_scale" })?;
619 if price_scale_vec.len() != 2 {
620 return Err(BuildError::PriceScaleWrongLen { expected: 2, actual: price_scale_vec.len() });
621 }
622 let price_scale: [U256; 2] = [price_scale_vec[0], price_scale_vec[1]];
623
624 let default_precs = compute_crypto_precisions(&state.token_decimals);
625 let precisions = state
626 .precisions
627 .as_deref()
628 .unwrap_or(&default_precs);
629 let balances: [U256; 3] = [state.balances[0], state.balances[1], state.balances[2]];
630 let prec_arr: [U256; 3] = [precisions[0], precisions[1], precisions[2]];
631
632 match state.variant {
633 CurveVariant::TriCryptoV1 => Ok(Pool::TriCryptoV1 {
634 balances,
635 precisions: prec_arr,
636 price_scale,
637 d,
638 ann,
639 gamma,
640 mid_fee,
641 out_fee,
642 fee_gamma,
643 }),
644 CurveVariant::TriCryptoNG => Ok(Pool::TriCryptoNG {
645 balances,
646 precisions: prec_arr,
647 price_scale,
648 d,
649 ann,
650 gamma,
651 mid_fee,
652 out_fee,
653 fee_gamma,
654 }),
655 _ => unreachable!("build_tricrypto called for non-tricrypto variant"),
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn interpolate_a_no_ramp() {
665 let a = U256::from(40_000u64);
667 let result = interpolate_a(a, a, 1000, 2000, 1500);
668 assert_eq!(result, a);
669 }
670
671 #[test]
672 fn interpolate_a_ramp_complete() {
673 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 3000);
675 assert_eq!(result, U256::from(40_000u64));
676 }
677
678 #[test]
679 fn interpolate_a_ramp_exactly_at_end() {
680 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 2000);
681 assert_eq!(result, U256::from(40_000u64));
682 }
683
684 #[test]
685 fn interpolate_a_ramp_up_midpoint() {
686 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 1500);
689 assert_eq!(result, U256::from(30_000u64));
690 }
691
692 #[test]
693 fn interpolate_a_ramp_down_midpoint() {
694 let result = interpolate_a(U256::from(40_000u64), U256::from(20_000u64), 1000, 2000, 1500);
697 assert_eq!(result, U256::from(30_000u64));
698 }
699
700 #[test]
701 fn interpolate_a_ramp_up_quarter() {
702 let result = interpolate_a(U256::from(10_000u64), U256::from(50_000u64), 0, 1000, 250);
705 assert_eq!(result, U256::from(20_000u64));
706 }
707
708 #[test]
709 fn interpolate_a_ramp_at_start() {
710 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 1000);
712 assert_eq!(result, U256::from(20_000u64));
713 }
714
715 #[test]
716 fn interpolate_a_integer_division_truncation() {
717 let result = interpolate_a(U256::from(10_000u64), U256::from(10_003u64), 0, 1000, 1);
721 assert_eq!(result, U256::from(10_000u64));
722 }
723
724 #[test]
725 fn build_stableswap_v0_basic() {
726 let state = RawPoolState {
727 variant: CurveVariant::StableSwapV0,
728 balances: vec![
729 U256::from(1_000_000_000_000_000_000_000u128), U256::from(1_000_000_000u128), U256::from(1_000_000_000u128), U256::from(1_000_000_000_000_000_000_000u128), ],
734 token_decimals: vec![18, 6, 6, 18],
735 amp: U256::from(200u64), fee: Some(U256::from(4_000_000u64)),
737 ..Default::default()
738 };
739
740 let pool = build_pool(&state).unwrap();
741 let dy = pool.get_amount_out(0, 1, U256::from(1_000_000_000_000_000_000u128));
743 assert!(dy.is_some());
744 }
745
746 #[test]
747 fn build_stableswap_v2_rates_18_6() {
748 let state = RawPoolState {
749 variant: CurveVariant::StableSwapV2,
750 balances: vec![
751 U256::from(1_000_000_000_000_000_000_000u128),
752 U256::from(1_000_000_000u128),
753 ],
754 token_decimals: vec![18, 6],
755 amp: U256::from(40_000u64), fee: Some(U256::from(4_000_000u64)),
757 ..Default::default()
758 };
759
760 let pool = build_pool(&state).unwrap();
761
762 match &pool {
764 Pool::StableSwapV2 { rates, .. } => {
765 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
766 assert_eq!(rates[1], U256::from(10u64).pow(U256::from(30u64)));
767 }
768 _ => panic!("wrong variant"),
769 }
770 }
771
772 #[test]
773 fn build_stableswap_ng_with_dynamic_rates() {
774 let oracle_rate = U256::from(1_050_000_000_000_000_000u128); let state = RawPoolState {
776 variant: CurveVariant::StableSwapNG,
777 balances: vec![
778 U256::from(1_000_000_000_000_000_000_000u128),
779 U256::from(1_000_000_000_000_000_000_000u128),
780 ],
781 token_decimals: vec![18, 18],
782 amp: U256::from(150_000u64),
783 fee: Some(U256::from(4_000_000u64)),
784 offpeg_fee_multiplier: Some(U256::from(20_000_000_000u128)),
785 dynamic_rates: Some(vec![
786 None, Some(oracle_rate), ]),
789 ..Default::default()
790 };
791
792 let pool = build_pool(&state).unwrap();
793 match &pool {
794 Pool::StableSwapNG { rates, .. } => {
795 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
796 assert_eq!(rates[1], oracle_rate);
797 }
798 _ => panic!("wrong variant"),
799 }
800 }
801
802 #[test]
803 fn build_stableswap_alend_precision_mul() {
804 let state = RawPoolState {
805 variant: CurveVariant::StableSwapALend,
806 balances: vec![
807 U256::from(1_000_000_000_000_000_000_000u128),
808 U256::from(1_000_000_000_000_000_000_000u128),
809 ],
810 token_decimals: vec![18, 18],
811 amp: U256::from(10_000u64), fee: Some(U256::from(4_000_000u64)),
813 offpeg_fee_multiplier: Some(U256::from(20_000_000_000u128)),
814 ..Default::default()
815 };
816
817 let pool = build_pool(&state).unwrap();
818 match &pool {
819 Pool::StableSwapALend { precision_mul, .. } => {
820 assert_eq!(precision_mul[0], U256::from(1u64));
822 assert_eq!(precision_mul[1], U256::from(1u64));
823 }
824 _ => panic!("wrong variant"),
825 }
826 }
827
828 #[test]
829 fn build_stableswap_meta_virtual_price_rate() {
830 let virtual_price = U256::from(1_020_000_000_000_000_000u128); let state = RawPoolState {
832 variant: CurveVariant::StableSwapMeta,
833 balances: vec![
834 U256::from(1_000_000_000u128), U256::from(1_000_000_000_000_000_000_000u128), ],
837 token_decimals: vec![2, 18],
838 amp: U256::from(150_000u64),
839 fee: Some(U256::from(4_000_000u64)),
840 dynamic_rates: Some(vec![
841 None, Some(virtual_price), ]),
844 ..Default::default()
845 };
846
847 let pool = build_pool(&state).unwrap();
848 match &pool {
849 Pool::StableSwapMeta { rates, .. } => {
850 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(34u64)));
851 assert_eq!(rates[1], virtual_price);
852 }
853 _ => panic!("wrong variant"),
854 }
855 }
856
857 #[test]
858 fn build_twocrypto_ng_basic() {
859 let state = RawPoolState {
860 variant: CurveVariant::TwoCryptoNG,
861 balances: vec![
862 U256::from(1_000_000_000_000_000_000_000u128),
863 U256::from(1_000_000_000_000_000_000_000u128),
864 ],
865 token_decimals: vec![18, 18],
866 amp: U256::from(540_000u64 * 10_000u64), mid_fee: Some(U256::from(3_000_000u64)),
868 out_fee: Some(U256::from(30_000_000u64)),
869 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
870 d: Some(U256::from(2_000_000_000_000_000_000_000u128)),
871 gamma: Some(U256::from(10_000_000_000_000u128)),
872 price_scale: Some(vec![U256::from(1_000_000_000_000_000_000u128)]),
873 ..Default::default()
874 };
875
876 let pool = build_pool(&state).unwrap();
877 match &pool {
878 Pool::TwoCryptoNG { precisions, ann, .. } => {
879 assert_eq!(precisions[0], U256::from(1u64)); assert_eq!(precisions[1], U256::from(1u64));
881 assert_eq!(*ann, state.amp);
882 }
883 _ => panic!("wrong variant"),
884 }
885 }
886
887 #[test]
888 fn build_twocrypto_stable_no_gamma() {
889 let state = RawPoolState {
890 variant: CurveVariant::TwoCryptoStable,
891 balances: vec![U256::from(1_000_000_000u128), U256::from(1_000_000_000u128)],
892 token_decimals: vec![6, 6],
893 amp: U256::from(540_000u64 * 10_000u64),
894 mid_fee: Some(U256::from(3_000_000u64)),
895 out_fee: Some(U256::from(30_000_000u64)),
896 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
897 d: Some(U256::from(2_000_000_000u128)),
898 price_scale: Some(vec![U256::from(1_000_000_000_000_000_000u128)]),
900 ..Default::default()
901 };
902
903 let pool = build_pool(&state).unwrap();
904 match &pool {
905 Pool::TwoCryptoStable { precisions, .. } => {
906 assert_eq!(precisions[0], U256::from(10u64).pow(U256::from(12u64)));
908 }
909 _ => panic!("wrong variant"),
910 }
911 }
912
913 #[test]
914 fn build_tricrypto_ng_basic() {
915 let state = RawPoolState {
916 variant: CurveVariant::TriCryptoNG,
917 balances: vec![
918 U256::from(1_000_000_000u128), U256::from(50_000_000u128), U256::from(500_000_000_000_000_000_000u128), ],
922 token_decimals: vec![6, 8, 18],
923 amp: U256::from(1_707_629u64 * 10_000u64),
924 mid_fee: Some(U256::from(3_000_000u64)),
925 out_fee: Some(U256::from(30_000_000u64)),
926 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
927 d: Some(U256::from(3_000_000_000_000_000_000_000u128)),
928 gamma: Some(U256::from(11_809_167_828_997u128)),
929 price_scale: Some(vec![
930 U256::from(60_000_000_000_000_000_000_000u128), U256::from(3_000_000_000_000_000_000_000u128), ]),
933 ..Default::default()
934 };
935
936 let pool = build_pool(&state).unwrap();
937 match &pool {
938 Pool::TriCryptoNG { precisions, price_scale, .. } => {
939 assert_eq!(precisions[0], U256::from(10u64).pow(U256::from(12u64))); assert_eq!(precisions[1], U256::from(10u64).pow(U256::from(10u64))); assert_eq!(precisions[2], U256::from(1u64)); assert_eq!(price_scale.len(), 2);
943 }
944 _ => panic!("wrong variant"),
945 }
946 }
947
948 #[test]
949 fn build_missing_fee_returns_error() {
950 let state = RawPoolState {
951 variant: CurveVariant::StableSwapV2,
952 balances: vec![U256::from(1u64), U256::from(1u64)],
953 token_decimals: vec![18, 18],
954 amp: U256::from(40_000u64),
955 ..Default::default()
957 };
958
959 let err = match build_pool(&state) {
960 Err(e) => e,
961 Ok(_) => panic!("expected error"),
962 };
963 assert!(matches!(err, BuildError::MissingField { field: "fee", .. }));
964 }
965
966 #[test]
967 fn build_decimals_mismatch_returns_error() {
968 let state = RawPoolState {
969 variant: CurveVariant::StableSwapV2,
970 balances: vec![U256::from(1u64), U256::from(1u64)],
971 token_decimals: vec![18], amp: U256::from(40_000u64),
973 fee: Some(U256::from(4_000_000u64)),
974 ..Default::default()
975 };
976
977 let err = match build_pool(&state) {
978 Err(e) => e,
979 Ok(_) => panic!("expected error"),
980 };
981 assert!(matches!(err, BuildError::DecimalsMismatch { .. }));
982 }
983
984 #[test]
985 fn build_twocrypto_wrong_coin_count() {
986 let state = RawPoolState {
987 variant: CurveVariant::TwoCryptoNG,
988 balances: vec![U256::from(1u64), U256::from(1u64), U256::from(1u64)],
989 token_decimals: vec![18, 18, 18],
990 amp: U256::from(1u64),
991 mid_fee: Some(U256::from(1u64)),
992 out_fee: Some(U256::from(1u64)),
993 fee_gamma: Some(U256::from(1u64)),
994 d: Some(U256::from(1u64)),
995 gamma: Some(U256::from(1u64)),
996 price_scale: Some(vec![U256::from(1u64)]),
997 ..Default::default()
998 };
999
1000 let err = match build_pool(&state) {
1001 Err(e) => e,
1002 Ok(_) => panic!("expected error"),
1003 };
1004 assert!(matches!(err, BuildError::WrongCoinCount { expected: 2, actual: 3, .. }));
1005 }
1006
1007 #[test]
1008 fn build_tricrypto_wrong_price_scale_len() {
1009 let state = RawPoolState {
1010 variant: CurveVariant::TriCryptoNG,
1011 balances: vec![U256::from(1u64), U256::from(1u64), U256::from(1u64)],
1012 token_decimals: vec![6, 8, 18],
1013 amp: U256::from(1u64),
1014 mid_fee: Some(U256::from(1u64)),
1015 out_fee: Some(U256::from(1u64)),
1016 fee_gamma: Some(U256::from(1u64)),
1017 d: Some(U256::from(1u64)),
1018 gamma: Some(U256::from(1u64)),
1019 price_scale: Some(vec![U256::from(1u64)]), ..Default::default()
1021 };
1022
1023 let err = match build_pool(&state) {
1024 Err(e) => e,
1025 Ok(_) => panic!("expected error"),
1026 };
1027 assert!(matches!(err, BuildError::PriceScaleWrongLen { expected: 2, actual: 1 }));
1028 }
1029
1030 #[test]
1031 fn build_ng_without_offpeg_defaults_to_fee_denominator() {
1032 let state = RawPoolState {
1034 variant: CurveVariant::StableSwapNG,
1035 balances: vec![U256::from(1u64), U256::from(1u64)],
1036 token_decimals: vec![18, 18],
1037 amp: U256::from(40_000u64),
1038 fee: Some(U256::from(4_000_000u64)),
1039 ..Default::default()
1041 };
1042
1043 let pool = build_pool(&state).expect("should succeed with defaulted offpeg");
1044 assert_eq!(pool.offpeg_fee_multiplier(), Some(U256::from(10_000_000_000u64)));
1046 }
1047
1048 #[test]
1049 fn build_ng_crvusd_sdai_matches_on_chain() {
1050 let state = RawPoolState {
1053 variant: CurveVariant::StableSwapNG,
1054 balances: vec![
1055 "3219009600398261994"
1056 .parse::<U256>()
1057 .expect("balance 0"),
1058 "311156701443769568"
1059 .parse::<U256>()
1060 .expect("balance 1"),
1061 ],
1062 token_decimals: vec![18, 18],
1063 amp: U256::from(150_000u64),
1064 fee: Some(U256::from(1_000_000u64)),
1065 dynamic_rates: Some(vec![
1067 Some(
1068 "1000000000000000000"
1069 .parse::<U256>()
1070 .expect("rate 0"),
1071 ),
1072 Some(
1073 "1173627645818786870"
1074 .parse::<U256>()
1075 .expect("rate 1"),
1076 ),
1077 ]),
1078 ..Default::default()
1079 };
1080
1081 let pool = build_pool(&state).expect("should build with defaulted offpeg");
1082 let dy = pool
1083 .get_amount_out(0, 1, U256::from(3_219_009_600_398_261u64))
1084 .expect("swap should succeed");
1085
1086 let expected = U256::from(2_720_818_166_217_034u64);
1088 let diff = if dy > expected { dy - expected } else { expected - dy };
1089 assert!(diff <= U256::from(1u64), "mismatch: got {dy}, expected {expected}, diff {diff}");
1090 }
1091
1092 #[test]
1093 fn build_meta_without_virtual_price_fails() {
1094 let state = RawPoolState {
1097 variant: CurveVariant::StableSwapMeta,
1098 balances: vec![
1099 U256::from(1_000_000_000u128),
1100 U256::from(1_000_000_000_000_000_000_000u128),
1101 ],
1102 token_decimals: vec![2, 18],
1103 amp: U256::from(150_000u64),
1104 fee: Some(U256::from(4_000_000u64)),
1105 ..Default::default()
1107 };
1108
1109 let err = match build_pool(&state) {
1110 Err(e) => e,
1111 Ok(_) => panic!("expected MetaMissingVirtualPrice error"),
1112 };
1113 assert!(matches!(err, BuildError::MetaMissingVirtualPrice));
1114 }
1115
1116 #[test]
1117 fn build_meta_with_partial_dynamic_rates_missing_vp_fails() {
1118 let state = RawPoolState {
1120 variant: CurveVariant::StableSwapMeta,
1121 balances: vec![
1122 U256::from(1_000_000_000u128),
1123 U256::from(1_000_000_000_000_000_000_000u128),
1124 ],
1125 token_decimals: vec![2, 18],
1126 amp: U256::from(150_000u64),
1127 fee: Some(U256::from(4_000_000u64)),
1128 dynamic_rates: Some(vec![None, None]), ..Default::default()
1130 };
1131
1132 let err = match build_pool(&state) {
1133 Err(e) => e,
1134 Ok(_) => panic!("expected MetaMissingVirtualPrice error"),
1135 };
1136 assert!(matches!(err, BuildError::MetaMissingVirtualPrice));
1137 }
1138
1139 #[test]
1140 fn rates_match_fuzz_registry_18_dec() {
1141 let rates = super::compute_stableswap_rates(&[18], &None);
1143 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
1144 }
1145
1146 #[test]
1147 fn rates_match_fuzz_registry_6_dec() {
1148 let rates = super::compute_stableswap_rates(&[6], &None);
1150 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(30u64)));
1151 }
1152
1153 #[test]
1154 fn rates_match_fuzz_registry_8_dec() {
1155 let rates = super::compute_stableswap_rates(&[8], &None);
1157 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(28u64)));
1158 }
1159
1160 #[test]
1161 fn rates_match_fuzz_registry_2_dec() {
1162 let rates = super::compute_stableswap_rates(&[2], &None);
1164 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(34u64)));
1165 }
1166
1167 #[test]
1168 fn precisions_match_fuzz_registry() {
1169 let precs = super::compute_crypto_precisions(&[6, 8, 18]);
1171 assert_eq!(precs[0], U256::from(10u64).pow(U256::from(12u64))); assert_eq!(precs[1], U256::from(10u64).pow(U256::from(10u64))); assert_eq!(precs[2], U256::from(1u64)); }
1175
1176 #[test]
1177 fn precision_mul_matches_fuzz_registry() {
1178 let pm = super::compute_crypto_precisions(&[18, 6]);
1180 assert_eq!(pm[0], U256::from(1u64));
1181 assert_eq!(pm[1], U256::from(10u64).pow(U256::from(12u64)));
1182 }
1183
1184 #[test]
1185 fn build_all_11_variants_succeed() {
1186 let stableswap_base = |variant: CurveVariant| -> RawPoolState {
1188 RawPoolState {
1189 variant,
1190 balances: vec![U256::from(1_000_000_000_000_000_000u128); 2],
1191 token_decimals: vec![18, 18],
1192 amp: U256::from(40_000u64),
1193 fee: Some(U256::from(4_000_000u64)),
1194 ..Default::default()
1195 }
1196 };
1197
1198 for v in
1200 [CurveVariant::StableSwapV0, CurveVariant::StableSwapV1, CurveVariant::StableSwapV2]
1201 {
1202 assert!(build_pool(&stableswap_base(v)).is_ok(), "failed for {v}");
1203 }
1204
1205 let mut meta = stableswap_base(CurveVariant::StableSwapMeta);
1207 meta.dynamic_rates = Some(vec![None, Some(U256::from(10u64).pow(U256::from(18u64)))]);
1208 assert!(build_pool(&meta).is_ok(), "failed for StableSwapMeta");
1209
1210 let mut ng = stableswap_base(CurveVariant::StableSwapNG);
1212 ng.offpeg_fee_multiplier = Some(U256::from(20_000_000_000u128));
1213 assert!(build_pool(&ng).is_ok(), "failed for StableSwapNG");
1214
1215 let mut alend = stableswap_base(CurveVariant::StableSwapALend);
1217 alend.offpeg_fee_multiplier = Some(U256::from(20_000_000_000u128));
1218 assert!(build_pool(&alend).is_ok(), "failed for StableSwapALend");
1219
1220 let crypto_base = |variant: CurveVariant, n: usize| -> RawPoolState {
1222 RawPoolState {
1223 variant,
1224 balances: vec![U256::from(1_000_000_000_000_000_000u128); n],
1225 token_decimals: vec![18; n],
1226 amp: U256::from(540_000u64 * 10_000u64),
1227 mid_fee: Some(U256::from(3_000_000u64)),
1228 out_fee: Some(U256::from(30_000_000u64)),
1229 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
1230 d: Some(U256::from(2_000_000_000_000_000_000_000u128)),
1231 gamma: Some(U256::from(10_000_000_000_000u128)),
1232 price_scale: Some(if n == 2 {
1233 vec![U256::from(10u64).pow(U256::from(18u64))]
1234 } else {
1235 vec![U256::from(10u64).pow(U256::from(18u64)); n - 1]
1236 }),
1237 eth_variant: Some(true),
1239 ..Default::default()
1240 }
1241 };
1242
1243 for v in [CurveVariant::TwoCryptoV1, CurveVariant::TwoCryptoNG] {
1245 assert!(build_pool(&crypto_base(v, 2)).is_ok(), "failed for {v}");
1246 }
1247
1248 let mut tcs = crypto_base(CurveVariant::TwoCryptoStable, 2);
1250 tcs.gamma = None;
1251 assert!(build_pool(&tcs).is_ok(), "failed for TwoCryptoStable");
1252
1253 for v in [CurveVariant::TriCryptoV1, CurveVariant::TriCryptoNG] {
1255 assert!(build_pool(&crypto_base(v, 3)).is_ok(), "failed for {v}");
1256 }
1257 }
1258
1259 fn u(s: &str) -> U256 {
1270 U256::from_str_radix(s, 10).unwrap()
1271 }
1272
1273 #[test]
1274 fn integration_stableswap_v0_susd() {
1275 let state = RawPoolState {
1277 variant: CurveVariant::StableSwapV0,
1278 balances: vec![
1279 u("1919848022082255699479"),
1280 u("1920322445"),
1281 u("1920171938"),
1282 u("21038816168255729764832232005"),
1283 ],
1284 token_decimals: vec![18, 6, 6, 18],
1285 amp: U256::from(256u64),
1286 fee: Some(U256::from(2_000_000u64)),
1287 ..Default::default()
1288 };
1289 let pool = build_pool(&state).unwrap();
1290 let dy = pool
1291 .get_amount_out(0, 1, u("19198480220822556994"))
1292 .unwrap();
1293 assert_eq!(dy, U256::from(19_009_291u64));
1294 }
1295
1296 #[test]
1297 fn integration_stableswap_v1_3pool() {
1298 let state = RawPoolState {
1300 variant: CurveVariant::StableSwapV1,
1301 balances: vec![
1302 u("45102835177280382580138407"),
1303 u("45853975278310"),
1304 u("72989152672276"),
1305 ],
1306 token_decimals: vec![18, 6, 6],
1307 amp: U256::from(4000u64),
1308 fee: Some(U256::from(1_500_000u64)),
1309 ..Default::default()
1310 };
1311 let pool = build_pool(&state).unwrap();
1312 let dy = pool
1313 .get_amount_out(0, 1, u("451028351772803825801384"))
1314 .unwrap();
1315 assert_eq!(dy, u("450961663745"));
1316 }
1317
1318 #[test]
1319 fn integration_stableswap_v2_frax_usdc() {
1320 let state = RawPoolState {
1322 variant: CurveVariant::StableSwapV2,
1323 balances: vec![u("6722234569994793202271485"), u("714493991383")],
1324 token_decimals: vec![18, 6],
1325 amp: U256::from(150_000u64),
1326 fee: Some(U256::from(1_000_000u64)),
1327 ..Default::default()
1328 };
1329 let pool = build_pool(&state).unwrap();
1330 let dy = pool
1331 .get_amount_out(0, 1, u("67222345699947932022714"))
1332 .unwrap();
1333 assert_eq!(dy, u("66561674655"));
1334 }
1335
1336 #[test]
1337 fn integration_stableswap_alend_aave() {
1338 let state = RawPoolState {
1340 variant: CurveVariant::StableSwapALend,
1341 balances: vec![u("968991099162993551077367"), u("1012448901351"), u("414282246850")],
1342 token_decimals: vec![18, 6, 6],
1343 amp: U256::from(200_000u64),
1344 fee: Some(U256::from(4_000_000u64)),
1345 offpeg_fee_multiplier: Some(u("20000000000")),
1346 ..Default::default()
1347 };
1348 let pool = build_pool(&state).unwrap();
1349 let dy = pool
1350 .get_amount_out(0, 1, u("9689910991629935510773"))
1351 .unwrap();
1352 assert_eq!(dy, u("9686201099"));
1353 }
1354
1355 #[test]
1356 fn integration_stableswap_ng_usde_dai() {
1357 let state = RawPoolState {
1359 variant: CurveVariant::StableSwapNG,
1360 balances: vec![u("124403796536542495997070"), u("95031311223261676260348")],
1361 token_decimals: vec![18, 18],
1362 amp: U256::from(40_000u64),
1363 fee: Some(U256::from(4_000_000u64)),
1364 offpeg_fee_multiplier: Some(u("20000000000")),
1365 dynamic_rates: Some(vec![
1366 Some(u("1000000000000000000")),
1367 Some(u("1000000000000000000")),
1368 ]),
1369 ..Default::default()
1370 };
1371 let pool = build_pool(&state).unwrap();
1372 let dy = pool
1373 .get_amount_out(0, 1, u("1244037965365424959970"))
1374 .unwrap();
1375 assert_eq!(dy, u("1242635841481792448583"));
1376 }
1377
1378 #[test]
1379 fn integration_stableswap_meta_gusd_3crv() {
1380 let state = RawPoolState {
1382 variant: CurveVariant::StableSwapMeta,
1383 balances: vec![u("59814423"), u("1210422553896217308280639")],
1384 token_decimals: vec![2, 18],
1385 amp: U256::from(100_000u64),
1386 fee: Some(U256::from(4_000_000u64)),
1387 dynamic_rates: Some(vec![
1388 None, Some(u("1039823717145796146")), ]),
1391 ..Default::default()
1392 };
1393 let pool = build_pool(&state).unwrap();
1394 let dy = pool
1395 .get_amount_out(0, 1, u("598144"))
1396 .unwrap();
1397 assert_eq!(dy, u("5755338887370979902172"));
1398 }
1399
1400 #[test]
1401 fn integration_twocrypto_v1_crv_eth() {
1402 let state = RawPoolState {
1404 variant: CurveVariant::TwoCryptoV1,
1405 balances: vec![u("33389428640766852909"), u("1538654846121127403001612563")],
1406 token_decimals: vec![18, 18],
1407 amp: U256::from(400_000u64),
1408 d: Some(u("3338917956478824050009")),
1409 gamma: Some(u("145000000000000")),
1410 price_scale: Some(vec![u("52805053500476")]),
1411 mid_fee: Some(U256::from(26_000_000u64)),
1412 out_fee: Some(U256::from(45_000_000u64)),
1413 fee_gamma: Some(u("230000000000000")),
1414 eth_variant: Some(true), ..Default::default()
1416 };
1417 let pool = build_pool(&state).unwrap();
1418 let dy = pool
1419 .get_amount_out(0, 1, u("333894286407668529"))
1420 .unwrap();
1421 assert_eq!(dy, u("15024547954512515366680912"));
1422 }
1423
1424 #[test]
1425 fn integration_twocrypto_ng_crvusd_fxn() {
1426 let state = RawPoolState {
1428 variant: CurveVariant::TwoCryptoNG,
1429 balances: vec![u("575304877931995002539"), u("1286854862507061937737")],
1430 token_decimals: vec![18, 18],
1431 amp: U256::from(400_000u64),
1432 d: Some(u("1309807915207365083258")),
1433 gamma: Some(u("145000000000000")),
1434 price_scale: Some(vec![u("578321621819309618")]),
1435 mid_fee: Some(U256::from(26_000_000u64)),
1436 out_fee: Some(U256::from(45_000_000u64)),
1437 fee_gamma: Some(u("230000000000000")),
1438 ..Default::default()
1439 };
1440 let pool = build_pool(&state).unwrap();
1441 let dy = pool
1442 .get_amount_out(0, 1, u("5753048779319950025"))
1443 .unwrap();
1444 assert_eq!(dy, u("12553693226638615366"));
1445 }
1446
1447 #[test]
1448 fn integration_twocrypto_stable_crvusd_weth() {
1449 let state = RawPoolState {
1451 variant: CurveVariant::TwoCryptoStable,
1452 balances: vec![u("17087755783041929282185464"), u("13675635632110845893058")],
1453 token_decimals: vec![18, 18],
1454 amp: U256::from(25_000u64),
1455 d: Some(u("53892663239303863640675237")),
1456 price_scale: Some(vec![u("2783064941591876143844")]),
1457 mid_fee: Some(U256::from(60_000_000u64)),
1458 out_fee: Some(U256::from(220_000_000u64)),
1459 fee_gamma: Some(u("1395000000000000")),
1460 ..Default::default()
1461 };
1462 let pool = build_pool(&state).unwrap();
1463 let dy = pool
1464 .get_amount_out(0, 1, u("170877557830419292821854"))
1465 .unwrap();
1466 assert_eq!(dy, u("77522288630419592645"));
1467 }
1468
1469 #[test]
1470 fn integration_tricrypto_v1_usdt_wbtc_weth() {
1471 let state = RawPoolState {
1473 variant: CurveVariant::TriCryptoV1,
1474 balances: vec![u("3687737692530"), u("5185841754"), u("1696614171366863858308")],
1475 token_decimals: vec![6, 8, 18],
1476 amp: U256::from(1_707_629u64),
1477 d: Some(u("11006845200255249518958282")),
1478 gamma: Some(u("11809167828997")),
1479 price_scale: Some(vec![u("70578404679338064954709"), u("2156666095129214805267")]),
1480 mid_fee: Some(U256::from(3_000_000u64)),
1481 out_fee: Some(U256::from(30_000_000u64)),
1482 fee_gamma: Some(u("500000000000000")),
1483 ..Default::default()
1484 };
1485 let pool = build_pool(&state).unwrap();
1486 let dy = pool
1487 .get_amount_out(0, 1, u("36877376925"))
1488 .unwrap();
1489 assert_eq!(dy, U256::from(51_646_866u64));
1490 }
1491
1492 #[test]
1493 fn integration_tricrypto_ng_usdc_wbtc_weth() {
1494 let state = RawPoolState {
1496 variant: CurveVariant::TriCryptoNG,
1497 balances: vec![u("3323859056394"), u("4735137544"), u("1544027711277257449902")],
1498 token_decimals: vec![6, 8, 18],
1499 amp: U256::from(1_707_629u64),
1500 d: Some(u("10010654847128420517547506")),
1501 gamma: Some(u("11809167828997")),
1502 price_scale: Some(vec![u("70750968814053384159761"), u("2161000205852311064272")]),
1503 mid_fee: Some(U256::from(3_000_000u64)),
1504 out_fee: Some(U256::from(30_000_000u64)),
1505 fee_gamma: Some(u("500000000000000")),
1506 ..Default::default()
1507 };
1508 let pool = build_pool(&state).unwrap();
1509 let dy = pool
1510 .get_amount_out(0, 1, u("33238590563"))
1511 .unwrap();
1512 assert_eq!(dy, U256::from(46_932_317u64));
1513 }
1514}