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