1#![doc = include_str!("../README.md")]
2pub mod dex;
75pub mod launchpad;
76pub mod nexus;
77
78pub mod constants;
79pub mod math;
80pub mod transfer_fee;
81pub mod transfer_hook;
82
83pub mod utils;
84
85#[cfg(test)]
86mod tests {
87
88 mod pda_tests {
89 use solana_address::address;
90
91 #[test]
92 fn launchpad_bonding_curve_pda() {
93 let base = address!("So11111111111111111111111111111111111111112");
94 let quote = address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
95 let (addr1, _) =
96 crate::launchpad::pda::find_bonding_curve_pda(&base, "e);
97 let (addr2, _) =
98 crate::launchpad::pda::find_bonding_curve_pda(&base, "e);
99 assert_eq!(addr1, addr2);
100 let (addr3, _) =
101 crate::launchpad::pda::find_bonding_curve_pda("e, &base);
102 assert_ne!(addr1, addr3);
103 }
104
105 #[test]
106 fn dex_pool_and_lp_mint_pdas_differ() {
107 let base = address!("So11111111111111111111111111111111111111112");
108 let quote = address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
109 let (pool, _) = crate::dex::pda::find_pool_pda(&base, "e);
110 let (lp, _) = crate::dex::pda::find_lp_mint_pda(&base, "e);
111 assert_ne!(pool, lp);
112 }
113
114 #[test]
115 fn nexus_staking_pdas() {
116 let user = address!("11111111111111111111111111111112");
117 let staking_mint =
118 address!("So11111111111111111111111111111111111111112");
119 let reward_mint =
120 address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
121
122 let (pos, _) = crate::nexus::pda::find_user_stake_position_pda(
123 &user,
124 &staking_mint,
125 );
126 let (debt, _) = crate::nexus::pda::find_user_reward_debt_pda(
127 &user,
128 &staking_mint,
129 &reward_mint,
130 );
131 assert_ne!(pos, debt);
132 }
133
134 #[test]
135 fn event_authority_pdas_differ_per_program() {
136 let (lp, _) = crate::launchpad::pda::find_event_authority_pda();
137 let (dex, _) = crate::dex::pda::find_event_authority_pda();
138 let (nexus, _) = crate::nexus::pda::find_event_authority_pda();
139 assert_ne!(lp, dex);
140 assert_ne!(dex, nexus);
141 assert_ne!(lp, nexus);
142 }
143 }
144
145 mod generated_constants {
146 use solana_address::Address;
147
148 struct PdaCheck<'a> {
149 label: &'a str,
150 address: Address,
151 bump: u8,
152 seeds: &'a [&'a [u8]],
153 program_id: Address,
154 }
155
156 #[track_caller]
157 fn assert_pda(check: PdaCheck<'_>) {
158 let (derived, derived_bump) =
159 Address::find_program_address(check.seeds, &check.program_id);
160 assert_eq!(check.address, derived, "{}: address", check.label);
161 assert_eq!(check.bump, derived_bump, "{}: bump", check.label);
162 }
163
164 #[test]
165 fn launchpad_pda_constants_match_derivation() {
166 use crate::launchpad::generated::SEND_LAUNCHPAD_ID as ID;
167 use crate::launchpad::pda::{
168 EVENT_AUTHORITY_ADDRESS, EVENT_AUTHORITY_BUMP,
169 EVENT_AUTHORITY_SEED, GLOBAL_CONFIG_ADDRESS,
170 GLOBAL_CONFIG_BUMP, GLOBAL_CONFIG_SEED,
171 MIGRATION_AUTHORITY_ADDRESS, MIGRATION_AUTHORITY_BUMP,
172 MIGRATION_AUTHORITY_SEED,
173 };
174
175 assert_pda(PdaCheck {
176 label: "launchpad global_config",
177 address: GLOBAL_CONFIG_ADDRESS,
178 bump: GLOBAL_CONFIG_BUMP,
179 seeds: &[GLOBAL_CONFIG_SEED],
180 program_id: ID,
181 });
182 assert_pda(PdaCheck {
183 label: "launchpad event_authority",
184 address: EVENT_AUTHORITY_ADDRESS,
185 bump: EVENT_AUTHORITY_BUMP,
186 seeds: &[EVENT_AUTHORITY_SEED],
187 program_id: ID,
188 });
189 assert_pda(PdaCheck {
190 label: "launchpad migration_authority",
191 address: MIGRATION_AUTHORITY_ADDRESS,
192 bump: MIGRATION_AUTHORITY_BUMP,
193 seeds: &[MIGRATION_AUTHORITY_SEED],
194 program_id: ID,
195 });
196 }
197
198 #[test]
199 fn dex_pda_constants_match_derivation() {
200 use crate::dex::generated::SEND_DEX_ID as ID;
201 use crate::dex::pda::{
202 EVENT_AUTHORITY_ADDRESS, EVENT_AUTHORITY_BUMP,
203 EVENT_AUTHORITY_SEED, GLOBAL_CONFIG_ADDRESS,
204 GLOBAL_CONFIG_BUMP, GLOBAL_CONFIG_SEED,
205 };
206
207 assert_pda(PdaCheck {
208 label: "dex global_config",
209 address: GLOBAL_CONFIG_ADDRESS,
210 bump: GLOBAL_CONFIG_BUMP,
211 seeds: &[GLOBAL_CONFIG_SEED],
212 program_id: ID,
213 });
214 assert_pda(PdaCheck {
215 label: "dex event_authority",
216 address: EVENT_AUTHORITY_ADDRESS,
217 bump: EVENT_AUTHORITY_BUMP,
218 seeds: &[EVENT_AUTHORITY_SEED],
219 program_id: ID,
220 });
221 }
222
223 #[test]
224 fn nexus_pda_constants_match_derivation() {
225 use crate::nexus::generated::SEND_NEXUS_ID as ID;
226 use crate::nexus::pda::{
227 ALT_REGISTRY_ADDRESS, ALT_REGISTRY_BUMP, ALT_REGISTRY_SEED,
228 EVENT_AUTHORITY_ADDRESS, EVENT_AUTHORITY_BUMP,
229 EVENT_AUTHORITY_SEED, GLOBAL_CONFIG_ADDRESS,
230 GLOBAL_CONFIG_BUMP, GLOBAL_CONFIG_SEED, STAKING_CONFIG_ADDRESS,
231 STAKING_CONFIG_BUMP, STAKING_CONFIG_SEED,
232 };
233
234 assert_pda(PdaCheck {
235 label: "nexus global_config",
236 address: GLOBAL_CONFIG_ADDRESS,
237 bump: GLOBAL_CONFIG_BUMP,
238 seeds: &[GLOBAL_CONFIG_SEED],
239 program_id: ID,
240 });
241 assert_pda(PdaCheck {
242 label: "nexus staking_config",
243 address: STAKING_CONFIG_ADDRESS,
244 bump: STAKING_CONFIG_BUMP,
245 seeds: &[STAKING_CONFIG_SEED],
246 program_id: ID,
247 });
248 assert_pda(PdaCheck {
249 label: "nexus event_authority",
250 address: EVENT_AUTHORITY_ADDRESS,
251 bump: EVENT_AUTHORITY_BUMP,
252 seeds: &[EVENT_AUTHORITY_SEED],
253 program_id: ID,
254 });
255 assert_pda(PdaCheck {
256 label: "nexus alt_registry",
257 address: ALT_REGISTRY_ADDRESS,
258 bump: ALT_REGISTRY_BUMP,
259 seeds: &[ALT_REGISTRY_SEED],
260 program_id: ID,
261 });
262 let platform =
264 crate::constants::find_platform_address(b"sendfun").0;
265 assert_eq!(
266 crate::nexus::pda::find_default_partner_pda(&platform),
267 crate::nexus::pda::find_partner_config_pda(
268 &platform,
269 &crate::constants::DEFAULT_PARTNER,
270 ),
271 );
272 assert_ne!(
273 crate::nexus::pda::find_default_partner_pda(&platform).0,
274 crate::nexus::pda::find_default_partner_pda(
275 &crate::constants::find_platform_address(b"acme").0
276 )
277 .0,
278 );
279 assert_eq!(
280 crate::nexus::pda::find_default_fee_preset_pda(&platform),
281 crate::nexus::pda::find_fee_preset_pda(&platform, 0),
282 );
283 assert_ne!(
285 crate::nexus::pda::find_default_fee_preset_pda(&platform).0,
286 crate::nexus::pda::find_default_fee_preset_pda(
287 &crate::constants::find_platform_address(b"acme").0
288 )
289 .0,
290 );
291 assert_eq!(
292 crate::nexus::pda::find_default_partner_metadata_pda(&platform),
293 crate::nexus::pda::find_partner_metadata_pda(
294 &platform,
295 &crate::constants::DEFAULT_PARTNER,
296 ),
297 );
298 assert_ne!(
300 crate::nexus::pda::find_default_partner_metadata_pda(&platform)
301 .0,
302 crate::nexus::pda::find_default_partner_metadata_pda(
303 &crate::constants::find_platform_address(b"acme").0
304 )
305 .0,
306 );
307 }
308
309 #[test]
312 fn sendfun_default_partner_addresses_are_pinned() {
313 use crate::constants::{DEFAULT_PARTNER, find_platform_address};
314 use solana_address::address;
315
316 let platform = find_platform_address(b"sendfun").0;
317 assert_eq!(
318 platform,
319 address!("2PJedAsa7pCnScks2XM3U4o2nPaTvvBmi2553VcCnGWB"),
320 "sendfun platform"
321 );
322
323 assert_eq!(
324 crate::nexus::pda::find_partner_config_pda(
325 &platform,
326 &DEFAULT_PARTNER
327 )
328 .0,
329 address!("B77P3Hvj9xLi4tcFtyjNcf6NjhkQrfJvLkz3nVZnBzec"),
330 "default partner config"
331 );
332 assert_eq!(
333 crate::nexus::pda::find_fee_preset_pda(&platform, 0).0,
334 address!("DTWXzPHfg7vjnhwhE7rxhdPZPGGFjuqD1excog3xmKTx"),
335 "default fee preset 0"
336 );
337 assert_eq!(
338 crate::nexus::pda::find_partner_metadata_pda(
339 &platform,
340 &DEFAULT_PARTNER
341 )
342 .0,
343 address!("8WqZvqH4B1LtcfxZHeAdYWCYvwfMaUkQvJqZsw4fmii5"),
344 "default partner metadata"
345 );
346 }
347 }
348
349 #[test]
350 fn bonding_curve_status_values() {
351 assert_eq!(
352 crate::launchpad::types::BondingCurveStatus::Funding as u8,
353 0
354 );
355 assert_eq!(
356 crate::launchpad::types::BondingCurveStatus::Completed as u8,
357 1
358 );
359 assert_eq!(
360 crate::launchpad::types::BondingCurveStatus::Migrated as u8,
361 2
362 );
363 assert_eq!(
364 crate::launchpad::types::BondingCurveStatus::Paused as u8,
365 3
366 );
367 }
368
369 #[test]
370 fn pool_status_values() {
371 assert_eq!(crate::dex::types::PoolStatus::Active as u8, 0);
372 assert_eq!(crate::dex::types::PoolStatus::Paused as u8, 1);
373 }
374
375 #[test]
376 fn launchpad_fees_total() {
377 let fees = crate::nexus::types::LaunchpadFees {
378 creation_fee_cents: 0,
379 protocol_fee_bps: 100,
380 max_creator_fee_bps: 50,
381 fee_decay_seconds: 0,
382 fee_decay_start_bps: 0,
383 };
384 assert_eq!(fees.total_fee_bps(30), Some(130));
385 }
386
387 #[test]
388 fn dex_fees_total() {
389 let fees = crate::nexus::types::DexFees {
390 creation_fee_cents: 0,
391 protocol_fee_bps: 80,
392 lp_fee_bps: 30,
393 max_creator_fee_bps: 40,
394 fee_decay_seconds: 0,
395 fee_decay_start_bps: 0,
396 };
397 assert_eq!(fees.total_fee_bps(25), Some(135));
398 }
399
400 mod from_account {
401 use borsh::BorshSerialize;
402 use solana_address::Address;
403
404 use crate::constants::{
405 DEX_PROGRAM_ID, LAUNCHPAD_PROGRAM_ID, NEXUS_PROGRAM_ID,
406 SYSTEM_PROGRAM_ID,
407 };
408 use crate::launchpad::accounts::{
409 BONDING_CURVE_DISCRIMINATOR, BondingCurve,
410 GLOBAL_CONFIG_DISCRIMINATOR, GlobalConfig,
411 };
412 use crate::launchpad::types::BondingCurveStatus;
413 use crate::nexus::accounts::StakingConfig;
414
415 const STRANGER: Address = Address::new_from_array([99; 32]);
416
417 fn curve() -> BondingCurve {
418 BondingCurve {
419 discriminator: BONDING_CURVE_DISCRIMINATOR,
420 version: 1,
421 bump: 254,
422 status: BondingCurveStatus::Funding,
423 base_mint: Address::new_from_array([1; 32]),
424 base_decimals: 6,
425 base_supply: 1_000_000_000_000_000,
426 initial_virtual_base: 1_073_000_000_000_000,
427 initial_virtual_quote: 30_000_000_000,
428 initial_real_base: 793_100_000_000_000,
429 quote_mint: Address::new_from_array([2; 32]),
430 quote_decimals: 9,
431 coin_creator: Address::new_from_array([3; 32]),
432 padding0: [0x0404_0404_0404_0404; 4],
433 partner: Address::new_from_array([5; 32]),
434 base_vault: Address::new_from_array([6; 32]),
435 quote_vault: Address::new_from_array([7; 32]),
436 virtual_base_reserves: 1_073_000_000_000_000,
437 virtual_quote_reserves: 30_000_000_000,
438 real_base_reserves: 793_100_000_000_000,
439 real_quote_reserves: 1,
440 created_at: 1_700_000_000,
441 protocol_owed: 0,
442 creator_owed: 0,
443 platform_config: Address::new_from_array([8; 32]),
444 creator_fee_mode: 0,
445 creator_fee_bps: 0,
446 dex_creator_fee_bps: 0,
447 reserved: [0; 256],
448 }
449 }
450
451 fn curve_data() -> Vec<u8> {
452 let mut data = Vec::new();
453 curve().serialize(&mut data).unwrap();
454 data
455 }
456
457 #[test]
458 fn decodes_an_account_its_program_owns() {
459 assert_eq!(
460 BondingCurve::from_account(
461 &LAUNCHPAD_PROGRAM_ID,
462 &curve_data()
463 )
464 .unwrap(),
465 curve()
466 );
467 }
468
469 #[test]
470 fn rejects_another_owner() {
471 let error = BondingCurve::from_account(&STRANGER, &curve_data())
472 .unwrap_err();
473 assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
474 assert_eq!(
475 error.to_string(),
476 format!(
477 "invalid account owner: {STRANGER}, expected {LAUNCHPAD_PROGRAM_ID}"
478 )
479 );
480 }
481
482 #[test]
483 fn rejects_another_accounts_discriminator() {
484 let error =
485 StakingConfig::from_account(&NEXUS_PROGRAM_ID, &curve_data())
486 .unwrap_err();
487 assert_eq!(error.to_string(), "invalid account discriminator");
488 }
489
490 #[test]
493 fn rejects_a_same_named_account_of_another_program() {
494 let config = GlobalConfig {
495 discriminator: GLOBAL_CONFIG_DISCRIMINATOR,
496 version: 1,
497 bump: 255,
498 authority: Address::new_from_array([1; 32]),
499 operator: Address::new_from_array([2; 32]),
500 create_token_disabled: false,
501 normalized_sol_price_cents: 15_000,
502 initial_base_supply: 1_000_000_000_000_000,
503 initial_virtual_base_reserves: 1_073_000_000_000_000,
504 initial_real_base_reserves: 793_100_000_000_000,
505 initial_virtual_quote_reserves: 30_000_000_000,
506 reserved: [0; 64],
507 };
508 let mut data = Vec::new();
509 config.serialize(&mut data).unwrap();
510
511 assert_eq!(
512 crate::dex::accounts::GLOBAL_CONFIG_DISCRIMINATOR,
513 GLOBAL_CONFIG_DISCRIMINATOR
514 );
515 assert!(
516 crate::dex::accounts::GlobalConfig::from_bytes(&data).is_ok()
517 );
518 let error = crate::dex::accounts::GlobalConfig::from_account(
519 &LAUNCHPAD_PROGRAM_ID,
520 &data,
521 )
522 .unwrap_err();
523 assert_eq!(
524 error.to_string(),
525 format!(
526 "invalid account owner: {LAUNCHPAD_PROGRAM_ID}, expected {DEX_PROGRAM_ID}"
527 )
528 );
529 }
530
531 #[test]
532 fn maybe_is_none_for_a_lamports_only_address() {
533 assert_eq!(
534 BondingCurve::from_account_maybe(&SYSTEM_PROGRAM_ID, &[])
535 .unwrap(),
536 None
537 );
538 }
539
540 #[test]
541 fn maybe_rejects_system_owned_data() {
542 let error = BondingCurve::from_account_maybe(
543 &SYSTEM_PROGRAM_ID,
544 &curve_data(),
545 )
546 .unwrap_err();
547 assert_eq!(
548 error.to_string(),
549 format!(
550 "invalid account owner: {SYSTEM_PROGRAM_ID}, expected {LAUNCHPAD_PROGRAM_ID}"
551 )
552 );
553 }
554
555 #[test]
556 fn maybe_rejects_empty_data_its_program_owns() {
557 let error =
558 BondingCurve::from_account_maybe(&LAUNCHPAD_PROGRAM_ID, &[])
559 .unwrap_err();
560 assert_eq!(error.to_string(), "invalid account discriminator");
561 }
562
563 #[test]
564 fn maybe_decodes_an_account_its_program_owns() {
565 assert_eq!(
566 BondingCurve::from_account_maybe(
567 &LAUNCHPAD_PROGRAM_ID,
568 &curve_data()
569 )
570 .unwrap(),
571 Some(curve())
572 );
573 }
574 }
575}