1use std::ops::Deref;
2
3use near_sdk::{near, require};
4use templar_primitives::number::Decimal;
5
6pub trait UsageCurve {
7 fn at(&self, usage_ratio: Decimal) -> Decimal;
8}
9
10#[derive(Clone, Debug, PartialEq, Eq)]
11#[near(serializers = [json, borsh])]
12pub enum InterestRateStrategy {
13 Linear(Linear),
14 Piecewise(Piecewise),
15 Exponential2(Exponential2),
16}
17
18impl InterestRateStrategy {
19 pub const fn zero() -> Self {
20 Self::Linear(Linear {
21 base: Decimal::ZERO,
22 top: Decimal::ZERO,
23 })
24 }
25
26 #[must_use]
27 pub fn linear(base: Decimal, top: Decimal) -> Option<Self> {
28 Some(Self::Linear(Linear::new(base, top)?))
29 }
30
31 #[must_use]
32 pub fn piecewise(
33 base: Decimal,
34 optimal: Decimal,
35 rate_1: Decimal,
36 rate_2: Decimal,
37 ) -> Option<Self> {
38 Some(Self::Piecewise(Piecewise::new(
39 base, optimal, rate_1, rate_2,
40 )?))
41 }
42
43 #[must_use]
44 pub fn exponential2(base: Decimal, top: Decimal, eccentricity: Decimal) -> Option<Self> {
45 Some(Self::Exponential2(Exponential2::new(
46 base,
47 top,
48 eccentricity,
49 )?))
50 }
51}
52
53impl Deref for InterestRateStrategy {
54 type Target = dyn UsageCurve;
55
56 fn deref(&self) -> &Self::Target {
57 match self {
58 Self::Linear(linear) => linear as &dyn UsageCurve,
59 Self::Piecewise(piecewise) => piecewise as &dyn UsageCurve,
60 Self::Exponential2(exponential2) => exponential2 as &dyn UsageCurve,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
69#[near(serializers = [borsh, json])]
70pub struct Linear {
71 base: Decimal,
72 top: Decimal,
73}
74
75impl Linear {
76 pub fn new(base: Decimal, top: Decimal) -> Option<Self> {
77 (base <= top).then_some(Self { base, top })
78 }
79}
80
81impl UsageCurve for Linear {
82 fn at(&self, usage_ratio: Decimal) -> Decimal {
83 usage_ratio * (self.top - self.base) + self.base
84 }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq)]
94#[near(serializers = [borsh, json])]
95#[serde(try_from = "PiecewiseParams", into = "PiecewiseParams")]
96pub struct Piecewise {
97 params: PiecewiseParams,
98 i_negative_rate_2_b: Decimal,
99}
100
101impl Piecewise {
102 pub fn new(base: Decimal, optimal: Decimal, rate_1: Decimal, rate_2: Decimal) -> Option<Self> {
103 if optimal > 1u32 {
104 return None;
105 }
106
107 if rate_1 > rate_2 {
108 return None;
109 }
110
111 Some(Self {
112 i_negative_rate_2_b: optimal * (rate_2 - rate_1) - base,
113 params: PiecewiseParams {
114 base,
115 optimal,
116 rate_1,
117 rate_2,
118 },
119 })
120 }
121}
122
123impl UsageCurve for Piecewise {
124 fn at(&self, usage_ratio: Decimal) -> Decimal {
125 require!(
126 usage_ratio <= Decimal::ONE,
127 "Invariant violation: Usage ratio cannot be over 100%.",
128 );
129
130 if usage_ratio < self.params.optimal {
131 self.params.rate_1 * usage_ratio + self.params.base
132 } else {
133 self.params.rate_2 * usage_ratio - self.i_negative_rate_2_b
134 }
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
139#[near(serializers = [json, borsh])]
140pub struct PiecewiseParams {
141 base: Decimal,
142 optimal: Decimal,
143 rate_1: Decimal,
144 rate_2: Decimal,
145}
146
147impl TryFrom<PiecewiseParams> for Piecewise {
148 type Error = &'static str;
149
150 fn try_from(
151 PiecewiseParams {
152 base,
153 optimal,
154 rate_1,
155 rate_2,
156 }: PiecewiseParams,
157 ) -> Result<Self, Self::Error> {
158 Self::new(base, optimal, rate_1, rate_2).ok_or("Invalid Piecewise parameters")
159 }
160}
161
162impl From<Piecewise> for PiecewiseParams {
163 fn from(value: Piecewise) -> Self {
164 value.params
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
172#[near(serializers = [borsh, json])]
173#[serde(try_from = "Exponential2Params", into = "Exponential2Params")]
174pub struct Exponential2 {
175 params: Exponential2Params,
176 i_factor: Decimal,
177}
178
179impl Exponential2 {
180 pub fn new(base: Decimal, top: Decimal, eccentricity: Decimal) -> Option<Self> {
183 if base > top {
184 return None;
185 }
186
187 if eccentricity > 24u32 || eccentricity.is_zero() {
188 return None;
189 }
190
191 #[allow(clippy::unwrap_used, reason = "Invariant checked above")]
192 Some(Self {
193 i_factor: (top - base) / (eccentricity.pow2().unwrap() - 1u32),
194 params: Exponential2Params {
195 base,
196 top,
197 eccentricity,
198 },
199 })
200 }
201}
202
203impl UsageCurve for Exponential2 {
204 fn at(&self, usage_ratio: Decimal) -> Decimal {
205 require!(
206 usage_ratio <= Decimal::ONE,
207 "Invariant violation: Usage ratio cannot be over 100%.",
208 );
209
210 #[allow(clippy::unwrap_used, reason = "Invariant checked above")]
211 (self.params.base
212 + self.i_factor * ((self.params.eccentricity * usage_ratio).pow2().unwrap() - 1u32))
213 }
214}
215
216#[derive(Debug, Clone, PartialEq, Eq)]
217#[near(serializers = [json, borsh])]
218pub struct Exponential2Params {
219 base: Decimal,
220 top: Decimal,
221 eccentricity: Decimal,
222}
223
224impl TryFrom<Exponential2Params> for Exponential2 {
225 type Error = &'static str;
226
227 fn try_from(
228 Exponential2Params {
229 base,
230 top,
231 eccentricity,
232 }: Exponential2Params,
233 ) -> Result<Self, Self::Error> {
234 Self::new(base, top, eccentricity).ok_or("Invalid Exponential2 parameters")
235 }
236}
237
238impl From<Exponential2> for Exponential2Params {
239 fn from(value: Exponential2) -> Self {
240 value.params
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use std::ops::Div;
247
248 use templar_primitives::dec;
249
250 use super::*;
251
252 #[test]
253 fn piecewise() {
254 let s = Piecewise::new(Decimal::ZERO, dec!("0.9"), dec!("0.035"), dec!("0.6")).unwrap();
255
256 assert!(s.at(Decimal::ZERO).near_equal(Decimal::ZERO));
257 assert!(s.at(dec!("0.1")).near_equal(dec!("0.0035")));
258 assert!(s.at(dec!("0.5")).near_equal(dec!("0.0175")));
259 assert!(s.at(dec!("0.6")).near_equal(dec!("0.021")));
260 assert!(s.at(dec!("0.9")).near_equal(dec!("0.0315")));
261 assert!(s.at(dec!("0.95")).near_equal(dec!("0.0615")));
262 assert!(s.at(Decimal::ONE).near_equal(dec!("0.0915")));
263 }
264
265 #[test]
273 #[should_panic(expected = "overflow")]
276 fn piecewise_new_underflows_when_base_exceeds_cross_term() {
277 let _ = Piecewise::new(dec!("0.5"), dec!("0.9"), dec!("0.0"), dec!("0.1"));
279 }
280
281 #[test]
282 fn exponential2() {
283 let s = Exponential2::new(dec!("0.005"), dec!("0.08"), dec!("6")).unwrap();
284 assert!(s.at(Decimal::ZERO).near_equal(dec!("0.005")));
285 assert!(s.at(dec!("0.25")).near_equal(dec!(
286 "0.00717669895803117868762306839097547161564207589375463826946828509045412494"
287 )));
288 assert!(s.at(Decimal::ONE_HALF).near_equal(Decimal::ONE.div(75u32)));
289 }
290}