diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index ad468443fe8..9f2d36c7db7 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -473,6 +473,57 @@ pub enum StakePoolInstruction { /// seed used to create ephemeral account. ephemeral_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 + /// corresponding transient stake account, redelegates it to an ephemeral stake + /// account, then merges that stake into the destination 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 source transient stake account and + /// ephemeral stake account do not exist. + /// + /// The amount of lamports to move must be at least twice rent-exemption + /// plus the minimum delegation amount. Rent-exemption is required for the + /// source transient stake account, and rent-exemption plus minimum delegation + /// is required for the destination ephemeral stake account. + /// + /// 0. `[]` Stake pool + /// 1. `[s]` Stake pool staker + /// 2. `[]` Stake pool withdraw authority + /// 3. `[w]` Validator list + /// 4. `[w]` Source canonical stake account to split from + /// 5. `[w]` Source transient stake account to receive split and be redelegated + /// 6. `[w]` Uninitialized ephemeral stake account to receive redelegation + /// 7. `[w]` Destination transient stake account to receive ephemeral stake by merge + /// 8. `[]` Destination stake account to receive transient stake after activation + /// 9. `[]` Destination validator vote account + /// 10. `[]` Clock sysvar + /// 11. `[]` Stake History sysvar + /// 12. `[]` Stake Config sysvar + /// 13. `[]` System program + /// 14. `[]` Stake program + Redelegate { + /// Amount of lamports to redelegate + #[allow(dead_code)] // but it's not + lamports: u64, + /// Seed used to create source transient stake account + #[allow(dead_code)] // but it's not + source_transient_stake_seed: u64, + /// Seed used to create destination ephemeral account. + #[allow(dead_code)] // but it's not + ephemeral_stake_seed: u64, + /// Seed used to create destination transient stake account. If there is + /// already transient stake, this must match the current seed, otherwise + /// it can be anything + #[allow(dead_code)] // but it's not + destination_transient_stake_seed: u64, + }, } /// Creates an 'initialize' instruction. @@ -756,6 +807,55 @@ pub fn increase_additional_validator_stake( } } +/// Creates `Redelegate` instruction (rebalance from one validator account to another) +pub fn redelegate( + program_id: &Pubkey, + stake_pool: &Pubkey, + staker: &Pubkey, + stake_pool_withdraw_authority: &Pubkey, + validator_list: &Pubkey, + source_validator_stake: &Pubkey, + source_transient_stake: &Pubkey, + ephemeral_stake: &Pubkey, + destination_transient_stake: &Pubkey, + destination_validator_stake: &Pubkey, + validator: &Pubkey, + lamports: u64, + source_transient_stake_seed: u64, + ephemeral_stake_seed: u64, + destination_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(*source_validator_stake, false), + AccountMeta::new(*source_transient_stake, false), + AccountMeta::new(*ephemeral_stake, false), + AccountMeta::new(*destination_transient_stake, false), + AccountMeta::new_readonly(*destination_validator_stake, false), + AccountMeta::new_readonly(*validator, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), + AccountMeta::new_readonly(stake::config::id(), false), + AccountMeta::new_readonly(system_program::id(), false), + AccountMeta::new_readonly(stake::program::id(), false), + ]; + Instruction { + program_id: *program_id, + accounts, + data: StakePoolInstruction::Redelegate { + lamports, + source_transient_stake_seed, + ephemeral_stake_seed, + destination_transient_stake_seed, + } + .try_to_vec() + .unwrap(), + } +} + /// Creates `SetPreferredDepositValidator` instruction pub fn set_preferred_validator( program_id: &Pubkey, diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 761272e1a6e..33355dc0bec 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -203,7 +203,7 @@ fn check_account_owner( } } -/// Checks if a stake acount can be managed by the pool +/// Checks if a stake account can be managed by the pool fn stake_is_usable_by_pool( meta: &stake::state::Meta, expected_authority: &Pubkey, @@ -214,13 +214,98 @@ fn stake_is_usable_by_pool( && meta.lockup == *expected_lockup } -/// Checks if a stake acount is active, without taking into account cooldowns +/// Checks if a stake account is active, without taking into account cooldowns fn stake_is_inactive_without_history(stake: &stake::state::Stake, epoch: Epoch) -> bool { stake.delegation.deactivation_epoch < epoch || (stake.delegation.activation_epoch == epoch && stake.delegation.deactivation_epoch == epoch) } +/// Check that the stake state is correct: usable by the pool and delegated to +/// the expected validator +fn check_stake_state( + stake_account_info: &AccountInfo, + withdraw_authority: &Pubkey, + vote_account_address: &Pubkey, + lockup: &stake::state::Lockup, +) -> Result<(), ProgramError> { + let (meta, stake) = get_stake_state(stake_account_info)?; + if !stake_is_usable_by_pool(&meta, withdraw_authority, lockup) { + msg!( + "Validator stake for {} not usable by pool, must be owned by withdraw authority", + vote_account_address + ); + return Err(StakePoolError::WrongStakeState.into()); + } + if stake.delegation.voter_pubkey != *vote_account_address { + msg!( + "Validator stake {} not delegated to {}", + stake_account_info.key, + vote_account_address + ); + return Err(StakePoolError::WrongStakeState.into()); + } + Ok(()) +} + +/// Checks if a validator stake account is valid, which means that it's usable by +/// the pool and delegated to the expected validator. These conditions can be violated +/// if a validator was force destaked during a cluster restart. +fn check_validator_stake_account( + stake_account_info: &AccountInfo, + program_id: &Pubkey, + stake_pool: &Pubkey, + withdraw_authority: &Pubkey, + vote_account_address: &Pubkey, + seed: u32, + lockup: &stake::state::Lockup, +) -> Result<(), ProgramError> { + check_account_owner(stake_account_info, &stake::program::id())?; + check_validator_stake_address( + program_id, + stake_pool, + stake_account_info.key, + vote_account_address, + NonZeroU32::new(seed), + )?; + check_stake_state( + stake_account_info, + withdraw_authority, + vote_account_address, + lockup, + )?; + Ok(()) +} + +/// Checks if a transient stake account is valid, which means that it's usable by +/// the pool and delegated to the expected validator. These conditions can be violated +/// if a validator was force destaked during a cluster restart. +fn check_transient_stake_account( + stake_account_info: &AccountInfo, + program_id: &Pubkey, + stake_pool: &Pubkey, + withdraw_authority: &Pubkey, + vote_account_address: &Pubkey, + seed: u64, + lockup: &stake::state::Lockup, +) -> Result<(), ProgramError> { + check_account_owner(stake_account_info, &stake::program::id())?; + check_transient_stake_address( + program_id, + stake_pool, + stake_account_info.key, + vote_account_address, + seed, + )?; + check_stake_state( + stake_account_info, + withdraw_authority, + vote_account_address, + lockup, + )?; + Ok(()) +} + /// Create a stake account on a PDA without transferring lamports fn create_stake_account<'a>( stake_account_info: AccountInfo<'a>, @@ -488,6 +573,42 @@ impl Processor { ) } + /// Issue stake::instruction::redelegate instruction to redelegate stake + #[allow(clippy::too_many_arguments)] + fn stake_redelegate<'a>( + stake_pool: &Pubkey, + source_account: AccountInfo<'a>, + authority: AccountInfo<'a>, + authority_type: &[u8], + bump_seed: u8, + destination_account: AccountInfo<'a>, + vote_account: AccountInfo<'a>, + stake_config: AccountInfo<'a>, + ) -> Result<(), ProgramError> { + let me_bytes = stake_pool.to_bytes(); + let authority_signature_seeds = [&me_bytes[..32], authority_type, &[bump_seed]]; + let signers = &[&authority_signature_seeds[..]]; + + let redelegate_instruction = &stake::instruction::redelegate( + source_account.key, + authority.key, + vote_account.key, + destination_account.key, + )[2]; + + invoke_signed( + redelegate_instruction, + &[ + source_account, + destination_account, + vote_account, + stake_config, + authority, + ], + signers, + ) + } + /// Issue a spl_token `Burn` instruction. #[allow(clippy::too_many_arguments)] fn token_burn<'a>( @@ -1468,32 +1589,15 @@ impl Processor { // explicit check here that the transient stake is increasing } - // Check that the validator stake account is actually delegated to the right - // validator. This can happen if a validator was force destaked during a - // cluster restart. - { - check_account_owner(validator_stake_account_info, stake_program_info.key)?; - check_validator_stake_address( - program_id, - stake_pool_info.key, - validator_stake_account_info.key, - vote_account_address, - NonZeroU32::new(validator_stake_info.validator_seed_suffix), - )?; - let (meta, stake) = get_stake_state(validator_stake_account_info)?; - if !stake_is_usable_by_pool(&meta, withdraw_authority_info.key, &stake_pool.lockup) { - msg!("Validator stake for {} not usable by pool, must be owned by withdraw authority", vote_account_address); - return Err(StakePoolError::WrongStakeState.into()); - } - if stake.delegation.voter_pubkey != *vote_account_address { - msg!( - "Validator stake {} not delegated to {}", - validator_stake_account_info.key, - vote_account_address - ); - return Err(StakePoolError::WrongStakeState.into()); - } - } + check_validator_stake_account( + validator_stake_account_info, + program_id, + stake_pool_info.key, + withdraw_authority_info.key, + vote_account_address, + validator_stake_info.validator_seed_suffix, + &stake_pool.lockup, + )?; if validator_stake_info.status != StakeStatus::Active { msg!("Validator is marked for removal and no longer allows increases"); @@ -1515,6 +1619,8 @@ impl Processor { } // the stake account rent exemption is withdrawn after the merge, so + // to add `lamports` to a validator, we need to create a stake account + // with `lamports + stake_rent` let total_lamports = lamports.saturating_add(stake_rent); if reserve_stake_account_info @@ -1668,6 +1774,316 @@ impl Processor { Ok(()) } + /// Processes `Redelegate` instruction. + #[inline(never)] // needed due to stack size violation + fn process_redelegate( + program_id: &Pubkey, + accounts: &[AccountInfo], + lamports: u64, + source_transient_stake_seed: u64, + ephemeral_stake_seed: u64, + destination_transient_stake_seed: u64, + ) -> 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 source_validator_stake_account_info = next_account_info(account_info_iter)?; + let source_transient_stake_account_info = next_account_info(account_info_iter)?; + let ephemeral_stake_account_info = next_account_info(account_info_iter)?; + let destination_transient_stake_account_info = next_account_info(account_info_iter)?; + let destination_validator_stake_account_info = next_account_info(account_info_iter)?; + let validator_vote_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 stake_history_info = next_account_info(account_info_iter)?; + let stake_config_info = next_account_info(account_info_iter)?; + let system_program_info = next_account_info(account_info_iter)?; + let stake_program_info = next_account_info(account_info_iter)?; + + check_system_program(system_program_info.key)?; + check_stake_program(stake_program_info.key)?; + check_account_owner(stake_pool_info, program_id)?; + + let stake_pool = try_from_slice_unchecked::(&stake_pool_info.data.borrow())?; + if !stake_pool.is_valid() { + msg!("Expected valid stake pool"); + return Err(StakePoolError::InvalidState.into()); + } + + stake_pool.check_authority_withdraw( + withdraw_authority_info.key, + program_id, + stake_pool_info.key, + )?; + stake_pool.check_staker(staker_info)?; + + if stake_pool.last_update_epoch < clock.epoch { + return Err(StakePoolError::StakeListAndPoolOutOfDate.into()); + } + + stake_pool.check_validator_list(validator_list_info)?; + check_account_owner(validator_list_info, program_id)?; + + let mut validator_list_data = validator_list_info.data.borrow_mut(); + let (header, mut validator_list) = + ValidatorListHeader::deserialize_vec(&mut validator_list_data)?; + if !header.is_valid() { + return Err(StakePoolError::InvalidState.into()); + } + + let rent = Rent::get()?; + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); + + // check that we're redelegating enough + let destination_transient_lamports = { + // redelegation requires that the source account maintains rent exemption and that + // the destination account has rent-exemption and minimum delegation + let minimum_redelegation_lamports = + current_minimum_delegation.saturating_add(stake_rent.saturating_mul(2)); + if lamports < minimum_redelegation_lamports { + msg!( + "Need more than {} lamports for redelegated stake and transient stake to meet minimum delegation requirement, {} provided", + minimum_redelegation_lamports, + lamports + ); + return Err(ProgramError::Custom( + stake::instruction::StakeError::InsufficientDelegation as u32, + )); + } + + // check that we're not draining the source account + let current_minimum_lamports = stake_rent.saturating_add(current_minimum_delegation); + if source_validator_stake_account_info + .lamports() + .saturating_sub(lamports) + < current_minimum_lamports + { + let max_split_amount = source_validator_stake_account_info + .lamports() + .saturating_sub(current_minimum_lamports); + msg!( + "Source stake does not have {} lamports for redelegation, must be {} at most", + lamports, + max_split_amount, + ); + return Err(ProgramError::InsufficientFunds); + } + lamports + .checked_sub(stake_rent) + .ok_or(StakePoolError::CalculationFailure)? + }; + + // check source account state + let (_, stake) = get_stake_state(source_validator_stake_account_info)?; + let vote_account_address = stake.delegation.voter_pubkey; + { + let maybe_validator_stake_info = + validator_list.find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, &vote_account_address) + }); + if maybe_validator_stake_info.is_none() { + msg!( + "Source vote account {} not found in stake pool", + vote_account_address + ); + return Err(StakePoolError::ValidatorNotFound.into()); + } + let mut validator_stake_info = maybe_validator_stake_info.unwrap(); + check_validator_stake_address( + program_id, + stake_pool_info.key, + source_validator_stake_account_info.key, + &vote_account_address, + NonZeroU32::new(validator_stake_info.validator_seed_suffix), + )?; + if validator_stake_info.transient_stake_lamports > 0 { + return Err(StakePoolError::TransientAccountInUse.into()); + } + if validator_stake_info.status != StakeStatus::Active { + msg!("Validator is marked for removal and no longer allows redelegation"); + return Err(StakePoolError::ValidatorNotFound.into()); + } + validator_stake_info.active_stake_lamports = validator_stake_info + .active_stake_lamports + .checked_sub(lamports) + .ok_or(StakePoolError::CalculationFailure)?; + validator_stake_info.transient_stake_lamports = stake_rent; + validator_stake_info.transient_seed_suffix = source_transient_stake_seed; + } + + // split from source, into source transient + { + // check transient account + let source_transient_stake_bump_seed = check_transient_stake_address( + program_id, + stake_pool_info.key, + source_transient_stake_account_info.key, + &vote_account_address, + source_transient_stake_seed, + )?; + let source_transient_stake_account_signer_seeds: &[&[_]] = &[ + TRANSIENT_STAKE_SEED_PREFIX, + &vote_account_address.to_bytes(), + &stake_pool_info.key.to_bytes(), + &source_transient_stake_seed.to_le_bytes(), + &[source_transient_stake_bump_seed], + ]; + + create_stake_account( + source_transient_stake_account_info.clone(), + source_transient_stake_account_signer_seeds, + system_program_info.clone(), + )?; + + Self::stake_split( + stake_pool_info.key, + source_validator_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + lamports, + source_transient_stake_account_info.clone(), + )?; + } + + // redelegate from source transient to ephemeral + { + let ephemeral_stake_bump_seed = check_ephemeral_stake_address( + program_id, + stake_pool_info.key, + ephemeral_stake_account_info.key, + ephemeral_stake_seed, + )?; + let ephemeral_stake_account_signer_seeds: &[&[_]] = &[ + EPHEMERAL_STAKE_SEED_PREFIX, + &stake_pool_info.key.to_bytes(), + &ephemeral_stake_seed.to_le_bytes(), + &[ephemeral_stake_bump_seed], + ]; + create_stake_account( + ephemeral_stake_account_info.clone(), + ephemeral_stake_account_signer_seeds, + system_program_info.clone(), + )?; + Self::stake_redelegate( + stake_pool_info.key, + source_transient_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + ephemeral_stake_account_info.clone(), + validator_vote_account_info.clone(), + stake_config_info.clone(), + )?; + } + + { + // check destination stake and transient stake accounts + let vote_account_address = validator_vote_account_info.key; + let maybe_validator_stake_info = + validator_list.find_mut::(|x| { + ValidatorStakeInfo::memcmp_pubkey(x, vote_account_address) + }); + if maybe_validator_stake_info.is_none() { + msg!( + "Destination vote account {} not found in stake pool", + vote_account_address + ); + return Err(StakePoolError::ValidatorNotFound.into()); + } + let mut validator_stake_info = maybe_validator_stake_info.unwrap(); + check_validator_stake_account( + destination_validator_stake_account_info, + program_id, + stake_pool_info.key, + withdraw_authority_info.key, + vote_account_address, + validator_stake_info.validator_seed_suffix, + &stake_pool.lockup, + )?; + if validator_stake_info.status != StakeStatus::Active { + msg!( + "Destination validator is marked for removal and no longer allows redelegation" + ); + return Err(StakePoolError::ValidatorNotFound.into()); + } + let transient_account_exists = validator_stake_info.transient_stake_lamports > 0; + validator_stake_info.transient_stake_lamports = validator_stake_info + .transient_stake_lamports + .checked_add(destination_transient_lamports) + .ok_or(StakePoolError::CalculationFailure)?; + + if transient_account_exists { + // if transient stake exists, make sure it's the right one and that it's + // usable by the pool + if validator_stake_info.transient_seed_suffix != destination_transient_stake_seed { + msg!("Provided seed {} does not match current seed {} for transient stake account", + destination_transient_stake_seed, + validator_stake_info.transient_seed_suffix + ); + return Err(StakePoolError::InvalidStakeAccountAddress.into()); + } + check_transient_stake_account( + destination_transient_stake_account_info, + program_id, + stake_pool_info.key, + withdraw_authority_info.key, + vote_account_address, + destination_transient_stake_seed, + &stake_pool.lockup, + )?; + Self::stake_merge( + stake_pool_info.key, + ephemeral_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + destination_transient_stake_account_info.clone(), + clock_info.clone(), + stake_history_info.clone(), + stake_program_info.clone(), + )?; + } else { + // otherwise, create the new account and split into it + let destination_transient_stake_bump_seed = check_transient_stake_address( + program_id, + stake_pool_info.key, + destination_transient_stake_account_info.key, + vote_account_address, + destination_transient_stake_seed, + )?; + let destination_transient_stake_account_signer_seeds: &[&[_]] = &[ + TRANSIENT_STAKE_SEED_PREFIX, + &vote_account_address.to_bytes(), + &stake_pool_info.key.to_bytes(), + &destination_transient_stake_seed.to_le_bytes(), + &[destination_transient_stake_bump_seed], + ]; + create_stake_account( + destination_transient_stake_account_info.clone(), + destination_transient_stake_account_signer_seeds, + system_program_info.clone(), + )?; + Self::stake_split( + stake_pool_info.key, + ephemeral_stake_account_info.clone(), + withdraw_authority_info.clone(), + AUTHORITY_WITHDRAW, + stake_pool.stake_withdraw_bump_seed, + destination_transient_lamports, + destination_transient_stake_account_info.clone(), + )?; + validator_stake_info.transient_seed_suffix = destination_transient_stake_seed; + } + } + + Ok(()) + } + /// Process `SetPreferredValidator` instruction #[inline(never)] // needed due to stack size violation fn process_set_preferred_validator( @@ -3462,6 +3878,22 @@ impl Processor { msg!("Instruction: UpdateTokenMetadata"); Self::process_update_pool_token_metadata(program_id, accounts, name, symbol, uri) } + StakePoolInstruction::Redelegate { + lamports, + source_transient_stake_seed, + ephemeral_stake_seed, + destination_transient_stake_seed, + } => { + msg!("Instruction: Redelegate"); + Self::process_redelegate( + program_id, + accounts, + lamports, + source_transient_stake_seed, + ephemeral_stake_seed, + destination_transient_stake_seed, + ) + } } } } diff --git a/stake-pool/program/tests/decrease.rs b/stake-pool/program/tests/decrease.rs index 7d263597635..b803344017e 100644 --- a/stake-pool/program/tests/decrease.rs +++ b/stake-pool/program/tests/decrease.rs @@ -235,6 +235,7 @@ async fn fail_with_unknown_validator() { &context.payer, &context.last_blockhash, &stake_pool_accounts.stake_pool.pubkey(), + 0, ) .await; diff --git a/stake-pool/program/tests/deposit.rs b/stake-pool/program/tests/deposit.rs index 0ccea7bd601..585f43ffcb1 100644 --- a/stake-pool/program/tests/deposit.rs +++ b/stake-pool/program/tests/deposit.rs @@ -626,6 +626,7 @@ async fn fail_with_unknown_validator() { &payer, &recent_blockhash, &stake_pool_accounts.stake_pool.pubkey(), + 0, ) .await; diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 07e9ff617d3..4a6842331c2 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -76,8 +76,8 @@ pub async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> Sol banks_client .get_account(*pubkey) .await + .expect("client error") .expect("account not found") - .expect("account empty") } #[allow(clippy::too_many_arguments)] @@ -754,6 +754,7 @@ pub async fn create_unknown_validator_stake( payer: &Keypair, recent_blockhash: &Hash, stake_pool: &Pubkey, + lamports: u64, ) -> ValidatorStakeAccount { let mut unknown_stake = ValidatorStakeAccount::new(stake_pool, NonZeroU32::new(1), 222); create_vote( @@ -779,7 +780,7 @@ pub async fn create_unknown_validator_stake( withdrawer: user.pubkey(), }, &stake::state::Lockup::default(), - current_minimum_delegation, + current_minimum_delegation + lamports, ) .await; delegate_stake_account( @@ -1679,6 +1680,53 @@ impl StakePoolAccounts { } } + #[allow(clippy::too_many_arguments)] + pub async fn redelegate( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + source_validator_stake: &Pubkey, + source_transient_stake: &Pubkey, + ephemeral_stake: &Pubkey, + destination_transient_stake: &Pubkey, + destination_validator_stake: &Pubkey, + validator: &Pubkey, + lamports: u64, + source_transient_stake_seed: u64, + ephemeral_stake_seed: u64, + destination_transient_stake_seed: u64, + ) -> Option { + let transaction = Transaction::new_signed_with_payer( + &[instruction::redelegate( + &id(), + &self.stake_pool.pubkey(), + &self.staker.pubkey(), + &self.withdraw_authority, + &self.validator_list.pubkey(), + source_validator_stake, + source_transient_stake, + ephemeral_stake, + destination_transient_stake, + destination_validator_stake, + validator, + lamports, + source_transient_stake_seed, + ephemeral_stake_seed, + destination_transient_stake_seed, + )], + Some(&payer.pubkey()), + &[payer, &self.staker], + *recent_blockhash, + ); + #[allow(clippy::useless_conversion)] // Remove during upgrade to 1.10 + banks_client + .process_transaction(transaction) + .await + .map_err(|e| e.into()) + .err() + } + pub async fn set_preferred_validator( &self, banks_client: &mut BanksClient, diff --git a/stake-pool/program/tests/increase.rs b/stake-pool/program/tests/increase.rs index 7967907c956..647525d71a7 100644 --- a/stake-pool/program/tests/increase.rs +++ b/stake-pool/program/tests/increase.rs @@ -238,6 +238,7 @@ async fn fail_with_unknown_validator() { &context.payer, &context.last_blockhash, &stake_pool_accounts.stake_pool.pubkey(), + 0, ) .await; diff --git a/stake-pool/program/tests/redelegate.rs b/stake-pool/program/tests/redelegate.rs new file mode 100644 index 00000000000..7bc6c948269 --- /dev/null +++ b/stake-pool/program/tests/redelegate.rs @@ -0,0 +1,1129 @@ +#![allow(clippy::integer_arithmetic)] +#![cfg(feature = "test-sbf")] + +mod helpers; + +use { + bincode::deserialize, + helpers::*, + solana_program::{ + clock::Epoch, hash::Hash, instruction::InstructionError, pubkey::Pubkey, stake, + }, + solana_program_test::*, + solana_sdk::{ + signature::{Keypair, Signer}, + stake::instruction::StakeError, + transaction::{Transaction, TransactionError}, + }, + spl_stake_pool::{ + error::StakePoolError, find_ephemeral_stake_program_address, + find_transient_stake_program_address, id, instruction, MINIMUM_RESERVE_LAMPORTS, + }, +}; + +async fn setup( + do_warp: bool, +) -> ( + ProgramTestContext, + Hash, + StakePoolAccounts, + ValidatorStakeAccount, + ValidatorStakeAccount, + u64, + u64, +) { + let mut context = program_test().start_with_context().await; + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + + let stake_pool_accounts = StakePoolAccounts::default(); + stake_pool_accounts + .initialize_stake_pool( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + MINIMUM_RESERVE_LAMPORTS + current_minimum_delegation + stake_rent, + ) + .await + .unwrap(); + + let source_validator_stake = simple_add_validator_to_pool( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts, + None, + ) + .await; + + let destination_validator_stake = simple_add_validator_to_pool( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts, + None, + ) + .await; + + let minimum_redelegate_lamports = current_minimum_delegation + stake_rent * 2; + simple_deposit_stake( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &stake_pool_accounts, + &source_validator_stake, + minimum_redelegate_lamports, + ) + .await + .unwrap(); + + let mut slot = 0; + if do_warp { + slot = context.genesis_config().epoch_schedule.first_normal_slot; + context.warp_to_slot(slot).unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &[ + source_validator_stake.vote.pubkey(), + destination_validator_stake.vote.pubkey(), + ], + false, + ) + .await; + } + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + + ( + context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + minimum_redelegate_lamports, + slot, + ) +} + +#[tokio::test] +async fn success() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + mut slot, + ) = setup(true).await; + + // Save validator stake + let pre_validator_stake_account = get_account( + &mut context.banks_client, + &source_validator_stake.stake_account, + ) + .await; + + // Save validator stake + let pre_destination_validator_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.stake_account, + ) + .await; + + // Check no transient stake + let transient_account = context + .banks_client + .get_account(source_validator_stake.transient_stake_account) + .await + .unwrap(); + assert!(transient_account.is_none()); + + let ephemeral_stake_seed = 100; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + // Check validator stake account balance + let validator_stake_account = get_account( + &mut context.banks_client, + &source_validator_stake.stake_account, + ) + .await; + let validator_stake_state = + deserialize::(&validator_stake_account.data).unwrap(); + assert_eq!( + pre_validator_stake_account.lamports - redelegate_lamports, + validator_stake_account.lamports + ); + assert_eq!( + validator_stake_state + .delegation() + .unwrap() + .deactivation_epoch, + Epoch::MAX + ); + + // Check source 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 source_transient_stake_account = get_account( + &mut context.banks_client, + &source_validator_stake.transient_stake_account, + ) + .await; + let transient_stake_state = + deserialize::(&source_transient_stake_account.data).unwrap(); + assert_eq!(source_transient_stake_account.lamports, stake_rent); + let transient_delegation = transient_stake_state.delegation().unwrap(); + assert_ne!(transient_delegation.deactivation_epoch, Epoch::MAX); + assert_eq!(transient_delegation.stake, redelegate_lamports - stake_rent); + + // Check ephemeral account doesn't exist + let maybe_account = context + .banks_client + .get_account(ephemeral_stake) + .await + .unwrap(); + assert!(maybe_account.is_none()); + + // Check destination transient stake account + let destination_transient_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.transient_stake_account, + ) + .await; + let transient_stake_state = + deserialize::(&destination_transient_stake_account.data).unwrap(); + assert_eq!( + destination_transient_stake_account.lamports, + redelegate_lamports - stake_rent + ); + let transient_delegation = transient_stake_state.delegation().unwrap(); + assert_eq!(transient_delegation.deactivation_epoch, Epoch::MAX); + assert_ne!(transient_delegation.activation_epoch, Epoch::MAX); + assert_eq!( + transient_delegation.stake, + redelegate_lamports - stake_rent * 2 + ); + + // Check validator list + let validator_list = stake_pool_accounts + .get_validator_list(&mut context.banks_client) + .await; + let source_item = validator_list + .find(&source_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!( + source_item.active_stake_lamports, + validator_stake_account.lamports + ); + assert_eq!( + source_item.transient_stake_lamports, + source_transient_stake_account.lamports + ); + assert_eq!( + source_item.transient_seed_suffix, + source_validator_stake.transient_stake_seed + ); + + let destination_item = validator_list + .find(&destination_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!( + destination_item.transient_stake_lamports, + destination_transient_stake_account.lamports + ); + assert_eq!( + destination_item.transient_seed_suffix, + destination_validator_stake.transient_stake_seed + ); + + // Warp forward and merge all + let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; + slot += slots_per_epoch; + context.warp_to_slot(slot).unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[ + source_validator_stake.vote.pubkey(), + destination_validator_stake.vote.pubkey(), + ], + false, + ) + .await; + + // Check transient accounts are gone + let maybe_account = context + .banks_client + .get_account(destination_validator_stake.transient_stake_account) + .await + .unwrap(); + assert!(maybe_account.is_none()); + let maybe_account = context + .banks_client + .get_account(source_validator_stake.transient_stake_account) + .await + .unwrap(); + assert!(maybe_account.is_none()); + + // Check validator list + let validator_list = stake_pool_accounts + .get_validator_list(&mut context.banks_client) + .await; + let source_item = validator_list + .find(&source_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!( + source_item.active_stake_lamports, + validator_stake_account.lamports + ); + assert_eq!(source_item.transient_stake_lamports, 0); + + let destination_item = validator_list + .find(&destination_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!(destination_item.transient_stake_lamports, 0); + assert_eq!( + destination_item.active_stake_lamports, + pre_destination_validator_stake_account.lamports + redelegate_lamports - stake_rent * 2 + ); + let post_destination_validator_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.stake_account, + ) + .await; + assert_eq!( + post_destination_validator_stake_account.lamports, + pre_destination_validator_stake_account.lamports + redelegate_lamports - stake_rent * 2 + ); +} + +#[tokio::test] +async fn success_with_increasing_stake() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + mut slot, + ) = setup(true).await; + + // Save validator stake + let pre_validator_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.stake_account, + ) + .await; + + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &last_blockhash, + ) + .await; + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + + let error = stake_pool_accounts + .increase_validator_stake( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + current_minimum_delegation, + destination_validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + let validator_list = stake_pool_accounts + .get_validator_list(&mut context.banks_client) + .await; + let destination_item = validator_list + .find(&destination_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!( + destination_item.transient_stake_lamports, + current_minimum_delegation + stake_rent + ); + let pre_transient_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.transient_stake_account, + ) + .await; + assert_eq!( + pre_transient_stake_account.lamports, + current_minimum_delegation + stake_rent + ); + + let ephemeral_stake_seed = 10; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + // fail with incorrect transient stake derivation + let wrong_transient_stake_seed = destination_validator_stake + .transient_stake_seed + .wrapping_add(1); + let (wrong_transient_stake_account, _) = find_transient_stake_program_address( + &id(), + &destination_validator_stake.vote.pubkey(), + &stake_pool_accounts.stake_pool.pubkey(), + wrong_transient_stake_seed, + ); + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &wrong_transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + wrong_transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::InvalidStakeAccountAddress as u32) + ) + ); + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + // Check destination transient stake account + let destination_transient_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.transient_stake_account, + ) + .await; + let transient_stake_state = + deserialize::(&destination_transient_stake_account.data).unwrap(); + // stake rent cancels out + assert_eq!( + destination_transient_stake_account.lamports, + redelegate_lamports + current_minimum_delegation + ); + + let transient_delegation = transient_stake_state.delegation().unwrap(); + assert_eq!(transient_delegation.deactivation_epoch, Epoch::MAX); + assert_ne!(transient_delegation.activation_epoch, Epoch::MAX); + assert_eq!( + transient_delegation.stake, + redelegate_lamports + current_minimum_delegation - stake_rent + ); + + // Check validator list + let validator_list = stake_pool_accounts + .get_validator_list(&mut context.banks_client) + .await; + let destination_item = validator_list + .find(&destination_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!( + destination_item.transient_stake_lamports, + destination_transient_stake_account.lamports + ); + assert_eq!( + destination_item.transient_seed_suffix, + destination_validator_stake.transient_stake_seed + ); + + // Warp forward and merge all + let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; + slot += slots_per_epoch; + context.warp_to_slot(slot).unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[ + source_validator_stake.vote.pubkey(), + destination_validator_stake.vote.pubkey(), + ], + false, + ) + .await; + + // Check transient account is gone + let maybe_account = context + .banks_client + .get_account(destination_validator_stake.transient_stake_account) + .await + .unwrap(); + assert!(maybe_account.is_none()); + + // Check validator list + let validator_list = stake_pool_accounts + .get_validator_list(&mut context.banks_client) + .await; + let destination_item = validator_list + .find(&destination_validator_stake.vote.pubkey()) + .unwrap(); + assert_eq!(destination_item.transient_stake_lamports, 0); + // redelegate is smart enough to activate *everything*, so there's only one rent-exemption + // worth of inactive stake! + assert_eq!( + destination_item.active_stake_lamports, + pre_validator_stake_account.lamports + redelegate_lamports + current_minimum_delegation + - stake_rent + ); + let post_validator_stake_account = get_account( + &mut context.banks_client, + &destination_validator_stake.stake_account, + ) + .await; + assert_eq!( + post_validator_stake_account.lamports, + pre_validator_stake_account.lamports + redelegate_lamports + current_minimum_delegation + - stake_rent + ); +} + +#[tokio::test] +async fn fail_with_decreasing_stake() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + mut slot, + ) = setup(false).await; + + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &last_blockhash, + ) + .await; + let rent = context.banks_client.get_rent().await.unwrap(); + let stake_rent = rent.minimum_balance(std::mem::size_of::()); + let minimum_decrease_lamports = current_minimum_delegation + stake_rent; + + simple_deposit_stake( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &stake_pool_accounts, + &destination_validator_stake, + redelegate_lamports, + ) + .await + .unwrap(); + + slot += context.genesis_config().epoch_schedule.first_normal_slot; + context.warp_to_slot(slot).unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[ + source_validator_stake.vote.pubkey(), + destination_validator_stake.vote.pubkey(), + ], + false, + ) + .await; + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&last_blockhash) + .await + .unwrap(); + + let error = stake_pool_accounts + .decrease_validator_stake( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &destination_validator_stake.stake_account, + &destination_validator_stake.transient_stake_account, + minimum_decrease_lamports, + destination_validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + let ephemeral_stake_seed = 20; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakeError::MergeTransientStake as u32) + ) + ); +} + +#[tokio::test] +async fn fail_with_wrong_withdraw_authority() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + _, + ) = setup(true).await; + + let ephemeral_stake_seed = 2; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + let wrong_withdraw_authority = Pubkey::new_unique(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::redelegate( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.staker.pubkey(), + &wrong_withdraw_authority, + &stake_pool_accounts.validator_list.pubkey(), + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.staker], + last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::InvalidProgramAddress as u32) + ) + ); +} + +#[tokio::test] +async fn fail_with_wrong_validator_list() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + _, + ) = setup(true).await; + + let ephemeral_stake_seed = 2; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + let wrong_validator_list = Pubkey::new_unique(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::redelegate( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.staker.pubkey(), + &stake_pool_accounts.withdraw_authority, + &wrong_validator_list, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.staker], + last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::InvalidValidatorStakeList as u32) + ) + ); +} + +#[tokio::test] +async fn fail_with_wrong_staker() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + _, + ) = setup(true).await; + + let ephemeral_stake_seed = 2; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + let wrong_staker = Keypair::new(); + let transaction = Transaction::new_signed_with_payer( + &[instruction::redelegate( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &wrong_staker.pubkey(), + &stake_pool_accounts.withdraw_authority, + &stake_pool_accounts.validator_list.pubkey(), + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + )], + Some(&context.payer.pubkey()), + &[&context.payer, &wrong_staker], + last_blockhash, + ); + let error = context + .banks_client + .process_transaction(transaction) + .await + .err() + .unwrap() + .unwrap(); + + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::WrongStaker as u32) + ) + ); +} + +#[tokio::test] +async fn fail_with_unknown_validator() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + _, + ) = setup(true).await; + + let unknown_validator_stake = create_unknown_validator_stake( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &stake_pool_accounts.stake_pool.pubkey(), + redelegate_lamports, + ) + .await; + + let ephemeral_stake_seed = 42; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &unknown_validator_stake.transient_stake_account, + &unknown_validator_stake.stake_account, + &unknown_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + unknown_validator_stake.transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::ValidatorNotFound as u32) + ) + ); + + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &unknown_validator_stake.stake_account, + &unknown_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + unknown_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::ValidatorNotFound as u32) + ) + ); +} + +#[tokio::test] +async fn fail_redelegate_twice() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + mut slot, + ) = setup(false).await; + + simple_deposit_stake( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &stake_pool_accounts, + &source_validator_stake, + redelegate_lamports, + ) + .await + .unwrap(); + + slot += context.genesis_config().epoch_schedule.first_normal_slot; + context.warp_to_slot(slot).unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[ + source_validator_stake.vote.pubkey(), + destination_validator_stake.vote.pubkey(), + ], + false, + ) + .await; + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&last_blockhash) + .await + .unwrap(); + + let ephemeral_stake_seed = 100; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await; + assert!(error.is_none()); + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&last_blockhash) + .await + .unwrap(); + + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakePoolError::TransientAccountInUse as u32) + ) + ); +} + +#[tokio::test] +async fn fail_with_small_lamport_amount() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + redelegate_lamports, + _, + ) = setup(true).await; + + let ephemeral_stake_seed = 7_000; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + redelegate_lamports - 1, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError( + 0, + InstructionError::Custom(StakeError::InsufficientDelegation as u32) + ) + ); +} + +#[tokio::test] +async fn fail_drain_source_account() { + let ( + mut context, + last_blockhash, + stake_pool_accounts, + source_validator_stake, + destination_validator_stake, + _, + _, + ) = setup(true).await; + + let validator_stake_account = get_account( + &mut context.banks_client, + &source_validator_stake.stake_account, + ) + .await; + + let ephemeral_stake_seed = 2; + let ephemeral_stake = find_ephemeral_stake_program_address( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + ephemeral_stake_seed, + ) + .0; + + let error = stake_pool_accounts + .redelegate( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &source_validator_stake.stake_account, + &source_validator_stake.transient_stake_account, + &ephemeral_stake, + &destination_validator_stake.transient_stake_account, + &destination_validator_stake.stake_account, + &destination_validator_stake.vote.pubkey(), + validator_stake_account.lamports, + source_validator_stake.transient_stake_seed, + ephemeral_stake_seed, + destination_validator_stake.transient_stake_seed, + ) + .await + .unwrap() + .unwrap(); + assert_eq!( + error, + TransactionError::InstructionError(0, InstructionError::InsufficientFunds) + ); +} diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs index 393e6504fd2..2c426af077a 100644 --- a/stake-pool/program/tests/withdraw.rs +++ b/stake-pool/program/tests/withdraw.rs @@ -472,6 +472,7 @@ async fn fail_with_unknown_validator() { &context.payer, &context.last_blockhash, &stake_pool_accounts.stake_pool.pubkey(), + 0, ) .await; diff --git a/stake-pool/program/tests/withdraw_edge_cases.rs b/stake-pool/program/tests/withdraw_edge_cases.rs index 84e9390f37c..b1abb9cef27 100644 --- a/stake-pool/program/tests/withdraw_edge_cases.rs +++ b/stake-pool/program/tests/withdraw_edge_cases.rs @@ -486,10 +486,16 @@ async fn success_and_fail_with_preferred_withdraw() { tokens_to_burn, ) = setup_for_withdraw(spl_token::id()).await; + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + let preferred_validator = simple_add_validator_to_pool( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, &stake_pool_accounts, None, ) @@ -499,7 +505,7 @@ async fn success_and_fail_with_preferred_withdraw() { .set_preferred_validator( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, instruction::PreferredValidatorType::Withdraw, Some(preferred_validator.vote.pubkey()), ) @@ -508,7 +514,7 @@ async fn success_and_fail_with_preferred_withdraw() { let _preferred_deposit = simple_deposit_stake( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, &stake_pool_accounts, &preferred_validator, TEST_STAKE_AMOUNT, @@ -521,7 +527,7 @@ async fn success_and_fail_with_preferred_withdraw() { .withdraw_stake( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, &user_stake_recipient.pubkey(), &user_transfer_authority, &deposit_info.pool_account.pubkey(), @@ -540,13 +546,19 @@ async fn success_and_fail_with_preferred_withdraw() { ) ); + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&last_blockhash) + .await + .unwrap(); + // success from preferred let new_authority = Pubkey::new_unique(); let error = stake_pool_accounts .withdraw_stake( &mut context.banks_client, &context.payer, - &context.last_blockhash, + &last_blockhash, &user_stake_recipient.pubkey(), &user_transfer_authority, &deposit_info.pool_account.pubkey(),