templar_common/vault/
mod.rs

1use std::num::NonZeroU8;
2
3use derive_more::{Display, From, Into};
4
5use crate::{
6    asset::{BorrowAsset, FungibleAsset},
7    supply::SupplyPosition,
8};
9pub use external::*;
10use near_sdk::{
11    env,
12    json_types::{U128, U64},
13    near, require, AccountId, Gas, Promise, PromiseOrValue,
14};
15pub use templar_curator_primitives::{
16    CapGroupId, CapGroupRecord, CapGroupUpdate, CapGroupUpdateKey,
17};
18pub use templar_vault_kernel::state::op_state::{
19    AllocatingState, IdleState, OpState, PayoutState, RefreshingState, TargetId, WithdrawingState,
20};
21pub use templar_vault_kernel::types::{ActualIdx, ExpectedIdx, TimestampNs};
22use templar_vault_kernel::Wad;
23
24pub use event::{
25    AllocationPositionIssueKind, Event, PositionReportOutcome, QueueAction, QueueStatus, Reason,
26    UnbrickPhase, WithdrawProgressPhase, WithdrawalAccountingKind,
27};
28pub use params::*;
29
30pub mod errors;
31pub mod event;
32pub mod external;
33pub mod gas;
34pub mod lock;
35pub mod params;
36pub mod restrictions;
37
38pub use errors::Error;
39pub use gas::*;
40pub use lock::Locker;
41pub use restrictions::*;
42
43/// Broad import surface for vault consumers.
44///
45/// Prefer `use templar_common::vault::prelude::*;` at call sites that need
46/// most vault types, constants, and wad/math helpers.
47pub mod prelude {
48    pub use super::event::{
49        AllocationPositionIssueKind, Event, PositionReportOutcome, QueueAction, QueueStatus,
50        Reason, UnbrickPhase, WithdrawProgressPhase, WithdrawalAccountingKind,
51    };
52    pub use super::external::*;
53    pub use super::gas::*;
54    pub use super::params::*;
55    pub use super::restrictions::*;
56    pub use super::{
57        require_at_least, storage_bytes_for_account_id, ActualIdx, AllocationDelta, AllocationPlan,
58        AllocationWeights, CapGroupId, CapGroupRecord, CapGroupUpdate, CapGroupUpdateKey, Delta,
59        DepositMsg, Error, EscrowSettlement, ExpectedIdx, Fee, FeeAccrualAnchor, Fees,
60        IdleBalanceDelta, IdleResyncOutcome, Locker, MarketConfiguration, MarketId,
61        PendingWithdrawal, RealAssetsReport, ResyncIdleReport, TimestampNs, VaultConfiguration,
62    };
63    pub use templar_vault_kernel::math::number::{Number, WIDE};
64    pub use templar_vault_kernel::{
65        compute_fee_shares, compute_fee_shares_from_assets, mul_div_ceil, mul_div_floor,
66        mul_wad_floor, Wad, MAX_FEE_WAD, MAX_MANAGEMENT_FEE_WAD, MAX_PERFORMANCE_FEE_WAD,
67    };
68}
69
70pub type AllocationWeights = Vec<(MarketId, U128)>;
71pub type AllocationPlan = Vec<(MarketId, u128)>;
72
73/// Report of real (live) total assets broken down by market, used for AUM refresh.
74#[derive(Debug, Clone)]
75#[near(serializers = [borsh, json])]
76pub struct RealAssetsReport {
77    pub total_assets: U128,
78    pub per_market: Vec<(MarketId, U128)>,
79    /// Block timestamp in nanoseconds when this report was generated.
80    pub refreshed_at: U64,
81}
82
83/// Outcome of an idle balance resynchronization attempt.
84#[derive(Debug, Clone, PartialEq, Eq)]
85#[near(serializers = [borsh, json])]
86pub enum IdleResyncOutcome {
87    /// Resync succeeded.
88    Ok,
89    /// ft_balance_of call failed.
90    BalanceReadFailed,
91    /// Vault not in expected state.
92    UnexpectedState,
93    /// Resync was a no-op (e.g. cooldown not elapsed).
94    Ignored,
95}
96
97/// Detailed report from an idle balance resync operation.
98#[derive(Debug, Clone, PartialEq, Eq)]
99#[near(serializers = [borsh, json])]
100pub struct ResyncIdleReport {
101    pub outcome: IdleResyncOutcome,
102    /// Balance snapshot before resync.
103    pub before_idle: U128,
104    /// Actual idle balance read from contract.
105    pub actual_idle: U128,
106    /// Balance snapshot after resync.
107    pub after_idle: U128,
108    /// Magnitude of increase adjustment.
109    pub increased_by: U128,
110    /// Magnitude of decrease adjustment.
111    pub decreased_by: U128,
112    /// Amount added to fee anchor to prevent donation fees.
113    pub fee_anchor_bump: U128,
114    /// Completion timestamp in nanoseconds.
115    pub resynced_at_ns: U64,
116}
117
118#[derive(
119    Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, From, Into, Display,
120)]
121#[near(serializers = [borsh, json])]
122#[display("{_0}")]
123pub struct MarketId(pub u32);
124
125impl From<MarketId> for u64 {
126    fn from(value: MarketId) -> Self {
127        u64::from(value.0)
128    }
129}
130
131impl TryFrom<u64> for MarketId {
132    type Error = <u32 as TryFrom<u64>>::Error;
133
134    fn try_from(value: u64) -> Result<Self, Self::Error> {
135        u32::try_from(value).map(Self)
136    }
137}
138
139/// Parsed from the string parameter `msg` passed by `*_transfer_call` to
140/// `*_on_transfer` calls.
141#[near(serializers = [json])]
142pub enum DepositMsg {
143    /// Add the attached tokens to the sender's vault position.
144    Supply,
145}
146
147/// Concrete configuration for a market.
148#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
149#[derive(Clone, Default)]
150#[near]
151pub struct MarketConfiguration {
152    /// Supply cap for this market (in underlying asset units)
153    pub cap: U128,
154    /// Whether market is enabled for deposits/withdrawals
155    pub enabled: bool,
156    /// Timestamp after which market can be removed (if pending removal)
157    pub removable_at: TimestampNs,
158    /// Cap group identifier used to throttle correlated exposure
159    pub cap_group_id: Option<CapGroupId>,
160}
161
162/// A fee configuration pairing a fee rate with its recipient.
163#[derive(Clone, Debug, PartialEq, Eq)]
164#[near(serializers = [borsh, json])]
165pub struct Fee<T> {
166    pub fee: T,
167    pub recipient: AccountId,
168}
169
170/// Complete fee configuration for the vault, including performance and management fees.
171#[derive(Clone, Debug, PartialEq, Eq)]
172#[near(serializers = [borsh, json])]
173pub struct Fees<T> {
174    pub performance: Fee<T>,
175    pub management: Fee<T>,
176    /// Optional cap on how fast `total_assets` is allowed to grow for fee accrual.
177    ///
178    /// Interpreted as an annualized WAD rate (1e18 = 100% per year). When set,
179    /// fee accrual uses `min(cur_total_assets, last_total_assets * (1 + max_rate * dt / YEAR))`
180    /// as the effective `cur_total_assets`.
181    pub max_total_assets_growth_rate: Option<T>,
182}
183
184impl From<Fee<Wad>> for Fee<U128> {
185    fn from(value: Fee<Wad>) -> Self {
186        Self {
187            fee: U128(u128::from(value.fee)),
188            recipient: value.recipient,
189        }
190    }
191}
192
193impl From<Fees<Wad>> for Fees<U128> {
194    fn from(value: Fees<Wad>) -> Self {
195        Self {
196            performance: value.performance.into(),
197            management: value.management.into(),
198            max_total_assets_growth_rate: value
199                .max_total_assets_growth_rate
200                .map(|rate| U128(u128::from(rate))),
201        }
202    }
203}
204
205impl From<Fee<U128>> for Fee<Wad> {
206    fn from(value: Fee<U128>) -> Self {
207        Self {
208            fee: Wad::from(value.fee.0),
209            recipient: value.recipient,
210        }
211    }
212}
213
214impl From<Fees<U128>> for Fees<Wad> {
215    fn from(value: Fees<U128>) -> Self {
216        Self {
217            performance: value.performance.into(),
218            management: value.management.into(),
219            max_total_assets_growth_rate: value
220                .max_total_assets_growth_rate
221                .map(|rate| rate.0.into()),
222        }
223    }
224}
225
226/// Configuration for the setup of a metavault.
227#[derive(Clone)]
228#[near(serializers = [json, borsh])]
229pub struct VaultConfiguration {
230    /// The account that owns this vault.
231    pub owner: AccountId,
232    /// The account that can submit allocation plans. See [AllocationMode].
233    pub curator: AccountId,
234    /// The emergency role that can cancel withdrawals and trigger deallocations.
235    pub sentinel: AccountId,
236    /// The underlying asset for this vault.
237    pub underlying_token: FungibleAsset<BorrowAsset>,
238    /// The initial timelock for this vault used for modifying the configuration.
239    pub initial_timelock_ns: U64,
240    /// Fee configuration for performance and management fees as well as their recipients.
241    pub fees: Fees<Wad>,
242    /// The skim account that can unorphan any assets erroneously sent to this vault.
243    pub skim_recipient: AccountId,
244    /// The name of the share token.
245    pub name: String,
246    /// The symbol of the share token.
247    pub symbol: String,
248    /// The number of decimals for the share token, usually would be the same as the underlying asset.
249    pub decimals: NonZeroU8,
250    /// Restrictions for this vault.
251    pub restrictions: Option<Restrictions>,
252    /// Optional cooldown (ns) between `refresh_markets` calls; defaults to 30 seconds when `None`.
253    pub refresh_cooldown_ns: Option<U64>,
254    /// Optional cooldown (ns) between `idle_resync` calls; defaults to 120 seconds when `None`.
255    pub idle_resync_cooldown_ns: Option<U64>,
256    /// Optional cooldown (ns) before a withdrawal can be executed; defaults to 24 hours when `None`.
257    pub withdrawal_cooldown_ns: Option<U64>,
258}
259
260/// A single market allocation delta specifying a market and an amount in underlying asset units.
261#[derive(Debug, Clone)]
262#[near(serializers = [borsh, json])]
263pub struct Delta {
264    pub market: MarketId,
265    pub amount: U128,
266}
267
268impl Delta {
269    pub fn new<T: Into<U128>>(market: MarketId, amount: T) -> Self {
270        Delta {
271            market,
272            amount: amount.into(),
273        }
274    }
275    pub fn validate(&self) {
276        require!(self.amount.0 > 0, "Delta amount must be greater than zero");
277    }
278}
279
280/// Allocation instruction for a single market. `Supply` forwards idle assets to the market. `Withdraw` creates a supply-withdrawal request in the market (does not execute it).
281#[derive(Debug, Clone)]
282#[near(serializers = [borsh, json])]
283pub enum AllocationDelta {
284    Supply(Delta),
285    Withdraw(Delta),
286}
287
288impl AsRef<Delta> for AllocationDelta {
289    fn as_ref(&self) -> &Delta {
290        match self {
291            AllocationDelta::Supply(d) | AllocationDelta::Withdraw(d) => d,
292        }
293    }
294}
295
296/// Settlement breakdown for escrowed withdrawal shares. Invariant: `to_burn + refund == original escrow_shares`.
297#[derive(Debug, Clone, Copy)]
298pub struct EscrowSettlement {
299    pub to_burn: u128,
300    pub refund: u128,
301}
302
303impl EscrowSettlement {
304    pub fn new(escrow_shares: u128, burn_shares: u128) -> Self {
305        let settlement = templar_vault_kernel::types::EscrowSettlement::from_escrow_and_burn(
306            escrow_shares,
307            burn_shares,
308        );
309        Self {
310            to_burn: settlement.to_burn,
311            refund: settlement.refund,
312        }
313    }
314}
315
316impl From<EscrowSettlement> for (u128, u128) {
317    fn from(tuple: EscrowSettlement) -> Self {
318        (tuple.to_burn, tuple.refund)
319    }
320}
321
322/// A queued withdrawal request with shares held in escrow. Fields use underlying asset units for `expected_assets` and nanosecond timestamps for `requested_at`.
323#[derive(Clone, Debug)]
324#[near(serializers = [borsh])]
325pub struct PendingWithdrawal {
326    pub owner: AccountId,
327    pub receiver: AccountId,
328    pub escrow_shares: u128,
329    pub expected_assets: u128,
330    pub requested_at: u64,
331}
332
333impl PendingWithdrawal {
334    #[must_use]
335    pub fn encoded_size() -> u64 {
336        storage_bytes_for_account_id()
337            + storage_bytes_for_account_id()
338            + 16  // escrow_shares: u128
339            + 16  // expected_assets: u128
340            + 8 // requested_at: u64
341    }
342}
343
344// Worst case size encoded for AccountId
345#[must_use]
346pub const fn storage_bytes_for_account_id() -> u64 {
347    // 4 bytes for length prefix + worst case size encoded for AccountId
348    4 + AccountId::MAX_LEN as u64
349}
350
351/// Direction and magnitude of an idle balance change. Emits an `IdleBalanceUpdated` event when applied.
352#[derive(Clone, Debug)]
353#[near(serializers = [borsh, json])]
354pub enum IdleBalanceDelta {
355    Increase(U128),
356    Decrease(U128),
357}
358
359impl IdleBalanceDelta {
360    pub fn apply(&self, balance: u128) -> u128 {
361        let new = match self {
362            IdleBalanceDelta::Increase(amount) => balance.saturating_add(amount.0),
363            IdleBalanceDelta::Decrease(amount) => balance.saturating_sub(amount.0),
364        };
365        Event::IdleBalanceUpdated {
366            prev: U128::from(balance),
367            delta: self.clone(),
368        }
369        .emit();
370        new
371    }
372}
373
374/// Anchor point for fee accrual: stores the total assets and nanosecond timestamp at which fees were last accrued.
375#[near(serializers = [borsh, json])]
376#[derive(Debug, Clone, Default)]
377pub struct FeeAccrualAnchor {
378    pub total_assets: U128,
379    pub timestamp_ns: U64,
380}
381
382#[cfg(test)]
383mod tests {
384    use super::{Fee, Fees, MarketId};
385    use near_sdk::json_types::U128;
386    use near_sdk::AccountId;
387    use templar_vault_kernel::Wad;
388
389    #[test]
390    fn market_id_try_from_u64_accepts_u32_range() {
391        assert_eq!(
392            MarketId::try_from(u64::from(u32::MAX)),
393            Ok(MarketId(u32::MAX))
394        );
395    }
396
397    #[test]
398    fn market_id_try_from_u64_rejects_out_of_range() {
399        assert!(MarketId::try_from(u64::from(u32::MAX) + 1).is_err());
400    }
401
402    #[test]
403    fn fees_roundtrip_between_u128_and_wad_preserves_values() {
404        let fees_u128 = Fees {
405            performance: Fee {
406                fee: U128(10),
407                recipient: "perf.testnet"
408                    .parse::<AccountId>()
409                    .expect("valid account id"),
410            },
411            management: Fee {
412                fee: U128(20),
413                recipient: "mgmt.testnet"
414                    .parse::<AccountId>()
415                    .expect("valid account id"),
416            },
417            max_total_assets_growth_rate: Some(U128(30)),
418        };
419
420        let fees_wad: Fees<Wad> = fees_u128.clone().into();
421        assert_eq!(u128::from(fees_wad.performance.fee), 10);
422        assert_eq!(u128::from(fees_wad.management.fee), 20);
423        assert_eq!(
424            fees_wad
425                .max_total_assets_growth_rate
426                .map(u128::from)
427                .expect("max rate must be present"),
428            30
429        );
430
431        let roundtrip: Fees<U128> = fees_wad.into();
432        assert_eq!(roundtrip.performance.fee.0, 10);
433        assert_eq!(roundtrip.management.fee.0, 20);
434        assert_eq!(
435            roundtrip.max_total_assets_growth_rate.map(|v| v.0),
436            Some(30)
437        );
438        assert_eq!(roundtrip.performance.recipient.as_str(), "perf.testnet");
439        assert_eq!(roundtrip.management.recipient.as_str(), "mgmt.testnet");
440    }
441}