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 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#[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#[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}