Skip to main content

sendfun_sdk/
nexus.rs

1pub(crate) mod generated;
2
3pub use crate::constants::NEXUS_PROGRAM_ID as ID;
4pub use generated::{accounts, errors, events, instructions, types};
5
6pub mod pda;
7
8use crate::math::fee_decay::{FeeDecayArgs, calculate_fee_decay_premium};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct EffectiveFeeArgs {
12	/// `BondingCurve.creator_fee_bps` or `Pool.creator_fee_bps`.
13	pub creator_fee_bps: u16,
14	/// The market's creation time, in Unix seconds.
15	pub created_at: i64,
16	/// The time of the trade, in Unix seconds.
17	pub now: i64,
18}
19
20impl types::LaunchpadFees {
21	/// Returns `protocol_fee_bps` plus `creator_fee_bps`. Pass
22	/// `BondingCurve.creator_fee_bps`, not `max_creator_fee_bps`. Returns
23	/// `None` on overflow.
24	#[must_use]
25	pub const fn total_fee_bps(&self, creator_fee_bps: u16) -> Option<u16> {
26		self.protocol_fee_bps.checked_add(creator_fee_bps)
27	}
28
29	/// Returns `total_fee_bps` plus the fee decay premium at `args.now`.
30	/// Returns `None` on overflow or on a negative time.
31	#[must_use]
32	pub fn effective_fee_bps(&self, args: EffectiveFeeArgs) -> Option<u16> {
33		let standard_fee_bps = self.total_fee_bps(args.creator_fee_bps)?;
34		add_decay_premium(
35			standard_fee_bps,
36			FeeDecayArgs {
37				current_timestamp: u64::try_from(args.now).ok()?,
38				created_at_timestamp: u64::try_from(args.created_at).ok()?,
39				decay_seconds: self.fee_decay_seconds,
40				decay_start_bps: self.fee_decay_start_bps,
41				standard_fee_bps: u64::from(standard_fee_bps),
42			},
43		)
44	}
45}
46
47impl types::DexFees {
48	/// Returns `protocol_fee_bps` plus `lp_fee_bps` plus `creator_fee_bps`.
49	/// Pass `Pool.creator_fee_bps`, not `max_creator_fee_bps`. Returns `None`
50	/// on overflow.
51	#[must_use]
52	pub const fn total_fee_bps(&self, creator_fee_bps: u16) -> Option<u16> {
53		match self.protocol_fee_bps.checked_add(self.lp_fee_bps) {
54			Some(v) => v.checked_add(creator_fee_bps),
55			None => None,
56		}
57	}
58
59	/// Returns `total_fee_bps` plus the fee decay premium at `args.now`.
60	/// Returns `None` on overflow or on a negative time.
61	#[must_use]
62	pub fn effective_fee_bps(&self, args: EffectiveFeeArgs) -> Option<u16> {
63		let standard_fee_bps = self.total_fee_bps(args.creator_fee_bps)?;
64		add_decay_premium(
65			standard_fee_bps,
66			FeeDecayArgs {
67				current_timestamp: u64::try_from(args.now).ok()?,
68				created_at_timestamp: u64::try_from(args.created_at).ok()?,
69				decay_seconds: self.fee_decay_seconds,
70				decay_start_bps: self.fee_decay_start_bps,
71				standard_fee_bps: u64::from(standard_fee_bps),
72			},
73		)
74	}
75}
76
77fn add_decay_premium(standard_fee_bps: u16, args: FeeDecayArgs) -> Option<u16> {
78	let premium = calculate_fee_decay_premium(args)?;
79	standard_fee_bps.checked_add(u16::try_from(premium).ok()?)
80}
81
82#[cfg(test)]
83mod tests {
84	use super::EffectiveFeeArgs;
85	use super::types::{DexFees, LaunchpadFees};
86
87	/// The partner max differs from each market rate in the tests.
88	const LAUNCHPAD: LaunchpadFees = LaunchpadFees {
89		creation_fee_cents: 0,
90		protocol_fee_bps: 100,
91		max_creator_fee_bps: 50,
92		fee_decay_seconds: 12,
93		fee_decay_start_bps: 5_000,
94	};
95
96	const DEX: DexFees = DexFees {
97		creation_fee_cents: 0,
98		protocol_fee_bps: 80,
99		lp_fee_bps: 30,
100		max_creator_fee_bps: 40,
101		fee_decay_seconds: 12,
102		fee_decay_start_bps: 5_000,
103	};
104
105	const fn at(now: i64, creator_fee_bps: u16) -> EffectiveFeeArgs {
106		EffectiveFeeArgs {
107			creator_fee_bps,
108			created_at: 100,
109			now,
110		}
111	}
112
113	#[test]
114	fn charges_the_decay_start_rate_at_creation() {
115		assert_eq!(LAUNCHPAD.effective_fee_bps(at(100, 30)), Some(5_000));
116	}
117
118	#[test]
119	fn decays_quadratically_rounding_the_premium_up() {
120		// 130 standard + ceil(6^2 * 4_870 / 12^2) = 130 + 1_218.
121		assert_eq!(LAUNCHPAD.effective_fee_bps(at(106, 30)), Some(1_348));
122	}
123
124	#[test]
125	fn falls_to_the_standard_rate_when_the_window_closes() {
126		assert_eq!(LAUNCHPAD.effective_fee_bps(at(112, 30)), Some(130));
127	}
128
129	#[test]
130	fn charges_the_full_premium_on_a_creation_time_in_the_future() {
131		let args = EffectiveFeeArgs {
132			created_at: 200,
133			..at(100, 30)
134		};
135		assert_eq!(LAUNCHPAD.effective_fee_bps(args), Some(5_000));
136	}
137
138	#[test]
139	fn rejects_a_negative_timestamp() {
140		let args = EffectiveFeeArgs {
141			created_at: -1,
142			..at(100, 30)
143		};
144		assert_eq!(LAUNCHPAD.effective_fee_bps(args), None);
145		assert_eq!(LAUNCHPAD.effective_fee_bps(at(-1, 30)), None);
146	}
147
148	/// A market's rate can be above the partner max. A partner can lower its
149	/// max after the market's creation.
150	#[test]
151	fn charges_the_curves_creator_rate_not_the_partner_max() {
152		// Below the max of 50.
153		assert_eq!(LAUNCHPAD.total_fee_bps(30), Some(130));
154		// Above: 180 + 6^2 * 4_820 / 12^2 = 180 + 1_205, exact.
155		assert_eq!(LAUNCHPAD.total_fee_bps(80), Some(180));
156		assert_eq!(LAUNCHPAD.effective_fee_bps(at(106, 80)), Some(1_385));
157		assert_eq!(LAUNCHPAD.effective_fee_bps(at(112, 80)), Some(180));
158		// Rate 0: 100 + 6^2 * 4_900 / 12^2 = 100 + 1_225, exact.
159		assert_eq!(LAUNCHPAD.total_fee_bps(0), Some(100));
160		assert_eq!(LAUNCHPAD.effective_fee_bps(at(106, 0)), Some(1_325));
161		assert_eq!(LAUNCHPAD.effective_fee_bps(at(112, 0)), Some(100));
162	}
163
164	#[test]
165	fn charges_the_pools_creator_rate_not_the_partner_max() {
166		// Below the max of 40: 135 + ceil(6^2 * 4_865 / 12^2) = 135 + 1_217.
167		assert_eq!(DEX.total_fee_bps(25), Some(135));
168		assert_eq!(DEX.effective_fee_bps(at(106, 25)), Some(1_352));
169		assert_eq!(DEX.effective_fee_bps(at(112, 25)), Some(135));
170		// Above: 170 + ceil(6^2 * 4_830 / 12^2) = 170 + 1_208.
171		assert_eq!(DEX.total_fee_bps(60), Some(170));
172		assert_eq!(DEX.effective_fee_bps(at(106, 60)), Some(1_378));
173		assert_eq!(DEX.effective_fee_bps(at(112, 60)), Some(170));
174		// Rate 0: 110 + ceil(6^2 * 4_890 / 12^2) = 110 + 1_223.
175		assert_eq!(DEX.total_fee_bps(0), Some(110));
176		assert_eq!(DEX.effective_fee_bps(at(106, 0)), Some(1_333));
177		assert_eq!(DEX.effective_fee_bps(at(112, 0)), Some(110));
178	}
179
180	#[test]
181	fn the_partner_max_never_moves_the_fee() {
182		for max_creator_fee_bps in [0, u16::MAX] {
183			let launchpad = LaunchpadFees {
184				max_creator_fee_bps,
185				..LAUNCHPAD
186			};
187			assert_eq!(launchpad.total_fee_bps(30), Some(130));
188			assert_eq!(launchpad.effective_fee_bps(at(106, 30)), Some(1_348));
189
190			let dex = DexFees {
191				max_creator_fee_bps,
192				..DEX
193			};
194			assert_eq!(dex.total_fee_bps(25), Some(135));
195			assert_eq!(dex.effective_fee_bps(at(106, 25)), Some(1_352));
196		}
197	}
198
199	#[test]
200	fn a_total_past_u16_is_none() {
201		assert_eq!(LAUNCHPAD.total_fee_bps(65_435), Some(u16::MAX));
202		assert_eq!(LAUNCHPAD.total_fee_bps(65_436), None);
203		assert_eq!(LAUNCHPAD.effective_fee_bps(at(112, 65_436)), None);
204		assert_eq!(DEX.total_fee_bps(65_425), Some(u16::MAX));
205		assert_eq!(DEX.total_fee_bps(65_426), None);
206		assert_eq!(DEX.effective_fee_bps(at(112, 65_426)), None);
207	}
208
209	#[test]
210	fn sums_protocol_lp_and_creator_on_the_dex() {
211		let fees = DexFees {
212			fee_decay_seconds: 0,
213			fee_decay_start_bps: 0,
214			..DEX
215		};
216		assert_eq!(fees.effective_fee_bps(at(100, 25)), Some(135));
217	}
218}