diff --git a/Cargo.lock b/Cargo.lock index 7747a849498..526ce8c740b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6886,6 +6886,7 @@ name = "spl-stake-pool" version = "0.7.0" dependencies = [ "arrayref", + "assert_matches", "bincode", "borsh 0.10.3", "bytemuck", diff --git a/stake-pool/program/Cargo.toml b/stake-pool/program/Cargo.toml index f90b0675344..275ce9da105 100644 --- a/stake-pool/program/Cargo.toml +++ b/stake-pool/program/Cargo.toml @@ -28,6 +28,7 @@ thiserror = "1.0" bincode = "1.3.1" [dev-dependencies] +assert_matches = "1.5.0" proptest = "1.2" solana-program-test = "1.16.13" solana-sdk = "1.16.13" diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 46d11255f78..d7e35590137 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -117,6 +117,9 @@ pub enum StakePoolInstruction { /// 7. `[]` Stake program id, RemoveValidatorFromPool, + /// NOTE: This instruction has been deprecated since version 0.7.0. Please + /// use `DecreaseValidatorStakeWithReserve` instead. + /// /// (Staker only) Decrease active stake on a validator, eventually moving it to the reserve /// /// Internally, this instruction splits a validator stake account into its @@ -477,6 +480,40 @@ pub enum StakePoolInstruction { ephemeral_stake_seed: u64, }, + /// (Staker only) Decrease active stake on a validator, eventually moving it to the reserve + /// + /// Internally, this instruction: + /// * withdraws enough lamports to make the transient account rent-exempt + /// * splits from a validator stake account into a transient stake account + /// * deactivates the transient stake account + /// + /// In order to rebalance the pool without taking custody, the staker needs + /// a way of reducing the stake on a stake account. This instruction splits + /// some amount of stake, up to the total activated stake, from the canonical + /// validator stake account, into its "transient" stake account. + /// + /// The instruction only succeeds if the transient stake account does not + /// exist. The amount of lamports to move must be at least rent-exemption plus + /// `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]` Reserve stake account, to fund rent exempt reserve + /// 5. `[w]` Canonical stake account to split from + /// 6. `[w]` Transient stake account to receive split + /// 7. `[]` Clock sysvar + /// 8. '[]' Stake history sysvar + /// 9. `[]` System program + /// 10. `[]` Stake program + DecreaseValidatorStakeWithReserve { + /// amount of lamports to split into the transient stake account + lamports: u64, + /// seed used to create transient stake account + transient_stake_seed: u64, + }, + /// (Staker only) Redelegate active stake on a validator, eventually moving it to another /// /// Internally, this instruction splits a validator stake account into its @@ -755,6 +792,10 @@ pub fn remove_validator_from_pool( /// Creates `DecreaseValidatorStake` instruction (rebalance from validator account to /// transient account) +#[deprecated( + since = "0.7.0", + note = "please use `decrease_validator_stake_with_reserve`" +)] pub fn decrease_validator_stake( program_id: &Pubkey, stake_pool: &Pubkey, @@ -833,6 +874,45 @@ pub fn decrease_additional_validator_stake( } } +/// Creates `DecreaseValidatorStakeWithReserve` instruction (rebalance from +/// validator account to transient account) +pub fn decrease_validator_stake_with_reserve( + program_id: &Pubkey, + stake_pool: &Pubkey, + staker: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + validator_list: &Pubkey, + reserve_stake: &Pubkey, + validator_stake: &Pubkey, + transient_stake: &Pubkey, + lamports: u64, + transient_stake_seed: u64, +) -> Instruction { + let accounts = vec![ + AccountMeta::new_readonly(*stake_pool, false), + 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(*transient_stake, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(stake::program::id(), false), + ]; + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::DecreaseValidatorStakeWithReserve { + lamports, + transient_stake_seed, + } + .try_to_vec() + .unwrap(), + } +} + /// Creates `IncreaseValidatorStake` instruction (rebalance from reserve account to /// transient account) pub fn increase_validator_stake( @@ -1174,12 +1254,13 @@ pub fn decrease_validator_stake_with_vote( stake_pool_address, transient_stake_seed, ); - decrease_validator_stake( + decrease_validator_stake_with_reserve( program_id, stake_pool_address, &stake_pool.staker, &pool_withdraw_authority, &stake_pool.validator_list, + &stake_pool.reserve_stake, &validator_stake_address, &transient_stake_address, lamports, diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 17ed181a77a..aeb40520307 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -1258,17 +1258,14 @@ impl Processor { let transient_stake_account_info = next_account_info(account_info_iter)?; let clock_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(clock_info)?; - let rent = if maybe_ephemeral_stake_seed.is_some() { - // instruction with ephemeral account doesn't take the rent account - Rent::get()? - } else { - // legacy instruction takes the rent account - let rent_info = next_account_info(account_info_iter)?; - Rent::from_account_info(rent_info)? - }; - let maybe_stake_history_info = maybe_ephemeral_stake_seed - .map(|_| next_account_info(account_info_iter)) - .transpose()?; + let (rent, maybe_stake_history_info) = + if maybe_ephemeral_stake_seed.is_some() || fund_rent_exempt_reserve { + (Rent::get()?, Some(next_account_info(account_info_iter)?)) + } else { + // legacy instruction takes the rent account + let rent_info = next_account_info(account_info_iter)?; + (Rent::from_account_info(rent_info)?, None) + }; let system_program_info = next_account_info(account_info_iter)?; let stake_program_info = next_account_info(account_info_iter)?; @@ -1487,6 +1484,35 @@ impl Processor { stake_space, )?; + // if needed, withdraw rent-exempt reserve for transient account + if let Some(reserve_stake_info) = maybe_reserve_stake_info { + let required_lamports = + stake_rent.saturating_sub(transient_stake_account_info.lamports()); + // in the case of doing a full split from an ephemeral account, + // the rent-exempt reserve moves over, so no need to fund it from + // the pool reserve + if source_stake_account_info.lamports() != split_lamports { + let stake_history_info = + maybe_stake_history_info.ok_or(StakePoolError::MissingRequiredSysvar)?; + if required_lamports >= reserve_stake_info.lamports() { + return Err(StakePoolError::ReserveDepleted.into()); + } + if required_lamports > 0 { + Self::stake_withdraw( + stake_pool_info.key, + reserve_stake_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + transient_stake_account_info.clone(), + clock_info.clone(), + stake_history_info.clone(), + required_lamports, + )?; + } + } + } + // split into transient stake account Self::stake_split( stake_pool_info.key, @@ -1517,13 +1543,8 @@ 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(split_lamports) - .ok_or(StakePoolError::CalculationFailure)? - .into(); + transient_stake_account_info.lamports().into(); validator_stake_info.transient_seed_suffix = transient_stake_seed.into(); Ok(()) @@ -3842,6 +3863,7 @@ impl Processor { transient_stake_seed, } => { msg!("Instruction: DecreaseValidatorStake"); + msg!("NOTE: This instruction is deprecated, please use `DecreaseValidatorStakeWithReserve`"); Self::process_decrease_validator_stake( program_id, accounts, @@ -3851,6 +3873,20 @@ impl Processor { false, ) } + StakePoolInstruction::DecreaseValidatorStakeWithReserve { + lamports, + transient_stake_seed, + } => { + msg!("Instruction: DecreaseValidatorStakeWithReserve"); + Self::process_decrease_validator_stake( + program_id, + accounts, + lamports, + transient_stake_seed, + None, + true, + ) + } StakePoolInstruction::DecreaseAdditionalValidatorStake { lamports, transient_stake_seed, diff --git a/stake-pool/program/tests/decrease.rs b/stake-pool/program/tests/decrease.rs index 493e75ad544..c6ea45428b9 100644 --- a/stake-pool/program/tests/decrease.rs +++ b/stake-pool/program/tests/decrease.rs @@ -4,6 +4,7 @@ mod helpers; use { + assert_matches::assert_matches, bincode::deserialize, helpers::*, solana_program::{clock::Epoch, instruction::InstructionError, pubkey::Pubkey, stake}, @@ -80,10 +81,11 @@ async fn setup() -> ( ) } -#[test_case(true; "additional")] -#[test_case(false; "no-additional")] +#[test_case(DecreaseInstruction::Additional; "additional")] +#[test_case(DecreaseInstruction::Reserve; "reserve")] +#[test_case(DecreaseInstruction::Deprecated; "deprecated")] #[tokio::test] -async fn success(use_additional_instruction: bool) { +async fn success(instruction_type: DecreaseInstruction) { let ( mut context, stake_pool_accounts, @@ -114,7 +116,7 @@ async fn success(use_additional_instruction: bool) { &validator_stake.transient_stake_account, decrease_lamports, validator_stake.transient_stake_seed, - use_additional_instruction, + instruction_type, ) .await; assert!(error.is_none(), "{:?}", error); @@ -147,16 +149,12 @@ async fn success(use_additional_instruction: bool) { .await; let transient_stake_state = deserialize::(&transient_stake_account.data).unwrap(); - let transient_lamports = if use_additional_instruction { - decrease_lamports + stake_rent - } else { - decrease_lamports - }; + let transient_lamports = decrease_lamports + stake_rent; assert_eq!(transient_stake_account.lamports, transient_lamports); - let reserve_lamports = if use_additional_instruction { - reserve_lamports - stake_rent - } else { + let reserve_lamports = if instruction_type == DecreaseInstruction::Deprecated { reserve_lamports + } else { + reserve_lamports - stake_rent }; let reserve_stake_account = get_account( &mut context.banks_client, @@ -181,12 +179,13 @@ async fn fail_with_wrong_withdraw_authority() { let wrong_authority = Pubkey::new_unique(); let transaction = Transaction::new_signed_with_payer( - &[instruction::decrease_validator_stake( + &[instruction::decrease_validator_stake_with_reserve( &id(), &stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.staker.pubkey(), &wrong_authority, &stake_pool_accounts.validator_list.pubkey(), + &stake_pool_accounts.reserve_stake.pubkey(), &validator_stake.stake_account, &validator_stake.transient_stake_account, decrease_lamports, @@ -204,13 +203,13 @@ async fn fail_with_wrong_withdraw_authority() { .unwrap() .unwrap(); - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::InvalidProgramAddress as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while decreasing with wrong withdraw authority"), - } + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::InvalidProgramAddress as u32) + ) + ); } #[tokio::test] @@ -227,12 +226,13 @@ async fn fail_with_wrong_validator_list() { stake_pool_accounts.validator_list = Keypair::new(); let transaction = Transaction::new_signed_with_payer( - &[instruction::decrease_validator_stake( + &[instruction::decrease_validator_stake_with_reserve( &id(), &stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.staker.pubkey(), &stake_pool_accounts.withdraw_authority, &stake_pool_accounts.validator_list.pubkey(), + &stake_pool_accounts.reserve_stake.pubkey(), &validator_stake.stake_account, &validator_stake.transient_stake_account, decrease_lamports, @@ -250,13 +250,13 @@ async fn fail_with_wrong_validator_list() { .unwrap() .unwrap(); - match error { - TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { - let program_error = StakePoolError::InvalidValidatorStakeList as u32; - assert_eq!(error_index, program_error); - } - _ => panic!("Wrong error occurs while decreasing with wrong validator stake list account"), - } + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::InvalidValidatorStakeList as u32) + ) + ); } #[tokio::test] @@ -274,12 +274,13 @@ async fn fail_with_unknown_validator() { .await; let transaction = Transaction::new_signed_with_payer( - &[instruction::decrease_validator_stake( + &[instruction::decrease_validator_stake_with_reserve( &id(), &stake_pool_accounts.stake_pool.pubkey(), &stake_pool_accounts.staker.pubkey(), &stake_pool_accounts.withdraw_authority, &stake_pool_accounts.validator_list.pubkey(), + &stake_pool_accounts.reserve_stake.pubkey(), &unknown_stake.stake_account, &unknown_stake.transient_stake_account, decrease_lamports, @@ -306,10 +307,11 @@ async fn fail_with_unknown_validator() { ); } -#[test_case(true; "additional")] -#[test_case(false; "no-additional")] +#[test_case(DecreaseInstruction::Additional; "additional")] +#[test_case(DecreaseInstruction::Reserve; "reserve")] +#[test_case(DecreaseInstruction::Deprecated; "deprecated")] #[tokio::test] -async fn fail_twice_diff_seed(use_additional_instruction: bool) { +async fn fail_twice_diff_seed(instruction_type: DecreaseInstruction) { let (mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, _) = setup().await; @@ -322,7 +324,7 @@ async fn fail_twice_diff_seed(use_additional_instruction: bool) { &validator_stake.transient_stake_account, decrease_lamports / 3, validator_stake.transient_stake_seed, - use_additional_instruction, + instruction_type, ) .await; assert!(error.is_none(), "{:?}", error); @@ -344,40 +346,44 @@ async fn fail_twice_diff_seed(use_additional_instruction: bool) { &transient_stake_address, decrease_lamports / 2, transient_stake_seed, - use_additional_instruction, + instruction_type, ) .await .unwrap() .unwrap(); - if use_additional_instruction { + if instruction_type == DecreaseInstruction::Additional { assert_eq!( error, TransactionError::InstructionError(0, InstructionError::InvalidSeeds) ); } else { - assert_eq!( + assert_matches!( error, TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::TransientAccountInUse as u32) - ) + _, + InstructionError::Custom(code) + ) if code == StakePoolError::TransientAccountInUse as u32 ); } } -#[test_case(true, true, true; "success-all-additional")] -#[test_case(true, false, true; "success-with-additional")] -#[test_case(false, true, false; "fail-without-additional")] -#[test_case(false, false, false; "fail-no-additional")] +#[test_case(true, DecreaseInstruction::Additional, DecreaseInstruction::Additional; "success-all-additional")] +#[test_case(true, DecreaseInstruction::Reserve, DecreaseInstruction::Additional; "success-with-additional")] +#[test_case(false, DecreaseInstruction::Additional, DecreaseInstruction::Reserve; "fail-without-additional")] +#[test_case(false, DecreaseInstruction::Reserve, DecreaseInstruction::Reserve; "fail-no-additional")] #[tokio::test] -async fn twice(success: bool, use_additional_first_time: bool, use_additional_second_time: bool) { +async fn twice( + success: bool, + first_instruction: DecreaseInstruction, + second_instruction: DecreaseInstruction, +) { let ( mut context, stake_pool_accounts, validator_stake, _deposit_info, decrease_lamports, - mut reserve_lamports, + reserve_lamports, ) = setup().await; let pre_stake_account = @@ -398,7 +404,7 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se &validator_stake.transient_stake_account, first_decrease, validator_stake.transient_stake_seed, - use_additional_first_time, + first_instruction, ) .await; assert!(error.is_none(), "{:?}", error); @@ -412,7 +418,7 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se &validator_stake.transient_stake_account, second_decrease, validator_stake.transient_stake_seed, - use_additional_second_time, + second_instruction, ) .await; @@ -453,11 +459,8 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se .await; let transient_stake_state = deserialize::(&transient_stake_account.data).unwrap(); - let mut transient_lamports = total_decrease; - if use_additional_first_time { - transient_lamports += stake_rent; - } - if use_additional_second_time { + let mut transient_lamports = total_decrease + stake_rent; + if second_instruction == DecreaseInstruction::Additional { transient_lamports += stake_rent; } assert_eq!(transient_stake_account.lamports, transient_lamports); @@ -480,10 +483,8 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se ); // reserve deducted properly - if use_additional_first_time { - reserve_lamports -= stake_rent; - } - if use_additional_second_time { + let mut reserve_lamports = reserve_lamports - stake_rent; + if second_instruction == DecreaseInstruction::Additional { reserve_lamports -= stake_rent; } let reserve_stake_account = get_account( @@ -494,20 +495,21 @@ async fn twice(success: bool, use_additional_first_time: bool, use_additional_se assert_eq!(reserve_stake_account.lamports, reserve_lamports); } else { let error = error.unwrap().unwrap(); - assert_eq!( + assert_matches!( error, TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::TransientAccountInUse as u32) - ) + _, + InstructionError::Custom(code) + ) if code == StakePoolError::TransientAccountInUse as u32 ); } } -#[test_case(true; "additional")] -#[test_case(false; "no-additional")] +#[test_case(DecreaseInstruction::Additional; "additional")] +#[test_case(DecreaseInstruction::Reserve; "reserve")] +#[test_case(DecreaseInstruction::Deprecated; "deprecated")] #[tokio::test] -async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { +async fn fail_with_small_lamport_amount(instruction_type: DecreaseInstruction) { let (mut context, stake_pool_accounts, validator_stake, _deposit_info, _decrease_lamports, _) = setup().await; @@ -523,25 +525,28 @@ async fn fail_with_small_lamport_amount(use_additional_instruction: bool) { &validator_stake.transient_stake_account, lamports, validator_stake.transient_stake_seed, - use_additional_instruction, + instruction_type, ) .await .unwrap() .unwrap(); - match error { - TransactionError::InstructionError(_, InstructionError::AccountNotRentExempt) => {} - _ => panic!("Wrong error occurs while try to decrease small stake"), - } + assert_matches!( + error, + TransactionError::InstructionError(_, InstructionError::AccountNotRentExempt) + ); } +#[test_case(DecreaseInstruction::Additional; "additional")] +#[test_case(DecreaseInstruction::Reserve; "reserve")] +#[test_case(DecreaseInstruction::Deprecated; "deprecated")] #[tokio::test] -async fn fail_big_overdraw() { +async fn fail_big_overdraw(instruction_type: DecreaseInstruction) { let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports, _) = setup().await; let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -549,21 +554,23 @@ async fn fail_big_overdraw() { &validator_stake.transient_stake_account, deposit_info.stake_lamports * 1_000_000, validator_stake.transient_stake_seed, + instruction_type, ) .await .unwrap() .unwrap(); - assert_eq!( + assert_matches!( error, - TransactionError::InstructionError(0, InstructionError::InsufficientFunds) + TransactionError::InstructionError(_, InstructionError::InsufficientFunds) ); } -#[test_case(true; "additional")] -#[test_case(false; "no-additional")] +#[test_case(DecreaseInstruction::Additional; "additional")] +#[test_case(DecreaseInstruction::Reserve; "reserve")] +#[test_case(DecreaseInstruction::Deprecated; "deprecated")] #[tokio::test] -async fn fail_overdraw(use_additional_instruction: bool) { +async fn fail_overdraw(instruction_type: DecreaseInstruction) { let (mut context, stake_pool_accounts, validator_stake, deposit_info, _decrease_lamports, _) = setup().await; @@ -579,15 +586,15 @@ async fn fail_overdraw(use_additional_instruction: bool) { &validator_stake.transient_stake_account, deposit_info.stake_lamports + stake_rent + 1, validator_stake.transient_stake_seed, - use_additional_instruction, + instruction_type, ) .await .unwrap() .unwrap(); - assert_eq!( + assert_matches!( error, - TransactionError::InstructionError(0, InstructionError::InsufficientFunds) + TransactionError::InstructionError(_, InstructionError::InsufficientFunds) ); } @@ -644,17 +651,17 @@ async fn fail_additional_with_increasing() { &validator_stake.transient_stake_account, decrease_lamports / 2, validator_stake.transient_stake_seed, - true, + DecreaseInstruction::Additional, ) .await .unwrap() .unwrap(); - assert_eq!( + assert_matches!( error, TransactionError::InstructionError( - 0, - InstructionError::Custom(StakePoolError::WrongStakeState as u32) - ) + _, + InstructionError::Custom(code) + ) if code == StakePoolError::WrongStakeState as u32 ); } diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index a07eec4030e..4f001295b28 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -1614,7 +1614,7 @@ impl StakePoolAccounts { } #[allow(clippy::too_many_arguments)] - pub async fn decrease_validator_stake( + pub async fn decrease_validator_stake_deprecated( &self, banks_client: &mut BanksClient, payer: &Keypair, @@ -1624,12 +1624,57 @@ impl StakePoolAccounts { lamports: u64, transient_stake_seed: u64, ) -> Option { - let mut instructions = vec![instruction::decrease_validator_stake( + #[allow(deprecated)] + let mut instructions = vec![ + system_instruction::transfer( + &payer.pubkey(), + transient_stake, + STAKE_ACCOUNT_RENT_EXEMPTION, + ), + instruction::decrease_validator_stake( + &id(), + &self.stake_pool.pubkey(), + &self.staker.pubkey(), + &self.withdraw_authority, + &self.validator_list.pubkey(), + validator_stake, + transient_stake, + lamports, + transient_stake_seed, + ), + ]; + self.maybe_add_compute_budget_instruction(&mut instructions); + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&payer.pubkey()), + &[payer, &self.staker], + *recent_blockhash, + ); + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + + #[allow(clippy::too_many_arguments)] + pub async fn decrease_validator_stake_with_reserve( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + validator_stake: &Pubkey, + transient_stake: &Pubkey, + lamports: u64, + transient_stake_seed: u64, + ) -> Option { + let mut instructions = vec![instruction::decrease_validator_stake_with_reserve( &id(), &self.stake_pool.pubkey(), &self.staker.pubkey(), &self.withdraw_authority, &self.validator_list.pubkey(), + &self.reserve_stake.pubkey(), validator_stake, transient_stake, lamports, @@ -1700,39 +1745,56 @@ impl StakePoolAccounts { transient_stake: &Pubkey, lamports: u64, transient_stake_seed: u64, - use_additional_instruction: bool, + instruction_type: DecreaseInstruction, ) -> Option { - if use_additional_instruction { - let ephemeral_stake_seed = 0; - let ephemeral_stake = find_ephemeral_stake_program_address( - &id(), - &self.stake_pool.pubkey(), - ephemeral_stake_seed, - ) - .0; - self.decrease_additional_validator_stake( - banks_client, - payer, - recent_blockhash, - validator_stake, - &ephemeral_stake, - transient_stake, - lamports, - transient_stake_seed, - ephemeral_stake_seed, - ) - .await - } else { - self.decrease_validator_stake( - banks_client, - payer, - recent_blockhash, - validator_stake, - transient_stake, - lamports, - transient_stake_seed, - ) - .await + match instruction_type { + DecreaseInstruction::Additional => { + let ephemeral_stake_seed = 0; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &self.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + self.decrease_additional_validator_stake( + banks_client, + payer, + recent_blockhash, + validator_stake, + &ephemeral_stake, + transient_stake, + lamports, + transient_stake_seed, + ephemeral_stake_seed, + ) + .await + } + DecreaseInstruction::Reserve => { + self.decrease_validator_stake_with_reserve( + banks_client, + payer, + recent_blockhash, + validator_stake, + transient_stake, + lamports, + transient_stake_seed, + ) + .await + } + DecreaseInstruction::Deprecated => + { + #[allow(deprecated)] + self.decrease_validator_stake_deprecated( + banks_client, + payer, + recent_blockhash, + validator_stake, + transient_stake, + lamports, + transient_stake_seed, + ) + .await + } } } @@ -2545,6 +2607,7 @@ pub fn add_token_account( pub async fn setup_for_withdraw( token_program_id: Pubkey, + reserve_lamports: u64, ) -> ( ProgramTestContext, StakePoolAccounts, @@ -2561,7 +2624,7 @@ pub async fn setup_for_withdraw( &mut context.banks_client, &context.payer, &context.last_blockhash, - MINIMUM_RESERVE_LAMPORTS, + reserve_lamports, ) .await .unwrap(); @@ -2629,3 +2692,10 @@ pub async fn setup_for_withdraw( tokens_to_withdraw, ) } + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum DecreaseInstruction { + Additional, + Reserve, + Deprecated, +} diff --git a/stake-pool/program/tests/increase.rs b/stake-pool/program/tests/increase.rs index b42dd789274..226123722eb 100644 --- a/stake-pool/program/tests/increase.rs +++ b/stake-pool/program/tests/increase.rs @@ -553,7 +553,7 @@ async fn fail_additional_with_decreasing() { .await; let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -561,6 +561,7 @@ async fn fail_additional_with_decreasing() { &validator_stake.transient_stake_account, current_minimum_delegation + stake_rent, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); diff --git a/stake-pool/program/tests/redelegate.rs b/stake-pool/program/tests/redelegate.rs index 707b7067baf..0fc70db6f99 100644 --- a/stake-pool/program/tests/redelegate.rs +++ b/stake-pool/program/tests/redelegate.rs @@ -651,7 +651,7 @@ async fn fail_with_decreasing_stake() { .unwrap(); let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -659,6 +659,7 @@ async fn fail_with_decreasing_stake() { &destination_validator_stake.transient_stake_account, minimum_decrease_lamports, destination_validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); diff --git a/stake-pool/program/tests/update_validator_list_balance.rs b/stake-pool/program/tests/update_validator_list_balance.rs index 1c6b336916d..831ad30d483 100644 --- a/stake-pool/program/tests/update_validator_list_balance.rs +++ b/stake-pool/program/tests/update_validator_list_balance.rs @@ -279,7 +279,7 @@ async fn merge_into_reserve() { println!("Decrease from all validators"); for stake_account in &stake_accounts { let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -287,6 +287,7 @@ async fn merge_into_reserve() { &stake_account.transient_stake_account, lamports, stake_account.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -554,7 +555,7 @@ async fn merge_transient_stake_after_remove() { // Decrease and remove all validators for stake_account in &stake_accounts { let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -562,6 +563,7 @@ async fn merge_transient_stake_after_remove() { &stake_account.transient_stake_account, deactivated_lamports, stake_account.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -616,7 +618,7 @@ async fn merge_transient_stake_after_remove() { ); assert_eq!( u64::from(validator_list.validators[0].transient_stake_lamports), - deactivated_lamports + deactivated_lamports + stake_rent ); // Update with merge, status should be ReadyForRemoval and no lamports diff --git a/stake-pool/program/tests/update_validator_list_balance_hijack.rs b/stake-pool/program/tests/update_validator_list_balance_hijack.rs index 8ef0e59e775..feb05248c94 100644 --- a/stake-pool/program/tests/update_validator_list_balance_hijack.rs +++ b/stake-pool/program/tests/update_validator_list_balance_hijack.rs @@ -220,7 +220,7 @@ async fn check_ignored_hijacked_transient_stake( println!("Decrease from all validators"); let stake_account = &stake_accounts[0]; let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -228,6 +228,7 @@ async fn check_ignored_hijacked_transient_stake( &stake_account.transient_stake_account, lamports, stake_account.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -384,7 +385,7 @@ async fn check_ignored_hijacked_validator_stake( let stake_account = &stake_accounts[0]; let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -392,6 +393,7 @@ async fn check_ignored_hijacked_validator_stake( &stake_account.transient_stake_account, lamports, stake_account.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); diff --git a/stake-pool/program/tests/vsa_remove.rs b/stake-pool/program/tests/vsa_remove.rs index 187306377f5..b305052fd11 100644 --- a/stake-pool/program/tests/vsa_remove.rs +++ b/stake-pool/program/tests/vsa_remove.rs @@ -455,7 +455,7 @@ async fn success_with_deactivating_transient_stake() { // increase the validator stake let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -463,6 +463,7 @@ async fn success_with_deactivating_transient_stake() { &validator_stake.transient_stake_account, TEST_STAKE_AMOUNT + stake_rent, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -546,7 +547,7 @@ async fn success_with_deactivating_transient_stake() { vote_account_address: validator_stake.vote.pubkey(), last_update_epoch: 0.into(), active_stake_lamports: (stake_rent + current_minimum_delegation).into(), - transient_stake_lamports: (TEST_STAKE_AMOUNT + stake_rent).into(), + transient_stake_lamports: (TEST_STAKE_AMOUNT + stake_rent * 2).into(), transient_seed_suffix: validator_stake.transient_stake_seed.into(), unused: 0.into(), validator_seed_suffix: validator_stake @@ -705,7 +706,7 @@ async fn success_with_hijacked_transient_account() { // decrease let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -713,6 +714,7 @@ async fn success_with_hijacked_transient_account() { &validator_stake.transient_stake_account, increase_amount, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs index 3fd449624b0..1baddb17199 100644 --- a/stake-pool/program/tests/withdraw.rs +++ b/stake-pool/program/tests/withdraw.rs @@ -49,7 +49,7 @@ async fn _success(token_program_id: Pubkey, test_type: SuccessTestType) { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(token_program_id).await; + ) = setup_for_withdraw(token_program_id, 0).await; // Save stake pool state before withdrawal let stake_pool_before = get_account( @@ -268,7 +268,7 @@ async fn fail_with_wrong_stake_program() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let new_authority = Pubkey::new_unique(); let wrong_stake_program = Pubkey::new_unique(); @@ -328,7 +328,7 @@ async fn fail_with_wrong_withdraw_authority() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let new_authority = Pubkey::new_unique(); stake_pool_accounts.withdraw_authority = Keypair::new().pubkey(); @@ -370,7 +370,7 @@ async fn fail_with_wrong_token_program_id() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let new_authority = Pubkey::new_unique(); let wrong_token_program = Keypair::new(); @@ -421,7 +421,7 @@ async fn fail_with_wrong_validator_list() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let new_authority = Pubkey::new_unique(); stake_pool_accounts.validator_list = Keypair::new(); @@ -465,7 +465,7 @@ async fn fail_with_unknown_validator() { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let unknown_stake = create_unknown_validator_stake( &mut context.banks_client, @@ -512,7 +512,7 @@ async fn fail_double_withdraw_to_the_same_account() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let new_authority = Pubkey::new_unique(); let error = stake_pool_accounts @@ -578,7 +578,7 @@ async fn fail_without_token_approval() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; revoke_tokens( &mut context.banks_client, @@ -630,7 +630,7 @@ async fn fail_with_not_enough_tokens() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let last_blockhash = context .banks_client @@ -773,7 +773,7 @@ async fn success_with_slippage(token_program_id: Pubkey) { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(token_program_id).await; + ) = setup_for_withdraw(token_program_id, 0).await; // Save user token balance let user_token_balance_before = get_token_balance( diff --git a/stake-pool/program/tests/withdraw_edge_cases.rs b/stake-pool/program/tests/withdraw_edge_cases.rs index 90cb5a7abc0..a2d1fd6c117 100644 --- a/stake-pool/program/tests/withdraw_edge_cases.rs +++ b/stake-pool/program/tests/withdraw_edge_cases.rs @@ -25,11 +25,11 @@ async fn fail_remove_validator() { user_transfer_authority, user_stake_recipient, _, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; // decrease a little stake, not all let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -37,6 +37,7 @@ async fn fail_remove_validator() { &validator_stake.transient_stake_account, deposit_info.stake_lamports / 2, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -46,7 +47,7 @@ async fn fail_remove_validator() { context.warp_to_slot(first_normal_slot + 1).unwrap(); // update to merge deactivated stake into reserve - stake_pool_accounts + let error = stake_pool_accounts .update_all( &mut context.banks_client, &context.payer, @@ -55,6 +56,7 @@ async fn fail_remove_validator() { false, ) .await; + assert!(error.is_none(), "{:?}", error); // Withdraw entire account, fail because some stake left let validator_stake_account = @@ -99,7 +101,7 @@ async fn success_remove_validator(multiple: u64) { user_transfer_authority, user_stake_recipient, _, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; // make pool tokens very valuable, so it isn't possible to exactly get down to the minimum transfer( @@ -129,7 +131,7 @@ async fn success_remove_validator(multiple: u64) { // decrease all of stake except for lamports_per_pool_token lamports, must be withdrawable let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -137,6 +139,7 @@ async fn success_remove_validator(multiple: u64) { &validator_stake.transient_stake_account, deposit_info.stake_lamports + stake_rent - lamports_per_pool_token, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -238,11 +241,11 @@ async fn fail_with_reserve() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; // decrease a little stake, not all let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -250,6 +253,7 @@ async fn fail_with_reserve() { &validator_stake.transient_stake_account, deposit_info.stake_lamports / 2, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -305,14 +309,14 @@ async fn success_with_reserve() { user_transfer_authority, user_stake_recipient, _, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); // decrease all of stake let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &context.last_blockhash, @@ -320,6 +324,7 @@ async fn success_with_reserve() { &validator_stake.transient_stake_account, deposit_info.stake_lamports + stake_rent, validator_stake.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -398,7 +403,7 @@ async fn success_with_reserve() { let stake_state = deserialize::(&reserve_stake_account.data).unwrap(); let meta = stake_state.meta().unwrap(); assert_eq!( - meta.rent_exempt_reserve + withdrawal_fee + deposit_fee, + meta.rent_exempt_reserve + withdrawal_fee + deposit_fee + stake_rent, reserve_stake_account.lamports ); @@ -421,7 +426,7 @@ async fn success_with_empty_preferred_withdraw() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let preferred_validator = simple_add_validator_to_pool( &mut context.banks_client, @@ -470,7 +475,7 @@ async fn success_and_fail_with_preferred_withdraw() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let last_blockhash = context .banks_client @@ -566,7 +571,7 @@ async fn fail_withdraw_from_transient() { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; let last_blockhash = context .banks_client @@ -605,7 +610,7 @@ async fn fail_withdraw_from_transient() { // decrease to minimum stake + 2 lamports let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -613,6 +618,7 @@ async fn fail_withdraw_from_transient() { &validator_stake_account.transient_stake_account, deposit_info.stake_lamports + stake_rent - 2, validator_stake_account.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -653,7 +659,7 @@ async fn success_withdraw_from_transient() { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), STAKE_ACCOUNT_RENT_EXEMPTION).await; let last_blockhash = context .banks_client @@ -692,7 +698,7 @@ async fn success_withdraw_from_transient() { // decrease all of stake let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -700,6 +706,7 @@ async fn success_withdraw_from_transient() { &validator_stake_account.transient_stake_account, deposit_info.stake_lamports + stake_rent, validator_stake_account.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); @@ -733,7 +740,7 @@ async fn success_with_small_preferred_withdraw() { user_transfer_authority, user_stake_recipient, tokens_to_burn, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let last_blockhash = context .banks_client @@ -806,7 +813,7 @@ async fn success_with_small_preferred_withdraw() { // decrease all stake except for 1 lamport let error = stake_pool_accounts - .decrease_validator_stake( + .decrease_validator_stake_either( &mut context.banks_client, &context.payer, &last_blockhash, @@ -814,6 +821,7 @@ async fn success_with_small_preferred_withdraw() { &preferred_validator.transient_stake_account, minimum_lamports, preferred_validator.transient_stake_seed, + DecreaseInstruction::Reserve, ) .await; assert!(error.is_none(), "{:?}", error); diff --git a/stake-pool/program/tests/withdraw_with_fee.rs b/stake-pool/program/tests/withdraw_with_fee.rs index 1a2b0134181..34c28b92c0f 100644 --- a/stake-pool/program/tests/withdraw_with_fee.rs +++ b/stake-pool/program/tests/withdraw_with_fee.rs @@ -22,7 +22,7 @@ async fn success_withdraw_all_fee_tokens() { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let last_blockhash = context .banks_client @@ -99,7 +99,7 @@ async fn success_empty_out_stake_with_fee() { user_transfer_authority, user_stake_recipient, tokens_to_withdraw, - ) = setup_for_withdraw(spl_token::id()).await; + ) = setup_for_withdraw(spl_token::id(), 0).await; let last_blockhash = context .banks_client