1use {
4 crate::{
5 error::SinglePoolError,
6 inline_mpl_token_metadata::{
7 self,
8 instruction::{create_metadata_accounts_v3, update_metadata_accounts_v2},
9 pda::find_metadata_account,
10 state::DataV2,
11 },
12 instruction::SinglePoolInstruction,
13 state::{SinglePool, SinglePoolAccountType},
14 MINT_DECIMALS, PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH, POOL_MINT_AUTHORITY_PREFIX,
15 POOL_MINT_PREFIX, POOL_MPL_AUTHORITY_PREFIX, POOL_ONRAMP_PREFIX, POOL_PREFIX,
16 POOL_STAKE_AUTHORITY_PREFIX, POOL_STAKE_PREFIX, VOTE_STATE_AUTHORIZED_WITHDRAWER_END,
17 VOTE_STATE_AUTHORIZED_WITHDRAWER_START, VOTE_STATE_DISCRIMINATOR_END,
18 },
19 borsh::BorshDeserialize,
20 solana_program::{
21 account_info::{next_account_info, AccountInfo},
22 borsh1::{get_packed_len, try_from_slice_unchecked},
23 entrypoint::ProgramResult,
24 msg,
25 native_token::LAMPORTS_PER_SOL,
26 program::invoke_signed,
27 program_error::ProgramError,
28 program_pack::Pack,
29 pubkey::Pubkey,
30 rent::Rent,
31 stake::{
32 self,
33 state::{Meta, Stake, StakeActivationStatus, StakeStateV2},
34 },
35 stake_history::Epoch,
36 system_instruction, system_program,
37 sysvar::{clock::Clock, stake_history::StakeHistorySysvar, Sysvar},
38 vote::program as vote_program,
39 },
40 spl_token::state::Mint,
41};
42
43fn calculate_deposit_amount(
46 pre_token_supply: u64,
47 pre_pool_stake: u64,
48 user_stake_to_deposit: u64,
49) -> Option<u64> {
50 if pre_pool_stake == 0 || pre_token_supply == 0 {
51 Some(user_stake_to_deposit)
52 } else {
53 u64::try_from(
54 (user_stake_to_deposit as u128)
55 .checked_mul(pre_token_supply as u128)?
56 .checked_div(pre_pool_stake as u128)?,
57 )
58 .ok()
59 }
60}
61
62fn calculate_withdraw_amount(
65 pre_token_supply: u64,
66 pre_pool_stake: u64,
67 user_tokens_to_burn: u64,
68) -> Option<u64> {
69 let numerator = (user_tokens_to_burn as u128).checked_mul(pre_pool_stake as u128)?;
70 let denominator = pre_token_supply as u128;
71 if numerator < denominator || denominator == 0 {
72 Some(0)
73 } else {
74 u64::try_from(numerator.checked_div(denominator)?).ok()
75 }
76}
77
78fn get_stake_state(stake_account_info: &AccountInfo) -> Result<(Meta, Stake), ProgramError> {
80 let stake_state = try_from_slice_unchecked::<StakeStateV2>(&stake_account_info.data.borrow())?;
81
82 match stake_state {
83 StakeStateV2::Stake(meta, stake, _) => Ok((meta, stake)),
84 _ => Err(SinglePoolError::WrongStakeStake.into()),
85 }
86}
87
88fn get_stake_amount(stake_account_info: &AccountInfo) -> Result<u64, ProgramError> {
90 Ok(get_stake_state(stake_account_info)?.1.delegation.stake)
91}
92
93fn is_stake_active_without_history(stake: &Stake, current_epoch: Epoch) -> bool {
95 stake.delegation.activation_epoch < current_epoch
96 && stake.delegation.deactivation_epoch == Epoch::MAX
97}
98
99fn is_stake_fully_active(stake_activation_status: &StakeActivationStatus) -> bool {
101 matches!(stake_activation_status, StakeActivationStatus {
102 effective,
103 activating: 0,
104 deactivating: 0,
105 } if *effective > 0)
106}
107
108fn check_pool_address(
110 program_id: &Pubkey,
111 vote_account_address: &Pubkey,
112 check_address: &Pubkey,
113) -> Result<u8, ProgramError> {
114 check_pool_pda(
115 program_id,
116 vote_account_address,
117 check_address,
118 &crate::find_pool_address_and_bump,
119 "pool",
120 SinglePoolError::InvalidPoolAccount,
121 )
122}
123
124fn check_pool_stake_address(
126 program_id: &Pubkey,
127 pool_address: &Pubkey,
128 check_address: &Pubkey,
129) -> Result<u8, ProgramError> {
130 check_pool_pda(
131 program_id,
132 pool_address,
133 check_address,
134 &crate::find_pool_stake_address_and_bump,
135 "stake account",
136 SinglePoolError::InvalidPoolStakeAccount,
137 )
138}
139
140fn check_pool_onramp_address(
142 program_id: &Pubkey,
143 pool_address: &Pubkey,
144 check_address: &Pubkey,
145) -> Result<u8, ProgramError> {
146 check_pool_pda(
147 program_id,
148 pool_address,
149 check_address,
150 &crate::find_pool_onramp_address_and_bump,
151 "onramp account",
152 SinglePoolError::InvalidPoolOnRampAccount,
153 )
154}
155
156fn check_pool_mint_address(
158 program_id: &Pubkey,
159 pool_address: &Pubkey,
160 check_address: &Pubkey,
161) -> Result<u8, ProgramError> {
162 check_pool_pda(
163 program_id,
164 pool_address,
165 check_address,
166 &crate::find_pool_mint_address_and_bump,
167 "mint",
168 SinglePoolError::InvalidPoolMint,
169 )
170}
171
172fn check_pool_stake_authority_address(
174 program_id: &Pubkey,
175 pool_address: &Pubkey,
176 check_address: &Pubkey,
177) -> Result<u8, ProgramError> {
178 check_pool_pda(
179 program_id,
180 pool_address,
181 check_address,
182 &crate::find_pool_stake_authority_address_and_bump,
183 "stake authority",
184 SinglePoolError::InvalidPoolStakeAuthority,
185 )
186}
187
188fn check_pool_mint_authority_address(
190 program_id: &Pubkey,
191 pool_address: &Pubkey,
192 check_address: &Pubkey,
193) -> Result<u8, ProgramError> {
194 check_pool_pda(
195 program_id,
196 pool_address,
197 check_address,
198 &crate::find_pool_mint_authority_address_and_bump,
199 "mint authority",
200 SinglePoolError::InvalidPoolMintAuthority,
201 )
202}
203
204fn check_pool_mpl_authority_address(
206 program_id: &Pubkey,
207 pool_address: &Pubkey,
208 check_address: &Pubkey,
209) -> Result<u8, ProgramError> {
210 check_pool_pda(
211 program_id,
212 pool_address,
213 check_address,
214 &crate::find_pool_mpl_authority_address_and_bump,
215 "MPL authority",
216 SinglePoolError::InvalidPoolMplAuthority,
217 )
218}
219
220fn check_pool_pda(
221 program_id: &Pubkey,
222 base_address: &Pubkey,
223 check_address: &Pubkey,
224 pda_lookup_fn: &dyn Fn(&Pubkey, &Pubkey) -> (Pubkey, u8),
225 pda_name: &str,
226 pool_error: SinglePoolError,
227) -> Result<u8, ProgramError> {
228 let (derived_address, bump_seed) = pda_lookup_fn(program_id, base_address);
229 if *check_address != derived_address {
230 msg!(
231 "Incorrect {} address for base {}: expected {}, received {}",
232 pda_name,
233 base_address,
234 derived_address,
235 check_address,
236 );
237 Err(pool_error.into())
238 } else {
239 Ok(bump_seed)
240 }
241}
242
243fn check_vote_account(vote_account_info: &AccountInfo) -> Result<(), ProgramError> {
245 check_account_owner(vote_account_info, &vote_program::id())?;
246
247 let vote_account_data = &vote_account_info.try_borrow_data()?;
248 let state_variant = vote_account_data
249 .get(..VOTE_STATE_DISCRIMINATOR_END)
250 .and_then(|s| s.try_into().ok())
251 .ok_or(SinglePoolError::UnparseableVoteAccount)?;
252
253 match u32::from_le_bytes(state_variant) {
254 1 | 2 => Ok(()),
255 0 => Err(SinglePoolError::LegacyVoteAccount.into()),
256 _ => Err(SinglePoolError::UnparseableVoteAccount.into()),
257 }
258}
259
260fn check_mpl_metadata_account_address(
262 metadata_address: &Pubkey,
263 pool_mint: &Pubkey,
264) -> Result<(), ProgramError> {
265 let (metadata_account_pubkey, _) = find_metadata_account(pool_mint);
266 if metadata_account_pubkey != *metadata_address {
267 Err(SinglePoolError::InvalidMetadataAccount.into())
268 } else {
269 Ok(())
270 }
271}
272
273fn check_system_program(program_id: &Pubkey) -> Result<(), ProgramError> {
275 if *program_id != system_program::id() {
276 msg!(
277 "Expected system program {}, received {}",
278 system_program::id(),
279 program_id
280 );
281 Err(ProgramError::IncorrectProgramId)
282 } else {
283 Ok(())
284 }
285}
286
287fn check_token_program(address: &Pubkey) -> Result<(), ProgramError> {
289 if *address != spl_token::id() {
290 msg!(
291 "Incorrect token program, expected {}, received {}",
292 spl_token::id(),
293 address
294 );
295 Err(ProgramError::IncorrectProgramId)
296 } else {
297 Ok(())
298 }
299}
300
301fn check_stake_program(program_id: &Pubkey) -> Result<(), ProgramError> {
303 if *program_id != stake::program::id() {
304 msg!(
305 "Expected stake program {}, received {}",
306 stake::program::id(),
307 program_id
308 );
309 Err(ProgramError::IncorrectProgramId)
310 } else {
311 Ok(())
312 }
313}
314
315fn check_mpl_metadata_program(program_id: &Pubkey) -> Result<(), ProgramError> {
317 if *program_id != inline_mpl_token_metadata::id() {
318 msg!(
319 "Expected MPL metadata program {}, received {}",
320 inline_mpl_token_metadata::id(),
321 program_id
322 );
323 Err(ProgramError::IncorrectProgramId)
324 } else {
325 Ok(())
326 }
327}
328
329fn check_account_owner(
331 account_info: &AccountInfo,
332 program_id: &Pubkey,
333) -> Result<(), ProgramError> {
334 if *program_id != *account_info.owner {
335 msg!(
336 "Expected account to be owned by program {}, received {}",
337 program_id,
338 account_info.owner
339 );
340 Err(ProgramError::IncorrectProgramId)
341 } else {
342 Ok(())
343 }
344}
345
346fn minimum_pool_balance() -> Result<u64, ProgramError> {
350 Ok(std::cmp::max(
351 stake::tools::get_minimum_delegation()?,
352 LAMPORTS_PER_SOL,
353 ))
354}
355
356pub struct Processor {}
358impl Processor {
359 #[allow(clippy::too_many_arguments)]
360 fn stake_merge<'a>(
361 pool_account_key: &Pubkey,
362 source_account: AccountInfo<'a>,
363 authority: AccountInfo<'a>,
364 bump_seed: u8,
365 destination_account: AccountInfo<'a>,
366 clock: AccountInfo<'a>,
367 stake_history: AccountInfo<'a>,
368 ) -> Result<(), ProgramError> {
369 let authority_seeds = &[
370 POOL_STAKE_AUTHORITY_PREFIX,
371 pool_account_key.as_ref(),
372 &[bump_seed],
373 ];
374 let signers = &[&authority_seeds[..]];
375
376 invoke_signed(
377 &stake::instruction::merge(destination_account.key, source_account.key, authority.key)
378 [0],
379 &[
380 destination_account,
381 source_account,
382 clock,
383 stake_history,
384 authority,
385 ],
386 signers,
387 )
388 }
389
390 fn stake_split<'a>(
391 pool_account_key: &Pubkey,
392 stake_account: AccountInfo<'a>,
393 authority: AccountInfo<'a>,
394 bump_seed: u8,
395 amount: u64,
396 split_stake: AccountInfo<'a>,
397 ) -> Result<(), ProgramError> {
398 let authority_seeds = &[
399 POOL_STAKE_AUTHORITY_PREFIX,
400 pool_account_key.as_ref(),
401 &[bump_seed],
402 ];
403 let signers = &[&authority_seeds[..]];
404
405 let split_instruction =
406 stake::instruction::split(stake_account.key, authority.key, amount, split_stake.key);
407
408 invoke_signed(
409 split_instruction.last().unwrap(),
410 &[stake_account, split_stake, authority],
411 signers,
412 )
413 }
414
415 #[allow(clippy::too_many_arguments)]
416 fn stake_authorize<'a>(
417 pool_account_key: &Pubkey,
418 stake_account: AccountInfo<'a>,
419 stake_authority: AccountInfo<'a>,
420 bump_seed: u8,
421 new_stake_authority: &Pubkey,
422 clock: AccountInfo<'a>,
423 ) -> Result<(), ProgramError> {
424 let authority_seeds = &[
425 POOL_STAKE_AUTHORITY_PREFIX,
426 pool_account_key.as_ref(),
427 &[bump_seed],
428 ];
429 let signers = &[&authority_seeds[..]];
430
431 let authorize_instruction = stake::instruction::authorize(
432 stake_account.key,
433 stake_authority.key,
434 new_stake_authority,
435 stake::state::StakeAuthorize::Staker,
436 None,
437 );
438
439 invoke_signed(
440 &authorize_instruction,
441 &[
442 stake_account.clone(),
443 clock.clone(),
444 stake_authority.clone(),
445 ],
446 signers,
447 )?;
448
449 let authorize_instruction = stake::instruction::authorize(
450 stake_account.key,
451 stake_authority.key,
452 new_stake_authority,
453 stake::state::StakeAuthorize::Withdrawer,
454 None,
455 );
456 invoke_signed(
457 &authorize_instruction,
458 &[stake_account, clock, stake_authority],
459 signers,
460 )
461 }
462
463 #[allow(clippy::too_many_arguments)]
464 fn stake_withdraw<'a>(
465 pool_account_key: &Pubkey,
466 stake_account: AccountInfo<'a>,
467 stake_authority: AccountInfo<'a>,
468 bump_seed: u8,
469 destination_account: AccountInfo<'a>,
470 clock: AccountInfo<'a>,
471 stake_history: AccountInfo<'a>,
472 lamports: u64,
473 ) -> Result<(), ProgramError> {
474 let authority_seeds = &[
475 POOL_STAKE_AUTHORITY_PREFIX,
476 pool_account_key.as_ref(),
477 &[bump_seed],
478 ];
479 let signers = &[&authority_seeds[..]];
480
481 let withdraw_instruction = stake::instruction::withdraw(
482 stake_account.key,
483 stake_authority.key,
484 destination_account.key,
485 lamports,
486 None,
487 );
488
489 invoke_signed(
490 &withdraw_instruction,
491 &[
492 stake_account,
493 destination_account,
494 clock,
495 stake_history,
496 stake_authority,
497 ],
498 signers,
499 )
500 }
501
502 #[allow(clippy::too_many_arguments)]
503 fn token_mint_to<'a>(
504 pool_account_key: &Pubkey,
505 token_program: AccountInfo<'a>,
506 mint: AccountInfo<'a>,
507 destination: AccountInfo<'a>,
508 authority: AccountInfo<'a>,
509 bump_seed: u8,
510 amount: u64,
511 ) -> Result<(), ProgramError> {
512 let authority_seeds = &[
513 POOL_MINT_AUTHORITY_PREFIX,
514 pool_account_key.as_ref(),
515 &[bump_seed],
516 ];
517 let signers = &[&authority_seeds[..]];
518
519 let ix = spl_token::instruction::mint_to(
520 token_program.key,
521 mint.key,
522 destination.key,
523 authority.key,
524 &[],
525 amount,
526 )?;
527
528 invoke_signed(&ix, &[mint, destination, authority], signers)
529 }
530
531 #[allow(clippy::too_many_arguments)]
532 fn token_burn<'a>(
533 pool_account_key: &Pubkey,
534 token_program: AccountInfo<'a>,
535 burn_account: AccountInfo<'a>,
536 mint: AccountInfo<'a>,
537 authority: AccountInfo<'a>,
538 bump_seed: u8,
539 amount: u64,
540 ) -> Result<(), ProgramError> {
541 let authority_seeds = &[
542 POOL_MINT_AUTHORITY_PREFIX,
543 pool_account_key.as_ref(),
544 &[bump_seed],
545 ];
546 let signers = &[&authority_seeds[..]];
547
548 let ix = spl_token::instruction::burn(
549 token_program.key,
550 burn_account.key,
551 mint.key,
552 authority.key,
553 &[],
554 amount,
555 )?;
556
557 invoke_signed(&ix, &[burn_account, mint, authority], signers)
558 }
559
560 fn process_initialize_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
561 let account_info_iter = &mut accounts.iter();
562 let vote_account_info = next_account_info(account_info_iter)?;
563 let pool_info = next_account_info(account_info_iter)?;
564 let pool_stake_info = next_account_info(account_info_iter)?;
565 let pool_mint_info = next_account_info(account_info_iter)?;
566 let pool_stake_authority_info = next_account_info(account_info_iter)?;
567 let pool_mint_authority_info = next_account_info(account_info_iter)?;
568 let rent_info = next_account_info(account_info_iter)?;
569 let rent = &Rent::from_account_info(rent_info)?;
570 let clock_info = next_account_info(account_info_iter)?;
571 let stake_history_info = next_account_info(account_info_iter)?;
572 let stake_config_info = next_account_info(account_info_iter)?;
573 let system_program_info = next_account_info(account_info_iter)?;
574 let token_program_info = next_account_info(account_info_iter)?;
575 let stake_program_info = next_account_info(account_info_iter)?;
576
577 check_vote_account(vote_account_info)?;
578 let pool_bump_seed = check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
579 let stake_bump_seed =
580 check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
581 let mint_bump_seed =
582 check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
583 let stake_authority_bump_seed = check_pool_stake_authority_address(
584 program_id,
585 pool_info.key,
586 pool_stake_authority_info.key,
587 )?;
588 let mint_authority_bump_seed = check_pool_mint_authority_address(
589 program_id,
590 pool_info.key,
591 pool_mint_authority_info.key,
592 )?;
593 check_system_program(system_program_info.key)?;
594 check_token_program(token_program_info.key)?;
595 check_stake_program(stake_program_info.key)?;
596
597 let pool_seeds = &[
598 POOL_PREFIX,
599 vote_account_info.key.as_ref(),
600 &[pool_bump_seed],
601 ];
602 let pool_signers = &[&pool_seeds[..]];
603
604 let stake_seeds = &[
605 POOL_STAKE_PREFIX,
606 pool_info.key.as_ref(),
607 &[stake_bump_seed],
608 ];
609 let stake_signers = &[&stake_seeds[..]];
610
611 let mint_seeds = &[POOL_MINT_PREFIX, pool_info.key.as_ref(), &[mint_bump_seed]];
612 let mint_signers = &[&mint_seeds[..]];
613
614 let stake_authority_seeds = &[
615 POOL_STAKE_AUTHORITY_PREFIX,
616 pool_info.key.as_ref(),
617 &[stake_authority_bump_seed],
618 ];
619 let stake_authority_signers = &[&stake_authority_seeds[..]];
620
621 let mint_authority_seeds = &[
622 POOL_MINT_AUTHORITY_PREFIX,
623 pool_info.key.as_ref(),
624 &[mint_authority_bump_seed],
625 ];
626 let mint_authority_signers = &[&mint_authority_seeds[..]];
627
628 let pool_space = get_packed_len::<SinglePool>();
630 if !rent.is_exempt(pool_info.lamports(), pool_space) {
631 return Err(SinglePoolError::WrongRentAmount.into());
632 }
633 if pool_info.data_len() != 0 {
634 return Err(SinglePoolError::PoolAlreadyInitialized.into());
635 }
636
637 invoke_signed(
638 &system_instruction::allocate(pool_info.key, pool_space as u64),
639 &[pool_info.clone()],
640 pool_signers,
641 )?;
642
643 invoke_signed(
644 &system_instruction::assign(pool_info.key, program_id),
645 &[pool_info.clone()],
646 pool_signers,
647 )?;
648
649 let mut pool = try_from_slice_unchecked::<SinglePool>(&pool_info.data.borrow())?;
650 pool.account_type = SinglePoolAccountType::Pool;
651 pool.vote_account_address = *vote_account_info.key;
652 borsh::to_writer(&mut pool_info.data.borrow_mut()[..], &pool)?;
653
654 let mint_space = spl_token::state::Mint::LEN;
656
657 invoke_signed(
658 &system_instruction::allocate(pool_mint_info.key, mint_space as u64),
659 &[pool_mint_info.clone()],
660 mint_signers,
661 )?;
662
663 invoke_signed(
664 &system_instruction::assign(pool_mint_info.key, token_program_info.key),
665 &[pool_mint_info.clone()],
666 mint_signers,
667 )?;
668
669 invoke_signed(
670 &spl_token::instruction::initialize_mint2(
671 token_program_info.key,
672 pool_mint_info.key,
673 pool_mint_authority_info.key,
674 None,
675 MINT_DECIMALS,
676 )?,
677 &[pool_mint_info.clone()],
678 mint_authority_signers,
679 )?;
680
681 let minimum_pool_balance = minimum_pool_balance()?;
684 let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
685 let stake_rent_plus_initial = rent
686 .minimum_balance(stake_space)
687 .saturating_add(minimum_pool_balance);
688
689 if pool_stake_info.lamports() < stake_rent_plus_initial {
690 return Err(SinglePoolError::WrongRentAmount.into());
691 }
692
693 let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key);
694
695 invoke_signed(
696 &system_instruction::allocate(pool_stake_info.key, stake_space as u64),
697 &[pool_stake_info.clone()],
698 stake_signers,
699 )?;
700
701 invoke_signed(
702 &system_instruction::assign(pool_stake_info.key, stake_program_info.key),
703 &[pool_stake_info.clone()],
704 stake_signers,
705 )?;
706
707 invoke_signed(
708 &stake::instruction::initialize_checked(pool_stake_info.key, &authorized),
709 &[
710 pool_stake_info.clone(),
711 rent_info.clone(),
712 pool_stake_authority_info.clone(),
713 pool_stake_authority_info.clone(),
714 ],
715 stake_authority_signers,
716 )?;
717
718 invoke_signed(
720 &stake::instruction::delegate_stake(
721 pool_stake_info.key,
722 pool_stake_authority_info.key,
723 vote_account_info.key,
724 ),
725 &[
726 pool_stake_info.clone(),
727 vote_account_info.clone(),
728 clock_info.clone(),
729 stake_history_info.clone(),
730 stake_config_info.clone(),
731 pool_stake_authority_info.clone(),
732 ],
733 stake_authority_signers,
734 )?;
735
736 Ok(())
737 }
738
739 fn process_replenish_pool(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
740 let account_info_iter = &mut accounts.iter();
741 let vote_account_info = next_account_info(account_info_iter)?;
742 let pool_info = next_account_info(account_info_iter)?;
743 let pool_stake_info = next_account_info(account_info_iter)?;
744 let pool_onramp_info = next_account_info(account_info_iter)?;
745 let pool_stake_authority_info = next_account_info(account_info_iter)?;
746 let clock_info = next_account_info(account_info_iter)?;
747 let clock = &Clock::from_account_info(clock_info)?;
748 let stake_history_info = next_account_info(account_info_iter)?;
749 let stake_config_info = next_account_info(account_info_iter)?;
750 let stake_program_info = next_account_info(account_info_iter)?;
751
752 let stake_history = &StakeHistorySysvar(clock.epoch);
753 let minimum_delegation = stake::tools::get_minimum_delegation()?;
754
755 check_vote_account(vote_account_info)?;
756 check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
757
758 SinglePool::from_account_info(pool_info, program_id)?;
759
760 check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
761 check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
762 let stake_authority_bump_seed = check_pool_stake_authority_address(
763 program_id,
764 pool_info.key,
765 pool_stake_authority_info.key,
766 )?;
767 check_stake_program(stake_program_info.key)?;
768
769 let (pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?;
771 let pool_stake_status = pool_stake_state
772 .delegation
773 .stake_activating_and_deactivating(
774 clock.epoch,
775 stake_history,
776 PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
777 );
778 let pool_stake_is_fully_active = is_stake_fully_active(&pool_stake_status);
779
780 let (option_onramp_status, onramp_deactivation_epoch, onramp_rent_exempt_reserve) =
783 match try_from_slice_unchecked::<StakeStateV2>(&pool_onramp_info.data.borrow()) {
784 Ok(StakeStateV2::Initialized(meta)) => (None, u64::MAX, meta.rent_exempt_reserve),
785 Ok(StakeStateV2::Stake(meta, stake, _)) => (
786 Some(stake.delegation.stake_activating_and_deactivating(
787 clock.epoch,
788 stake_history,
789 PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
790 )),
791 stake.delegation.deactivation_epoch,
792 meta.rent_exempt_reserve,
793 ),
794 _ => return Err(SinglePoolError::OnRampDoesntExist.into()),
795 };
796
797 let stake_authority_seeds = &[
798 POOL_STAKE_AUTHORITY_PREFIX,
799 pool_info.key.as_ref(),
800 &[stake_authority_bump_seed],
801 ];
802 let stake_authority_signers = &[&stake_authority_seeds[..]];
803
804 if pool_stake_state.delegation.deactivation_epoch == clock.epoch
807 || (pool_stake_state.delegation.deactivation_epoch < clock.epoch
808 && pool_stake_status.effective == 0)
809 {
810 invoke_signed(
811 &stake::instruction::delegate_stake(
812 pool_stake_info.key,
813 pool_stake_authority_info.key,
814 vote_account_info.key,
815 ),
816 &[
817 pool_stake_info.clone(),
818 vote_account_info.clone(),
819 clock_info.clone(),
820 stake_history_info.clone(),
821 stake_config_info.clone(),
822 pool_stake_authority_info.clone(),
823 ],
824 stake_authority_signers,
825 )?;
826 }
827
828 if pool_stake_is_fully_active {
830 let pool_excess_lamports = pool_stake_info
832 .lamports()
833 .saturating_sub(pool_stake_state.delegation.stake)
834 .saturating_sub(pool_stake_meta.rent_exempt_reserve);
835
836 if let Some(ref onramp_status) = option_onramp_status {
838 if is_stake_fully_active(onramp_status) {
839 invoke_signed(
840 &stake::instruction::move_stake(
841 pool_onramp_info.key,
842 pool_stake_info.key,
843 pool_stake_authority_info.key,
844 onramp_status.effective,
845 ),
846 &[
847 pool_onramp_info.clone(),
848 pool_stake_info.clone(),
849 pool_stake_authority_info.clone(),
850 ],
851 stake_authority_signers,
852 )?;
853 }
854 }
855
856 if pool_excess_lamports > 0 {
858 invoke_signed(
859 &stake::instruction::move_lamports(
860 pool_stake_info.key,
861 pool_onramp_info.key,
862 pool_stake_authority_info.key,
863 pool_excess_lamports,
864 ),
865 &[
866 pool_stake_info.clone(),
867 pool_onramp_info.clone(),
868 pool_stake_authority_info.clone(),
869 ],
870 stake_authority_signers,
871 )?;
872 }
873
874 let onramp_non_rent_lamports = pool_onramp_info
879 .lamports()
880 .saturating_sub(onramp_rent_exempt_reserve);
881 let must_delegate_onramp = match option_onramp_status.unwrap_or_default() {
882 StakeActivationStatus {
884 effective: 0,
885 activating,
886 deactivating: 0,
887 } if activating > 0 => {
888 onramp_non_rent_lamports >= minimum_delegation
889 && onramp_non_rent_lamports > activating
890 }
891 StakeActivationStatus {
894 effective: _,
895 activating: 0,
896 deactivating,
897 } if deactivating == 0 || onramp_deactivation_epoch == clock.epoch => {
898 onramp_non_rent_lamports >= minimum_delegation
899 }
900 _ => false,
902 };
903
904 if must_delegate_onramp {
905 invoke_signed(
906 &stake::instruction::delegate_stake(
907 pool_onramp_info.key,
908 pool_stake_authority_info.key,
909 vote_account_info.key,
910 ),
911 &[
912 pool_onramp_info.clone(),
913 vote_account_info.clone(),
914 clock_info.clone(),
915 stake_history_info.clone(),
916 stake_config_info.clone(),
917 pool_stake_authority_info.clone(),
918 ],
919 stake_authority_signers,
920 )?;
921 }
922 }
923
924 Ok(())
925 }
926
927 fn process_deposit_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
928 let account_info_iter = &mut accounts.iter();
929 let pool_info = next_account_info(account_info_iter)?;
930 let pool_stake_info = next_account_info(account_info_iter)?;
931 let pool_mint_info = next_account_info(account_info_iter)?;
932 let pool_stake_authority_info = next_account_info(account_info_iter)?;
933 let pool_mint_authority_info = next_account_info(account_info_iter)?;
934 let user_stake_info = next_account_info(account_info_iter)?;
935 let user_token_account_info = next_account_info(account_info_iter)?;
936 let user_lamport_account_info = next_account_info(account_info_iter)?;
937 let clock_info = next_account_info(account_info_iter)?;
938 let clock = &Clock::from_account_info(clock_info)?;
939 let stake_history_info = next_account_info(account_info_iter)?;
940 let token_program_info = next_account_info(account_info_iter)?;
941 let stake_program_info = next_account_info(account_info_iter)?;
942
943 SinglePool::from_account_info(pool_info, program_id)?;
944
945 check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
946 let stake_authority_bump_seed = check_pool_stake_authority_address(
947 program_id,
948 pool_info.key,
949 pool_stake_authority_info.key,
950 )?;
951 let mint_authority_bump_seed = check_pool_mint_authority_address(
952 program_id,
953 pool_info.key,
954 pool_mint_authority_info.key,
955 )?;
956 check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
957 check_token_program(token_program_info.key)?;
958 check_stake_program(stake_program_info.key)?;
959
960 if pool_stake_info.key == user_stake_info.key {
961 return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
962 }
963
964 let minimum_pool_balance = minimum_pool_balance()?;
965
966 let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
967 let pre_pool_stake = pool_stake_state
968 .delegation
969 .stake
970 .saturating_sub(minimum_pool_balance);
971 msg!("Available stake pre merge {}", pre_pool_stake);
972
973 let (user_stake_meta, user_stake_state) = get_stake_state(user_stake_info)?;
976 if user_stake_meta.authorized
977 != stake::state::Authorized::auto(pool_stake_authority_info.key)
978 || is_stake_active_without_history(&pool_stake_state, clock.epoch)
979 != is_stake_active_without_history(&user_stake_state, clock.epoch)
980 {
981 return Err(SinglePoolError::WrongStakeStake.into());
982 }
983
984 Self::stake_merge(
988 pool_info.key,
989 user_stake_info.clone(),
990 pool_stake_authority_info.clone(),
991 stake_authority_bump_seed,
992 pool_stake_info.clone(),
993 clock_info.clone(),
994 stake_history_info.clone(),
995 )?;
996
997 let (pool_stake_meta, pool_stake_state) = get_stake_state(pool_stake_info)?;
998 let post_pool_stake = pool_stake_state
999 .delegation
1000 .stake
1001 .saturating_sub(minimum_pool_balance);
1002 let post_pool_lamports = pool_stake_info.lamports();
1003 msg!("Available stake post merge {}", post_pool_stake);
1004
1005 let stake_added = post_pool_stake
1007 .checked_sub(pre_pool_stake)
1008 .ok_or(SinglePoolError::ArithmeticOverflow)?;
1009
1010 let excess_lamports = post_pool_lamports
1013 .checked_sub(pool_stake_state.delegation.stake)
1014 .and_then(|amount| amount.checked_sub(pool_stake_meta.rent_exempt_reserve))
1015 .ok_or(SinglePoolError::ArithmeticOverflow)?;
1016
1017 if user_stake_info.lamports() != 0 {
1019 return Err(SinglePoolError::UnexpectedMathError.into());
1020 }
1021
1022 let token_supply = {
1023 let pool_mint_data = pool_mint_info.try_borrow_data()?;
1024 let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?;
1025 pool_mint.supply
1026 };
1027
1028 let new_pool_tokens = calculate_deposit_amount(token_supply, pre_pool_stake, stake_added)
1030 .ok_or(SinglePoolError::UnexpectedMathError)?;
1031
1032 if new_pool_tokens == 0 {
1033 return Err(SinglePoolError::DepositTooSmall.into());
1034 }
1035
1036 Self::token_mint_to(
1038 pool_info.key,
1039 token_program_info.clone(),
1040 pool_mint_info.clone(),
1041 user_token_account_info.clone(),
1042 pool_mint_authority_info.clone(),
1043 mint_authority_bump_seed,
1044 new_pool_tokens,
1045 )?;
1046
1047 if excess_lamports > 0 {
1049 Self::stake_withdraw(
1050 pool_info.key,
1051 pool_stake_info.clone(),
1052 pool_stake_authority_info.clone(),
1053 stake_authority_bump_seed,
1054 user_lamport_account_info.clone(),
1055 clock_info.clone(),
1056 stake_history_info.clone(),
1057 excess_lamports,
1058 )?;
1059 }
1060
1061 Ok(())
1062 }
1063
1064 fn process_withdraw_stake(
1065 program_id: &Pubkey,
1066 accounts: &[AccountInfo],
1067 user_stake_authority: &Pubkey,
1068 token_amount: u64,
1069 ) -> ProgramResult {
1070 let account_info_iter = &mut accounts.iter();
1071 let pool_info = next_account_info(account_info_iter)?;
1072 let pool_stake_info = next_account_info(account_info_iter)?;
1073 let pool_mint_info = next_account_info(account_info_iter)?;
1074 let pool_stake_authority_info = next_account_info(account_info_iter)?;
1075 let pool_mint_authority_info = next_account_info(account_info_iter)?;
1076 let user_stake_info = next_account_info(account_info_iter)?;
1077 let user_token_account_info = next_account_info(account_info_iter)?;
1078 let clock_info = next_account_info(account_info_iter)?;
1079 let token_program_info = next_account_info(account_info_iter)?;
1080 let stake_program_info = next_account_info(account_info_iter)?;
1081
1082 SinglePool::from_account_info(pool_info, program_id)?;
1083
1084 check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
1085 let stake_authority_bump_seed = check_pool_stake_authority_address(
1086 program_id,
1087 pool_info.key,
1088 pool_stake_authority_info.key,
1089 )?;
1090 let mint_authority_bump_seed = check_pool_mint_authority_address(
1091 program_id,
1092 pool_info.key,
1093 pool_mint_authority_info.key,
1094 )?;
1095 check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
1096 check_token_program(token_program_info.key)?;
1097 check_stake_program(stake_program_info.key)?;
1098
1099 if pool_stake_info.key == user_stake_info.key {
1100 return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
1101 }
1102
1103 let minimum_pool_balance = minimum_pool_balance()?;
1104
1105 let pre_pool_stake =
1106 get_stake_amount(pool_stake_info)?.saturating_sub(minimum_pool_balance);
1107 msg!("Available stake pre split {}", pre_pool_stake);
1108
1109 let token_supply = {
1110 let pool_mint_data = pool_mint_info.try_borrow_data()?;
1111 let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?;
1112 pool_mint.supply
1113 };
1114
1115 let withdraw_stake = calculate_withdraw_amount(token_supply, pre_pool_stake, token_amount)
1117 .ok_or(SinglePoolError::UnexpectedMathError)?;
1118
1119 if withdraw_stake == 0 {
1120 return Err(SinglePoolError::WithdrawalTooSmall.into());
1121 }
1122
1123 if withdraw_stake > pre_pool_stake || withdraw_stake == pool_stake_info.lamports() {
1125 return Err(SinglePoolError::WithdrawalTooLarge.into());
1126 }
1127
1128 Self::token_burn(
1130 pool_info.key,
1131 token_program_info.clone(),
1132 user_token_account_info.clone(),
1133 pool_mint_info.clone(),
1134 pool_mint_authority_info.clone(),
1135 mint_authority_bump_seed,
1136 token_amount,
1137 )?;
1138
1139 Self::stake_split(
1141 pool_info.key,
1142 pool_stake_info.clone(),
1143 pool_stake_authority_info.clone(),
1144 stake_authority_bump_seed,
1145 withdraw_stake,
1146 user_stake_info.clone(),
1147 )?;
1148
1149 Self::stake_authorize(
1151 pool_info.key,
1152 user_stake_info.clone(),
1153 pool_stake_authority_info.clone(),
1154 stake_authority_bump_seed,
1155 user_stake_authority,
1156 clock_info.clone(),
1157 )?;
1158
1159 let post_pool_stake =
1160 get_stake_amount(pool_stake_info)?.saturating_sub(minimum_pool_balance);
1161 msg!("Available stake post split {}", post_pool_stake);
1162
1163 Ok(())
1164 }
1165
1166 fn process_create_pool_token_metadata(
1167 program_id: &Pubkey,
1168 accounts: &[AccountInfo],
1169 ) -> ProgramResult {
1170 let account_info_iter = &mut accounts.iter();
1171 let pool_info = next_account_info(account_info_iter)?;
1172 let pool_mint_info = next_account_info(account_info_iter)?;
1173 let pool_mint_authority_info = next_account_info(account_info_iter)?;
1174 let pool_mpl_authority_info = next_account_info(account_info_iter)?;
1175 let payer_info = next_account_info(account_info_iter)?;
1176 let metadata_info = next_account_info(account_info_iter)?;
1177 let mpl_token_metadata_program_info = next_account_info(account_info_iter)?;
1178 let system_program_info = next_account_info(account_info_iter)?;
1179
1180 let pool = SinglePool::from_account_info(pool_info, program_id)?;
1181
1182 let mint_authority_bump_seed = check_pool_mint_authority_address(
1183 program_id,
1184 pool_info.key,
1185 pool_mint_authority_info.key,
1186 )?;
1187 let mpl_authority_bump_seed = check_pool_mpl_authority_address(
1188 program_id,
1189 pool_info.key,
1190 pool_mpl_authority_info.key,
1191 )?;
1192 check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
1193 check_system_program(system_program_info.key)?;
1194 check_account_owner(payer_info, &system_program::id())?;
1195 check_mpl_metadata_program(mpl_token_metadata_program_info.key)?;
1196 check_mpl_metadata_account_address(metadata_info.key, pool_mint_info.key)?;
1197
1198 if !payer_info.is_signer {
1199 msg!("Payer did not sign metadata creation");
1200 return Err(SinglePoolError::SignatureMissing.into());
1201 }
1202
1203 let vote_address_str = pool.vote_account_address.to_string();
1204 let token_name = format!("SPL Single Pool {}", &vote_address_str[0..15]);
1205 let token_symbol = format!("st{}", &vote_address_str[0..7]);
1206
1207 let new_metadata_instruction = create_metadata_accounts_v3(
1208 *mpl_token_metadata_program_info.key,
1209 *metadata_info.key,
1210 *pool_mint_info.key,
1211 *pool_mint_authority_info.key,
1212 *payer_info.key,
1213 *pool_mpl_authority_info.key,
1214 token_name,
1215 token_symbol,
1216 "".to_string(),
1217 );
1218
1219 let mint_authority_seeds = &[
1220 POOL_MINT_AUTHORITY_PREFIX,
1221 pool_info.key.as_ref(),
1222 &[mint_authority_bump_seed],
1223 ];
1224 let mpl_authority_seeds = &[
1225 POOL_MPL_AUTHORITY_PREFIX,
1226 pool_info.key.as_ref(),
1227 &[mpl_authority_bump_seed],
1228 ];
1229 let signers = &[&mint_authority_seeds[..], &mpl_authority_seeds[..]];
1230
1231 invoke_signed(
1232 &new_metadata_instruction,
1233 &[
1234 metadata_info.clone(),
1235 pool_mint_info.clone(),
1236 pool_mint_authority_info.clone(),
1237 payer_info.clone(),
1238 pool_mpl_authority_info.clone(),
1239 system_program_info.clone(),
1240 ],
1241 signers,
1242 )?;
1243
1244 Ok(())
1245 }
1246
1247 fn process_update_pool_token_metadata(
1248 program_id: &Pubkey,
1249 accounts: &[AccountInfo],
1250 name: String,
1251 symbol: String,
1252 uri: String,
1253 ) -> ProgramResult {
1254 let account_info_iter = &mut accounts.iter();
1255 let vote_account_info = next_account_info(account_info_iter)?;
1256 let pool_info = next_account_info(account_info_iter)?;
1257 let pool_mpl_authority_info = next_account_info(account_info_iter)?;
1258 let authorized_withdrawer_info = next_account_info(account_info_iter)?;
1259 let metadata_info = next_account_info(account_info_iter)?;
1260 let mpl_token_metadata_program_info = next_account_info(account_info_iter)?;
1261
1262 check_vote_account(vote_account_info)?;
1263 check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
1264
1265 let pool = SinglePool::from_account_info(pool_info, program_id)?;
1266 if pool.vote_account_address != *vote_account_info.key {
1267 return Err(SinglePoolError::InvalidPoolAccount.into());
1268 }
1269
1270 let mpl_authority_bump_seed = check_pool_mpl_authority_address(
1271 program_id,
1272 pool_info.key,
1273 pool_mpl_authority_info.key,
1274 )?;
1275 let pool_mint_address = crate::find_pool_mint_address(program_id, pool_info.key);
1276 check_mpl_metadata_program(mpl_token_metadata_program_info.key)?;
1277 check_mpl_metadata_account_address(metadata_info.key, &pool_mint_address)?;
1278
1279 let vote_account_data = &vote_account_info.try_borrow_data()?;
1284 let vote_account_withdrawer = vote_account_data
1285 .get(VOTE_STATE_AUTHORIZED_WITHDRAWER_START..VOTE_STATE_AUTHORIZED_WITHDRAWER_END)
1286 .and_then(|x| Pubkey::try_from(x).ok())
1287 .ok_or(SinglePoolError::UnparseableVoteAccount)?;
1288
1289 if *authorized_withdrawer_info.key != vote_account_withdrawer {
1290 msg!("Vote account authorized withdrawer does not match the account provided.");
1291 return Err(SinglePoolError::InvalidMetadataSigner.into());
1292 }
1293
1294 if !authorized_withdrawer_info.is_signer {
1295 msg!("Vote account authorized withdrawer did not sign metadata update.");
1296 return Err(SinglePoolError::SignatureMissing.into());
1297 }
1298
1299 let update_metadata_accounts_instruction = update_metadata_accounts_v2(
1300 *mpl_token_metadata_program_info.key,
1301 *metadata_info.key,
1302 *pool_mpl_authority_info.key,
1303 None,
1304 Some(DataV2 {
1305 name,
1306 symbol,
1307 uri,
1308 seller_fee_basis_points: 0,
1309 creators: None,
1310 collection: None,
1311 uses: None,
1312 }),
1313 None,
1314 Some(true),
1315 );
1316
1317 let mpl_authority_seeds = &[
1318 POOL_MPL_AUTHORITY_PREFIX,
1319 pool_info.key.as_ref(),
1320 &[mpl_authority_bump_seed],
1321 ];
1322 let signers = &[&mpl_authority_seeds[..]];
1323
1324 invoke_signed(
1325 &update_metadata_accounts_instruction,
1326 &[metadata_info.clone(), pool_mpl_authority_info.clone()],
1327 signers,
1328 )?;
1329
1330 Ok(())
1331 }
1332
1333 fn process_initialize_pool_onramp(
1334 program_id: &Pubkey,
1335 accounts: &[AccountInfo],
1336 ) -> ProgramResult {
1337 let account_info_iter = &mut accounts.iter();
1338 let pool_info = next_account_info(account_info_iter)?;
1339 let pool_onramp_info = next_account_info(account_info_iter)?;
1340 let pool_stake_authority_info = next_account_info(account_info_iter)?;
1341 let rent_info = next_account_info(account_info_iter)?;
1342 let rent = &Rent::from_account_info(rent_info)?;
1343 let system_program_info = next_account_info(account_info_iter)?;
1344 let stake_program_info = next_account_info(account_info_iter)?;
1345
1346 SinglePool::from_account_info(pool_info, program_id)?;
1347
1348 let onramp_bump_seed =
1349 check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
1350 let stake_authority_bump_seed = check_pool_stake_authority_address(
1351 program_id,
1352 pool_info.key,
1353 pool_stake_authority_info.key,
1354 )?;
1355 check_system_program(system_program_info.key)?;
1356 check_stake_program(stake_program_info.key)?;
1357
1358 let onramp_seeds = &[
1359 POOL_ONRAMP_PREFIX,
1360 pool_info.key.as_ref(),
1361 &[onramp_bump_seed],
1362 ];
1363 let onramp_signers = &[&onramp_seeds[..]];
1364
1365 let stake_authority_seeds = &[
1366 POOL_STAKE_AUTHORITY_PREFIX,
1367 pool_info.key.as_ref(),
1368 &[stake_authority_bump_seed],
1369 ];
1370 let stake_authority_signers = &[&stake_authority_seeds[..]];
1371
1372 let stake_space = std::mem::size_of::<stake::state::StakeStateV2>();
1374 let stake_rent = rent.minimum_balance(stake_space);
1375
1376 if pool_onramp_info.lamports() < stake_rent {
1377 return Err(SinglePoolError::WrongRentAmount.into());
1378 }
1379
1380 let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key);
1381
1382 invoke_signed(
1383 &system_instruction::allocate(pool_onramp_info.key, stake_space as u64),
1384 &[pool_onramp_info.clone()],
1385 onramp_signers,
1386 )?;
1387
1388 invoke_signed(
1389 &system_instruction::assign(pool_onramp_info.key, stake_program_info.key),
1390 &[pool_onramp_info.clone()],
1391 onramp_signers,
1392 )?;
1393
1394 invoke_signed(
1395 &stake::instruction::initialize_checked(pool_onramp_info.key, &authorized),
1396 &[
1397 pool_onramp_info.clone(),
1398 rent_info.clone(),
1399 pool_stake_authority_info.clone(),
1400 pool_stake_authority_info.clone(),
1401 ],
1402 stake_authority_signers,
1403 )?;
1404
1405 Ok(())
1406 }
1407
1408 pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
1410 let instruction = SinglePoolInstruction::try_from_slice(input)?;
1411 match instruction {
1412 SinglePoolInstruction::InitializePool => {
1413 msg!("Instruction: InitializePool");
1414 Self::process_initialize_pool(program_id, accounts)
1415 }
1416 SinglePoolInstruction::ReplenishPool => {
1417 msg!("Instruction: ReplenishPool");
1418 Self::process_replenish_pool(program_id, accounts)
1419 }
1420 SinglePoolInstruction::DepositStake => {
1421 msg!("Instruction: DepositStake");
1422 Self::process_deposit_stake(program_id, accounts)
1423 }
1424 SinglePoolInstruction::WithdrawStake {
1425 user_stake_authority,
1426 token_amount,
1427 } => {
1428 msg!("Instruction: WithdrawStake");
1429 Self::process_withdraw_stake(
1430 program_id,
1431 accounts,
1432 &user_stake_authority,
1433 token_amount,
1434 )
1435 }
1436 SinglePoolInstruction::CreateTokenMetadata => {
1437 msg!("Instruction: CreateTokenMetadata");
1438 Self::process_create_pool_token_metadata(program_id, accounts)
1439 }
1440 SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri } => {
1441 msg!("Instruction: UpdateTokenMetadata");
1442 Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri)
1443 }
1444 SinglePoolInstruction::InitializePoolOnRamp => {
1445 msg!("Instruction: InitializePoolOnRamp");
1446 Self::process_initialize_pool_onramp(program_id, accounts)
1447 }
1448 }
1449 }
1450}
1451
1452#[cfg(test)]
1453#[allow(clippy::arithmetic_side_effects)]
1454mod tests {
1455 use {
1456 super::*,
1457 approx::assert_relative_eq,
1458 rand::{
1459 distr::{Distribution, Uniform},
1460 rngs::StdRng,
1461 seq::IteratorRandom,
1462 Rng, SeedableRng,
1463 },
1464 std::collections::BTreeMap,
1465 test_case::test_case,
1466 };
1467
1468 const INFLATION_BASE_RATE: f64 = 0.0004;
1470
1471 #[derive(Clone, Debug, Default)]
1472 struct PoolState {
1473 pub token_supply: u64,
1474 pub total_stake: u64,
1475 pub user_token_balances: BTreeMap<Pubkey, u64>,
1476 }
1477 impl PoolState {
1478 #[rustfmt::skip]
1482 pub fn deposit(&mut self, user_pubkey: &Pubkey, stake_to_deposit: u64) -> Option<u64> {
1483 calculate_deposit_amount(self.token_supply, self.total_stake, stake_to_deposit)
1484 .and_then(|tokens_to_mint| self.token_supply.checked_add(tokens_to_mint)
1485 .and_then(|new_token_supply| self.total_stake.checked_add(stake_to_deposit)
1486 .and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey).or(Some(0))
1487 .and_then(|old_user_token_balance| old_user_token_balance.checked_add(tokens_to_mint)
1488 .map(|new_user_token_balance| {
1489 self.token_supply = new_token_supply;
1490 self.total_stake = new_total_stake;
1491 let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance);
1492 tokens_to_mint
1493 })))))
1494 }
1495
1496 #[rustfmt::skip]
1500 pub fn withdraw(&mut self, user_pubkey: &Pubkey, tokens_to_burn: u64) -> Option<u64> {
1501 calculate_withdraw_amount(self.token_supply, self.total_stake, tokens_to_burn)
1502 .and_then(|stake_to_withdraw| self.token_supply.checked_sub(tokens_to_burn)
1503 .and_then(|new_token_supply| self.total_stake.checked_sub(stake_to_withdraw)
1504 .and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey)
1505 .and_then(|old_user_token_balance| old_user_token_balance.checked_sub(tokens_to_burn)
1506 .map(|new_user_token_balance| {
1507 self.token_supply = new_token_supply;
1508 self.total_stake = new_total_stake;
1509 let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance);
1510 stake_to_withdraw
1511 })))))
1512 }
1513
1514 pub fn reward(&mut self, reward_amount: u64) {
1516 self.total_stake = self.total_stake.checked_add(reward_amount).unwrap();
1517 }
1518
1519 pub fn tokens(&self, user_pubkey: &Pubkey) -> u64 {
1521 *self.user_token_balances.get(user_pubkey).unwrap_or(&0)
1522 }
1523
1524 pub fn stake(&self, user_pubkey: &Pubkey) -> u64 {
1526 let tokens = self.tokens(user_pubkey);
1527 if tokens > 0 {
1528 u64::try_from(tokens as u128 * self.total_stake as u128 / self.token_supply as u128)
1529 .unwrap()
1530 } else {
1531 0
1532 }
1533 }
1534
1535 pub fn share(&self, user_pubkey: &Pubkey) -> f64 {
1537 let tokens = self.tokens(user_pubkey);
1538 if tokens > 0 {
1539 tokens as f64 / self.token_supply as f64
1540 } else {
1541 0.0
1542 }
1543 }
1544 }
1545
1546 #[test]
1549 fn simple_deposit_withdraw() {
1550 let mut pool = PoolState::default();
1551 let alice = Pubkey::new_unique();
1552 let bob = Pubkey::new_unique();
1553 let chad = Pubkey::new_unique();
1554
1555 pool.deposit(&alice, 250).unwrap();
1557 assert_eq!(pool.tokens(&alice), 250);
1558 assert_eq!(pool.token_supply, 250);
1559 assert_eq!(pool.total_stake, 250);
1560
1561 pool.deposit(&bob, 750).unwrap();
1563 assert_eq!(pool.tokens(&bob), 750);
1564 assert_eq!(pool.token_supply, 1000);
1565 assert_eq!(pool.total_stake, 1000);
1566
1567 assert_relative_eq!(pool.share(&alice), 0.25);
1571 assert_relative_eq!(pool.share(&bob), 0.75);
1572 pool.reward(1000);
1573 assert_eq!(pool.stake(&alice), pool.tokens(&alice) * 2);
1574 assert_eq!(pool.stake(&bob), pool.tokens(&bob) * 2);
1575 assert_relative_eq!(pool.share(&alice), 0.25);
1576 assert_relative_eq!(pool.share(&bob), 0.75);
1577
1578 let stake_removed = pool.withdraw(&alice, 125).unwrap();
1583 pool.deposit(&chad, 250).unwrap();
1584 assert_eq!(stake_removed, 250);
1585 assert_relative_eq!(pool.share(&alice), 0.125);
1586 assert_relative_eq!(pool.share(&bob), 0.75);
1587
1588 let stake_removed = pool.withdraw(&bob, 750).unwrap();
1590 assert_eq!(stake_removed, 1500);
1591 assert_relative_eq!(pool.share(&bob), 0.0);
1592 pool.withdraw(&chad, 125).unwrap();
1593 assert_relative_eq!(pool.share(&alice), 1.0);
1594 }
1595
1596 #[test_case(rand::random(), false, false; "no_rewards")]
1602 #[test_case(rand::random(), true, false; "with_rewards")]
1603 #[test_case(rand::random(), true, true; "no_minimum")]
1604 fn random_deposit_withdraw(seed: u64, with_rewards: bool, no_minimum: bool) {
1605 println!(
1606 "TEST SEED: {}. edit the test case to pass this value if needed to debug failures",
1607 seed
1608 );
1609 let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
1610
1611 let deposit_range = Uniform::try_from(LAMPORTS_PER_SOL..LAMPORTS_PER_SOL * 1000).unwrap();
1616 let minnow_range = Uniform::try_from(1..LAMPORTS_PER_SOL).unwrap();
1617 let op_range = Uniform::try_from(if with_rewards { 0.0..1.0 } else { 0.0..0.65 }).unwrap();
1618 let std_range = Uniform::try_from(0.0..1.0).unwrap();
1619
1620 let deposit_amount = |prng: &mut StdRng| {
1621 if no_minimum && prng.random_bool(0.2) {
1622 minnow_range.sample(prng)
1623 } else {
1624 deposit_range.sample(prng)
1625 }
1626 };
1627
1628 for _ in 0..100 {
1630 let mut pool = PoolState::default();
1635
1636 let mut users = vec![];
1641 let user_count: usize = prng.random_range(1..=100);
1642 for _ in 0..user_count {
1643 let user = Pubkey::new_unique();
1644
1645 if prng.random_bool(0.5) {
1646 pool.deposit(&user, deposit_amount(&mut prng)).unwrap();
1647 }
1648
1649 users.push(user);
1650 }
1651
1652 for _ in 0..1000 {
1656 match op_range.sample(&mut prng) {
1657 n if n <= 0.35 => {
1660 let user = users.iter().choose(&mut prng).unwrap();
1661 let prev_share = pool.share(user);
1662 let prev_stake = pool.stake(user);
1663 let prev_token_supply = pool.token_supply;
1664 let prev_total_stake = pool.total_stake;
1665
1666 let stake_deposited = deposit_amount(&mut prng);
1667 let tokens_minted = pool.deposit(user, stake_deposited).unwrap();
1668
1669 assert_eq!(pool.total_stake - prev_total_stake, stake_deposited);
1671
1672 assert!(
1674 (pool.stake(user) as i64 - prev_stake as i64 - stake_deposited as i64)
1675 .abs()
1676 <= 2
1677 );
1678
1679 assert_eq!(pool.token_supply - prev_token_supply, tokens_minted);
1681
1682 if prev_total_stake > 0 {
1684 assert_relative_eq!(
1685 pool.share(user) - prev_share,
1686 pool.stake(user) as f64 / pool.total_stake as f64
1687 - prev_stake as f64 / prev_total_stake as f64,
1688 epsilon = 1e-6
1689 );
1690 }
1691 }
1692
1693 n if n > 0.35 && n <= 0.65 => {
1696 if let Some(user) = users
1697 .iter()
1698 .filter(|user| pool.tokens(user) > 0)
1699 .choose(&mut prng)
1700 {
1701 let prev_tokens = pool.tokens(user);
1702 let prev_share = pool.share(user);
1703 let prev_stake = pool.stake(user);
1704 let prev_token_supply = pool.token_supply;
1705 let prev_total_stake = pool.total_stake;
1706
1707 let tokens_burned = if std_range.sample(&mut prng) <= 0.1 {
1708 prev_tokens
1709 } else {
1710 prng.random_range(0..prev_tokens)
1711 };
1712 let stake_received = pool.withdraw(user, tokens_burned).unwrap();
1713
1714 assert_eq!(prev_total_stake - pool.total_stake, stake_received);
1716
1717 assert!(
1719 (prev_stake as i64
1720 - pool.stake(user) as i64
1721 - stake_received as i64)
1722 .abs()
1723 <= 2
1724 );
1725
1726 assert_eq!(prev_token_supply - pool.token_supply, tokens_burned);
1728
1729 if pool.total_stake > 0 {
1731 assert_relative_eq!(
1732 prev_share - pool.share(user),
1733 prev_stake as f64 / prev_total_stake as f64
1734 - pool.stake(user) as f64 / pool.total_stake as f64,
1735 epsilon = 1e-6
1736 );
1737 }
1738 };
1739 }
1740
1741 _ => {
1745 assert!(with_rewards);
1746
1747 let prev_shares_stakes = users
1748 .iter()
1749 .map(|user| (user, pool.share(user), pool.stake(user)))
1750 .filter(|(_, _, stake)| stake > &0)
1751 .collect::<Vec<_>>();
1752
1753 pool.reward((pool.total_stake as f64 * INFLATION_BASE_RATE) as u64);
1754
1755 for (user, prev_share, prev_stake) in prev_shares_stakes {
1756 assert_eq!(pool.share(user), prev_share);
1758
1759 let curr_stake = pool.stake(user);
1760 let stake_share = prev_stake as f64 * INFLATION_BASE_RATE;
1761 let stake_diff = (curr_stake - prev_stake) as f64;
1762
1763 assert!((stake_share - stake_diff).abs() <= 2.0);
1766 }
1767 }
1768 }
1769 }
1770 }
1771 }
1772}