sendfun_sdk/math/
fee_decay.rs1pub struct FeeDecayArgs {
2 pub current_timestamp: u64,
3 pub created_at_timestamp: u64,
4 pub decay_seconds: u16,
5 pub decay_start_bps: u16,
6 pub standard_fee_bps: u64,
7}
8
9#[must_use]
12pub fn calculate_fee_decay_premium(args: FeeDecayArgs) -> Option<u64> {
13 if args.decay_seconds == 0 {
14 return Some(0);
15 }
16
17 let start_bps = u64::from(args.decay_start_bps);
18
19 let Some(elapsed) = args
21 .current_timestamp
22 .checked_sub(args.created_at_timestamp)
23 else {
24 return start_bps.checked_sub(args.standard_fee_bps).or(Some(0));
25 };
26 let decay_seconds_u64 = u64::from(args.decay_seconds);
27
28 if elapsed >= decay_seconds_u64 {
29 return Some(0);
30 }
31 if start_bps <= args.standard_fee_bps {
32 return Some(0);
33 }
34
35 let range = start_bps.checked_sub(args.standard_fee_bps)?;
36 let remaining = decay_seconds_u64.checked_sub(elapsed)?;
37
38 let numerator = u128::from(remaining)
39 .checked_mul(u128::from(remaining))?
40 .checked_mul(u128::from(range))?;
41 let denominator = u128::from(decay_seconds_u64)
42 .checked_mul(u128::from(decay_seconds_u64))?;
43
44 let mut result = numerator.checked_div(denominator)?;
46 let remainder = numerator.checked_rem(denominator)?;
47 if remainder > 0 {
48 result = result.checked_add(1)?;
49 }
50 u64::try_from(result).ok()
51}
52
53#[cfg(test)]
54mod tests {
55 use super::*;
56
57 const STANDARD: FeeDecayArgs = FeeDecayArgs {
58 current_timestamp: 100,
59 created_at_timestamp: 100,
60 decay_seconds: 12,
61 decay_start_bps: 5000,
62 standard_fee_bps: 100,
63 };
64
65 #[test]
66 fn test_decay_disabled() {
67 assert_eq!(
68 calculate_fee_decay_premium(FeeDecayArgs {
69 created_at_timestamp: 50,
70 decay_seconds: 0,
71 ..STANDARD
72 }),
73 Some(0),
74 );
75 }
76
77 #[test]
78 fn test_decay_at_start() {
79 assert_eq!(calculate_fee_decay_premium(STANDARD), Some(4900));
80 }
81
82 #[test]
83 fn test_decay_past_window() {
84 assert_eq!(
85 calculate_fee_decay_premium(FeeDecayArgs {
86 current_timestamp: 200,
87 ..STANDARD
88 }),
89 Some(0),
90 );
91 }
92
93 #[test]
94 fn test_decay_exactly_at_window_end() {
95 assert_eq!(
96 calculate_fee_decay_premium(FeeDecayArgs {
97 current_timestamp: 112,
98 ..STANDARD
99 }),
100 Some(0),
101 );
102 }
103
104 #[test]
105 fn test_decay_midpoint() {
106 assert_eq!(
107 calculate_fee_decay_premium(FeeDecayArgs {
108 current_timestamp: 106,
109 ..STANDARD
110 }),
111 Some(1225),
112 );
113 }
114
115 #[test]
116 fn test_decay_one_second_remaining() {
117 assert_eq!(
118 calculate_fee_decay_premium(FeeDecayArgs {
119 current_timestamp: 111,
120 ..STANDARD
121 }),
122 Some(35),
123 );
124 }
125
126 #[test]
127 fn test_decay_start_less_than_standard() {
128 assert_eq!(
129 calculate_fee_decay_premium(FeeDecayArgs {
130 decay_start_bps: 100,
131 ..STANDARD
132 }),
133 Some(0),
134 );
135 assert_eq!(
136 calculate_fee_decay_premium(FeeDecayArgs {
137 decay_start_bps: 50,
138 ..STANDARD
139 }),
140 Some(0),
141 );
142 }
143
144 #[test]
145 fn test_decay_created_in_future() {
146 assert_eq!(
147 calculate_fee_decay_premium(FeeDecayArgs {
148 created_at_timestamp: 200,
149 ..STANDARD
150 }),
151 Some(4900),
152 );
153 }
154
155 #[test]
156 fn test_overflow_protection() {
157 assert_eq!(
158 calculate_fee_decay_premium(FeeDecayArgs {
159 current_timestamp: 0,
160 created_at_timestamp: 0,
161 decay_seconds: u16::MAX,
162 decay_start_bps: 10000,
163 standard_fee_bps: 100,
164 }),
165 Some(9900),
166 );
167 }
168
169 #[test]
170 fn test_max_bps_u64_downcast_safe() {
171 assert_eq!(
172 calculate_fee_decay_premium(FeeDecayArgs {
173 current_timestamp: 0,
174 created_at_timestamp: 0,
175 decay_seconds: u16::MAX,
176 decay_start_bps: u16::MAX,
177 standard_fee_bps: 0,
178 }),
179 Some(u64::from(u16::MAX)),
180 );
181 }
182}