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 pub creator_fee_bps: u16,
14 pub created_at: i64,
16 pub now: i64,
18}
19
20impl types::LaunchpadFees {
21 #[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 #[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 #[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 #[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 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 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 #[test]
151 fn charges_the_curves_creator_rate_not_the_partner_max() {
152 assert_eq!(LAUNCHPAD.total_fee_bps(30), Some(130));
154 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 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 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 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 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}