levana_perpswap_cosmos/contracts/market/
deferred_execution.rs

1//! Deferred execution work items.
2//!
3//! This allows the protocol to ensure only fresh prices are used for price-sensitive operations.
4use std::{fmt, num::ParseIntError};
5
6use crate::constants::event_key;
7use crate::prelude::*;
8use cosmwasm_std::StdResult;
9use cw_storage_plus::{IntKey, Key, KeyDeserialize, Prefixer, PrimaryKey};
10
11use super::{
12    entry::{SlippageAssert, StopLoss},
13    order::OrderId,
14    position::PositionId,
15};
16
17/// A unique numeric ID for each deferred execution in the protocol.
18#[cw_serde]
19#[derive(Copy, PartialOrd, Ord, Eq)]
20pub struct DeferredExecId(Uint64);
21
22impl std::hash::Hash for DeferredExecId {
23    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
24        self.0.u64().hash(state);
25    }
26}
27
28impl DeferredExecId {
29    /// First ID issued. We start with 1 instead of 0 for user friendliness.
30    pub fn first() -> Self {
31        DeferredExecId(Uint64::one())
32    }
33
34    /// Get the next deferred exec ID. Will panic if you overflow.
35    pub fn next(self) -> Self {
36        DeferredExecId((self.0.u64() + 1).into())
37    }
38
39    /// Get the underlying `u64` representation.
40    pub fn u64(self) -> u64 {
41        self.0.u64()
42    }
43
44    /// Generate from a raw u64
45    pub fn from_u64(x: u64) -> Self {
46        DeferredExecId(x.into())
47    }
48}
49
50impl<'a> PrimaryKey<'a> for DeferredExecId {
51    type Prefix = ();
52    type SubPrefix = ();
53    type Suffix = Self;
54    type SuperSuffix = Self;
55
56    fn key(&self) -> Vec<Key> {
57        vec![Key::Val64(self.0.u64().to_cw_bytes())]
58    }
59}
60
61impl<'a> Prefixer<'a> for DeferredExecId {
62    fn prefix(&self) -> Vec<Key> {
63        vec![Key::Val64(self.0.u64().to_cw_bytes())]
64    }
65}
66
67impl KeyDeserialize for DeferredExecId {
68    type Output = DeferredExecId;
69
70    const KEY_ELEMS: u16 = 1;
71
72    #[inline(always)]
73    fn from_vec(value: Vec<u8>) -> StdResult<Self::Output> {
74        u64::from_vec(value).map(|x| DeferredExecId(Uint64::new(x)))
75    }
76}
77
78impl fmt::Display for DeferredExecId {
79    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
80        write!(f, "{}", self.0)
81    }
82}
83
84impl FromStr for DeferredExecId {
85    type Err = ParseIntError;
86    fn from_str(src: &str) -> Result<Self, ParseIntError> {
87        src.parse().map(|x| DeferredExecId(Uint64::new(x)))
88    }
89}
90
91/// Enumeration API for getting deferred exec IDs
92#[cw_serde]
93pub struct ListDeferredExecsResp {
94    /// Next batch of items
95    pub items: Vec<DeferredExecWithStatus>,
96    /// Only `Some` if more IDs exist
97    pub next_start_after: Option<DeferredExecId>,
98}
99
100/// Result of trying to query a single deferred execution item.
101#[cw_serde]
102pub enum GetDeferredExecResp {
103    /// The requested ID was found
104    Found {
105        /// The current state of the item
106        item: Box<DeferredExecWithStatus>,
107    },
108    /// The requested ID was not found
109    NotFound {},
110}
111
112/// A deferred execution work item and its current status.
113#[cw_serde]
114pub struct DeferredExecWithStatus {
115    /// ID of this item
116    pub id: DeferredExecId,
117    /// Timestamp this was created, and therefore minimum price update timestamp needed
118    pub created: Timestamp,
119    /// Status
120    pub status: DeferredExecStatus,
121    /// Who owns (i.e. created) this item?
122    pub owner: Addr,
123    /// Work item
124    pub item: DeferredExecItem,
125}
126
127/// Current status of a deferred execution work item
128#[cw_serde]
129pub enum DeferredExecStatus {
130    /// Waiting to be cranked
131    Pending,
132    /// Successfully applied
133    Success {
134        /// Entity in the system that was impacted by this execution
135        target: DeferredExecCompleteTarget,
136        /// Timestamp when it was successfully executed
137        executed: Timestamp,
138    },
139    /// Did not successfully apply
140    Failure {
141        /// Reason it didn't apply successfully
142        reason: String,
143        /// Timestamp when it failed execution
144        executed: Timestamp,
145        /// Price point when it was cranked, if applicable
146        crank_price: Option<PricePoint>,
147    },
148}
149
150impl DeferredExecStatus {
151    /// Is this item still pending execution?
152    pub fn is_pending(&self) -> bool {
153        match self {
154            DeferredExecStatus::Pending => true,
155            DeferredExecStatus::Success { .. } => false,
156            DeferredExecStatus::Failure { .. } => false,
157        }
158    }
159    /// Is this item a failure?
160    pub fn is_failure(&self) -> bool {
161        match self {
162            DeferredExecStatus::Pending => false,
163            DeferredExecStatus::Success { .. } => false,
164            DeferredExecStatus::Failure { .. } => true,
165        }
166    }
167
168    /// Is this item a success?
169    pub fn is_success(&self) -> bool {
170        match self {
171            DeferredExecStatus::Pending => false,
172            DeferredExecStatus::Success { .. } => true,
173            DeferredExecStatus::Failure { .. } => false,
174        }
175    }
176}
177
178/// A deferred execution work item
179#[cw_serde]
180#[allow(clippy::large_enum_variant)]
181pub enum DeferredExecItem {
182    /// Open a new position
183    OpenPosition {
184        /// Assertion that the price has not moved too far
185        slippage_assert: Option<SlippageAssert>,
186        /// Leverage of new position
187        leverage: LeverageToBase,
188        /// Direction of new position
189        direction: DirectionToBase,
190        /// Maximum gains of new position
191        #[deprecated(note = "use take_profit instead")]
192        max_gains: Option<MaxGainsInQuote>,
193        /// Stop loss price of new position
194        stop_loss_override: Option<PriceBaseInQuote>,
195        /// Take profit price of new position
196        #[serde(alias = "take_profit_override")]
197        take_profit: Option<TakeProfitTrader>,
198        /// The amount of collateral provided
199        amount: NonZero<Collateral>,
200        /// Crank fee already charged
201        ///
202        /// Note that this field only exists for variants where there isn't a
203        /// position or order to charge the fee against. In those cases, the position/order
204        /// itself is immediately updated to reflect the new charge.
205        crank_fee: Collateral,
206        /// Crank fee charged, in USD
207        crank_fee_usd: Usd,
208    },
209    /// Add collateral to a position, causing leverage to decrease
210    ///
211    /// The amount of collateral to add must be attached as funds
212    UpdatePositionAddCollateralImpactLeverage {
213        /// ID of position to update
214        id: PositionId,
215        /// The amount of collateral provided
216        amount: NonZero<Collateral>,
217    },
218    /// Add collateral to a position, causing notional size to increase
219    ///
220    /// The amount of collateral to add must be attached as funds
221    UpdatePositionAddCollateralImpactSize {
222        /// ID of position to update
223        id: PositionId,
224        /// Assertion that the price has not moved too far
225        slippage_assert: Option<SlippageAssert>,
226        /// The amount of collateral provided
227        amount: NonZero<Collateral>,
228    },
229
230    /// Remove collateral from a position, causing leverage to increase
231    UpdatePositionRemoveCollateralImpactLeverage {
232        /// ID of position to update
233        id: PositionId,
234        /// Amount of funds to remove from the position
235        amount: NonZero<Collateral>,
236    },
237    /// Remove collateral from a position, causing notional size to decrease
238    UpdatePositionRemoveCollateralImpactSize {
239        /// ID of position to update
240        id: PositionId,
241        /// Amount of funds to remove from the position
242        amount: NonZero<Collateral>,
243        /// Assertion that the price has not moved too far
244        slippage_assert: Option<SlippageAssert>,
245    },
246
247    /// Modify the leverage of the position
248    ///
249    /// This will impact the notional size of the position
250    UpdatePositionLeverage {
251        /// ID of position to update
252        id: PositionId,
253        /// New leverage of the position
254        leverage: LeverageToBase,
255        /// Assertion that the price has not moved too far
256        slippage_assert: Option<SlippageAssert>,
257    },
258
259    /// Modify the max gains of a position
260    UpdatePositionMaxGains {
261        /// ID of position to update
262        id: PositionId,
263        /// New max gains of the position
264        max_gains: MaxGainsInQuote,
265    },
266
267    /// Modify the take profit price of a position
268    UpdatePositionTakeProfitPrice {
269        /// ID of position to update
270        id: PositionId,
271        /// New take profit price of the position
272        price: TakeProfitTrader,
273    },
274
275    /// Modify the stop loss price of a position
276    UpdatePositionStopLossPrice {
277        /// ID of position to update
278        id: PositionId,
279        /// New stop loss price of the position
280        stop_loss: StopLoss,
281    },
282
283    /// Close a position
284    ClosePosition {
285        /// ID of position to close
286        id: PositionId,
287        /// Assertion that the price has not moved too far
288        slippage_assert: Option<SlippageAssert>,
289    },
290
291    /// Set a stop loss or take profit override.
292    SetTriggerOrder {
293        /// ID of position to modify
294        id: PositionId,
295        /// New stop loss price of the position
296        /// Passing None will remove the override.
297        stop_loss_override: Option<PriceBaseInQuote>,
298        /// New take_profit price of the position
299        /// Passing None will bypass changing this
300        #[serde(alias = "take_profit_override")]
301        take_profit: Option<TakeProfitTrader>,
302    },
303
304    /// Set a limit order to open a position when the price of the asset hits
305    /// the specified trigger price.
306    PlaceLimitOrder {
307        /// Price when the order should trigger
308        trigger_price: PriceBaseInQuote,
309        /// Leverage of new position
310        leverage: LeverageToBase,
311        /// Direction of new position
312        direction: DirectionToBase,
313        /// Maximum gains of new position
314        #[deprecated(note = "use take_profit instead")]
315        max_gains: Option<MaxGainsInQuote>,
316        /// Stop loss price of new position
317        stop_loss_override: Option<PriceBaseInQuote>,
318        /// Take profit price of new position
319        #[serde(alias = "take_profit_override")]
320        take_profit: Option<TakeProfitTrader>,
321        /// The amount of collateral provided
322        amount: NonZero<Collateral>,
323        /// Crank fee already charged
324        crank_fee: Collateral,
325        /// Crank fee charged, in USD
326        crank_fee_usd: Usd,
327    },
328
329    /// Cancel an open limit order
330    CancelLimitOrder {
331        /// ID of the order
332        order_id: OrderId,
333    },
334}
335
336/// What entity within the system will be affected by this.
337#[cw_serde]
338#[derive(Copy)]
339pub enum DeferredExecTarget {
340    /// For open positions or limit orders, no ID exists yet
341    DoesNotExist,
342    /// Modifying an existing position
343    Position(PositionId),
344    /// Modifying an existing limit order
345    Order(OrderId),
346}
347
348/// After successful execution of an item, what did it impact?
349///
350/// Unlike [DeferredExecTarget] because, after execution, we always have a specific position or order impacted.
351#[cw_serde]
352#[derive(Copy)]
353pub enum DeferredExecCompleteTarget {
354    /// Create or Modify an existing position
355    Position(PositionId),
356    /// Create or Modify an existing limit order
357    Order(OrderId),
358}
359
360impl DeferredExecTarget {
361    /// The position ID, if present
362    pub fn position_id(&self) -> Option<PositionId> {
363        match self {
364            DeferredExecTarget::DoesNotExist | DeferredExecTarget::Order(_) => None,
365            DeferredExecTarget::Position(pos_id) => Some(*pos_id),
366        }
367    }
368
369    /// The order ID, if present
370    pub fn order_id(&self) -> Option<OrderId> {
371        match self {
372            DeferredExecTarget::DoesNotExist | DeferredExecTarget::Position(_) => None,
373            DeferredExecTarget::Order(order_id) => Some(*order_id),
374        }
375    }
376}
377
378impl DeferredExecItem {
379    /// What entity in the system is targetted by this item.
380    pub fn target(&self) -> DeferredExecTarget {
381        match self {
382            DeferredExecItem::OpenPosition { .. } => DeferredExecTarget::DoesNotExist,
383            DeferredExecItem::UpdatePositionAddCollateralImpactLeverage { id, .. } => {
384                DeferredExecTarget::Position(*id)
385            }
386            DeferredExecItem::UpdatePositionAddCollateralImpactSize { id, .. } => {
387                DeferredExecTarget::Position(*id)
388            }
389            DeferredExecItem::UpdatePositionRemoveCollateralImpactLeverage { id, .. } => {
390                DeferredExecTarget::Position(*id)
391            }
392            DeferredExecItem::UpdatePositionRemoveCollateralImpactSize { id, .. } => {
393                DeferredExecTarget::Position(*id)
394            }
395            DeferredExecItem::UpdatePositionLeverage { id, .. } => {
396                DeferredExecTarget::Position(*id)
397            }
398            DeferredExecItem::UpdatePositionMaxGains { id, .. } => {
399                DeferredExecTarget::Position(*id)
400            }
401            DeferredExecItem::UpdatePositionTakeProfitPrice { id, .. } => {
402                DeferredExecTarget::Position(*id)
403            }
404            DeferredExecItem::UpdatePositionStopLossPrice { id, .. } => {
405                DeferredExecTarget::Position(*id)
406            }
407            DeferredExecItem::ClosePosition { id, .. } => DeferredExecTarget::Position(*id),
408            DeferredExecItem::SetTriggerOrder { id, .. } => DeferredExecTarget::Position(*id),
409            DeferredExecItem::PlaceLimitOrder { .. } => DeferredExecTarget::DoesNotExist,
410            DeferredExecItem::CancelLimitOrder { order_id } => DeferredExecTarget::Order(*order_id),
411        }
412    }
413
414    /// How much collateral was deposited with this item.
415    pub fn deposited_amount(&self) -> Collateral {
416        match self {
417            DeferredExecItem::OpenPosition { amount, .. }
418            | DeferredExecItem::UpdatePositionAddCollateralImpactLeverage { amount, .. }
419            | DeferredExecItem::UpdatePositionAddCollateralImpactSize { amount, .. }
420            | DeferredExecItem::PlaceLimitOrder { amount, .. } => amount.raw(),
421            DeferredExecItem::UpdatePositionRemoveCollateralImpactLeverage { .. }
422            | DeferredExecItem::UpdatePositionRemoveCollateralImpactSize { .. }
423            | DeferredExecItem::UpdatePositionLeverage { .. }
424            | DeferredExecItem::UpdatePositionMaxGains { .. }
425            | DeferredExecItem::UpdatePositionTakeProfitPrice { .. }
426            | DeferredExecItem::UpdatePositionStopLossPrice { .. }
427            | DeferredExecItem::ClosePosition { .. }
428            | DeferredExecItem::SetTriggerOrder { .. }
429            | DeferredExecItem::CancelLimitOrder { .. } => Collateral::zero(),
430        }
431    }
432}
433
434/// Event emitted when a deferred execution is queued.
435#[derive(Clone, Debug)]
436pub struct DeferredExecQueuedEvent {
437    /// ID
438    pub deferred_exec_id: DeferredExecId,
439    /// What entity is targetted by this item
440    pub target: DeferredExecTarget,
441    /// Address that queued the event
442    pub owner: Addr,
443}
444
445impl From<DeferredExecQueuedEvent> for Event {
446    fn from(
447        DeferredExecQueuedEvent {
448            deferred_exec_id,
449            target,
450            owner,
451        }: DeferredExecQueuedEvent,
452    ) -> Self {
453        let mut event = Event::new("deferred-exec-queued")
454            .add_attribute(event_key::DEFERRED_EXEC_ID, deferred_exec_id.to_string())
455            .add_attribute(event_key::DEFERRED_EXEC_OWNER, owner);
456        match target {
457            DeferredExecTarget::DoesNotExist => {
458                event = event.add_attribute(event_key::DEFERRED_EXEC_TARGET, "does-not-exist");
459            }
460            DeferredExecTarget::Position(position_id) => {
461                event = event
462                    .add_attribute(event_key::POS_ID, position_id.to_string())
463                    .add_attribute(event_key::DEFERRED_EXEC_TARGET, "position");
464            }
465            DeferredExecTarget::Order(order_id) => {
466                event = event
467                    .add_attribute(event_key::ORDER_ID, order_id.to_string())
468                    .add_attribute(event_key::DEFERRED_EXEC_TARGET, "order");
469            }
470        }
471        event
472    }
473}
474
475impl TryFrom<Event> for DeferredExecQueuedEvent {
476    type Error = anyhow::Error;
477
478    fn try_from(evt: Event) -> anyhow::Result<Self> {
479        Ok(Self {
480            deferred_exec_id: evt
481                .u64_attr(event_key::DEFERRED_EXEC_ID)
482                .map(DeferredExecId::from_u64)?,
483            owner: evt.unchecked_addr_attr(event_key::DEFERRED_EXEC_OWNER)?,
484            target: match evt.string_attr(event_key::DEFERRED_EXEC_TARGET)?.as_str() {
485                "does-not-exist" => DeferredExecTarget::DoesNotExist,
486                "position" => DeferredExecTarget::Position(
487                    evt.u64_attr(event_key::POS_ID).map(PositionId::new)?,
488                ),
489                "order" => {
490                    DeferredExecTarget::Order(evt.u64_attr(event_key::ORDER_ID).map(OrderId::new)?)
491                }
492                _ => anyhow::bail!("invalid deferred exec target"),
493            },
494        })
495    }
496}
497
498/// Event when a deferred execution item is executed via the crank.
499#[derive(Debug)]
500pub struct DeferredExecExecutedEvent {
501    /// ID
502    pub deferred_exec_id: DeferredExecId,
503    /// Entity targeted by this action
504    pub target: DeferredExecTarget,
505    /// Address that owns this item
506    pub owner: Addr,
507    /// Was this item executed successfully?
508    pub success: bool,
509    /// Text description of what happened
510    pub desc: String,
511}
512
513impl From<DeferredExecExecutedEvent> for Event {
514    fn from(
515        DeferredExecExecutedEvent {
516            deferred_exec_id,
517            target,
518            owner,
519            success,
520            desc,
521        }: DeferredExecExecutedEvent,
522    ) -> Self {
523        let mut event = Event::new("deferred-exec-executed")
524            .add_attribute(event_key::DEFERRED_EXEC_ID, deferred_exec_id.to_string())
525            .add_attribute(event_key::DEFERRED_EXEC_OWNER, owner)
526            .add_attribute(event_key::SUCCESS, if success { "true" } else { "false" })
527            .add_attribute(event_key::DESC, desc);
528
529        match target {
530            DeferredExecTarget::DoesNotExist => {
531                event = event.add_attribute(event_key::DEFERRED_EXEC_TARGET, "does-not-exist");
532            }
533            DeferredExecTarget::Position(position_id) => {
534                event = event
535                    .add_attribute(event_key::POS_ID, position_id.to_string())
536                    .add_attribute(event_key::DEFERRED_EXEC_TARGET, "position");
537            }
538            DeferredExecTarget::Order(order_id) => {
539                event = event
540                    .add_attribute(event_key::ORDER_ID, order_id.to_string())
541                    .add_attribute(event_key::DEFERRED_EXEC_TARGET, "order");
542            }
543        }
544        event
545    }
546}
547
548impl TryFrom<Event> for DeferredExecExecutedEvent {
549    type Error = anyhow::Error;
550
551    fn try_from(evt: Event) -> anyhow::Result<Self> {
552        Ok(Self {
553            deferred_exec_id: evt
554                .u64_attr(event_key::DEFERRED_EXEC_ID)
555                .map(DeferredExecId::from_u64)?,
556            owner: evt.unchecked_addr_attr(event_key::DEFERRED_EXEC_OWNER)?,
557            success: evt.bool_attr(event_key::SUCCESS)?,
558            desc: evt.string_attr(event_key::DESC)?,
559            target: match evt.string_attr(event_key::DEFERRED_EXEC_TARGET)?.as_str() {
560                "does-not-exist" => DeferredExecTarget::DoesNotExist,
561                "position" => DeferredExecTarget::Position(
562                    evt.u64_attr(event_key::POS_ID).map(PositionId::new)?,
563                ),
564                "order" => {
565                    DeferredExecTarget::Order(evt.u64_attr(event_key::ORDER_ID).map(OrderId::new)?)
566                }
567                _ => anyhow::bail!("invalid deferred exec target"),
568            },
569        })
570    }
571}
572
573/// Event when fees are returned to a user
574pub struct FeesReturnedEvent {
575    /// Who overpaid the fees and received them back
576    pub recipient: Addr,
577    /// Amount received in collateral
578    pub amount: NonZero<Collateral>,
579    /// Current USD amount
580    pub amount_usd: NonZero<Usd>,
581}
582
583impl From<FeesReturnedEvent> for Event {
584    fn from(
585        FeesReturnedEvent {
586            recipient,
587            amount,
588            amount_usd,
589        }: FeesReturnedEvent,
590    ) -> Self {
591        Event::new("fees-returned")
592            .add_attribute("recipient", recipient.into_string())
593            .add_attribute("amount", amount.to_string())
594            .add_attribute("amount_usd", amount_usd.to_string())
595    }
596}