Skip to main content

spl_single_pool/
processor.rs

1//! program state processor
2
3use {
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_account_info::{next_account_info, AccountInfo},
21    solana_borsh::v1::try_from_slice_unchecked,
22    solana_clock::{Clock, Epoch},
23    solana_cpi::invoke_signed,
24    solana_msg::msg,
25    solana_native_token::LAMPORTS_PER_SOL,
26    solana_program_entrypoint::ProgramResult,
27    solana_program_error::ProgramError,
28    solana_program_pack::Pack,
29    solana_pubkey::Pubkey,
30    solana_rent::Rent,
31    solana_stake_interface::{
32        self as stake,
33        state::{Meta, Stake, StakeActivationStatus, StakeStateV2},
34        sysvar::stake_history::StakeHistorySysvar,
35    },
36    solana_system_interface::{instruction as system_instruction, program as system_program},
37    solana_sysvar::{Sysvar, SysvarSerialize},
38    solana_vote_interface::program as vote_program,
39    spl_token_interface::{self as spl_token, state::Mint},
40};
41
42/// Calculate pool tokens to mint, given outstanding token supply, pool active
43/// stake, and deposit active stake
44fn calculate_deposit_amount(
45    pre_token_supply: u64,
46    pre_pool_stake: u64,
47    user_stake_to_deposit: u64,
48) -> Option<u64> {
49    if pre_pool_stake == 0 || pre_token_supply == 0 {
50        Some(user_stake_to_deposit)
51    } else {
52        u64::try_from(
53            (user_stake_to_deposit as u128)
54                .checked_mul(pre_token_supply as u128)?
55                .checked_div(pre_pool_stake as u128)?,
56        )
57        .ok()
58    }
59}
60
61/// Calculate pool stake to return, given outstanding token supply, pool active
62/// stake, and tokens to redeem
63fn calculate_withdraw_amount(
64    pre_token_supply: u64,
65    pre_pool_stake: u64,
66    user_tokens_to_burn: u64,
67) -> Option<u64> {
68    let numerator = (user_tokens_to_burn as u128).checked_mul(pre_pool_stake as u128)?;
69    let denominator = pre_token_supply as u128;
70    if numerator < denominator || denominator == 0 {
71        Some(0)
72    } else {
73        u64::try_from(numerator.checked_div(denominator)?).ok()
74    }
75}
76
77/// Deserialize the stake state from `AccountInfo`
78fn get_stake_state(stake_account_info: &AccountInfo) -> Result<(Meta, Stake), ProgramError> {
79    let stake_state = try_from_slice_unchecked::<StakeStateV2>(&stake_account_info.data.borrow())?;
80
81    match stake_state {
82        StakeStateV2::Stake(meta, stake, _) => Ok((meta, stake)),
83        _ => Err(SinglePoolError::WrongStakeStake.into()),
84    }
85}
86
87/// Deserialize the stake amount from `AccountInfo`
88fn get_stake_amount(stake_account_info: &AccountInfo) -> Result<u64, ProgramError> {
89    Ok(get_stake_state(stake_account_info)?.1.delegation.stake)
90}
91
92/// Determine if stake is active
93fn is_stake_active_without_history(stake: &Stake, current_epoch: Epoch) -> bool {
94    stake.delegation.activation_epoch < current_epoch
95        && stake.delegation.deactivation_epoch == Epoch::MAX
96}
97
98/// Determine if stake is fully active with history
99fn is_stake_fully_active(stake_activation_status: &StakeActivationStatus) -> bool {
100    matches!(stake_activation_status, StakeActivationStatus {
101            effective,
102            activating: 0,
103            deactivating: 0,
104        } if *effective > 0)
105}
106
107/// Check pool account address for the validator vote account
108fn check_pool_address(
109    program_id: &Pubkey,
110    vote_account_address: &Pubkey,
111    check_address: &Pubkey,
112) -> Result<u8, ProgramError> {
113    check_pool_pda(
114        program_id,
115        vote_account_address,
116        check_address,
117        &crate::find_pool_address_and_bump,
118        "pool",
119        SinglePoolError::InvalidPoolAccount,
120    )
121}
122
123/// Check pool stake account address for the pool account
124fn check_pool_stake_address(
125    program_id: &Pubkey,
126    pool_address: &Pubkey,
127    check_address: &Pubkey,
128) -> Result<u8, ProgramError> {
129    check_pool_pda(
130        program_id,
131        pool_address,
132        check_address,
133        &crate::find_pool_stake_address_and_bump,
134        "stake account",
135        SinglePoolError::InvalidPoolStakeAccount,
136    )
137}
138
139/// Check pool on-ramp account address for the pool account
140fn check_pool_onramp_address(
141    program_id: &Pubkey,
142    pool_address: &Pubkey,
143    check_address: &Pubkey,
144) -> Result<u8, ProgramError> {
145    check_pool_pda(
146        program_id,
147        pool_address,
148        check_address,
149        &crate::find_pool_onramp_address_and_bump,
150        "onramp account",
151        SinglePoolError::InvalidPoolOnRampAccount,
152    )
153}
154
155/// Check pool mint address for the pool account
156fn check_pool_mint_address(
157    program_id: &Pubkey,
158    pool_address: &Pubkey,
159    check_address: &Pubkey,
160) -> Result<u8, ProgramError> {
161    check_pool_pda(
162        program_id,
163        pool_address,
164        check_address,
165        &crate::find_pool_mint_address_and_bump,
166        "mint",
167        SinglePoolError::InvalidPoolMint,
168    )
169}
170
171/// Check pool stake authority address for the pool account
172fn check_pool_stake_authority_address(
173    program_id: &Pubkey,
174    pool_address: &Pubkey,
175    check_address: &Pubkey,
176) -> Result<u8, ProgramError> {
177    check_pool_pda(
178        program_id,
179        pool_address,
180        check_address,
181        &crate::find_pool_stake_authority_address_and_bump,
182        "stake authority",
183        SinglePoolError::InvalidPoolStakeAuthority,
184    )
185}
186
187/// Check pool mint authority address for the pool account
188fn check_pool_mint_authority_address(
189    program_id: &Pubkey,
190    pool_address: &Pubkey,
191    check_address: &Pubkey,
192) -> Result<u8, ProgramError> {
193    check_pool_pda(
194        program_id,
195        pool_address,
196        check_address,
197        &crate::find_pool_mint_authority_address_and_bump,
198        "mint authority",
199        SinglePoolError::InvalidPoolMintAuthority,
200    )
201}
202
203/// Check pool MPL authority address for the pool account
204fn check_pool_mpl_authority_address(
205    program_id: &Pubkey,
206    pool_address: &Pubkey,
207    check_address: &Pubkey,
208) -> Result<u8, ProgramError> {
209    check_pool_pda(
210        program_id,
211        pool_address,
212        check_address,
213        &crate::find_pool_mpl_authority_address_and_bump,
214        "MPL authority",
215        SinglePoolError::InvalidPoolMplAuthority,
216    )
217}
218
219fn check_pool_pda(
220    program_id: &Pubkey,
221    base_address: &Pubkey,
222    check_address: &Pubkey,
223    pda_lookup_fn: &dyn Fn(&Pubkey, &Pubkey) -> (Pubkey, u8),
224    pda_name: &str,
225    pool_error: SinglePoolError,
226) -> Result<u8, ProgramError> {
227    let (derived_address, bump_seed) = pda_lookup_fn(program_id, base_address);
228    if *check_address != derived_address {
229        msg!(
230            "Incorrect {} address for base {}: expected {}, received {}",
231            pda_name,
232            base_address,
233            derived_address,
234            check_address,
235        );
236        Err(pool_error.into())
237    } else {
238        Ok(bump_seed)
239    }
240}
241
242/// Check vote account is owned by the vote program and not a legacy variant
243fn check_vote_account(vote_account_info: &AccountInfo) -> Result<(), ProgramError> {
244    check_account_owner(vote_account_info, &vote_program::id())?;
245
246    let vote_account_data = &vote_account_info.try_borrow_data()?;
247    let state_variant = vote_account_data
248        .get(..VOTE_STATE_DISCRIMINATOR_END)
249        .and_then(|s| s.try_into().ok())
250        .ok_or(SinglePoolError::UnparseableVoteAccount)?;
251
252    #[allow(clippy::manual_range_patterns)]
253    match u32::from_le_bytes(state_variant) {
254        1 | 2 | 3 => Ok(()),
255        0 => Err(SinglePoolError::LegacyVoteAccount.into()),
256        _ => Err(SinglePoolError::UnparseableVoteAccount.into()),
257    }
258}
259
260/// Check MPL metadata account address for the pool mint
261fn 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
273/// Check system program address
274fn 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
287/// Check token program address
288fn 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
301/// Check stake program address
302fn 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
315/// Check MPL metadata program
316fn 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
329/// Check account owner is the given program
330fn 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
346/// Minimum balance of delegated stake required to create a pool. We require at least
347/// 1 sol to avoid minting tokens for these lamports (locking them in the pool) since
348/// they would *become* locked after `stake_raise_minimum_delegation_to_1_sol` is active.
349fn minimum_pool_balance() -> Result<u64, ProgramError> {
350    Ok(std::cmp::max(
351        stake::tools::get_minimum_delegation()?,
352        LAMPORTS_PER_SOL,
353    ))
354}
355
356/// Program state handler.
357pub 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        // create the pool. user has already transferred in rent
629        let pool_space = SinglePool::size_of();
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        // create the pool mint. user has already transferred in rent
655        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        // create the pool stake account. user has already transferred in rent plus at
682        // least the minimum
683        let minimum_pool_balance = minimum_pool_balance()?;
684        let stake_space = StakeStateV2::size_of();
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        // delegate stake so it activates
719        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        // we expect these numbers to be equal but get them separately in case of future changes
770        let rent = Rent::get()?;
771        let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());
772        let onramp_rent_exempt_reserve = rent.minimum_balance(pool_onramp_info.data_len());
773
774        // get main pool account, we require it to be fully active for most operations
775        let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
776        let pool_stake_status = pool_stake_state
777            .delegation
778            .stake_activating_and_deactivating(
779                clock.epoch,
780                stake_history,
781                PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
782            );
783        let pool_stake_is_fully_active = is_stake_fully_active(&pool_stake_status);
784
785        // get on-ramp and its status. we have to match because unlike the main account it could be Initialized
786        // if it doesnt exist, it must first be created with InitializePoolOnRamp
787        let (option_onramp_status, onramp_deactivation_epoch) =
788            match try_from_slice_unchecked::<StakeStateV2>(&pool_onramp_info.data.borrow()) {
789                Ok(StakeStateV2::Initialized(_)) => (None, u64::MAX),
790                Ok(StakeStateV2::Stake(_, stake, _)) => (
791                    Some(stake.delegation.stake_activating_and_deactivating(
792                        clock.epoch,
793                        stake_history,
794                        PERPETUAL_NEW_WARMUP_COOLDOWN_RATE_EPOCH,
795                    )),
796                    stake.delegation.deactivation_epoch,
797                ),
798                _ => return Err(SinglePoolError::OnRampDoesntExist.into()),
799            };
800
801        let stake_authority_seeds = &[
802            POOL_STAKE_AUTHORITY_PREFIX,
803            pool_info.key.as_ref(),
804            &[stake_authority_bump_seed],
805        ];
806        let stake_authority_signers = &[&stake_authority_seeds[..]];
807
808        // if pool stake is deactivating this epoch, or has fully deactivated, delegate it
809        // this may happen as a result of `DeactivateDelinquent`
810        if pool_stake_state.delegation.deactivation_epoch == clock.epoch
811            || (pool_stake_state.delegation.deactivation_epoch < clock.epoch
812                && pool_stake_status.effective == 0)
813        {
814            invoke_signed(
815                &stake::instruction::delegate_stake(
816                    pool_stake_info.key,
817                    pool_stake_authority_info.key,
818                    vote_account_info.key,
819                ),
820                &[
821                    pool_stake_info.clone(),
822                    vote_account_info.clone(),
823                    clock_info.clone(),
824                    stake_history_info.clone(),
825                    stake_config_info.clone(),
826                    pool_stake_authority_info.clone(),
827                ],
828                stake_authority_signers,
829            )?;
830        }
831
832        // if pool is fully active, we can move stake to the main account and lamports to the on-ramp
833        if pool_stake_is_fully_active {
834            // determine excess lamports in the main account before we touch either of them
835            let pool_excess_lamports = pool_stake_info
836                .lamports()
837                .saturating_sub(pool_stake_state.delegation.stake)
838                .saturating_sub(pool_rent_exempt_reserve);
839
840            // if the on-ramp is fully active, move its stake to the main pool account
841            if let Some(ref onramp_status) = option_onramp_status {
842                if is_stake_fully_active(onramp_status) {
843                    invoke_signed(
844                        &stake::instruction::move_stake(
845                            pool_onramp_info.key,
846                            pool_stake_info.key,
847                            pool_stake_authority_info.key,
848                            onramp_status.effective,
849                        ),
850                        &[
851                            pool_onramp_info.clone(),
852                            pool_stake_info.clone(),
853                            pool_stake_authority_info.clone(),
854                        ],
855                        stake_authority_signers,
856                    )?;
857                }
858            }
859
860            // if there are any excess lamports to move to the on-ramp, move them
861            if pool_excess_lamports > 0 {
862                invoke_signed(
863                    &stake::instruction::move_lamports(
864                        pool_stake_info.key,
865                        pool_onramp_info.key,
866                        pool_stake_authority_info.key,
867                        pool_excess_lamports,
868                    ),
869                    &[
870                        pool_stake_info.clone(),
871                        pool_onramp_info.clone(),
872                        pool_stake_authority_info.clone(),
873                    ],
874                    stake_authority_signers,
875                )?;
876            }
877
878            // finally, delegate the on-ramp account if it has sufficient undelegated lamports
879            // if activating, this means more lamports than the current activating delegation
880            // in all cases, this means having enough to cover the minimum delegation
881            // we do nothing if partially active. we know it cannot be fully active because of MoveStake
882            let onramp_non_rent_lamports = pool_onramp_info
883                .lamports()
884                .saturating_sub(onramp_rent_exempt_reserve);
885            let must_delegate_onramp = match option_onramp_status.unwrap_or_default() {
886                // activating
887                StakeActivationStatus {
888                    effective: 0,
889                    activating,
890                    deactivating: 0,
891                } if activating > 0 => {
892                    onramp_non_rent_lamports >= minimum_delegation
893                        && onramp_non_rent_lamports > activating
894                }
895                // inactive, or deactivating this epoch due to DeactivateDelinquent
896                // effective may be nonzero here because we are using the status prior to MoveStake
897                StakeActivationStatus {
898                    effective: _,
899                    activating: 0,
900                    deactivating,
901                } if deactivating == 0 || onramp_deactivation_epoch == clock.epoch => {
902                    onramp_non_rent_lamports >= minimum_delegation
903                }
904                // partially active, partially inactive, or some state beyond mortal reckoning
905                _ => false,
906            };
907
908            if must_delegate_onramp {
909                invoke_signed(
910                    &stake::instruction::delegate_stake(
911                        pool_onramp_info.key,
912                        pool_stake_authority_info.key,
913                        vote_account_info.key,
914                    ),
915                    &[
916                        pool_onramp_info.clone(),
917                        vote_account_info.clone(),
918                        clock_info.clone(),
919                        stake_history_info.clone(),
920                        stake_config_info.clone(),
921                        pool_stake_authority_info.clone(),
922                    ],
923                    stake_authority_signers,
924                )?;
925            }
926        }
927
928        Ok(())
929    }
930
931    fn process_deposit_stake(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
932        let account_info_iter = &mut accounts.iter();
933        let pool_info = next_account_info(account_info_iter)?;
934        let pool_stake_info = next_account_info(account_info_iter)?;
935        let pool_mint_info = {
936            let account_info = next_account_info(account_info_iter)?;
937            // we havent validated pool_info yet, so this doesnt validate onramp, but it doesnt matter
938            // for now we just need to know whether to skip an account, we dont actually use it
939            if check_pool_onramp_address(program_id, pool_info.key, account_info.key).is_ok() {
940                next_account_info(account_info_iter)?
941            } else {
942                account_info
943            }
944        };
945        let pool_stake_authority_info = next_account_info(account_info_iter)?;
946        let pool_mint_authority_info = next_account_info(account_info_iter)?;
947        let user_stake_info = next_account_info(account_info_iter)?;
948        let user_token_account_info = next_account_info(account_info_iter)?;
949        let user_lamport_account_info = next_account_info(account_info_iter)?;
950        let clock_info = next_account_info(account_info_iter)?;
951        let clock = &Clock::from_account_info(clock_info)?;
952        let stake_history_info = next_account_info(account_info_iter)?;
953        let token_program_info = next_account_info(account_info_iter)?;
954        let stake_program_info = next_account_info(account_info_iter)?;
955
956        SinglePool::from_account_info(pool_info, program_id)?;
957
958        check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
959        check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
960        let stake_authority_bump_seed = check_pool_stake_authority_address(
961            program_id,
962            pool_info.key,
963            pool_stake_authority_info.key,
964        )?;
965        let mint_authority_bump_seed = check_pool_mint_authority_address(
966            program_id,
967            pool_info.key,
968            pool_mint_authority_info.key,
969        )?;
970        check_token_program(token_program_info.key)?;
971        check_stake_program(stake_program_info.key)?;
972
973        if pool_stake_info.key == user_stake_info.key {
974            return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
975        }
976
977        let onramp_account_address = crate::find_pool_onramp_address(program_id, pool_info.key);
978        if onramp_account_address == *user_stake_info.key {
979            return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
980        }
981
982        let rent = Rent::get()?;
983        let pool_rent_exempt_reserve = rent.minimum_balance(pool_stake_info.data_len());
984
985        let minimum_pool_balance = minimum_pool_balance()?;
986
987        let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
988        let pre_pool_stake = pool_stake_state
989            .delegation
990            .stake
991            .saturating_sub(minimum_pool_balance);
992        let pre_pool_excess_lamports = pool_stake_info
993            .lamports()
994            .checked_sub(pool_stake_state.delegation.stake)
995            .and_then(|amount| amount.checked_sub(pool_rent_exempt_reserve))
996            .ok_or(SinglePoolError::ArithmeticOverflow)?;
997        msg!("Available stake pre merge {}", pre_pool_stake);
998
999        // user can deposit active stake into an active pool or inactive stake into an
1000        // activating pool
1001        let (user_stake_meta, user_stake_state) = get_stake_state(user_stake_info)?;
1002        if user_stake_meta.authorized
1003            != stake::state::Authorized::auto(pool_stake_authority_info.key)
1004            || is_stake_active_without_history(&pool_stake_state, clock.epoch)
1005                != is_stake_active_without_history(&user_stake_state, clock.epoch)
1006        {
1007            return Err(SinglePoolError::WrongStakeStake.into());
1008        }
1009
1010        // merge the user stake account, which is preauthed to us, into the pool stake
1011        // account this merge succeeding implicitly validates authority/lockup
1012        // of the user stake account
1013        Self::stake_merge(
1014            pool_info.key,
1015            user_stake_info.clone(),
1016            pool_stake_authority_info.clone(),
1017            stake_authority_bump_seed,
1018            pool_stake_info.clone(),
1019            clock_info.clone(),
1020            stake_history_info.clone(),
1021        )?;
1022
1023        let (_, pool_stake_state) = get_stake_state(pool_stake_info)?;
1024        let post_pool_stake = pool_stake_state
1025            .delegation
1026            .stake
1027            .saturating_sub(minimum_pool_balance);
1028        let post_pool_lamports = pool_stake_info.lamports();
1029        msg!("Available stake post merge {}", post_pool_stake);
1030
1031        // stake lamports added, as a stake difference
1032        let stake_added = post_pool_stake
1033            .checked_sub(pre_pool_stake)
1034            .ok_or(SinglePoolError::ArithmeticOverflow)?;
1035
1036        // if there were excess lamports in the user-provided account, we return them
1037        // this includes their rent-exempt reserve if the pool is fully active
1038        let user_excess_lamports = post_pool_lamports
1039            .checked_sub(pool_stake_state.delegation.stake)
1040            .and_then(|amount| amount.checked_sub(pool_rent_exempt_reserve))
1041            .and_then(|amount| amount.checked_sub(pre_pool_excess_lamports))
1042            .ok_or(SinglePoolError::ArithmeticOverflow)?;
1043
1044        // sanity check: the user stake account is empty
1045        if user_stake_info.lamports() != 0 {
1046            return Err(SinglePoolError::UnexpectedMathError.into());
1047        }
1048
1049        let token_supply = {
1050            let pool_mint_data = pool_mint_info.try_borrow_data()?;
1051            let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?;
1052            pool_mint.supply
1053        };
1054
1055        // deposit amount is determined off stake because we return excess rent
1056        let new_pool_tokens = calculate_deposit_amount(token_supply, pre_pool_stake, stake_added)
1057            .ok_or(SinglePoolError::UnexpectedMathError)?;
1058
1059        if new_pool_tokens == 0 {
1060            return Err(SinglePoolError::DepositTooSmall.into());
1061        }
1062
1063        // mint tokens to the user corresponding to their stake deposit
1064        Self::token_mint_to(
1065            pool_info.key,
1066            token_program_info.clone(),
1067            pool_mint_info.clone(),
1068            user_token_account_info.clone(),
1069            pool_mint_authority_info.clone(),
1070            mint_authority_bump_seed,
1071            new_pool_tokens,
1072        )?;
1073
1074        // return any unstaked lamports the user stake account merged in
1075        if user_excess_lamports > 0 {
1076            Self::stake_withdraw(
1077                pool_info.key,
1078                pool_stake_info.clone(),
1079                pool_stake_authority_info.clone(),
1080                stake_authority_bump_seed,
1081                user_lamport_account_info.clone(),
1082                clock_info.clone(),
1083                stake_history_info.clone(),
1084                user_excess_lamports,
1085            )?;
1086        }
1087
1088        Ok(())
1089    }
1090
1091    fn process_withdraw_stake(
1092        program_id: &Pubkey,
1093        accounts: &[AccountInfo],
1094        user_stake_authority: &Pubkey,
1095        token_amount: u64,
1096    ) -> ProgramResult {
1097        let account_info_iter = &mut accounts.iter();
1098        let pool_info = next_account_info(account_info_iter)?;
1099        let pool_stake_info = next_account_info(account_info_iter)?;
1100        let pool_mint_info = {
1101            let account_info = next_account_info(account_info_iter)?;
1102            // we havent validated pool_info yet, so this doesnt validate onramp, but it doesnt matter
1103            // for now we just need to know whether to skip an account, we dont actually use it
1104            if check_pool_onramp_address(program_id, pool_info.key, account_info.key).is_ok() {
1105                next_account_info(account_info_iter)?
1106            } else {
1107                account_info
1108            }
1109        };
1110        let pool_stake_authority_info = next_account_info(account_info_iter)?;
1111        let pool_mint_authority_info = next_account_info(account_info_iter)?;
1112        let user_stake_info = next_account_info(account_info_iter)?;
1113        let user_token_account_info = next_account_info(account_info_iter)?;
1114        let clock_info = next_account_info(account_info_iter)?;
1115        let token_program_info = next_account_info(account_info_iter)?;
1116        let stake_program_info = next_account_info(account_info_iter)?;
1117
1118        SinglePool::from_account_info(pool_info, program_id)?;
1119
1120        check_pool_stake_address(program_id, pool_info.key, pool_stake_info.key)?;
1121        check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
1122        let stake_authority_bump_seed = check_pool_stake_authority_address(
1123            program_id,
1124            pool_info.key,
1125            pool_stake_authority_info.key,
1126        )?;
1127        let mint_authority_bump_seed = check_pool_mint_authority_address(
1128            program_id,
1129            pool_info.key,
1130            pool_mint_authority_info.key,
1131        )?;
1132        check_token_program(token_program_info.key)?;
1133        check_stake_program(stake_program_info.key)?;
1134
1135        if pool_stake_info.key == user_stake_info.key {
1136            return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
1137        }
1138
1139        let onramp_account_address = crate::find_pool_onramp_address(program_id, pool_info.key);
1140        if onramp_account_address == *user_stake_info.key {
1141            return Err(SinglePoolError::InvalidPoolStakeAccountUsage.into());
1142        }
1143
1144        let minimum_pool_balance = minimum_pool_balance()?;
1145
1146        let pre_pool_stake =
1147            get_stake_amount(pool_stake_info)?.saturating_sub(minimum_pool_balance);
1148        msg!("Available stake pre split {}", pre_pool_stake);
1149
1150        let token_supply = {
1151            let pool_mint_data = pool_mint_info.try_borrow_data()?;
1152            let pool_mint = Mint::unpack_from_slice(&pool_mint_data)?;
1153            pool_mint.supply
1154        };
1155
1156        // withdraw amount is determined off stake just like deposit amount
1157        let withdraw_stake = calculate_withdraw_amount(token_supply, pre_pool_stake, token_amount)
1158            .ok_or(SinglePoolError::UnexpectedMathError)?;
1159
1160        if withdraw_stake == 0 {
1161            return Err(SinglePoolError::WithdrawalTooSmall.into());
1162        }
1163
1164        // the second case should never be true, but its best to be sure
1165        if withdraw_stake > pre_pool_stake || withdraw_stake == pool_stake_info.lamports() {
1166            return Err(SinglePoolError::WithdrawalTooLarge.into());
1167        }
1168
1169        // burn user tokens corresponding to the amount of stake they wish to withdraw
1170        Self::token_burn(
1171            pool_info.key,
1172            token_program_info.clone(),
1173            user_token_account_info.clone(),
1174            pool_mint_info.clone(),
1175            pool_mint_authority_info.clone(),
1176            mint_authority_bump_seed,
1177            token_amount,
1178        )?;
1179
1180        // split stake into a blank stake account the user has created for this purpose
1181        Self::stake_split(
1182            pool_info.key,
1183            pool_stake_info.clone(),
1184            pool_stake_authority_info.clone(),
1185            stake_authority_bump_seed,
1186            withdraw_stake,
1187            user_stake_info.clone(),
1188        )?;
1189
1190        // assign both authorities on the new stake account to the user
1191        Self::stake_authorize(
1192            pool_info.key,
1193            user_stake_info.clone(),
1194            pool_stake_authority_info.clone(),
1195            stake_authority_bump_seed,
1196            user_stake_authority,
1197            clock_info.clone(),
1198        )?;
1199
1200        let post_pool_stake =
1201            get_stake_amount(pool_stake_info)?.saturating_sub(minimum_pool_balance);
1202        msg!("Available stake post split {}", post_pool_stake);
1203
1204        Ok(())
1205    }
1206
1207    fn process_create_pool_token_metadata(
1208        program_id: &Pubkey,
1209        accounts: &[AccountInfo],
1210    ) -> ProgramResult {
1211        let account_info_iter = &mut accounts.iter();
1212        let pool_info = next_account_info(account_info_iter)?;
1213        let pool_mint_info = next_account_info(account_info_iter)?;
1214        let pool_mint_authority_info = next_account_info(account_info_iter)?;
1215        let pool_mpl_authority_info = next_account_info(account_info_iter)?;
1216        let payer_info = next_account_info(account_info_iter)?;
1217        let metadata_info = next_account_info(account_info_iter)?;
1218        let mpl_token_metadata_program_info = next_account_info(account_info_iter)?;
1219        let system_program_info = next_account_info(account_info_iter)?;
1220
1221        let pool = SinglePool::from_account_info(pool_info, program_id)?;
1222
1223        let mint_authority_bump_seed = check_pool_mint_authority_address(
1224            program_id,
1225            pool_info.key,
1226            pool_mint_authority_info.key,
1227        )?;
1228        let mpl_authority_bump_seed = check_pool_mpl_authority_address(
1229            program_id,
1230            pool_info.key,
1231            pool_mpl_authority_info.key,
1232        )?;
1233        check_pool_mint_address(program_id, pool_info.key, pool_mint_info.key)?;
1234        check_system_program(system_program_info.key)?;
1235        check_account_owner(payer_info, &system_program::id())?;
1236        check_mpl_metadata_program(mpl_token_metadata_program_info.key)?;
1237        check_mpl_metadata_account_address(metadata_info.key, pool_mint_info.key)?;
1238
1239        if !payer_info.is_signer {
1240            msg!("Payer did not sign metadata creation");
1241            return Err(SinglePoolError::SignatureMissing.into());
1242        }
1243
1244        let vote_address_str = pool.vote_account_address.to_string();
1245        let token_name = format!("SPL Single Pool {}", &vote_address_str[0..15]);
1246        let token_symbol = format!("st{}", &vote_address_str[0..7]);
1247
1248        let new_metadata_instruction = create_metadata_accounts_v3(
1249            *mpl_token_metadata_program_info.key,
1250            *metadata_info.key,
1251            *pool_mint_info.key,
1252            *pool_mint_authority_info.key,
1253            *payer_info.key,
1254            *pool_mpl_authority_info.key,
1255            token_name,
1256            token_symbol,
1257            "".to_string(),
1258        );
1259
1260        let mint_authority_seeds = &[
1261            POOL_MINT_AUTHORITY_PREFIX,
1262            pool_info.key.as_ref(),
1263            &[mint_authority_bump_seed],
1264        ];
1265        let mpl_authority_seeds = &[
1266            POOL_MPL_AUTHORITY_PREFIX,
1267            pool_info.key.as_ref(),
1268            &[mpl_authority_bump_seed],
1269        ];
1270        let signers = &[&mint_authority_seeds[..], &mpl_authority_seeds[..]];
1271
1272        invoke_signed(
1273            &new_metadata_instruction,
1274            &[
1275                metadata_info.clone(),
1276                pool_mint_info.clone(),
1277                pool_mint_authority_info.clone(),
1278                payer_info.clone(),
1279                pool_mpl_authority_info.clone(),
1280                system_program_info.clone(),
1281            ],
1282            signers,
1283        )?;
1284
1285        Ok(())
1286    }
1287
1288    fn process_update_pool_token_metadata(
1289        program_id: &Pubkey,
1290        accounts: &[AccountInfo],
1291        name: String,
1292        symbol: String,
1293        uri: String,
1294    ) -> ProgramResult {
1295        let account_info_iter = &mut accounts.iter();
1296        let vote_account_info = next_account_info(account_info_iter)?;
1297        let pool_info = next_account_info(account_info_iter)?;
1298        let pool_mpl_authority_info = next_account_info(account_info_iter)?;
1299        let authorized_withdrawer_info = next_account_info(account_info_iter)?;
1300        let metadata_info = next_account_info(account_info_iter)?;
1301        let mpl_token_metadata_program_info = next_account_info(account_info_iter)?;
1302
1303        check_vote_account(vote_account_info)?;
1304        check_pool_address(program_id, vote_account_info.key, pool_info.key)?;
1305
1306        let pool = SinglePool::from_account_info(pool_info, program_id)?;
1307        if pool.vote_account_address != *vote_account_info.key {
1308            return Err(SinglePoolError::InvalidPoolAccount.into());
1309        }
1310
1311        let mpl_authority_bump_seed = check_pool_mpl_authority_address(
1312            program_id,
1313            pool_info.key,
1314            pool_mpl_authority_info.key,
1315        )?;
1316        let pool_mint_address = crate::find_pool_mint_address(program_id, pool_info.key);
1317        check_mpl_metadata_program(mpl_token_metadata_program_info.key)?;
1318        check_mpl_metadata_account_address(metadata_info.key, &pool_mint_address)?;
1319
1320        // we use authorized_withdrawer to authenticate the caller controls the vote
1321        // account this is safer than using an authorized_voter since those keys
1322        // live hot and validator-operators we spoke with indicated this would
1323        // be their preference as well
1324        let vote_account_data = &vote_account_info.try_borrow_data()?;
1325        let vote_account_withdrawer = vote_account_data
1326            .get(VOTE_STATE_AUTHORIZED_WITHDRAWER_START..VOTE_STATE_AUTHORIZED_WITHDRAWER_END)
1327            .and_then(|x| Pubkey::try_from(x).ok())
1328            .ok_or(SinglePoolError::UnparseableVoteAccount)?;
1329
1330        if *authorized_withdrawer_info.key != vote_account_withdrawer {
1331            msg!("Vote account authorized withdrawer does not match the account provided.");
1332            return Err(SinglePoolError::InvalidMetadataSigner.into());
1333        }
1334
1335        if !authorized_withdrawer_info.is_signer {
1336            msg!("Vote account authorized withdrawer did not sign metadata update.");
1337            return Err(SinglePoolError::SignatureMissing.into());
1338        }
1339
1340        let update_metadata_accounts_instruction = update_metadata_accounts_v2(
1341            *mpl_token_metadata_program_info.key,
1342            *metadata_info.key,
1343            *pool_mpl_authority_info.key,
1344            None,
1345            Some(DataV2 {
1346                name,
1347                symbol,
1348                uri,
1349                seller_fee_basis_points: 0,
1350                creators: None,
1351                collection: None,
1352                uses: None,
1353            }),
1354            None,
1355            Some(true),
1356        );
1357
1358        let mpl_authority_seeds = &[
1359            POOL_MPL_AUTHORITY_PREFIX,
1360            pool_info.key.as_ref(),
1361            &[mpl_authority_bump_seed],
1362        ];
1363        let signers = &[&mpl_authority_seeds[..]];
1364
1365        invoke_signed(
1366            &update_metadata_accounts_instruction,
1367            &[metadata_info.clone(), pool_mpl_authority_info.clone()],
1368            signers,
1369        )?;
1370
1371        Ok(())
1372    }
1373
1374    fn process_initialize_pool_onramp(
1375        program_id: &Pubkey,
1376        accounts: &[AccountInfo],
1377    ) -> ProgramResult {
1378        let account_info_iter = &mut accounts.iter();
1379        let pool_info = next_account_info(account_info_iter)?;
1380        let pool_onramp_info = next_account_info(account_info_iter)?;
1381        let pool_stake_authority_info = next_account_info(account_info_iter)?;
1382        let rent_info = next_account_info(account_info_iter)?;
1383        let rent = &Rent::from_account_info(rent_info)?;
1384        let system_program_info = next_account_info(account_info_iter)?;
1385        let stake_program_info = next_account_info(account_info_iter)?;
1386
1387        SinglePool::from_account_info(pool_info, program_id)?;
1388
1389        let onramp_bump_seed =
1390            check_pool_onramp_address(program_id, pool_info.key, pool_onramp_info.key)?;
1391        let stake_authority_bump_seed = check_pool_stake_authority_address(
1392            program_id,
1393            pool_info.key,
1394            pool_stake_authority_info.key,
1395        )?;
1396        check_system_program(system_program_info.key)?;
1397        check_stake_program(stake_program_info.key)?;
1398
1399        let onramp_seeds = &[
1400            POOL_ONRAMP_PREFIX,
1401            pool_info.key.as_ref(),
1402            &[onramp_bump_seed],
1403        ];
1404        let onramp_signers = &[&onramp_seeds[..]];
1405
1406        let stake_authority_seeds = &[
1407            POOL_STAKE_AUTHORITY_PREFIX,
1408            pool_info.key.as_ref(),
1409            &[stake_authority_bump_seed],
1410        ];
1411        let stake_authority_signers = &[&stake_authority_seeds[..]];
1412
1413        // create the pool on-ramp account. user has already transferred in rent
1414        let stake_space = StakeStateV2::size_of();
1415        let stake_rent = rent.minimum_balance(stake_space);
1416
1417        if pool_onramp_info.lamports() < stake_rent {
1418            return Err(SinglePoolError::WrongRentAmount.into());
1419        }
1420
1421        let authorized = stake::state::Authorized::auto(pool_stake_authority_info.key);
1422
1423        invoke_signed(
1424            &system_instruction::allocate(pool_onramp_info.key, stake_space as u64),
1425            &[pool_onramp_info.clone()],
1426            onramp_signers,
1427        )?;
1428
1429        invoke_signed(
1430            &system_instruction::assign(pool_onramp_info.key, stake_program_info.key),
1431            &[pool_onramp_info.clone()],
1432            onramp_signers,
1433        )?;
1434
1435        invoke_signed(
1436            &stake::instruction::initialize_checked(pool_onramp_info.key, &authorized),
1437            &[
1438                pool_onramp_info.clone(),
1439                rent_info.clone(),
1440                pool_stake_authority_info.clone(),
1441                pool_stake_authority_info.clone(),
1442            ],
1443            stake_authority_signers,
1444        )?;
1445
1446        Ok(())
1447    }
1448
1449    /// Processes [Instruction](enum.Instruction.html).
1450    pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
1451        let instruction = SinglePoolInstruction::try_from_slice(input)?;
1452        match instruction {
1453            SinglePoolInstruction::InitializePool => {
1454                msg!("Instruction: InitializePool");
1455                Self::process_initialize_pool(program_id, accounts)
1456            }
1457            SinglePoolInstruction::ReplenishPool => {
1458                msg!("Instruction: ReplenishPool");
1459                Self::process_replenish_pool(program_id, accounts)
1460            }
1461            SinglePoolInstruction::DepositStake => {
1462                msg!("Instruction: DepositStake");
1463                Self::process_deposit_stake(program_id, accounts)
1464            }
1465            SinglePoolInstruction::WithdrawStake {
1466                user_stake_authority,
1467                token_amount,
1468            } => {
1469                msg!("Instruction: WithdrawStake");
1470                Self::process_withdraw_stake(
1471                    program_id,
1472                    accounts,
1473                    &user_stake_authority,
1474                    token_amount,
1475                )
1476            }
1477            SinglePoolInstruction::CreateTokenMetadata => {
1478                msg!("Instruction: CreateTokenMetadata");
1479                Self::process_create_pool_token_metadata(program_id, accounts)
1480            }
1481            SinglePoolInstruction::UpdateTokenMetadata { name, symbol, uri } => {
1482                msg!("Instruction: UpdateTokenMetadata");
1483                Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri)
1484            }
1485            SinglePoolInstruction::InitializePoolOnRamp => {
1486                msg!("Instruction: InitializePoolOnRamp");
1487                Self::process_initialize_pool_onramp(program_id, accounts)
1488            }
1489        }
1490    }
1491}
1492
1493#[cfg(test)]
1494#[allow(clippy::arithmetic_side_effects)]
1495mod tests {
1496    use {
1497        super::*,
1498        approx::assert_relative_eq,
1499        rand::{
1500            distr::{Distribution, Uniform},
1501            rngs::StdRng,
1502            seq::IteratorRandom,
1503            Rng, SeedableRng,
1504        },
1505        std::collections::BTreeMap,
1506        test_case::test_case,
1507    };
1508
1509    // approximately 6%/yr assuking 146 epochs
1510    const INFLATION_BASE_RATE: f64 = 0.0004;
1511
1512    #[derive(Clone, Debug, Default)]
1513    struct PoolState {
1514        pub token_supply: u64,
1515        pub total_stake: u64,
1516        pub user_token_balances: BTreeMap<Pubkey, u64>,
1517    }
1518    impl PoolState {
1519        // deposits a given amount of stake and returns the equivalent tokens on success
1520        // note this is written as unsugared do-notation, so *any* failure returns None
1521        // otherwise returns the value produced by its respective calculate function
1522        #[rustfmt::skip]
1523        pub fn deposit(&mut self, user_pubkey: &Pubkey, stake_to_deposit: u64) -> Option<u64> {
1524            calculate_deposit_amount(self.token_supply, self.total_stake, stake_to_deposit)
1525                .and_then(|tokens_to_mint| self.token_supply.checked_add(tokens_to_mint)
1526                .and_then(|new_token_supply| self.total_stake.checked_add(stake_to_deposit)
1527                .and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey).or(Some(0))
1528                .and_then(|old_user_token_balance| old_user_token_balance.checked_add(tokens_to_mint)
1529                .map(|new_user_token_balance| {
1530                    self.token_supply = new_token_supply;
1531                    self.total_stake = new_total_stake;
1532                    let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance);
1533                    tokens_to_mint
1534            })))))
1535        }
1536
1537        // burns a given amount of tokens and returns the equivalent stake on success
1538        // note this is written as unsugared do-notation, so *any* failure returns None
1539        // otherwise returns the value produced by its respective calculate function
1540        #[rustfmt::skip]
1541        pub fn withdraw(&mut self, user_pubkey: &Pubkey, tokens_to_burn: u64) -> Option<u64> {
1542            calculate_withdraw_amount(self.token_supply, self.total_stake, tokens_to_burn)
1543                .and_then(|stake_to_withdraw| self.token_supply.checked_sub(tokens_to_burn)
1544                .and_then(|new_token_supply| self.total_stake.checked_sub(stake_to_withdraw)
1545                .and_then(|new_total_stake| self.user_token_balances.remove(user_pubkey)
1546                .and_then(|old_user_token_balance| old_user_token_balance.checked_sub(tokens_to_burn)
1547                .map(|new_user_token_balance| {
1548                    self.token_supply = new_token_supply;
1549                    self.total_stake = new_total_stake;
1550                    let _ = self.user_token_balances.insert(*user_pubkey, new_user_token_balance);
1551                    stake_to_withdraw
1552            })))))
1553        }
1554
1555        // adds an arbitrary amount of stake, as if inflation rewards were granted
1556        pub fn reward(&mut self, reward_amount: u64) {
1557            self.total_stake = self.total_stake.checked_add(reward_amount).unwrap();
1558        }
1559
1560        // get the token balance for a user
1561        pub fn tokens(&self, user_pubkey: &Pubkey) -> u64 {
1562            *self.user_token_balances.get(user_pubkey).unwrap_or(&0)
1563        }
1564
1565        // get the amount of stake that belongs to a user
1566        pub fn stake(&self, user_pubkey: &Pubkey) -> u64 {
1567            let tokens = self.tokens(user_pubkey);
1568            if tokens > 0 {
1569                u64::try_from(tokens as u128 * self.total_stake as u128 / self.token_supply as u128)
1570                    .unwrap()
1571            } else {
1572                0
1573            }
1574        }
1575
1576        // get the share of the pool that belongs to a user, as a float between 0 and 1
1577        pub fn share(&self, user_pubkey: &Pubkey) -> f64 {
1578            let tokens = self.tokens(user_pubkey);
1579            if tokens > 0 {
1580                tokens as f64 / self.token_supply as f64
1581            } else {
1582                0.0
1583            }
1584        }
1585    }
1586
1587    // this deterministically tests basic behavior of calculate_deposit_amount and
1588    // calculate_withdraw_amount
1589    #[test]
1590    fn simple_deposit_withdraw() {
1591        let mut pool = PoolState::default();
1592        let alice = Pubkey::new_unique();
1593        let bob = Pubkey::new_unique();
1594        let chad = Pubkey::new_unique();
1595
1596        // first deposit. alice now has 250
1597        pool.deposit(&alice, 250).unwrap();
1598        assert_eq!(pool.tokens(&alice), 250);
1599        assert_eq!(pool.token_supply, 250);
1600        assert_eq!(pool.total_stake, 250);
1601
1602        // second deposit. bob now has 750
1603        pool.deposit(&bob, 750).unwrap();
1604        assert_eq!(pool.tokens(&bob), 750);
1605        assert_eq!(pool.token_supply, 1000);
1606        assert_eq!(pool.total_stake, 1000);
1607
1608        // alice controls 25% of the pool and bob controls 75%. rewards should accrue
1609        // likewise use nice even numbers, we can test fiddly stuff in the
1610        // stochastic cases
1611        assert_relative_eq!(pool.share(&alice), 0.25);
1612        assert_relative_eq!(pool.share(&bob), 0.75);
1613        pool.reward(1000);
1614        assert_eq!(pool.stake(&alice), pool.tokens(&alice) * 2);
1615        assert_eq!(pool.stake(&bob), pool.tokens(&bob) * 2);
1616        assert_relative_eq!(pool.share(&alice), 0.25);
1617        assert_relative_eq!(pool.share(&bob), 0.75);
1618
1619        // alice harvests rewards, reducing her share of the *previous* pool size to
1620        // 12.5% but because the pool itself has shrunk to 87.5%, its actually
1621        // more like 14.3% luckily chad deposits immediately after to make our
1622        // math easier
1623        let stake_removed = pool.withdraw(&alice, 125).unwrap();
1624        pool.deposit(&chad, 250).unwrap();
1625        assert_eq!(stake_removed, 250);
1626        assert_relative_eq!(pool.share(&alice), 0.125);
1627        assert_relative_eq!(pool.share(&bob), 0.75);
1628
1629        // bob and chad exit the pool
1630        let stake_removed = pool.withdraw(&bob, 750).unwrap();
1631        assert_eq!(stake_removed, 1500);
1632        assert_relative_eq!(pool.share(&bob), 0.0);
1633        pool.withdraw(&chad, 125).unwrap();
1634        assert_relative_eq!(pool.share(&alice), 1.0);
1635    }
1636
1637    // this stochastically tests calculate_deposit_amount and
1638    // calculate_withdraw_amount the objective is specifically to ensure that
1639    // the math does not fail on any combination of state changes the no_minimum
1640    // case is to account for a future where small deposits are possible through
1641    // multistake
1642    #[test_case(rand::random(), false, false; "no_rewards")]
1643    #[test_case(rand::random(), true, false; "with_rewards")]
1644    #[test_case(rand::random(), true, true; "no_minimum")]
1645    fn random_deposit_withdraw(seed: u64, with_rewards: bool, no_minimum: bool) {
1646        println!(
1647            "TEST SEED: {seed}. edit the test case to pass this value if needed to debug failures",
1648        );
1649        let mut prng = rand::rngs::StdRng::seed_from_u64(seed);
1650
1651        // deposit_range is the range of typical deposits within minimum_delegation
1652        // minnow_range is under the minimum for cases where we test that
1653        // op_range is how we roll whether to deposit, withdraw, or reward
1654        // std_range is a standard probability
1655        let deposit_range = Uniform::try_from(LAMPORTS_PER_SOL..LAMPORTS_PER_SOL * 1000).unwrap();
1656        let minnow_range = Uniform::try_from(1..LAMPORTS_PER_SOL).unwrap();
1657        let op_range = Uniform::try_from(if with_rewards { 0.0..1.0 } else { 0.0..0.65 }).unwrap();
1658        let std_range = Uniform::try_from(0.0..1.0).unwrap();
1659
1660        let deposit_amount = |prng: &mut StdRng| {
1661            if no_minimum && prng.random_bool(0.2) {
1662                minnow_range.sample(prng)
1663            } else {
1664                deposit_range.sample(prng)
1665            }
1666        };
1667
1668        // run everything a number of times to get a good sample
1669        for _ in 0..100 {
1670            // PoolState tracks all outstanding tokens and the total combined stake
1671            // there is no reasonable way to track "deposited stake" because reward accrual
1672            // makes this concept incoherent a token corresponds to a
1673            // percentage, not a stake value
1674            let mut pool = PoolState::default();
1675
1676            // generate between 1 and 100 users and have ~half of them deposit
1677            // note for most of these tests we adhere to the minimum delegation
1678            // one of the thing we want to see is deposit size being many ooms larger than
1679            // reward size
1680            let mut users = vec![];
1681            let user_count: usize = prng.random_range(1..=100);
1682            for _ in 0..user_count {
1683                let user = Pubkey::new_unique();
1684
1685                if prng.random_bool(0.5) {
1686                    pool.deposit(&user, deposit_amount(&mut prng)).unwrap();
1687                }
1688
1689                users.push(user);
1690            }
1691
1692            // now we do a set of arbitrary operations and confirm invariants hold
1693            // we underweight withdraw a little bit to lessen the chances we random walk to
1694            // an empty pool
1695            for _ in 0..1000 {
1696                match op_range.sample(&mut prng) {
1697                    // deposit a random amount of stake for tokens with a random user
1698                    // check their stake, tokens, and share increase by the expected amount
1699                    n if n <= 0.35 => {
1700                        let user = users.iter().choose(&mut prng).unwrap();
1701                        let prev_share = pool.share(user);
1702                        let prev_stake = pool.stake(user);
1703                        let prev_token_supply = pool.token_supply;
1704                        let prev_total_stake = pool.total_stake;
1705
1706                        let stake_deposited = deposit_amount(&mut prng);
1707                        let tokens_minted = pool.deposit(user, stake_deposited).unwrap();
1708
1709                        // stake increased by exactly the deposit amount
1710                        assert_eq!(pool.total_stake - prev_total_stake, stake_deposited);
1711
1712                        // calculated stake fraction is within 2 lamps of deposit amount
1713                        assert!(
1714                            (pool.stake(user) as i64 - prev_stake as i64 - stake_deposited as i64)
1715                                .abs()
1716                                <= 2
1717                        );
1718
1719                        // tokens increased by exactly the mint amount
1720                        assert_eq!(pool.token_supply - prev_token_supply, tokens_minted);
1721
1722                        // tokens per supply increased with stake per total
1723                        if prev_total_stake > 0 {
1724                            assert_relative_eq!(
1725                                pool.share(user) - prev_share,
1726                                pool.stake(user) as f64 / pool.total_stake as f64
1727                                    - prev_stake as f64 / prev_total_stake as f64,
1728                                epsilon = 1e-6
1729                            );
1730                        }
1731                    }
1732
1733                    // burn a random amount of tokens from a random user with outstanding deposits
1734                    // check their stake, tokens, and share decrease by the expected amount
1735                    n if n > 0.35 && n <= 0.65 => {
1736                        if let Some(user) = users
1737                            .iter()
1738                            .filter(|user| pool.tokens(user) > 0)
1739                            .choose(&mut prng)
1740                        {
1741                            let prev_tokens = pool.tokens(user);
1742                            let prev_share = pool.share(user);
1743                            let prev_stake = pool.stake(user);
1744                            let prev_token_supply = pool.token_supply;
1745                            let prev_total_stake = pool.total_stake;
1746
1747                            let tokens_burned = if std_range.sample(&mut prng) <= 0.1 {
1748                                prev_tokens
1749                            } else {
1750                                prng.random_range(0..prev_tokens)
1751                            };
1752                            let stake_received = pool.withdraw(user, tokens_burned).unwrap();
1753
1754                            // stake decreased by exactly the withdraw amount
1755                            assert_eq!(prev_total_stake - pool.total_stake, stake_received);
1756
1757                            // calculated stake fraction is within 2 lamps of withdraw amount
1758                            assert!(
1759                                (prev_stake as i64
1760                                    - pool.stake(user) as i64
1761                                    - stake_received as i64)
1762                                    .abs()
1763                                    <= 2
1764                            );
1765
1766                            // tokens decreased by the burn amount
1767                            assert_eq!(prev_token_supply - pool.token_supply, tokens_burned);
1768
1769                            // tokens per supply decreased with stake per total
1770                            if pool.total_stake > 0 {
1771                                assert_relative_eq!(
1772                                    prev_share - pool.share(user),
1773                                    prev_stake as f64 / prev_total_stake as f64
1774                                        - pool.stake(user) as f64 / pool.total_stake as f64,
1775                                    epsilon = 1e-6
1776                                );
1777                            }
1778                        };
1779                    }
1780
1781                    // run a single epoch worth of rewards
1782                    // check all user shares stay the same and stakes increase by the expected
1783                    // amount
1784                    _ => {
1785                        assert!(with_rewards);
1786
1787                        let prev_shares_stakes = users
1788                            .iter()
1789                            .map(|user| (user, pool.share(user), pool.stake(user)))
1790                            .filter(|(_, _, stake)| stake > &0)
1791                            .collect::<Vec<_>>();
1792
1793                        pool.reward((pool.total_stake as f64 * INFLATION_BASE_RATE) as u64);
1794
1795                        for (user, prev_share, prev_stake) in prev_shares_stakes {
1796                            // shares are the same before and after
1797                            assert_eq!(pool.share(user), prev_share);
1798
1799                            let curr_stake = pool.stake(user);
1800                            let stake_share = prev_stake as f64 * INFLATION_BASE_RATE;
1801                            let stake_diff = (curr_stake - prev_stake) as f64;
1802
1803                            // stake increase is within 2 lamps when calculated as a difference or a
1804                            // percentage
1805                            assert!((stake_share - stake_diff).abs() <= 2.0);
1806                        }
1807                    }
1808                }
1809            }
1810        }
1811    }
1812}