stable_swap_math/
curve.rs

1//! Swap calculations and curve invariant implementation
2
3use 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
11/// Number of coins in a swap.
12/// The Saber StableSwap only supports 2 tokens.
13pub const N_COINS: u8 = 2;
14
15/// Timestamp at 0
16pub const ZERO_TS: i64 = 0;
17
18/// Minimum ramp duration, in seconds.
19pub const MIN_RAMP_DURATION: i64 = 86_400;
20
21/// Minimum amplification coefficient.
22pub const MIN_AMP: u64 = 1;
23
24/// Maximum amplification coefficient.
25pub const MAX_AMP: u64 = 1_000_000;
26
27/// Maximum number of tokens to swap at once.
28pub const MAX_TOKENS_IN: u64 = u64::MAX >> 4;
29
30/// Encodes all results of swapping from a source token to a destination token.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub struct SwapResult {
33    /// New amount of source token
34    pub new_source_amount: u64,
35    /// New amount of destination token
36    pub new_destination_amount: u64,
37    /// Amount of destination token swapped
38    pub amount_swapped: u64,
39    /// Admin fee for the swap
40    pub admin_fee: u64,
41    /// Fee for the swap
42    pub fee: u64,
43}
44
45/// The [StableSwap] invariant calculator.
46///
47/// This is primarily used to calculate two quantities:
48/// - `D`, the swap invariant, and
49/// - `Y`, the amount of tokens swapped in an instruction.
50///
51/// This calculator also contains several helper utilities for computing
52/// swap, withdraw, and deposit amounts.
53///
54/// # Resources:
55///
56/// - [Curve StableSwap paper](https://curve.fi/files/stableswap-paper.pdf)
57/// - [StableSwap Python model](https://github.com/saber-hq/stable-swap/blob/master/stable-swap-math/sim/simulation.py)
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub struct StableSwap {
60    /// Initial amplification coefficient (A)
61    initial_amp_factor: u64,
62    /// Target amplification coefficient (A)
63    target_amp_factor: u64,
64    /// Current unix timestamp
65    current_ts: i64,
66    /// Ramp A start timestamp
67    start_ramp_ts: i64,
68    /// Ramp A stop timestamp
69    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    /// Constructs a new [StableSwap] from a [SwapInfo].
85    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    /// Constructs a new [StableSwap] invariant calculator.
96    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        // d = (ann * sum_x + d_prod * n_coins) * d / ((ann - 1) * d + (n_coins + 1) * d_prod)
122        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    /// Compute the amplification coefficient (A).
134    ///
135    /// The amplification coefficient is used to determine the slippage incurred when
136    /// performing swaps. The lower it is, the closer the invariant is to the constant product[^stableswap].
137    ///
138    /// The amplication coefficient linearly increases with respect to time,
139    /// based on the [`SwapInfo::start_ramp_ts`] and [`SwapInfo::stop_ramp_ts`] parameters.
140    ///
141    /// [^stableswap]: [Egorov, "StableSwap," 2019.](https://curve.fi/files/stableswap-paper.pdf)
142    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            // Compute amp factor based on ramp time
148            if self.target_amp_factor >= self.initial_amp_factor {
149                // Ramp up
150                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                // Ramp down
160                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            // when stop_ramp_ts == 0 or current_ts >= stop_ramp_ts
171            Some(self.target_amp_factor)
172        }
173    }
174
175    /// Computes the Stable Swap invariant (D).
176    ///
177    /// The invariant is defined as follows:
178    ///
179    /// ```text
180    /// A * sum(x_i) * n**n + D = A * D * n**n + D**(n+1) / (n**n * prod(x_i))
181    /// ```
182    ///
183    /// # Arguments
184    ///
185    /// - `amount_a` - The amount of token A owned by the LP pool. (i.e. token A reserves)
186    /// - `amount_b` - The amount of token B owned by the LP pool. (i.e. token B reserves)
187    ///
188    /// *For more info on reserves, see [stable_swap_client::state::SwapTokenInfo::reserves].*
189    pub fn compute_d(&self, amount_a: u64, amount_b: u64) -> Option<U192> {
190        let sum_x = amount_a.checked_add(amount_b)?; // sum(x_i), a.k.a S
191        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            // Newton's method to approximate D
199            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                // Equality with the precision of 1
212                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    /// Computes the amount of pool tokens to mint after a deposit.
226    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        // Initial invariant
236        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        // Invariant after change
243        let d_1 = self.compute_d(new_balances[0], new_balances[1])?;
244        if d_1 <= d_0 {
245            None
246        } else {
247            // Recalculate the invariant accounting for fees
248            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    /// Compute the swap amount `y` in proportion to `x`.
271    ///
272    /// Solve for `y`:
273    ///
274    /// ```text
275    /// y**2 + y * (sum' - (A*n**n - 1) * D / (A * n**n)) = D ** (n + 1) / (n ** (2 * n) * prod' * A)
276    /// y**2 + b*y = c
277    /// ```
278    #[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())?; // A * n ** n
282
283        // sum' = prod' = x
284        // c =  D ** (n + 1) / (n ** (2 * n) * prod' * A)
285        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        // b = sum' - (A*n**n - 1) * D / (A * n**n)
292        let b = d.checked_div(ann.into())?.checked_add(x.into())?; // d is subtracted on line 147
293
294        // Solve for y by approximating: y**2 + b*y = c
295        let mut y_prev: U192;
296        let mut y = d;
297        for _ in 0..256 {
298            y_prev = y;
299            // y = (y * y + c) / (2 * y + b - d);
300            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    /// Computes the swap amount `y` in proportion to `x`.
315    pub fn compute_y(&self, x: u64, d: U192) -> Option<u64> {
316        self.compute_y_raw(x, d)?.to_u64()
317    }
318
319    /// Calculates the withdrawal amount when withdrawing only one type of token.
320    ///
321    /// Calculation:
322    ///
323    /// 1. Get current D
324    /// 2. Solve Eqn against `y_i` for `D - _token_amount`
325    pub fn compute_withdraw_one(
326        &self,
327        pool_token_amount: u64,
328        pool_token_supply: u64,
329        swap_base_amount: u64,  // Same denomination of token to be withdrawn
330        swap_quote_amount: u64, // Counter denomination of token to be withdrawn
331        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        // expected_base_amount = swap_base_amount * d_1 / d_0 - new_y;
342        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        // expected_quote_amount = swap_quote_amount - swap_quote_amount * d_1 / d_0;
348        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        // new_base_amount = swap_base_amount - expected_base_amount * fee / fee_denominator;
355        let new_base_amount = swap_base_amount
356            .checked_sub(fees.normalized_trade_fee(N_COINS, expected_base_amount)?)?;
357        // new_quote_amount = swap_quote_amount - expected_quote_amount * fee / fee_denominator;
358        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)?; // Withdraw less to account for rounding errors
363        let dy_0 = swap_base_amount.checked_sub(new_y)?;
364
365        Some((dy, dy_0.checked_sub(dy)?))
366    }
367
368    /// Compute SwapResult after an exchange
369    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        // https://github.com/curvefi/curve-contract/blob/b0bbf77f8f93c9c5f4e415bce9cd71f0cdee960e/contracts/pool-templates/base/SwapTemplateBase.vy#L466
381        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,    // Start at 1 to prevent divide by 0 when computing d
546            amount_b in 1..MAX_TOKENS_IN,    // Start at 1 to prevent divide by 0 when computing d
547        ) {
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        // Specific cases
559        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);  // Pool token supply not changed on swaps
1139        }
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            // Make sure we will get at least one trading token out for each
1169            // side, otherwise the calculation fails
1170            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}