levana_perpswap_cosmos/contracts/market/
fees.rs

1//! Events and helper methods for fees.
2use crate::prelude::*;
3
4use super::config::Config;
5
6impl Config {
7    /// Calculate the trade fee based on the given old and new position parameters.
8    ///
9    /// When opening a new position, you can use [Config::calculate_trade_fee_open].
10    pub fn calculate_trade_fee(
11        &self,
12        old_notional_size_in_collateral: Signed<Collateral>,
13        new_notional_size_in_collateral: Signed<Collateral>,
14        old_counter_collateral: Collateral,
15        new_counter_collateral: Collateral,
16    ) -> Result<Collateral> {
17        debug_assert!(
18            old_notional_size_in_collateral.is_zero()
19                || (old_notional_size_in_collateral.is_negative()
20                    == new_notional_size_in_collateral.is_negative())
21        );
22        let old_notional_size_in_collateral = old_notional_size_in_collateral.abs_unsigned();
23        let new_notional_size_in_collateral = new_notional_size_in_collateral.abs_unsigned();
24        let notional_size_fee = match new_notional_size_in_collateral
25            .checked_sub(old_notional_size_in_collateral)
26            .ok()
27        {
28            Some(delta) => {
29                debug_assert!(old_notional_size_in_collateral <= new_notional_size_in_collateral);
30                delta.checked_mul_dec(self.trading_fee_notional_size)?
31            }
32            None => {
33                debug_assert!(old_notional_size_in_collateral > new_notional_size_in_collateral);
34                Collateral::zero()
35            }
36        };
37        let counter_collateral_fee = match new_counter_collateral
38            .checked_sub(old_counter_collateral)
39            .ok()
40        {
41            Some(delta) => {
42                debug_assert!(old_counter_collateral <= new_counter_collateral);
43                delta.checked_mul_dec(self.trading_fee_counter_collateral)?
44            }
45            None => {
46                debug_assert!(old_counter_collateral > new_counter_collateral);
47                Collateral::zero()
48            }
49        };
50        notional_size_fee
51            .checked_add(counter_collateral_fee)
52            .context("Overflow when calculating trading fee")
53    }
54
55    /// Same as [Config::calculate_trade_fee] but for opening a new position.
56    pub fn calculate_trade_fee_open(
57        &self,
58        notional_size_in_collateral: Signed<Collateral>,
59        counter_collateral: Collateral,
60    ) -> Result<Collateral> {
61        self.calculate_trade_fee(
62            Signed::zero(),
63            notional_size_in_collateral,
64            Collateral::zero(),
65            counter_collateral,
66        )
67    }
68}
69
70/// Events for fees.
71pub mod events {
72    use super::*;
73    use crate::contracts::market::order::OrderId;
74    use crate::contracts::market::position::PositionId;
75    use crate::{constants::event_key, contracts::market::deferred_execution::DeferredExecId};
76    use cosmwasm_std::{Decimal256, Event};
77
78    /// Represents either a [PositionId] or an [OrderId]
79    #[derive(Debug, Clone)]
80    pub enum TradeId {
81        /// An open position
82        Position(PositionId),
83        /// A pending limit order
84        LimitOrder(OrderId),
85        /// A deferred execution item not connected to a position or order
86        Deferred(DeferredExecId),
87    }
88
89    /// The type of fee that was paid out
90    #[derive(Debug, Clone, Copy)]
91    pub enum FeeSource {
92        /// Trading fees
93        Trading,
94        /// Borrow fees
95        Borrow,
96        /// Delta neutrality fee
97        DeltaNeutrality,
98    }
99
100    impl FeeSource {
101        fn as_str(self) -> &'static str {
102            match self {
103                FeeSource::Trading => "trading",
104                FeeSource::Borrow => "borrow",
105                FeeSource::DeltaNeutrality => "delta-neutrality",
106            }
107        }
108    }
109
110    impl FromStr for FeeSource {
111        type Err = anyhow::Error;
112
113        fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
114            match s {
115                "trading" => Ok(FeeSource::Trading),
116                "borrow" => Ok(FeeSource::Borrow),
117                "delta-neutrality" => Ok(FeeSource::DeltaNeutrality),
118                _ => Err(anyhow::anyhow!("Unknown FeeSource {s}")),
119            }
120        }
121    }
122
123    /// Event fired whenever a fee is collected
124    #[derive(Debug, Clone)]
125    pub struct FeeEvent {
126        /// Position that triggered the fee
127        pub trade_id: TradeId,
128        /// Source of the fee
129        pub fee_source: FeeSource,
130        /// Amount paid to LP holders, in collateral
131        pub lp_amount: Collateral,
132        /// Amount paid to LP holders, in USD
133        pub lp_amount_usd: Usd,
134        /// Amount paid to xLP holders, in collateral
135        pub xlp_amount: Collateral,
136        /// Amount paid to xLP holders, in USD
137        pub xlp_amount_usd: Usd,
138        /// Amount paid to the protocol/DAO, in collateral
139        pub protocol_amount: Collateral,
140        /// Amount paid to the protocol/DAO, in USD
141        pub protocol_amount_usd: Usd,
142    }
143
144    impl From<FeeEvent> for Event {
145        fn from(
146            FeeEvent {
147                trade_id,
148                fee_source,
149                lp_amount,
150                lp_amount_usd,
151                xlp_amount,
152                xlp_amount_usd,
153                protocol_amount,
154                protocol_amount_usd,
155            }: FeeEvent,
156        ) -> Self {
157            let (trade_id_key, trade_id_val) = match trade_id {
158                TradeId::Position(pos_id) => ("pos-id", pos_id.to_string()),
159                TradeId::LimitOrder(order_id) => ("order-id", order_id.to_string()),
160                TradeId::Deferred(deferred_id) => ("deferred-id", deferred_id.to_string()),
161            };
162
163            Event::new("fee")
164                .add_attribute(trade_id_key, trade_id_val)
165                .add_attribute("source", fee_source.as_str())
166                .add_attribute("lp-amount", lp_amount.to_string())
167                .add_attribute("lp-amount-usd", lp_amount_usd.to_string())
168                .add_attribute("xlp-amount", xlp_amount.to_string())
169                .add_attribute("xlp-amount-usd", xlp_amount_usd.to_string())
170                .add_attribute("protocol-amount", protocol_amount.to_string())
171                .add_attribute("protocol-amount-usd", protocol_amount_usd.to_string())
172        }
173    }
174    impl TryFrom<Event> for FeeEvent {
175        type Error = anyhow::Error;
176
177        fn try_from(evt: Event) -> anyhow::Result<Self> {
178            let trade_id = match evt.try_u64_attr("pos-id")? {
179                Some(pos_id) => TradeId::Position(PositionId::new(pos_id)),
180                None => match evt.try_u64_attr("order-id")? {
181                    Some(order_id) => TradeId::LimitOrder(OrderId::new(order_id)),
182                    None => {
183                        let deferred_id = evt.u64_attr("deferred-id")?;
184                        TradeId::Deferred(DeferredExecId::from_u64(deferred_id))
185                    }
186                },
187            };
188
189            Ok(FeeEvent {
190                trade_id,
191                fee_source: evt.string_attr("source")?.parse()?,
192                lp_amount: evt.decimal_attr("lp-amount")?,
193                lp_amount_usd: evt.decimal_attr("lp-amount-usd")?,
194                xlp_amount: evt.decimal_attr("xlp-amount")?,
195                xlp_amount_usd: evt.decimal_attr("xlp-amount-usd")?,
196                protocol_amount: evt.decimal_attr("protocol-amount")?,
197                protocol_amount_usd: evt.decimal_attr("protocol-amount-usd")?,
198            })
199        }
200    }
201
202    /// Event when a funding payment is made
203    pub struct FundingPaymentEvent {
204        /// Position that paid (or received) the payment
205        pub pos_id: PositionId,
206        /// Size of the payment, negative means paid to the posiiton
207        pub amount: Signed<Collateral>,
208        /// Amount expressed in USD
209        pub amount_usd: Signed<Usd>,
210        /// Whether the position is long or short
211        pub direction: DirectionToBase,
212    }
213
214    impl From<&FundingPaymentEvent> for Event {
215        fn from(src: &FundingPaymentEvent) -> Self {
216            Event::new("funding-payment")
217                .add_attribute("pos-id", src.pos_id.to_string())
218                .add_attribute("amount", src.amount.to_string())
219                .add_attribute("amount-usd", src.amount_usd.to_string())
220                .add_attribute(event_key::DIRECTION, src.direction.as_str())
221        }
222    }
223    impl From<FundingPaymentEvent> for Event {
224        fn from(src: FundingPaymentEvent) -> Self {
225            (&src).into()
226        }
227    }
228
229    impl TryFrom<Event> for FundingPaymentEvent {
230        type Error = anyhow::Error;
231
232        fn try_from(evt: Event) -> anyhow::Result<Self> {
233            Ok(FundingPaymentEvent {
234                pos_id: PositionId::new(evt.u64_attr("pos-id")?),
235                amount: evt.number_attr("amount")?,
236                amount_usd: evt.number_attr("amount-usd")?,
237                direction: evt.direction_attr(event_key::DIRECTION)?,
238            })
239        }
240    }
241
242    /// The funding rate was changed
243    pub struct FundingRateChangeEvent {
244        /// When the change happened
245        pub time: Timestamp,
246        /// Long is in terms of base, not notional
247        pub long_rate_base: Number,
248        /// Short is in terms of base, not notional
249        pub short_rate_base: Number,
250    }
251
252    impl From<FundingRateChangeEvent> for Event {
253        fn from(
254            FundingRateChangeEvent {
255                time,
256                long_rate_base,
257                short_rate_base,
258            }: FundingRateChangeEvent,
259        ) -> Self {
260            Event::new("funding-rate-change")
261                .add_attribute("time", time.to_string())
262                .add_attribute("long-rate", long_rate_base.to_string())
263                .add_attribute("short-rate", short_rate_base.to_string())
264        }
265    }
266
267    impl TryFrom<Event> for FundingRateChangeEvent {
268        type Error = anyhow::Error;
269
270        fn try_from(evt: Event) -> anyhow::Result<Self> {
271            Ok(FundingRateChangeEvent {
272                time: evt.timestamp_attr("time")?,
273                long_rate_base: evt.number_attr("long-rate-base")?,
274                short_rate_base: evt.number_attr("short-rate-base")?,
275            })
276        }
277    }
278
279    /// The borrow fee was changed
280    pub struct BorrowFeeChangeEvent {
281        /// When it was changed
282        pub time: Timestamp,
283        /// Sum of LP and xLP rate
284        pub total_rate: Decimal256,
285        /// Amount paid to LP holders
286        pub lp_rate: Decimal256,
287        /// Amount paid to xLP holders
288        pub xlp_rate: Decimal256,
289    }
290
291    impl From<BorrowFeeChangeEvent> for Event {
292        fn from(
293            BorrowFeeChangeEvent {
294                time,
295                total_rate,
296                lp_rate,
297                xlp_rate,
298            }: BorrowFeeChangeEvent,
299        ) -> Self {
300            Event::new("borrow-fee-change")
301                .add_attribute("time", time.to_string())
302                .add_attribute("total", total_rate.to_string())
303                .add_attribute("lp", lp_rate.to_string())
304                .add_attribute("xlp", xlp_rate.to_string())
305        }
306    }
307
308    impl TryFrom<Event> for BorrowFeeChangeEvent {
309        type Error = anyhow::Error;
310
311        fn try_from(evt: Event) -> Result<Self, Self::Error> {
312            Ok(BorrowFeeChangeEvent {
313                time: evt.timestamp_attr("time")?,
314                total_rate: evt.decimal_attr("total")?,
315                lp_rate: evt.decimal_attr("lp")?,
316                xlp_rate: evt.decimal_attr("xlp")?,
317            })
318        }
319    }
320
321    /// A crank fee was collected
322    pub struct CrankFeeEvent {
323        /// Position that paid the fee
324        pub trade_id: TradeId,
325        /// Amount paid, in collateral
326        pub amount: Collateral,
327        /// Amount paid, in USD
328        pub amount_usd: Usd,
329        /// Old crank fee fund balance
330        pub old_balance: Collateral,
331        /// New crank fee fund balance
332        pub new_balance: Collateral,
333    }
334
335    impl From<CrankFeeEvent> for Event {
336        fn from(
337            CrankFeeEvent {
338                trade_id,
339                amount,
340                amount_usd,
341                old_balance,
342                new_balance,
343            }: CrankFeeEvent,
344        ) -> Self {
345            let (trade_id_key, trade_id_val) = match trade_id {
346                TradeId::Position(pos_id) => ("pos-id", pos_id.to_string()),
347                TradeId::LimitOrder(order_id) => ("order-id", order_id.to_string()),
348                TradeId::Deferred(deferred_id) => ("deferred-id", deferred_id.to_string()),
349            };
350
351            Event::new("crank-fee")
352                .add_attribute(trade_id_key, trade_id_val)
353                .add_attribute("amount", amount.to_string())
354                .add_attribute("amount-usd", amount_usd.to_string())
355                .add_attribute("old-balance", old_balance.to_string())
356                .add_attribute("new-balance", new_balance.to_string())
357        }
358    }
359    impl TryFrom<Event> for CrankFeeEvent {
360        type Error = anyhow::Error;
361
362        fn try_from(evt: Event) -> anyhow::Result<Self> {
363            let trade_id = match evt.try_u64_attr("pos-id")? {
364                Some(pos_id) => TradeId::Position(PositionId::new(pos_id)),
365                None => match evt.try_u64_attr("order-id")? {
366                    Some(order_id) => TradeId::LimitOrder(OrderId::new(order_id)),
367                    None => {
368                        let deferred_id = evt.u64_attr("deferred-id")?;
369                        TradeId::Deferred(DeferredExecId::from_u64(deferred_id))
370                    }
371                },
372            };
373
374            Ok(CrankFeeEvent {
375                trade_id,
376                amount: evt.decimal_attr("amount")?,
377                amount_usd: evt.decimal_attr("amount-usd")?,
378                old_balance: evt.decimal_attr("old-balance")?,
379                new_balance: evt.decimal_attr("new-balance")?,
380            })
381        }
382    }
383
384    /// Crank reward was earned by a cranker
385    pub struct CrankFeeEarnedEvent {
386        /// Which wallet received the fee
387        pub recipient: Addr,
388        /// Amount allocated to the wallet, in collateral
389        pub amount: NonZero<Collateral>,
390        /// Amount allocated to the wallet, in USD
391        pub amount_usd: NonZero<Usd>,
392    }
393
394    impl From<CrankFeeEarnedEvent> for Event {
395        fn from(
396            CrankFeeEarnedEvent {
397                recipient,
398                amount,
399                amount_usd,
400            }: CrankFeeEarnedEvent,
401        ) -> Self {
402            Event::new("crank-fee-claimed")
403                .add_attribute("recipient", recipient.to_string())
404                .add_attribute("amount", amount.to_string())
405                .add_attribute("amount-usd", amount_usd.to_string())
406        }
407    }
408    impl TryFrom<Event> for CrankFeeEarnedEvent {
409        type Error = anyhow::Error;
410
411        fn try_from(evt: Event) -> anyhow::Result<Self> {
412            Ok(CrankFeeEarnedEvent {
413                recipient: evt.unchecked_addr_attr("recipient")?,
414                amount: evt.non_zero_attr("amount")?,
415                amount_usd: evt.non_zero_attr("amount-usd")?,
416            })
417        }
418    }
419
420    /// Emitted when there is insufficient liquidation margin for a fee
421    pub struct InsufficientMarginEvent {
422        /// Position that had insufficient margin
423        pub pos: PositionId,
424        /// Type of fee that couldn't be covered
425        pub fee_type: FeeType,
426        /// Funds available
427        pub available: Signed<Collateral>,
428        /// Fee amount requested
429        pub requested: Signed<Collateral>,
430        /// Description of what happened
431        pub desc: Option<String>,
432    }
433    impl From<&InsufficientMarginEvent> for Event {
434        fn from(
435            InsufficientMarginEvent {
436                pos,
437                fee_type,
438                available,
439                requested,
440                desc,
441            }: &InsufficientMarginEvent,
442        ) -> Self {
443            let evt = Event::new(event_key::INSUFFICIENT_MARGIN)
444                .add_attribute(event_key::POS_ID, pos.to_string())
445                .add_attribute(event_key::FEE_TYPE, fee_type.as_str())
446                .add_attribute(event_key::AVAILABLE, available.to_string())
447                .add_attribute(event_key::REQUESTED, requested.to_string());
448            match desc {
449                Some(desc) => evt.add_attribute(event_key::DESC, desc),
450                None => evt,
451            }
452        }
453    }
454    impl From<InsufficientMarginEvent> for Event {
455        fn from(event: InsufficientMarginEvent) -> Self {
456            (&event).into()
457        }
458    }
459
460    /// Fee type which can have insufficient margin available
461    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
462    pub enum FeeType {
463        /// There is insufficient active collateral for the liquidation margin.
464        Overall,
465        /// Insufficient borrow fee portion of the liquidation margin.
466        Borrow,
467        /// Insufficient delta neutrality fee portion of the liquidation margin.
468        DeltaNeutrality,
469        /// Insufficient funding payment portion of the liquidation margin.
470        Funding,
471        /// Insufficient crank fee portion of the liquidation margin.
472        Crank,
473        /// Protocol-wide insufficient funding payments.
474        ///
475        /// This means that the protocol itself would reach insolvency if we
476        /// paid the funding payments this payment expects.
477        FundingTotal,
478    }
479
480    impl FeeType {
481        /// Represent as a string
482        pub fn as_str(self) -> &'static str {
483            match self {
484                FeeType::Overall => "overall",
485                FeeType::Borrow => "borrow",
486                FeeType::DeltaNeutrality => "delta-neutrality",
487                FeeType::Funding => "funding",
488                FeeType::FundingTotal => "funding-total",
489                FeeType::Crank => "crank",
490            }
491        }
492    }
493}
494
495#[cfg(test)]
496mod tests {
497    use crate::contracts::market::spot_price::SpotPriceConfig;
498
499    use super::*;
500
501    #[test]
502    fn trade_fee_open() {
503        let config = Config {
504            trading_fee_notional_size: "0.01".parse().unwrap(),
505            trading_fee_counter_collateral: "0.02".parse().unwrap(),
506            ..Config::new(SpotPriceConfig::Manual {
507                admin: Addr::unchecked("foo"),
508            })
509        };
510        assert_eq!(
511            config
512                .calculate_trade_fee_open("-500".parse().unwrap(), "200".parse().unwrap())
513                .unwrap(),
514            "9".parse().unwrap()
515        )
516    }
517
518    #[test]
519    fn trade_fee_update() {
520        let config = Config {
521            trading_fee_notional_size: "0.01".parse().unwrap(),
522            trading_fee_counter_collateral: "0.02".parse().unwrap(),
523            ..Config::new(SpotPriceConfig::Manual {
524                admin: Addr::unchecked("foo"),
525            })
526        };
527        assert_eq!(
528            config
529                .calculate_trade_fee(
530                    "-100".parse().unwrap(),
531                    "-500".parse().unwrap(),
532                    "100".parse().unwrap(),
533                    "200".parse().unwrap()
534                )
535                .unwrap(),
536            "6".parse().unwrap()
537        );
538        assert_eq!(
539            config
540                .calculate_trade_fee(
541                    "-100".parse().unwrap(),
542                    "-500".parse().unwrap(),
543                    "300".parse().unwrap(),
544                    "200".parse().unwrap()
545                )
546                .unwrap(),
547            "4".parse().unwrap()
548        );
549        assert_eq!(
550            config
551                .calculate_trade_fee(
552                    "-600".parse().unwrap(),
553                    "-500".parse().unwrap(),
554                    "300".parse().unwrap(),
555                    "200".parse().unwrap()
556                )
557                .unwrap(),
558            "0".parse().unwrap()
559        );
560        assert_eq!(
561            config
562                .calculate_trade_fee(
563                    "-600".parse().unwrap(),
564                    "-500".parse().unwrap(),
565                    "100".parse().unwrap(),
566                    "200".parse().unwrap()
567                )
568                .unwrap(),
569            "2".parse().unwrap()
570        );
571    }
572}