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