1extern crate alloc;
7
8use core::mem;
9
10use crate::effects::{KernelEffect, KernelEvent, WithdrawalSkipReason};
11use crate::error::{InvalidConfigCode, InvalidStateCode, KernelError};
12use crate::{
13 math::{
14 number::Number,
15 wad::{mul_div_ceil, mul_div_floor},
16 },
17 restrictions::{RestrictionKind, Restrictions},
18};
19use crate::{
20 state::{
21 op_state::{AllocationPlanEntry, OpState, PayoutState, TargetId},
22 queue::{compute_idle_settlement, is_past_cooldown, QueueError, WithdrawQueue},
23 vault::{FeeAccrualAnchor, VaultConfig, VaultState},
24 },
25 transitions::TransitionResult,
26};
27use crate::{
28 transitions::{start_withdrawal, TransitionError, WithdrawalRequest},
29 types::{Address, TimestampNs},
30};
31use alloc::vec;
32use alloc::vec::Vec;
33
34#[cfg(any(feature = "action-refresh-fees", test))]
35use crate::math::wad::{
36 compute_fee_shares_from_assets, compute_management_fee_shares, total_assets_for_fee_accrual,
37};
38#[cfg(any(feature = "action-recovery", test))]
39use crate::transitions::stop_withdrawal;
40
41#[cfg(any(feature = "action-allocation-lifecycle", test))]
42use crate::transitions::{complete_allocation, start_allocation};
43#[cfg(any(feature = "action-refresh-lifecycle", test))]
44use crate::transitions::{complete_refresh, start_refresh};
45
46#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
48#[derive(Clone, PartialEq, Eq)]
49pub struct KernelResult {
50 pub state: VaultState,
51 pub effects: Vec<KernelEffect>,
52}
53
54impl KernelResult {
55 #[must_use]
56 pub fn new(state: VaultState, effects: Vec<KernelEffect>) -> Self {
57 Self { state, effects }
58 }
59}
60
61#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
63#[derive(Clone, PartialEq, Eq)]
64pub enum PayoutOutcome {
65 Success,
66 Failure,
67}
68
69#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
71#[derive(Clone, PartialEq, Eq)]
72pub struct IdlePayoutPlan {
73 pub op_id: u64,
74 pub request_id: u64,
75 pub owner: Address,
76 pub receiver: Address,
77 pub assets_out: u128,
78 pub burn_shares: u128,
79}
80
81#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
82#[derive(Clone, PartialEq, Eq)]
83enum WithdrawalQueueOutcome {
84 None,
85 CoolingDown { requested_at_ns: TimestampNs },
86 InsufficientLiquidity,
87 Ready(WithdrawalRequest),
88}
89
90#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
91#[derive(Clone, Copy, PartialEq, Eq)]
92pub(crate) struct PendingWithdrawalHead {
93 pub(crate) id: u64,
94 pub(crate) owner: Address,
95 pub(crate) receiver: Address,
96 pub(crate) escrow_shares: u128,
97 pub(crate) expected_assets: u128,
98 pub(crate) requested_at_ns: TimestampNs,
99}
100
101#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
102#[derive(Clone, PartialEq, Eq)]
103enum WithdrawalHeadOutcome {
104 Skip(WithdrawalSkipReason),
105 CoolingDown { requested_at_ns: TimestampNs },
106 InsufficientLiquidity,
107 Ready,
108}
109
110#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
111#[derive(Clone, Copy, PartialEq, Eq)]
112pub(crate) struct PayoutSettlement {
113 pub(crate) burn_shares: u128,
114 pub(crate) refund_shares: u128,
115 pub(crate) completed_amount: u128,
116 pub(crate) success: bool,
117}
118
119#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
120#[derive(Clone, PartialEq, Eq)]
121#[cfg(any(feature = "action-recovery", test))]
122pub(crate) struct EmergencyResetOutcome {
123 pub(crate) state: VaultState,
124 pub(crate) op_id: u64,
125 pub(crate) from_code: u32,
126 pub(crate) refund_owner: Option<Address>,
127 pub(crate) refund_shares: u128,
128}
129
130#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
131#[derive(Clone, Copy, PartialEq, Eq)]
132pub(crate) struct WithdrawalRequestPlan {
133 pub(crate) owner: Address,
134 pub(crate) receiver: Address,
135 pub(crate) shares: u128,
136 pub(crate) expected_assets: u128,
137}
138
139#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
140#[derive(Clone, Copy, PartialEq, Eq)]
141#[cfg(any(feature = "action-sync-external", test))]
142struct ExternalAssetSyncPlan {
143 new_external_assets: u128,
144 new_total_assets: u128,
145}
146
147pub fn plan_idle_payout(
152 state: &VaultState,
153 min_withdrawal_assets: u128,
154) -> Result<IdlePayoutPlan, KernelError> {
155 planning::plan_idle_payout(state, min_withdrawal_assets)
156}
157
158#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
163#[derive(Clone, PartialEq, Eq)]
164pub enum KernelAction {
165 BeginAllocating {
169 op_id: u64,
170 plan: Vec<AllocationPlanEntry>,
171 now_ns: TimestampNs,
172 },
173
174 Deposit {
176 owner: Address,
177 receiver: Address,
178 assets_in: u128,
179 min_shares_out: u128,
180 now_ns: TimestampNs,
181 },
182
183 AtomicWithdraw {
184 owner: Address,
185 receiver: Address,
186 operator: Address,
187 assets_out: u128,
188 max_shares_burned: u128,
189 now_ns: TimestampNs,
190 },
191
192 AtomicRedeem {
193 owner: Address,
194 receiver: Address,
195 operator: Address,
196 shares: u128,
197 min_assets_out: u128,
198 now_ns: TimestampNs,
199 },
200
201 RequestWithdraw {
203 owner: Address,
204 receiver: Address,
205 shares: u128,
206 min_assets_out: u128,
207 now_ns: TimestampNs,
208 },
209
210 ExecuteWithdraw { now_ns: TimestampNs },
214
215 BeginRefreshing {
219 op_id: u64,
220 plan: Vec<TargetId>,
221 now_ns: TimestampNs,
222 },
223
224 FinishAllocating { op_id: u64, now_ns: TimestampNs },
228
229 SyncExternalAssets {
231 new_external_assets: u128,
232 op_id: u64,
233 now_ns: TimestampNs,
234 },
235
236 RebalanceWithdraw {
237 op_id: u64,
238 amount: u128,
239 now_ns: TimestampNs,
240 },
241
242 FinishRefreshing { op_id: u64, now_ns: TimestampNs },
246
247 AbortRefreshing { op_id: u64 },
251
252 SettlePayout { op_id: u64, outcome: PayoutOutcome },
256
257 AbortAllocating { op_id: u64 },
261
262 AbortWithdrawing { op_id: u64 },
266
267 RefreshFees { now_ns: TimestampNs },
269
270 Pause { paused: bool },
272
273 EmergencyReset,
281}
282
283impl KernelAction {
284 #[must_use]
285 pub fn begin_allocating(
286 op_id: u64,
287 plan: Vec<AllocationPlanEntry>,
288 now_ns: TimestampNs,
289 ) -> Self {
290 Self::BeginAllocating {
291 op_id,
292 plan,
293 now_ns,
294 }
295 }
296
297 #[must_use]
298 pub fn deposit(
299 owner: Address,
300 receiver: Address,
301 assets_in: u128,
302 min_shares_out: u128,
303 now_ns: TimestampNs,
304 ) -> Self {
305 Self::Deposit {
306 owner,
307 receiver,
308 assets_in,
309 min_shares_out,
310 now_ns,
311 }
312 }
313
314 #[must_use]
315 pub fn atomic_withdraw(
316 owner: Address,
317 receiver: Address,
318 operator: Address,
319 assets_out: u128,
320 max_shares_burned: u128,
321 now_ns: TimestampNs,
322 ) -> Self {
323 Self::AtomicWithdraw {
324 owner,
325 receiver,
326 operator,
327 assets_out,
328 max_shares_burned,
329 now_ns,
330 }
331 }
332
333 #[must_use]
334 pub fn atomic_redeem(
335 owner: Address,
336 receiver: Address,
337 operator: Address,
338 shares: u128,
339 min_assets_out: u128,
340 now_ns: TimestampNs,
341 ) -> Self {
342 Self::AtomicRedeem {
343 owner,
344 receiver,
345 operator,
346 shares,
347 min_assets_out,
348 now_ns,
349 }
350 }
351
352 #[must_use]
353 pub fn request_withdraw(
354 owner: Address,
355 receiver: Address,
356 shares: u128,
357 min_assets_out: u128,
358 now_ns: TimestampNs,
359 ) -> Self {
360 Self::RequestWithdraw {
361 owner,
362 receiver,
363 shares,
364 min_assets_out,
365 now_ns,
366 }
367 }
368
369 #[must_use]
370 pub fn execute_withdraw(now_ns: TimestampNs) -> Self {
371 Self::ExecuteWithdraw { now_ns }
372 }
373
374 #[must_use]
375 pub fn begin_refreshing(op_id: u64, plan: Vec<TargetId>, now_ns: TimestampNs) -> Self {
376 Self::BeginRefreshing {
377 op_id,
378 plan,
379 now_ns,
380 }
381 }
382
383 #[must_use]
384 pub fn finish_allocating(op_id: u64, now_ns: TimestampNs) -> Self {
385 Self::FinishAllocating { op_id, now_ns }
386 }
387
388 #[must_use]
389 pub fn sync_external_assets(
390 new_external_assets: u128,
391 op_id: u64,
392 now_ns: TimestampNs,
393 ) -> Self {
394 Self::SyncExternalAssets {
395 new_external_assets,
396 op_id,
397 now_ns,
398 }
399 }
400
401 #[must_use]
402 pub fn rebalance_withdraw(op_id: u64, amount: u128, now_ns: TimestampNs) -> Self {
403 Self::RebalanceWithdraw {
404 op_id,
405 amount,
406 now_ns,
407 }
408 }
409
410 #[must_use]
411 pub fn finish_refreshing(op_id: u64, now_ns: TimestampNs) -> Self {
412 Self::FinishRefreshing { op_id, now_ns }
413 }
414
415 #[must_use]
416 pub fn abort_refreshing(op_id: u64) -> Self {
417 Self::AbortRefreshing { op_id }
418 }
419
420 #[must_use]
421 pub fn settle_payout(op_id: u64, outcome: PayoutOutcome) -> Self {
422 Self::SettlePayout { op_id, outcome }
423 }
424
425 #[must_use]
426 pub fn abort_allocating(op_id: u64) -> Self {
427 Self::AbortAllocating { op_id }
428 }
429
430 #[must_use]
431 pub fn abort_withdrawing(op_id: u64) -> Self {
432 Self::AbortWithdrawing { op_id }
433 }
434
435 #[must_use]
436 pub fn refresh_fees(now_ns: TimestampNs) -> Self {
437 Self::RefreshFees { now_ns }
438 }
439
440 #[must_use]
441 pub fn pause(paused: bool) -> Self {
442 Self::Pause { paused }
443 }
444
445 #[must_use]
446 pub const fn emergency_reset() -> Self {
447 Self::EmergencyReset
448 }
449
450 #[must_use]
451 pub const fn op_id(&self) -> Option<u64> {
452 match self {
453 Self::BeginAllocating { op_id, .. }
454 | Self::BeginRefreshing { op_id, .. }
455 | Self::FinishAllocating { op_id, .. }
456 | Self::SyncExternalAssets { op_id, .. }
457 | Self::RebalanceWithdraw { op_id, .. }
458 | Self::FinishRefreshing { op_id, .. }
459 | Self::AbortRefreshing { op_id }
460 | Self::SettlePayout { op_id, .. }
461 | Self::AbortAllocating { op_id, .. }
462 | Self::AbortWithdrawing { op_id, .. } => Some(*op_id),
463 Self::Deposit { .. }
464 | Self::AtomicWithdraw { .. }
465 | Self::AtomicRedeem { .. }
466 | Self::RequestWithdraw { .. }
467 | Self::ExecuteWithdraw { .. }
468 | Self::RefreshFees { .. }
469 | Self::Pause { .. }
470 | Self::EmergencyReset => None,
471 }
472 }
473
474 #[must_use]
475 pub const fn timestamp_ns(&self) -> Option<TimestampNs> {
476 match self {
477 Self::BeginAllocating { now_ns, .. }
478 | Self::Deposit { now_ns, .. }
479 | Self::AtomicWithdraw { now_ns, .. }
480 | Self::AtomicRedeem { now_ns, .. }
481 | Self::RequestWithdraw { now_ns, .. }
482 | Self::ExecuteWithdraw { now_ns }
483 | Self::BeginRefreshing { now_ns, .. }
484 | Self::FinishAllocating { now_ns, .. }
485 | Self::SyncExternalAssets { now_ns, .. }
486 | Self::RebalanceWithdraw { now_ns, .. }
487 | Self::FinishRefreshing { now_ns, .. }
488 | Self::RefreshFees { now_ns } => Some(*now_ns),
489 Self::AbortRefreshing { .. }
490 | Self::SettlePayout { .. }
491 | Self::AbortAllocating { .. }
492 | Self::AbortWithdrawing { .. }
493 | Self::Pause { .. }
494 | Self::EmergencyReset => None,
495 }
496 }
497}
498
499#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
503#[derive(Clone, Copy, PartialEq, Eq)]
504pub struct EffectiveTotals {
505 pub supply: u128,
506 pub assets: u128,
507}
508
509pub fn effective_totals(state: &VaultState, config: &VaultConfig) -> EffectiveTotals {
511 conversions::effective_totals(state, config)
512}
513
514pub fn convert_to_shares(state: &VaultState, config: &VaultConfig, assets: u128) -> u128 {
516 conversions::convert_to_shares(state, config, assets)
517}
518
519pub fn convert_to_assets(state: &VaultState, config: &VaultConfig, shares: u128) -> u128 {
521 conversions::convert_to_assets(state, config, shares)
522}
523
524pub fn convert_to_shares_ceil(state: &VaultState, config: &VaultConfig, assets: u128) -> u128 {
528 conversions::convert_to_shares_ceil(state, config, assets)
529}
530
531pub fn convert_to_assets_ceil(state: &VaultState, config: &VaultConfig, shares: u128) -> u128 {
535 conversions::convert_to_assets_ceil(state, config, shares)
536}
537
538pub fn convert_to_shares_bounded(
540 state: &VaultState,
541 config: &VaultConfig,
542 assets: u128,
543 cap: u128,
544 error: InvalidStateCode,
545) -> Result<u128, KernelError> {
546 conversions::convert_to_shares_bounded(state, config, assets, cap, error)
547}
548
549pub fn convert_to_assets_bounded(
551 state: &VaultState,
552 config: &VaultConfig,
553 shares: u128,
554 cap: u128,
555 error: InvalidStateCode,
556) -> Result<u128, KernelError> {
557 conversions::convert_to_assets_bounded(state, config, shares, cap, error)
558}
559
560pub fn convert_to_shares_ceil_bounded(
562 state: &VaultState,
563 config: &VaultConfig,
564 assets: u128,
565 cap: u128,
566 error: InvalidStateCode,
567) -> Result<u128, KernelError> {
568 conversions::convert_to_shares_ceil_bounded(state, config, assets, cap, error)
569}
570
571pub fn convert_to_assets_ceil_bounded(
573 state: &VaultState,
574 config: &VaultConfig,
575 shares: u128,
576 cap: u128,
577 error: InvalidStateCode,
578) -> Result<u128, KernelError> {
579 conversions::convert_to_assets_ceil_bounded(state, config, shares, cap, error)
580}
581
582#[inline]
584#[must_use]
585pub fn preview_deposit_shares(state: &VaultState, config: &VaultConfig, assets: u128) -> u128 {
586 convert_to_shares(state, config, assets)
587}
588
589#[inline]
591#[must_use]
592pub fn preview_withdraw_assets(state: &VaultState, config: &VaultConfig, shares: u128) -> u128 {
593 convert_to_assets(state, config, shares)
594}
595
596#[cfg(any(feature = "action-recovery", feature = "action-sync-external", test))]
597fn require_active_op_id(
598 op_state: &OpState,
599 provided: u64,
600 error_code: InvalidStateCode,
601) -> Result<(), KernelError> {
602 let active = match op_state.op_id() {
603 Some(active) => active,
604 None => return Err(KernelError::from(error_code)),
605 };
606 if active != provided {
607 return Err(KernelError::OpIdMismatch {
608 expected: active,
609 actual: provided,
610 });
611 }
612 Ok(())
613}
614
615#[inline]
617fn check_op_id(expected: u64, actual: u64) -> Result<(), KernelError> {
618 if expected != actual {
619 return Err(KernelError::OpIdMismatch { expected, actual });
620 }
621 Ok(())
622}
623
624pub(crate) fn validate_queue_head(
629 queue: &WithdrawQueue,
630 request_id: u64,
631 owner: &Address,
632 receiver: &Address,
633 escrow_shares: u128,
634) -> Result<(), KernelError> {
635 let Some((head_id, pending)) = queue.head() else {
636 return Err(KernelError::NoPendingWithdrawals);
637 };
638 if head_id != request_id
639 || pending.owner != *owner
640 || pending.receiver != *receiver
641 || pending.escrow_shares != escrow_shares
642 {
643 return Err(KernelError::from(
644 InvalidStateCode::WithdrawalQueueHeadMismatch,
645 ));
646 }
647 Ok(())
648}
649
650#[inline]
654fn push_refund_shares(
655 effects: &mut Vec<KernelEffect>,
656 escrow: Address,
657 owner: Address,
658 shares: u128,
659) {
660 if shares > 0 {
661 effects.push(KernelEffect::TransferShares {
662 from: escrow,
663 to: owner,
664 shares,
665 });
666 }
667}
668
669#[cfg(any(feature = "action-refresh-fees", test))]
670#[inline]
671fn mint_fee_shares(
672 effects: &mut Vec<KernelEffect>,
673 total_supply: &mut u128,
674 shares: Number,
675 recipient: Address,
676) -> Result<(), KernelError> {
677 if shares > Number::zero() {
678 let remaining = u128::MAX.saturating_sub(*total_supply);
679 if shares > Number::from(remaining) {
680 return Err(KernelError::from(
681 InvalidStateCode::FeeMintOverflowTotalSupply,
682 ));
683 }
684 let minted = shares.as_u128_trunc();
685 *total_supply = total_supply
686 .checked_add(minted)
687 .ok_or_else(|| KernelError::from(InvalidStateCode::FeeMintOverflowTotalSupply))?;
688 effects.push(KernelEffect::MintShares {
689 owner: recipient,
690 shares: minted,
691 });
692 }
693 Ok(())
694}
695
696#[inline]
697fn map_transition_result<T>(result: Result<T, TransitionError>) -> Result<T, KernelError> {
698 result.map_err(KernelError::Transition)
699}
700
701#[inline]
702fn apply_transition_result(
703 mut state: VaultState,
704 result: Result<TransitionResult, TransitionError>,
705) -> Result<KernelResult, KernelError> {
706 let result = map_transition_result(result)?;
707 state.op_state = result.new_state;
708 Ok(KernelResult::new(state, result.effects))
709}
710
711#[inline]
712fn map_queue_error(err: QueueError) -> KernelError {
713 match err {
714 QueueError::QueueFull { current, max } => KernelError::QueueFull { current, max },
715 QueueError::CacheOverflow => {
716 KernelError::from(InvalidStateCode::WithdrawalQueueCacheOverflow)
717 }
718 QueueError::WithdrawalNotFound { .. } => {
719 KernelError::from(InvalidStateCode::WithdrawalQueueMissingEntry)
720 }
721 QueueError::QueueEmpty => KernelError::from(InvalidStateCode::UnexpectedEmptyQueue),
722 QueueError::InvariantViolation { .. } => {
723 KernelError::from(InvalidStateCode::WithdrawalQueueInvariantViolation)
724 }
725 }
726}
727
728#[allow(clippy::too_many_arguments)]
730fn handle_deposit(
731 mut state: VaultState,
732 config: &VaultConfig,
733 restrictions: Option<&Restrictions>,
734 self_id: &Address,
735 owner: Address,
736 receiver: Address,
737 assets_in: u128,
738 min_shares_out: u128,
739 now_ns: TimestampNs,
740) -> Result<KernelResult, KernelError> {
741 enforce_restrictions(config, restrictions, self_id, &owner)?;
742 enforce_restrictions(config, restrictions, self_id, &receiver)?;
743 if !state.is_idle() {
744 return Err(KernelError::from(InvalidStateCode::DepositRequiresIdle));
745 }
746 if assets_in == 0 {
747 return Err(KernelError::ZeroAmount);
748 }
749
750 let mut effects = Vec::new();
751 #[cfg(any(feature = "action-refresh-fees", test))]
752 if should_refresh_fees_before_deposit(&state, config, now_ns) {
753 let mut refresh = handle_refresh_fees(state, config, now_ns)?;
754 state = refresh.state;
755 effects.append(&mut refresh.effects);
756 }
757
758 let shares_out = convert_to_shares_bounded(
759 &state,
760 config,
761 assets_in,
762 u128::MAX.saturating_sub(state.total_shares),
763 InvalidStateCode::MintOverflowTotalShares,
764 )?;
765 if shares_out == 0 {
766 return Err(KernelError::ZeroAmount);
767 }
768 if shares_out < min_shares_out {
769 return Err(KernelError::Slippage {
770 min: min_shares_out,
771 actual: shares_out,
772 });
773 }
774
775 state.total_assets = state
776 .total_assets
777 .checked_add(assets_in)
778 .ok_or_else(|| KernelError::from(InvalidStateCode::DepositOverflowTotalAssets))?;
779 state.idle_assets = state
780 .idle_assets
781 .checked_add(assets_in)
782 .ok_or_else(|| KernelError::from(InvalidStateCode::DepositOverflowIdleAssets))?;
783 state.total_shares = state
784 .total_shares
785 .checked_add(shares_out)
786 .ok_or_else(|| KernelError::from(InvalidStateCode::MintOverflowTotalShares))?;
787 state.fee_anchor = FeeAccrualAnchor::new(state.total_assets, now_ns);
788
789 effects.extend([
790 KernelEffect::TransferAssetsFrom {
791 from: owner,
792 to: *self_id,
793 amount: assets_in,
794 },
795 KernelEffect::MintShares {
796 owner: receiver,
797 shares: shares_out,
798 },
799 KernelEffect::EmitEvent {
800 event: crate::effects::KernelEvent::DepositProcessed {
801 owner,
802 receiver,
803 assets_in,
804 shares_out,
805 },
806 },
807 ]);
808
809 Ok(KernelResult::new(state, effects))
810}
811
812#[cfg(any(feature = "action-refresh-fees", test))]
813#[inline]
814fn should_refresh_fees_before_deposit(
815 state: &VaultState,
816 config: &VaultConfig,
817 now_ns: TimestampNs,
818) -> bool {
819 state.total_shares > 0
820 && config.fees.has_active_slot_fees()
821 && now_ns > state.fee_anchor.timestamp_ns
822}
823
824#[inline]
825fn push_atomic_burn_shares(
826 effects: &mut Vec<KernelEffect>,
827 owner: Address,
828 operator: Address,
829 shares: u128,
830) {
831 if operator == owner {
832 effects.push(KernelEffect::BurnShares { owner, shares });
833 } else {
834 effects.push(KernelEffect::BurnSharesFrom {
835 spender: operator,
836 owner,
837 shares,
838 });
839 }
840}
841
842#[inline]
843fn enforce_withdrawal_actors(
844 config: &VaultConfig,
845 restrictions: Option<&Restrictions>,
846 self_id: &Address,
847 owner: &Address,
848 receiver: &Address,
849) -> Result<(), KernelError> {
850 enforce_restrictions(config, restrictions, self_id, owner)?;
851 enforce_restrictions(config, restrictions, self_id, receiver)
852}
853
854#[inline]
855fn require_idle_with_nonzero_amount(
856 state: &VaultState,
857 idle_error: InvalidStateCode,
858 amount: u128,
859) -> Result<(), KernelError> {
860 if !state.is_idle() {
861 return Err(KernelError::from(idle_error));
862 }
863 if amount == 0 {
864 return Err(KernelError::ZeroAmount);
865 }
866 Ok(())
867}
868
869#[inline]
870fn restricted_withdraw_actor(
871 restrictions: Option<&Restrictions>,
872 self_id: &Address,
873 owner: &Address,
874 receiver: &Address,
875) -> Option<RestrictionKind> {
876 restrictions
877 .and_then(|r| r.is_restricted(owner))
878 .or_else(|| restrictions.and_then(|r| r.is_restricted_allowing_self(receiver, self_id)))
879}
880
881#[inline]
882fn pending_withdrawal_skip_reason(
883 restrictions: Option<&Restrictions>,
884 self_id: &Address,
885 owner: &Address,
886 receiver: &Address,
887 expected_assets: u128,
888) -> Option<WithdrawalSkipReason> {
889 if restricted_withdraw_actor(restrictions, self_id, owner, receiver).is_some() {
890 Some(WithdrawalSkipReason::Restricted)
891 } else if expected_assets == 0 {
892 Some(WithdrawalSkipReason::ZeroExpectedAssets)
893 } else {
894 None
895 }
896}
897
898fn dequeue_skipped_withdrawal(
899 state: &mut VaultState,
900 self_id: &Address,
901 skipped_effects: &mut Vec<KernelEffect>,
902 reason: WithdrawalSkipReason,
903) -> Result<(), KernelError> {
904 let (pending_id, pending) = state
905 .withdraw_queue
906 .dequeue()
907 .ok_or(KernelError::NoPendingWithdrawals)?;
908 push_refund_shares(
909 skipped_effects,
910 *self_id,
911 pending.owner,
912 pending.escrow_shares,
913 );
914 skipped_effects.push(KernelEffect::EmitEvent {
915 event: KernelEvent::WithdrawalSkipped {
916 id: pending_id,
917 owner: pending.owner,
918 receiver: pending.receiver,
919 escrow_shares: pending.escrow_shares,
920 expected_assets: pending.expected_assets,
921 reason,
922 },
923 });
924 Ok(())
925}
926
927#[inline]
928pub(crate) fn pending_withdrawal_head(state: &VaultState) -> Option<PendingWithdrawalHead> {
929 state
930 .withdraw_queue
931 .head()
932 .map(|(id, pending)| PendingWithdrawalHead {
933 id,
934 owner: pending.owner,
935 receiver: pending.receiver,
936 escrow_shares: pending.escrow_shares,
937 expected_assets: pending.expected_assets,
938 requested_at_ns: pending.requested_at_ns,
939 })
940}
941
942#[inline]
943fn classify_withdrawal_head(
944 head: PendingWithdrawalHead,
945 config: &VaultConfig,
946 restrictions: Option<&Restrictions>,
947 self_id: &Address,
948 now_ns: TimestampNs,
949 available_assets: u128,
950) -> WithdrawalHeadOutcome {
951 if let Some(reason) = pending_withdrawal_skip_reason(
952 restrictions,
953 self_id,
954 &head.owner,
955 &head.receiver,
956 head.expected_assets,
957 ) {
958 WithdrawalHeadOutcome::Skip(reason)
959 } else if !is_past_cooldown(head.requested_at_ns, now_ns, config.withdrawal_cooldown_ns) {
960 WithdrawalHeadOutcome::CoolingDown {
961 requested_at_ns: head.requested_at_ns,
962 }
963 } else if !has_actionable_withdrawal_liquidity(head.expected_assets, available_assets) {
964 WithdrawalHeadOutcome::InsufficientLiquidity
965 } else {
966 WithdrawalHeadOutcome::Ready
967 }
968}
969
970#[inline]
971fn has_actionable_withdrawal_liquidity(expected_assets: u128, available_assets: u128) -> bool {
972 available_assets >= expected_assets
973}
974
975#[inline]
976pub(crate) fn withdrawal_request_from_head(
977 state: &mut VaultState,
978 head: PendingWithdrawalHead,
979) -> WithdrawalRequest {
980 WithdrawalRequest {
981 op_id: state.allocate_op_id(),
982 request_id: head.id,
983 amount: head.expected_assets,
984 receiver: head.receiver,
985 owner: head.owner,
986 escrow_shares: head.escrow_shares,
987 }
988}
989
990#[inline]
991fn plan_withdrawal_request(
992 state: &VaultState,
993 config: &VaultConfig,
994 owner: Address,
995 receiver: Address,
996 shares: u128,
997 min_assets_out: u128,
998) -> Result<WithdrawalRequestPlan, KernelError> {
999 let expected_assets = convert_to_assets_bounded(
1000 state,
1001 config,
1002 shares,
1003 state.total_assets,
1004 InvalidStateCode::RequestWithdrawExpectedAssetsExceedTotalAssets,
1005 )?;
1006 if expected_assets < min_assets_out {
1007 return Err(KernelError::Slippage {
1008 min: min_assets_out,
1009 actual: expected_assets,
1010 });
1011 }
1012 if expected_assets < config.min_withdrawal_assets {
1013 return Err(KernelError::MinWithdrawal {
1014 amount: expected_assets,
1015 min: config.min_withdrawal_assets,
1016 });
1017 }
1018
1019 Ok(WithdrawalRequestPlan {
1020 owner,
1021 receiver,
1022 shares,
1023 expected_assets,
1024 })
1025}
1026
1027#[inline]
1028pub(crate) fn apply_withdrawal_request_plan(
1029 mut state: VaultState,
1030 config: &VaultConfig,
1031 self_id: &Address,
1032 request_plan: WithdrawalRequestPlan,
1033 now_ns: TimestampNs,
1034) -> Result<KernelResult, KernelError> {
1035 let id = state
1036 .withdraw_queue
1037 .enqueue(
1038 request_plan.owner,
1039 request_plan.receiver,
1040 request_plan.shares,
1041 request_plan.expected_assets,
1042 now_ns,
1043 config.max_pending_withdrawals,
1044 )
1045 .map_err(map_queue_error)?;
1046
1047 let effects = vec![
1048 KernelEffect::TransferShares {
1049 from: request_plan.owner,
1050 to: *self_id,
1051 shares: request_plan.shares,
1052 },
1053 KernelEffect::EmitEvent {
1054 event: crate::effects::KernelEvent::WithdrawalRequested {
1055 id,
1056 owner: request_plan.owner,
1057 receiver: request_plan.receiver,
1058 shares: request_plan.shares,
1059 expected_assets: request_plan.expected_assets,
1060 },
1061 },
1062 ];
1063
1064 Ok(KernelResult::new(state, effects))
1065}
1066
1067#[inline]
1068#[cfg(any(feature = "action-sync-external", test))]
1069fn ensure_sync_external_state_allowed(op_state: &OpState) -> Result<(), KernelError> {
1070 match op_state {
1071 OpState::Allocating(_) | OpState::Withdrawing(_) | OpState::Refreshing(_) => Ok(()),
1072 _ => Err(KernelError::from(
1073 InvalidStateCode::SyncExternalRequiresAllowedStates,
1074 )),
1075 }
1076}
1077
1078#[inline]
1079#[cfg(any(feature = "action-sync-external", test))]
1080fn plan_external_asset_sync(
1081 state: &VaultState,
1082 new_external_assets: u128,
1083) -> Result<ExternalAssetSyncPlan, KernelError> {
1084 let new_total_assets = state
1085 .idle_assets
1086 .checked_add(new_external_assets)
1087 .ok_or_else(|| KernelError::from(InvalidStateCode::SyncExternalOverflowIdlePlusExternal))?;
1088
1089 Ok(ExternalAssetSyncPlan {
1090 new_external_assets,
1091 new_total_assets,
1092 })
1093}
1094
1095fn next_withdrawal_queue_outcome(
1096 state: &mut VaultState,
1097 config: &VaultConfig,
1098 restrictions: Option<&Restrictions>,
1099 self_id: &Address,
1100 now_ns: TimestampNs,
1101 skipped_effects: &mut Vec<KernelEffect>,
1102) -> Result<WithdrawalQueueOutcome, KernelError> {
1103 loop {
1104 let Some(head) = pending_withdrawal_head(state) else {
1105 return Ok(WithdrawalQueueOutcome::None);
1106 };
1107
1108 match classify_withdrawal_head(
1109 head,
1110 config,
1111 restrictions,
1112 self_id,
1113 now_ns,
1114 state.idle_assets,
1115 ) {
1116 WithdrawalHeadOutcome::Skip(reason) => {
1117 dequeue_skipped_withdrawal(state, self_id, skipped_effects, reason)?;
1118 }
1119 WithdrawalHeadOutcome::CoolingDown { requested_at_ns } => {
1120 return Ok(WithdrawalQueueOutcome::CoolingDown { requested_at_ns });
1121 }
1122 WithdrawalHeadOutcome::InsufficientLiquidity => {
1123 return Ok(WithdrawalQueueOutcome::InsufficientLiquidity);
1124 }
1125 WithdrawalHeadOutcome::Ready => {
1126 return Ok(WithdrawalQueueOutcome::Ready(withdrawal_request_from_head(
1127 state, head,
1128 )));
1129 }
1130 }
1131 }
1132}
1133
1134#[allow(clippy::too_many_arguments)]
1135fn handle_atomic_withdraw(
1136 mut state: VaultState,
1137 config: &VaultConfig,
1138 restrictions: Option<&Restrictions>,
1139 self_id: &Address,
1140 owner: Address,
1141 receiver: Address,
1142 operator: Address,
1143 assets_out: u128,
1144 max_shares_burned: u128,
1145) -> Result<KernelResult, KernelError> {
1146 enforce_withdrawal_actors(config, restrictions, self_id, &owner, &receiver)?;
1147 require_idle_with_nonzero_amount(
1148 &state,
1149 InvalidStateCode::AtomicWithdrawRequiresIdle,
1150 assets_out,
1151 )?;
1152
1153 let shares = convert_to_shares_ceil_bounded(
1154 &state,
1155 config,
1156 assets_out,
1157 state.total_shares,
1158 InvalidStateCode::AtomicWithdrawBurnExceedsTotalShares,
1159 )?;
1160 if shares == 0 {
1161 return Err(KernelError::ZeroAmount);
1162 }
1163 if shares > max_shares_burned {
1164 return Err(KernelError::Slippage {
1165 min: max_shares_burned,
1166 actual: shares,
1167 });
1168 }
1169
1170 if assets_out > state.idle_assets {
1171 return Err(KernelError::from(
1172 InvalidStateCode::AtomicWithdrawExceedsIdleAssets,
1173 ));
1174 }
1175 state.total_shares = state
1176 .total_shares
1177 .checked_sub(shares)
1178 .ok_or_else(|| KernelError::from(InvalidStateCode::AtomicWithdrawBurnExceedsTotalShares))?;
1179 state.idle_assets = state
1180 .idle_assets
1181 .checked_sub(assets_out)
1182 .ok_or_else(|| KernelError::from(InvalidStateCode::AtomicWithdrawExceedsIdleAssets))?;
1183 state.total_assets = state
1184 .total_assets
1185 .checked_sub(assets_out)
1186 .ok_or_else(|| KernelError::from(InvalidStateCode::AtomicWithdrawTotalAssetsUnderflow))?;
1187
1188 let mut effects = Vec::new();
1189 push_atomic_burn_shares(&mut effects, owner, operator, shares);
1190 effects.push(KernelEffect::TransferAssets {
1191 to: receiver,
1192 amount: assets_out,
1193 });
1194 effects.push(KernelEffect::EmitEvent {
1195 event: KernelEvent::AtomicWithdrawProcessed {
1196 owner,
1197 receiver,
1198 shares_burned: shares,
1199 assets_out,
1200 },
1201 });
1202 Ok(KernelResult::new(state, effects))
1203}
1204
1205#[allow(clippy::too_many_arguments)]
1206fn handle_atomic_redeem(
1207 mut state: VaultState,
1208 config: &VaultConfig,
1209 restrictions: Option<&Restrictions>,
1210 self_id: &Address,
1211 owner: Address,
1212 receiver: Address,
1213 operator: Address,
1214 shares: u128,
1215 min_assets_out: u128,
1216) -> Result<KernelResult, KernelError> {
1217 enforce_withdrawal_actors(config, restrictions, self_id, &owner, &receiver)?;
1218 require_idle_with_nonzero_amount(&state, InvalidStateCode::AtomicWithdrawRequiresIdle, shares)?;
1219
1220 let assets_out = convert_to_assets_bounded(
1221 &state,
1222 config,
1223 shares,
1224 state.idle_assets,
1225 InvalidStateCode::AtomicWithdrawExceedsIdleAssets,
1226 )?;
1227 if assets_out == 0 {
1228 return Err(KernelError::ZeroAmount);
1229 }
1230 if assets_out < min_assets_out {
1231 return Err(KernelError::Slippage {
1232 min: min_assets_out,
1233 actual: assets_out,
1234 });
1235 }
1236 if assets_out > state.idle_assets {
1237 return Err(KernelError::from(
1238 InvalidStateCode::AtomicWithdrawExceedsIdleAssets,
1239 ));
1240 }
1241
1242 state.total_shares = state
1243 .total_shares
1244 .checked_sub(shares)
1245 .ok_or_else(|| KernelError::from(InvalidStateCode::AtomicWithdrawBurnExceedsTotalShares))?;
1246 state.idle_assets = state
1247 .idle_assets
1248 .checked_sub(assets_out)
1249 .ok_or_else(|| KernelError::from(InvalidStateCode::AtomicWithdrawExceedsIdleAssets))?;
1250 state.total_assets = state
1251 .total_assets
1252 .checked_sub(assets_out)
1253 .ok_or_else(|| KernelError::from(InvalidStateCode::AtomicWithdrawTotalAssetsUnderflow))?;
1254
1255 let mut effects = Vec::new();
1256 push_atomic_burn_shares(&mut effects, owner, operator, shares);
1257 effects.push(KernelEffect::TransferAssets {
1258 to: receiver,
1259 amount: assets_out,
1260 });
1261 effects.push(KernelEffect::EmitEvent {
1262 event: KernelEvent::AtomicWithdrawProcessed {
1263 owner,
1264 receiver,
1265 shares_burned: shares,
1266 assets_out,
1267 },
1268 });
1269 Ok(KernelResult::new(state, effects))
1270}
1271
1272#[allow(clippy::too_many_arguments)]
1274fn handle_request_withdraw(
1275 state: VaultState,
1276 config: &VaultConfig,
1277 restrictions: Option<&Restrictions>,
1278 self_id: &Address,
1279 owner: Address,
1280 receiver: Address,
1281 shares: u128,
1282 min_assets_out: u128,
1283 now_ns: TimestampNs,
1284) -> Result<KernelResult, KernelError> {
1285 if !config.is_max_pending_valid() {
1286 return Err(KernelError::from(
1287 InvalidConfigCode::MaxPendingWithdrawalsExceedsLimit,
1288 ));
1289 }
1290
1291 enforce_withdrawal_actors(config, restrictions, self_id, &owner, &receiver)?;
1292 require_idle_with_nonzero_amount(
1293 &state,
1294 InvalidStateCode::RequestWithdrawRequiresIdle,
1295 shares,
1296 )?;
1297
1298 let request_plan =
1299 plan_withdrawal_request(&state, config, owner, receiver, shares, min_assets_out)?;
1300
1301 apply_withdrawal_request_plan(state, config, self_id, request_plan, now_ns)
1302}
1303
1304fn handle_execute_withdraw(
1306 mut state: VaultState,
1307 config: &VaultConfig,
1308 restrictions: Option<&Restrictions>,
1309 self_id: &Address,
1310 now_ns: TimestampNs,
1311) -> Result<KernelResult, KernelError> {
1312 if !state.op_state.is_idle() {
1313 let error_code = if state.op_state.is_withdrawing() {
1314 InvalidStateCode::ExecuteWithdrawRequiresIdleUseCallbacks
1315 } else {
1316 InvalidStateCode::ExecuteWithdrawRequiresIdle
1317 };
1318 return Err(KernelError::from(error_code));
1319 }
1320
1321 if is_globally_paused(config, restrictions) {
1322 return Err(KernelError::Restricted(RestrictionKind::Paused));
1323 }
1324
1325 let mut skipped_effects = Vec::new();
1326 match next_withdrawal_queue_outcome(
1327 &mut state,
1328 config,
1329 restrictions,
1330 self_id,
1331 now_ns,
1332 &mut skipped_effects,
1333 )? {
1334 WithdrawalQueueOutcome::None => {
1335 if skipped_effects.is_empty() {
1336 Err(KernelError::NoPendingWithdrawals)
1337 } else {
1338 Ok(KernelResult::new(state, skipped_effects))
1339 }
1340 }
1341 WithdrawalQueueOutcome::CoolingDown { requested_at_ns } => {
1342 if skipped_effects.is_empty() {
1343 Err(KernelError::Cooldown {
1344 requested_at: requested_at_ns.into(),
1345 now: now_ns.into(),
1346 cooldown_ns: config.withdrawal_cooldown_ns,
1347 })
1348 } else {
1349 Ok(KernelResult::new(state, skipped_effects))
1350 }
1351 }
1352 WithdrawalQueueOutcome::InsufficientLiquidity => {
1353 if skipped_effects.is_empty() {
1354 Err(KernelError::from(
1355 InvalidStateCode::WithdrawalLiquidityBelowMinimum,
1356 ))
1357 } else {
1358 Ok(KernelResult::new(state, skipped_effects))
1359 }
1360 }
1361 WithdrawalQueueOutcome::Ready(request) => {
1362 let transition = start_withdrawal(mem::take(&mut state.op_state), request);
1363 let mut result = apply_transition_result(state, transition)?;
1364 skipped_effects.append(&mut result.effects);
1365 result.effects = skipped_effects;
1366 Ok(result)
1367 }
1368 }
1369}
1370
1371#[cfg(any(feature = "action-allocation-lifecycle", test))]
1373fn handle_begin_allocating(
1374 mut state: VaultState,
1375 op_id: u64,
1376 plan: Vec<AllocationPlanEntry>,
1377) -> Result<KernelResult, KernelError> {
1378 let result = map_transition_result(start_allocation(
1379 mem::take(&mut state.op_state),
1380 plan,
1381 op_id,
1382 ))?;
1383
1384 let alloc_total = match result.new_state.as_allocating() {
1386 Some(allocating) => allocating.remaining,
1387 None => {
1388 return Err(KernelError::from(
1389 InvalidStateCode::StartAllocationMustReturnAllocating,
1390 ))
1391 }
1392 };
1393
1394 if alloc_total > state.idle_assets {
1395 return Err(KernelError::from(
1396 InvalidStateCode::AllocationPlanExceedsIdleAssets,
1397 ));
1398 }
1399
1400 state.idle_assets -= alloc_total;
1401 state.sync_total_assets();
1402 state.op_state = result.new_state;
1403 Ok(KernelResult::new(state, result.effects))
1404}
1405
1406#[cfg(any(feature = "action-allocation-lifecycle", test))]
1408fn handle_finish_allocating(
1409 mut state: VaultState,
1410 _config: &VaultConfig,
1411 _restrictions: Option<&Restrictions>,
1412 _self_id: &Address,
1413 op_id: u64,
1414 _now_ns: TimestampNs,
1415) -> Result<KernelResult, KernelError> {
1416 let transition = complete_allocation(mem::take(&mut state.op_state), op_id, None);
1417 apply_transition_result(state, transition)
1418}
1419
1420#[cfg(any(feature = "action-sync-external", test))]
1421fn handle_sync_external_assets(
1422 mut state: VaultState,
1423 new_external_assets: u128,
1424 op_id: u64,
1425) -> Result<KernelResult, KernelError> {
1426 require_active_op_id(
1427 &state.op_state,
1428 op_id,
1429 InvalidStateCode::SyncExternalRequiresActiveOp,
1430 )?;
1431
1432 ensure_sync_external_state_allowed(&state.op_state)?;
1433 let sync_plan = plan_external_asset_sync(&state, new_external_assets)?;
1434
1435 state.external_assets = sync_plan.new_external_assets;
1436 state.total_assets = sync_plan.new_total_assets;
1437
1438 let total_assets = state.total_assets;
1439 Ok(KernelResult::new(
1440 state,
1441 vec![KernelEffect::EmitEvent {
1442 event: crate::effects::KernelEvent::ExternalAssetsSynced {
1443 op_id,
1444 new_external_assets: sync_plan.new_external_assets,
1445 total_assets,
1446 },
1447 }],
1448 ))
1449}
1450
1451#[cfg(any(feature = "action-sync-external", test))]
1452fn handle_rebalance_withdraw(
1453 mut state: VaultState,
1454 op_id: u64,
1455 amount: u128,
1456) -> Result<KernelResult, KernelError> {
1457 match &state.op_state {
1458 OpState::Idle => {}
1459 OpState::Allocating(_) => {
1460 require_active_op_id(
1461 &state.op_state,
1462 op_id,
1463 InvalidStateCode::SyncExternalRequiresActiveOp,
1464 )?;
1465 }
1466 _ => {
1467 return Err(KernelError::from(
1468 InvalidStateCode::RebalanceWithdrawRequiresIdle,
1469 ));
1470 }
1471 }
1472
1473 if amount > state.external_assets {
1474 return Err(KernelError::from(
1475 InvalidStateCode::RebalanceWithdrawExceedsExternalAssets,
1476 ));
1477 }
1478
1479 state.external_assets -= amount;
1480 state.idle_assets = state
1481 .idle_assets
1482 .checked_add(amount)
1483 .ok_or_else(|| KernelError::from(InvalidStateCode::RebalanceWithdrawOverflowsIdleAssets))?;
1484 state.sync_total_assets();
1485
1486 let new_external_assets = state.external_assets;
1487 let total_assets = state.total_assets;
1488 Ok(KernelResult::new(
1489 state,
1490 vec![KernelEffect::EmitEvent {
1491 event: crate::effects::KernelEvent::ExternalAssetsSynced {
1492 op_id,
1493 new_external_assets,
1494 total_assets,
1495 },
1496 }],
1497 ))
1498}
1499
1500#[cfg(any(feature = "action-recovery", test))]
1501fn handle_abort_refreshing(mut state: VaultState, op_id: u64) -> Result<KernelResult, KernelError> {
1502 require_active_op_id(
1503 &state.op_state,
1504 op_id,
1505 InvalidStateCode::AbortRefreshingRequiresActiveOp,
1506 )?;
1507
1508 if !matches!(state.op_state, OpState::Refreshing(_)) {
1509 return Err(KernelError::from(
1510 InvalidStateCode::AbortRefreshingRequiresRefreshing,
1511 ));
1512 }
1513
1514 state.op_state = OpState::Idle;
1515 Ok(KernelResult::new(state, vec![]))
1516}
1517
1518#[cfg(any(feature = "action-recovery", test))]
1519fn handle_abort_allocating(mut state: VaultState, op_id: u64) -> Result<KernelResult, KernelError> {
1520 let alloc = match &state.op_state {
1521 OpState::Allocating(s) => s,
1522 _ => {
1523 return Err(KernelError::from(
1524 InvalidStateCode::AbortAllocatingRequiresAllocating,
1525 ))
1526 }
1527 };
1528
1529 check_op_id(alloc.op_id, op_id)?;
1530 state.restore_to_idle(alloc.remaining);
1531 state.op_state = OpState::Idle;
1532 Ok(KernelResult::new(state, vec![]))
1533}
1534
1535#[cfg(any(feature = "action-recovery", test))]
1536fn handle_abort_withdrawing(
1537 mut state: VaultState,
1538 self_id: &Address,
1539 op_id: u64,
1540) -> Result<KernelResult, KernelError> {
1541 let withdraw = match &state.op_state {
1542 OpState::Withdrawing(s) => s,
1543 _ => {
1544 return Err(KernelError::from(
1545 InvalidStateCode::AbortWithdrawingRequiresWithdrawing,
1546 ))
1547 }
1548 };
1549
1550 check_op_id(withdraw.op_id, op_id)?;
1551 validate_queue_head(
1552 &state.withdraw_queue,
1553 withdraw.request_id,
1554 &withdraw.owner,
1555 &withdraw.receiver,
1556 withdraw.escrow_shares,
1557 )?;
1558
1559 state.restore_to_idle(withdraw.collected);
1560
1561 let result = map_transition_result(stop_withdrawal(
1562 mem::take(&mut state.op_state),
1563 op_id,
1564 *self_id,
1565 ))?;
1566 state.op_state = result.new_state;
1567 state.withdraw_queue.dequeue();
1568 Ok(KernelResult::new(state, result.effects))
1569}
1570
1571#[inline]
1572pub(crate) fn plan_payout_settlement(
1573 payout: &PayoutState,
1574 outcome: PayoutOutcome,
1575) -> Result<PayoutSettlement, KernelError> {
1576 match outcome {
1577 PayoutOutcome::Success => {
1578 let burn_shares = payout.burn_shares;
1579 let refund_shares = payout
1580 .escrow_shares
1581 .checked_sub(payout.burn_shares)
1582 .ok_or_else(|| {
1583 KernelError::from(InvalidStateCode::PayoutSuccessSettlementMismatch)
1584 })?;
1585 Ok(PayoutSettlement {
1586 burn_shares,
1587 refund_shares,
1588 completed_amount: payout.amount,
1589 success: true,
1590 })
1591 }
1592 PayoutOutcome::Failure => Ok(PayoutSettlement {
1593 burn_shares: 0,
1594 refund_shares: payout.escrow_shares,
1595 completed_amount: 0,
1596 success: false,
1597 }),
1598 }
1599}
1600
1601pub(crate) fn apply_payout_settlement(
1602 state: &mut VaultState,
1603 payout: &PayoutState,
1604 settlement: PayoutSettlement,
1605 escrow_address: Address,
1606 effects: &mut Vec<KernelEffect>,
1607) -> Result<(), KernelError> {
1608 if settlement.burn_shares > 0 {
1609 effects.push(KernelEffect::BurnShares {
1610 owner: escrow_address,
1611 shares: settlement.burn_shares,
1612 });
1613 state.total_shares = state
1614 .total_shares
1615 .checked_sub(settlement.burn_shares)
1616 .ok_or_else(|| KernelError::from(InvalidStateCode::PayoutBurnExceedsTotalShares))?;
1617 }
1618
1619 push_refund_shares(
1620 effects,
1621 escrow_address,
1622 payout.owner,
1623 settlement.refund_shares,
1624 );
1625
1626 if settlement.success {
1627 state.idle_assets = state
1628 .idle_assets
1629 .checked_sub(payout.amount)
1630 .ok_or_else(|| KernelError::from(InvalidStateCode::PayoutFailureRestoreIdleMismatch))?;
1631 state.sync_total_assets();
1632 }
1633
1634 state.op_state = OpState::Idle;
1635 Ok(())
1636}
1637
1638fn handle_settle_payout(
1640 mut state: VaultState,
1641 self_id: &Address,
1642 op_id: u64,
1643 outcome: PayoutOutcome,
1644) -> Result<KernelResult, KernelError> {
1645 let payout = match mem::take(&mut state.op_state) {
1646 OpState::Payout(s) => s,
1647 _ => {
1648 return Err(KernelError::from(
1649 InvalidStateCode::SettlePayoutRequiresPayout,
1650 ))
1651 }
1652 };
1653
1654 check_op_id(payout.op_id, op_id)?;
1655
1656 validate_queue_head(
1657 &state.withdraw_queue,
1658 payout.request_id,
1659 &payout.owner,
1660 &payout.receiver,
1661 payout.escrow_shares,
1662 )?;
1663
1664 let escrow_address = *self_id;
1665 let mut effects = Vec::new();
1666
1667 let settlement = plan_payout_settlement(&payout, outcome)?;
1668 apply_payout_settlement(
1669 &mut state,
1670 &payout,
1671 settlement,
1672 escrow_address,
1673 &mut effects,
1674 )?;
1675
1676 effects.push(KernelEffect::EmitEvent {
1677 event: KernelEvent::PayoutCompleted {
1678 op_id,
1679 success: settlement.success,
1680 burn_shares: settlement.burn_shares,
1681 refund_shares: settlement.refund_shares,
1682 amount: settlement.completed_amount,
1683 },
1684 });
1685
1686 state.withdraw_queue.dequeue();
1687 Ok(KernelResult::new(state, effects))
1688}
1689
1690#[cfg(any(feature = "action-refresh-fees", test))]
1691fn handle_refresh_fees(
1692 mut state: VaultState,
1693 config: &VaultConfig,
1694 now_ns: TimestampNs,
1695) -> Result<KernelResult, KernelError> {
1696 if !state.is_idle() {
1697 return Err(KernelError::from(InvalidStateCode::RefreshFeesRequiresIdle));
1698 }
1699
1700 if now_ns <= state.fee_anchor.timestamp_ns {
1702 return Err(KernelError::from(
1703 InvalidStateCode::FeeRefreshTimestampMustAdvance,
1704 ));
1705 }
1706
1707 let cur_total_assets = state.total_assets;
1708 let mut total_supply = state.total_shares;
1709 let anchor = state.fee_anchor;
1710 let mut effects = Vec::new();
1711
1712 if total_supply > 0 && anchor.is_uninitialized() && cur_total_assets == 0 {
1713 state.fee_anchor = FeeAccrualAnchor::new(cur_total_assets, now_ns);
1714 effects.push(KernelEffect::EmitEvent {
1715 event: crate::effects::KernelEvent::FeesRefreshed {
1716 now_ns: now_ns.into(),
1717 total_assets: cur_total_assets,
1718 },
1719 });
1720 return Ok(KernelResult::new(state, effects));
1721 }
1722
1723 let fee_total_assets = total_assets_for_fee_accrual(
1725 cur_total_assets,
1726 anchor.total_assets,
1727 anchor.timestamp_ns.into(),
1728 now_ns.into(),
1729 config.fees.max_total_assets_growth_rate,
1730 );
1731
1732 let mgmt_shares = compute_management_fee_shares(
1734 fee_total_assets,
1735 cur_total_assets,
1736 total_supply,
1737 config.fees.management.fee_wad,
1738 anchor.timestamp_ns.into(),
1739 now_ns.into(),
1740 );
1741 mint_fee_shares(
1742 &mut effects,
1743 &mut total_supply,
1744 mgmt_shares,
1745 config.fees.management.recipient,
1746 )?;
1747
1748 let profit = fee_total_assets.saturating_sub(anchor.total_assets);
1750 let fee_assets = config
1751 .fees
1752 .performance
1753 .fee_wad
1754 .apply_floored(Number::from(profit));
1755 let perf_shares = compute_fee_shares_from_assets(
1756 fee_assets,
1757 Number::from(cur_total_assets),
1758 Number::from(total_supply),
1759 );
1760 mint_fee_shares(
1761 &mut effects,
1762 &mut total_supply,
1763 perf_shares,
1764 config.fees.performance.recipient,
1765 )?;
1766
1767 state.total_shares = total_supply;
1768 state.fee_anchor = FeeAccrualAnchor::new(cur_total_assets, now_ns);
1769
1770 effects.push(KernelEffect::EmitEvent {
1771 event: crate::effects::KernelEvent::FeesRefreshed {
1772 now_ns: now_ns.into(),
1773 total_assets: cur_total_assets,
1774 },
1775 });
1776
1777 Ok(KernelResult::new(state, effects))
1778}
1779
1780#[cfg(any(feature = "action-recovery", test))]
1781pub(crate) fn plan_emergency_reset(
1782 mut state: VaultState,
1783) -> Result<EmergencyResetOutcome, KernelError> {
1784 let prev_state = mem::take(&mut state.op_state);
1785 let from_code = prev_state.kind_code();
1786 let op_id = match prev_state.op_id() {
1787 Some(op_id) => op_id,
1788 None => {
1789 return Err(KernelError::from(
1790 InvalidStateCode::EmergencyResetAlreadyIdle,
1791 ))
1792 }
1793 };
1794
1795 let mut refund_owner = None;
1796 let mut refund_shares = 0;
1797
1798 match prev_state {
1799 OpState::Idle => {
1800 return Err(KernelError::from(
1801 InvalidStateCode::EmergencyResetAlreadyIdle,
1802 ))
1803 }
1804 OpState::Refreshing(_) => {
1805 }
1807 OpState::Allocating(alloc) => {
1808 state.restore_to_idle(alloc.remaining);
1810 }
1811 OpState::Withdrawing(w) => {
1812 refund_owner = Some(w.owner);
1813 refund_shares = w.escrow_shares;
1814 state.restore_to_idle(w.collected);
1816 state.withdraw_queue.dequeue();
1817 }
1818 OpState::Payout(p) => {
1819 refund_owner = Some(p.owner);
1820 refund_shares = p.escrow_shares;
1821 state.restore_to_idle(p.amount);
1823 state.withdraw_queue.dequeue();
1824 }
1825 }
1826
1827 state.op_state = OpState::Idle;
1828 state.fee_anchor = FeeAccrualAnchor::new(state.total_assets, state.fee_anchor.timestamp_ns);
1829
1830 Ok(EmergencyResetOutcome {
1831 state,
1832 op_id,
1833 from_code,
1834 refund_owner,
1835 refund_shares,
1836 })
1837}
1838
1839#[cfg(any(feature = "action-recovery", test))]
1840pub(crate) fn handle_emergency_reset(
1841 state: VaultState,
1842 self_id: &Address,
1843) -> Result<KernelResult, KernelError> {
1844 let outcome = plan_emergency_reset(state)?;
1845 let mut effects = Vec::new();
1846 if let Some(owner) = outcome.refund_owner {
1847 push_refund_shares(&mut effects, *self_id, owner, outcome.refund_shares);
1848 }
1849 effects.push(KernelEffect::EmitEvent {
1850 event: KernelEvent::EmergencyResetCompleted {
1851 op_id: outcome.op_id,
1852 from_state: outcome.from_code,
1853 },
1854 });
1855
1856 Ok(KernelResult::new(outcome.state, effects))
1857}
1858
1859#[allow(unused_mut)]
1861pub fn apply_action(
1862 mut state: VaultState,
1863 config: &VaultConfig,
1864 restrictions: Option<&Restrictions>,
1865 self_id: &Address,
1866 action: KernelAction,
1867) -> Result<KernelResult, KernelError> {
1868 dispatch::apply_action(state, config, restrictions, self_id, action)
1869}
1870
1871fn enforce_restrictions(
1872 config: &VaultConfig,
1873 restrictions: Option<&Restrictions>,
1874 self_id: &Address,
1875 actor: &Address,
1876) -> Result<(), KernelError> {
1877 access::enforce_restrictions(config, restrictions, self_id, actor)
1878}
1879
1880#[inline]
1881fn is_globally_paused(config: &VaultConfig, restrictions: Option<&Restrictions>) -> bool {
1882 let _ = restrictions;
1883 config.paused
1884}
1885
1886mod planning {
1887 use super::*;
1888
1889 pub(super) fn plan_idle_payout(
1890 state: &VaultState,
1891 min_withdrawal_assets: u128,
1892 ) -> Result<IdlePayoutPlan, KernelError> {
1893 let (request_owner, request_receiver, request_escrow, request_expected) = state
1894 .withdraw_queue
1895 .head()
1896 .map(|(_, request)| {
1897 (
1898 request.owner,
1899 request.receiver,
1900 request.escrow_shares,
1901 request.expected_assets,
1902 )
1903 })
1904 .ok_or_else(|| KernelError::from(InvalidStateCode::UnexpectedEmptyQueue))?;
1905
1906 let withdrawing = match &state.op_state {
1907 OpState::Withdrawing(withdrawing) => withdrawing,
1908 _ => {
1909 return Err(KernelError::from(
1910 InvalidStateCode::ExecuteWithdrawRequiresIdleUseCallbacks,
1911 ))
1912 }
1913 };
1914
1915 if request_owner != withdrawing.owner
1916 || request_receiver != withdrawing.receiver
1917 || request_escrow != withdrawing.escrow_shares
1918 {
1919 return Err(KernelError::from(
1920 InvalidStateCode::WithdrawalQueueHeadMismatch,
1921 ));
1922 }
1923
1924 let available_assets = state.idle_assets;
1925 let _ = min_withdrawal_assets;
1926 if !has_actionable_withdrawal_liquidity(request_expected, available_assets) {
1927 return Err(KernelError::from(
1928 InvalidStateCode::WithdrawalLiquidityBelowMinimum,
1929 ));
1930 }
1931
1932 let Some(settlement) =
1933 compute_idle_settlement(request_escrow, request_expected, state.idle_assets)
1934 else {
1935 return Err(KernelError::from(
1936 InvalidStateCode::WithdrawalLiquidityBelowMinimum,
1937 ));
1938 };
1939
1940 if settlement.assets_out == 0 {
1941 return Err(KernelError::from(
1942 InvalidStateCode::WithdrawalLiquidityBelowMinimum,
1943 ));
1944 }
1945
1946 Ok(IdlePayoutPlan {
1947 op_id: withdrawing.op_id,
1948 request_id: withdrawing.request_id,
1949 owner: withdrawing.owner,
1950 receiver: withdrawing.receiver,
1951 assets_out: settlement.assets_out,
1952 burn_shares: settlement.settlement.to_burn,
1953 })
1954 }
1955}
1956
1957mod conversions {
1958 use super::*;
1959
1960 pub(super) fn effective_totals(state: &VaultState, config: &VaultConfig) -> EffectiveTotals {
1961 EffectiveTotals {
1962 supply: state
1963 .total_shares
1964 .saturating_add(config.virtual_shares.max(1)),
1965 assets: state
1966 .total_assets
1967 .saturating_add(config.virtual_assets.max(1)),
1968 }
1969 }
1970
1971 pub(super) fn convert_to_shares(
1972 state: &VaultState,
1973 config: &VaultConfig,
1974 assets: u128,
1975 ) -> u128 {
1976 let t = effective_totals(state, config);
1977 u128::from(mul_div_floor(
1978 Number::from(assets),
1979 Number::from(t.supply),
1980 Number::from(t.assets),
1981 ))
1982 }
1983
1984 pub(super) fn convert_to_shares_bounded(
1985 state: &VaultState,
1986 config: &VaultConfig,
1987 assets: u128,
1988 cap: u128,
1989 error: InvalidStateCode,
1990 ) -> Result<u128, KernelError> {
1991 let t = effective_totals(state, config);
1992 mul_div_floor_bounded_u128(assets, t.supply, t.assets, cap, error)
1993 }
1994
1995 pub(super) fn convert_to_assets(
1996 state: &VaultState,
1997 config: &VaultConfig,
1998 shares: u128,
1999 ) -> u128 {
2000 let t = effective_totals(state, config);
2001 u128::from(mul_div_floor(
2002 Number::from(shares),
2003 Number::from(t.assets),
2004 Number::from(t.supply),
2005 ))
2006 }
2007
2008 pub(super) fn convert_to_assets_bounded(
2009 state: &VaultState,
2010 config: &VaultConfig,
2011 shares: u128,
2012 cap: u128,
2013 error: InvalidStateCode,
2014 ) -> Result<u128, KernelError> {
2015 let t = effective_totals(state, config);
2016 mul_div_floor_bounded_u128(shares, t.assets, t.supply, cap, error)
2017 }
2018
2019 pub(super) fn convert_to_shares_ceil(
2020 state: &VaultState,
2021 config: &VaultConfig,
2022 assets: u128,
2023 ) -> u128 {
2024 let t = effective_totals(state, config);
2025 u128::from(mul_div_ceil(
2026 Number::from(assets),
2027 Number::from(t.supply),
2028 Number::from(t.assets),
2029 ))
2030 }
2031
2032 pub(super) fn convert_to_shares_ceil_bounded(
2033 state: &VaultState,
2034 config: &VaultConfig,
2035 assets: u128,
2036 cap: u128,
2037 error: InvalidStateCode,
2038 ) -> Result<u128, KernelError> {
2039 let t = effective_totals(state, config);
2040 mul_div_ceil_bounded_u128(assets, t.supply, t.assets, cap, error)
2041 }
2042
2043 pub(super) fn convert_to_assets_ceil(
2044 state: &VaultState,
2045 config: &VaultConfig,
2046 shares: u128,
2047 ) -> u128 {
2048 let t = effective_totals(state, config);
2049 u128::from(mul_div_ceil(
2050 Number::from(shares),
2051 Number::from(t.assets),
2052 Number::from(t.supply),
2053 ))
2054 }
2055
2056 pub(super) fn convert_to_assets_ceil_bounded(
2057 state: &VaultState,
2058 config: &VaultConfig,
2059 shares: u128,
2060 cap: u128,
2061 error: InvalidStateCode,
2062 ) -> Result<u128, KernelError> {
2063 let t = effective_totals(state, config);
2064 mul_div_ceil_bounded_u128(shares, t.assets, t.supply, cap, error)
2065 }
2066
2067 fn mul_div_floor_bounded_u128(
2068 x: u128,
2069 y: u128,
2070 denominator: u128,
2071 cap: u128,
2072 error: InvalidStateCode,
2073 ) -> Result<u128, KernelError> {
2074 bounded_u128(
2075 mul_div_floor(Number::from(x), Number::from(y), Number::from(denominator)),
2076 cap,
2077 error,
2078 )
2079 }
2080
2081 fn mul_div_ceil_bounded_u128(
2082 x: u128,
2083 y: u128,
2084 denominator: u128,
2085 cap: u128,
2086 error: InvalidStateCode,
2087 ) -> Result<u128, KernelError> {
2088 bounded_u128(
2089 mul_div_ceil(Number::from(x), Number::from(y), Number::from(denominator)),
2090 cap,
2091 error,
2092 )
2093 }
2094
2095 fn bounded_u128(
2096 quotient: Number,
2097 cap: u128,
2098 error: InvalidStateCode,
2099 ) -> Result<u128, KernelError> {
2100 if quotient > Number::from(cap) {
2101 return Err(KernelError::from(error));
2102 }
2103 Ok(quotient.as_u128_trunc())
2104 }
2105}
2106
2107mod access {
2108 use super::*;
2109
2110 pub(super) fn enforce_restrictions(
2111 config: &VaultConfig,
2112 restrictions: Option<&Restrictions>,
2113 _self_id: &Address,
2114 actor: &Address,
2115 ) -> Result<(), KernelError> {
2116 if config.paused {
2117 return Err(KernelError::Restricted(RestrictionKind::Paused));
2118 }
2119 if let Some(restrictions) = restrictions {
2120 if let Some(kind) = restrictions.is_restricted(actor) {
2121 return Err(KernelError::Restricted(kind));
2122 }
2123 }
2124 Ok(())
2125 }
2126}
2127
2128mod dispatch {
2129 use super::*;
2130
2131 #[allow(unused_mut)]
2132 #[allow(clippy::too_many_lines)]
2133 pub(super) fn apply_action(
2134 mut state: VaultState,
2135 config: &VaultConfig,
2136 restrictions: Option<&Restrictions>,
2137 self_id: &Address,
2138 action: KernelAction,
2139 ) -> Result<KernelResult, KernelError> {
2140 match action {
2141 KernelAction::Deposit {
2142 owner,
2143 receiver,
2144 assets_in,
2145 min_shares_out,
2146 now_ns,
2147 } => handle_deposit(
2148 state,
2149 config,
2150 restrictions,
2151 self_id,
2152 owner,
2153 receiver,
2154 assets_in,
2155 min_shares_out,
2156 now_ns,
2157 ),
2158
2159 KernelAction::AtomicWithdraw {
2160 owner,
2161 receiver,
2162 operator,
2163 assets_out,
2164 max_shares_burned,
2165 now_ns: _,
2166 } => handle_atomic_withdraw(
2167 state,
2168 config,
2169 restrictions,
2170 self_id,
2171 owner,
2172 receiver,
2173 operator,
2174 assets_out,
2175 max_shares_burned,
2176 ),
2177
2178 KernelAction::AtomicRedeem {
2179 owner,
2180 receiver,
2181 operator,
2182 shares,
2183 min_assets_out,
2184 now_ns: _,
2185 } => handle_atomic_redeem(
2186 state,
2187 config,
2188 restrictions,
2189 self_id,
2190 owner,
2191 receiver,
2192 operator,
2193 shares,
2194 min_assets_out,
2195 ),
2196
2197 KernelAction::RequestWithdraw {
2198 owner,
2199 receiver,
2200 shares,
2201 min_assets_out,
2202 now_ns,
2203 } => handle_request_withdraw(
2204 state,
2205 config,
2206 restrictions,
2207 self_id,
2208 owner,
2209 receiver,
2210 shares,
2211 min_assets_out,
2212 now_ns,
2213 ),
2214
2215 KernelAction::ExecuteWithdraw { now_ns } => {
2216 handle_execute_withdraw(state, config, restrictions, self_id, now_ns)
2217 }
2218
2219 #[cfg(any(feature = "action-allocation-lifecycle", test))]
2220 KernelAction::BeginAllocating { op_id, plan, .. } => {
2221 handle_begin_allocating(state, op_id, plan)
2222 }
2223 #[cfg(not(any(feature = "action-allocation-lifecycle", test)))]
2224 KernelAction::BeginAllocating { .. } => Err(KernelError::NotImplemented),
2225
2226 #[cfg(any(feature = "action-allocation-lifecycle", test))]
2227 KernelAction::FinishAllocating { op_id, now_ns } => {
2228 handle_finish_allocating(state, config, restrictions, self_id, op_id, now_ns)
2229 }
2230 #[cfg(not(any(feature = "action-allocation-lifecycle", test)))]
2231 KernelAction::FinishAllocating { .. } => Err(KernelError::NotImplemented),
2232
2233 #[cfg(any(feature = "action-refresh-lifecycle", test))]
2234 KernelAction::BeginRefreshing { op_id, plan, .. } => {
2235 let transition = start_refresh(mem::take(&mut state.op_state), plan, op_id);
2236 apply_transition_result(state, transition)
2237 }
2238 #[cfg(not(any(feature = "action-refresh-lifecycle", test)))]
2239 KernelAction::BeginRefreshing { .. } => Err(KernelError::NotImplemented),
2240
2241 #[cfg(any(feature = "action-refresh-lifecycle", test))]
2242 KernelAction::FinishRefreshing { op_id, .. } => {
2243 let transition = complete_refresh(mem::take(&mut state.op_state), op_id);
2244 apply_transition_result(state, transition)
2245 }
2246 #[cfg(not(any(feature = "action-refresh-lifecycle", test)))]
2247 KernelAction::FinishRefreshing { .. } => Err(KernelError::NotImplemented),
2248
2249 #[cfg(any(feature = "action-sync-external", test))]
2250 KernelAction::SyncExternalAssets {
2251 new_external_assets,
2252 op_id,
2253 ..
2254 } => handle_sync_external_assets(state, new_external_assets, op_id),
2255 #[cfg(not(any(feature = "action-sync-external", test)))]
2256 KernelAction::SyncExternalAssets { .. } => Err(KernelError::NotImplemented),
2257
2258 #[cfg(any(feature = "action-sync-external", test))]
2259 KernelAction::RebalanceWithdraw { op_id, amount, .. } => {
2260 handle_rebalance_withdraw(state, op_id, amount)
2261 }
2262 #[cfg(not(any(feature = "action-sync-external", test)))]
2263 KernelAction::RebalanceWithdraw { .. } => Err(KernelError::NotImplemented),
2264
2265 #[cfg(any(feature = "action-recovery", test))]
2266 KernelAction::AbortRefreshing { op_id } => handle_abort_refreshing(state, op_id),
2267 #[cfg(not(any(feature = "action-recovery", test)))]
2268 KernelAction::AbortRefreshing { .. } => Err(KernelError::NotImplemented),
2269
2270 #[cfg(any(feature = "action-recovery", test))]
2271 KernelAction::AbortAllocating { op_id } => handle_abort_allocating(state, op_id),
2272 #[cfg(not(any(feature = "action-recovery", test)))]
2273 KernelAction::AbortAllocating { .. } => Err(KernelError::NotImplemented),
2274
2275 #[cfg(any(feature = "action-recovery", test))]
2276 KernelAction::AbortWithdrawing { op_id } => {
2277 handle_abort_withdrawing(state, self_id, op_id)
2278 }
2279 #[cfg(not(any(feature = "action-recovery", test)))]
2280 KernelAction::AbortWithdrawing { .. } => Err(KernelError::NotImplemented),
2281
2282 KernelAction::SettlePayout { op_id, outcome } => {
2283 handle_settle_payout(state, self_id, op_id, outcome)
2284 }
2285
2286 #[cfg(any(feature = "action-pause", test))]
2287 KernelAction::Pause { paused } => Ok(KernelResult::new(
2288 state,
2289 vec![KernelEffect::EmitEvent {
2290 event: crate::effects::KernelEvent::PauseUpdated { paused },
2291 }],
2292 )),
2293 #[cfg(not(any(feature = "action-pause", test)))]
2294 KernelAction::Pause { .. } => Err(KernelError::NotImplemented),
2295
2296 #[cfg(any(feature = "action-refresh-fees", test))]
2297 KernelAction::RefreshFees { now_ns } => handle_refresh_fees(state, config, now_ns),
2298 #[cfg(not(any(feature = "action-refresh-fees", test)))]
2299 KernelAction::RefreshFees { .. } => Err(KernelError::NotImplemented),
2300
2301 #[cfg(any(feature = "action-recovery", test))]
2302 KernelAction::EmergencyReset => handle_emergency_reset(state, self_id),
2303 #[cfg(not(any(feature = "action-recovery", test)))]
2304 KernelAction::EmergencyReset => Err(KernelError::NotImplemented),
2305 }
2306 }
2307}
2308
2309#[cfg(test)]
2312mod tests;