Skip to main content

sendfun_sdk/math/
fees.rs

1pub struct FeeSplitArgs {
2	pub fee_amount: u64,
3	pub protocol_bps: u64,
4	pub lp_bps: u64,
5	pub base_total_bps: u64,
6	pub decay_premium_bps: u64,
7}
8
9/// Splits `fee_amount` into `(protocol, lp, creator, sniper)`. LP and creator
10/// round down. Protocol gets the decay premium and the remainder. `sniper` is
11/// the premium part of `protocol`.
12#[must_use]
13pub fn split_fee_amount(args: FeeSplitArgs) -> Option<(u64, u64, u64, u64)> {
14	let fee_amount = args.fee_amount;
15	// A zero fee splits even when the total rate is zero.
16	if fee_amount == 0 {
17		return Some((0, 0, 0, 0));
18	}
19	let effective_total_bps =
20		args.base_total_bps.checked_add(args.decay_premium_bps)?;
21	if effective_total_bps == 0 {
22		return None;
23	}
24
25	if args.base_total_bps == 0 {
26		return Some((fee_amount, 0, 0, fee_amount));
27	}
28
29	let lp_fee = u128::from(fee_amount)
30		.checked_mul(u128::from(args.lp_bps))?
31		.checked_div(u128::from(effective_total_bps))?;
32	let lp_fee = u64::try_from(lp_fee).ok()?;
33
34	let creator_bps = args
35		.base_total_bps
36		.checked_sub(args.protocol_bps)?
37		.checked_sub(args.lp_bps)?;
38	let creator_fee = u128::from(fee_amount)
39		.checked_mul(u128::from(creator_bps))?
40		.checked_div(u128::from(effective_total_bps))?;
41	let creator_fee = u64::try_from(creator_fee).ok()?;
42
43	let sniper_fee = u128::from(fee_amount)
44		.checked_mul(u128::from(args.decay_premium_bps))?
45		.checked_div(u128::from(effective_total_bps))?;
46	let sniper_fee = u64::try_from(sniper_fee).ok()?;
47
48	let protocol_fee =
49		fee_amount.checked_sub(lp_fee)?.checked_sub(creator_fee)?;
50
51	Some((protocol_fee, lp_fee, creator_fee, sniper_fee))
52}
53
54#[cfg(test)]
55mod tests {
56	use super::*;
57
58	const STANDARD: FeeSplitArgs = FeeSplitArgs {
59		fee_amount: 1000,
60		protocol_bps: 200,
61		lp_bps: 200,
62		base_total_bps: 1000,
63		decay_premium_bps: 0,
64	};
65
66	#[test]
67	fn test_split_fee_amount_no_dust_lost() {
68		let (protocol_fee, lp_fee, creator_fee, sniper_fee) =
69			split_fee_amount(STANDARD).unwrap();
70
71		assert_eq!(protocol_fee, 200);
72		assert_eq!(lp_fee, 200);
73		assert_eq!(creator_fee, 600);
74		assert_eq!(sniper_fee, 0);
75		assert_eq!(protocol_fee + lp_fee + creator_fee, 1000);
76	}
77
78	#[test]
79	fn test_split_fee_amount_protocol_wins_rounding() {
80		let (protocol_fee, lp_fee, creator_fee, sniper_fee) =
81			split_fee_amount(FeeSplitArgs {
82				fee_amount: 1001,
83				..STANDARD
84			})
85			.unwrap();
86
87		assert_eq!(lp_fee, 200);
88		assert_eq!(creator_fee, 600);
89		assert_eq!(protocol_fee, 201);
90		assert_eq!(sniper_fee, 0);
91		assert_eq!(protocol_fee + lp_fee + creator_fee, 1001);
92	}
93
94	#[test]
95	fn test_split_fee_with_decay_protocol_wins_rounding() {
96		let (protocol_fee, lp_fee, creator_fee, sniper_fee) =
97			split_fee_amount(FeeSplitArgs {
98				fee_amount: 1001,
99				decay_premium_bps: 500,
100				..STANDARD
101			})
102			.unwrap();
103
104		assert_eq!(lp_fee, 133);
105		assert_eq!(creator_fee, 400);
106		assert_eq!(protocol_fee, 468);
107		assert_eq!(sniper_fee, 333);
108		assert_eq!(protocol_fee + lp_fee + creator_fee, 1001);
109	}
110
111	#[test]
112	fn test_split_fee_max_amount() {
113		let fee_amount = 1_000_000_000_000_000u64;
114		let result = split_fee_amount(FeeSplitArgs {
115			fee_amount,
116			protocol_bps: 100,
117			lp_bps: 100,
118			base_total_bps: 300,
119			decay_premium_bps: 0,
120		});
121		assert!(result.is_some());
122		let (protocol, lp, creator, _decay) = result.unwrap();
123		assert_eq!(protocol + lp + creator, fee_amount);
124	}
125
126	#[test]
127	fn test_split_fee_zero_amount() {
128		assert_eq!(
129			split_fee_amount(FeeSplitArgs {
130				fee_amount: 0,
131				protocol_bps: 100,
132				lp_bps: 100,
133				base_total_bps: 300,
134				decay_premium_bps: 0,
135			}),
136			Some((0, 0, 0, 0)),
137		);
138	}
139
140	#[test]
141	fn test_split_fee_invalid_bps_underflows() {
142		assert_eq!(
143			split_fee_amount(FeeSplitArgs {
144				protocol_bps: 600,
145				lp_bps: 500,
146				..STANDARD
147			}),
148			None,
149		);
150	}
151
152	#[test]
153	fn test_split_fee_zero_total_bps() {
154		assert_eq!(
155			split_fee_amount(FeeSplitArgs {
156				fee_amount: 1000,
157				protocol_bps: 0,
158				lp_bps: 0,
159				base_total_bps: 0,
160				decay_premium_bps: 0,
161			}),
162			None,
163		);
164	}
165
166	#[test]
167	fn test_split_fee_zero_amount_and_zero_total_bps() {
168		assert_eq!(
169			split_fee_amount(FeeSplitArgs {
170				fee_amount: 0,
171				protocol_bps: 0,
172				lp_bps: 0,
173				base_total_bps: 0,
174				decay_premium_bps: 0,
175			}),
176			Some((0, 0, 0, 0)),
177		);
178	}
179}