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            #[cfg(feature = "soroban")]
65            {
66                self.0.serialize(serializer)
67            }
68
69            #[cfg(not(feature = "soroban"))]
70            {
71                Serialize::serialize(&self.0, serializer)
72            }
73        }
74    }
75
76    impl<'de> Deserialize<'de> for Wad {
77        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
78        where
79            D: Deserializer<'de>,
80        {
81            #[cfg(feature = "soroban")]
82            {
83                u128::deserialize(deserializer).map(|value| Wad(Number::from(value)))
84            }
85
86            #[cfg(not(feature = "soroban"))]
87            {
88                <Number as Deserialize>::deserialize(deserializer).map(Wad)
89            }
90        }
91    }
92}
93
94#[cfg(feature = "borsh")]
95mod borsh_impl {
96    use super::*;
97    use borsh::{self, BorshDeserialize, BorshSerialize};
98
99    impl BorshSerialize for Wad {
100        fn serialize<W: borsh::io::Write>(&self, writer: &mut W) -> borsh::io::Result<()> {
101            BorshSerialize::serialize(&self.0, writer)
102        }
103    }
104
105    impl BorshDeserialize for Wad {
106        fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
107            <Number as BorshDeserialize>::deserialize_reader(reader).map(Wad)
108        }
109    }
110}
111
112#[cfg(feature = "borsh-schema")]
113mod borsh_schema_impl {
114    use super::*;
115    use alloc::collections::BTreeMap;
116    use borsh::schema::{add_definition, Declaration, Definition};
117    use borsh::BorshSchema;
118
119    impl BorshSchema for Wad {
120        fn add_definitions_recursively(definitions: &mut BTreeMap<Declaration, Definition>) {
121            let definition = Definition::Primitive(32);
122            add_definition(Self::declaration(), definition, definitions);
123        }
124
125        fn declaration() -> Declaration {
126            "Wad".into()
127        }
128    }
129}
130
131#[cfg(feature = "schemars")]
132mod schemars_impl {
133    use super::*;
134    use alloc::string::ToString;
135    use schemars::r#gen::SchemaGenerator;
136    use schemars::schema::Schema;
137    use schemars::JsonSchema;
138
139    impl JsonSchema for Wad {
140        fn schema_name() -> alloc::string::String {
141            "Wad".to_string()
142        }
143
144        fn json_schema(generator: &mut SchemaGenerator) -> Schema {
145            let mut schema = generator.subschema_for::<Number>().into_object();
146            schema.metadata().description =
147                Some("Wad fixed fraction backed by 256-bit unsigned integer".to_string());
148            schema.string().pattern = Some("^(0|[1-9][0-9]{0,77})$".to_string());
149            schema.into()
150        }
151    }
152}
153
154impl Wad {
155    /// Scaling factor (1e18).
156    pub const SCALE: u128 = 1_000_000_000_000_000_000u128;
157
158    pub const ZERO: Self = Wad(Number::ZERO);
159    pub const ONE: Self = Wad(Number(U256([Self::SCALE as u64, 0, 0, 0])));
160
161    /// Returns zero.
162    #[inline]
163    #[must_use]
164    pub const fn zero() -> Self {
165        Self::ZERO
166    }
167
168    /// Returns one unit (1.0 in WAD scale).
169    #[inline]
170    #[must_use]
171    pub const fn one() -> Self {
172        Self::ONE
173    }
174
175    #[inline]
176    #[must_use]
177    pub fn is_zero(&self) -> bool {
178        self.0.is_zero()
179    }
180
181    #[inline]
182    #[must_use]
183    pub fn is_one(&self) -> bool {
184        self.0 .0 == U256::from(Self::SCALE)
185    }
186
187    /// Returns the lower 128 bits (truncation) of this WAD value.
188    #[inline]
189    #[must_use]
190    pub fn as_u128_trunc(self) -> u128 {
191        self.0.as_u128_trunc()
192    }
193
194    /// Applies this WAD-scaled fraction to an unscaled Number, floored.
195    #[inline]
196    #[must_use]
197    pub fn apply_floored(self, amount: Number) -> Number {
198        mul_wad_floor(amount, self)
199    }
200}
201
202impl From<u128> for Wad {
203    #[inline]
204    fn from(v: u128) -> Self {
205        Wad(Number::from(v))
206    }
207}
208
209impl From<Wad> for u128 {
210    #[inline]
211    fn from(w: Wad) -> u128 {
212        w.as_u128_trunc()
213    }
214}
215
216impl Div<u128> for Wad {
217    type Output = Wad;
218    #[inline]
219    fn div(self, rhs: u128) -> Wad {
220        Wad(self.0 / rhs)
221    }
222}
223impl Div<Number> for Wad {
224    type Output = Wad;
225    #[inline]
226    fn div(self, rhs: Number) -> Wad {
227        Wad(self.0 / rhs)
228    }
229}
230
231/// Computes fee shares to mint given:
232/// - `cur_total_assets`: current total assets under management
233/// - `last_total_assets`: previous total assets snapshot
234/// - `performance_fee`: WAD fraction (1e18 = 100%)
235/// - `total_supply`: current total share supply
236///
237/// Floors intermediate divisions; returns 0 when no profit, zero fee, zero supply,
238/// or when the fee consumes all assets (`cur_total_assets` == `fee_assets`).
239#[inline]
240#[must_use]
241pub fn compute_fee_shares(
242    cur_total_assets: Number,
243    last_total_assets: Number,
244    performance_fee: Wad,
245    total_supply: Number,
246) -> Number {
247    let profit = cur_total_assets.saturating_sub(last_total_assets);
248    compute_fee_shares_from_assets(
249        performance_fee.apply_floored(profit),
250        cur_total_assets,
251        total_supply,
252    )
253}
254
255/// Computes fee shares to mint from a raw `fee_assets` amount, given current total assets and supply.
256/// Returns 0 when fee is zero, supply is zero, or fee consumes all assets.
257#[inline]
258#[must_use]
259pub fn compute_fee_shares_from_assets(
260    fee_assets: Number,
261    cur_total_assets: Number,
262    total_supply: Number,
263) -> Number {
264    if fee_assets.is_zero() || total_supply.is_zero() {
265        return Number::zero();
266    }
267    if fee_assets.0 >= cur_total_assets.0 {
268        return Number::zero();
269    }
270    let denom = Number(cur_total_assets.0 - fee_assets.0);
271    Number::mul_div_floor(fee_assets, total_supply, denom)
272}
273
274/// Multiplies x by `y/Wad::SCALE` and floors: floor(x * y / 1e18).
275/// y is a WAD-scaled fraction (1e18 = 100%), and x is an unscaled amount.
276#[inline]
277#[must_use]
278pub fn mul_wad_floor(x: Number, y: Wad) -> Number {
279    Number::mul_div_floor(x, y.0, Number::from(Wad::SCALE))
280}
281
282/// Multiplies and divides with flooring: floor(x * y / denom).
283/// Uses 512-bit intermediate (U512) to avoid overflow; returns 0 if denom is 0.
284#[inline]
285#[must_use]
286pub fn mul_div_floor(x: Number, y: Number, denom: Number) -> Number {
287    Number::mul_div_floor(x, y, denom)
288}
289
290/// Multiplies and divides with ceiling: ceil(x * y / denom).
291/// Uses 512-bit intermediate (U512) to avoid overflow; returns 0 if denom is 0.
292/// Implemented via quotient/remainder to avoid relying on addition overflow behavior.
293#[inline]
294#[must_use]
295pub fn mul_div_ceil(x: Number, y: Number, denom: Number) -> Number {
296    Number::mul_div_ceil(x, y, denom)
297}
298
299/// Nanoseconds in a standard year (365 days).
300pub const YEAR_NS: u64 = 365 * 24 * 60 * 60 * 1_000_000_000;
301
302/// Compute the effective total_assets for fee accrual, clamping growth
303/// to the max rate if configured.
304///
305/// When `max_rate` is `Some`, limits the effective total_assets to
306/// `anchor_total_assets * (1 + max_rate * elapsed / YEAR)`. If the anchor
307/// asset value is zero, any positive current assets are treated as uncapped
308/// zero-anchor growth and excluded from fee accrual until the anchor is
309/// advanced by the caller.
310#[inline]
311#[must_use]
312pub fn total_assets_for_fee_accrual(
313    cur_total_assets: u128,
314    anchor_total_assets: u128,
315    anchor_timestamp_ns: u64,
316    now_ns: u64,
317    max_rate: Option<Wad>,
318) -> u128 {
319    let Some(max_rate) = max_rate else {
320        return cur_total_assets;
321    };
322    if cur_total_assets <= anchor_total_assets || now_ns < anchor_timestamp_ns {
323        return cur_total_assets;
324    }
325    if anchor_total_assets == 0 {
326        return 0;
327    }
328    let elapsed_ns = now_ns - anchor_timestamp_ns;
329    if elapsed_ns == 0 {
330        return anchor_total_assets;
331    }
332    let annual_max_increase = max_rate.apply_floored(Number::from(anchor_total_assets));
333    let max_increase = mul_div_floor(
334        annual_max_increase,
335        Number::from(u128::from(elapsed_ns)),
336        Number::from(u128::from(YEAR_NS)),
337    )
338    .as_u128_saturating();
339    let max_total_assets = anchor_total_assets.saturating_add(max_increase);
340    cur_total_assets.min(max_total_assets)
341}
342
343/// Compute management fee shares (time-based fee pro-rated over elapsed time).
344///
345/// Returns the number of shares to mint for management fees.
346#[inline]
347#[must_use]
348pub fn compute_management_fee_shares(
349    fee_assets_base: u128,
350    cur_total_assets: u128,
351    total_supply: u128,
352    management_fee_wad: Wad,
353    last_timestamp_ns: u64,
354    now_ns: u64,
355) -> Number {
356    if management_fee_wad.is_zero() || total_supply == 0 || now_ns <= last_timestamp_ns {
357        return Number::zero();
358    }
359    let elapsed_ns = now_ns - last_timestamp_ns;
360    let annual_fee_assets = management_fee_wad.apply_floored(Number::from(fee_assets_base));
361    let fee_assets = mul_div_floor(
362        annual_fee_assets,
363        Number::from(u128::from(elapsed_ns)),
364        Number::from(u128::from(YEAR_NS)),
365    );
366    compute_fee_shares_from_assets(
367        fee_assets,
368        Number::from(cur_total_assets),
369        Number::from(total_supply),
370    )
371}
372
373#[cfg(test)]
374mod tests;