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