stable_swap_math/
price.rs

1//! Utilities for getting the virtual price of a pool.
2
3use crate::{bn::U192, curve::StableSwap};
4
5/// Utilities for calculating the virtual price of a Saber LP token.
6///
7/// This is especially useful for if you want to use a Saber LP token as collateral.
8///
9/// # Calculating liquidation value
10///
11/// To use a Saber LP token as collateral, you will need to fetch the prices
12/// of both of the tokens in the pool and get the min of the two. Then,
13/// use the [SaberSwap::calculate_virtual_price_of_pool_tokens] function to
14/// get the virtual price.
15///
16/// This virtual price is resilient to manipulations of the LP token price.
17///
18/// Hence, `min_lp_price = min_value * virtual_price`.
19///
20/// # Additional Reading
21/// - [Chainlink: Using Chainlink Oracles to Securely Utilize Curve LP Pools](https://blog.chain.link/using-chainlink-oracles-to-securely-utilize-curve-lp-pools/)
22#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
23pub struct SaberSwap {
24    /// Initial amp factor, or `A`.
25    ///
26    /// See [`StableSwap::compute_amp_factor`].
27    pub initial_amp_factor: u64,
28    /// Target amp factor, or `A`.
29    ///
30    /// See [`StableSwap::compute_amp_factor`].
31    pub target_amp_factor: u64,
32    /// Current timestmap.
33    pub current_ts: i64,
34    /// Start ramp timestamp for calculating the amp factor, or `A`.
35    ///
36    /// See [`StableSwap::compute_amp_factor`].
37    pub start_ramp_ts: i64,
38    /// Stop ramp timestamp for calculating the amp factor, or `A`.
39    ///
40    /// See [`StableSwap::compute_amp_factor`].
41    pub stop_ramp_ts: i64,
42
43    /// Total supply of LP tokens.
44    ///
45    /// This is `pool_mint.supply`, where `pool_mint` is an SPL Token Mint.
46    pub lp_mint_supply: u64,
47    /// Amount of token A.
48    ///
49    /// This is `token_a.reserve.amount`, where `token_a.reserve` is an SPL Token Token Account.
50    pub token_a_reserve: u64,
51    /// Amount of token B.
52    ///
53    /// This is `token_b.reserve.amount`, where `token_b.reserve` is an SPL Token Token Account.
54    pub token_b_reserve: u64,
55}
56
57impl From<&SaberSwap> for crate::curve::StableSwap {
58    fn from(swap: &SaberSwap) -> Self {
59        crate::curve::StableSwap::new(
60            swap.initial_amp_factor,
61            swap.target_amp_factor,
62            swap.current_ts,
63            swap.start_ramp_ts,
64            swap.stop_ramp_ts,
65        )
66    }
67}
68
69impl SaberSwap {
70    /// Calculates the amount of pool tokens represented by the given amount of virtual tokens.
71    ///
72    /// A virtual token is the denomination of virtual price. For example, if there is a virtual price of 1.04
73    /// on USDC-USDT LP, then 1 virtual token maps to 1/1.04 USDC-USDT LP tokens.
74    ///
75    /// This is useful for building assets that are backed by LP tokens.
76    /// An example of this is [Cashio](https://github.com/CashioApp/cashio), which
77    /// allows users to mint $CASH tokens based on the virtual price of underlying LP tokens.
78    ///
79    /// # Arguments
80    ///
81    /// - `virtual_amount` - The number of "virtual" underlying tokens.
82    pub fn calculate_pool_tokens_from_virtual_amount(&self, virtual_amount: u64) -> Option<u64> {
83        U192::from(virtual_amount)
84            .checked_mul(self.lp_mint_supply.into())?
85            .checked_div(self.compute_d()?)?
86            .to_u64()
87    }
88
89    /// Calculates the virtual price of the given amount of pool tokens.
90    ///
91    /// The virtual price is defined as the current price of the pool LP token
92    /// relative to the underlying pool assets.
93    ///
94    /// The virtual price in the StableSwap algorithm is obtained through taking the invariance
95    /// of the pool, which by default takes every token as valued at 1.00 of the underlying.
96    /// You can get the virtual price of each pool by calling this function
97    /// for it.[^chainlink]
98    ///
99    /// [^chainlink]: Source: <https://blog.chain.link/using-chainlink-oracles-to-securely-utilize-curve-lp-pools/>
100    pub fn calculate_virtual_price_of_pool_tokens(&self, pool_token_amount: u64) -> Option<u64> {
101        self.compute_d()?
102            .checked_mul(pool_token_amount.into())?
103            .checked_div(self.lp_mint_supply.into())?
104            .to_u64()
105    }
106
107    /// Computes D, which is the virtual price times the total supply of the pool.
108    pub fn compute_d(&self) -> Option<U192> {
109        let calculator = StableSwap::from(self);
110        calculator.compute_d(self.token_a_reserve, self.token_b_reserve)
111    }
112}
113
114#[cfg(test)]
115#[allow(clippy::unwrap_used)]
116mod tests {
117    use proptest::prelude::*;
118
119    use super::SaberSwap;
120
121    prop_compose! {
122        fn arb_swap_unsafe()(
123            token_a_reserve in 1_u64..=u64::MAX,
124            token_b_reserve in 1_u64..=u64::MAX,
125            lp_mint_supply in 1_u64..=u64::MAX
126        ) -> SaberSwap {
127            SaberSwap {
128                initial_amp_factor: 1,
129                target_amp_factor: 1,
130                current_ts: 1,
131                start_ramp_ts: 1,
132                stop_ramp_ts: 1,
133
134                lp_mint_supply,
135                token_a_reserve,
136                token_b_reserve
137            }
138        }
139    }
140
141    prop_compose! {
142        #[allow(clippy::integer_arithmetic)]
143        fn arb_token_amount(decimals: u8)(
144            amount in 1_u64..=(u64::MAX / 10u64.pow(decimals.into())),
145        ) -> u64 {
146            amount
147        }
148    }
149
150    prop_compose! {
151        fn arb_swap_reserves()(
152            decimals in 0_u8..=19_u8,
153            swap in arb_swap_unsafe()
154        ) (
155            token_a_reserve in arb_token_amount(decimals),
156            token_b_reserve in arb_token_amount(decimals),
157            swap in Just(swap)
158        ) -> SaberSwap {
159            SaberSwap {
160                token_a_reserve,
161                token_b_reserve,
162                ..swap
163            }
164        }
165    }
166
167    prop_compose! {
168        fn arb_swap()(
169            swap in arb_swap_reserves()
170        ) (
171            // targeting a maximum virtual price of 4
172            // anything higher than this is a bit ridiculous
173            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)),
174            swap in Just(swap)
175        ) -> SaberSwap {
176            SaberSwap {
177                lp_mint_supply,
178                ..swap
179            }
180        }
181    }
182
183    proptest! {
184      #[test]
185      fn test_invertible(
186          swap in arb_swap(),
187          amount in 0_u64..=u64::MAX
188      ) {
189        let maybe_virt = swap.calculate_virtual_price_of_pool_tokens(amount);
190        if maybe_virt.is_none() {
191            // ignore virt calculation failures, since they won't be used in production
192            return Ok(());
193        }
194        let virt = maybe_virt.unwrap();
195        if virt == 0 {
196            // this case doesn't matter because it's a noop.
197            return Ok(());
198        }
199
200        let result_lp = swap.calculate_pool_tokens_from_virtual_amount(virt).unwrap();
201
202        // tokens should never be created.
203        prop_assert!(result_lp <= amount);
204
205        // these numbers should be very close to each other.
206        prop_assert!(1.0_f64 - (result_lp as f64) / (amount as f64) < 0.001_f64);
207      }
208    }
209}