templar_vault_kernel/math/
wad.rs

1//! Chain-agnostic WAD math primitives for vault share calculations.
2//!
3//! Provides `Wad` (18-decimal fixed-point) type for precise fee and share calculations.
4
5use core::ops::Div;
6
7use derive_more::{From, Into};
8use primitive_types::U256;
9
10use super::number::Number;
11
12/// Maximum annualized management fee rate: 5%.
13pub const MAX_MANAGEMENT_FEE_WAD: u128 = Wad::SCALE / 100 * 5;
14
15/// Maximum performance fee rate on profits: 50%.
16pub const MAX_PERFORMANCE_FEE_WAD: u128 = Wad::SCALE / 100 * 50;
17
18/// Backwards-compatible alias for `MAX_PERFORMANCE_FEE_WAD`.
19pub const MAX_FEE_WAD: u128 = MAX_PERFORMANCE_FEE_WAD;
20
21/// An 18-decimal fixed-point value (1e18 = 100%), backed by U256.
22///
23/// When the `serde` feature is enabled, serializes transparently as Number
24/// (which serializes to a decimal string for JSON compatibility).
25#[cfg_attr(not(target_arch = "wasm32"), derive(Debug))]
26#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, From, Into)]
27pub struct Wad(pub Number);
28
29#[cfg(all(feature = "serde", not(feature = "postcard")))]
30mod serde_impl {
31    use super::*;
32    use serde::{Deserialize, Deserializer, Serialize, Serializer};
33
34    impl Serialize for Wad {
35        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
36        where
37            S: Serializer,
38        {
39            // Transparent serialization via Number - use fully qualified syntax
40            Serialize::serialize(&self.0, serializer)
41        }
42    }
43
44    impl<'de> Deserialize<'de> for Wad {
45        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
46        where
47            D: Deserializer<'de>,
48        {
49            <Number as Deserialize>::deserialize(deserializer).map(Wad)
50        }
51    }
52}
53
54#[cfg(feature = "postcard")]
55mod postcard_serde_impl {
56    use super::*;
57    use serde::{Deserialize, Deserializer, Serialize, Serializer};
58
59    impl Serialize for Wad {
60        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
61        where
62            S: Serializer,
63        {
64            Serialize::serialize(&self.0, serializer)
65        }
66    }
67
68    impl<'de> Deserialize<'de> for Wad {
69        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
70        where
71            D: Deserializer<'de>,
72        {
73            <Number as Deserialize>::deserialize(deserializer).map(Wad)
74        }
75    }
76}
77
78#[cfg(feature = "borsh")]
79mod borsh_impl {
80    use super::*;
81    use borsh::{self, BorshDeserialize, BorshSerialize};
82
83    impl BorshSerialize for Wad {
84        fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
85            BorshSerialize::serialize(&self.0, writer)
86        }
87    }
88
89    impl BorshDeserialize for Wad {
90        fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
91            <Number as BorshDeserialize>::deserialize_reader(reader).map(Wad)
92        }
93    }
94}
95
96#[cfg(feature = "borsh-schema")]
97mod borsh_schema_impl {
98    use super::*;
99    use alloc::collections::BTreeMap;
100    use borsh::schema::{add_definition, Declaration, Definition};
101    use borsh::BorshSchema;
102
103    impl BorshSchema for Wad {
104        fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) {
105            let definition = Definition::Primitive(32);
106            add_definition(Self::declaration(), definition, definitions);
107        }
108
109        fn declaration() -> Declaration {
110            "Wad".into()
111        }
112    }
113}
114
115#[cfg(feature = "schemars")]
116mod schemars_impl {
117    use super::*;
118    use alloc::string::ToString;
119    use schemars::r#gen::SchemaGenerator;
120    use schemars::schema::Schema;
121    use schemars::JsonSchema;
122
123    impl JsonSchema for Wad {
124        fn schema_name() -> alloc::string::String {
125            "Wad".to_string()
126        }
127
128        fn json_schema(generator: &mut SchemaGenerator) -> Schema {
129            let mut schema = generator.subschema_for::<Number>().into_object();
130            schema.metadata().description =
131                Some("Wad fixed fraction backed by 256-bit unsigned integer".to_string());
132            schema.string().pattern = Some("^(0|[1-9][0-9]{0,77})$".to_string());
133            schema.into()
134        }
135    }
136}
137
138impl Wad {
139    /// Scaling factor (1e18).
140    pub const SCALE: u128 = 1_000_000_000_000_000_000u128;
141
142    pub const ZERO: Self = Wad(Number::ZERO);
143    pub const ONE: Self = Wad(Number(U256([Self::SCALE as u64, 0, 0, 0])));
144
145    /// Returns zero.
146    #[inline]
147    #[must_use]
148    pub const fn zero() -> Self {
149        Self::ZERO
150    }
151
152    /// Returns one unit (1.0 in WAD scale).
153    #[inline]
154    #[must_use]
155    pub const fn one() -> Self {
156        Self::ONE
157    }
158
159    #[inline]
160    #[must_use]
161    pub fn is_zero(&self) -> bool {
162        self.0.is_zero()
163    }
164
165    #[inline]
166    #[must_use]
167    pub fn is_one(&self) -> bool {
168        self.0 .0 == U256::from(Self::SCALE)
169    }
170
171    /// Returns the lower 128 bits (truncation) of this WAD value.
172    #[inline]
173    #[must_use]
174    pub fn as_u128_trunc(self) -> u128 {
175        self.0.as_u128_trunc()
176    }
177
178    /// Applies this WAD-scaled fraction to an unscaled Number, floored.
179    #[inline]
180    #[must_use]
181    pub fn apply_floored(self, amount: Number) -> Number {
182        Number::mul_div_floor(amount, self.0, Number::from(Self::SCALE))
183    }
184}
185
186impl From<u128> for Wad {
187    #[inline]
188    fn from(v: u128) -> Self {
189        Wad(Number::from(v))
190    }
191}
192
193impl From<Wad> for u128 {
194    #[inline]
195    fn from(w: Wad) -> u128 {
196        w.as_u128_trunc()
197    }
198}
199
200impl Div<u128> for Wad {
201    type Output = Wad;
202    #[inline]
203    fn div(self, rhs: u128) -> Wad {
204        Wad(self.0 / rhs)
205    }
206}
207impl Div<Number> for Wad {
208    type Output = Wad;
209    #[inline]
210    fn div(self, rhs: Number) -> Wad {
211        Wad(self.0 / rhs)
212    }
213}
214
215/// Computes fee shares to mint given:
216/// - `cur_total_assets`: current total assets under management
217/// - `last_total_assets`: previous total assets snapshot
218/// - `performance_fee`: WAD fraction (1e18 = 100%)
219/// - `total_supply`: current total share supply
220///
221/// Floors intermediate divisions; returns 0 when no profit, zero fee, zero supply,
222/// or when the fee consumes all assets (`cur_total_assets` == `fee_assets`).
223#[inline]
224#[must_use]
225pub fn compute_fee_shares(
226    cur_total_assets: Number,
227    last_total_assets: Number,
228    performance_fee: Wad,
229    total_supply: Number,
230) -> Number {
231    let profit = cur_total_assets.saturating_sub(last_total_assets);
232    compute_fee_shares_from_assets(
233        performance_fee.apply_floored(profit),
234        cur_total_assets,
235        total_supply,
236    )
237}
238
239/// Computes fee shares to mint from a raw `fee_assets` amount, given current total assets and supply.
240/// Returns 0 when fee is zero, supply is zero, or fee consumes all assets.
241#[inline]
242#[must_use]
243pub fn compute_fee_shares_from_assets(
244    fee_assets: Number,
245    cur_total_assets: Number,
246    total_supply: Number,
247) -> Number {
248    if fee_assets.is_zero() || total_supply.is_zero() {
249        return Number::zero();
250    }
251    if fee_assets.0 >= cur_total_assets.0 {
252        return Number::zero();
253    }
254    let denom = Number(cur_total_assets.0 - fee_assets.0);
255    Number::mul_div_floor(fee_assets, total_supply, denom)
256}
257
258/// Multiplies x by `y/Wad::SCALE` and floors: floor(x * y / 1e18).
259/// y is a WAD-scaled fraction (1e18 = 100%), and x is an unscaled amount.
260#[inline]
261#[must_use]
262pub fn mul_wad_floor(x: Number, y: Wad) -> Number {
263    y.apply_floored(x)
264}
265
266/// Multiplies and divides with flooring: floor(x * y / denom).
267/// Uses 512-bit intermediate (U512) to avoid overflow; returns 0 if denom is 0.
268#[inline]
269#[must_use]
270pub fn mul_div_floor(x: Number, y: Number, denom: Number) -> Number {
271    Number::mul_div_floor(x, y, denom)
272}
273
274/// Multiplies and divides with ceiling: ceil(x * y / denom).
275/// Uses 512-bit intermediate (U512) to avoid overflow; returns 0 if denom is 0.
276/// Implemented via quotient/remainder to avoid relying on addition overflow behavior.
277#[inline]
278#[must_use]
279pub fn mul_div_ceil(x: Number, y: Number, denom: Number) -> Number {
280    Number::mul_div_ceil(x, y, denom)
281}
282
283/// Nanoseconds in a standard year (365 days).
284pub const YEAR_NS: u64 = 365 * 24 * 60 * 60 * 1_000_000_000;
285
286/// Compute the effective total_assets for fee accrual, clamping growth
287/// to the max rate if configured.
288///
289/// When `max_rate` is `Some`, limits the effective total_assets to
290/// `anchor_total_assets * (1 + max_rate * elapsed / YEAR)`.
291#[inline]
292#[must_use]
293pub fn total_assets_for_fee_accrual(
294    cur_total_assets: u128,
295    anchor_total_assets: u128,
296    anchor_timestamp_ns: u64,
297    now_ns: u64,
298    max_rate: Option<Wad>,
299) -> u128 {
300    let Some(max_rate) = max_rate else {
301        return cur_total_assets;
302    };
303    if cur_total_assets <= anchor_total_assets
304        || anchor_total_assets == 0
305        || now_ns < anchor_timestamp_ns
306    {
307        return cur_total_assets;
308    }
309    let elapsed_ns = now_ns - anchor_timestamp_ns;
310    if elapsed_ns == 0 {
311        return anchor_total_assets;
312    }
313    let annual_max_increase = max_rate.apply_floored(Number::from(anchor_total_assets));
314    let max_increase = mul_div_floor(
315        annual_max_increase,
316        Number::from(u128::from(elapsed_ns)),
317        Number::from(u128::from(YEAR_NS)),
318    )
319    .as_u128_saturating();
320    let max_total_assets = anchor_total_assets.saturating_add(max_increase);
321    cur_total_assets.min(max_total_assets)
322}
323
324/// Compute management fee shares (time-based fee pro-rated over elapsed time).
325///
326/// Returns the number of shares to mint for management fees.
327#[inline]
328#[must_use]
329pub fn compute_management_fee_shares(
330    fee_assets_base: u128,
331    cur_total_assets: u128,
332    total_supply: u128,
333    management_fee_wad: Wad,
334    last_timestamp_ns: u64,
335    now_ns: u64,
336) -> Number {
337    if management_fee_wad.is_zero() || total_supply == 0 || now_ns <= last_timestamp_ns {
338        return Number::zero();
339    }
340    let elapsed_ns = now_ns - last_timestamp_ns;
341    let annual_fee_assets = management_fee_wad.apply_floored(Number::from(fee_assets_base));
342    let fee_assets = mul_div_floor(
343        annual_fee_assets,
344        Number::from(u128::from(elapsed_ns)),
345        Number::from(u128::from(YEAR_NS)),
346    );
347    compute_fee_shares_from_assets(
348        fee_assets,
349        Number::from(cur_total_assets),
350        Number::from(total_supply),
351    )
352}
353
354#[cfg(test)]
355mod tests;