diff --git a/stake-pool/js/src/index.ts b/stake-pool/js/src/index.ts index 4e932033aba..a65274efb4c 100644 --- a/stake-pool/js/src/index.ts +++ b/stake-pool/js/src/index.ts @@ -795,6 +795,7 @@ export async function decreaseValidatorStake( stakePool: stakePoolAddress, staker: stakePool.account.data.staker, validatorList: stakePool.account.data.validatorList, + reserveStake: stakePool.account.data.reserveStake, transientStakeSeed: transientStakeSeed.toNumber(), withdrawAuthority, validatorStake, diff --git a/stake-pool/js/src/instructions.ts b/stake-pool/js/src/instructions.ts index 2a8fc9afeeb..7c1d645ac8c 100644 --- a/stake-pool/js/src/instructions.ts +++ b/stake-pool/js/src/instructions.ts @@ -226,6 +226,7 @@ export type DecreaseValidatorStakeParams = { }; export interface DecreaseAdditionalValidatorStakeParams extends DecreaseValidatorStakeParams { + reserveStake: PublicKey; ephemeralStake: PublicKey; ephemeralStakeSeed: number; } @@ -612,6 +613,7 @@ export class StakePoolInstruction { staker, withdrawAuthority, validatorList, + reserveStake, validatorStake, transientStake, lamports, @@ -628,6 +630,7 @@ export class StakePoolInstruction { { pubkey: staker, isSigner: true, isWritable: false }, { pubkey: withdrawAuthority, isSigner: false, isWritable: false }, { pubkey: validatorList, isSigner: false, isWritable: true }, + { pubkey: reserveStake, isSigner: false, isWritable: true }, { pubkey: validatorStake, isSigner: false, isWritable: true }, { pubkey: ephemeralStake, isSigner: false, isWritable: true }, { pubkey: transientStake, isSigner: false, isWritable: true }, diff --git a/stake-pool/program/src/error.rs b/stake-pool/program/src/error.rs index 9433a2304be..83bcb1a0247 100644 --- a/stake-pool/program/src/error.rs +++ b/stake-pool/program/src/error.rs @@ -154,6 +154,9 @@ pub enum StakePoolError { /// the rent-exempt reserve for a stake account. #[error("ReserveDepleted")] ReserveDepleted, + /// Missing required sysvar account + #[error("Missing required sysvar account")] + MissingRequiredSysvar, } impl From for ProgramError { fn from(e: StakePoolError) -> Self { diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 6a0f6ad0816..6e9c57880b2 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -446,24 +446,28 @@ pub enum StakePoolInstruction { /// /// Works regardless if the transient stake account already exists. /// - /// Internally, this instruction splits a validator stake account into an - /// ephemeral stake account, deactivates it, then merges or splits it into - /// the transient stake account delegated to the appropriate validator. + /// Internally, this instruction: + /// * withdraws rent-exempt reserve lamports from the reserve into the ephemeral stake + /// * splits a validator stake account into an ephemeral stake account + /// * deactivates the ephemeral account + /// * merges or splits the ephemeral account into the transient stake account + /// delegated to the appropriate validator /// - /// The amount of lamports to move must be at least rent-exemption plus + /// The amount of lamports to move must be at least /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. /// /// 0. `[]` Stake pool /// 1. `[s]` Stake pool staker /// 2. `[]` Stake pool withdraw authority /// 3. `[w]` Validator list - /// 4. `[w]` Canonical stake account to split from - /// 5. `[w]` Uninitialized ephemeral stake account to receive stake - /// 6. `[w]` Transient stake account - /// 7. `[]` Clock sysvar - /// 8. '[]' Stake history sysvar - /// 9. `[]` System program - /// 10. `[]` Stake program + /// 4. `[w]` Reserve stake account, to fund rent exempt reserve + /// 5. `[w]` Canonical stake account to split from + /// 6. `[w]` Uninitialized ephemeral stake account to receive stake + /// 7. `[w]` Transient stake account + /// 8. `[]` Clock sysvar + /// 9. '[]' Stake history sysvar + /// 10. `[]` System program + /// 11. `[]` Stake program DecreaseAdditionalValidatorStake { /// amount of lamports to split into the transient stake account lamports: u64, @@ -793,6 +797,7 @@ pub fn decrease_additional_validator_stake( staker: &Pubkey, stake_pool_withdraw_authority: &Pubkey, validator_list: &Pubkey, + reserve_stake: &Pubkey, validator_stake: &Pubkey, ephemeral_stake: &Pubkey, transient_stake: &Pubkey, @@ -805,6 +810,7 @@ pub fn decrease_additional_validator_stake( AccountMeta::new_readonly(*staker, true), AccountMeta::new_readonly(*stake_pool_withdraw_authority, false), AccountMeta::new(*validator_list, false), + AccountMeta::new(*reserve_stake, false), AccountMeta::new(*validator_stake, false), AccountMeta::new(*ephemeral_stake, false), AccountMeta::new(*transient_stake, false), @@ -1211,6 +1217,7 @@ pub fn decrease_additional_validator_stake_with_vote( &stake_pool.staker, &pool_withdraw_authority, &stake_pool.validator_list, + &stake_pool.reserve_stake, &validator_stake_address, &ephemeral_stake_address, &transient_stake_address, diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 2024d674126..938fcf1799a 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -1241,12 +1241,16 @@ impl Processor { lamports: u64, transient_stake_seed: u64, maybe_ephemeral_stake_seed: Option, + fund_rent_exempt_reserve: bool, ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; let staker_info = next_account_info(account_info_iter)?; let withdraw_authority_info = next_account_info(account_info_iter)?; let validator_list_info = next_account_info(account_info_iter)?; + let maybe_reserve_stake_info = fund_rent_exempt_reserve + .then(|| next_account_info(account_info_iter)) + .transpose()?; let validator_stake_account_info = next_account_info(account_info_iter)?; let maybe_ephemeral_stake_account_info = maybe_ephemeral_stake_seed .map(|_| next_account_info(account_info_iter)) @@ -1298,6 +1302,10 @@ impl Processor { return Err(StakePoolError::InvalidState.into()); } + if let Some(reserve_stake_info) = maybe_reserve_stake_info { + stake_pool.check_reserve_stake(reserve_stake_info)?; + } + let (meta, stake) = get_stake_state(validator_stake_account_info)?; let vote_account_address = stake.delegation.voter_pubkey; @@ -1339,10 +1347,10 @@ impl Processor { } let stake_space = std::mem::size_of::(); - let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let stake_rent = rent.minimum_balance(stake_space); - let current_minimum_lamports = - stake_rent.saturating_add(minimum_delegation(stake_minimum_delegation)); + + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; + let current_minimum_lamports = minimum_delegation(stake_minimum_delegation); if lamports < current_minimum_lamports { msg!( "Need at least {} lamports for transient stake to meet minimum delegation and rent-exempt requirements, {} provided", @@ -1366,7 +1374,7 @@ impl Processor { return Err(ProgramError::InsufficientFunds); } - let source_stake_account_info = + let (source_stake_account_info, split_lamports) = if let Some((ephemeral_stake_seed, ephemeral_stake_account_info)) = maybe_ephemeral_stake_seed.zip(maybe_ephemeral_stake_account_info) { @@ -1388,6 +1396,30 @@ impl Processor { stake_space, )?; + // if needed, withdraw rent-exempt reserve for ephemeral account + if let Some(reserve_stake_info) = maybe_reserve_stake_info { + let required_lamports_for_rent_exemption = + stake_rent.saturating_sub(ephemeral_stake_account_info.lamports()); + if required_lamports_for_rent_exemption > 0 { + if required_lamports_for_rent_exemption >= reserve_stake_info.lamports() { + return Err(StakePoolError::ReserveDepleted.into()); + } + let stake_history_info = maybe_stake_history_info + .ok_or(StakePoolError::MissingRequiredSysvar)?; + Self::stake_withdraw( + stake_pool_info.key, + reserve_stake_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + ephemeral_stake_account_info.clone(), + clock_info.clone(), + stake_history_info.clone(), + required_lamports_for_rent_exemption, + )?; + } + } + // split into ephemeral stake account Self::stake_split( stake_pool_info.key, @@ -1408,11 +1440,14 @@ impl Processor { stake_pool.stake_withdraw_bump_seed, )?; - ephemeral_stake_account_info + ( + ephemeral_stake_account_info, + ephemeral_stake_account_info.lamports(), + ) } else { // if no ephemeral account is provided, split everything from the // validator stake account, into the transient stake account - validator_stake_account_info + (validator_stake_account_info, lamports) }; let transient_stake_bump_seed = check_transient_stake_address( @@ -1459,7 +1494,7 @@ impl Processor { withdraw_authority_info.clone(), AUTHORITY_WITHDRAW, stake_pool.stake_withdraw_bump_seed, - lamports, + split_lamports, transient_stake_account_info.clone(), )?; @@ -1482,9 +1517,11 @@ impl Processor { .checked_sub(lamports) .ok_or(StakePoolError::CalculationFailure)? .into(); + // `split_lamports` may be greater than `lamports` if the reserve stake + // funded the rent-exempt reserve validator_stake_info.transient_stake_lamports = u64::from(validator_stake_info.transient_stake_lamports) - .checked_add(lamports) + .checked_add(split_lamports) .ok_or(StakePoolError::CalculationFailure)? .into(); validator_stake_info.transient_seed_suffix = transient_stake_seed.into(); @@ -3811,6 +3848,7 @@ impl Processor { lamports, transient_stake_seed, None, + false, ) } StakePoolInstruction::DecreaseAdditionalValidatorStake { @@ -3825,6 +3863,7 @@ impl Processor { lamports, transient_stake_seed, Some(ephemeral_stake_seed), + true, ) } StakePoolInstruction::IncreaseValidatorStake { @@ -4038,6 +4077,7 @@ impl PrintProgramError for StakePoolError { StakePoolError::ExceededSlippage => msg!("Error: instruction exceeds desired slippage limit"), StakePoolError::IncorrectMintDecimals => msg!("Error: Provided mint does not have 9 decimals to match SOL"), StakePoolError::ReserveDepleted => msg!("Error: Pool reserve does not have enough lamports to fund rent-exempt reserve in split destination. Deposit more SOL in reserve, or pre-fund split destination with the rent-exempt reserve for a stake account."), + StakePoolError::MissingRequiredSysvar => msg!("Missing required sysvar account"), } } } diff --git a/stake-pool/program/tests/decrease.rs b/stake-pool/program/tests/decrease.rs index e20e752ed45..493e75ad544 100644 --- a/stake-pool/program/tests/decrease.rs +++ b/stake-pool/program/tests/decrease.rs @@ -25,6 +25,7 @@ async fn setup() -> ( ValidatorStakeAccount, DepositStakeAccount, u64, + u64, ) { let mut context = program_test().start_with_context().await; let rent = context.banks_client.get_rent().await.unwrap(); @@ -37,12 +38,13 @@ async fn setup() -> ( .await; let stake_pool_accounts = StakePoolAccounts::default(); + let reserve_lamports = MINIMUM_RESERVE_LAMPORTS + stake_rent + current_minimum_delegation; stake_pool_accounts .initialize_stake_pool( &mut context.banks_client, &context.payer, &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS + stake_rent + current_minimum_delegation, + reserve_lamports, ) .await .unwrap(); @@ -74,6 +76,7 @@ async fn setup() -> ( validator_stake_account, deposit_info, decrease_lamports, + reserve_lamports + stake_rent, ) } @@ -81,8 +84,14 @@ async fn setup() -> ( #[test_case(false; "no-additional")] #[tokio::test] async fn success(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = - setup().await; + let ( + mut context, + stake_pool_accounts, + validator_stake, + _deposit_info, + decrease_lamports, + reserve_lamports, + ) = setup().await; // Save validator stake let pre_validator_stake_account = @@ -128,6 +137,9 @@ async fn success(use_additional_instruction: bool) { ); // Check transient stake account state and balance + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + let transient_stake_account = get_account( &mut context.banks_client, &validator_stake.transient_stake_account, @@ -135,7 +147,23 @@ async fn success(use_additional_instruction: bool) { .await; let transient_stake_state = deserialize::(&transient_stake_account.data).unwrap(); - assert_eq!(transient_stake_account.lamports, decrease_lamports); + let transient_lamports = if use_additional_instruction { + decrease_lamports + stake_rent + } else { + decrease_lamports + }; + assert_eq!(transient_stake_account.lamports, transient_lamports); + let reserve_lamports = if use_additional_instruction { + reserve_lamports - stake_rent + } else { + reserve_lamports + }; + let reserve_stake_account = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await; + assert_eq!(reserve_stake_account.lamports, reserve_lamports); assert_ne!( transient_stake_state .delegation() @@ -147,7 +175,7 @@ async fn success(use_additional_instruction: bool) { #[tokio::test] async fn fail_with_wrong_withdraw_authority() { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, _) = setup().await; let wrong_authority = Pubkey::new_unique(); @@ -187,8 +215,14 @@ async fn fail_with_wrong_withdraw_authority() { #[tokio::test] async fn fail_with_wrong_validator_list() { - let (mut context, mut stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = - setup().await; + let ( + mut context, + mut stake_pool_accounts, + validator_stake, + _deposit_info, + decrease_lamports, + _, + ) = setup().await; stake_pool_accounts.validator_list = Keypair::new(); @@ -227,7 +261,7 @@ async fn fail_with_wrong_validator_list() { #[tokio::test] async fn fail_with_unknown_validator() { - let (mut context, stake_pool_accounts, _validator_stake, _deposit_info, decrease_lamports) = + let (mut context, stake_pool_accounts, _validator_stake, _deposit_info, decrease_lamports, _) = setup().await; let unknown_stake = create_unknown_validator_stake( @@ -276,7 +310,7 @@ async fn fail_with_unknown_validator() { #[test_case(false; "no-additional")] #[tokio::test] async fn fail_twice_diff_seed(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, _) = setup().await; let error = stake_pool_accounts @@ -337,12 +371,21 @@ async fn fail_twice_diff_seed(use_additional_instruction: bool) { #[test_case(false, false, false; "fail-no-additional")] #[tokio::test] async fn twice(success: bool, use_additional_first_time: bool, use_additional_second_time: bool) { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports) = - setup().await; + let ( + mut context, + stake_pool_accounts, + validator_stake, + _deposit_info, + decrease_lamports, + mut reserve_lamports, + ) = setup().await; let pre_stake_account = get_account(&mut context.banks_client, &validator_stake.stake_account).await; + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + let first_decrease = decrease_lamports / 3; let second_decrease = decrease_lamports / 2; let total_decrease = first_decrease + second_decrease; @@ -410,7 +453,14 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se .await; let transient_stake_state = deserialize::(&transient_stake_account.data).unwrap(); - assert_eq!(transient_stake_account.lamports, total_decrease); + let mut transient_lamports = total_decrease; + if use_additional_first_time { + transient_lamports += stake_rent; + } + if use_additional_second_time { + transient_lamports += stake_rent; + } + assert_eq!(transient_stake_account.lamports, transient_lamports); assert_ne!( transient_stake_state .delegation() @@ -424,7 +474,24 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se .get_validator_list(&mut context.banks_client) .await; let entry = validator_list.find(&validator_stake.vote.pubkey()).unwrap(); - assert_eq!(u64::from(entry.transient_stake_lamports), total_decrease); + assert_eq!( + u64::from(entry.transient_stake_lamports), + transient_lamports + ); + + // reserve deducted properly + if use_additional_first_time { + reserve_lamports -= stake_rent; + } + if use_additional_second_time { + reserve_lamports -= stake_rent; + } + let reserve_stake_account = get_account( + &mut context.banks_client, + &stake_pool_accounts.reserve_stake.pubkey(), + ) + .await; + assert_eq!(reserve_stake_account.lamports, reserve_lamports); } else { let error = error.unwrap().unwrap(); assert_eq!( @@ -441,7 +508,7 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se #[test_case(false; "no-additional")] #[tokio::test] async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, _deposit_info, _decrease_lamports) = + let (mut context, stake_pool_accounts, validator_stake, _deposit_info, _decrease_lamports, _) = setup().await; let rent = context.banks_client.get_rent().await.unwrap(); @@ -470,7 +537,7 @@ async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { #[tokio::test] async fn fail_big_overdraw() { - let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports) = + let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports, _) = setup().await; let error = stake_pool_accounts @@ -497,7 +564,7 @@ async fn fail_big_overdraw() { #[test_case(false; "no-additional")] #[tokio::test] async fn fail_overdraw(use_additional_instruction: bool) { - let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports) = + let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports, _) = setup().await; let rent = context.banks_client.get_rent().await.unwrap(); @@ -526,7 +593,8 @@ async fn fail_overdraw(use_additional_instruction: bool) { #[tokio::test] async fn fail_additional_with_increasing() { - let (mut context, stake_pool_accounts, validator_stake, _, decrease_lamports) = setup().await; + let (mut context, stake_pool_accounts, validator_stake, _, decrease_lamports, _) = + setup().await; let current_minimum_delegation = stake_pool_get_minimum_delegation( &mut context.banks_client, diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 0682e30ae54..4dccee90f07 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -1668,6 +1668,7 @@ impl StakePoolAccounts { &self.staker.pubkey(), &self.withdraw_authority, &self.validator_list.pubkey(), + &self.reserve_stake.pubkey(), validator_stake, ephemeral_stake, transient_stake,