templar_vault_kernel/actions/
mod.rs

1//! Kernel action dispatch for vault state transitions.
2//!
3//! This module defines the public `KernelAction` enum and a dispatcher that
4//! applies actions to `VaultState` and returns effects.
5
6extern 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/// Result of applying a kernel action.
47#[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/// Outcome for payout settlement.
62#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
63#[derive(Clone, PartialEq, Eq)]
64pub enum PayoutOutcome {
65    Success,
66    Failure,
67}
68
69/// Planned payout details for satisfying a queued withdrawal from idle assets.
70#[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
147/// Plan an idle-funded payout from the current vault state.
148///
149/// Returns an error when the current idle liquidity cannot produce an actionable
150/// payout for the queue head.
151pub 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/// Kernel actions supported by the dispatcher.
159///
160/// These actions drive the vault state machine. Each action validates preconditions,
161/// updates state, and returns effects to be executed by the chain-specific runtime.
162#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
163#[derive(Clone, PartialEq, Eq)]
164pub enum KernelAction {
165    /// Begin allocating idle assets to external markets according to a plan.
166    ///
167    /// Transition: Idle -> Allocating
168    BeginAllocating {
169        op_id: u64,
170        plan: Vec<AllocationPlanEntry>,
171        now_ns: TimestampNs,
172    },
173
174    /// Deposit assets into the vault and mint shares to the receiver.
175    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    /// Request a withdrawal by escrowing shares in the queue.
202    RequestWithdraw {
203        owner: Address,
204        receiver: Address,
205        shares: u128,
206        min_assets_out: u128,
207        now_ns: TimestampNs,
208    },
209
210    /// Execute the next pending withdrawal from the queue.
211    ///
212    /// Transition: Idle -> Withdrawing
213    ExecuteWithdraw { now_ns: TimestampNs },
214
215    /// Begin refreshing external market balances.
216    ///
217    /// Transition: Idle -> Refreshing
218    BeginRefreshing {
219        op_id: u64,
220        plan: Vec<TargetId>,
221        now_ns: TimestampNs,
222    },
223
224    /// Complete an allocation operation.
225    ///
226    /// Transition: Allocating -> Idle or Withdrawing
227    FinishAllocating { op_id: u64, now_ns: TimestampNs },
228
229    /// Sync external asset balances during an active operation.
230    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    /// Complete a refresh operation.
243    ///
244    /// Transition: Refreshing -> Idle
245    FinishRefreshing { op_id: u64, now_ns: TimestampNs },
246
247    /// Abort a refresh operation (e.g., on external call failure).
248    ///
249    /// Transition: Refreshing -> Idle
250    AbortRefreshing { op_id: u64 },
251
252    /// Settle a payout after asset transfer attempt.
253    ///
254    /// Transition: Payout -> Idle
255    SettlePayout { op_id: u64, outcome: PayoutOutcome },
256
257    /// Abort an allocation operation (e.g., on external call failure).
258    ///
259    /// Transition: Allocating -> Idle
260    AbortAllocating { op_id: u64 },
261
262    /// Abort a withdrawal operation (e.g., on external call failure).
263    ///
264    /// Transition: Withdrawing -> Idle
265    AbortWithdrawing { op_id: u64 },
266
267    /// Refresh fee calculations and mint fee shares.
268    RefreshFees { now_ns: TimestampNs },
269
270    /// Emit a pause-state update for executor-owned pause configuration.
271    Pause { paused: bool },
272
273    /// Emergency reset: force the vault back to Idle from any non-Idle state.
274    ///
275    /// Unlike the regular abort actions, this does not require op_id matching.
276    /// For Withdrawing/Payout states, escrowed shares are refunded to the owner
277    /// and the queue head is dequeued.
278    ///
279    /// Authorization (Owner-only, timelock-gated) must be enforced by the executor.
280    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/// Effective totals after applying virtual share/asset offsets.
500///
501/// Named fields prevent callers from confusing supply vs assets.
502#[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
509/// Compute effective totals including virtual shares/assets for conversion math.
510pub fn effective_totals(state: &VaultState, config: &VaultConfig) -> EffectiveTotals {
511    conversions::effective_totals(state, config)
512}
513
514/// Convert an asset amount to shares (floor rounding — fewer shares, favors vault).
515pub fn convert_to_shares(state: &VaultState, config: &VaultConfig, assets: u128) -> u128 {
516    conversions::convert_to_shares(state, config, assets)
517}
518
519/// Convert a share amount to assets (floor rounding — fewer assets, favors vault).
520pub fn convert_to_assets(state: &VaultState, config: &VaultConfig, shares: u128) -> u128 {
521    conversions::convert_to_assets(state, config, shares)
522}
523
524/// Convert an asset amount to shares (ceil rounding — more shares, favors user).
525///
526/// Used by ERC-4626 `preview_withdraw` to compute shares burned (rounds against user).
527pub fn convert_to_shares_ceil(state: &VaultState, config: &VaultConfig, assets: u128) -> u128 {
528    conversions::convert_to_shares_ceil(state, config, assets)
529}
530
531/// Convert a share amount to assets (ceil rounding — more assets, favors user).
532///
533/// Used by ERC-4626 `preview_mint` to compute assets needed (rounds against user).
534pub fn convert_to_assets_ceil(state: &VaultState, config: &VaultConfig, shares: u128) -> u128 {
535    conversions::convert_to_assets_ceil(state, config, shares)
536}
537
538/// Convert assets to shares and reject quotients above the operation's legal cap.
539pub 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
549/// Convert shares to assets and reject quotients above the operation's legal cap.
550pub 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
560/// Convert assets to shares with ceil rounding and reject quotients above the operation's cap.
561pub 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
571/// Convert shares to assets with ceil rounding and reject quotients above the operation's cap.
572pub 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/// Preview the shares minted for a deposit of `assets` using kernel conversions.
583#[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/// Preview the assets redeemed for `shares` using kernel conversions.
590#[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/// Validate that a destructured op_id matches the provided one.
616#[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
624/// Validate that the withdrawal queue head matches the expected owner/receiver/escrow.
625///
626/// Used by both `AbortWithdrawing` and `SettlePayout` to ensure consistency
627/// between the op-state and the queue.
628pub(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/// Push a `TransferShares` effect to refund escrowed shares to an owner.
651///
652/// No-op if `shares` is zero.
653#[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/// Process a deposit: validate restrictions, convert assets→shares, update totals.
729#[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/// Enqueue a withdrawal request: validate, compute expected assets, escrow shares.
1273#[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
1304/// Execute the next queued withdrawal after cooldown.
1305fn 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/// Start an allocation: transition to Allocating and decrement idle assets.
1372#[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    // Compute allocation total from the plan and decrement idle_assets.
1385    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/// Finish an allocation without advancing the withdrawal queue.
1407#[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
1638/// Settle a payout after asset transfer attempt (success or failure).
1639fn 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    // Reject backwards time to prevent fee calculation issues
1701    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    // Cap effective total_assets for fee accrual (mitigates donation attacks)
1724    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    // Management fees (time-based, pro-rated over elapsed time)
1733    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    // Performance fees (profit-based)
1749    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            // No assets in-flight, just reset.
1806        }
1807        OpState::Allocating(alloc) => {
1808            // Restore unallocated assets back to idle.
1809            state.restore_to_idle(alloc.remaining);
1810        }
1811        OpState::Withdrawing(w) => {
1812            refund_owner = Some(w.owner);
1813            refund_shares = w.escrow_shares;
1814            // Restore any collected assets back to idle.
1815            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            // Restore payout amount back to idle.
1822            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/// Apply a kernel action to state, returning updated state and effects.
1860#[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// Tests
2310
2311#[cfg(test)]
2312mod tests;