Skip to main content

spl_single_pool/
processor.rs

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