1use crate::restrictions::RestrictionKind;
4use crate::transitions::TransitionError;
5
6#[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#[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#[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}