levana_perpswap_cosmos/error/
market.rs

1//! Special error types for the market contract.
2//!
3//! This module is intended as a stop-gap measure. The perps protocol overall
4//! uses `anyhow` for error handling, and then uses a `PerpError` type to
5//! represent known error cases that require special handling by consumers of
6//! the contracts.
7//!
8//! Generally we would like to move `PerpError` over to using `thiserror`, and
9//! then have a duality of error handling: `anyhow::Error` for general purpose
10//! errors (like serialization issues) that do not require special handling, and
11//! `PerpError` for well described error types with known payloads. However,
12//! making such a change would be an invasive change to the codebase.
13//!
14//! Instead, in the short term, we use this module to provide well-typed
15//! `thiserror` error values that can be converted to `PerpError` values.
16
17use crate::prelude::*;
18
19/// An error type for known market errors with potentially special error handling.
20#[derive(thiserror::Error, Debug, serde::Serialize, serde::Deserialize, PartialEq, Eq, Clone)]
21#[serde(rename_all = "snake_case", tag = "error_id")]
22#[allow(missing_docs)]
23pub enum MarketError {
24    #[error(
25        "Infinite max gains can only be used on long positions for collateral-is-base markets"
26    )]
27    InvalidInfiniteMaxGains {
28        market_type: MarketType,
29        direction: DirectionToBase,
30    },
31    #[error(
32        "Infinite take profit price can only be used on long positions for collateral-is-base markets"
33    )]
34    InvalidInfiniteTakeProfitPrice {
35        market_type: MarketType,
36        direction: DirectionToBase,
37    },
38    #[error("Max gains are too large")]
39    MaxGainsTooLarge {},
40    #[error("Unable to withdraw {requested}. Only {available} LP tokens held.")]
41    WithdrawTooMuch {
42        requested: NonZero<LpToken>,
43        available: NonZero<LpToken>,
44    },
45    #[error("Insufficient unlocked liquidity for withdrawal. Requested {requested_collateral} ({requested_lp} LP tokens), only {unlocked} liquidity available until min liquidity.")]
46    InsufficientLiquidityForWithdrawal {
47        requested_lp: NonZero<LpToken>,
48        requested_collateral: NonZero<Collateral>,
49        unlocked: Collateral,
50    },
51    #[error("Missing position: {id}")]
52    MissingPosition { id: String },
53    #[error("Trader leverage {new_leverage} is out of range ({low_allowed}..{high_allowed}]")]
54    TraderLeverageOutOfRange {
55        low_allowed: Decimal256,
56        high_allowed: Decimal256,
57        new_leverage: Decimal256,
58        current_leverage: Option<Decimal256>,
59    },
60    #[error("Deposit collateral is too small. Deposited {deposit_collateral}, or {deposit_usd} USD. Minimum is {minimum_usd} USD")]
61    MinimumDeposit {
62        deposit_collateral: Collateral,
63        deposit_usd: Usd,
64        minimum_usd: Usd,
65    },
66    #[error("Cannot open or update positions currently, the position queue size is {current_queue}, while the allowed size is {max_size}. Please try again later")]
67    Congestion {
68        current_queue: u32,
69        max_size: u32,
70        reason: CongestionReason,
71    },
72    #[error("Deposit would exceed maximum liquidity allowed. Current liquidity: {current} USD. Deposit size: {deposit} USD. Maximum allowed: {max} USD.")]
73    MaxLiquidity {
74        price_collateral_in_usd: PriceCollateralInUsd,
75        current: Usd,
76        deposit: Usd,
77        max: Usd,
78    },
79    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol is already too long")]
80    DeltaNeutralityFeeAlreadyLong {
81        cap: Number,
82        sensitivity: Number,
83        instant_before: Number,
84        net_notional_before: Signed<Notional>,
85        net_notional_after: Signed<Notional>,
86    },
87    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol is already too short")]
88    DeltaNeutralityFeeAlreadyShort {
89        cap: Number,
90        sensitivity: Number,
91        instant_before: Number,
92        net_notional_before: Signed<Notional>,
93        net_notional_after: Signed<Notional>,
94    },
95    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would become too long")]
96    DeltaNeutralityFeeNewlyLong {
97        cap: Number,
98        sensitivity: Number,
99        instant_after: Number,
100        net_notional_before: Signed<Notional>,
101        net_notional_after: Signed<Notional>,
102    },
103    #[error( "Cannot perform this action since it would exceed delta neutrality limits - protocol would become too short")]
104    DeltaNeutralityFeeNewlyShort {
105        cap: Number,
106        sensitivity: Number,
107        instant_after: Number,
108        net_notional_before: Signed<Notional>,
109        net_notional_after: Signed<Notional>,
110    },
111    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would go from too long to too short")]
112    DeltaNeutralityFeeLongToShort {
113        cap: Number,
114        sensitivity: Number,
115        instant_before: Number,
116        instant_after: Number,
117        net_notional_before: Signed<Notional>,
118        net_notional_after: Signed<Notional>,
119    },
120    #[error("Cannot perform this action since it would exceed delta neutrality limits - protocol would go from too short to too long")]
121    DeltaNeutralityFeeShortToLong {
122        cap: Number,
123        sensitivity: Number,
124        instant_before: Number,
125        instant_after: Number,
126        net_notional_before: Signed<Notional>,
127        net_notional_after: Signed<Notional>,
128    },
129    #[error("Liquidity cooldown in effect, will end in {seconds_remaining} seconds.")]
130    LiquidityCooldown {
131        ends_at: Timestamp,
132        seconds_remaining: u64,
133    },
134    #[error("Cannot perform the given action while a pending action is waiting for the position")]
135    PendingDeferredExec {},
136    #[error("The difference between oldest and newest publish timestamp is too large. Oldest: {oldest}. Newest: {newest}.")]
137    VolatilePriceFeedTimeDelta {
138        oldest: Timestamp,
139        newest: Timestamp,
140    },
141    #[error("Limit order {order_id} is already canceling")]
142    LimitOrderAlreadyCanceling { order_id: Uint64 },
143    #[error("Position {position_id} is already closing")]
144    PositionAlreadyClosing { position_id: Uint64 },
145    #[error(
146        "No price publish time found, there is likely a spot price config error for this market"
147    )]
148    NoPricePublishTimeFound,
149    #[error("Cannot close position {id}, it was already closed at {close_time}. Close reason: {reason}.")]
150    PositionAlreadyClosed {
151        id: Uint64,
152        close_time: Timestamp,
153        reason: String,
154    },
155    #[error("Insufficient locked liquidity in protocol to perform the given unlock. Requested: {requested}. Total locked: {total_locked}.")]
156    InsufficientLiquidityForUnlock {
157        requested: NonZero<Collateral>,
158        total_locked: Collateral,
159    },
160    #[error("Insufficient unlocked liquidity in the protocol. Requested: {requested}. Total available: {total_unlocked}. Total allowed with carry leverage restrictions: {allowed}.")]
161    Liquidity {
162        /// Total amount of liquidity requested to take from unlocked pool.
163        requested: NonZero<Collateral>,
164        /// Total amount of liquidity available in the unlocked pool.
165        total_unlocked: Collateral,
166        /// Liquidity allowed to be taken for this action.
167        ///
168        /// In particular, carry leverage may restrict the total amount of
169        /// liquidity that can be used to ensure sufficient funds for cash-and-carry
170        /// balancing operations.
171        allowed: Collateral,
172    },
173}
174
175/// Was the price provided by the trader too high or too low?
176#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
177#[serde(rename_all = "snake_case")]
178pub enum TriggerPriceMustBe {
179    /// Specified price must be less than the bound
180    Less,
181    /// Specified price must be greater than the bound
182    Greater,
183}
184
185impl Display for TriggerPriceMustBe {
186    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
187        write!(
188            f,
189            "{}",
190            match self {
191                TriggerPriceMustBe::Greater => "greater",
192                TriggerPriceMustBe::Less => "less",
193            }
194        )
195    }
196}
197
198/// What type of price trigger occurred?
199#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
200#[serde(rename_all = "snake_case")]
201pub enum TriggerType {
202    /// A stop loss
203    StopLoss,
204    /// A take profit
205    TakeProfit,
206}
207
208impl Display for TriggerType {
209    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
210        write!(
211            f,
212            "{}",
213            match self {
214                TriggerType::StopLoss => "stop loss",
215                TriggerType::TakeProfit => "take profit",
216            }
217        )
218    }
219}
220
221/// What was the user doing when they hit the congestion error message?
222#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, serde::Serialize, serde::Deserialize)]
223#[serde(rename_all = "snake_case")]
224pub enum CongestionReason {
225    /// Opening a new position via market order
226    OpenMarket,
227    /// Placing a new limit order
228    PlaceLimit,
229    /// Updating an existing position
230    Update,
231    /// Setting a trigger price on an existing position
232    SetTrigger,
233}
234
235impl MarketError {
236    /// Convert into an `anyhow::Error`.
237    ///
238    /// This method will first convert into a `PerpError` and then wrap that
239    /// in `anyhow::Error`.
240    pub fn into_anyhow(self) -> anyhow::Error {
241        let description = format!("{self}");
242        self.into_perp_error(description).into()
243    }
244
245    /// Try to convert from an `anyhow::Error`.
246    pub fn try_from_anyhow(err: &anyhow::Error) -> Result<Self> {
247        (|| {
248            let err = err
249                .downcast_ref::<PerpError<MarketError>>()
250                .context("Not a PerpError<MarketError>")?;
251            err.data
252                .clone()
253                .context("PerpError<MarketError> without a data field")
254        })()
255        .with_context(|| format!("try_from_anyhow failed on: {err:?}"))
256    }
257
258    /// Convert into a `PerpError`.
259    fn into_perp_error(self, description: String) -> PerpError<MarketError> {
260        let id = self.get_error_id();
261        PerpError {
262            id,
263            domain: ErrorDomain::Market,
264            description,
265            data: Some(self),
266        }
267    }
268
269    /// Get the [ErrorId] for this value.
270    fn get_error_id(&self) -> ErrorId {
271        match self {
272            MarketError::InvalidInfiniteMaxGains { .. } => ErrorId::InvalidInfiniteMaxGains,
273            MarketError::InvalidInfiniteTakeProfitPrice { .. } => {
274                ErrorId::InvalidInfiniteTakeProfitPrice
275            }
276            MarketError::MaxGainsTooLarge {} => ErrorId::MaxGainsTooLarge,
277            MarketError::WithdrawTooMuch { .. } => ErrorId::WithdrawTooMuch,
278            MarketError::InsufficientLiquidityForWithdrawal { .. } => {
279                ErrorId::InsufficientLiquidityForWithdrawal
280            }
281            MarketError::MissingPosition { .. } => ErrorId::MissingPosition,
282            MarketError::TraderLeverageOutOfRange { .. } => ErrorId::TraderLeverageOutOfRange,
283            MarketError::MinimumDeposit { .. } => ErrorId::MinimumDeposit,
284            MarketError::Congestion { .. } => ErrorId::Congestion,
285            MarketError::MaxLiquidity { .. } => ErrorId::MaxLiquidity,
286            MarketError::DeltaNeutralityFeeAlreadyLong { .. } => {
287                ErrorId::DeltaNeutralityFeeAlreadyLong
288            }
289            MarketError::DeltaNeutralityFeeAlreadyShort { .. } => {
290                ErrorId::DeltaNeutralityFeeAlreadyShort
291            }
292            MarketError::DeltaNeutralityFeeNewlyLong { .. } => ErrorId::DeltaNeutralityFeeNewlyLong,
293            MarketError::DeltaNeutralityFeeNewlyShort { .. } => {
294                ErrorId::DeltaNeutralityFeeNewlyShort
295            }
296            MarketError::DeltaNeutralityFeeLongToShort { .. } => {
297                ErrorId::DeltaNeutralityFeeLongToShort
298            }
299            MarketError::DeltaNeutralityFeeShortToLong { .. } => {
300                ErrorId::DeltaNeutralityFeeShortToLong
301            }
302            MarketError::LiquidityCooldown { .. } => ErrorId::LiquidityCooldown,
303            MarketError::PendingDeferredExec {} => ErrorId::PendingDeferredExec,
304            MarketError::VolatilePriceFeedTimeDelta { .. } => ErrorId::VolatilePriceFeedTimeDelta,
305            MarketError::LimitOrderAlreadyCanceling { .. } => ErrorId::LimitOrderAlreadyCanceling,
306            MarketError::PositionAlreadyClosing { .. } => ErrorId::PositionAlreadyClosing,
307            MarketError::NoPricePublishTimeFound => ErrorId::NoPricePublishTimeFound,
308            MarketError::PositionAlreadyClosed { .. } => ErrorId::PositionAlreadyClosed,
309            MarketError::InsufficientLiquidityForUnlock { .. } => {
310                ErrorId::InsufficientLiquidityForUnlock
311            }
312            MarketError::Liquidity { .. } => ErrorId::Liquidity,
313        }
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use super::*;
320
321    #[test]
322    fn into_perp_error() {
323        let market_error = MarketError::WithdrawTooMuch {
324            requested: "100".parse().unwrap(),
325            available: "50".parse().unwrap(),
326        };
327        let expected = PerpError {
328            id: ErrorId::WithdrawTooMuch,
329            domain: ErrorDomain::Market,
330            description: "Unable to withdraw 100. Only 50 LP tokens held.".to_owned(),
331            data: Some(market_error.clone()),
332        };
333        let anyhow_error = market_error.clone().into_anyhow();
334        let actual = anyhow_error.downcast_ref::<PerpError<_>>().unwrap();
335        assert_eq!(&expected, actual);
336
337        let market_error2 = MarketError::try_from_anyhow(&anyhow_error).unwrap();
338        assert_eq!(market_error, market_error2);
339    }
340}