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}