1use near_sdk::{json_types::U64, near};
2use templar_primitives::number::Decimal;
3
4use crate::asset::{AssetClass, FungibleAssetAmount};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
7#[near(serializers = [json, borsh])]
8pub enum Fee<T: AssetClass> {
9 Flat(FungibleAssetAmount<T>),
10 Proportional(Decimal),
11}
12
13impl<T: AssetClass> Fee<T> {
14 pub fn zero() -> Self {
15 Self::Flat(FungibleAssetAmount::zero())
16 }
17
18 pub fn of(&self, amount: FungibleAssetAmount<T>) -> Option<FungibleAssetAmount<T>> {
19 match self {
20 Fee::Flat(f) => Some(*f),
21 Fee::Proportional(factor) => (factor * u128::from(amount))
22 .to_u128_ceil()
23 .map(FungibleAssetAmount::new),
24 }
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29#[near(serializers = [json, borsh])]
30pub struct TimeBasedFee<T: AssetClass> {
31 pub fee: Fee<T>,
32 pub duration: U64,
33 pub behavior: TimeBasedFeeFunction,
34}
35
36impl<T: AssetClass> TimeBasedFee<T> {
37 pub fn zero() -> Self {
38 Self {
39 fee: Fee::Flat(0.into()),
40 duration: 0.into(),
41 behavior: TimeBasedFeeFunction::Fixed,
42 }
43 }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
47#[near(serializers = [json, borsh])]
48pub enum TimeBasedFeeFunction {
49 Fixed,
50 Linear,
51}
52
53impl<T: AssetClass> TimeBasedFee<T> {
54 pub fn of(
55 &self,
56 amount: FungibleAssetAmount<T>,
57 duration: u64,
58 ) -> Option<FungibleAssetAmount<T>> {
59 let base_fee = self.fee.of(amount)?;
60
61 if self.duration.0 == 0 {
62 return Some(0.into());
63 }
64
65 match self.behavior {
66 TimeBasedFeeFunction::Fixed => {
67 if duration >= self.duration.0 {
68 Some(0.into())
69 } else {
70 Some(base_fee)
71 }
72 }
73 TimeBasedFeeFunction::Linear => {
74 (Decimal::from(self.duration.0.saturating_sub(duration)) * u128::from(base_fee)
75 / Decimal::from(self.duration.0))
76 .to_u128_ceil()
77 .map(FungibleAssetAmount::new)
78 }
79 }
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use templar_primitives::dec;
86
87 use crate::asset::BorrowAsset;
88
89 use super::{TimeBasedFeeFunction::*, *};
90
91 type Amount = FungibleAssetAmount<BorrowAsset>;
92
93 fn time_based(
94 fee: Fee<BorrowAsset>,
95 duration_ms: u64,
96 behavior: TimeBasedFeeFunction,
97 ) -> TimeBasedFee<BorrowAsset> {
98 TimeBasedFee {
99 fee,
100 duration: duration_ms.into(),
101 behavior,
102 }
103 }
104
105 #[test]
106 fn flat_fee_is_constant() {
107 assert_eq!(
108 Fee::<BorrowAsset>::Flat(100.into()).of(1_000.into()),
109 Some(Amount::new(100)),
110 );
111 }
112
113 #[test]
114 fn proportional_fee_rounds_up() {
115 assert_eq!(
117 Fee::<BorrowAsset>::Proportional(dec!("0.001")).of(1_005.into()),
118 Some(Amount::new(2)),
119 );
120 }
121
122 #[test]
123 fn fixed_before_expiry_charges_full_fee() {
124 let f = time_based(Fee::Flat(100.into()), 1000 * 60 * 60 * 24 * 30, Fixed);
125 assert_eq!(f.of(1_000.into(), 10_000), Some(Amount::new(100)));
126 }
127
128 #[test]
129 fn fixed_at_or_after_expiry_charges_nothing() {
130 let f = time_based(Fee::Flat(100.into()), 1000, Fixed);
131 assert_eq!(f.of(1_000.into(), 1000), Some(Amount::new(0)));
132 assert_eq!(f.of(1_000.into(), 2000), Some(Amount::new(0)));
133 }
134
135 #[test]
136 fn zero_configured_duration_is_always_free() {
137 let f = time_based(Fee::Flat(100.into()), 0, Fixed);
138 assert_eq!(f.of(1_000.into(), 0), Some(Amount::new(0)));
139 }
140
141 #[test]
142 fn linear_interpolates_toward_expiry_and_rounds_up() {
143 let f = time_based(Fee::Flat(100.into()), 1000, Linear);
144 assert_eq!(f.of(1_000.into(), 0), Some(Amount::new(100)));
146 assert_eq!(f.of(1_000.into(), 250), Some(Amount::new(75)));
148 assert_eq!(f.of(1_000.into(), 251), Some(Amount::new(75)));
150 }
151
152 #[test]
153 fn linear_past_expiry_saturates_to_zero() {
154 let f = time_based(Fee::Flat(100.into()), 1000, Linear);
155 assert_eq!(f.of(1_000.into(), 5000), Some(Amount::new(0)));
156 }
157}