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