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 pub balances: Vec<U256>,
112
113 pub token_decimals: Vec<u8>,
116
117 pub amp: U256,
162
163 pub fee: Option<U256>,
166
167 pub mid_fee: Option<U256>,
169
170 pub out_fee: Option<U256>,
172
173 pub fee_gamma: Option<U256>,
175
176 pub offpeg_fee_multiplier: Option<U256>,
178
179 pub price_scale: Option<Vec<U256>>,
183
184 pub d: Option<U256>,
186
187 pub gamma: Option<U256>,
191
192 pub dynamic_rates: Option<Vec<Option<U256>>>,
223
224 pub precisions: Option<Vec<U256>>,
235
236 pub eth_variant: Option<bool>,
248}
249
250impl Default for RawPoolState {
251 fn default() -> Self {
252 Self {
253 variant: CurveVariant::StableSwapV2,
254 balances: Vec::new(),
255 token_decimals: Vec::new(),
256 amp: U256::ZERO,
257 fee: None,
258 mid_fee: None,
259 out_fee: None,
260 fee_gamma: None,
261 offpeg_fee_multiplier: None,
262 price_scale: None,
263 d: None,
264 gamma: None,
265 dynamic_rates: None,
266 precisions: None,
267 eth_variant: None,
268 }
269 }
270}
271
272pub fn interpolate_a(
298 initial_a: U256,
299 future_a: U256,
300 initial_a_time: u64,
301 future_a_time: u64,
302 block_timestamp: u64,
303) -> U256 {
304 if block_timestamp >= future_a_time {
305 return future_a;
306 }
307
308 let elapsed = U256::from(block_timestamp - initial_a_time);
311 let duration = U256::from(future_a_time - initial_a_time);
312
313 if future_a > initial_a {
314 initial_a + (future_a - initial_a) * elapsed / duration
315 } else {
316 initial_a - (initial_a - future_a) * elapsed / duration
317 }
318}
319
320fn compute_stableswap_rates(
329 token_decimals: &[u8],
330 dynamic_rates: &Option<Vec<Option<U256>>>,
331) -> Vec<U256> {
332 token_decimals
333 .iter()
334 .enumerate()
335 .map(|(i, &decimals)| {
336 if let Some(ref rates) = dynamic_rates {
338 if let Some(Some(rate)) = rates.get(i) {
339 return *rate;
340 }
341 }
342 U256::from(10u64).pow(U256::from(36 - decimals as u32))
344 })
345 .collect()
346}
347
348fn compute_crypto_precisions(token_decimals: &[u8]) -> Vec<U256> {
355 token_decimals
356 .iter()
357 .map(|&decimals| U256::from(10u64).pow(U256::from(18 - decimals as u32)))
358 .collect()
359}
360
361macro_rules! require {
363 ($state:expr, $field:ident) => {
364 $state
365 .$field
366 .ok_or(BuildError::MissingField {
367 variant: $state.variant,
368 field: stringify!($field),
369 })?
370 };
371}
372
373pub fn build_pool(state: &RawPoolState) -> Result<Pool, BuildError> {
382 if state.balances.len() != state.token_decimals.len() {
384 return Err(BuildError::DecimalsMismatch {
385 balances_len: state.balances.len(),
386 decimals_len: state.token_decimals.len(),
387 });
388 }
389
390 if let Some(ref dr) = state.dynamic_rates {
392 if dr.len() != state.balances.len() {
393 return Err(BuildError::DynamicRatesMismatch {
394 balances_len: state.balances.len(),
395 rates_len: dr.len(),
396 });
397 }
398 }
399
400 match state.variant {
401 CurveVariant::StableSwapV0 |
402 CurveVariant::StableSwapV1 |
403 CurveVariant::StableSwapV2 |
404 CurveVariant::StableSwapSTETH |
405 CurveVariant::StableSwapMeta => build_stableswap_plain(state),
406 CurveVariant::StableSwapNG => build_stableswap_ng(state),
407 CurveVariant::StableSwapALend => build_stableswap_alend(state),
408 CurveVariant::TwoCryptoV1 | CurveVariant::TwoCryptoNG | CurveVariant::TwoCryptoStable => {
409 build_twocrypto(state)
410 }
411 CurveVariant::TriCryptoV1 | CurveVariant::TriCryptoNG => build_tricrypto(state),
412 }
413}
414
415fn build_stableswap_plain(state: &RawPoolState) -> Result<Pool, BuildError> {
418 let fee = require!(state, fee);
419
420 for (i, &d) in state.token_decimals.iter().enumerate() {
421 if d > 36 {
422 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 36 });
423 }
424 }
425
426 if state.variant == CurveVariant::StableSwapMeta {
429 let n = state.balances.len();
430 let has_vp = state
431 .dynamic_rates
432 .as_ref()
433 .and_then(|dr| dr.get(n - 1))
434 .map(|r| r.is_some())
435 .unwrap_or(false);
436 if !has_vp {
437 return Err(BuildError::MetaMissingVirtualPrice);
438 }
439 }
440
441 let rates = compute_stableswap_rates(&state.token_decimals, &state.dynamic_rates);
442 let balances = state.balances.clone();
443 let amp = state.amp;
444
445 Ok(match state.variant {
446 CurveVariant::StableSwapV0 => Pool::StableSwapV0 { balances, rates, amp, fee },
447 CurveVariant::StableSwapV1 => Pool::StableSwapV1 { balances, rates, amp, fee },
448 CurveVariant::StableSwapV2 => Pool::StableSwapV2 { balances, rates, amp, fee },
449 CurveVariant::StableSwapSTETH => Pool::StableSwapSTETH { balances, rates, amp, fee },
450 CurveVariant::StableSwapMeta => Pool::StableSwapMeta { balances, rates, amp, fee },
451 _ => unreachable!(),
452 })
453}
454
455fn build_stableswap_ng(state: &RawPoolState) -> Result<Pool, BuildError> {
456 let fee = require!(state, fee);
457 let offpeg = state
462 .offpeg_fee_multiplier
463 .unwrap_or(U256::from(10_000_000_000u64));
464
465 for (i, &d) in state.token_decimals.iter().enumerate() {
466 if d > 36 {
467 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 36 });
468 }
469 }
470
471 let rates = compute_stableswap_rates(&state.token_decimals, &state.dynamic_rates);
472
473 Ok(Pool::StableSwapNG {
474 balances: state.balances.clone(),
475 rates,
476 amp: state.amp,
477 fee,
478 offpeg_fee_multiplier: offpeg,
479 })
480}
481
482fn build_stableswap_alend(state: &RawPoolState) -> Result<Pool, BuildError> {
483 let fee = require!(state, fee);
484 let offpeg = require!(state, offpeg_fee_multiplier);
485
486 for (i, &d) in state.token_decimals.iter().enumerate() {
487 if d > 18 {
488 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
489 }
490 }
491
492 let precision_mul = compute_crypto_precisions(&state.token_decimals);
493
494 Ok(Pool::StableSwapALend {
495 balances: state.balances.clone(),
496 precision_mul,
497 amp: state.amp,
498 fee,
499 offpeg_fee_multiplier: offpeg,
500 })
501}
502
503fn build_twocrypto(state: &RawPoolState) -> Result<Pool, BuildError> {
504 if state.balances.len() != 2 {
505 return Err(BuildError::WrongCoinCount {
506 variant: state.variant,
507 expected: 2,
508 actual: state.balances.len(),
509 });
510 }
511
512 for (i, &d) in state.token_decimals.iter().enumerate() {
513 if d > 18 {
514 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
515 }
516 }
517
518 let mid_fee = require!(state, mid_fee);
519 let out_fee = require!(state, out_fee);
520 let fee_gamma = require!(state, fee_gamma);
521 let d = require!(state, d);
522 let ann = state.amp;
523
524 let price_scale_vec = state
525 .price_scale
526 .as_ref()
527 .ok_or(BuildError::MissingField { variant: state.variant, field: "price_scale" })?;
528 if price_scale_vec.len() != 1 {
529 return Err(BuildError::PriceScaleWrongLen { expected: 1, actual: price_scale_vec.len() });
530 }
531 let price_scale = price_scale_vec[0];
532
533 let default_precs = compute_crypto_precisions(&state.token_decimals);
534 let precisions = state
535 .precisions
536 .as_deref()
537 .unwrap_or(&default_precs);
538 let balances: [U256; 2] = [state.balances[0], state.balances[1]];
539 let prec_arr: [U256; 2] = [precisions[0], precisions[1]];
540
541 match state.variant {
542 CurveVariant::TwoCryptoV1 => {
543 let gamma = require!(state, gamma);
544 let eth_variant = require!(state, eth_variant);
547 Ok(Pool::TwoCryptoV1 {
548 balances,
549 precisions: prec_arr,
550 price_scale,
551 d,
552 ann,
553 gamma,
554 mid_fee,
555 out_fee,
556 fee_gamma,
557 eth_variant,
558 })
559 }
560 CurveVariant::TwoCryptoNG => {
561 let gamma = require!(state, gamma);
562 Ok(Pool::TwoCryptoNG {
563 balances,
564 precisions: prec_arr,
565 price_scale,
566 d,
567 ann,
568 gamma,
569 mid_fee,
570 out_fee,
571 fee_gamma,
572 })
573 }
574 CurveVariant::TwoCryptoStable => Ok(Pool::TwoCryptoStable {
575 balances,
576 precisions: prec_arr,
577 price_scale,
578 d,
579 ann,
580 mid_fee,
581 out_fee,
582 fee_gamma,
583 }),
584 _ => unreachable!("build_twocrypto called for non-twocrypto variant"),
585 }
586}
587
588fn build_tricrypto(state: &RawPoolState) -> Result<Pool, BuildError> {
589 if state.balances.len() != 3 {
590 return Err(BuildError::WrongCoinCount {
591 variant: state.variant,
592 expected: 3,
593 actual: state.balances.len(),
594 });
595 }
596
597 for (i, &d) in state.token_decimals.iter().enumerate() {
598 if d > 18 {
599 return Err(BuildError::DecimalsTooLarge { index: i, decimals: d, max: 18 });
600 }
601 }
602
603 let mid_fee = require!(state, mid_fee);
604 let out_fee = require!(state, out_fee);
605 let fee_gamma = require!(state, fee_gamma);
606 let d = require!(state, d);
607 let gamma = require!(state, gamma);
608 let ann = state.amp;
609
610 let price_scale_vec = state
611 .price_scale
612 .as_ref()
613 .ok_or(BuildError::MissingField { variant: state.variant, field: "price_scale" })?;
614 if price_scale_vec.len() != 2 {
615 return Err(BuildError::PriceScaleWrongLen { expected: 2, actual: price_scale_vec.len() });
616 }
617 let price_scale: [U256; 2] = [price_scale_vec[0], price_scale_vec[1]];
618
619 let default_precs = compute_crypto_precisions(&state.token_decimals);
620 let precisions = state
621 .precisions
622 .as_deref()
623 .unwrap_or(&default_precs);
624 let balances: [U256; 3] = [state.balances[0], state.balances[1], state.balances[2]];
625 let prec_arr: [U256; 3] = [precisions[0], precisions[1], precisions[2]];
626
627 match state.variant {
628 CurveVariant::TriCryptoV1 => Ok(Pool::TriCryptoV1 {
629 balances,
630 precisions: prec_arr,
631 price_scale,
632 d,
633 ann,
634 gamma,
635 mid_fee,
636 out_fee,
637 fee_gamma,
638 }),
639 CurveVariant::TriCryptoNG => Ok(Pool::TriCryptoNG {
640 balances,
641 precisions: prec_arr,
642 price_scale,
643 d,
644 ann,
645 gamma,
646 mid_fee,
647 out_fee,
648 fee_gamma,
649 }),
650 _ => unreachable!("build_tricrypto called for non-tricrypto variant"),
651 }
652}
653
654#[cfg(test)]
655mod tests {
656 use super::*;
657
658 #[test]
659 fn interpolate_a_no_ramp() {
660 let a = U256::from(40_000u64);
662 let result = interpolate_a(a, a, 1000, 2000, 1500);
663 assert_eq!(result, a);
664 }
665
666 #[test]
667 fn interpolate_a_ramp_complete() {
668 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 3000);
670 assert_eq!(result, U256::from(40_000u64));
671 }
672
673 #[test]
674 fn interpolate_a_ramp_exactly_at_end() {
675 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 2000);
676 assert_eq!(result, U256::from(40_000u64));
677 }
678
679 #[test]
680 fn interpolate_a_ramp_up_midpoint() {
681 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 1500);
684 assert_eq!(result, U256::from(30_000u64));
685 }
686
687 #[test]
688 fn interpolate_a_ramp_down_midpoint() {
689 let result = interpolate_a(U256::from(40_000u64), U256::from(20_000u64), 1000, 2000, 1500);
692 assert_eq!(result, U256::from(30_000u64));
693 }
694
695 #[test]
696 fn interpolate_a_ramp_up_quarter() {
697 let result = interpolate_a(U256::from(10_000u64), U256::from(50_000u64), 0, 1000, 250);
700 assert_eq!(result, U256::from(20_000u64));
701 }
702
703 #[test]
704 fn interpolate_a_ramp_at_start() {
705 let result = interpolate_a(U256::from(20_000u64), U256::from(40_000u64), 1000, 2000, 1000);
707 assert_eq!(result, U256::from(20_000u64));
708 }
709
710 #[test]
711 fn interpolate_a_integer_division_truncation() {
712 let result = interpolate_a(U256::from(10_000u64), U256::from(10_003u64), 0, 1000, 1);
716 assert_eq!(result, U256::from(10_000u64));
717 }
718
719 #[test]
720 fn build_stableswap_v0_basic() {
721 let state = RawPoolState {
722 variant: CurveVariant::StableSwapV0,
723 balances: vec![
724 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), ],
729 token_decimals: vec![18, 6, 6, 18],
730 amp: U256::from(200u64), fee: Some(U256::from(4_000_000u64)),
732 ..Default::default()
733 };
734
735 let pool = build_pool(&state).unwrap();
736 let dy = pool.get_amount_out(0, 1, U256::from(1_000_000_000_000_000_000u128));
738 assert!(dy.is_some());
739 }
740
741 #[test]
742 fn build_stableswap_v2_rates_18_6() {
743 let state = RawPoolState {
744 variant: CurveVariant::StableSwapV2,
745 balances: vec![
746 U256::from(1_000_000_000_000_000_000_000u128),
747 U256::from(1_000_000_000u128),
748 ],
749 token_decimals: vec![18, 6],
750 amp: U256::from(40_000u64), fee: Some(U256::from(4_000_000u64)),
752 ..Default::default()
753 };
754
755 let pool = build_pool(&state).unwrap();
756
757 match &pool {
759 Pool::StableSwapV2 { rates, .. } => {
760 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
761 assert_eq!(rates[1], U256::from(10u64).pow(U256::from(30u64)));
762 }
763 _ => panic!("wrong variant"),
764 }
765 }
766
767 #[test]
768 fn build_stableswap_ng_with_dynamic_rates() {
769 let oracle_rate = U256::from(1_050_000_000_000_000_000u128); let state = RawPoolState {
771 variant: CurveVariant::StableSwapNG,
772 balances: vec![
773 U256::from(1_000_000_000_000_000_000_000u128),
774 U256::from(1_000_000_000_000_000_000_000u128),
775 ],
776 token_decimals: vec![18, 18],
777 amp: U256::from(150_000u64),
778 fee: Some(U256::from(4_000_000u64)),
779 offpeg_fee_multiplier: Some(U256::from(20_000_000_000u128)),
780 dynamic_rates: Some(vec![
781 None, Some(oracle_rate), ]),
784 ..Default::default()
785 };
786
787 let pool = build_pool(&state).unwrap();
788 match &pool {
789 Pool::StableSwapNG { rates, .. } => {
790 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
791 assert_eq!(rates[1], oracle_rate);
792 }
793 _ => panic!("wrong variant"),
794 }
795 }
796
797 #[test]
798 fn build_stableswap_alend_precision_mul() {
799 let state = RawPoolState {
800 variant: CurveVariant::StableSwapALend,
801 balances: vec![
802 U256::from(1_000_000_000_000_000_000_000u128),
803 U256::from(1_000_000_000_000_000_000_000u128),
804 ],
805 token_decimals: vec![18, 18],
806 amp: U256::from(10_000u64), fee: Some(U256::from(4_000_000u64)),
808 offpeg_fee_multiplier: Some(U256::from(20_000_000_000u128)),
809 ..Default::default()
810 };
811
812 let pool = build_pool(&state).unwrap();
813 match &pool {
814 Pool::StableSwapALend { precision_mul, .. } => {
815 assert_eq!(precision_mul[0], U256::from(1u64));
817 assert_eq!(precision_mul[1], U256::from(1u64));
818 }
819 _ => panic!("wrong variant"),
820 }
821 }
822
823 #[test]
824 fn build_stableswap_meta_virtual_price_rate() {
825 let virtual_price = U256::from(1_020_000_000_000_000_000u128); let state = RawPoolState {
827 variant: CurveVariant::StableSwapMeta,
828 balances: vec![
829 U256::from(1_000_000_000u128), U256::from(1_000_000_000_000_000_000_000u128), ],
832 token_decimals: vec![2, 18],
833 amp: U256::from(150_000u64),
834 fee: Some(U256::from(4_000_000u64)),
835 dynamic_rates: Some(vec![
836 None, Some(virtual_price), ]),
839 ..Default::default()
840 };
841
842 let pool = build_pool(&state).unwrap();
843 match &pool {
844 Pool::StableSwapMeta { rates, .. } => {
845 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(34u64)));
846 assert_eq!(rates[1], virtual_price);
847 }
848 _ => panic!("wrong variant"),
849 }
850 }
851
852 #[test]
853 fn build_twocrypto_ng_basic() {
854 let state = RawPoolState {
855 variant: CurveVariant::TwoCryptoNG,
856 balances: vec![
857 U256::from(1_000_000_000_000_000_000_000u128),
858 U256::from(1_000_000_000_000_000_000_000u128),
859 ],
860 token_decimals: vec![18, 18],
861 amp: U256::from(540_000u64 * 10_000u64), mid_fee: Some(U256::from(3_000_000u64)),
863 out_fee: Some(U256::from(30_000_000u64)),
864 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
865 d: Some(U256::from(2_000_000_000_000_000_000_000u128)),
866 gamma: Some(U256::from(10_000_000_000_000u128)),
867 price_scale: Some(vec![U256::from(1_000_000_000_000_000_000u128)]),
868 ..Default::default()
869 };
870
871 let pool = build_pool(&state).unwrap();
872 match &pool {
873 Pool::TwoCryptoNG { precisions, ann, .. } => {
874 assert_eq!(precisions[0], U256::from(1u64)); assert_eq!(precisions[1], U256::from(1u64));
876 assert_eq!(*ann, state.amp);
877 }
878 _ => panic!("wrong variant"),
879 }
880 }
881
882 #[test]
883 fn build_twocrypto_stable_no_gamma() {
884 let state = RawPoolState {
885 variant: CurveVariant::TwoCryptoStable,
886 balances: vec![U256::from(1_000_000_000u128), U256::from(1_000_000_000u128)],
887 token_decimals: vec![6, 6],
888 amp: U256::from(540_000u64 * 10_000u64),
889 mid_fee: Some(U256::from(3_000_000u64)),
890 out_fee: Some(U256::from(30_000_000u64)),
891 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
892 d: Some(U256::from(2_000_000_000u128)),
893 price_scale: Some(vec![U256::from(1_000_000_000_000_000_000u128)]),
895 ..Default::default()
896 };
897
898 let pool = build_pool(&state).unwrap();
899 match &pool {
900 Pool::TwoCryptoStable { precisions, .. } => {
901 assert_eq!(precisions[0], U256::from(10u64).pow(U256::from(12u64)));
903 }
904 _ => panic!("wrong variant"),
905 }
906 }
907
908 #[test]
909 fn build_tricrypto_ng_basic() {
910 let state = RawPoolState {
911 variant: CurveVariant::TriCryptoNG,
912 balances: vec![
913 U256::from(1_000_000_000u128), U256::from(50_000_000u128), U256::from(500_000_000_000_000_000_000u128), ],
917 token_decimals: vec![6, 8, 18],
918 amp: U256::from(1_707_629u64 * 10_000u64),
919 mid_fee: Some(U256::from(3_000_000u64)),
920 out_fee: Some(U256::from(30_000_000u64)),
921 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
922 d: Some(U256::from(3_000_000_000_000_000_000_000u128)),
923 gamma: Some(U256::from(11_809_167_828_997u128)),
924 price_scale: Some(vec![
925 U256::from(60_000_000_000_000_000_000_000u128), U256::from(3_000_000_000_000_000_000_000u128), ]),
928 ..Default::default()
929 };
930
931 let pool = build_pool(&state).unwrap();
932 match &pool {
933 Pool::TriCryptoNG { precisions, price_scale, .. } => {
934 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);
938 }
939 _ => panic!("wrong variant"),
940 }
941 }
942
943 #[test]
944 fn build_missing_fee_returns_error() {
945 let state = RawPoolState {
946 variant: CurveVariant::StableSwapV2,
947 balances: vec![U256::from(1u64), U256::from(1u64)],
948 token_decimals: vec![18, 18],
949 amp: U256::from(40_000u64),
950 ..Default::default()
952 };
953
954 let err = match build_pool(&state) {
955 Err(e) => e,
956 Ok(_) => panic!("expected error"),
957 };
958 assert!(matches!(err, BuildError::MissingField { field: "fee", .. }));
959 }
960
961 #[test]
962 fn build_decimals_mismatch_returns_error() {
963 let state = RawPoolState {
964 variant: CurveVariant::StableSwapV2,
965 balances: vec![U256::from(1u64), U256::from(1u64)],
966 token_decimals: vec![18], amp: U256::from(40_000u64),
968 fee: Some(U256::from(4_000_000u64)),
969 ..Default::default()
970 };
971
972 let err = match build_pool(&state) {
973 Err(e) => e,
974 Ok(_) => panic!("expected error"),
975 };
976 assert!(matches!(err, BuildError::DecimalsMismatch { .. }));
977 }
978
979 #[test]
980 fn build_twocrypto_wrong_coin_count() {
981 let state = RawPoolState {
982 variant: CurveVariant::TwoCryptoNG,
983 balances: vec![U256::from(1u64), U256::from(1u64), U256::from(1u64)],
984 token_decimals: vec![18, 18, 18],
985 amp: U256::from(1u64),
986 mid_fee: Some(U256::from(1u64)),
987 out_fee: Some(U256::from(1u64)),
988 fee_gamma: Some(U256::from(1u64)),
989 d: Some(U256::from(1u64)),
990 gamma: Some(U256::from(1u64)),
991 price_scale: Some(vec![U256::from(1u64)]),
992 ..Default::default()
993 };
994
995 let err = match build_pool(&state) {
996 Err(e) => e,
997 Ok(_) => panic!("expected error"),
998 };
999 assert!(matches!(err, BuildError::WrongCoinCount { expected: 2, actual: 3, .. }));
1000 }
1001
1002 #[test]
1003 fn build_tricrypto_wrong_price_scale_len() {
1004 let state = RawPoolState {
1005 variant: CurveVariant::TriCryptoNG,
1006 balances: vec![U256::from(1u64), U256::from(1u64), U256::from(1u64)],
1007 token_decimals: vec![6, 8, 18],
1008 amp: U256::from(1u64),
1009 mid_fee: Some(U256::from(1u64)),
1010 out_fee: Some(U256::from(1u64)),
1011 fee_gamma: Some(U256::from(1u64)),
1012 d: Some(U256::from(1u64)),
1013 gamma: Some(U256::from(1u64)),
1014 price_scale: Some(vec![U256::from(1u64)]), ..Default::default()
1016 };
1017
1018 let err = match build_pool(&state) {
1019 Err(e) => e,
1020 Ok(_) => panic!("expected error"),
1021 };
1022 assert!(matches!(err, BuildError::PriceScaleWrongLen { expected: 2, actual: 1 }));
1023 }
1024
1025 #[test]
1026 fn build_ng_without_offpeg_defaults_to_fee_denominator() {
1027 let state = RawPoolState {
1029 variant: CurveVariant::StableSwapNG,
1030 balances: vec![U256::from(1u64), U256::from(1u64)],
1031 token_decimals: vec![18, 18],
1032 amp: U256::from(40_000u64),
1033 fee: Some(U256::from(4_000_000u64)),
1034 ..Default::default()
1036 };
1037
1038 let pool = build_pool(&state).expect("should succeed with defaulted offpeg");
1039 assert_eq!(pool.offpeg_fee_multiplier(), Some(U256::from(10_000_000_000u64)));
1041 }
1042
1043 #[test]
1044 fn build_ng_crvusd_sdai_matches_on_chain() {
1045 let state = RawPoolState {
1048 variant: CurveVariant::StableSwapNG,
1049 balances: vec![
1050 "3219009600398261994"
1051 .parse::<U256>()
1052 .expect("balance 0"),
1053 "311156701443769568"
1054 .parse::<U256>()
1055 .expect("balance 1"),
1056 ],
1057 token_decimals: vec![18, 18],
1058 amp: U256::from(150_000u64),
1059 fee: Some(U256::from(1_000_000u64)),
1060 dynamic_rates: Some(vec![
1062 Some(
1063 "1000000000000000000"
1064 .parse::<U256>()
1065 .expect("rate 0"),
1066 ),
1067 Some(
1068 "1173627645818786870"
1069 .parse::<U256>()
1070 .expect("rate 1"),
1071 ),
1072 ]),
1073 ..Default::default()
1074 };
1075
1076 let pool = build_pool(&state).expect("should build with defaulted offpeg");
1077 let dy = pool
1078 .get_amount_out(0, 1, U256::from(3_219_009_600_398_261u64))
1079 .expect("swap should succeed");
1080
1081 let expected = U256::from(2_720_818_166_217_034u64);
1083 let diff = if dy > expected { dy - expected } else { expected - dy };
1084 assert!(diff <= U256::from(1u64), "mismatch: got {dy}, expected {expected}, diff {diff}");
1085 }
1086
1087 #[test]
1088 fn build_meta_without_virtual_price_fails() {
1089 let state = RawPoolState {
1092 variant: CurveVariant::StableSwapMeta,
1093 balances: vec![
1094 U256::from(1_000_000_000u128),
1095 U256::from(1_000_000_000_000_000_000_000u128),
1096 ],
1097 token_decimals: vec![2, 18],
1098 amp: U256::from(150_000u64),
1099 fee: Some(U256::from(4_000_000u64)),
1100 ..Default::default()
1102 };
1103
1104 let err = match build_pool(&state) {
1105 Err(e) => e,
1106 Ok(_) => panic!("expected MetaMissingVirtualPrice error"),
1107 };
1108 assert!(matches!(err, BuildError::MetaMissingVirtualPrice));
1109 }
1110
1111 #[test]
1112 fn build_meta_with_partial_dynamic_rates_missing_vp_fails() {
1113 let state = RawPoolState {
1115 variant: CurveVariant::StableSwapMeta,
1116 balances: vec![
1117 U256::from(1_000_000_000u128),
1118 U256::from(1_000_000_000_000_000_000_000u128),
1119 ],
1120 token_decimals: vec![2, 18],
1121 amp: U256::from(150_000u64),
1122 fee: Some(U256::from(4_000_000u64)),
1123 dynamic_rates: Some(vec![None, None]), ..Default::default()
1125 };
1126
1127 let err = match build_pool(&state) {
1128 Err(e) => e,
1129 Ok(_) => panic!("expected MetaMissingVirtualPrice error"),
1130 };
1131 assert!(matches!(err, BuildError::MetaMissingVirtualPrice));
1132 }
1133
1134 #[test]
1135 fn rates_match_fuzz_registry_18_dec() {
1136 let rates = super::compute_stableswap_rates(&[18], &None);
1138 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(18u64)));
1139 }
1140
1141 #[test]
1142 fn rates_match_fuzz_registry_6_dec() {
1143 let rates = super::compute_stableswap_rates(&[6], &None);
1145 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(30u64)));
1146 }
1147
1148 #[test]
1149 fn rates_match_fuzz_registry_8_dec() {
1150 let rates = super::compute_stableswap_rates(&[8], &None);
1152 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(28u64)));
1153 }
1154
1155 #[test]
1156 fn rates_match_fuzz_registry_2_dec() {
1157 let rates = super::compute_stableswap_rates(&[2], &None);
1159 assert_eq!(rates[0], U256::from(10u64).pow(U256::from(34u64)));
1160 }
1161
1162 #[test]
1163 fn precisions_match_fuzz_registry() {
1164 let precs = super::compute_crypto_precisions(&[6, 8, 18]);
1166 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)); }
1170
1171 #[test]
1172 fn precision_mul_matches_fuzz_registry() {
1173 let pm = super::compute_crypto_precisions(&[18, 6]);
1175 assert_eq!(pm[0], U256::from(1u64));
1176 assert_eq!(pm[1], U256::from(10u64).pow(U256::from(12u64)));
1177 }
1178
1179 #[test]
1180 fn build_all_11_variants_succeed() {
1181 let stableswap_base = |variant: CurveVariant| -> RawPoolState {
1183 RawPoolState {
1184 variant,
1185 balances: vec![U256::from(1_000_000_000_000_000_000u128); 2],
1186 token_decimals: vec![18, 18],
1187 amp: U256::from(40_000u64),
1188 fee: Some(U256::from(4_000_000u64)),
1189 ..Default::default()
1190 }
1191 };
1192
1193 for v in
1195 [CurveVariant::StableSwapV0, CurveVariant::StableSwapV1, CurveVariant::StableSwapV2]
1196 {
1197 assert!(build_pool(&stableswap_base(v)).is_ok(), "failed for {v}");
1198 }
1199
1200 let mut meta = stableswap_base(CurveVariant::StableSwapMeta);
1202 meta.dynamic_rates = Some(vec![None, Some(U256::from(10u64).pow(U256::from(18u64)))]);
1203 assert!(build_pool(&meta).is_ok(), "failed for StableSwapMeta");
1204
1205 let mut ng = stableswap_base(CurveVariant::StableSwapNG);
1207 ng.offpeg_fee_multiplier = Some(U256::from(20_000_000_000u128));
1208 assert!(build_pool(&ng).is_ok(), "failed for StableSwapNG");
1209
1210 let mut alend = stableswap_base(CurveVariant::StableSwapALend);
1212 alend.offpeg_fee_multiplier = Some(U256::from(20_000_000_000u128));
1213 assert!(build_pool(&alend).is_ok(), "failed for StableSwapALend");
1214
1215 let crypto_base = |variant: CurveVariant, n: usize| -> RawPoolState {
1217 RawPoolState {
1218 variant,
1219 balances: vec![U256::from(1_000_000_000_000_000_000u128); n],
1220 token_decimals: vec![18; n],
1221 amp: U256::from(540_000u64 * 10_000u64),
1222 mid_fee: Some(U256::from(3_000_000u64)),
1223 out_fee: Some(U256::from(30_000_000u64)),
1224 fee_gamma: Some(U256::from(500_000_000_000_000u128)),
1225 d: Some(U256::from(2_000_000_000_000_000_000_000u128)),
1226 gamma: Some(U256::from(10_000_000_000_000u128)),
1227 price_scale: Some(if n == 2 {
1228 vec![U256::from(10u64).pow(U256::from(18u64))]
1229 } else {
1230 vec![U256::from(10u64).pow(U256::from(18u64)); n - 1]
1231 }),
1232 eth_variant: Some(true),
1234 ..Default::default()
1235 }
1236 };
1237
1238 for v in [CurveVariant::TwoCryptoV1, CurveVariant::TwoCryptoNG] {
1240 assert!(build_pool(&crypto_base(v, 2)).is_ok(), "failed for {v}");
1241 }
1242
1243 let mut tcs = crypto_base(CurveVariant::TwoCryptoStable, 2);
1245 tcs.gamma = None;
1246 assert!(build_pool(&tcs).is_ok(), "failed for TwoCryptoStable");
1247
1248 for v in [CurveVariant::TriCryptoV1, CurveVariant::TriCryptoNG] {
1250 assert!(build_pool(&crypto_base(v, 3)).is_ok(), "failed for {v}");
1251 }
1252 }
1253
1254 fn u(s: &str) -> U256 {
1265 U256::from_str_radix(s, 10).unwrap()
1266 }
1267
1268 #[test]
1269 fn integration_stableswap_v0_susd() {
1270 let state = RawPoolState {
1272 variant: CurveVariant::StableSwapV0,
1273 balances: vec![
1274 u("1919848022082255699479"),
1275 u("1920322445"),
1276 u("1920171938"),
1277 u("21038816168255729764832232005"),
1278 ],
1279 token_decimals: vec![18, 6, 6, 18],
1280 amp: U256::from(256u64),
1281 fee: Some(U256::from(2_000_000u64)),
1282 ..Default::default()
1283 };
1284 let pool = build_pool(&state).unwrap();
1285 let dy = pool
1286 .get_amount_out(0, 1, u("19198480220822556994"))
1287 .unwrap();
1288 assert_eq!(dy, U256::from(19_009_291u64));
1289 }
1290
1291 #[test]
1292 fn integration_stableswap_v1_3pool() {
1293 let state = RawPoolState {
1295 variant: CurveVariant::StableSwapV1,
1296 balances: vec![
1297 u("45102835177280382580138407"),
1298 u("45853975278310"),
1299 u("72989152672276"),
1300 ],
1301 token_decimals: vec![18, 6, 6],
1302 amp: U256::from(4000u64),
1303 fee: Some(U256::from(1_500_000u64)),
1304 ..Default::default()
1305 };
1306 let pool = build_pool(&state).unwrap();
1307 let dy = pool
1308 .get_amount_out(0, 1, u("451028351772803825801384"))
1309 .unwrap();
1310 assert_eq!(dy, u("450961663745"));
1311 }
1312
1313 #[test]
1314 fn integration_stableswap_v2_frax_usdc() {
1315 let state = RawPoolState {
1317 variant: CurveVariant::StableSwapV2,
1318 balances: vec![u("6722234569994793202271485"), u("714493991383")],
1319 token_decimals: vec![18, 6],
1320 amp: U256::from(150_000u64),
1321 fee: Some(U256::from(1_000_000u64)),
1322 ..Default::default()
1323 };
1324 let pool = build_pool(&state).unwrap();
1325 let dy = pool
1326 .get_amount_out(0, 1, u("67222345699947932022714"))
1327 .unwrap();
1328 assert_eq!(dy, u("66561674655"));
1329 }
1330
1331 #[test]
1332 fn integration_stableswap_alend_aave() {
1333 let state = RawPoolState {
1335 variant: CurveVariant::StableSwapALend,
1336 balances: vec![u("968991099162993551077367"), u("1012448901351"), u("414282246850")],
1337 token_decimals: vec![18, 6, 6],
1338 amp: U256::from(200_000u64),
1339 fee: Some(U256::from(4_000_000u64)),
1340 offpeg_fee_multiplier: Some(u("20000000000")),
1341 ..Default::default()
1342 };
1343 let pool = build_pool(&state).unwrap();
1344 let dy = pool
1345 .get_amount_out(0, 1, u("9689910991629935510773"))
1346 .unwrap();
1347 assert_eq!(dy, u("9686201099"));
1348 }
1349
1350 #[test]
1351 fn integration_stableswap_ng_usde_dai() {
1352 let state = RawPoolState {
1354 variant: CurveVariant::StableSwapNG,
1355 balances: vec![u("124403796536542495997070"), u("95031311223261676260348")],
1356 token_decimals: vec![18, 18],
1357 amp: U256::from(40_000u64),
1358 fee: Some(U256::from(4_000_000u64)),
1359 offpeg_fee_multiplier: Some(u("20000000000")),
1360 dynamic_rates: Some(vec![
1361 Some(u("1000000000000000000")),
1362 Some(u("1000000000000000000")),
1363 ]),
1364 ..Default::default()
1365 };
1366 let pool = build_pool(&state).unwrap();
1367 let dy = pool
1368 .get_amount_out(0, 1, u("1244037965365424959970"))
1369 .unwrap();
1370 assert_eq!(dy, u("1242635841481792448583"));
1371 }
1372
1373 #[test]
1374 fn integration_stableswap_meta_gusd_3crv() {
1375 let state = RawPoolState {
1377 variant: CurveVariant::StableSwapMeta,
1378 balances: vec![u("59814423"), u("1210422553896217308280639")],
1379 token_decimals: vec![2, 18],
1380 amp: U256::from(100_000u64),
1381 fee: Some(U256::from(4_000_000u64)),
1382 dynamic_rates: Some(vec![
1383 None, Some(u("1039823717145796146")), ]),
1386 ..Default::default()
1387 };
1388 let pool = build_pool(&state).unwrap();
1389 let dy = pool
1390 .get_amount_out(0, 1, u("598144"))
1391 .unwrap();
1392 assert_eq!(dy, u("5755338887370979902172"));
1393 }
1394
1395 #[test]
1396 fn integration_twocrypto_v1_crv_eth() {
1397 let state = RawPoolState {
1399 variant: CurveVariant::TwoCryptoV1,
1400 balances: vec![u("33389428640766852909"), u("1538654846121127403001612563")],
1401 token_decimals: vec![18, 18],
1402 amp: U256::from(400_000u64),
1403 d: Some(u("3338917956478824050009")),
1404 gamma: Some(u("145000000000000")),
1405 price_scale: Some(vec![u("52805053500476")]),
1406 mid_fee: Some(U256::from(26_000_000u64)),
1407 out_fee: Some(U256::from(45_000_000u64)),
1408 fee_gamma: Some(u("230000000000000")),
1409 eth_variant: Some(true), ..Default::default()
1411 };
1412 let pool = build_pool(&state).unwrap();
1413 let dy = pool
1414 .get_amount_out(0, 1, u("333894286407668529"))
1415 .unwrap();
1416 assert_eq!(dy, u("15024547954512515366680912"));
1417 }
1418
1419 #[test]
1420 fn integration_twocrypto_ng_crvusd_fxn() {
1421 let state = RawPoolState {
1423 variant: CurveVariant::TwoCryptoNG,
1424 balances: vec![u("575304877931995002539"), u("1286854862507061937737")],
1425 token_decimals: vec![18, 18],
1426 amp: U256::from(400_000u64),
1427 d: Some(u("1309807915207365083258")),
1428 gamma: Some(u("145000000000000")),
1429 price_scale: Some(vec![u("578321621819309618")]),
1430 mid_fee: Some(U256::from(26_000_000u64)),
1431 out_fee: Some(U256::from(45_000_000u64)),
1432 fee_gamma: Some(u("230000000000000")),
1433 ..Default::default()
1434 };
1435 let pool = build_pool(&state).unwrap();
1436 let dy = pool
1437 .get_amount_out(0, 1, u("5753048779319950025"))
1438 .unwrap();
1439 assert_eq!(dy, u("12553693226638615366"));
1440 }
1441
1442 #[test]
1443 fn integration_twocrypto_stable_crvusd_weth() {
1444 let state = RawPoolState {
1446 variant: CurveVariant::TwoCryptoStable,
1447 balances: vec![u("17087755783041929282185464"), u("13675635632110845893058")],
1448 token_decimals: vec![18, 18],
1449 amp: U256::from(25_000u64),
1450 d: Some(u("53892663239303863640675237")),
1451 price_scale: Some(vec![u("2783064941591876143844")]),
1452 mid_fee: Some(U256::from(60_000_000u64)),
1453 out_fee: Some(U256::from(220_000_000u64)),
1454 fee_gamma: Some(u("1395000000000000")),
1455 ..Default::default()
1456 };
1457 let pool = build_pool(&state).unwrap();
1458 let dy = pool
1459 .get_amount_out(0, 1, u("170877557830419292821854"))
1460 .unwrap();
1461 assert_eq!(dy, u("77522288630419592645"));
1462 }
1463
1464 #[test]
1465 fn integration_tricrypto_v1_usdt_wbtc_weth() {
1466 let state = RawPoolState {
1468 variant: CurveVariant::TriCryptoV1,
1469 balances: vec![u("3687737692530"), u("5185841754"), u("1696614171366863858308")],
1470 token_decimals: vec![6, 8, 18],
1471 amp: U256::from(1_707_629u64),
1472 d: Some(u("11006845200255249518958282")),
1473 gamma: Some(u("11809167828997")),
1474 price_scale: Some(vec![u("70578404679338064954709"), u("2156666095129214805267")]),
1475 mid_fee: Some(U256::from(3_000_000u64)),
1476 out_fee: Some(U256::from(30_000_000u64)),
1477 fee_gamma: Some(u("500000000000000")),
1478 ..Default::default()
1479 };
1480 let pool = build_pool(&state).unwrap();
1481 let dy = pool
1482 .get_amount_out(0, 1, u("36877376925"))
1483 .unwrap();
1484 assert_eq!(dy, U256::from(51_646_866u64));
1485 }
1486
1487 #[test]
1488 fn integration_tricrypto_ng_usdc_wbtc_weth() {
1489 let state = RawPoolState {
1491 variant: CurveVariant::TriCryptoNG,
1492 balances: vec![u("3323859056394"), u("4735137544"), u("1544027711277257449902")],
1493 token_decimals: vec![6, 8, 18],
1494 amp: U256::from(1_707_629u64),
1495 d: Some(u("10010654847128420517547506")),
1496 gamma: Some(u("11809167828997")),
1497 price_scale: Some(vec![u("70750968814053384159761"), u("2161000205852311064272")]),
1498 mid_fee: Some(U256::from(3_000_000u64)),
1499 out_fee: Some(U256::from(30_000_000u64)),
1500 fee_gamma: Some(u("500000000000000")),
1501 ..Default::default()
1502 };
1503 let pool = build_pool(&state).unwrap();
1504 let dy = pool
1505 .get_amount_out(0, 1, u("33238590563"))
1506 .unwrap();
1507 assert_eq!(dy, U256::from(46_932_317u64));
1508 }
1509}