Skip to main content

sendfun_sdk/math/
fee_decay.rs

1pub 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/// Fee decay premium, in bps. It falls quadratically from
10/// `decay_start_bps - standard_fee_bps` at creation to 0 at `decay_seconds`.
11#[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	// A creation time in the future pays the full premium.
20	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	// Rounds up. The payer pays the remainder.
45	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}