templar_vault_kernel/
error.rs

1//! Kernel error types.
2
3use crate::restrictions::RestrictionKind;
4use crate::transitions::TransitionError;
5
6/// Indexed invalid-state reasons for stable wasm diagnostics.
7#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
8#[derive(Clone, Copy, PartialEq, Eq)]
9#[repr(u16)]
10pub enum InvalidStateCode {
11    Unknown = 0,
12    WithdrawalQueueHeadMismatch = 1,
13    FeeMintOverflowTotalSupply = 2,
14    WithdrawalQueueCacheOverflow = 3,
15    WithdrawalQueueMissingEntry = 4,
16    UnexpectedEmptyQueue = 5,
17    WithdrawalQueueInvariantViolation = 6,
18    DepositRequiresIdle = 7,
19    DepositOverflowTotalAssets = 8,
20    DepositOverflowIdleAssets = 9,
21    MintOverflowTotalShares = 10,
22    RequestWithdrawRequiresIdle = 11,
23    ExecuteWithdrawRequiresIdle = 12,
24    ExecuteWithdrawRequiresIdleUseCallbacks = 13,
25    StartAllocationMustReturnAllocating = 14,
26    AllocationPlanExceedsIdleAssets = 15,
27    SyncExternalRequiresActiveOp = 16,
28    SyncExternalRequiresAllowedStates = 17,
29    SyncExternalOverflowIdlePlusExternal = 18,
30    AbortRefreshingRequiresActiveOp = 20,
31    AbortRefreshingRequiresRefreshing = 21,
32    AbortAllocatingRequiresAllocating = 22,
33    AbortAllocatingRestoreIdleMismatch = 23,
34    AbortWithdrawingRequiresWithdrawing = 24,
35    AbortWithdrawingRefundMismatch = 25,
36    SettlePayoutRequiresPayout = 26,
37    PayoutSuccessSettlementMismatch = 27,
38    PayoutBurnExceedsTotalShares = 28,
39    PayoutFailureSettlementMismatch = 29,
40    PayoutFailureRestoreIdleMismatch = 30,
41    RefreshFeesRequiresIdle = 31,
42    FeeRefreshTimestampMustAdvance = 32,
43    EmergencyResetAlreadyIdle = 33,
44    AtomicWithdrawRequiresIdle = 34,
45    AtomicWithdrawExceedsIdleAssets = 35,
46    AtomicWithdrawBurnExceedsTotalShares = 36,
47    AtomicWithdrawTotalAssetsUnderflow = 37,
48    RebalanceWithdrawRequiresIdle = 38,
49    RebalanceWithdrawExceedsExternalAssets = 39,
50    RebalanceWithdrawOverflowsIdleAssets = 40,
51    WithdrawalLiquidityBelowMinimum = 41,
52    RequestWithdrawExpectedAssetsExceedTotalAssets = 42,
53}
54
55impl InvalidStateCode {
56    #[inline]
57    #[must_use]
58    pub const fn index(self) -> u16 {
59        self as u16
60    }
61
62    #[inline]
63    #[must_use]
64    pub const fn message(self) -> &'static str {
65        match self {
66            Self::Unknown => "invalid state",
67            Self::WithdrawalQueueHeadMismatch => "withdrawal queue head mismatch",
68            Self::FeeMintOverflowTotalSupply => "fee minting would overflow total_supply",
69            Self::WithdrawalQueueCacheOverflow => "withdrawal queue cache overflow",
70            Self::WithdrawalQueueMissingEntry => "withdrawal queue missing entry",
71            Self::UnexpectedEmptyQueue => "withdrawal queue unexpectedly empty",
72            Self::WithdrawalQueueInvariantViolation => "withdrawal queue invariant violation",
73            Self::DepositRequiresIdle => "deposit requires Idle",
74            Self::DepositOverflowTotalAssets => "deposit would overflow total_assets",
75            Self::DepositOverflowIdleAssets => "deposit would overflow idle_assets",
76            Self::MintOverflowTotalShares => "minting would overflow total_shares",
77            Self::RequestWithdrawRequiresIdle => "request_withdraw requires Idle",
78            Self::ExecuteWithdrawRequiresIdle => "execute_withdraw requires Idle",
79            Self::ExecuteWithdrawRequiresIdleUseCallbacks => {
80                "execute_withdraw requires Idle (use withdrawal callbacks to advance)"
81            }
82            Self::StartAllocationMustReturnAllocating => "start_allocation must return Allocating",
83            Self::AllocationPlanExceedsIdleAssets => "allocation plan exceeds idle_assets",
84            Self::SyncExternalRequiresActiveOp => "sync_external_assets requires active op",
85            Self::SyncExternalRequiresAllowedStates => {
86                "sync_external_assets requires Allocating/Withdrawing/Refreshing"
87            }
88            Self::SyncExternalOverflowIdlePlusExternal => {
89                "sync_external_assets overflow: idle + external exceeds u128"
90            }
91            Self::AbortRefreshingRequiresActiveOp => "abort_refreshing requires active op",
92            Self::AbortRefreshingRequiresRefreshing => "abort_refreshing requires Refreshing",
93            Self::AbortAllocatingRequiresAllocating => "abort_allocating requires Allocating",
94            Self::AbortAllocatingRestoreIdleMismatch => "abort_allocating restore_idle mismatch",
95            Self::AbortWithdrawingRequiresWithdrawing => "abort_withdrawing requires Withdrawing",
96            Self::AbortWithdrawingRefundMismatch => "abort_withdrawing refund_shares mismatch",
97            Self::SettlePayoutRequiresPayout => "settle_payout requires Payout",
98            Self::PayoutSuccessSettlementMismatch => "payout success settlement mismatch",
99            Self::PayoutBurnExceedsTotalShares => "payout burn exceeds total_shares",
100            Self::PayoutFailureSettlementMismatch => "payout failure settlement mismatch",
101            Self::PayoutFailureRestoreIdleMismatch => {
102                "payout failure restore_idle must equal payout.amount"
103            }
104            Self::RefreshFeesRequiresIdle => "refresh_fees requires Idle",
105            Self::FeeRefreshTimestampMustAdvance => "fee refresh timestamp must advance",
106            Self::EmergencyResetAlreadyIdle => "emergency_reset: vault is already Idle",
107            Self::AtomicWithdrawRequiresIdle => "atomic_withdraw requires Idle",
108            Self::AtomicWithdrawExceedsIdleAssets => "atomic_withdraw exceeds idle_assets",
109            Self::AtomicWithdrawBurnExceedsTotalShares => {
110                "atomic_withdraw burn exceeds total_shares"
111            }
112            Self::AtomicWithdrawTotalAssetsUnderflow => {
113                "atomic_withdraw would underflow total_assets"
114            }
115            Self::RebalanceWithdrawRequiresIdle => "rebalance_withdraw requires Idle",
116            Self::RebalanceWithdrawExceedsExternalAssets => {
117                "rebalance_withdraw exceeds external_assets"
118            }
119            Self::RebalanceWithdrawOverflowsIdleAssets => {
120                "rebalance_withdraw would overflow idle_assets"
121            }
122            Self::WithdrawalLiquidityBelowMinimum => {
123                "withdrawal liquidity below minimum payout amount"
124            }
125            Self::RequestWithdrawExpectedAssetsExceedTotalAssets => {
126                "request_withdraw expected assets exceed total_assets"
127            }
128        }
129    }
130}
131
132#[cfg(not(target_arch = "wasm32"))]
133impl core::fmt::Display for InvalidStateCode {
134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        f.write_str(self.message())
136    }
137}
138
139/// Indexed invalid-config reasons for stable wasm diagnostics.
140#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
141#[derive(Clone, Copy, PartialEq, Eq)]
142#[repr(u16)]
143pub enum InvalidConfigCode {
144    Unknown = 0,
145    MaxPendingWithdrawalsExceedsLimit = 1,
146}
147
148impl InvalidConfigCode {
149    #[inline]
150    #[must_use]
151    pub const fn index(self) -> u16 {
152        self as u16
153    }
154
155    #[inline]
156    #[must_use]
157    pub const fn message(self) -> &'static str {
158        match self {
159            Self::Unknown => "invalid config",
160            Self::MaxPendingWithdrawalsExceedsLimit => {
161                "max_pending_withdrawals exceeds MAX_PENDING"
162            }
163        }
164    }
165}
166
167#[cfg(not(target_arch = "wasm32"))]
168impl core::fmt::Display for InvalidConfigCode {
169    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
170        f.write_str(self.message())
171    }
172}
173
174#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
175#[derive(Clone, Copy, PartialEq, Eq)]
176#[repr(u32)]
177pub enum KernelErrorCode {
178    InvalidState = 1000,
179    OpIdMismatch = 1001,
180    Slippage = 1002,
181    MinWithdrawal = 1003,
182    QueueFull = 1004,
183    NoPendingWithdrawals = 1005,
184    Cooldown = 1006,
185    Transition = 1007,
186    NotImplemented = 1008,
187    Restricted = 1009,
188    InvalidConfig = 1010,
189    ZeroAmount = 1011,
190}
191
192impl KernelErrorCode {
193    #[inline]
194    #[must_use]
195    pub const fn index(self) -> u32 {
196        self as u32
197    }
198}
199
200const INVALID_STATE_INDEXED_BASE: u32 = 100_000;
201const INVALID_CONFIG_INDEXED_BASE: u32 = 101_000;
202
203#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
204#[derive(Clone, Copy, PartialEq, Eq)]
205pub enum KernelDiagnosticCode {
206    Base(KernelErrorCode),
207    InvalidState(InvalidStateCode),
208    InvalidConfig(InvalidConfigCode),
209}
210
211impl KernelDiagnosticCode {
212    #[inline]
213    #[must_use]
214    pub const fn family(self) -> KernelErrorCode {
215        match self {
216            Self::Base(code) => code,
217            Self::InvalidState(_) => KernelErrorCode::InvalidState,
218            Self::InvalidConfig(_) => KernelErrorCode::InvalidConfig,
219        }
220    }
221
222    #[inline]
223    #[must_use]
224    pub const fn family_code(self) -> u32 {
225        self.family().index()
226    }
227
228    #[inline]
229    #[must_use]
230    pub const fn detailed_code(self) -> u32 {
231        match self {
232            Self::Base(code) => code.index(),
233            Self::InvalidState(code) => INVALID_STATE_INDEXED_BASE + code.index() as u32,
234            Self::InvalidConfig(code) => INVALID_CONFIG_INDEXED_BASE + code.index() as u32,
235        }
236    }
237
238    #[inline]
239    #[must_use]
240    pub const fn index(self) -> u32 {
241        self.family_code()
242    }
243
244    #[inline]
245    #[must_use]
246    pub const fn indexed_code(self) -> u32 {
247        self.detailed_code()
248    }
249}
250
251impl From<KernelErrorCode> for KernelDiagnosticCode {
252    fn from(code: KernelErrorCode) -> Self {
253        Self::Base(code)
254    }
255}
256
257impl From<InvalidStateCode> for KernelDiagnosticCode {
258    fn from(code: InvalidStateCode) -> Self {
259        Self::InvalidState(code)
260    }
261}
262
263impl From<InvalidConfigCode> for KernelDiagnosticCode {
264    fn from(code: InvalidConfigCode) -> Self {
265        Self::InvalidConfig(code)
266    }
267}
268
269pub trait HasKernelDiagnosticCode {
270    fn diagnostic_code(&self) -> KernelDiagnosticCode;
271}
272
273/// Errors that can occur when applying kernel actions.
274#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
275#[derive(Clone, PartialEq, Eq)]
276pub enum KernelError {
277    InvalidState(InvalidStateCode),
278    OpIdMismatch {
279        expected: u64,
280        actual: u64,
281    },
282    Slippage {
283        min: u128,
284        actual: u128,
285    },
286    MinWithdrawal {
287        amount: u128,
288        min: u128,
289    },
290    QueueFull {
291        current: u32,
292        max: u32,
293    },
294    NoPendingWithdrawals,
295    Cooldown {
296        requested_at: u64,
297        now: u64,
298        cooldown_ns: u64,
299    },
300    Transition(TransitionError),
301    NotImplemented,
302    Restricted(RestrictionKind),
303    InvalidConfig(InvalidConfigCode),
304    ZeroAmount,
305}
306
307impl KernelError {
308    #[inline]
309    #[must_use]
310    pub const fn diagnostic_code(&self) -> KernelDiagnosticCode {
311        match self {
312            Self::InvalidState(code) => KernelDiagnosticCode::InvalidState(*code),
313            Self::OpIdMismatch { .. } => KernelDiagnosticCode::Base(KernelErrorCode::OpIdMismatch),
314            Self::Slippage { .. } => KernelDiagnosticCode::Base(KernelErrorCode::Slippage),
315            Self::MinWithdrawal { .. } => {
316                KernelDiagnosticCode::Base(KernelErrorCode::MinWithdrawal)
317            }
318            Self::QueueFull { .. } => KernelDiagnosticCode::Base(KernelErrorCode::QueueFull),
319            Self::NoPendingWithdrawals => {
320                KernelDiagnosticCode::Base(KernelErrorCode::NoPendingWithdrawals)
321            }
322            Self::Cooldown { .. } => KernelDiagnosticCode::Base(KernelErrorCode::Cooldown),
323            Self::Transition(_) => KernelDiagnosticCode::Base(KernelErrorCode::Transition),
324            Self::NotImplemented => KernelDiagnosticCode::Base(KernelErrorCode::NotImplemented),
325            Self::Restricted(_) => KernelDiagnosticCode::Base(KernelErrorCode::Restricted),
326            Self::InvalidConfig(code) => KernelDiagnosticCode::InvalidConfig(*code),
327            Self::ZeroAmount => KernelDiagnosticCode::Base(KernelErrorCode::ZeroAmount),
328        }
329    }
330
331    #[inline]
332    #[must_use]
333    pub const fn family(&self) -> KernelErrorCode {
334        self.diagnostic_code().family()
335    }
336
337    #[inline]
338    #[must_use]
339    pub const fn family_code(&self) -> u32 {
340        self.diagnostic_code().family_code()
341    }
342
343    #[inline]
344    #[must_use]
345    pub const fn detailed_code(&self) -> u32 {
346        self.diagnostic_code().detailed_code()
347    }
348}
349
350impl From<&KernelError> for KernelDiagnosticCode {
351    fn from(error: &KernelError) -> Self {
352        error.diagnostic_code()
353    }
354}
355
356impl HasKernelDiagnosticCode for KernelDiagnosticCode {
357    fn diagnostic_code(&self) -> KernelDiagnosticCode {
358        *self
359    }
360}
361
362impl HasKernelDiagnosticCode for KernelError {
363    fn diagnostic_code(&self) -> KernelDiagnosticCode {
364        KernelError::diagnostic_code(self)
365    }
366}
367
368impl HasKernelDiagnosticCode for &KernelError {
369    fn diagnostic_code(&self) -> KernelDiagnosticCode {
370        KernelError::diagnostic_code(self)
371    }
372}
373
374impl HasKernelDiagnosticCode for KernelErrorCode {
375    fn diagnostic_code(&self) -> KernelDiagnosticCode {
376        (*self).into()
377    }
378}
379
380impl HasKernelDiagnosticCode for InvalidStateCode {
381    fn diagnostic_code(&self) -> KernelDiagnosticCode {
382        (*self).into()
383    }
384}
385
386impl HasKernelDiagnosticCode for InvalidConfigCode {
387    fn diagnostic_code(&self) -> KernelDiagnosticCode {
388        (*self).into()
389    }
390}
391
392impl From<InvalidStateCode> for KernelError {
393    fn from(code: InvalidStateCode) -> Self {
394        Self::InvalidState(code)
395    }
396}
397
398impl From<InvalidConfigCode> for KernelError {
399    fn from(code: InvalidConfigCode) -> Self {
400        Self::InvalidConfig(code)
401    }
402}
403
404impl From<TransitionError> for KernelError {
405    fn from(error: TransitionError) -> Self {
406        Self::Transition(error)
407    }
408}
409
410#[cfg(not(target_arch = "wasm32"))]
411impl core::fmt::Display for KernelError {
412    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
413        match self {
414            Self::InvalidState(code) => write!(f, "{code} (code {})", self.detailed_code()),
415            Self::OpIdMismatch { expected, actual } => {
416                write!(f, "op id mismatch: expected {expected}, actual {actual}")
417            }
418            Self::Slippage { min, actual } => {
419                write!(f, "slippage exceeded: min {min}, actual {actual}")
420            }
421            Self::MinWithdrawal { amount, min } => {
422                write!(f, "minimum withdrawal not met: amount {amount}, min {min}")
423            }
424            Self::QueueFull { current, max } => {
425                write!(f, "withdrawal queue full: current {current}, max {max}")
426            }
427            Self::NoPendingWithdrawals => f.write_str("no pending withdrawals"),
428            Self::Cooldown {
429                requested_at,
430                now,
431                cooldown_ns,
432            } => write!(
433                f,
434                "cooldown active: requested_at {requested_at}, now {now}, cooldown_ns {cooldown_ns}"
435            ),
436            Self::Transition(error) => match error {
437                TransitionError::WrongState => f.write_str("transition error: wrong state"),
438                TransitionError::OpIdMismatch { expected, actual } => write!(
439                    f,
440                    "transition error: op id mismatch: expected {expected}, actual {actual}"
441                ),
442                TransitionError::EmptyAllocationPlan => {
443                    f.write_str("transition error: empty allocation plan")
444                }
445                TransitionError::EmptyRefreshPlan => {
446                    f.write_str("transition error: empty refresh plan")
447                }
448                TransitionError::ZeroWithdrawalAmount => {
449                    f.write_str("transition error: zero withdrawal amount")
450                }
451                TransitionError::ZeroEscrowShares => {
452                    f.write_str("transition error: zero escrow shares")
453                }
454                TransitionError::InvalidIndex { index, max } => {
455                    write!(f, "transition error: invalid index: index {index}, max {max}")
456                }
457                TransitionError::CollectionOverflow {
458                    collected,
459                    remaining,
460                } => write!(
461                    f,
462                    "transition error: collection overflow: collected {collected}, remaining {remaining}"
463                ),
464                TransitionError::AllocationOverflow {
465                    allocated,
466                    remaining,
467                } => write!(
468                    f,
469                    "transition error: allocation overflow: allocated {allocated}, remaining {remaining}"
470                ),
471                TransitionError::ZeroAllocationAmount => {
472                    f.write_str("transition error: zero allocation amount")
473                }
474                TransitionError::BurnExceedsEscrow { burn, escrow } => write!(
475                    f,
476                    "transition error: burn exceeds escrow: burn {burn}, escrow {escrow}"
477                ),
478                TransitionError::WithdrawalIncomplete {
479                    remaining,
480                    collected,
481                } => write!(
482                    f,
483                    "transition error: withdrawal incomplete: remaining {remaining}, collected {collected}"
484                ),
485            },
486            Self::NotImplemented => f.write_str("action not implemented"),
487            Self::Restricted(kind) => match kind {
488                RestrictionKind::Paused => f.write_str("restricted: paused"),
489                RestrictionKind::Blacklisted => f.write_str("restricted: blacklisted"),
490                RestrictionKind::NotWhitelisted => f.write_str("restricted: not whitelisted"),
491            },
492            Self::InvalidConfig(code) => write!(f, "{code} (code {})", self.detailed_code()),
493            Self::ZeroAmount => f.write_str("amount must be greater than zero"),
494        }
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::{
501        HasKernelDiagnosticCode, InvalidConfigCode, InvalidStateCode, KernelDiagnosticCode,
502        KernelError, KernelErrorCode,
503    };
504
505    #[test]
506    fn kernel_diagnostic_code_from_impls_map_to_expected_variants() {
507        assert_eq!(
508            KernelDiagnosticCode::from(KernelErrorCode::Slippage),
509            KernelDiagnosticCode::Base(KernelErrorCode::Slippage)
510        );
511        assert_eq!(
512            KernelDiagnosticCode::from(InvalidStateCode::DepositRequiresIdle),
513            KernelDiagnosticCode::InvalidState(InvalidStateCode::DepositRequiresIdle)
514        );
515        assert_eq!(
516            KernelDiagnosticCode::from(InvalidConfigCode::MaxPendingWithdrawalsExceedsLimit),
517            KernelDiagnosticCode::InvalidConfig(
518                InvalidConfigCode::MaxPendingWithdrawalsExceedsLimit,
519            )
520        );
521    }
522
523    #[test]
524    fn kernel_error_diagnostic_naming_aliases_match_existing_behavior() {
525        let error: KernelError = InvalidStateCode::DepositRequiresIdle.into();
526        let diagnostic = error.diagnostic_code();
527
528        assert_eq!(diagnostic.family(), KernelErrorCode::InvalidState);
529        assert_eq!(diagnostic.family(), error.family());
530        assert_eq!(diagnostic.family_code(), error.family_code());
531        assert_eq!(diagnostic.detailed_code(), error.detailed_code());
532    }
533
534    #[test]
535    fn has_kernel_diagnostic_code_trait_is_ergonomic_across_supported_types() {
536        let error: KernelError = InvalidConfigCode::MaxPendingWithdrawalsExceedsLimit.into();
537
538        assert_eq!(
539            KernelDiagnosticCode::from(&error),
540            HasKernelDiagnosticCode::diagnostic_code(&error)
541        );
542        assert_eq!(
543            HasKernelDiagnosticCode::diagnostic_code(&KernelErrorCode::InvalidConfig),
544            KernelDiagnosticCode::Base(KernelErrorCode::InvalidConfig)
545        );
546        assert_eq!(
547            HasKernelDiagnosticCode::diagnostic_code(
548                &InvalidConfigCode::MaxPendingWithdrawalsExceedsLimit,
549            ),
550            KernelDiagnosticCode::InvalidConfig(
551                InvalidConfigCode::MaxPendingWithdrawalsExceedsLimit,
552            )
553        );
554    }
555}