templar_vault_kernel/fee/mod.rs
1//! Fee structures for vault operations.
2//!
3//! Portable across NEAR and Soroban.
4//!
5//! This module provides two fee representation approaches:
6//! - `Fee<T>` / `Fees<T>`: Generic types with string recipients for chain flexibility
7//! - `FeeSlot` / `FeesSpec`: Spec-compliant types with fixed-size `Address` recipients
8//!
9//! Use `Fee<Wad>` for NEAR where `AccountId` is naturally a string.
10//! Use `FeeSlot` when strict spec conformance with 32-byte addresses is required.
11
12use alloc::string::String;
13
14use crate::math::wad::Wad;
15use crate::types::Address;
16
17// Generic Fee Types (String recipient - flexible)
18
19/// A fee configuration with a rate and recipient.
20///
21/// This generic type uses a string recipient for maximum chain flexibility.
22/// For spec-compliant 32-byte address recipients, see `FeeSlot`.
23#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
24#[derive(Clone, PartialEq, Eq)]
25pub struct Fee<T> {
26 /// The fee rate (interpretation depends on T).
27 pub fee: T,
28 /// The recipient identifier (account/address as string).
29 pub recipient: String,
30}
31
32impl<T> Fee<T> {
33 /// Create a new generic fee entry.
34 #[inline]
35 #[must_use]
36 pub fn new(fee: T, recipient: impl Into<String>) -> Self {
37 Self {
38 fee,
39 recipient: recipient.into(),
40 }
41 }
42}
43
44/// Collection of fees for a vault.
45///
46/// This generic type uses `Fee<T>` with string recipients.
47/// For spec-compliant types, see `FeesSpec`.
48#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
49#[derive(Clone, PartialEq, Eq)]
50pub struct Fees<T> {
51 /// Performance fee (charged on profits).
52 pub performance: Fee<T>,
53 /// Management fee (charged over time).
54 pub management: Fee<T>,
55 /// Optional cap on how fast `total_assets` is allowed to grow for fee accrual.
56 ///
57 /// Interpreted as an annualized WAD rate (1e18 = 100% per year). When set,
58 /// fee accrual uses `min(cur_total_assets, last_total_assets * (1 + max_rate * dt / YEAR))`
59 /// as the effective `cur_total_assets`.
60 pub max_total_assets_growth_rate: Option<T>,
61}
62
63impl<T> Fees<T> {
64 /// Create a new generic fees configuration.
65 #[inline]
66 #[must_use]
67 pub const fn new(
68 performance: Fee<T>,
69 management: Fee<T>,
70 max_total_assets_growth_rate: Option<T>,
71 ) -> Self {
72 Self {
73 performance,
74 management,
75 max_total_assets_growth_rate,
76 }
77 }
78}
79
80// Spec-Compliant Fee Types (Address recipient - fixed size)
81
82/// A fee slot with a WAD-scaled rate and 32-byte address recipient.
83///
84/// This type matches the kernel spec exactly:
85/// - `fee_wad`: WAD-scaled fee rate (1e18 = 100%)
86/// - `recipient`: 32-byte canonical address
87///
88/// The executor is responsible for mapping chain-native addresses to/from
89/// this canonical 32-byte format.
90#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
91#[derive(Clone, Copy, PartialEq, Eq)]
92pub struct FeeSlot {
93 /// The fee rate as a WAD value (1e18 = 100%).
94 pub fee_wad: Wad,
95 /// The recipient as a 32-byte canonical address.
96 pub recipient: Address,
97}
98
99impl FeeSlot {
100 /// Create a new fee slot.
101 #[inline]
102 #[must_use]
103 pub const fn new(fee_wad: Wad, recipient: Address) -> Self {
104 Self { fee_wad, recipient }
105 }
106
107 pub const ZERO: Self = Self {
108 fee_wad: Wad::ZERO,
109 recipient: Address([0u8; 32]),
110 };
111
112 /// Create a zero fee slot (no fee, zero address).
113 #[inline]
114 #[must_use]
115 pub const fn zero() -> Self {
116 Self::ZERO
117 }
118
119 #[inline]
120 #[must_use]
121 pub fn has_rate(&self) -> bool {
122 !self.fee_wad.is_zero()
123 }
124
125 /// Check if this fee slot has a zero rate.
126 #[inline]
127 #[must_use]
128 pub fn is_zero_rate(&self) -> bool {
129 !self.has_rate()
130 }
131}
132
133impl Default for FeeSlot {
134 fn default() -> Self {
135 Self::zero()
136 }
137}
138
139/// Spec-compliant fee collection using `FeeSlot` with 32-byte addresses.
140///
141/// This type matches the kernel spec exactly and uses fixed-size addresses
142/// for performance and predictable serialization.
143#[templar_vault_macros::vault_derive(borsh, serde, postcard)]
144#[derive(Clone, Copy, PartialEq, Eq)]
145pub struct FeesSpec {
146 /// Performance fee (charged on profits).
147 pub performance: FeeSlot,
148 /// Management fee (charged over time).
149 pub management: FeeSlot,
150 /// Optional cap on total assets growth rate for fee accrual.
151 ///
152 /// Interpreted as an annualized WAD rate (1e18 = 100% per year).
153 pub max_total_assets_growth_rate: Option<Wad>,
154}
155
156impl FeesSpec {
157 /// Create a new fees configuration.
158 #[inline]
159 #[must_use]
160 pub const fn new(
161 performance: FeeSlot,
162 management: FeeSlot,
163 max_total_assets_growth_rate: Option<Wad>,
164 ) -> Self {
165 Self {
166 performance,
167 management,
168 max_total_assets_growth_rate,
169 }
170 }
171
172 #[inline]
173 #[must_use]
174 pub fn has_active_slot_fees(&self) -> bool {
175 self.performance.has_rate() || self.management.has_rate()
176 }
177
178 #[inline]
179 #[must_use]
180 pub fn has_growth_cap(&self) -> bool {
181 self.max_total_assets_growth_rate.is_some()
182 }
183
184 /// Returns true when all fee fields are unset/zeroed.
185 #[inline]
186 #[must_use]
187 pub fn is_zero(&self) -> bool {
188 !self.has_active_slot_fees() && !self.has_growth_cap()
189 }
190
191 pub const ZERO: Self = Self {
192 performance: FeeSlot::ZERO,
193 management: FeeSlot::ZERO,
194 max_total_assets_growth_rate: None,
195 };
196
197 /// Create a fees configuration with no fees.
198 #[inline]
199 #[must_use]
200 pub const fn zero() -> Self {
201 Self::ZERO
202 }
203}
204
205impl Default for FeesSpec {
206 fn default() -> Self {
207 Self::zero()
208 }
209}
210
211// Tests
212
213#[cfg(test)]
214mod tests;