levana_perpswap_cosmos/contracts/market/
liquidity.rs

1//! Data types for tracking liquidity
2use crate::prelude::*;
3use anyhow::{Context, Result};
4use cosmwasm_schema::cw_serde;
5use cosmwasm_std::OverflowError;
6
7/// Protocol wide stats on liquidity
8#[cw_serde]
9#[derive(Default)]
10pub struct LiquidityStats {
11    /// Collateral locked as counter collateral in the protocol
12    pub locked: Collateral,
13    /// Total amount of collateral available to be used as liquidity
14    pub unlocked: Collateral,
15    /// Total number of LP tokens
16    pub total_lp: LpToken,
17    /// Total number of xLP tokens
18    pub total_xlp: LpToken,
19}
20
21impl LiquidityStats {
22    /// Total amount of locked and unlocked collateral.
23    pub fn total_collateral(&self) -> Result<Collateral, OverflowError> {
24        self.locked + self.unlocked
25    }
26
27    /// Total number of LP and xLP tokens
28    pub fn total_tokens(&self) -> Result<LpToken, OverflowError> {
29        self.total_lp + self.total_xlp
30    }
31
32    /// Calculate the amount of collateral for a given number of LP tokens
33    ///
34    /// This method can fail due to arithmetic overflow. It can also fail if
35    /// invariants are violated, specifically if there is 0 collateral in the
36    /// pool when this is called with a non-zero amount of LP.
37    ///
38    /// Note that even with a non-zero input value for `lp`, due to rounding
39    /// errors this function may return 0 collateral.
40    pub fn lp_to_collateral(&self, lp: LpToken) -> Result<Collateral> {
41        if lp.is_zero() {
42            return Ok(Collateral::zero());
43        }
44        let total_collateral = self.total_collateral()?;
45
46        anyhow::ensure!(
47            !total_collateral.approx_eq(Collateral::zero()),
48            "LiquidityStats::lp_to_collateral: no liquidity is in the pool"
49        );
50        let total_tokens = self.total_tokens()?;
51        debug_assert_ne!(total_tokens, LpToken::zero());
52
53        Ok(Collateral::from_decimal256(
54            total_collateral
55                .into_decimal256()
56                .checked_mul(lp.into_decimal256())?
57                .checked_div(total_tokens.into_decimal256())?,
58        ))
59    }
60
61    /// Same as [Self::lp_to_collateral], but treats round-to-zero as an error.
62    pub fn lp_to_collateral_non_zero(&self, lp: NonZero<LpToken>) -> Result<NonZero<Collateral>> {
63        self.lp_to_collateral(lp.raw()).and_then(|c| {
64            NonZero::new(c)
65                .context("lp_to_collateral_non_zero: amount of backing collateral rounded to 0")
66        })
67    }
68
69    /// Calculate how many LP tokens would be produced from the given collateral.
70    ///
71    /// If there is currently no liquidity in the pool, this will use a 1:1 ratio.
72    pub fn collateral_to_lp(&self, amount: NonZero<Collateral>) -> Result<NonZero<LpToken>> {
73        let total_collateral = self.total_collateral()?;
74
75        NonZero::new(LpToken::from_decimal256(if total_collateral.is_zero() {
76            debug_assert!(self.total_lp.is_zero());
77            debug_assert!(self.total_xlp.is_zero());
78            amount.into_decimal256()
79        } else {
80            self.total_tokens()?
81                .into_decimal256()
82                .checked_mul(amount.into_decimal256())?
83                .checked_div(total_collateral.into_decimal256())?
84        }))
85        .context("liquidity_deposit_inner: new shares is (impossibly) 0")
86    }
87
88    /// approximate equality comparison, helpful for tests
89    pub fn approx_eq(&self, other: &Self) -> bool {
90        self.locked.approx_eq(other.locked)
91            && self.unlocked.approx_eq(other.unlocked)
92            && self.total_lp.approx_eq(other.total_lp)
93            && self.total_xlp.approx_eq(other.total_xlp)
94    }
95}
96
97/// Liquidity events
98pub mod events {
99    use super::LiquidityStats;
100    use crate::prelude::*;
101    use cosmwasm_std::Event;
102
103    /// Liquidity was withdrawn from the system
104    pub struct WithdrawEvent {
105        /// Number of LP tokens burned
106        pub burned_shares: NonZero<LpToken>,
107        /// Collateral returned to the provider
108        pub withdrawn_funds: NonZero<Collateral>,
109        /// USD value of the collateral
110        pub withdrawn_funds_usd: NonZero<Usd>,
111    }
112
113    impl From<WithdrawEvent> for cosmwasm_std::Event {
114        fn from(src: WithdrawEvent) -> Self {
115            cosmwasm_std::Event::new("liquidity-withdraw").add_attributes(vec![
116                ("burned-shares", src.burned_shares.to_string()),
117                ("withdrawn-funds", src.withdrawn_funds.to_string()),
118                ("withdrawn-funds-usd", src.withdrawn_funds_usd.to_string()),
119            ])
120        }
121    }
122
123    /// Liquidity deposited into the protocol
124    pub struct DepositEvent {
125        /// Amount of collateral deposited
126        pub amount: NonZero<Collateral>,
127        /// Value of deposit in USD
128        pub amount_usd: NonZero<Usd>,
129        /// Number of tokens minted from this deposit
130        pub shares: NonZero<LpToken>,
131    }
132
133    impl From<DepositEvent> for cosmwasm_std::Event {
134        fn from(src: DepositEvent) -> Self {
135            cosmwasm_std::Event::new("liquidity-deposit").add_attributes(vec![
136                ("amount", src.amount.to_string()),
137                ("amount-usd", src.amount_usd.to_string()),
138                ("shares", src.shares.to_string()),
139            ])
140        }
141    }
142
143    /// Liquidity was locked into a position as counter collateral.
144    pub struct LockEvent {
145        /// Amount of liquidity that was locked
146        pub amount: NonZero<Collateral>,
147    }
148
149    impl From<LockEvent> for cosmwasm_std::Event {
150        fn from(src: LockEvent) -> Self {
151            cosmwasm_std::Event::new("liquidity-lock")
152                .add_attribute("amount", src.amount.to_string())
153        }
154    }
155
156    /// Liquidity was unlocked from a position back into the unlocked pool.
157    pub struct UnlockEvent {
158        /// Amount of liquidity that was unlocked.
159        pub amount: NonZero<Collateral>,
160    }
161
162    impl From<UnlockEvent> for cosmwasm_std::Event {
163        fn from(src: UnlockEvent) -> Self {
164            cosmwasm_std::Event::new("liquidity-unlock")
165                .add_attribute("amount", src.amount.to_string())
166        }
167    }
168
169    /// Amount of locked liquidity changed from price exposure in liquifunding.
170    pub struct LockUpdateEvent {
171        /// Increase or decrease in locked liquidity in the pool.
172        pub amount: Signed<Collateral>,
173    }
174
175    impl From<LockUpdateEvent> for cosmwasm_std::Event {
176        fn from(src: LockUpdateEvent) -> Self {
177            cosmwasm_std::Event::new("liquidity-update")
178                .add_attributes(vec![("amount", src.amount.to_string())])
179        }
180    }
181
182    /// Provides current size of the liquidity pool
183    pub struct LiquidityPoolSizeEvent {
184        /// Total locked collateral
185        pub locked: Collateral,
186        /// Locked collateral in USD
187        pub locked_usd: Usd,
188        /// Total unlocked collateral
189        pub unlocked: Collateral,
190        /// Unlocked collateral in USD
191        pub unlocked_usd: Usd,
192        /// Total collateral (locked and unlocked) backing LP tokens
193        pub lp_collateral: Collateral,
194        /// Total collateral (locked and unlocked) backing xLP tokens
195        pub xlp_collateral: Collateral,
196        /// Total number of LP tokens
197        pub total_lp: LpToken,
198        /// Total number of xLP tokens
199        pub total_xlp: LpToken,
200    }
201
202    impl LiquidityPoolSizeEvent {
203        /// Generate a value from protocol stats and the current price.
204        pub fn from_stats(stats: &LiquidityStats, price: &PricePoint) -> Result<Self> {
205            let total_collateral = stats.total_collateral()?;
206            let total_tokens = stats.total_tokens()?;
207            let (lp_collateral, xlp_collateral) = if total_tokens.is_zero() {
208                debug_assert_eq!(total_collateral, Collateral::zero());
209                (Collateral::zero(), Collateral::zero())
210            } else {
211                let lp_collateral = total_collateral.into_decimal256()
212                    * stats.total_lp.into_decimal256()
213                    / total_tokens.into_decimal256();
214                let lp_collateral = Collateral::from_decimal256(lp_collateral);
215                let xlp_collateral = (total_collateral - lp_collateral)?;
216
217                (lp_collateral, xlp_collateral)
218            };
219
220            Ok(Self {
221                locked: stats.locked,
222                locked_usd: price.collateral_to_usd(stats.locked),
223                unlocked: stats.unlocked,
224                unlocked_usd: price.collateral_to_usd(stats.unlocked),
225                lp_collateral,
226                xlp_collateral,
227                total_lp: stats.total_lp,
228                total_xlp: stats.total_xlp,
229            })
230        }
231    }
232
233    impl From<LiquidityPoolSizeEvent> for cosmwasm_std::Event {
234        fn from(src: LiquidityPoolSizeEvent) -> Self {
235            cosmwasm_std::Event::new("liquidity-pool-size").add_attributes(vec![
236                ("locked", src.locked.to_string()),
237                ("locked-usd", src.locked_usd.to_string()),
238                ("unlocked", src.unlocked.to_string()),
239                ("unlocked-usd", src.unlocked_usd.to_string()),
240                ("lp-collateral", src.lp_collateral.to_string()),
241                ("xlp-collateral", src.xlp_collateral.to_string()),
242                ("total-lp", src.total_lp.to_string()),
243                ("total-xlp", src.total_xlp.to_string()),
244            ])
245        }
246    }
247
248    impl TryFrom<Event> for LiquidityPoolSizeEvent {
249        type Error = anyhow::Error;
250
251        fn try_from(evt: Event) -> anyhow::Result<Self> {
252            Ok(LiquidityPoolSizeEvent {
253                locked: evt.decimal_attr("locked")?,
254                locked_usd: evt.decimal_attr("locked-usd")?,
255                unlocked: evt.decimal_attr("unlocked")?,
256                unlocked_usd: evt.decimal_attr("unlocked-usd")?,
257                lp_collateral: evt.decimal_attr("lp-collateral")?,
258                xlp_collateral: evt.decimal_attr("xlp-collateral")?,
259                total_lp: evt.decimal_attr("total-lp")?,
260                total_xlp: evt.decimal_attr("total-xlp")?,
261            })
262        }
263    }
264
265    /// Tracks when the delta neutrality ratio has been updated.
266    #[derive(Debug)]
267    pub struct DeltaNeutralityRatioEvent {
268        /// Total locked and unlocked liquidity in the pool
269        pub total_liquidity: Collateral,
270        /// Total long interest (using direction to base), in notional
271        pub long_interest: Notional,
272        /// Total short interest (using direction to base), in notional
273        pub short_interest: Notional,
274        /// Net notional: long - short
275        pub net_notional: Signed<Notional>,
276        /// Current notional price
277        pub price_notional: Price,
278        /// Current delta neutrality ratio: net-notional in collateral / total liquidity.
279        pub delta_neutrality_ratio: Signed<Decimal256>,
280    }
281
282    impl From<DeltaNeutralityRatioEvent> for cosmwasm_std::Event {
283        fn from(
284            DeltaNeutralityRatioEvent {
285                total_liquidity,
286                long_interest,
287                short_interest,
288                net_notional,
289                price_notional,
290                delta_neutrality_ratio,
291            }: DeltaNeutralityRatioEvent,
292        ) -> Self {
293            cosmwasm_std::Event::new("delta-neutrality-ratio").add_attributes(vec![
294                ("total-liquidity", total_liquidity.to_string()),
295                ("long-interest", long_interest.to_string()),
296                ("short-interest", short_interest.to_string()),
297                ("net-notional", net_notional.to_string()),
298                ("price-notional", price_notional.to_string()),
299                ("delta-neutrality-ratio", delta_neutrality_ratio.to_string()),
300            ])
301        }
302    }
303
304    impl TryFrom<Event> for DeltaNeutralityRatioEvent {
305        type Error = anyhow::Error;
306
307        fn try_from(evt: Event) -> anyhow::Result<Self> {
308            Ok(DeltaNeutralityRatioEvent {
309                total_liquidity: evt.decimal_attr("total-liquidity")?,
310                long_interest: evt.decimal_attr("long-interest")?,
311                short_interest: evt.decimal_attr("short-interest")?,
312                net_notional: evt.number_attr("net-notional")?,
313                price_notional: Price::try_from_number(evt.number_attr("price-notional")?)?,
314                delta_neutrality_ratio: evt.number_attr("delta-neutrality-ratio")?,
315            })
316        }
317    }
318}