1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
//! Utilities for getting the virtual price of a pool.

use crate::{bn::U192, curve::StableSwap};

/// Utilities for calculating the virtual price of a Saber LP token.
///
/// This is especially useful for if you want to use a Saber LP token as collateral.
///
/// # Calculating liquidation value
///
/// To use a Saber LP token as collateral, you will need to fetch the prices
/// of both of the tokens in the pool and get the min of the two. Then,
/// use the [SaberSwap::calculate_virtual_price_of_pool_tokens] function to
/// get the virtual price.
///
/// This virtual price is resilient to manipulations of the LP token price.
///
/// Hence, `min_lp_price = min_value * virtual_price`.
///
/// # Additional Reading
/// - [Chainlink: Using Chainlink Oracles to Securely Utilize Curve LP Pools](https://blog.chain.link/using-chainlink-oracles-to-securely-utilize-curve-lp-pools/)
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct SaberSwap {
    /// Initial amp factor, or `A`.
    ///
    /// See [`StableSwap::compute_amp_factor`].
    pub initial_amp_factor: u64,
    /// Target amp factor, or `A`.
    ///
    /// See [`StableSwap::compute_amp_factor`].
    pub target_amp_factor: u64,
    /// Current timestmap.
    pub current_ts: i64,
    /// Start ramp timestamp for calculating the amp factor, or `A`.
    ///
    /// See [`StableSwap::compute_amp_factor`].
    pub start_ramp_ts: i64,
    /// Stop ramp timestamp for calculating the amp factor, or `A`.
    ///
    /// See [`StableSwap::compute_amp_factor`].
    pub stop_ramp_ts: i64,

    /// Total supply of LP tokens.
    ///
    /// This is `pool_mint.supply`, where `pool_mint` is an SPL Token Mint.
    pub lp_mint_supply: u64,
    /// Amount of token A.
    ///
    /// This is `token_a.reserve.amount`, where `token_a.reserve` is an SPL Token Token Account.
    pub token_a_reserve: u64,
    /// Amount of token B.
    ///
    /// This is `token_b.reserve.amount`, where `token_b.reserve` is an SPL Token Token Account.
    pub token_b_reserve: u64,
}

impl From<&SaberSwap> for crate::curve::StableSwap {
    fn from(swap: &SaberSwap) -> Self {
        crate::curve::StableSwap::new(
            swap.initial_amp_factor,
            swap.target_amp_factor,
            swap.current_ts,
            swap.start_ramp_ts,
            swap.stop_ramp_ts,
        )
    }
}

impl SaberSwap {
    /// Calculates the amount of pool tokens represented by the given amount of virtual tokens.
    ///
    /// A virtual token is the denomination of virtual price. For example, if there is a virtual price of 1.04
    /// on USDC-USDT LP, then 1 virtual token maps to 1/1.04 USDC-USDT LP tokens.
    ///
    /// This is useful for building assets that are backed by LP tokens.
    /// An example of this is [Cashio](https://github.com/CashioApp/cashio), which
    /// allows users to mint $CASH tokens based on the virtual price of underlying LP tokens.
    ///
    /// # Arguments
    ///
    /// - `virtual_amount` - The number of "virtual" underlying tokens.
    pub fn calculate_pool_tokens_from_virtual_amount(&self, virtual_amount: u64) -> Option<u64> {
        U192::from(virtual_amount)
            .checked_mul(self.lp_mint_supply.into())?
            .checked_div(self.compute_d()?)?
            .to_u64()
    }

    /// Calculates the virtual price of the given amount of pool tokens.
    ///
    /// The virtual price is defined as the current price of the pool LP token
    /// relative to the underlying pool assets.
    ///
    /// The virtual price in the StableSwap algorithm is obtained through taking the invariance
    /// of the pool, which by default takes every token as valued at 1.00 of the underlying.
    /// You can get the virtual price of each pool by calling this function
    /// for it.[^chainlink]
    ///
    /// [^chainlink]: Source: <https://blog.chain.link/using-chainlink-oracles-to-securely-utilize-curve-lp-pools/>
    pub fn calculate_virtual_price_of_pool_tokens(&self, pool_token_amount: u64) -> Option<u64> {
        self.compute_d()?
            .checked_mul(pool_token_amount.into())?
            .checked_div(self.lp_mint_supply.into())?
            .to_u64()
    }

    /// Computes D, which is the virtual price times the total supply of the pool.
    pub fn compute_d(&self) -> Option<U192> {
        let calculator = StableSwap::from(self);
        calculator.compute_d(self.token_a_reserve, self.token_b_reserve)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
    use proptest::prelude::*;

    use super::SaberSwap;

    prop_compose! {
        fn arb_swap_unsafe()(
            token_a_reserve in 1_u64..=u64::MAX,
            token_b_reserve in 1_u64..=u64::MAX,
            lp_mint_supply in 1_u64..=u64::MAX
        ) -> SaberSwap {
            SaberSwap {
                initial_amp_factor: 1,
                target_amp_factor: 1,
                current_ts: 1,
                start_ramp_ts: 1,
                stop_ramp_ts: 1,

                lp_mint_supply,
                token_a_reserve,
                token_b_reserve
            }
        }
    }

    prop_compose! {
        #[allow(clippy::integer_arithmetic)]
        fn arb_token_amount(decimals: u8)(
            amount in 1_u64..=(u64::MAX / 10u64.pow(decimals.into())),
        ) -> u64 {
            amount
        }
    }

    prop_compose! {
        fn arb_swap_reserves()(
            decimals in 0_u8..=19_u8,
            swap in arb_swap_unsafe()
        ) (
            token_a_reserve in arb_token_amount(decimals),
            token_b_reserve in arb_token_amount(decimals),
            swap in Just(swap)
        ) -> SaberSwap {
            SaberSwap {
                token_a_reserve,
                token_b_reserve,
                ..swap
            }
        }
    }

    prop_compose! {
        fn arb_swap()(
            swap in arb_swap_reserves()
        ) (
            // targeting a maximum virtual price of 4
            // anything higher than this is a bit ridiculous
            lp_mint_supply in 1_u64.max((swap.token_a_reserve.min(swap.token_b_reserve)) / 4)..=(swap.token_a_reserve.checked_add(swap.token_b_reserve).unwrap_or(u64::MAX)),
            swap in Just(swap)
        ) -> SaberSwap {
            SaberSwap {
                lp_mint_supply,
                ..swap
            }
        }
    }

    proptest! {
      #[test]
      fn test_invertible(
          swap in arb_swap(),
          amount in 0_u64..=u64::MAX
      ) {
        let maybe_virt = swap.calculate_virtual_price_of_pool_tokens(amount);
        if maybe_virt.is_none() {
            // ignore virt calculation failures, since they won't be used in production
            return Ok(());
        }
        let virt = maybe_virt.unwrap();
        if virt == 0 {
            // this case doesn't matter because it's a noop.
            return Ok(());
        }

        let result_lp = swap.calculate_pool_tokens_from_virtual_amount(virt).unwrap();

        // tokens should never be created.
        prop_assert!(result_lp <= amount);

        // these numbers should be very close to each other.
        prop_assert!(1.0_f64 - (result_lp as f64) / (amount as f64) < 0.001_f64);
      }
    }
}