1use crate::{bn::U192, math::FeeCalculator};
4use num_traits::ToPrimitive;
5use stable_swap_client::{
6 fees::Fees,
7 solana_program::{clock::Clock, program_error::ProgramError, sysvar::Sysvar},
8 state::SwapInfo,
9};
10
11pub const N_COINS: u8 = 2;
14
15pub const ZERO_TS: i64 = 0;
17
18pub const MIN_RAMP_DURATION: i64 = 86_400;
20
21pub const MIN_AMP: u64 = 1;
23
24pub const MAX_AMP: u64 = 1_000_000;
26
27pub const MAX_TOKENS_IN: u64 = u64::MAX >> 4;
29
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct SwapResult {
33 pub new_source_amount: u64,
35 pub new_destination_amount: u64,
37 pub amount_swapped: u64,
39 pub admin_fee: u64,
41 pub fee: u64,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub struct StableSwap {
60 initial_amp_factor: u64,
62 target_amp_factor: u64,
64 current_ts: i64,
66 start_ramp_ts: i64,
68 stop_ramp_ts: i64,
70}
71
72impl TryFrom<&SwapInfo> for StableSwap {
73 type Error = ProgramError;
74
75 fn try_from(info: &SwapInfo) -> Result<Self, ProgramError> {
76 Ok(StableSwap::new_from_swap_info(
77 info,
78 Clock::get()?.unix_timestamp,
79 ))
80 }
81}
82
83impl StableSwap {
84 pub fn new_from_swap_info(info: &SwapInfo, current_ts: i64) -> StableSwap {
86 StableSwap::new(
87 info.initial_amp_factor,
88 info.target_amp_factor,
89 current_ts,
90 info.start_ramp_ts,
91 info.stop_ramp_ts,
92 )
93 }
94
95 pub fn new(
97 initial_amp_factor: u64,
98 target_amp_factor: u64,
99 current_ts: i64,
100 start_ramp_ts: i64,
101 stop_ramp_ts: i64,
102 ) -> Self {
103 Self {
104 initial_amp_factor,
105 target_amp_factor,
106 current_ts,
107 start_ramp_ts,
108 stop_ramp_ts,
109 }
110 }
111
112 fn compute_next_d(
113 &self,
114 amp_factor: u64,
115 d_init: U192,
116 d_prod: U192,
117 sum_x: u64,
118 ) -> Option<U192> {
119 let ann = amp_factor.checked_mul(N_COINS.into())?;
120 let leverage = (sum_x as u128).checked_mul(ann.into())?;
121 let numerator = d_init.checked_mul(
123 d_prod
124 .checked_mul(N_COINS.into())?
125 .checked_add(leverage.into())?,
126 )?;
127 let denominator = d_init
128 .checked_mul(ann.checked_sub(1)?.into())?
129 .checked_add(d_prod.checked_mul((N_COINS.checked_add(1)?).into())?)?;
130 numerator.checked_div(denominator)
131 }
132
133 pub fn compute_amp_factor(&self) -> Option<u64> {
143 if self.current_ts < self.stop_ramp_ts {
144 let time_range = self.stop_ramp_ts.checked_sub(self.start_ramp_ts)?;
145 let time_delta = self.current_ts.checked_sub(self.start_ramp_ts)?;
146
147 if self.target_amp_factor >= self.initial_amp_factor {
149 let amp_range = self
151 .target_amp_factor
152 .checked_sub(self.initial_amp_factor)?;
153 let amp_delta = (amp_range as u128)
154 .checked_mul(time_delta.to_u128()?)?
155 .checked_div(time_range.to_u128()?)?
156 .to_u64()?;
157 self.initial_amp_factor.checked_add(amp_delta)
158 } else {
159 let amp_range = self
161 .initial_amp_factor
162 .checked_sub(self.target_amp_factor)?;
163 let amp_delta = (amp_range as u128)
164 .checked_mul(time_delta.to_u128()?)?
165 .checked_div(time_range.to_u128()?)?
166 .to_u64()?;
167 self.initial_amp_factor.checked_sub(amp_delta)
168 }
169 } else {
170 Some(self.target_amp_factor)
172 }
173 }
174
175 pub fn compute_d(&self, amount_a: u64, amount_b: u64) -> Option<U192> {
190 let sum_x = amount_a.checked_add(amount_b)?; if sum_x == 0 {
192 Some(0.into())
193 } else {
194 let amp_factor = self.compute_amp_factor()?;
195 let amount_a_times_coins = amount_a.checked_mul(N_COINS.into())?;
196 let amount_b_times_coins = amount_b.checked_mul(N_COINS.into())?;
197
198 let mut d_prev: U192;
200 let mut d: U192 = sum_x.into();
201 for _ in 0..256 {
202 let mut d_prod = d;
203 d_prod = d_prod
204 .checked_mul(d)?
205 .checked_div(amount_a_times_coins.into())?;
206 d_prod = d_prod
207 .checked_mul(d)?
208 .checked_div(amount_b_times_coins.into())?;
209 d_prev = d;
210 d = self.compute_next_d(amp_factor, d, d_prod, sum_x)?;
211 if d > d_prev {
213 if d.checked_sub(d_prev)? <= 1.into() {
214 break;
215 }
216 } else if d_prev.checked_sub(d)? <= 1.into() {
217 break;
218 }
219 }
220
221 Some(d)
222 }
223 }
224
225 pub fn compute_mint_amount_for_deposit(
227 &self,
228 deposit_amount_a: u64,
229 deposit_amount_b: u64,
230 swap_amount_a: u64,
231 swap_amount_b: u64,
232 pool_token_supply: u64,
233 fees: &Fees,
234 ) -> Option<u64> {
235 let d_0 = self.compute_d(swap_amount_a, swap_amount_b)?;
237 let old_balances = [swap_amount_a, swap_amount_b];
238 let mut new_balances = [
239 swap_amount_a.checked_add(deposit_amount_a)?,
240 swap_amount_b.checked_add(deposit_amount_b)?,
241 ];
242 let d_1 = self.compute_d(new_balances[0], new_balances[1])?;
244 if d_1 <= d_0 {
245 None
246 } else {
247 for i in 0..new_balances.len() {
249 let ideal_balance = d_1
250 .checked_mul(old_balances[i].into())?
251 .checked_div(d_0)?
252 .to_u64()?;
253 let difference = if ideal_balance > new_balances[i] {
254 ideal_balance.checked_sub(new_balances[i])?
255 } else {
256 new_balances[i].checked_sub(ideal_balance)?
257 };
258 let fee = fees.normalized_trade_fee(N_COINS, difference)?;
259 new_balances[i] = new_balances[i].checked_sub(fee)?;
260 }
261
262 let d_2 = self.compute_d(new_balances[0], new_balances[1])?;
263 U192::from(pool_token_supply)
264 .checked_mul(d_2.checked_sub(d_0)?)?
265 .checked_div(d_0)?
266 .to_u64()
267 }
268 }
269
270 #[allow(clippy::many_single_char_names)]
279 pub fn compute_y_raw(&self, x: u64, d: U192) -> Option<U192> {
280 let amp_factor = self.compute_amp_factor()?;
281 let ann = amp_factor.checked_mul(N_COINS.into())?; let mut c = d
286 .checked_mul(d)?
287 .checked_div(x.checked_mul(N_COINS.into())?.into())?;
288 c = c
289 .checked_mul(d)?
290 .checked_div(ann.checked_mul(N_COINS.into())?.into())?;
291 let b = d.checked_div(ann.into())?.checked_add(x.into())?; let mut y_prev: U192;
296 let mut y = d;
297 for _ in 0..256 {
298 y_prev = y;
299 let y_numerator = y.checked_pow(2.into())?.checked_add(c)?;
301 let y_denominator = y.checked_mul(2.into())?.checked_add(b)?.checked_sub(d)?;
302 y = y_numerator.checked_div(y_denominator)?;
303 if y > y_prev {
304 if y.checked_sub(y_prev)? <= 1.into() {
305 break;
306 }
307 } else if y_prev.checked_sub(y)? <= 1.into() {
308 break;
309 }
310 }
311 Some(y)
312 }
313
314 pub fn compute_y(&self, x: u64, d: U192) -> Option<u64> {
316 self.compute_y_raw(x, d)?.to_u64()
317 }
318
319 pub fn compute_withdraw_one(
326 &self,
327 pool_token_amount: u64,
328 pool_token_supply: u64,
329 swap_base_amount: u64, swap_quote_amount: u64, fees: &Fees,
332 ) -> Option<(u64, u64)> {
333 let d_0 = self.compute_d(swap_base_amount, swap_quote_amount)?;
334 let d_1 = d_0.checked_sub(
335 U192::from(pool_token_amount)
336 .checked_mul(d_0)?
337 .checked_div(pool_token_supply.into())?,
338 )?;
339 let new_y = self.compute_y(swap_quote_amount, d_1)?;
340
341 let expected_base_amount = U192::from(swap_base_amount)
343 .checked_mul(d_1)?
344 .checked_div(d_0)?
345 .to_u64()?
346 .checked_sub(new_y)?;
347 let expected_quote_amount = swap_quote_amount.checked_sub(
349 U192::from(swap_quote_amount)
350 .checked_mul(d_1)?
351 .checked_div(d_0)?
352 .to_u64()?,
353 )?;
354 let new_base_amount = swap_base_amount
356 .checked_sub(fees.normalized_trade_fee(N_COINS, expected_base_amount)?)?;
357 let new_quote_amount = swap_quote_amount
359 .checked_sub(fees.normalized_trade_fee(N_COINS, expected_quote_amount)?)?;
360 let dy = new_base_amount
361 .checked_sub(self.compute_y(new_quote_amount, d_1)?)?
362 .checked_sub(1)?; let dy_0 = swap_base_amount.checked_sub(new_y)?;
364
365 Some((dy, dy_0.checked_sub(dy)?))
366 }
367
368 pub fn swap_to(
370 &self,
371 source_amount: u64,
372 swap_source_amount: u64,
373 swap_destination_amount: u64,
374 fees: &Fees,
375 ) -> Option<SwapResult> {
376 let y = self.compute_y(
377 swap_source_amount.checked_add(source_amount)?,
378 self.compute_d(swap_source_amount, swap_destination_amount)?,
379 )?;
380 let dy = swap_destination_amount.checked_sub(y)?.checked_sub(1)?;
382 let dy_fee = fees.trade_fee(dy)?;
383 let admin_fee = fees.admin_trade_fee(dy_fee)?;
384
385 let amount_swapped = dy.checked_sub(dy_fee)?;
386 let new_destination_amount = swap_destination_amount
387 .checked_sub(amount_swapped)?
388 .checked_sub(admin_fee)?;
389 let new_source_amount = swap_source_amount.checked_add(source_amount)?;
390
391 Some(SwapResult {
392 new_source_amount,
393 new_destination_amount,
394 amount_swapped,
395 admin_fee,
396 fee: dy_fee,
397 })
398 }
399}
400
401#[cfg(test)]
402#[allow(
403 clippy::unwrap_used,
404 clippy::integer_arithmetic,
405 clippy::too_many_arguments
406)]
407mod tests {
408 use super::*;
409 use crate::pool_converter::PoolTokenConverter;
410 use proptest::prelude::*;
411 use rand::Rng;
412 use sim::{Model, MODEL_FEE_DENOMINATOR, MODEL_FEE_NUMERATOR};
413 use std::cmp;
414
415 const ZERO_FEES: Fees = Fees {
416 admin_trade_fee_numerator: 0,
417 admin_trade_fee_denominator: 1000,
418 admin_withdraw_fee_numerator: 0,
419 admin_withdraw_fee_denominator: 1000,
420 trade_fee_numerator: 0,
421 trade_fee_denominator: 1000,
422 withdraw_fee_numerator: 0,
423 withdraw_fee_denominator: 1000,
424 };
425 const MODEL_FEES: Fees = Fees {
426 admin_trade_fee_numerator: 0,
427 admin_trade_fee_denominator: 1,
428 admin_withdraw_fee_numerator: 0,
429 admin_withdraw_fee_denominator: 1,
430 trade_fee_numerator: MODEL_FEE_NUMERATOR,
431 trade_fee_denominator: MODEL_FEE_DENOMINATOR,
432 withdraw_fee_numerator: 0,
433 withdraw_fee_denominator: 1,
434 };
435
436 const RAMP_TICKS: i64 = 100000;
437
438 #[test]
439 fn test_ramp_amp_up() {
440 let mut rng = rand::thread_rng();
441 let initial_amp_factor = 100;
442 let target_amp_factor = initial_amp_factor * 2;
443 let start_ramp_ts = rng.gen_range(ZERO_TS..=i64::MAX - RAMP_TICKS);
444 let stop_ramp_ts = start_ramp_ts + MIN_RAMP_DURATION;
445 println!(
446 "start_ramp_ts: {}, stop_ramp_ts: {}",
447 start_ramp_ts, stop_ramp_ts
448 );
449
450 for tick in 0..RAMP_TICKS {
451 let current_ts = start_ramp_ts + tick;
452 let invariant = StableSwap::new(
453 initial_amp_factor,
454 target_amp_factor,
455 current_ts,
456 start_ramp_ts,
457 stop_ramp_ts,
458 );
459 let expected = if tick >= MIN_RAMP_DURATION {
460 target_amp_factor
461 } else {
462 initial_amp_factor + (initial_amp_factor * tick as u64 / MIN_RAMP_DURATION as u64)
463 };
464 assert_eq!(invariant.compute_amp_factor().unwrap(), expected);
465 }
466 }
467
468 #[test]
469 fn test_ramp_amp_down() {
470 let mut rng = rand::thread_rng();
471 let initial_amp_factor = 100;
472 let target_amp_factor = initial_amp_factor / 10;
473 let amp_range = initial_amp_factor - target_amp_factor;
474 let start_ramp_ts = rng.gen_range(ZERO_TS..=i64::MAX - RAMP_TICKS);
475 let stop_ramp_ts = start_ramp_ts + MIN_RAMP_DURATION;
476 println!(
477 "start_ramp_ts: {}, stop_ramp_ts: {}",
478 start_ramp_ts, stop_ramp_ts
479 );
480
481 for tick in 0..RAMP_TICKS {
482 let current_ts = start_ramp_ts + tick;
483 let invariant = StableSwap::new(
484 initial_amp_factor,
485 target_amp_factor,
486 current_ts,
487 start_ramp_ts,
488 stop_ramp_ts,
489 );
490 let expected = if tick >= MIN_RAMP_DURATION {
491 target_amp_factor
492 } else {
493 initial_amp_factor - (amp_range * tick as u64 / MIN_RAMP_DURATION as u64)
494 };
495 assert_eq!(invariant.compute_amp_factor().unwrap(), expected);
496 }
497 }
498
499 fn check_d(
500 model: &Model,
501 amount_a: u64,
502 amount_b: u64,
503 current_ts: i64,
504 start_ramp_ts: i64,
505 stop_ramp_ts: i64,
506 ) -> U192 {
507 let swap = StableSwap {
508 initial_amp_factor: model.amp_factor,
509 target_amp_factor: model.amp_factor,
510 current_ts,
511 start_ramp_ts,
512 stop_ramp_ts,
513 };
514 let d = swap.compute_d(amount_a, amount_b).unwrap();
515 assert_eq!(d, model.sim_d().into());
516 d
517 }
518
519 fn check_y(
520 model: &Model,
521 x: u64,
522 d: U192,
523 current_ts: i64,
524 start_ramp_ts: i64,
525 stop_ramp_ts: i64,
526 ) {
527 let swap = StableSwap {
528 initial_amp_factor: model.amp_factor,
529 target_amp_factor: model.amp_factor,
530 current_ts,
531 start_ramp_ts,
532 stop_ramp_ts,
533 };
534 assert_eq!(
535 swap.compute_y_raw(x, d).unwrap().to_u128().unwrap(),
536 model.sim_y(0, 1, x)
537 )
538 }
539
540 proptest! {
541 #[test]
542 fn test_curve_math(
543 current_ts in ZERO_TS..i64::MAX,
544 amp_factor in MIN_AMP..=MAX_AMP,
545 amount_a in 1..MAX_TOKENS_IN, amount_b in 1..MAX_TOKENS_IN, ) {
548 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
549 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
550 let model = Model::new(amp_factor, vec![amount_a, amount_b], N_COINS);
551 let d = check_d(&model, amount_a, amount_b, current_ts, start_ramp_ts, stop_ramp_ts);
552 check_y(&model, amount_a, d, current_ts, start_ramp_ts, stop_ramp_ts);
553 }
554 }
555
556 #[test]
557 fn test_curve_math_specific() {
558 let current_ts = ZERO_TS;
560 let start_ramp_ts = ZERO_TS;
561 let stop_ramp_ts = ZERO_TS;
562 let model_no_balance = Model::new(1, vec![0, 0], N_COINS);
563 check_d(
564 &model_no_balance,
565 0,
566 0,
567 current_ts,
568 start_ramp_ts,
569 stop_ramp_ts,
570 );
571
572 let amount_a: u64 = 1046129065254161082;
573 let amount_b: u64 = 1250710035549196829;
574 let model = Model::new(1188, vec![amount_a, amount_b], N_COINS);
575 let d = check_d(
576 &model,
577 amount_a,
578 amount_b,
579 current_ts,
580 start_ramp_ts,
581 stop_ramp_ts,
582 );
583 let amount_x: u64 = 2045250484898639148;
584 check_y(&model, amount_x, d, current_ts, start_ramp_ts, stop_ramp_ts);
585
586 let amount_a: u64 = 862538457714585493;
587 let amount_b: u64 = 492548187909826733;
588 let model = Model::new(9, vec![amount_a, amount_b], N_COINS);
589 let d = check_d(
590 &model,
591 amount_a,
592 amount_b,
593 current_ts,
594 start_ramp_ts,
595 stop_ramp_ts,
596 );
597 let amount_x: u64 = 8155777549389559399;
598 check_y(&model, amount_x, d, current_ts, start_ramp_ts, stop_ramp_ts);
599 }
600
601 #[test]
602 fn test_compute_mint_amount_for_deposit() {
603 let initial_amp_factor = MIN_AMP;
604 let target_amp_factor = MAX_AMP;
605 let current_ts = MIN_RAMP_DURATION / 2;
606 let start_ramp_ts = ZERO_TS;
607 let stop_ramp_ts = MIN_RAMP_DURATION;
608 let invariant = StableSwap::new(
609 initial_amp_factor,
610 target_amp_factor,
611 current_ts,
612 start_ramp_ts,
613 stop_ramp_ts,
614 );
615
616 let deposit_amount_a = MAX_TOKENS_IN;
617 let deposit_amount_b = MAX_TOKENS_IN;
618 let swap_amount_a = MAX_TOKENS_IN;
619 let swap_amount_b = MAX_TOKENS_IN;
620 let pool_token_supply = MAX_TOKENS_IN;
621 let actual_mint_amount = invariant
622 .compute_mint_amount_for_deposit(
623 deposit_amount_a,
624 deposit_amount_b,
625 swap_amount_a,
626 swap_amount_b,
627 pool_token_supply,
628 &MODEL_FEES,
629 )
630 .unwrap();
631 let expected_mint_amount = MAX_TOKENS_IN;
632 assert_eq!(actual_mint_amount, expected_mint_amount);
633 }
634
635 #[test]
636 fn test_curve_math_with_random_inputs() {
637 for _ in 0..100 {
638 let mut rng = rand::thread_rng();
639
640 let amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP);
641 let amount_a: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
642 let amount_b: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
643 let start_ramp_ts: i64 = rng.gen_range(ZERO_TS..=i64::MAX);
644 let stop_ramp_ts: i64 = rng.gen_range(start_ramp_ts..=i64::MAX);
645 let current_ts: i64 = rng.gen_range(start_ramp_ts..=stop_ramp_ts);
646 println!("testing curve_math_with_random_inputs:");
647 println!(
648 "current_ts: {}, start_ramp_ts: {}, stop_ramp_ts: {}",
649 current_ts, start_ramp_ts, stop_ramp_ts
650 );
651 println!(
652 "amp_factor: {}, amount_a: {}, amount_b: {}",
653 amp_factor, amount_a, amount_b,
654 );
655
656 let model = Model::new(amp_factor, vec![amount_a, amount_b], N_COINS);
657 let d = check_d(
658 &model,
659 amount_a,
660 amount_b,
661 current_ts,
662 start_ramp_ts,
663 stop_ramp_ts,
664 );
665 let amount_x: u64 = rng.gen_range(0..=amount_a);
666
667 println!("amount_x: {}", amount_x);
668 check_y(&model, amount_x, d, current_ts, start_ramp_ts, stop_ramp_ts);
669 }
670 }
671
672 fn check_swap(
673 initial_amp_factor: u64,
674 target_amp_factor: u64,
675 current_ts: i64,
676 start_ramp_ts: i64,
677 stop_ramp_ts: i64,
678 source_amount: u64,
679 swap_source_amount: u64,
680 swap_destination_amount: u64,
681 ) {
682 let swap = StableSwap::new(
683 initial_amp_factor,
684 target_amp_factor,
685 current_ts,
686 start_ramp_ts,
687 stop_ramp_ts,
688 );
689 let result = swap
690 .swap_to(
691 source_amount,
692 swap_source_amount,
693 swap_destination_amount,
694 &MODEL_FEES,
695 )
696 .unwrap();
697 let model = Model::new(
698 swap.compute_amp_factor().unwrap(),
699 vec![swap_source_amount, swap_destination_amount],
700 N_COINS,
701 );
702
703 let expected_amount_swapped = model.sim_exchange(0, 1, source_amount.into());
704 let diff = (expected_amount_swapped as i128 - result.amount_swapped as i128).abs();
705 let tolerance = std::cmp::max(1, expected_amount_swapped as i128 / 1_000_000_000);
706 assert!(
707 diff <= tolerance,
708 "result={:?}, expected_amount_swapped={}, amp={}, source_amount={}, swap_source_amount={}, swap_destination_amount={}, diff={}",
709 result,
710 expected_amount_swapped,
711 swap.compute_amp_factor().unwrap(),
712 source_amount,
713 swap_source_amount,
714 swap_destination_amount,
715 diff
716 );
717 assert_eq!(result.new_source_amount, swap_source_amount + source_amount);
718 assert_eq!(
719 result.new_destination_amount,
720 swap_destination_amount - result.amount_swapped
721 );
722 }
723
724 proptest! {
725 #[test]
726 fn test_swap_calculation(
727 current_ts in ZERO_TS..i64::MAX,
728 amp_factor in MIN_AMP..=MAX_AMP,
729 source_amount in 0..MAX_TOKENS_IN,
730 swap_source_amount in 0..MAX_TOKENS_IN,
731 swap_destination_amount in 0..MAX_TOKENS_IN,
732 ) {
733 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
734 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
735 check_swap(
736 amp_factor,
737 amp_factor,
738 current_ts,
739 start_ramp_ts,
740 stop_ramp_ts,
741 source_amount,
742 swap_source_amount,
743 swap_destination_amount,
744 );
745 }
746 }
747
748 #[test]
749 fn test_swap_calculation_with_random_inputs() {
750 for _ in 0..100 {
751 let mut rng = rand::thread_rng();
752
753 let initial_amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP);
754 let target_amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP);
755 let start_ramp_ts: i64 = rng.gen_range(ZERO_TS..=i64::MAX);
756 let stop_ramp_ts: i64 = rng.gen_range(start_ramp_ts..=i64::MAX);
757 let current_ts: i64 = rng.gen_range(start_ramp_ts..=stop_ramp_ts);
758 let source_amount: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
759 let swap_source_amount: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
760 let swap_destination_amount: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
761 println!("testing swap_calculation_with_random_inputs:");
762 println!(
763 "current_ts: {}, start_ramp_ts: {}, stop_ramp_ts: {}",
764 current_ts, start_ramp_ts, stop_ramp_ts
765 );
766 println!(
767 "initial_amp_factor: {}, target_amp_factor: {}, source_amount: {}, swap_source_amount: {}, swap_destination_amount: {}",
768 initial_amp_factor, target_amp_factor, source_amount, swap_source_amount, swap_destination_amount
769 );
770
771 check_swap(
772 initial_amp_factor,
773 target_amp_factor,
774 current_ts,
775 start_ramp_ts,
776 stop_ramp_ts,
777 source_amount,
778 swap_source_amount,
779 swap_destination_amount,
780 );
781 }
782 }
783
784 #[derive(Debug)]
785 struct SwapTest<'a> {
786 pub stable_swap: &'a StableSwap,
787 pub swap_reserve_balance_a: u64,
788 pub swap_reserve_balance_b: u64,
789 pub user_token_balance_a: u64,
790 pub user_token_balance_b: u64,
791 }
792
793 impl SwapTest<'_> {
794 pub fn swap_a_to_b(&mut self, swap_amount: u64) {
795 self.do_swap(true, swap_amount)
796 }
797
798 pub fn swap_b_to_a(&mut self, swap_amount: u64) {
799 self.do_swap(false, swap_amount)
800 }
801
802 fn do_swap(&mut self, swap_a_to_b: bool, source_amount: u64) {
803 let (swap_source_amount, swap_dest_amount) = match swap_a_to_b {
804 true => (self.swap_reserve_balance_a, self.swap_reserve_balance_b),
805 false => (self.swap_reserve_balance_b, self.swap_reserve_balance_a),
806 };
807
808 let SwapResult {
809 new_source_amount,
810 new_destination_amount,
811 amount_swapped,
812 ..
813 } = self
814 .stable_swap
815 .swap_to(
816 source_amount,
817 swap_source_amount,
818 swap_dest_amount,
819 &ZERO_FEES,
820 )
821 .unwrap();
822
823 match swap_a_to_b {
824 true => {
825 self.swap_reserve_balance_a = new_source_amount;
826 self.swap_reserve_balance_b = new_destination_amount;
827 self.user_token_balance_a -= source_amount;
828 self.user_token_balance_b += amount_swapped;
829 }
830 false => {
831 self.swap_reserve_balance_a = new_destination_amount;
832 self.swap_reserve_balance_b = new_source_amount;
833 self.user_token_balance_a += amount_swapped;
834 self.user_token_balance_b -= source_amount;
835 }
836 }
837 }
838 }
839
840 proptest! {
841 #[test]
842 fn test_swaps_does_not_result_in_more_tokens(
843 amp_factor in MIN_AMP..=MAX_AMP,
844 initial_user_token_a_amount in 10_000_000..MAX_TOKENS_IN >> 16,
845 initial_user_token_b_amount in 10_000_000..MAX_TOKENS_IN >> 16,
846 ) {
847
848 let stable_swap = StableSwap {
849 initial_amp_factor: amp_factor,
850 target_amp_factor: amp_factor,
851 current_ts: ZERO_TS,
852 start_ramp_ts: ZERO_TS,
853 stop_ramp_ts: ZERO_TS
854 };
855 let mut t = SwapTest { stable_swap: &stable_swap, swap_reserve_balance_a: MAX_TOKENS_IN, swap_reserve_balance_b: MAX_TOKENS_IN, user_token_balance_a: initial_user_token_a_amount, user_token_balance_b: initial_user_token_b_amount };
856
857 const ITERATIONS: u64 = 100;
858 const SHRINK_MULTIPLIER: u64= 10;
859
860 for i in 0..ITERATIONS {
861 let before_balance_a = t.user_token_balance_a;
862 let before_balance_b = t.user_token_balance_b;
863 let swap_amount = before_balance_a / ((i + 1) * SHRINK_MULTIPLIER);
864 t.swap_a_to_b(swap_amount);
865 let after_balance = t.user_token_balance_a + t.user_token_balance_b;
866
867 assert!(before_balance_a + before_balance_b >= after_balance, "before_a: {}, before_b: {}, after_a: {}, after_b: {}, swap: {:?}", before_balance_a, before_balance_b, t.user_token_balance_a, t.user_token_balance_b, stable_swap);
868 }
869
870 for i in 0..ITERATIONS {
871 let before_balance_a = t.user_token_balance_a;
872 let before_balance_b = t.user_token_balance_b;
873 let swap_amount = before_balance_a / ((i + 1) * SHRINK_MULTIPLIER);
874 t.swap_a_to_b(swap_amount);
875 let after_balance = t.user_token_balance_a + t.user_token_balance_b;
876
877 assert!(before_balance_a + before_balance_b >= after_balance, "before_a: {}, before_b: {}, after_a: {}, after_b: {}, swap: {:?}", before_balance_a, before_balance_b, t.user_token_balance_a, t.user_token_balance_b, stable_swap);
878 }
879 }
880 }
881
882 #[test]
883 fn test_swaps_does_not_result_in_more_tokens_specific_one() {
884 const AMP_FACTOR: u64 = 324449;
885 const INITIAL_SWAP_RESERVE_AMOUNT: u64 = 100_000_000_000;
886 const INITIAL_USER_TOKEN_AMOUNT: u64 = 10_000_000_000;
887
888 let stable_swap = StableSwap {
889 initial_amp_factor: AMP_FACTOR,
890 target_amp_factor: AMP_FACTOR,
891 current_ts: ZERO_TS,
892 start_ramp_ts: ZERO_TS,
893 stop_ramp_ts: ZERO_TS,
894 };
895
896 let mut t = SwapTest {
897 stable_swap: &stable_swap,
898 swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT,
899 swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT,
900 user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT,
901 user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT,
902 };
903
904 t.swap_a_to_b(2097152);
905 t.swap_a_to_b(8053063680);
906 t.swap_a_to_b(48);
907 assert!(t.user_token_balance_a + t.user_token_balance_b <= INITIAL_USER_TOKEN_AMOUNT * 2);
908 }
909
910 #[test]
911 fn test_swaps_does_not_result_in_more_tokens_specific_two() {
912 const AMP_FACTOR: u64 = 186512;
913 const INITIAL_SWAP_RESERVE_AMOUNT: u64 = 100_000_000_000;
914 const INITIAL_USER_TOKEN_AMOUNT: u64 = 1_000_000_000;
915
916 let stable_swap = StableSwap {
917 initial_amp_factor: AMP_FACTOR,
918 target_amp_factor: AMP_FACTOR,
919 current_ts: ZERO_TS,
920 start_ramp_ts: ZERO_TS,
921 stop_ramp_ts: ZERO_TS,
922 };
923
924 let mut t = SwapTest {
925 stable_swap: &stable_swap,
926 swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT,
927 swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT,
928 user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT,
929 user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT,
930 };
931
932 t.swap_b_to_a(33579101);
933 t.swap_a_to_b(2097152);
934 assert!(t.user_token_balance_a + t.user_token_balance_b <= INITIAL_USER_TOKEN_AMOUNT * 2);
935 }
936
937 #[test]
938 fn test_swaps_does_not_result_in_more_tokens_specific_three() {
939 const AMP_FACTOR: u64 = 1220;
940 const INITIAL_SWAP_RESERVE_AMOUNT: u64 = 100_000_000_000;
941 const INITIAL_USER_TOKEN_AMOUNT: u64 = 1_000_000_000;
942
943 let stable_swap = StableSwap {
944 initial_amp_factor: AMP_FACTOR,
945 target_amp_factor: AMP_FACTOR,
946 current_ts: ZERO_TS,
947 start_ramp_ts: ZERO_TS,
948 stop_ramp_ts: ZERO_TS,
949 };
950
951 let mut t = SwapTest {
952 stable_swap: &stable_swap,
953 swap_reserve_balance_a: INITIAL_SWAP_RESERVE_AMOUNT,
954 swap_reserve_balance_b: INITIAL_SWAP_RESERVE_AMOUNT,
955 user_token_balance_a: INITIAL_USER_TOKEN_AMOUNT,
956 user_token_balance_b: INITIAL_USER_TOKEN_AMOUNT,
957 };
958
959 t.swap_b_to_a(65535);
960 t.swap_b_to_a(6133503);
961 t.swap_a_to_b(65535);
962 assert!(t.user_token_balance_a + t.user_token_balance_b <= INITIAL_USER_TOKEN_AMOUNT * 2);
963 }
964
965 fn check_withdraw_one(
966 initial_amp_factor: u64,
967 target_amp_factor: u64,
968 current_ts: i64,
969 start_ramp_ts: i64,
970 stop_ramp_ts: i64,
971 pool_token_amount: u64,
972 pool_token_supply: u64,
973 swap_base_amount: u64,
974 swap_quote_amount: u64,
975 ) {
976 let swap = StableSwap::new(
977 initial_amp_factor,
978 target_amp_factor,
979 current_ts,
980 start_ramp_ts,
981 stop_ramp_ts,
982 );
983 let result = swap
984 .compute_withdraw_one(
985 pool_token_amount,
986 pool_token_supply,
987 swap_base_amount,
988 swap_quote_amount,
989 &MODEL_FEES,
990 )
991 .unwrap();
992 let model = Model::new_with_pool_tokens(
993 swap.compute_amp_factor().unwrap(),
994 vec![swap_base_amount, swap_quote_amount],
995 N_COINS,
996 pool_token_supply,
997 );
998 assert_eq!(
999 result.0,
1000 model.sim_calc_withdraw_one_coin(pool_token_amount, 0).0
1001 );
1002 assert_eq!(
1003 result.1,
1004 model.sim_calc_withdraw_one_coin(pool_token_amount, 0).1
1005 );
1006 }
1007
1008 proptest! {
1009 #[test]
1010 fn test_compute_withdraw_one(
1011 current_ts in ZERO_TS..i64::MAX,
1012 amp_factor in MIN_AMP..=MAX_AMP,
1013 pool_token_amount in 1..MAX_TOKENS_IN / 2,
1014 swap_base_amount in 1..MAX_TOKENS_IN / 2,
1015 swap_quote_amount in 1..MAX_TOKENS_IN / 2,
1016 ) {
1017 let pool_token_supply = MAX_TOKENS_IN;
1018 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
1019 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
1020 check_withdraw_one(
1021 amp_factor,
1022 amp_factor,
1023 current_ts,
1024 start_ramp_ts,
1025 stop_ramp_ts,
1026 pool_token_amount,
1027 pool_token_supply,
1028 swap_base_amount,
1029 swap_quote_amount,
1030 );
1031 }
1032 }
1033
1034 #[test]
1035 fn test_compute_withdraw_one_with_random_inputs() {
1036 for _ in 0..100 {
1037 let mut rng = rand::thread_rng();
1038
1039 let initial_amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP);
1040 let target_amp_factor: u64 = rng.gen_range(MIN_AMP..=MAX_AMP);
1041 let start_ramp_ts: i64 = rng.gen_range(ZERO_TS..=i64::MAX);
1042 let stop_ramp_ts: i64 = rng.gen_range(start_ramp_ts..=i64::MAX);
1043 let current_ts: i64 = rng.gen_range(start_ramp_ts..=stop_ramp_ts);
1044 let swap_base_amount: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
1045 let swap_quote_amount: u64 = rng.gen_range(1..=MAX_TOKENS_IN);
1046 let pool_token_supply = swap_base_amount + swap_quote_amount;
1047 let pool_token_amount: u64 = rng.gen_range(1..=pool_token_supply);
1048 println!("testing compute_withdraw_one_with_random_inputs:");
1049 println!(
1050 "current_ts: {}, start_ramp_ts: {}, stop_ramp_ts: {}",
1051 current_ts, start_ramp_ts, stop_ramp_ts
1052 );
1053 println!(
1054 "initial_amp_factor: {}, target_amp_factor: {}, swap_base_amount: {}, swap_quote_amount: {}, pool_token_amount: {}, pool_token_supply: {}",
1055 initial_amp_factor, target_amp_factor, swap_base_amount, swap_quote_amount, pool_token_amount, pool_token_supply
1056 );
1057
1058 check_withdraw_one(
1059 initial_amp_factor,
1060 target_amp_factor,
1061 current_ts,
1062 start_ramp_ts,
1063 stop_ramp_ts,
1064 pool_token_amount,
1065 pool_token_supply,
1066 swap_base_amount,
1067 swap_quote_amount,
1068 );
1069 }
1070 }
1071
1072 proptest! {
1073 #[test]
1074 fn test_virtual_price_does_not_decrease_from_deposit(
1075 current_ts in ZERO_TS..i64::MAX,
1076 amp_factor in MIN_AMP..=MAX_AMP,
1077 deposit_amount_a in 0..MAX_TOKENS_IN >> 2,
1078 deposit_amount_b in 0..MAX_TOKENS_IN >> 2,
1079 swap_token_a_amount in 0..MAX_TOKENS_IN,
1080 swap_token_b_amount in 0..MAX_TOKENS_IN,
1081 pool_token_supply in 0..MAX_TOKENS_IN,
1082 ) {
1083 let deposit_amount_a = deposit_amount_a;
1084 let deposit_amount_b = deposit_amount_b;
1085 let swap_token_a_amount = swap_token_a_amount;
1086 let swap_token_b_amount = swap_token_b_amount;
1087 let pool_token_supply = pool_token_supply;
1088
1089 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
1090 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
1091 let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts);
1092 let d0 = invariant.compute_d(swap_token_a_amount, swap_token_b_amount).unwrap();
1093
1094 let mint_amount = invariant.compute_mint_amount_for_deposit(
1095 deposit_amount_a,
1096 deposit_amount_b,
1097 swap_token_a_amount,
1098 swap_token_b_amount,
1099 pool_token_supply,
1100 &MODEL_FEES,
1101 );
1102 prop_assume!(mint_amount.is_some());
1103
1104 let new_swap_token_a_amount = swap_token_a_amount + deposit_amount_a;
1105 let new_swap_token_b_amount = swap_token_b_amount + deposit_amount_b;
1106 let new_pool_token_supply = pool_token_supply + mint_amount.unwrap();
1107 let d1 = invariant.compute_d(new_swap_token_a_amount, new_swap_token_b_amount).unwrap();
1108
1109 assert!(d0 < d1);
1110 assert!(d0 / pool_token_supply <= d1 / new_pool_token_supply);
1111 }
1112 }
1113
1114 proptest! {
1115 #[test]
1116 fn test_virtual_price_does_not_decrease_from_swap(
1117 current_ts in ZERO_TS..i64::MAX,
1118 amp_factor in MIN_AMP..=MAX_AMP,
1119 source_token_amount in 0..MAX_TOKENS_IN,
1120 swap_source_amount in 0..MAX_TOKENS_IN,
1121 swap_destination_amount in 0..MAX_TOKENS_IN,
1122 ) {
1123 let source_token_amount = source_token_amount;
1124 let swap_source_amount = swap_source_amount;
1125 let swap_destination_amount = swap_destination_amount;
1126
1127 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
1128 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
1129 let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts);
1130 let d0 = invariant.compute_d(swap_source_amount, swap_destination_amount).unwrap();
1131
1132 let swap_result = invariant.swap_to(source_token_amount, swap_source_amount, swap_destination_amount, &MODEL_FEES);
1133 prop_assume!(swap_result.is_some());
1134
1135 let swap_result = swap_result.unwrap();
1136 let d1 = invariant.compute_d(swap_result.new_source_amount, swap_result.new_destination_amount).unwrap();
1137
1138 assert!(d0 <= d1); }
1140 }
1141
1142 proptest! {
1143 #[test]
1144 fn test_virtual_price_does_not_decrease_from_withdraw(
1145 current_ts in ZERO_TS..i64::MAX,
1146 amp_factor in MIN_AMP..=MAX_AMP,
1147 (pool_token_supply, pool_token_amount) in total_and_intermediate(),
1148 swap_token_a_amount in 0..MAX_TOKENS_IN,
1149 swap_token_b_amount in 0..MAX_TOKENS_IN,
1150 ) {
1151 let swap_token_a_amount = swap_token_a_amount;
1152 let swap_token_b_amount = swap_token_b_amount;
1153 let pool_token_amount = pool_token_amount;
1154 let pool_token_supply = pool_token_supply;
1155
1156 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
1157 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
1158 let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts);
1159 let d0 = invariant.compute_d(swap_token_a_amount, swap_token_b_amount).unwrap();
1160
1161 let converter = PoolTokenConverter {
1162 supply: pool_token_supply,
1163 token_a: swap_token_a_amount,
1164 token_b: swap_token_b_amount,
1165 fees: &MODEL_FEES,
1166 };
1167
1168 prop_assume!((pool_token_amount as u128) * (swap_token_a_amount as u128) / (pool_token_supply as u128) >= 1);
1171 prop_assume!((pool_token_amount as u128) * (swap_token_b_amount as u128) / (pool_token_supply as u128) >= 1);
1172
1173 let (withdraw_amount_a, _, _) = converter.token_a_rate(pool_token_amount).unwrap();
1174 let (withdraw_amount_b, _, _) = converter.token_b_rate(pool_token_amount).unwrap();
1175
1176 let new_swap_token_a_amount = swap_token_a_amount - withdraw_amount_a;
1177 let new_swap_token_b_amount = swap_token_b_amount - withdraw_amount_b;
1178 let d1 = invariant.compute_d(new_swap_token_a_amount, new_swap_token_b_amount).unwrap();
1179 let new_pool_token_supply = pool_token_supply - pool_token_amount;
1180
1181 assert!(d0 / pool_token_supply <= d1 / new_pool_token_supply);
1182 }
1183 }
1184
1185 proptest! {
1186 #[test]
1187 fn test_virtual_price_does_not_decrease_from_withdraw_one(
1188 current_ts in ZERO_TS..i64::MAX,
1189 amp_factor in MIN_AMP..MAX_AMP,
1190 (pool_token_supply, pool_token_amount) in total_and_intermediate(),
1191 base_token_amount in 0..MAX_TOKENS_IN,
1192 quote_token_amount in 0..MAX_TOKENS_IN,
1193 ) {
1194 let base_token_amount = base_token_amount;
1195 let quote_token_amount = quote_token_amount;
1196 let pool_token_amount = pool_token_amount;
1197 let pool_token_supply = pool_token_supply;
1198
1199 let start_ramp_ts = cmp::max(0, current_ts - MIN_RAMP_DURATION);
1200 let stop_ramp_ts = cmp::min(i64::MAX, current_ts + MIN_RAMP_DURATION);
1201 let invariant = StableSwap::new(amp_factor, amp_factor, current_ts, start_ramp_ts, stop_ramp_ts);
1202 let d0 = invariant.compute_d(base_token_amount, quote_token_amount).unwrap();
1203
1204 prop_assume!(U192::from(pool_token_amount) * U192::from(base_token_amount) / U192::from(pool_token_supply) >= U192::from(1));
1205 let (withdraw_amount, _) = invariant.compute_withdraw_one(pool_token_amount, pool_token_supply, base_token_amount, quote_token_amount, &MODEL_FEES).unwrap();
1206
1207 let new_base_token_amount = base_token_amount - withdraw_amount;
1208 let d1 = invariant.compute_d(new_base_token_amount, quote_token_amount).unwrap();
1209 let new_pool_token_supply = pool_token_supply - pool_token_amount;
1210
1211 assert!(d0 / pool_token_supply <= d1 / new_pool_token_supply);
1212 }
1213 }
1214
1215 prop_compose! {
1216 pub fn total_and_intermediate()(total in 1..MAX_TOKENS_IN)
1217 (intermediate in 1..total, total in Just(total))
1218 -> (u64, u64) {
1219 (total, intermediate)
1220 }
1221 }
1222}