Skip to main content

spl_stake_pool/
state.rs

1//! State transition types
2
3use {
4    crate::{
5        big_vec::BigVec, error::StakePoolError, MAX_WITHDRAWAL_FEE_INCREASE,
6        MAX_WITHDRAWAL_FEE_INCREASE_FACTOR, WITHDRAWAL_BASELINE_FEE,
7    },
8    borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
9    bytemuck::{Pod, Zeroable},
10    num_derive::{FromPrimitive, ToPrimitive},
11    num_traits::{FromPrimitive, ToPrimitive},
12    solana_account_info::AccountInfo,
13    solana_borsh::v1::get_instance_packed_len,
14    solana_msg::msg,
15    solana_program_error::ProgramError,
16    solana_program_memory::sol_memcmp,
17    solana_program_pack::{Pack, Sealed},
18    solana_pubkey::{Pubkey, PUBKEY_BYTES},
19    solana_stake_interface::state::Lockup,
20    spl_pod::primitives::{PodU32, PodU64},
21    spl_token_2022_interface::{
22        extension::{BaseStateWithExtensions, ExtensionType, StateWithExtensions},
23        state::{Account, AccountState, Mint},
24    },
25    std::{borrow::Borrow, convert::TryFrom, fmt, matches},
26};
27
28/// Enum representing the account type managed by the program
29#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
30pub enum AccountType {
31    /// If the account has not been initialized, the enum will be 0
32    #[default]
33    Uninitialized,
34    /// Stake pool
35    StakePool,
36    /// Validator stake list
37    ValidatorList,
38}
39
40/// Initialized program details.
41#[repr(C)]
42#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
43pub struct StakePool {
44    /// Account type, must be `StakePool` currently
45    pub account_type: AccountType,
46
47    /// Manager authority, allows for updating the staker, manager, and fee
48    /// account
49    pub manager: Pubkey,
50
51    /// Staker authority, allows for adding and removing validators, and
52    /// managing stake distribution
53    pub staker: Pubkey,
54
55    /// Stake deposit authority
56    ///
57    /// If a depositor pubkey is specified on initialization, then deposits must
58    /// be signed by this authority. If no deposit authority is specified,
59    /// then the stake pool will default to the result of:
60    /// `Pubkey::find_program_address(
61    ///     &[&stake_pool_address.as_ref(), b"deposit"],
62    ///     program_id,
63    /// )`
64    pub stake_deposit_authority: Pubkey,
65
66    /// Stake withdrawal authority bump seed
67    /// for `create_program_address(&[state::StakePool account, "withdrawal"])`
68    pub stake_withdraw_bump_seed: u8,
69
70    /// Validator stake list storage account
71    pub validator_list: Pubkey,
72
73    /// Reserve stake account, holds deactivated stake
74    pub reserve_stake: Pubkey,
75
76    /// Pool Mint
77    pub pool_mint: Pubkey,
78
79    /// Manager fee account
80    pub manager_fee_account: Pubkey,
81
82    /// Pool token program id
83    pub token_program_id: Pubkey,
84
85    /// Total stake under management.
86    /// Note that if `last_update_epoch` does not match the current epoch then
87    /// this field may not be accurate
88    pub total_lamports: u64,
89
90    /// Total supply of pool tokens (should always match the supply in the Pool
91    /// Mint)
92    pub pool_token_supply: u64,
93
94    /// Last epoch the `total_lamports` field was updated
95    pub last_update_epoch: u64,
96
97    /// Lockup that all stakes in the pool must have
98    pub lockup: Lockup,
99
100    /// Fee taken as a proportion of rewards each epoch
101    pub epoch_fee: Fee,
102
103    /// Fee for next epoch
104    pub next_epoch_fee: FutureEpoch<Fee>,
105
106    /// Preferred deposit validator vote account pubkey
107    pub preferred_deposit_validator_vote_address: Option<Pubkey>,
108
109    /// Preferred withdraw validator vote account pubkey
110    pub preferred_withdraw_validator_vote_address: Option<Pubkey>,
111
112    /// Fee assessed on stake deposits
113    pub stake_deposit_fee: Fee,
114
115    /// Fee assessed on withdrawals
116    pub stake_withdrawal_fee: Fee,
117
118    /// Future stake withdrawal fee, to be set for the following epoch
119    pub next_stake_withdrawal_fee: FutureEpoch<Fee>,
120
121    /// Fees paid out to referrers on referred stake deposits.
122    /// Expressed as a percentage (0 - 100) of deposit fees.
123    /// i.e. `stake_deposit_fee`% of stake deposited is collected as deposit
124    /// fees for every deposit and `stake_referral_fee`% of the collected
125    /// stake deposit fees is paid out to the referrer
126    pub stake_referral_fee: u8,
127
128    /// Toggles whether the `DepositSol` instruction requires a signature from
129    /// this `sol_deposit_authority`
130    pub sol_deposit_authority: Option<Pubkey>,
131
132    /// Fee assessed on SOL deposits
133    pub sol_deposit_fee: Fee,
134
135    /// Fees paid out to referrers on referred SOL deposits.
136    /// Expressed as a percentage (0 - 100) of SOL deposit fees.
137    /// i.e. `sol_deposit_fee`% of SOL deposited is collected as deposit fees
138    /// for every deposit and `sol_referral_fee`% of the collected SOL
139    /// deposit fees is paid out to the referrer
140    pub sol_referral_fee: u8,
141
142    /// Toggles whether the `WithdrawSol` instruction requires a signature from
143    /// the `deposit_authority`
144    pub sol_withdraw_authority: Option<Pubkey>,
145
146    /// Fee assessed on SOL withdrawals
147    pub sol_withdrawal_fee: Fee,
148
149    /// Future SOL withdrawal fee, to be set for the following epoch
150    pub next_sol_withdrawal_fee: FutureEpoch<Fee>,
151
152    /// Last epoch's total pool tokens, used only for APR estimation
153    pub last_epoch_pool_token_supply: u64,
154
155    /// Last epoch's total lamports, used only for APR estimation
156    pub last_epoch_total_lamports: u64,
157}
158impl StakePool {
159    /// calculate the pool tokens that should be minted for a deposit of
160    /// `stake_lamports`
161    #[inline]
162    pub fn calc_pool_tokens_for_deposit(&self, stake_lamports: u64) -> Option<u64> {
163        if self.total_lamports == 0 || self.pool_token_supply == 0 {
164            return Some(stake_lamports);
165        }
166        u64::try_from(
167            (stake_lamports as u128)
168                .checked_mul(self.pool_token_supply as u128)?
169                .checked_div(self.total_lamports as u128)?,
170        )
171        .ok()
172    }
173
174    /// calculate lamports amount on withdrawal
175    #[inline]
176    pub fn calc_lamports_withdraw_amount(&self, pool_tokens: u64) -> Option<u64> {
177        // `checked_div` returns `None` for a 0 quotient result, but in this
178        // case, a return of 0 is valid for small amounts of pool tokens. So
179        // we check for that separately
180        let numerator = (pool_tokens as u128).checked_mul(self.total_lamports as u128)?;
181        let denominator = self.pool_token_supply as u128;
182        if numerator < denominator || denominator == 0 {
183            Some(0)
184        } else {
185            u64::try_from(numerator.checked_div(denominator)?).ok()
186        }
187    }
188
189    /// calculate pool tokens to be deducted as withdrawal fees
190    #[inline]
191    pub fn calc_pool_tokens_stake_withdrawal_fee(&self, pool_tokens: u64) -> Option<u64> {
192        u64::try_from(self.stake_withdrawal_fee.apply(pool_tokens)?).ok()
193    }
194
195    /// calculate pool tokens to be deducted as withdrawal fees
196    #[inline]
197    pub fn calc_pool_tokens_sol_withdrawal_fee(&self, pool_tokens: u64) -> Option<u64> {
198        u64::try_from(self.sol_withdrawal_fee.apply(pool_tokens)?).ok()
199    }
200
201    /// calculate pool tokens to be deducted as stake deposit fees
202    #[inline]
203    pub fn calc_pool_tokens_stake_deposit_fee(&self, pool_tokens_minted: u64) -> Option<u64> {
204        u64::try_from(self.stake_deposit_fee.apply(pool_tokens_minted)?).ok()
205    }
206
207    /// calculate pool tokens to be deducted from deposit fees as referral fees
208    #[inline]
209    pub fn calc_pool_tokens_stake_referral_fee(&self, stake_deposit_fee: u64) -> Option<u64> {
210        u64::try_from(
211            (stake_deposit_fee as u128)
212                .checked_mul(self.stake_referral_fee as u128)?
213                .checked_div(100u128)?,
214        )
215        .ok()
216    }
217
218    /// calculate pool tokens to be deducted as SOL deposit fees
219    #[inline]
220    pub fn calc_pool_tokens_sol_deposit_fee(&self, pool_tokens_minted: u64) -> Option<u64> {
221        u64::try_from(self.sol_deposit_fee.apply(pool_tokens_minted)?).ok()
222    }
223
224    /// calculate pool tokens to be deducted from SOL deposit fees as referral
225    /// fees
226    #[inline]
227    pub fn calc_pool_tokens_sol_referral_fee(&self, sol_deposit_fee: u64) -> Option<u64> {
228        u64::try_from(
229            (sol_deposit_fee as u128)
230                .checked_mul(self.sol_referral_fee as u128)?
231                .checked_div(100u128)?,
232        )
233        .ok()
234    }
235
236    /// Calculate the fee in pool tokens that goes to the manager
237    ///
238    /// This function assumes that `reward_lamports` has not already been added
239    /// to the stake pool's `total_lamports`
240    #[inline]
241    pub fn calc_epoch_fee_amount(&self, reward_lamports: u64) -> Option<u64> {
242        if reward_lamports == 0 {
243            return Some(0);
244        }
245        let total_lamports = (self.total_lamports as u128).checked_add(reward_lamports as u128)?;
246        let fee_lamports = self.epoch_fee.apply(reward_lamports)?;
247        if total_lamports == fee_lamports || self.pool_token_supply == 0 {
248            Some(reward_lamports)
249        } else {
250            u64::try_from(
251                (self.pool_token_supply as u128)
252                    .checked_mul(fee_lamports)?
253                    .checked_div(total_lamports.checked_sub(fee_lamports)?)?,
254            )
255            .ok()
256        }
257    }
258
259    /// Get the current value of pool tokens, rounded up
260    #[inline]
261    pub fn get_lamports_per_pool_token(&self) -> Option<u64> {
262        self.total_lamports
263            .checked_add(self.pool_token_supply)?
264            .checked_sub(1)?
265            .checked_div(self.pool_token_supply)
266    }
267
268    /// Checks that the withdraw or deposit authority is valid
269    fn check_program_derived_authority(
270        authority_address: &Pubkey,
271        program_id: &Pubkey,
272        stake_pool_address: &Pubkey,
273        authority_seed: &[u8],
274        bump_seed: u8,
275    ) -> Result<(), ProgramError> {
276        let expected_address = Pubkey::create_program_address(
277            &[stake_pool_address.as_ref(), authority_seed, &[bump_seed]],
278            program_id,
279        )?;
280
281        if *authority_address == expected_address {
282            Ok(())
283        } else {
284            msg!(
285                "Incorrect authority provided, expected {}, received {}",
286                expected_address,
287                authority_address
288            );
289            Err(StakePoolError::InvalidProgramAddress.into())
290        }
291    }
292
293    /// Check if the manager fee info is a valid token program account
294    /// capable of receiving tokens from the mint.
295    pub fn check_manager_fee_info(
296        &self,
297        manager_fee_info: &AccountInfo,
298    ) -> Result<(), ProgramError> {
299        let account_data = manager_fee_info.try_borrow_data()?;
300        let token_account = StateWithExtensions::<Account>::unpack(&account_data)?;
301        if manager_fee_info.owner != &self.token_program_id
302            || token_account.base.state != AccountState::Initialized
303            || token_account.base.mint != self.pool_mint
304        {
305            msg!("Manager fee account is not owned by token program, is not initialized, or does not match stake pool's mint");
306            return Err(StakePoolError::InvalidFeeAccount.into());
307        }
308        let extensions = token_account.get_extension_types()?;
309        if extensions
310            .iter()
311            .any(|x| !is_extension_supported_for_fee_account(x))
312        {
313            return Err(StakePoolError::UnsupportedFeeAccountExtension.into());
314        }
315        Ok(())
316    }
317
318    /// Checks that the withdraw authority is valid
319    #[inline]
320    pub(crate) fn check_authority_withdraw(
321        &self,
322        withdraw_authority: &Pubkey,
323        program_id: &Pubkey,
324        stake_pool_address: &Pubkey,
325    ) -> Result<(), ProgramError> {
326        Self::check_program_derived_authority(
327            withdraw_authority,
328            program_id,
329            stake_pool_address,
330            crate::AUTHORITY_WITHDRAW,
331            self.stake_withdraw_bump_seed,
332        )
333    }
334    /// Checks that the deposit authority is valid
335    #[inline]
336    pub(crate) fn check_stake_deposit_authority(
337        &self,
338        stake_deposit_authority: &Pubkey,
339    ) -> Result<(), ProgramError> {
340        if self.stake_deposit_authority == *stake_deposit_authority {
341            Ok(())
342        } else {
343            Err(StakePoolError::InvalidStakeDepositAuthority.into())
344        }
345    }
346
347    /// Checks that the deposit authority is valid
348    /// Does nothing if `sol_deposit_authority` is currently not set
349    #[inline]
350    pub(crate) fn check_sol_deposit_authority(
351        &self,
352        maybe_sol_deposit_authority: Result<&AccountInfo, ProgramError>,
353    ) -> Result<(), ProgramError> {
354        if let Some(auth) = self.sol_deposit_authority {
355            let sol_deposit_authority = maybe_sol_deposit_authority?;
356            if auth != *sol_deposit_authority.key {
357                msg!("Expected {}, received {}", auth, sol_deposit_authority.key);
358                return Err(StakePoolError::InvalidSolDepositAuthority.into());
359            }
360            if !sol_deposit_authority.is_signer {
361                msg!("SOL Deposit authority signature missing");
362                return Err(StakePoolError::SignatureMissing.into());
363            }
364        }
365        Ok(())
366    }
367
368    /// Checks that the sol withdraw authority is valid
369    /// Does nothing if `sol_withdraw_authority` is currently not set
370    #[inline]
371    pub(crate) fn check_sol_withdraw_authority(
372        &self,
373        maybe_sol_withdraw_authority: Result<&AccountInfo, ProgramError>,
374    ) -> Result<(), ProgramError> {
375        if let Some(auth) = self.sol_withdraw_authority {
376            let sol_withdraw_authority = maybe_sol_withdraw_authority?;
377            if auth != *sol_withdraw_authority.key {
378                return Err(StakePoolError::InvalidSolWithdrawAuthority.into());
379            }
380            if !sol_withdraw_authority.is_signer {
381                msg!("SOL withdraw authority signature missing");
382                return Err(StakePoolError::SignatureMissing.into());
383            }
384        }
385        Ok(())
386    }
387
388    /// Check mint is correct
389    #[inline]
390    pub(crate) fn check_mint(&self, mint_info: &AccountInfo) -> Result<u8, ProgramError> {
391        if *mint_info.key != self.pool_mint {
392            Err(StakePoolError::WrongPoolMint.into())
393        } else {
394            let mint_data = mint_info.try_borrow_data()?;
395            let mint = StateWithExtensions::<Mint>::unpack(&mint_data)?;
396            Ok(mint.base.decimals)
397        }
398    }
399
400    /// Check manager validity and signature
401    pub(crate) fn check_manager(&self, manager_info: &AccountInfo) -> Result<(), ProgramError> {
402        if *manager_info.key != self.manager {
403            msg!(
404                "Incorrect manager provided, expected {}, received {}",
405                self.manager,
406                manager_info.key
407            );
408            return Err(StakePoolError::WrongManager.into());
409        }
410        if !manager_info.is_signer {
411            msg!("Manager signature missing");
412            return Err(StakePoolError::SignatureMissing.into());
413        }
414        Ok(())
415    }
416
417    /// Check staker validity and signature
418    pub(crate) fn check_staker(&self, staker_info: &AccountInfo) -> Result<(), ProgramError> {
419        if *staker_info.key != self.staker {
420            msg!(
421                "Incorrect staker provided, expected {}, received {}",
422                self.staker,
423                staker_info.key
424            );
425            return Err(StakePoolError::WrongStaker.into());
426        }
427        if !staker_info.is_signer {
428            msg!("Staker signature missing");
429            return Err(StakePoolError::SignatureMissing.into());
430        }
431        Ok(())
432    }
433
434    /// Check the validator list is valid
435    pub fn check_validator_list(
436        &self,
437        validator_list_info: &AccountInfo,
438    ) -> Result<(), ProgramError> {
439        if *validator_list_info.key != self.validator_list {
440            msg!(
441                "Invalid validator list provided, expected {}, received {}",
442                self.validator_list,
443                validator_list_info.key
444            );
445            Err(StakePoolError::InvalidValidatorStakeList.into())
446        } else {
447            Ok(())
448        }
449    }
450
451    /// Check the reserve stake is valid
452    pub fn check_reserve_stake(
453        &self,
454        reserve_stake_info: &AccountInfo,
455    ) -> Result<(), ProgramError> {
456        if *reserve_stake_info.key != self.reserve_stake {
457            msg!(
458                "Invalid reserve stake provided, expected {}, received {}",
459                self.reserve_stake,
460                reserve_stake_info.key
461            );
462            Err(StakePoolError::InvalidProgramAddress.into())
463        } else {
464            Ok(())
465        }
466    }
467
468    /// Check if `StakePool` is actually initialized as a stake pool
469    pub fn is_valid(&self) -> bool {
470        self.account_type == AccountType::StakePool
471    }
472
473    /// Check if `StakePool` is currently uninitialized
474    pub fn is_uninitialized(&self) -> bool {
475        self.account_type == AccountType::Uninitialized
476    }
477
478    /// Updates one of the `StakePool`'s fees.
479    pub fn update_fee(&mut self, fee: &FeeType) -> Result<(), StakePoolError> {
480        match fee {
481            FeeType::SolReferral(new_fee) => self.sol_referral_fee = *new_fee,
482            FeeType::StakeReferral(new_fee) => self.stake_referral_fee = *new_fee,
483            FeeType::Epoch(new_fee) => self.next_epoch_fee = FutureEpoch::new(*new_fee),
484            FeeType::StakeWithdrawal(new_fee) => {
485                new_fee.check_withdrawal(&self.stake_withdrawal_fee)?;
486                self.next_stake_withdrawal_fee = FutureEpoch::new(*new_fee)
487            }
488            FeeType::SolWithdrawal(new_fee) => {
489                new_fee.check_withdrawal(&self.sol_withdrawal_fee)?;
490                self.next_sol_withdrawal_fee = FutureEpoch::new(*new_fee)
491            }
492            FeeType::SolDeposit(new_fee) => self.sol_deposit_fee = *new_fee,
493            FeeType::StakeDeposit(new_fee) => self.stake_deposit_fee = *new_fee,
494        };
495        Ok(())
496    }
497}
498
499/// Checks if the given extension is supported for the stake pool mint
500pub fn is_extension_supported_for_mint(extension_type: &ExtensionType) -> bool {
501    const SUPPORTED_EXTENSIONS: [ExtensionType; 8] = [
502        ExtensionType::Uninitialized,
503        ExtensionType::TransferFeeConfig,
504        ExtensionType::ConfidentialTransferMint,
505        ExtensionType::ConfidentialTransferFeeConfig,
506        ExtensionType::DefaultAccountState,
507        ExtensionType::InterestBearingConfig,
508        ExtensionType::MetadataPointer,
509        ExtensionType::TokenMetadata,
510    ];
511    if !SUPPORTED_EXTENSIONS.contains(extension_type) {
512        msg!(
513            "Stake pool mint account cannot have the {:?} extension",
514            extension_type
515        );
516        false
517    } else {
518        true
519    }
520}
521
522/// Checks if the given extension is supported for the stake pool's fee account
523pub fn is_extension_supported_for_fee_account(extension_type: &ExtensionType) -> bool {
524    // Note: this does not include the `ConfidentialTransferAccount` extension
525    // because it is possible to block non-confidential transfers with the
526    // extension enabled.
527    const SUPPORTED_EXTENSIONS: [ExtensionType; 4] = [
528        ExtensionType::Uninitialized,
529        ExtensionType::TransferFeeAmount,
530        ExtensionType::ImmutableOwner,
531        ExtensionType::CpiGuard,
532    ];
533    if !SUPPORTED_EXTENSIONS.contains(extension_type) {
534        msg!("Fee account cannot have the {:?} extension", extension_type);
535        false
536    } else {
537        true
538    }
539}
540
541/// Storage list for all validator stake accounts in the pool.
542#[repr(C)]
543#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
544pub struct ValidatorList {
545    /// Data outside of the validator list, separated out for cheaper
546    /// deserialization
547    pub header: ValidatorListHeader,
548
549    /// List of stake info for each validator in the pool
550    pub validators: Vec<ValidatorStakeInfo>,
551}
552
553/// Helper type to deserialize just the start of a `ValidatorList`
554#[repr(C)]
555#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
556pub struct ValidatorListHeader {
557    /// Account type, must be `ValidatorList` currently
558    pub account_type: AccountType,
559
560    /// Maximum allowable number of validators
561    pub max_validators: u32,
562}
563
564/// Status of the stake account in the validator list, for accounting
565#[derive(
566    ToPrimitive,
567    FromPrimitive,
568    Copy,
569    Clone,
570    Debug,
571    Default,
572    PartialEq,
573    BorshDeserialize,
574    BorshSerialize,
575    BorshSchema,
576)]
577pub enum StakeStatus {
578    /// Stake account is active, there may be a transient stake as well
579    #[default]
580    Active,
581    /// Only transient stake account exists, when a transient stake is
582    /// deactivating during validator removal
583    DeactivatingTransient,
584    /// No more validator stake accounts exist, entry ready for removal during
585    /// `UpdateStakePoolBalance`
586    ReadyForRemoval,
587    /// Only the validator stake account is deactivating, no transient stake
588    /// account exists
589    DeactivatingValidator,
590    /// Both the transient and validator stake account are deactivating, when
591    /// a validator is removed with a transient stake active
592    DeactivatingAll,
593}
594
595/// Wrapper struct that can be `Pod`, containing a byte that *should* be a valid
596/// `StakeStatus` underneath.
597#[repr(transparent)]
598#[derive(
599    Clone,
600    Copy,
601    Debug,
602    Default,
603    PartialEq,
604    Pod,
605    Zeroable,
606    BorshDeserialize,
607    BorshSerialize,
608    BorshSchema,
609)]
610pub struct PodStakeStatus(u8);
611impl PodStakeStatus {
612    /// Downgrade the status towards ready for removal by removing the validator
613    /// stake
614    pub fn remove_validator_stake(&mut self) -> Result<(), ProgramError> {
615        let status = StakeStatus::try_from(*self)?;
616        let new_self = match status {
617            StakeStatus::Active
618            | StakeStatus::DeactivatingTransient
619            | StakeStatus::ReadyForRemoval => status,
620            StakeStatus::DeactivatingAll => StakeStatus::DeactivatingTransient,
621            StakeStatus::DeactivatingValidator => StakeStatus::ReadyForRemoval,
622        };
623        *self = new_self.into();
624        Ok(())
625    }
626    /// Downgrade the status towards ready for removal by removing the transient
627    /// stake
628    pub fn remove_transient_stake(&mut self) -> Result<(), ProgramError> {
629        let status = StakeStatus::try_from(*self)?;
630        let new_self = match status {
631            StakeStatus::Active
632            | StakeStatus::DeactivatingValidator
633            | StakeStatus::ReadyForRemoval => status,
634            StakeStatus::DeactivatingAll => StakeStatus::DeactivatingValidator,
635            StakeStatus::DeactivatingTransient => StakeStatus::ReadyForRemoval,
636        };
637        *self = new_self.into();
638        Ok(())
639    }
640}
641impl TryFrom<PodStakeStatus> for StakeStatus {
642    type Error = ProgramError;
643    fn try_from(pod: PodStakeStatus) -> Result<Self, Self::Error> {
644        FromPrimitive::from_u8(pod.0).ok_or(ProgramError::InvalidAccountData)
645    }
646}
647impl From<StakeStatus> for PodStakeStatus {
648    fn from(status: StakeStatus) -> Self {
649        // unwrap is safe here because the variants of `StakeStatus` fit very
650        // comfortably within a `u8`
651        PodStakeStatus(status.to_u8().unwrap())
652    }
653}
654
655/// Withdrawal type, figured out during `process_withdraw_stake`
656#[derive(Debug, PartialEq)]
657pub(crate) enum StakeWithdrawSource {
658    /// Some of an active stake account, but not all
659    Active,
660    /// Some of a transient stake account
661    Transient,
662    /// Take a whole validator stake account
663    ValidatorRemoval,
664}
665
666/// Information about a validator in the pool
667///
668/// NOTE: ORDER IS VERY IMPORTANT HERE, PLEASE DO NOT RE-ORDER THE FIELDS UNLESS
669/// THERE'S AN EXTREMELY GOOD REASON.
670///
671/// To save on BPF instructions, the serialized bytes are reinterpreted with a
672/// `bytemuck` transmute, which means that this structure cannot have any
673/// undeclared alignment-padding in its representation.
674#[repr(C)]
675#[derive(
676    Clone,
677    Copy,
678    Debug,
679    Default,
680    PartialEq,
681    Pod,
682    Zeroable,
683    BorshDeserialize,
684    BorshSerialize,
685    BorshSchema,
686)]
687pub struct ValidatorStakeInfo {
688    /// Amount of lamports on the validator stake account, including rent
689    ///
690    /// Note that if `last_update_epoch` does not match the current epoch then
691    /// this field may not be accurate
692    pub active_stake_lamports: PodU64,
693
694    /// Amount of transient stake delegated to this validator
695    ///
696    /// Note that if `last_update_epoch` does not match the current epoch then
697    /// this field may not be accurate
698    pub transient_stake_lamports: PodU64,
699
700    /// Last epoch the active and transient stake lamports fields were updated
701    pub last_update_epoch: PodU64,
702
703    /// Transient account seed suffix, used to derive the transient stake
704    /// account address
705    pub transient_seed_suffix: PodU64,
706
707    /// Unused space, initially meant to specify the end of seed suffixes
708    pub unused: PodU32,
709
710    /// Validator account seed suffix
711    pub validator_seed_suffix: PodU32, // really `Option<NonZeroU32>` so 0 is `None`
712
713    /// Status of the validator stake account
714    pub status: PodStakeStatus,
715
716    /// Validator vote account address
717    pub vote_account_address: Pubkey,
718}
719
720impl ValidatorStakeInfo {
721    /// Get the total lamports on this validator (active and transient)
722    pub fn stake_lamports(&self) -> Result<u64, StakePoolError> {
723        u64::from(self.active_stake_lamports)
724            .checked_add(self.transient_stake_lamports.into())
725            .ok_or(StakePoolError::CalculationFailure)
726    }
727
728    /// Performs a very cheap comparison, for checking if this validator stake
729    /// info matches the vote account address
730    pub fn memcmp_pubkey(data: &[u8], vote_address: &Pubkey) -> bool {
731        unsafe {
732            sol_memcmp(
733                &data[41..41_usize.saturating_add(PUBKEY_BYTES)],
734                vote_address.as_ref(),
735                PUBKEY_BYTES,
736            ) == 0
737        }
738    }
739
740    /// Performs a comparison, used to check if this validator stake
741    /// info has more active lamports than some limit
742    pub fn active_lamports_greater_than(data: &[u8], lamports: &u64) -> bool {
743        // without this unwrap, compute usage goes up significantly
744        u64::try_from_slice(&data[0..8]).unwrap() > *lamports
745    }
746
747    /// Performs a comparison, used to check if this validator stake
748    /// info has more transient lamports than some limit
749    pub fn transient_lamports_greater_than(data: &[u8], lamports: &u64) -> bool {
750        // without this unwrap, compute usage goes up significantly
751        u64::try_from_slice(&data[8..16]).unwrap() > *lamports
752    }
753
754    /// Check that the validator stake info is totally removed
755    pub fn is_removed(data: &[u8]) -> bool {
756        FromPrimitive::from_u8(data[40]) == Some(StakeStatus::ReadyForRemoval)
757            && data[0..16] == [0; 16] // active and transient stake lamports are 0
758    }
759
760    /// Check that the validator stake info is active
761    pub fn is_active(data: &[u8]) -> bool {
762        FromPrimitive::from_u8(data[40]) == Some(StakeStatus::Active)
763    }
764}
765
766impl Sealed for ValidatorStakeInfo {}
767
768impl Pack for ValidatorStakeInfo {
769    const LEN: usize = 73;
770    fn pack_into_slice(&self, data: &mut [u8]) {
771        // Removing this unwrap would require changing from `Pack` to some other
772        // trait or `bytemuck`, so it stays in for now
773        borsh::to_writer(data, self).unwrap();
774    }
775    fn unpack_from_slice(src: &[u8]) -> Result<Self, ProgramError> {
776        let unpacked = Self::try_from_slice(src)?;
777        Ok(unpacked)
778    }
779}
780
781impl ValidatorList {
782    /// Create an empty instance containing space for `max_validators` and
783    /// preferred validator keys
784    pub fn new(max_validators: u32) -> Self {
785        Self {
786            header: ValidatorListHeader {
787                account_type: AccountType::ValidatorList,
788                max_validators,
789            },
790            validators: vec![ValidatorStakeInfo::default(); max_validators as usize],
791        }
792    }
793
794    /// Calculate the number of validator entries that fit in the provided
795    /// length
796    pub fn calculate_max_validators(buffer_length: usize) -> usize {
797        let header_size = ValidatorListHeader::LEN.saturating_add(4);
798        buffer_length
799            .saturating_sub(header_size)
800            .saturating_div(ValidatorStakeInfo::LEN)
801    }
802
803    /// Check if contains validator with particular pubkey
804    pub fn contains(&self, vote_account_address: &Pubkey) -> bool {
805        self.validators
806            .iter()
807            .any(|x| x.vote_account_address == *vote_account_address)
808    }
809
810    /// Check if contains validator with particular pubkey
811    pub fn find_mut(&mut self, vote_account_address: &Pubkey) -> Option<&mut ValidatorStakeInfo> {
812        self.validators
813            .iter_mut()
814            .find(|x| x.vote_account_address == *vote_account_address)
815    }
816    /// Check if contains validator with particular pubkey
817    pub fn find(&self, vote_account_address: &Pubkey) -> Option<&ValidatorStakeInfo> {
818        self.validators
819            .iter()
820            .find(|x| x.vote_account_address == *vote_account_address)
821    }
822
823    /// Check if the list has any active stake
824    pub fn has_active_stake(&self) -> bool {
825        self.validators
826            .iter()
827            .any(|x| u64::from(x.active_stake_lamports) > 0)
828    }
829}
830
831impl ValidatorListHeader {
832    const LEN: usize = 1 + 4;
833
834    /// Check if validator stake list is actually initialized as a validator
835    /// stake list
836    pub fn is_valid(&self) -> bool {
837        self.account_type == AccountType::ValidatorList
838    }
839
840    /// Check if the validator stake list is uninitialized
841    pub fn is_uninitialized(&self) -> bool {
842        self.account_type == AccountType::Uninitialized
843    }
844
845    /// Extracts a slice of `ValidatorStakeInfo` types from the vec part
846    /// of the `ValidatorList`
847    pub fn deserialize_mut_slice<'a>(
848        big_vec: &'a mut BigVec,
849        skip: usize,
850        len: usize,
851    ) -> Result<&'a mut [ValidatorStakeInfo], ProgramError> {
852        big_vec.deserialize_mut_slice::<ValidatorStakeInfo>(skip, len)
853    }
854
855    /// Extracts the validator list into its header and internal `BigVec`
856    pub fn deserialize_vec(data: &mut [u8]) -> Result<(Self, BigVec<'_>), ProgramError> {
857        let mut data_mut = data.borrow();
858        let header = ValidatorListHeader::deserialize(&mut data_mut)?;
859        let length = get_instance_packed_len(&header)?;
860
861        let big_vec = BigVec {
862            data: &mut data[length..],
863        };
864        Ok((header, big_vec))
865    }
866}
867
868/// Wrapper type that "counts down" epochs, which is Borsh-compatible with the
869/// native `Option`
870#[repr(C)]
871#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
872pub enum FutureEpoch<T> {
873    /// Nothing is set
874    #[default]
875    None,
876    /// Value is ready after the next epoch boundary
877    One(T),
878    /// Value is ready after two epoch boundaries
879    Two(T),
880}
881impl<T> FutureEpoch<T> {
882    /// Create a new value to be unlocked in a two epochs
883    pub fn new(value: T) -> Self {
884        Self::Two(value)
885    }
886}
887impl<T: Clone> FutureEpoch<T> {
888    /// Update the epoch, to be done after `get`ting the underlying value
889    pub fn update_epoch(&mut self) {
890        match self {
891            Self::None => {}
892            Self::One(_) => {
893                // The value has waited its last epoch
894                *self = Self::None;
895            }
896            // The value still has to wait one more epoch after this
897            Self::Two(v) => {
898                *self = Self::One(v.clone());
899            }
900        }
901    }
902
903    /// Get the value if it's ready, which is only at `One` epoch remaining
904    pub fn get(&self) -> Option<&T> {
905        match self {
906            Self::None | Self::Two(_) => None,
907            Self::One(v) => Some(v),
908        }
909    }
910}
911impl<T> From<FutureEpoch<T>> for Option<T> {
912    fn from(v: FutureEpoch<T>) -> Option<T> {
913        match v {
914            FutureEpoch::None => None,
915            FutureEpoch::One(inner) | FutureEpoch::Two(inner) => Some(inner),
916        }
917    }
918}
919
920/// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of
921/// the rewards
922/// If either the numerator or the denominator is 0, the fee is considered to be
923/// 0
924#[repr(C)]
925#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
926pub struct Fee {
927    /// denominator of the fee ratio
928    pub denominator: u64,
929    /// numerator of the fee ratio
930    pub numerator: u64,
931}
932
933impl Fee {
934    /// Applies the Fee's rates to a given amount, `amt`
935    /// returning the amount to be subtracted from it as fees
936    /// (0 if denominator is 0 or amt is 0),
937    /// or None if overflow occurs
938    #[inline]
939    pub fn apply(&self, amt: u64) -> Option<u128> {
940        if self.denominator == 0 {
941            return Some(0);
942        }
943        let numerator = (amt as u128).checked_mul(self.numerator as u128)?;
944        // ceiling the calculation by adding (denominator - 1) to the numerator
945        let denominator = self.denominator as u128;
946        numerator
947            .checked_add(denominator)?
948            .checked_sub(1)?
949            .checked_div(denominator)
950    }
951
952    /// Withdrawal fees have some additional restrictions, this function checks
953    /// if those are met, returning an error if not.
954    pub fn check_withdrawal(&self, old_withdrawal_fee: &Fee) -> Result<(), StakePoolError> {
955        // If the previous withdrawal fee was 0, we allow the fee to be set to
956        // WITHDRAWAL_BASELINE_FEE * MAX_WITHDRAWAL_FEE_INCREASE_FACTOR
957        let (old_num, old_denom) =
958            if old_withdrawal_fee.denominator == 0 || old_withdrawal_fee.numerator == 0 {
959                (
960                    WITHDRAWAL_BASELINE_FEE.numerator,
961                    WITHDRAWAL_BASELINE_FEE.denominator,
962                )
963            } else {
964                (old_withdrawal_fee.numerator, old_withdrawal_fee.denominator)
965            };
966
967        // Check that new_fee - old_fee <= MAX_WITHDRAWAL_FEE_INCREASE
968        // Program fails if provided numerator or denominator is too large, resulting in
969        // overflow
970        if (old_denom as u128)
971            .checked_mul(self.denominator as u128)
972            .and_then(|x| x.checked_mul(MAX_WITHDRAWAL_FEE_INCREASE.numerator as u128))
973            .ok_or(StakePoolError::CalculationFailure)?
974            < ((self.numerator as u128)
975                .checked_mul(old_denom as u128)
976                .ok_or(StakePoolError::CalculationFailure)?
977                .saturating_sub(
978                    (self.denominator as u128)
979                        .checked_mul(old_num as u128)
980                        .ok_or(StakePoolError::CalculationFailure)?,
981                )
982                .checked_mul(MAX_WITHDRAWAL_FEE_INCREASE.denominator as u128)
983                .ok_or(StakePoolError::CalculationFailure)?)
984        {
985            msg!(
986                "Fee increase exceeds maximum allowed, need to increase by {} / {}",
987                MAX_WITHDRAWAL_FEE_INCREASE.numerator,
988                MAX_WITHDRAWAL_FEE_INCREASE.denominator,
989            );
990            return Err(StakePoolError::FeeIncreaseTooHigh);
991        }
992
993        // Check that new_fee / old_fee <= MAX_WITHDRAWAL_FEE_INCREASE_FACTOR
994        // Program fails if provided numerator or denominator is too large, resulting in
995        // overflow
996        if (old_num as u128)
997            .checked_mul(self.denominator as u128)
998            .and_then(|x| x.checked_mul(MAX_WITHDRAWAL_FEE_INCREASE_FACTOR.numerator as u128))
999            .ok_or(StakePoolError::CalculationFailure)?
1000            < (self.numerator as u128)
1001                .checked_mul(old_denom as u128)
1002                .and_then(|x| x.checked_mul(MAX_WITHDRAWAL_FEE_INCREASE_FACTOR.denominator as u128))
1003                .ok_or(StakePoolError::CalculationFailure)?
1004        {
1005            msg!(
1006                "Fee increase exceeds maximum allowed, proposed increase factor ({} / {})",
1007                self.numerator.saturating_mul(old_denom),
1008                old_num.saturating_mul(self.denominator),
1009            );
1010            return Err(StakePoolError::FeeIncreaseTooHigh);
1011        }
1012        Ok(())
1013    }
1014}
1015
1016impl fmt::Display for Fee {
1017    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
1018        if self.numerator > 0 && self.denominator > 0 {
1019            write!(f, "{}/{}", self.numerator, self.denominator)
1020        } else {
1021            write!(f, "none")
1022        }
1023    }
1024}
1025
1026/// The type of fees that can be set on the stake pool
1027#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
1028pub enum FeeType {
1029    /// Referral fees for SOL deposits
1030    SolReferral(u8),
1031    /// Referral fees for stake deposits
1032    StakeReferral(u8),
1033    /// Management fee paid per epoch
1034    Epoch(Fee),
1035    /// Stake withdrawal fee
1036    StakeWithdrawal(Fee),
1037    /// Deposit fee for SOL deposits
1038    SolDeposit(Fee),
1039    /// Deposit fee for stake deposits
1040    StakeDeposit(Fee),
1041    /// SOL withdrawal fee
1042    SolWithdrawal(Fee),
1043}
1044
1045impl FeeType {
1046    /// Checks if the provided fee is too high, returning an error if so
1047    pub fn check_too_high(&self) -> Result<(), StakePoolError> {
1048        let too_high = match self {
1049            Self::SolReferral(pct) => *pct > 100u8,
1050            Self::StakeReferral(pct) => *pct > 100u8,
1051            Self::Epoch(fee) => fee.numerator > fee.denominator,
1052            Self::StakeWithdrawal(fee) => fee.numerator > fee.denominator,
1053            Self::SolWithdrawal(fee) => fee.numerator > fee.denominator,
1054            Self::SolDeposit(fee) => fee.numerator > fee.denominator,
1055            Self::StakeDeposit(fee) => fee.numerator > fee.denominator,
1056        };
1057        if too_high {
1058            msg!("Fee greater than 100%: {:?}", self);
1059            return Err(StakePoolError::FeeTooHigh);
1060        }
1061        Ok(())
1062    }
1063
1064    /// Returns if the contained fee can only be updated earliest on the next
1065    /// epoch
1066    #[inline]
1067    pub fn can_only_change_next_epoch(&self) -> bool {
1068        matches!(
1069            self,
1070            Self::StakeWithdrawal(_) | Self::SolWithdrawal(_) | Self::Epoch(_)
1071        )
1072    }
1073}
1074
1075#[cfg(test)]
1076mod test {
1077    #![allow(clippy::arithmetic_side_effects)]
1078    use {
1079        super::*,
1080        proptest::prelude::*,
1081        solana_borsh::v1::{get_packed_len, try_from_slice_unchecked},
1082        solana_clock::{DEFAULT_SLOTS_PER_EPOCH, DEFAULT_S_PER_SLOT, SECONDS_PER_DAY},
1083        solana_native_token::LAMPORTS_PER_SOL,
1084    };
1085
1086    fn uninitialized_validator_list() -> ValidatorList {
1087        ValidatorList {
1088            header: ValidatorListHeader {
1089                account_type: AccountType::Uninitialized,
1090                max_validators: 0,
1091            },
1092            validators: vec![],
1093        }
1094    }
1095
1096    fn test_validator_list(max_validators: u32) -> ValidatorList {
1097        ValidatorList {
1098            header: ValidatorListHeader {
1099                account_type: AccountType::ValidatorList,
1100                max_validators,
1101            },
1102            validators: vec![
1103                ValidatorStakeInfo {
1104                    status: StakeStatus::Active.into(),
1105                    vote_account_address: Pubkey::new_from_array([1; 32]),
1106                    active_stake_lamports: u64::from_le_bytes([255; 8]).into(),
1107                    transient_stake_lamports: u64::from_le_bytes([128; 8]).into(),
1108                    last_update_epoch: u64::from_le_bytes([64; 8]).into(),
1109                    transient_seed_suffix: 0.into(),
1110                    unused: 0.into(),
1111                    validator_seed_suffix: 0.into(),
1112                },
1113                ValidatorStakeInfo {
1114                    status: StakeStatus::DeactivatingTransient.into(),
1115                    vote_account_address: Pubkey::new_from_array([2; 32]),
1116                    active_stake_lamports: 998877665544.into(),
1117                    transient_stake_lamports: 222222222.into(),
1118                    last_update_epoch: 11223445566.into(),
1119                    transient_seed_suffix: 0.into(),
1120                    unused: 0.into(),
1121                    validator_seed_suffix: 0.into(),
1122                },
1123                ValidatorStakeInfo {
1124                    status: StakeStatus::ReadyForRemoval.into(),
1125                    vote_account_address: Pubkey::new_from_array([3; 32]),
1126                    active_stake_lamports: 0.into(),
1127                    transient_stake_lamports: 0.into(),
1128                    last_update_epoch: 999999999999999.into(),
1129                    transient_seed_suffix: 0.into(),
1130                    unused: 0.into(),
1131                    validator_seed_suffix: 0.into(),
1132                },
1133            ],
1134        }
1135    }
1136
1137    #[test]
1138    fn state_packing() {
1139        let max_validators = 10_000;
1140        let size = get_instance_packed_len(&ValidatorList::new(max_validators)).unwrap();
1141        let stake_list = uninitialized_validator_list();
1142        let mut byte_vec = vec![0u8; size];
1143        let bytes = byte_vec.as_mut_slice();
1144        borsh::to_writer(bytes, &stake_list).unwrap();
1145        let stake_list_unpacked = try_from_slice_unchecked::<ValidatorList>(&byte_vec).unwrap();
1146        assert_eq!(stake_list_unpacked, stake_list);
1147
1148        // Empty, one preferred key
1149        let stake_list = ValidatorList {
1150            header: ValidatorListHeader {
1151                account_type: AccountType::ValidatorList,
1152                max_validators: 0,
1153            },
1154            validators: vec![],
1155        };
1156        let mut byte_vec = vec![0u8; size];
1157        let bytes = byte_vec.as_mut_slice();
1158        borsh::to_writer(bytes, &stake_list).unwrap();
1159        let stake_list_unpacked = try_from_slice_unchecked::<ValidatorList>(&byte_vec).unwrap();
1160        assert_eq!(stake_list_unpacked, stake_list);
1161
1162        // With several accounts
1163        let stake_list = test_validator_list(max_validators);
1164        let mut byte_vec = vec![0u8; size];
1165        let bytes = byte_vec.as_mut_slice();
1166        borsh::to_writer(bytes, &stake_list).unwrap();
1167        let stake_list_unpacked = try_from_slice_unchecked::<ValidatorList>(&byte_vec).unwrap();
1168        assert_eq!(stake_list_unpacked, stake_list);
1169    }
1170
1171    #[test]
1172    fn validator_list_active_stake() {
1173        let max_validators = 10_000;
1174        let mut validator_list = test_validator_list(max_validators);
1175        assert!(validator_list.has_active_stake());
1176        for validator in validator_list.validators.iter_mut() {
1177            validator.active_stake_lamports = 0.into();
1178        }
1179        assert!(!validator_list.has_active_stake());
1180    }
1181
1182    #[test]
1183    fn validator_list_deserialize_mut_slice() {
1184        let max_validators = 10;
1185        let stake_list = test_validator_list(max_validators);
1186        let mut serialized = borsh::to_vec(&stake_list).unwrap();
1187        let (header, mut big_vec) = ValidatorListHeader::deserialize_vec(&mut serialized).unwrap();
1188        let list = ValidatorListHeader::deserialize_mut_slice(
1189            &mut big_vec,
1190            0,
1191            stake_list.validators.len(),
1192        )
1193        .unwrap();
1194        assert_eq!(header.account_type, AccountType::ValidatorList);
1195        assert_eq!(header.max_validators, max_validators);
1196        assert!(list
1197            .iter()
1198            .zip(stake_list.validators.iter())
1199            .all(|(a, b)| a == b));
1200
1201        let list = ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 1, 2).unwrap();
1202        assert!(list
1203            .iter()
1204            .zip(stake_list.validators[1..].iter())
1205            .all(|(a, b)| a == b));
1206        let list = ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 2, 1).unwrap();
1207        assert!(list
1208            .iter()
1209            .zip(stake_list.validators[2..].iter())
1210            .all(|(a, b)| a == b));
1211        let list = ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 0, 2).unwrap();
1212        assert!(list
1213            .iter()
1214            .zip(stake_list.validators[..2].iter())
1215            .all(|(a, b)| a == b));
1216
1217        assert_eq!(
1218            ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 0, 4).unwrap_err(),
1219            ProgramError::AccountDataTooSmall
1220        );
1221        assert_eq!(
1222            ValidatorListHeader::deserialize_mut_slice(&mut big_vec, 1, 3).unwrap_err(),
1223            ProgramError::AccountDataTooSmall
1224        );
1225    }
1226
1227    #[test]
1228    fn validator_list_iter() {
1229        let max_validators = 10;
1230        let stake_list = test_validator_list(max_validators);
1231        let mut serialized = borsh::to_vec(&stake_list).unwrap();
1232        let (_, big_vec) = ValidatorListHeader::deserialize_vec(&mut serialized).unwrap();
1233        for (a, b) in big_vec
1234            .deserialize_slice::<ValidatorStakeInfo>(0, big_vec.len() as usize)
1235            .unwrap()
1236            .iter()
1237            .zip(stake_list.validators.iter())
1238        {
1239            assert_eq!(a, b);
1240        }
1241    }
1242
1243    proptest! {
1244        #[test]
1245        fn stake_list_size_calculation(test_amount in 0..=100_000_u32) {
1246            let validators = ValidatorList::new(test_amount);
1247            let size = get_instance_packed_len(&validators).unwrap();
1248            assert_eq!(ValidatorList::calculate_max_validators(size), test_amount as usize);
1249            assert_eq!(ValidatorList::calculate_max_validators(size.saturating_add(1)), test_amount as usize);
1250            assert_eq!(ValidatorList::calculate_max_validators(size.saturating_add(get_packed_len::<ValidatorStakeInfo>())), (test_amount + 1)as usize);
1251            assert_eq!(ValidatorList::calculate_max_validators(size.saturating_sub(1)), (test_amount.saturating_sub(1)) as usize);
1252        }
1253    }
1254
1255    prop_compose! {
1256        fn fee()(denominator in 1..=u16::MAX)(
1257            denominator in Just(denominator),
1258            numerator in 0..=denominator,
1259        ) -> (u64, u64) {
1260            (numerator as u64, denominator as u64)
1261        }
1262    }
1263
1264    prop_compose! {
1265        fn total_stake_and_rewards()(total_lamports in 1..u64::MAX)(
1266            total_lamports in Just(total_lamports),
1267            rewards in 0..=total_lamports,
1268        ) -> (u64, u64) {
1269            (total_lamports - rewards, rewards)
1270        }
1271    }
1272
1273    #[test]
1274    fn specific_fee_calculation() {
1275        // 10% of 10 SOL in rewards should be 1 SOL in fees
1276        let epoch_fee = Fee {
1277            numerator: 1,
1278            denominator: 10,
1279        };
1280        let mut stake_pool = StakePool {
1281            total_lamports: 100 * LAMPORTS_PER_SOL,
1282            pool_token_supply: 100 * LAMPORTS_PER_SOL,
1283            epoch_fee,
1284            ..StakePool::default()
1285        };
1286        let reward_lamports = 10 * LAMPORTS_PER_SOL;
1287        let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
1288
1289        stake_pool.total_lamports += reward_lamports;
1290        stake_pool.pool_token_supply += pool_token_fee;
1291
1292        let fee_lamports = stake_pool
1293            .calc_lamports_withdraw_amount(pool_token_fee)
1294            .unwrap();
1295        assert_eq!(fee_lamports, LAMPORTS_PER_SOL - 1); // off-by-one due to
1296                                                        // truncation
1297    }
1298
1299    #[test]
1300    fn zero_withdraw_calculation() {
1301        let epoch_fee = Fee {
1302            numerator: 0,
1303            denominator: 1,
1304        };
1305        let stake_pool = StakePool {
1306            epoch_fee,
1307            ..StakePool::default()
1308        };
1309        let fee_lamports = stake_pool.calc_lamports_withdraw_amount(0).unwrap();
1310        assert_eq!(fee_lamports, 0);
1311    }
1312
1313    #[test]
1314    fn divide_by_zero_fee() {
1315        let stake_pool = StakePool {
1316            total_lamports: 0,
1317            epoch_fee: Fee {
1318                numerator: 1,
1319                denominator: 10,
1320            },
1321            ..StakePool::default()
1322        };
1323        let rewards = 10;
1324        let fee = stake_pool.calc_epoch_fee_amount(rewards).unwrap();
1325        assert_eq!(fee, rewards);
1326    }
1327
1328    #[test]
1329    fn approximate_apr_calculation() {
1330        // 8% / year means roughly .044% / epoch
1331        let stake_pool = StakePool {
1332            last_epoch_total_lamports: 100_000,
1333            last_epoch_pool_token_supply: 100_000,
1334            total_lamports: 100_044,
1335            pool_token_supply: 100_000,
1336            ..StakePool::default()
1337        };
1338        let pool_token_value =
1339            stake_pool.total_lamports as f64 / stake_pool.pool_token_supply as f64;
1340        let last_epoch_pool_token_value = stake_pool.last_epoch_total_lamports as f64
1341            / stake_pool.last_epoch_pool_token_supply as f64;
1342        let epoch_rate = pool_token_value / last_epoch_pool_token_value - 1.0;
1343        const SECONDS_PER_EPOCH: f64 = DEFAULT_SLOTS_PER_EPOCH as f64 * DEFAULT_S_PER_SLOT;
1344        const EPOCHS_PER_YEAR: f64 = SECONDS_PER_DAY as f64 * 365.25 / SECONDS_PER_EPOCH;
1345        const EPSILON: f64 = 0.00001;
1346        let yearly_rate = epoch_rate * EPOCHS_PER_YEAR;
1347        assert!((yearly_rate - 0.080355).abs() < EPSILON);
1348    }
1349
1350    proptest! {
1351        #[test]
1352        fn fee_calculation(
1353            (numerator, denominator) in fee(),
1354            (total_lamports, reward_lamports) in total_stake_and_rewards(),
1355        ) {
1356            let epoch_fee = Fee { denominator, numerator };
1357            let mut stake_pool = StakePool {
1358                total_lamports,
1359                pool_token_supply: total_lamports,
1360                epoch_fee,
1361                ..StakePool::default()
1362            };
1363            let pool_token_fee = stake_pool.calc_epoch_fee_amount(reward_lamports).unwrap();
1364
1365            stake_pool.total_lamports += reward_lamports;
1366            stake_pool.pool_token_supply += pool_token_fee;
1367
1368            let fee_lamports = stake_pool.calc_lamports_withdraw_amount(pool_token_fee).unwrap();
1369            let max_fee_lamports = u64::try_from((reward_lamports as u128) * (epoch_fee.numerator as u128) / (epoch_fee.denominator as u128)).unwrap();
1370            assert!(max_fee_lamports >= fee_lamports,
1371                "Max possible fee must always be greater than or equal to what is actually withdrawn, max {} actual {}",
1372                max_fee_lamports,
1373                fee_lamports);
1374
1375            // since we do two "flooring" conversions, the max epsilon should be
1376            // correct up to 2 lamports (one for each floor division), plus a
1377            // correction for huge discrepancies between rewards and total stake
1378            let epsilon = 2 + reward_lamports / total_lamports;
1379            assert!(max_fee_lamports - fee_lamports <= epsilon,
1380                "Max expected fee in lamports {}, actually receive {}, epsilon {}",
1381                max_fee_lamports, fee_lamports, epsilon);
1382        }
1383    }
1384
1385    prop_compose! {
1386        fn total_tokens_and_deposit()(total_lamports in 1..u64::MAX)(
1387            total_lamports in Just(total_lamports),
1388            pool_token_supply in 1..=total_lamports,
1389            deposit_lamports in 1..total_lamports,
1390        ) -> (u64, u64, u64) {
1391            (total_lamports - deposit_lamports, pool_token_supply.saturating_sub(deposit_lamports).max(1), deposit_lamports)
1392        }
1393    }
1394
1395    proptest! {
1396        #[test]
1397        fn deposit_and_withdraw(
1398            (total_lamports, pool_token_supply, deposit_stake) in total_tokens_and_deposit()
1399        ) {
1400            let mut stake_pool = StakePool {
1401                total_lamports,
1402                pool_token_supply,
1403                ..StakePool::default()
1404            };
1405            let deposit_result = stake_pool.calc_pool_tokens_for_deposit(deposit_stake).unwrap();
1406            prop_assume!(deposit_result > 0);
1407            stake_pool.total_lamports += deposit_stake;
1408            stake_pool.pool_token_supply += deposit_result;
1409            let withdraw_result = stake_pool.calc_lamports_withdraw_amount(deposit_result).unwrap();
1410            assert!(withdraw_result <= deposit_stake);
1411
1412            // also test splitting the withdrawal in two operations
1413            if deposit_result >= 2 {
1414                let first_half_deposit = deposit_result / 2;
1415                let first_withdraw_result = stake_pool.calc_lamports_withdraw_amount(first_half_deposit).unwrap();
1416                stake_pool.total_lamports -= first_withdraw_result;
1417                stake_pool.pool_token_supply -= first_half_deposit;
1418                let second_half_deposit = deposit_result - first_half_deposit; // do the whole thing
1419                let second_withdraw_result = stake_pool.calc_lamports_withdraw_amount(second_half_deposit).unwrap();
1420                assert!(first_withdraw_result + second_withdraw_result <= deposit_stake);
1421            }
1422        }
1423    }
1424
1425    #[test]
1426    fn specific_split_withdrawal() {
1427        let total_lamports = 1_100_000_000_000;
1428        let pool_token_supply = 1_000_000_000_000;
1429        let deposit_stake = 3;
1430        let mut stake_pool = StakePool {
1431            total_lamports,
1432            pool_token_supply,
1433            ..StakePool::default()
1434        };
1435        let deposit_result = stake_pool
1436            .calc_pool_tokens_for_deposit(deposit_stake)
1437            .unwrap();
1438        assert!(deposit_result > 0);
1439        stake_pool.total_lamports += deposit_stake;
1440        stake_pool.pool_token_supply += deposit_result;
1441        let withdraw_result = stake_pool
1442            .calc_lamports_withdraw_amount(deposit_result / 2)
1443            .unwrap();
1444        assert!(withdraw_result * 2 <= deposit_stake);
1445    }
1446
1447    #[test]
1448    fn withdraw_all() {
1449        let total_lamports = 1_100_000_000_000;
1450        let pool_token_supply = 1_000_000_000_000;
1451        let mut stake_pool = StakePool {
1452            total_lamports,
1453            pool_token_supply,
1454            ..StakePool::default()
1455        };
1456        // take everything out at once
1457        let withdraw_result = stake_pool
1458            .calc_lamports_withdraw_amount(pool_token_supply)
1459            .unwrap();
1460        assert_eq!(stake_pool.total_lamports, withdraw_result);
1461
1462        // take out 1, then the rest
1463        let withdraw_result = stake_pool.calc_lamports_withdraw_amount(1).unwrap();
1464        stake_pool.total_lamports -= withdraw_result;
1465        stake_pool.pool_token_supply -= 1;
1466        let withdraw_result = stake_pool
1467            .calc_lamports_withdraw_amount(stake_pool.pool_token_supply)
1468            .unwrap();
1469        assert_eq!(stake_pool.total_lamports, withdraw_result);
1470
1471        // take out all except 1, then the rest
1472        let mut stake_pool = StakePool {
1473            total_lamports,
1474            pool_token_supply,
1475            ..StakePool::default()
1476        };
1477        let withdraw_result = stake_pool
1478            .calc_lamports_withdraw_amount(pool_token_supply - 1)
1479            .unwrap();
1480        stake_pool.total_lamports -= withdraw_result;
1481        stake_pool.pool_token_supply = 1;
1482        assert_ne!(stake_pool.total_lamports, 0);
1483
1484        let withdraw_result = stake_pool.calc_lamports_withdraw_amount(1).unwrap();
1485        assert_eq!(stake_pool.total_lamports, withdraw_result);
1486    }
1487
1488    #[test]
1489    fn check_withdrawal_fee_overflow_failure() {
1490        let old_fee = Fee {
1491            numerator: 1,
1492            denominator: u64::MAX,
1493        };
1494        let new_fee = Fee {
1495            numerator: u64::MAX,
1496            denominator: u64::MAX,
1497        };
1498        assert_eq!(
1499            StakePoolError::CalculationFailure,
1500            new_fee.check_withdrawal(&old_fee).unwrap_err()
1501        );
1502    }
1503
1504    #[test]
1505    fn check_withdrawal_fee_max_increase() {
1506        let old_fee = Fee {
1507            numerator: 10,
1508            denominator: 200,
1509        };
1510        let new_fee = Fee {
1511            numerator: 11_000_000_000_000_001,
1512            denominator: 200_000_000_000_000_000,
1513        };
1514        assert_eq!(
1515            StakePoolError::FeeIncreaseTooHigh,
1516            new_fee.check_withdrawal(&old_fee).unwrap_err()
1517        );
1518    }
1519}