templar_common/vault/
mod.rs

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