From 4d75735d75f171f144d0e9ee3593c5e558d8ad6b Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Sat, 27 Aug 2022 00:35:26 +0200 Subject: [PATCH 1/6] stake-pool: Support dynamic minimum delegation amount --- stake-pool/program/src/instruction.rs | 14 ++-- stake-pool/program/src/lib.rs | 17 +++-- stake-pool/program/src/processor.rs | 69 +++++++++++++++---- stake-pool/program/src/state.rs | 4 +- stake-pool/program/tests/decrease.rs | 9 ++- stake-pool/program/tests/deposit.rs | 16 ++++- stake-pool/program/tests/helpers/mod.rs | 48 +++++++++++-- stake-pool/program/tests/huge_pool.rs | 7 +- stake-pool/program/tests/increase.rs | 12 +++- .../tests/update_validator_list_balance.rs | 10 ++- stake-pool/program/tests/vsa_remove.rs | 12 +++- stake-pool/program/tests/withdraw.rs | 42 ++++++++--- 12 files changed, 203 insertions(+), 57 deletions(-) diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 04353fbbff9..501fa29fe2a 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -83,7 +83,7 @@ pub enum StakePoolInstruction { /// list of managed validators. /// /// The stake account will have the rent-exempt amount plus - /// `crate::MINIMUM_ACTIVE_STAKE` (currently 0.001 SOL). + /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)` /// /// 0. `[w]` Stake pool /// 1. `[s]` Staker @@ -103,8 +103,8 @@ pub enum StakePoolInstruction { /// (Staker only) Removes validator from the pool /// /// Only succeeds if the validator stake account has the minimum of - /// `crate::MINIMUM_ACTIVE_STAKE` (currently 0.001 SOL) plus the rent-exempt - /// amount. + /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)` + /// plus the rent-exempt amount. /// /// 0. `[w]` Stake pool /// 1. `[s]` Staker @@ -158,9 +158,8 @@ pub enum StakePoolInstruction { /// will do the work of merging once it's ready. /// /// This instruction only succeeds if the transient stake account does not exist. - /// The minimum amount to move is rent-exemption plus `crate::MINIMUM_ACTIVE_STAKE` - /// (currently 0.001 SOL) in order to avoid issues on credits observed when - /// merging active stakes later. + /// The minimum amount to move is rent-exemption plus + /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)`. /// /// 0. `[]` Stake pool /// 1. `[s]` Stake pool staker @@ -281,7 +280,8 @@ pub enum StakePoolInstruction { /// /// Succeeds if the stake account has enough SOL to cover the desired amount /// of pool tokens, and if the withdrawal keeps the total staked amount - /// above the minimum of rent-exempt amount + 0.001 SOL. + /// above the minimum of rent-exempt amount + + /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)` /// /// When allowing withdrawals, the order of priority goes: /// diff --git a/stake-pool/program/src/lib.rs b/stake-pool/program/src/lib.rs index aa6931ddf2f..5437dd92cff 100644 --- a/stake-pool/program/src/lib.rs +++ b/stake-pool/program/src/lib.rs @@ -15,7 +15,7 @@ pub mod entrypoint; pub use solana_program; use { crate::state::Fee, - solana_program::{native_token::LAMPORTS_PER_SOL, pubkey::Pubkey, stake::state::Meta}, + solana_program::{pubkey::Pubkey, stake::state::Meta}, }; /// Seed for deposit authority seed @@ -29,10 +29,12 @@ const TRANSIENT_STAKE_SEED_PREFIX: &[u8] = b"transient"; /// Minimum amount of staked SOL required in a validator stake account to allow /// for merges without a mismatch on credits observed -pub const MINIMUM_ACTIVE_STAKE: u64 = LAMPORTS_PER_SOL; +pub const MINIMUM_ACTIVE_STAKE: u64 = 1_000_000; /// Minimum amount of SOL in the reserve -pub const MINIMUM_RESERVE_LAMPORTS: u64 = LAMPORTS_PER_SOL; +/// NOTE: This can be changed to 0 once the `stake_allow_zero_undelegated_amount` +/// feature is enabled on all clusters +pub const MINIMUM_RESERVE_LAMPORTS: u64 = 1; /// Maximum amount of validator stake accounts to update per /// `UpdateValidatorListBalance` instruction, based on compute limits @@ -58,9 +60,14 @@ pub const MAX_TRANSIENT_STAKE_ACCOUNTS: usize = 10; /// Get the stake amount under consideration when calculating pool token /// conversions #[inline] -pub fn minimum_stake_lamports(meta: &Meta) -> u64 { +pub fn minimum_stake_lamports(meta: &Meta, stake_program_minimum_delegation: u64) -> u64 { meta.rent_exempt_reserve - .saturating_add(MINIMUM_ACTIVE_STAKE) + .saturating_add(minimum_delegation(stake_program_minimum_delegation)) +} + +/// Get the minimum delegation required by a stake account in a stake pool +pub fn minimum_delegation(stake_program_minimum_delegation: u64) -> u64 { + std::cmp::max(stake_program_minimum_delegation, MINIMUM_ACTIVE_STAKE) } /// Get the stake amount under consideration when calculating pool token diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index d26c4efbae5..033e126ca12 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -5,12 +5,12 @@ use { error::StakePoolError, find_deposit_authority_program_address, instruction::{FundingType, PreferredValidatorType, StakePoolInstruction}, - minimum_reserve_lamports, minimum_stake_lamports, + minimum_delegation, minimum_reserve_lamports, minimum_stake_lamports, state::{ AccountType, Fee, FeeType, StakePool, StakeStatus, ValidatorList, ValidatorListHeader, ValidatorStakeInfo, }, - AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, MINIMUM_ACTIVE_STAKE, TRANSIENT_STAKE_SEED_PREFIX, + AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, TRANSIENT_STAKE_SEED_PREFIX, }, borsh::{BorshDeserialize, BorshSerialize}, mpl_token_metadata::{ @@ -26,7 +26,7 @@ use { decode_error::DecodeError, entrypoint::ProgramResult, msg, - program::{invoke, invoke_signed}, + program::{get_return_data, invoke, invoke_signed}, program_error::{PrintProgramError, ProgramError}, program_pack::Pack, pubkey::Pubkey, @@ -35,6 +35,7 @@ use { sysvar::Sysvar, }, spl_token::state::Mint, + std::convert::TryInto, }; /// Deserialize the stake state from AccountInfo @@ -514,6 +515,26 @@ impl Processor { ) } + /// Issue stake::instruction::get_minimum_delegation instruction to get the + /// minimum stake delegation + fn stake_program_minimum_delegation( + stake_program_info: AccountInfo, + ) -> Result { + let instruction = stake::instruction::get_minimum_delegation(); + let stake_program_id = stake_program_info.key; + invoke(&instruction, &[stake_program_info])?; + get_return_data() + .ok_or(ProgramError::InvalidInstructionData) + .and_then(|(key, data)| { + if key != *stake_program_id { + return Err(ProgramError::IncorrectProgramId); + } + data.try_into() + .map(u64::from_le_bytes) + .map_err(|_| ProgramError::InvalidInstructionData) + }) + } + /// Issue a spl_token `Burn` instruction. #[allow(clippy::too_many_arguments)] fn token_burn<'a>( @@ -907,7 +928,10 @@ impl Processor { // Fund the stake account with the minimum + rent-exempt balance let space = std::mem::size_of::(); - let required_lamports = MINIMUM_ACTIVE_STAKE + rent.minimum_balance(space); + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let required_lamports = + minimum_delegation(stake_minimum_delegation) + rent.minimum_balance(space); // Create new stake account create_pda_account( @@ -1031,7 +1055,9 @@ impl Processor { let mut validator_stake_info = maybe_validator_stake_info.unwrap(); let stake_lamports = **stake_account_info.lamports.borrow(); - let required_lamports = minimum_stake_lamports(&meta); + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); if stake_lamports != required_lamports { msg!( "Attempting to remove validator account with {} lamports, must have {} lamports", @@ -1041,11 +1067,12 @@ impl Processor { return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); } - if stake.delegation.stake != MINIMUM_ACTIVE_STAKE { + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); + if stake.delegation.stake != current_minimum_delegation { msg!( "Error: attempting to remove stake with delegation of {} lamports, must have {} lamports", stake.delegation.stake, - MINIMUM_ACTIVE_STAKE + current_minimum_delegation ); return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); } @@ -1224,7 +1251,9 @@ impl Processor { .lamports() .checked_sub(lamports) .ok_or(ProgramError::InsufficientFunds)?; - let required_lamports = minimum_stake_lamports(&meta); + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); if remaining_lamports < required_lamports { msg!("Need at least {} lamports in the stake account after decrease, {} requested, {} is the current possible maximum", required_lamports, @@ -1394,10 +1423,13 @@ impl Processor { } let stake_rent = rent.minimum_balance(std::mem::size_of::()); - if lamports < MINIMUM_ACTIVE_STAKE { + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); + if lamports < current_minimum_delegation { msg!( "Need more than {} lamports for transient stake to be rent-exempt and mergeable, {} provided", - MINIMUM_ACTIVE_STAKE, + current_minimum_delegation, lamports ); return Err(ProgramError::AccountNotRentExempt); @@ -1577,6 +1609,9 @@ impl Processor { return Err(StakePoolError::InvalidState.into()); } + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); let validator_iter = &mut validator_slice .iter_mut() .zip(validator_stake_accounts.chunks_exact(2)); @@ -1750,7 +1785,7 @@ impl Processor { active_stake_lamports = stake .delegation .stake - .checked_sub(MINIMUM_ACTIVE_STAKE) + .checked_sub(current_minimum_delegation) .ok_or(StakePoolError::CalculationFailure)?; } else { msg!("Validator stake account no longer part of the pool, ignoring"); @@ -2195,10 +2230,13 @@ impl Processor { .ok_or(StakePoolError::CalculationFailure)?; stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?; + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); validator_stake_info.active_stake_lamports = post_validator_stake .delegation .stake - .checked_sub(MINIMUM_ACTIVE_STAKE) + .checked_sub(current_minimum_delegation) .ok_or(StakePoolError::CalculationFailure)?; Ok(()) @@ -2508,8 +2546,11 @@ impl Processor { } let remaining_lamports = stake.delegation.stake.saturating_sub(withdraw_lamports); - if remaining_lamports < MINIMUM_ACTIVE_STAKE { - msg!("Attempting to withdraw {} lamports from validator account with {} stake lamports, {} must remain", withdraw_lamports, stake.delegation.stake, MINIMUM_ACTIVE_STAKE); + let stake_minimum_delegation = + Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); + if remaining_lamports < current_minimum_delegation { + msg!("Attempting to withdraw {} lamports from validator account with {} stake lamports, {} must remain", withdraw_lamports, stake.delegation.stake, current_minimum_delegation); return Err(StakePoolError::StakeLamportsNotEqualToMinimum.into()); } Some((validator_stake_info, withdrawing_from_transient_stake)) diff --git a/stake-pool/program/src/state.rs b/stake-pool/program/src/state.rs index fc54676b3ba..1eaecdf7692 100644 --- a/stake-pool/program/src/state.rs +++ b/stake-pool/program/src/state.rs @@ -534,8 +534,8 @@ impl Default for StakeStatus { #[derive(Clone, Copy, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] pub struct ValidatorStakeInfo { /// Amount of active stake delegated to this validator, minus the minimum - /// required stake amount of rent-exemption + `crate::MINIMUM_ACTIVE_STAKE` - /// (currently 1 SOL). + /// required stake amount of rent-exemption + + /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)`. /// /// Note that if `last_update_epoch` does not match the current epoch then /// this field may not be accurate diff --git a/stake-pool/program/tests/decrease.rs b/stake-pool/program/tests/decrease.rs index a050b407bbc..11be6e7cc3e 100644 --- a/stake-pool/program/tests/decrease.rs +++ b/stake-pool/program/tests/decrease.rs @@ -15,7 +15,7 @@ use { }, spl_stake_pool::{ error::StakePoolError, find_transient_stake_program_address, id, instruction, - MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MINIMUM_RESERVE_LAMPORTS, }, }; @@ -50,18 +50,21 @@ async fn setup() -> ( ) .await; + let current_minimum_delegation = + stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; + let deposit_info = simple_deposit_stake( &mut banks_client, &payer, &recent_blockhash, &stake_pool_accounts, &validator_stake_account, - MINIMUM_ACTIVE_STAKE * 2 + stake_rent, + current_minimum_delegation * 2 + stake_rent, ) .await .unwrap(); - let decrease_lamports = MINIMUM_ACTIVE_STAKE + stake_rent; + let decrease_lamports = current_minimum_delegation + stake_rent; ( banks_client, diff --git a/stake-pool/program/tests/deposit.rs b/stake-pool/program/tests/deposit.rs index 4a586550e6e..7f3ad276a9f 100644 --- a/stake-pool/program/tests/deposit.rs +++ b/stake-pool/program/tests/deposit.rs @@ -248,8 +248,14 @@ async fn success() { let stake_state = deserialize::(&validator_stake_account.data).unwrap(); let meta = stake_state.meta().unwrap(); + let stake_minimum_delegation = stake_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; assert_eq!( - validator_stake_account.lamports - minimum_stake_lamports(&meta), + validator_stake_account.lamports - minimum_stake_lamports(&meta, stake_minimum_delegation), post_validator_stake_item.stake_lamports() ); assert_eq!(post_validator_stake_item.transient_stake_lamports, 0); @@ -443,8 +449,14 @@ async fn success_with_extra_stake_lamports() { let stake_state = deserialize::(&validator_stake_account.data).unwrap(); let meta = stake_state.meta().unwrap(); + let stake_minimum_delegation = stake_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; assert_eq!( - validator_stake_account.lamports - minimum_stake_lamports(&meta), + validator_stake_account.lamports - minimum_stake_lamports(&meta, stake_minimum_delegation), post_validator_stake_item.stake_lamports() ); assert_eq!(post_validator_stake_item.transient_stake_lamports, 0); diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 346483130d0..0bbeb1c4b63 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -15,6 +15,7 @@ use { solana_sdk::{ account::{Account, WritableAccount}, clock::{Clock, Epoch}, + native_token::LAMPORTS_PER_SOL, signature::{Keypair, Signer}, transaction::Transaction, transport::TransportError, @@ -26,11 +27,12 @@ use { spl_stake_pool::{ find_deposit_authority_program_address, find_stake_program_address, find_transient_stake_program_address, find_withdraw_authority_program_address, id, - instruction, + instruction, minimum_delegation, processor::Processor, state::{self, FeeType, ValidatorList}, - MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MINIMUM_RESERVE_LAMPORTS, }, + std::convert::TryInto, }; pub const TEST_STAKE_AMOUNT: u64 = 1_500_000_000; @@ -546,6 +548,39 @@ pub async fn delegate_stake_account( banks_client.process_transaction(transaction).await.unwrap(); } +pub async fn stake_get_minimum_delegation( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, +) -> u64 { + let transaction = Transaction::new_signed_with_payer( + &[stake::instruction::get_minimum_delegation()], + Some(&payer.pubkey()), + &[payer], + *recent_blockhash, + ); + let mut data = banks_client + .simulate_transaction(transaction) + .await + .unwrap() + .simulation_details + .unwrap() + .return_data + .unwrap() + .data; + data.resize(8, 0); + data.try_into().map(u64::from_le_bytes).unwrap() +} + +pub async fn stake_pool_get_minimum_delegation( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, +) -> u64 { + let stake_minimum = stake_get_minimum_delegation(banks_client, payer, recent_blockhash).await; + minimum_delegation(stake_minimum) +} + pub async fn authorize_stake_account( banks_client: &mut BanksClient, payer: &Keypair, @@ -586,6 +621,9 @@ pub async fn create_unknown_validator_stake( .await; let user = Keypair::new(); let fake_validator_stake = Keypair::new(); + let stake_minimum_delegation = + stake_get_minimum_delegation(banks_client, payer, recent_blockhash).await; + let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); create_independent_stake_account( banks_client, payer, @@ -596,7 +634,7 @@ pub async fn create_unknown_validator_stake( withdrawer: user.pubkey(), }, &stake::state::Lockup::default(), - MINIMUM_ACTIVE_STAKE, + current_minimum_delegation, ) .await; delegate_stake_account( @@ -1711,8 +1749,8 @@ pub fn add_validator_stake_account( let (stake_address, _) = find_stake_program_address(&id(), voter_pubkey, stake_pool_pubkey); program_test.add_account(stake_address, stake_account); - let active_stake_lamports = stake_amount - MINIMUM_ACTIVE_STAKE; - // add to validator list + let active_stake_lamports = stake_amount - LAMPORTS_PER_SOL; // hack + // add to validator list validator_list.validators.push(state::ValidatorStakeInfo { status: state::StakeStatus::Active, vote_account_address: *voter_pubkey, diff --git a/stake-pool/program/tests/huge_pool.rs b/stake-pool/program/tests/huge_pool.rs index b74186b248b..194a971d64c 100644 --- a/stake-pool/program/tests/huge_pool.rs +++ b/stake-pool/program/tests/huge_pool.rs @@ -7,6 +7,7 @@ use { solana_program::{borsh::try_from_slice_unchecked, pubkey::Pubkey, stake}, solana_program_test::*, solana_sdk::{ + native_token::LAMPORTS_PER_SOL, signature::{Keypair, Signer}, transaction::Transaction, }, @@ -14,7 +15,7 @@ use { find_stake_program_address, find_transient_stake_program_address, id, instruction::{self, PreferredValidatorType}, state::{StakePool, StakeStatus, ValidatorList}, - MAX_VALIDATORS_TO_UPDATE, MINIMUM_ACTIVE_STAKE, + MAX_VALIDATORS_TO_UPDATE, }, }; @@ -221,7 +222,7 @@ async fn update() { #[tokio::test] async fn remove_validator_from_pool() { let (mut context, stake_pool_accounts, vote_account_pubkeys, _, _, _, _) = - setup(HUGE_POOL_SIZE, HUGE_POOL_SIZE, MINIMUM_ACTIVE_STAKE).await; + setup(HUGE_POOL_SIZE, HUGE_POOL_SIZE, LAMPORTS_PER_SOL).await; let first_vote = vote_account_pubkeys[0]; let (stake_address, _) = @@ -427,7 +428,7 @@ async fn add_validator_to_pool() { &stake_pool_pubkey, transient_stake_seed, ); - let increase_amount = MINIMUM_ACTIVE_STAKE; + let increase_amount = LAMPORTS_PER_SOL; let error = stake_pool_accounts .increase_validator_stake( &mut context.banks_client, diff --git a/stake-pool/program/tests/increase.rs b/stake-pool/program/tests/increase.rs index 5f73615bddc..ffd28bc1dfc 100644 --- a/stake-pool/program/tests/increase.rs +++ b/stake-pool/program/tests/increase.rs @@ -15,7 +15,7 @@ use { }, spl_stake_pool::{ error::StakePoolError, find_transient_stake_program_address, id, instruction, - MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MINIMUM_RESERVE_LAMPORTS, }, }; @@ -48,13 +48,16 @@ async fn setup() -> ( ) .await; + let current_minimum_delegation = + stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; + let _deposit_info = simple_deposit_stake( &mut banks_client, &payer, &recent_blockhash, &stake_pool_accounts, &validator_stake_account, - MINIMUM_ACTIVE_STAKE, + current_minimum_delegation, ) .await .unwrap(); @@ -354,6 +357,9 @@ async fn fail_with_small_lamport_amount() { _reserve_lamports, ) = setup().await; + let current_minimum_delegation = + stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; + let error = stake_pool_accounts .increase_validator_stake( &mut banks_client, @@ -362,7 +368,7 @@ async fn fail_with_small_lamport_amount() { &validator_stake.transient_stake_account, &validator_stake.stake_account, &validator_stake.vote.pubkey(), - MINIMUM_ACTIVE_STAKE - 1, + current_minimum_delegation - 1, validator_stake.transient_stake_seed, ) .await diff --git a/stake-pool/program/tests/update_validator_list_balance.rs b/stake-pool/program/tests/update_validator_list_balance.rs index 75277c0e9ea..7ceb6483ccb 100644 --- a/stake-pool/program/tests/update_validator_list_balance.rs +++ b/stake-pool/program/tests/update_validator_list_balance.rs @@ -16,7 +16,7 @@ use { find_transient_stake_program_address, find_withdraw_authority_program_address, id, instruction, state::{StakePool, StakeStatus, ValidatorList}, - MAX_VALIDATORS_TO_UPDATE, MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MAX_VALIDATORS_TO_UPDATE, MINIMUM_RESERVE_LAMPORTS, }, spl_token::state::Mint, }; @@ -440,7 +440,13 @@ async fn merge_into_validator_stake() { // Check validator stake accounts have the expected balance now: // validator stake account minimum + deposited lamports + rents + increased lamports let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let expected_lamports = MINIMUM_ACTIVE_STAKE + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let expected_lamports = current_minimum_delegation + lamports + reserve_lamports / stake_accounts.len() as u64 + stake_rent; diff --git a/stake-pool/program/tests/vsa_remove.rs b/stake-pool/program/tests/vsa_remove.rs index 209d5193573..f80c53338b9 100644 --- a/stake-pool/program/tests/vsa_remove.rs +++ b/stake-pool/program/tests/vsa_remove.rs @@ -20,7 +20,7 @@ use { }, spl_stake_pool::{ error::StakePoolError, find_transient_stake_program_address, id, instruction, state, - MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MINIMUM_RESERVE_LAMPORTS, }, }; @@ -692,7 +692,13 @@ async fn success_with_hijacked_transient_account() { setup().await; let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let increase_amount = MINIMUM_ACTIVE_STAKE + stake_rent; + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let increase_amount = current_minimum_delegation + stake_rent; // increase stake on validator let error = stake_pool_accounts @@ -770,7 +776,7 @@ async fn success_with_hijacked_transient_account() { system_instruction::transfer( &context.payer.pubkey(), &transient_stake_address, - MINIMUM_RESERVE_LAMPORTS + stake_rent, + current_minimum_delegation + stake_rent, ), stake::instruction::initialize( &transient_stake_address, diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs index 96a899e5fef..31cf72d5ca8 100644 --- a/stake-pool/program/tests/withdraw.rs +++ b/stake-pool/program/tests/withdraw.rs @@ -21,7 +21,7 @@ use { }, spl_stake_pool::{ error::StakePoolError, id, instruction, minimum_stake_lamports, state, - MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MINIMUM_RESERVE_LAMPORTS, }, spl_token::error::TokenError, }; @@ -57,13 +57,16 @@ async fn setup() -> ( ) .await; + let current_minimum_delegation = + stake_pool_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; + let deposit_info = simple_deposit_stake( &mut banks_client, &payer, &recent_blockhash, &stake_pool_accounts, &validator_stake_account, - MINIMUM_ACTIVE_STAKE * 3, + current_minimum_delegation * 3, ) .await .unwrap(); @@ -306,8 +309,10 @@ async fn _success(test_type: SuccessTestType) { let stake_state = deserialize::(&validator_stake_account.data).unwrap(); let meta = stake_state.meta().unwrap(); + let stake_minimum_delegation = + stake_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; assert_eq!( - validator_stake_account.lamports - minimum_stake_lamports(&meta), + validator_stake_account.lamports - minimum_stake_lamports(&meta, stake_minimum_delegation), validator_stake_item.active_stake_lamports ); @@ -876,7 +881,13 @@ async fn success_with_reserve() { let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let deposit_lamports = (MINIMUM_ACTIVE_STAKE + stake_rent) * 2; + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let deposit_lamports = (current_minimum_delegation + stake_rent) * 2; let deposit_info = simple_deposit_stake( &mut context.banks_client, @@ -1252,7 +1263,13 @@ async fn fail_withdraw_from_transient() { let rent = context.banks_client.get_rent().await.unwrap(); let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let deposit_lamports = (MINIMUM_ACTIVE_STAKE + stake_rent) * 2; + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let deposit_lamports = (current_minimum_delegation + stake_rent) * 2; let deposit_info = simple_deposit_stake( &mut context.banks_client, @@ -1373,7 +1390,13 @@ async fn success_withdraw_from_transient() { let stake_rent = rent.minimum_balance(std::mem::size_of::()); // compensate for the fee and the minimum balance in the transient stake account - let deposit_lamports = (MINIMUM_ACTIVE_STAKE + stake_rent) * 3; + let current_minimum_delegation = stake_pool_get_minimum_delegation( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + ) + .await; + let deposit_lamports = (current_minimum_delegation + stake_rent) * 3; let deposit_info = simple_deposit_stake( &mut context.banks_client, @@ -1581,7 +1604,10 @@ async fn success_empty_out_stake_with_fee() { let stake_state = deserialize::(&validator_stake_account.data).unwrap(); let meta = stake_state.meta().unwrap(); - let lamports_to_withdraw = validator_stake_account.lamports - minimum_stake_lamports(&meta); + let stake_minimum_delegation = + stake_get_minimum_delegation(&mut banks_client, &payer, &recent_blockhash).await; + let lamports_to_withdraw = + validator_stake_account.lamports - minimum_stake_lamports(&meta, stake_minimum_delegation); let stake_pool_account = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; let stake_pool = @@ -1621,6 +1647,6 @@ async fn success_empty_out_stake_with_fee() { let meta = stake_state.meta().unwrap(); assert_eq!( validator_stake_account.lamports, - minimum_stake_lamports(&meta) + minimum_stake_lamports(&meta, stake_minimum_delegation) ); } From 12de645a99dc5209a6fa7fc884dbc70e82912fac Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Mon, 29 Aug 2022 22:41:47 +0200 Subject: [PATCH 2/6] Use minimum_delegation in CLI --- stake-pool/cli/src/main.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/stake-pool/cli/src/main.rs b/stake-pool/cli/src/main.rs index 07315aac22f..1ed088721bb 100644 --- a/stake-pool/cli/src/main.rs +++ b/stake-pool/cli/src/main.rs @@ -45,8 +45,9 @@ use { self, find_stake_program_address, find_transient_stake_program_address, find_withdraw_authority_program_address, instruction::{FundingType, PreferredValidatorType}, + minimum_delegation, state::{Fee, FeeType, StakePool, ValidatorList}, - MINIMUM_ACTIVE_STAKE, MINIMUM_RESERVE_LAMPORTS, + MINIMUM_RESERVE_LAMPORTS, }, std::cmp::Ordering, std::{process::exit, sync::Arc}, @@ -1219,9 +1220,11 @@ fn prepare_withdraw_accounts( stake_pool_address: &Pubkey, skip_fee: bool, ) -> Result, Error> { + let stake_minimum_delegation = rpc_client.get_stake_minimum_delegation()?; + let stake_pool_minimum_delegation = minimum_delegation(stake_minimum_delegation); let min_balance = rpc_client .get_minimum_balance_for_rent_exemption(STAKE_STATE_LEN)? - .saturating_add(MINIMUM_ACTIVE_STAKE); + .saturating_add(stake_pool_minimum_delegation); let pool_mint = get_token_mint(rpc_client, &stake_pool.pool_mint)?; let validator_list: ValidatorList = get_validator_list(rpc_client, &stake_pool.validator_list)?; @@ -1386,6 +1389,9 @@ fn command_withdraw_stake( }) .flatten(); + let stake_minimum_delegation = config.rpc_client.get_stake_minimum_delegation()?; + let stake_pool_minimum_delegation = minimum_delegation(stake_minimum_delegation); + let withdraw_accounts = if use_reserve { vec![WithdrawAccount { stake_address: stake_pool.reserve_stake, @@ -1400,7 +1406,7 @@ fn command_withdraw_stake( .voter_pubkey; if let Some(vote_account_address) = vote_account_address { if *vote_account_address != vote_account { - return Err(format!("Provided withdrawal vote account {} does not match delegation on stake receiver account {}, + return Err(format!("Provided withdrawal vote account {} does not match delegation on stake receiver account {}, remove this flag or provide a different stake account delegated to {}", vote_account_address, vote_account, vote_account_address).into()); } } @@ -1422,7 +1428,7 @@ fn command_withdraw_stake( .calc_lamports_withdraw_amount( stake_account .lamports - .saturating_sub(MINIMUM_ACTIVE_STAKE) + .saturating_sub(stake_pool_minimum_delegation) .saturating_sub(stake_account_rent_exemption), ) .unwrap(); @@ -1454,7 +1460,7 @@ fn command_withdraw_stake( .calc_lamports_withdraw_amount( stake_account .lamports - .saturating_sub(MINIMUM_ACTIVE_STAKE) + .saturating_sub(stake_pool_minimum_delegation) .saturating_sub(stake_account_rent_exemption), ) .unwrap(); From 3b35c4739b1f34081423358319e34b1411abe0a4 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Mon, 29 Aug 2022 23:28:05 +0200 Subject: [PATCH 3/6] Address feedback --- stake-pool/program/src/instruction.rs | 8 ++--- stake-pool/program/src/processor.rs | 44 +++++-------------------- stake-pool/program/src/state.rs | 2 +- stake-pool/program/tests/helpers/mod.rs | 8 +++-- 4 files changed, 19 insertions(+), 43 deletions(-) diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index 501fa29fe2a..4a11e759430 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -83,7 +83,7 @@ pub enum StakePoolInstruction { /// list of managed validators. /// /// The stake account will have the rent-exempt amount plus - /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)` + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. /// /// 0. `[w]` Stake pool /// 1. `[s]` Staker @@ -103,7 +103,7 @@ pub enum StakePoolInstruction { /// (Staker only) Removes validator from the pool /// /// Only succeeds if the validator stake account has the minimum of - /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)` + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. /// plus the rent-exempt amount. /// /// 0. `[w]` Stake pool @@ -159,7 +159,7 @@ pub enum StakePoolInstruction { /// /// This instruction only succeeds if the transient stake account does not exist. /// The minimum amount to move is rent-exemption plus - /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)`. + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. /// /// 0. `[]` Stake pool /// 1. `[s]` Stake pool staker @@ -281,7 +281,7 @@ pub enum StakePoolInstruction { /// Succeeds if the stake account has enough SOL to cover the desired amount /// of pool tokens, and if the withdrawal keeps the total staked amount /// above the minimum of rent-exempt amount + - /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)` + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. /// /// When allowing withdrawals, the order of priority goes: /// diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 033e126ca12..faefaf4fefe 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -26,7 +26,7 @@ use { decode_error::DecodeError, entrypoint::ProgramResult, msg, - program::{get_return_data, invoke, invoke_signed}, + program::{invoke, invoke_signed}, program_error::{PrintProgramError, ProgramError}, program_pack::Pack, pubkey::Pubkey, @@ -35,7 +35,6 @@ use { sysvar::Sysvar, }, spl_token::state::Mint, - std::convert::TryInto, }; /// Deserialize the stake state from AccountInfo @@ -515,26 +514,6 @@ impl Processor { ) } - /// Issue stake::instruction::get_minimum_delegation instruction to get the - /// minimum stake delegation - fn stake_program_minimum_delegation( - stake_program_info: AccountInfo, - ) -> Result { - let instruction = stake::instruction::get_minimum_delegation(); - let stake_program_id = stake_program_info.key; - invoke(&instruction, &[stake_program_info])?; - get_return_data() - .ok_or(ProgramError::InvalidInstructionData) - .and_then(|(key, data)| { - if key != *stake_program_id { - return Err(ProgramError::IncorrectProgramId); - } - data.try_into() - .map(u64::from_le_bytes) - .map_err(|_| ProgramError::InvalidInstructionData) - }) - } - /// Issue a spl_token `Burn` instruction. #[allow(clippy::too_many_arguments)] fn token_burn<'a>( @@ -928,8 +907,7 @@ impl Processor { // Fund the stake account with the minimum + rent-exempt balance let space = std::mem::size_of::(); - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let required_lamports = minimum_delegation(stake_minimum_delegation) + rent.minimum_balance(space); @@ -1055,8 +1033,7 @@ impl Processor { let mut validator_stake_info = maybe_validator_stake_info.unwrap(); let stake_lamports = **stake_account_info.lamports.borrow(); - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); if stake_lamports != required_lamports { msg!( @@ -1251,8 +1228,7 @@ impl Processor { .lamports() .checked_sub(lamports) .ok_or(ProgramError::InsufficientFunds)?; - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let required_lamports = minimum_stake_lamports(&meta, stake_minimum_delegation); if remaining_lamports < required_lamports { msg!("Need at least {} lamports in the stake account after decrease, {} requested, {} is the current possible maximum", @@ -1423,8 +1399,7 @@ impl Processor { } let stake_rent = rent.minimum_balance(std::mem::size_of::()); - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); if lamports < current_minimum_delegation { msg!( @@ -1609,8 +1584,7 @@ impl Processor { return Err(StakePoolError::InvalidState.into()); } - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); let validator_iter = &mut validator_slice .iter_mut() @@ -2230,8 +2204,7 @@ impl Processor { .ok_or(StakePoolError::CalculationFailure)?; stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?; - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); validator_stake_info.active_stake_lamports = post_validator_stake .delegation @@ -2546,8 +2519,7 @@ impl Processor { } let remaining_lamports = stake.delegation.stake.saturating_sub(withdraw_lamports); - let stake_minimum_delegation = - Self::stake_program_minimum_delegation(stake_program_info.clone())?; + let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); if remaining_lamports < current_minimum_delegation { msg!("Attempting to withdraw {} lamports from validator account with {} stake lamports, {} must remain", withdraw_lamports, stake.delegation.stake, current_minimum_delegation); diff --git a/stake-pool/program/src/state.rs b/stake-pool/program/src/state.rs index 1eaecdf7692..fcb0ab57d81 100644 --- a/stake-pool/program/src/state.rs +++ b/stake-pool/program/src/state.rs @@ -535,7 +535,7 @@ impl Default for StakeStatus { pub struct ValidatorStakeInfo { /// Amount of active stake delegated to this validator, minus the minimum /// required stake amount of rent-exemption + - /// `max(crate::MINIMUM_ACTIVE_STAKE, stake_minimum_delegation)`. + /// `max(crate::MINIMUM_ACTIVE_STAKE, solana_program::stake::tools::get_minimum_delegation())`. /// /// Note that if `last_update_epoch` does not match the current epoch then /// this field may not be accurate diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 0bbeb1c4b63..4b9f9579bf5 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -1749,8 +1749,12 @@ pub fn add_validator_stake_account( let (stake_address, _) = find_stake_program_address(&id(), voter_pubkey, stake_pool_pubkey); program_test.add_account(stake_address, stake_account); - let active_stake_lamports = stake_amount - LAMPORTS_PER_SOL; // hack - // add to validator list + + // Hack the active stake lamports to the current amount given by the runtime. + // Since program_test hasn't been started, there's no usable banks_client for + // fetching the minimum stake delegation. + let active_stake_lamports = stake_amount - LAMPORTS_PER_SOL; + validator_list.validators.push(state::ValidatorStakeInfo { status: state::StakeStatus::Active, vote_account_address: *voter_pubkey, From 5bde39d1839f6c52f9994a2ae9121bb4935e68b3 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Tue, 30 Aug 2022 01:11:10 +0200 Subject: [PATCH 4/6] Update minimum delegation in python test --- stake-pool/py/stake_pool/constants.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stake-pool/py/stake_pool/constants.py b/stake-pool/py/stake_pool/constants.py index 3af4dce4fda..d58a99fe368 100644 --- a/stake-pool/py/stake_pool/constants.py +++ b/stake-pool/py/stake_pool/constants.py @@ -11,7 +11,7 @@ MAX_VALIDATORS_TO_UPDATE: int = 5 """Maximum number of validators to update during UpdateValidatorListBalance.""" -MINIMUM_RESERVE_LAMPORTS: int = MINIMUM_DELEGATION +MINIMUM_RESERVE_LAMPORTS: int = 1 """Minimum balance required in the stake pool reserve""" MINIMUM_ACTIVE_STAKE: int = MINIMUM_DELEGATION From 4cd41956cca12ebc73ef8daad89a7a46e7e6bb70 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Tue, 30 Aug 2022 19:56:08 +0200 Subject: [PATCH 5/6] Address feedback --- stake-pool/program/src/lib.rs | 1 + stake-pool/program/src/processor.rs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/stake-pool/program/src/lib.rs b/stake-pool/program/src/lib.rs index 5437dd92cff..321654abf69 100644 --- a/stake-pool/program/src/lib.rs +++ b/stake-pool/program/src/lib.rs @@ -66,6 +66,7 @@ pub fn minimum_stake_lamports(meta: &Meta, stake_program_minimum_delegation: u64 } /// Get the minimum delegation required by a stake account in a stake pool +#[inline] pub fn minimum_delegation(stake_program_minimum_delegation: u64) -> u64 { std::cmp::max(stake_program_minimum_delegation, MINIMUM_ACTIVE_STAKE) } diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index faefaf4fefe..417db3550e1 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -908,8 +908,8 @@ impl Processor { // Fund the stake account with the minimum + rent-exempt balance let space = std::mem::size_of::(); let stake_minimum_delegation = stake::tools::get_minimum_delegation()?; - let required_lamports = - minimum_delegation(stake_minimum_delegation) + rent.minimum_balance(space); + let required_lamports = minimum_delegation(stake_minimum_delegation) + .saturating_add(rent.minimum_balance(space)); // Create new stake account create_pda_account( @@ -1407,7 +1407,9 @@ impl Processor { current_minimum_delegation, lamports ); - return Err(ProgramError::AccountNotRentExempt); + return Err(ProgramError::Custom( + stake::instruction::StakeError::InsufficientDelegation as u32, + )); } // the stake account rent exemption is withdrawn after the merge, so From 8bafc18620fe2d60c52665758b414407b0ef29c4 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Tue, 30 Aug 2022 20:37:49 +0200 Subject: [PATCH 6/6] Improve error message --- stake-pool/program/src/processor.rs | 2 +- stake-pool/program/tests/increase.rs | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 417db3550e1..77fdb2b44a1 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -1403,7 +1403,7 @@ impl Processor { let current_minimum_delegation = minimum_delegation(stake_minimum_delegation); if lamports < current_minimum_delegation { msg!( - "Need more than {} lamports for transient stake to be rent-exempt and mergeable, {} provided", + "Need more than {} lamports for transient stake to meet minimum delegation requirement, {} provided", current_minimum_delegation, lamports ); diff --git a/stake-pool/program/tests/increase.rs b/stake-pool/program/tests/increase.rs index ffd28bc1dfc..ac9c91918e1 100644 --- a/stake-pool/program/tests/increase.rs +++ b/stake-pool/program/tests/increase.rs @@ -11,6 +11,7 @@ use { solana_program_test::*, solana_sdk::{ signature::{Keypair, Signer}, + stake::instruction::StakeError, transaction::{Transaction, TransactionError}, }, spl_stake_pool::{ @@ -376,7 +377,10 @@ async fn fail_with_small_lamport_amount() { .unwrap(); match error { - TransactionError::InstructionError(_, InstructionError::AccountNotRentExempt) => {} + TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => { + let program_error = StakeError::InsufficientDelegation as u32; + assert_eq!(error_index, program_error); + } _ => panic!("Wrong error"), } }