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);
}
}
}