levana_perpswap_cosmos/contracts/market/
crank.rs

1//! Data types and events for cranking.
2use super::deferred_execution::{DeferredExecId, DeferredExecTarget};
3use super::position::PositionId;
4use crate::contracts::market::order::OrderId;
5use crate::contracts::market::position::LiquidationReason;
6use crate::prelude::*;
7
8/// What work is currently available for the crank.
9#[cw_serde]
10pub enum CrankWorkInfo {
11    /// Closing all open positions
12    CloseAllPositions {
13        /// Next position to be closed
14        position: PositionId,
15    },
16    /// Resetting all LP balances to 0 after all liquidity is drained
17    ResetLpBalances {},
18    /// Liquifund a position
19    Liquifunding {
20        /// Next position to be liquifunded
21        position: PositionId,
22    },
23    /// Liquidate a position.
24    ///
25    /// Includes max gains, take profit, and stop loss.
26    Liquidation {
27        /// Position to liquidate
28        position: PositionId,
29        /// Reason for the liquidation
30        liquidation_reason: LiquidationReason,
31    },
32    /// Deferred execution (open/update/closed) can be executed.
33    DeferredExec {
34        /// ID to be processed
35        deferred_exec_id: DeferredExecId,
36        /// Target of the action
37        target: DeferredExecTarget,
38    },
39    /// Limit order can be opened
40    LimitOrder {
41        /// ID of the order to be opened
42        order_id: OrderId,
43    },
44    /// Finished all processing for a given price update
45    Completed {},
46}
47
48impl CrankWorkInfo {
49    /// Should a cranker receive rewards for performing this action?
50    ///
51    /// We generally want to give out rewards for actions that are directly
52    /// user initiated and will be receiving a crank fee paid into the system. Actions
53    /// which are overall protocol maintenance without a specific user action may be
54    /// unfunded. A simple "attack" we want to avoid is a cranker flooding the system
55    /// with unnecessary price updates + cranks to continue making a profit off of
56    /// "Completed" items.
57    pub fn receives_crank_rewards(&self) -> bool {
58        match self {
59            CrankWorkInfo::CloseAllPositions { .. }
60            | CrankWorkInfo::ResetLpBalances {}
61            | CrankWorkInfo::Completed { .. } => false,
62            CrankWorkInfo::Liquifunding { .. }
63            | CrankWorkInfo::Liquidation { .. }
64            | CrankWorkInfo::DeferredExec { .. }
65            | CrankWorkInfo::LimitOrder { .. } => true,
66        }
67    }
68}
69
70/// Events related to the crank
71pub mod events {
72    use std::borrow::Cow;
73
74    use super::*;
75    use cosmwasm_std::Event;
76
77    /// Batch processing of multiple cranks
78    pub struct CrankExecBatchEvent {
79        /// How many cranks were requested
80        pub requested: u64,
81        /// How many paying cranks were processed
82        pub paying: u64,
83        /// How many cranks were actually processed
84        pub actual: Vec<(CrankWorkInfo, PricePoint)>,
85    }
86
87    impl From<CrankExecBatchEvent> for Event {
88        fn from(
89            CrankExecBatchEvent {
90                requested,
91                paying,
92                actual,
93            }: CrankExecBatchEvent,
94        ) -> Self {
95            let mut event = Event::new("crank-batch-exec")
96                .add_attribute("requested", requested.to_string())
97                .add_attribute("actual", actual.len().to_string())
98                .add_attribute("paying", paying.to_string());
99
100            for (idx, (work, price_point)) in actual.into_iter().enumerate() {
101                event = event.add_attribute(
102                    format!("work-{}", idx + 1),
103                    match work {
104                        CrankWorkInfo::CloseAllPositions { .. } => {
105                            Cow::Borrowed("close-all-positions")
106                        }
107                        CrankWorkInfo::ResetLpBalances {} => "reset-lp-balances".into(),
108                        CrankWorkInfo::Liquifunding { position, .. } => {
109                            format!("liquifund {position}").into()
110                        }
111                        CrankWorkInfo::Liquidation { position, .. } => {
112                            format!("liquidation {position}").into()
113                        }
114                        CrankWorkInfo::DeferredExec {
115                            deferred_exec_id, ..
116                        } => format!("deferred exec {deferred_exec_id}").into(),
117                        CrankWorkInfo::LimitOrder { order_id, .. } => {
118                            format!("limit order {order_id}").into()
119                        }
120                        CrankWorkInfo::Completed {} => {
121                            format!("completed {}", price_point.timestamp).into()
122                        }
123                    },
124                )
125            }
126
127            event
128        }
129    }
130
131    /// CrankWorkInfo with a given price point
132    pub struct CrankWorkInfoEvent {
133        /// The work info itself
134        pub work_info: CrankWorkInfo,
135        /// A price point, i.e. the price point that triggered the work
136        pub price_point: PricePoint,
137    }
138
139    impl From<CrankWorkInfoEvent> for Event {
140        fn from(
141            CrankWorkInfoEvent {
142                work_info,
143                price_point,
144            }: CrankWorkInfoEvent,
145        ) -> Self {
146            let mut event = Event::new("crank-work")
147                .add_attribute(
148                    "kind",
149                    match work_info {
150                        CrankWorkInfo::CloseAllPositions { .. } => "close-all-positions",
151                        CrankWorkInfo::ResetLpBalances { .. } => "reset-lp-balances",
152                        CrankWorkInfo::Completed { .. } => "completed",
153                        CrankWorkInfo::Liquidation { .. } => "liquidation",
154                        CrankWorkInfo::Liquifunding { .. } => "liquifunding",
155                        CrankWorkInfo::DeferredExec { .. } => "deferred-exec",
156                        CrankWorkInfo::LimitOrder { .. } => "limit-order",
157                    },
158                )
159                // Keeping this for backwards-compat with indexer, though it should be deprecated
160                // and deserialized from price-point itself
161                .add_attribute("price-point-timestamp", price_point.timestamp.to_string())
162                .add_attribute("price-point", serde_json::to_string(&price_point).unwrap());
163
164            let (position_id, order_id) = match work_info {
165                CrankWorkInfo::CloseAllPositions { position } => (Some(position), None),
166                CrankWorkInfo::ResetLpBalances {} => (None, None),
167                CrankWorkInfo::Completed {} => (None, None),
168                CrankWorkInfo::Liquidation {
169                    position,
170                    liquidation_reason: _,
171                } => (Some(position), None),
172                CrankWorkInfo::Liquifunding { position } => (Some(position), None),
173                CrankWorkInfo::DeferredExec {
174                    deferred_exec_id: _,
175                    target,
176                } => (target.position_id(), target.order_id()),
177                CrankWorkInfo::LimitOrder { order_id } => (None, Some(order_id)),
178            };
179
180            if let Some(position_id) = position_id {
181                event = event.add_attribute("pos-id", position_id.to_string());
182            }
183
184            if let CrankWorkInfo::Liquidation {
185                liquidation_reason, ..
186            } = work_info
187            {
188                event = event.add_attribute("liquidation-reason", liquidation_reason.to_string());
189            }
190
191            if let CrankWorkInfo::DeferredExec {
192                deferred_exec_id,
193                target,
194            } = work_info
195            {
196                event = event
197                    .add_attribute("deferred-exec-id", deferred_exec_id.to_string())
198                    .add_attribute(
199                        "deferred-exec-target",
200                        match target {
201                            DeferredExecTarget::DoesNotExist => "not-exist",
202                            DeferredExecTarget::Position { .. } => "position",
203                            DeferredExecTarget::Order { .. } => "order",
204                        },
205                    );
206            }
207
208            if let Some(order_id) = order_id {
209                event = event.add_attribute("order-id", order_id.to_string());
210            }
211
212            event
213        }
214    }
215
216    impl TryFrom<Event> for CrankWorkInfoEvent {
217        type Error = anyhow::Error;
218
219        fn try_from(evt: Event) -> anyhow::Result<Self> {
220            let get_position_id =
221                || -> anyhow::Result<PositionId> { Ok(PositionId::new(evt.u64_attr("pos-id")?)) };
222            let get_order_id =
223                || -> anyhow::Result<OrderId> { Ok(OrderId::new(evt.u64_attr("order-id")?)) };
224
225            let get_liquidation_reason = || -> anyhow::Result<LiquidationReason> {
226                match evt.string_attr("liquidation-reason")?.as_str() {
227                    "liquidated" => Ok(LiquidationReason::Liquidated),
228                    "take-profit" => Ok(LiquidationReason::TakeProfit),
229                    _ => Err(PerpError::unimplemented().into()),
230                }
231            };
232
233            let work_info = evt.map_attr_result("kind", |s| match s {
234                "completed" => Ok(CrankWorkInfo::Completed {}),
235                "liquifunding" => Ok(CrankWorkInfo::Liquifunding {
236                    position: get_position_id()?,
237                }),
238                "liquidation" => Ok(CrankWorkInfo::Liquidation {
239                    position: get_position_id()?,
240                    liquidation_reason: get_liquidation_reason()?,
241                }),
242                "limit-order" => Ok(CrankWorkInfo::LimitOrder {
243                    order_id: get_order_id()?,
244                }),
245                "close-all-positions" => Ok(CrankWorkInfo::CloseAllPositions {
246                    position: get_position_id()?,
247                }),
248                "reset-lp-balances" => Ok(CrankWorkInfo::ResetLpBalances {}),
249                "deferred-exec" => Ok(CrankWorkInfo::DeferredExec {
250                    deferred_exec_id: DeferredExecId::from_u64(evt.u64_attr("deferred-exec-id")?),
251                    target: evt.map_attr_result(
252                        "deferred-exec-target",
253                        |x| -> Result<DeferredExecTarget> {
254                            match x {
255                                "not-exist" => Ok(DeferredExecTarget::DoesNotExist),
256                                "position" => get_position_id().map(DeferredExecTarget::Position),
257                                "order" => get_order_id().map(DeferredExecTarget::Order),
258                                _ => Err(PerpError::unimplemented().into()),
259                            }
260                        },
261                    )?,
262                }),
263
264                _ => Err(PerpError::unimplemented().into()),
265            })?;
266
267            Ok(Self {
268                work_info,
269                price_point: evt.json_attr("price-point")?,
270            })
271        }
272    }
273
274    // Not sure if this is needed anymore, but keeping it in for backwards compat at least
275    impl TryFrom<Event> for CrankWorkInfo {
276        type Error = anyhow::Error;
277
278        fn try_from(evt: Event) -> anyhow::Result<Self> {
279            CrankWorkInfoEvent::try_from(evt).map(|x| x.work_info)
280        }
281    }
282}