templar_vault_kernel/state/vault/
mod.rs

1//! Core vault state and configuration types.
2//!
3//! This module provides the chain-agnostic `VaultState` struct that holds
4//! all state required by the kernel, including the withdrawal queue.
5//! Executors are responsible for persisting this state to storage.
6
7use crate::fee::FeesSpec;
8use crate::state::op_state::OpState;
9use crate::state::queue::WithdrawQueue;
10use crate::types::TimestampNs;
11
12/// Maximum pending withdrawal queue length.
13/// This is an absolute upper bound enforced by the kernel.
14pub const MAX_PENDING: usize = 1024;
15
16/// Anchor point for fee accrual calculations.
17///
18/// Stores the total assets and timestamp at which fees were last accrued.
19/// Used to calculate time-weighted management fees and performance fees
20/// based on AUM growth.
21#[templar_vault_macros::vault_derive(borsh, serde)]
22#[derive(Clone, Copy, PartialEq, Eq)]
23pub struct FeeAccrualAnchor {
24    pub total_assets: u128,
25    pub timestamp_ns: TimestampNs,
26}
27
28impl FeeAccrualAnchor {
29    #[inline]
30    #[must_use]
31    pub const fn new(total_assets: u128, timestamp_ns: TimestampNs) -> Self {
32        Self {
33            total_assets,
34            timestamp_ns,
35        }
36    }
37
38    #[inline]
39    #[must_use]
40    pub const fn zero() -> Self {
41        Self {
42            total_assets: 0,
43            timestamp_ns: TimestampNs::ZERO,
44        }
45    }
46
47    #[inline]
48    #[must_use]
49    pub const fn is_uninitialized(self) -> bool {
50        self.total_assets == 0 && self.timestamp_ns.as_u64() == 0
51    }
52
53    #[inline]
54    pub fn update(&mut self, total_assets: u128, timestamp_ns: TimestampNs) {
55        self.total_assets = total_assets;
56        self.timestamp_ns = timestamp_ns;
57    }
58}
59
60impl Default for FeeAccrualAnchor {
61    fn default() -> Self {
62        Self::zero()
63    }
64}
65
66/// Static configuration for a vault.
67///
68/// These settings can typically only be changed through governance.
69///
70/// # Fee Recipients
71///
72/// Fee recipients are 32-byte addresses. Executors are responsible for mapping
73/// chain-native account identifiers (e.g., NEAR AccountId, Soroban Address) to
74/// this canonical 32-byte format, typically using a SHA256 hash.
75#[templar_vault_macros::vault_derive(borsh, serde)]
76#[derive(Clone, Copy, PartialEq, Eq)]
77pub struct VaultConfig {
78    pub fees: FeesSpec,
79    pub min_withdrawal_assets: u128,
80    pub withdrawal_cooldown_ns: u64,
81    pub max_pending_withdrawals: u32,
82    pub paused: bool,
83    pub virtual_shares: u128,
84    pub virtual_assets: u128,
85}
86
87impl VaultConfig {
88    #[inline]
89    #[must_use]
90    pub fn is_max_pending_valid(&self) -> bool {
91        (self.max_pending_withdrawals as usize) <= MAX_PENDING
92    }
93}
94
95/// Core kernel vault state.
96///
97/// This struct contains all the state managed by the kernel. Chain-specific
98/// executors are responsible for:
99/// - Persisting this state to storage
100/// - Handling share/asset token balances
101///
102/// # Invariants
103///
104/// - `total_assets == idle_assets + external_assets`
105/// - `withdraw_queue.check_invariants()`
106/// - `next_op_id` is monotonically increasing
107/// - Operations can only proceed when `op_state` allows them
108#[templar_vault_macros::vault_derive(borsh, serde)]
109#[derive(Clone, PartialEq, Eq)]
110pub struct VaultState {
111    pub total_assets: u128,
112    pub total_shares: u128,
113    pub idle_assets: u128,
114    pub external_assets: u128,
115    pub fee_anchor: FeeAccrualAnchor,
116    pub op_state: OpState,
117    pub withdraw_queue: WithdrawQueue,
118    pub next_op_id: u64,
119}
120
121impl VaultState {
122    #[inline]
123    #[must_use]
124    pub fn new() -> Self {
125        Self {
126            total_assets: 0,
127            total_shares: 0,
128            idle_assets: 0,
129            external_assets: 0,
130            fee_anchor: FeeAccrualAnchor::zero(),
131            op_state: OpState::Idle,
132            withdraw_queue: WithdrawQueue::new(),
133            next_op_id: 0,
134        }
135    }
136
137    #[inline]
138    #[must_use]
139    pub fn with_initial(
140        total_assets: u128,
141        total_shares: u128,
142        idle_assets: u128,
143        external_assets: u128,
144        timestamp_ns: TimestampNs,
145    ) -> Self {
146        let computed_total = crate::unwrap_abort!(
147            idle_assets.checked_add(external_assets),
148            crate::abort::OVERFLOW,
149        );
150        assert!(total_assets == computed_total);
151        Self {
152            total_assets,
153            total_shares,
154            idle_assets,
155            external_assets,
156            fee_anchor: FeeAccrualAnchor::new(total_assets, timestamp_ns),
157            op_state: OpState::Idle,
158            withdraw_queue: WithdrawQueue::new(),
159            next_op_id: 0,
160        }
161    }
162
163    /// Check the fundamental accounting invariant.
164    ///
165    /// Returns `true` if `total_assets == idle_assets + external_assets`.
166    #[inline]
167    #[must_use]
168    pub fn check_invariant(&self) -> bool {
169        self.idle_assets
170            .checked_add(self.external_assets)
171            .is_some_and(|sum| self.total_assets == sum)
172            && self.withdraw_queue.check_invariants()
173    }
174
175    /// Allocate and return the next operation ID.
176    ///
177    /// Increments `next_op_id` and returns the previous value.
178    #[inline]
179    pub fn allocate_op_id(&mut self) -> u64 {
180        let id = self.next_op_id;
181        self.next_op_id =
182            crate::unwrap_abort!(self.next_op_id.checked_add(1), crate::abort::OVERFLOW,);
183        id
184    }
185
186    /// Check if the vault is idle (no operation in progress).
187    #[inline]
188    #[must_use]
189    pub fn is_idle(&self) -> bool {
190        self.op_state.is_idle()
191    }
192
193    /// Get the current operation ID if an operation is in progress.
194    #[inline]
195    #[must_use]
196    pub fn current_op_id(&self) -> Option<u64> {
197        self.op_state.op_id()
198    }
199
200    /// Recompute `total_assets` from `idle_assets + external_assets`.
201    ///
202    /// Call this after any mutation of `idle_assets` or `external_assets`
203    /// to restore the fundamental accounting invariant.
204    #[inline]
205    pub fn sync_total_assets(&mut self) {
206        self.total_assets = crate::unwrap_abort!(
207            self.idle_assets.checked_add(self.external_assets),
208            crate::abort::OVERFLOW,
209        );
210    }
211
212    /// Add `amount` back to idle assets and recompute totals.
213    ///
214    /// Common pattern during abort / emergency-reset paths where
215    /// in-flight assets are returned to idle.
216    #[inline]
217    pub fn restore_to_idle(&mut self, amount: u128) {
218        self.idle_assets =
219            crate::unwrap_abort!(self.idle_assets.checked_add(amount), crate::abort::OVERFLOW,);
220        self.sync_total_assets();
221    }
222}
223
224impl Default for VaultState {
225    fn default() -> Self {
226        Self::new()
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::VaultState;
233
234    #[test]
235    #[should_panic]
236    fn with_initial_panics_on_overflowed_component_sum() {
237        let _ = VaultState::with_initial(u128::MAX, 0, u128::MAX, 1, crate::TimestampNs(0));
238    }
239
240    #[test]
241    #[should_panic]
242    fn allocate_op_id_panics_on_overflow() {
243        let mut state = VaultState::new();
244        state.next_op_id = u64::MAX;
245
246        let _ = state.allocate_op_id();
247    }
248
249    #[test]
250    #[should_panic]
251    fn sync_total_assets_panics_on_overflow() {
252        let mut state = VaultState::new();
253        state.idle_assets = u128::MAX;
254        state.external_assets = 1;
255
256        state.sync_total_assets();
257    }
258}