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