From c4269cb989fe8145531096d807f80d4f6fcea382 Mon Sep 17 00:00:00 2001 From: Jon Cinque Date: Fri, 27 Jan 2023 02:10:24 +0100 Subject: [PATCH] stake-pool: Wait at least two epoch boundaries to set fee (#3979) --- docs/src/stake-pool/cli.md | 11 +- stake-pool/cli/src/output.rs | 9 +- stake-pool/program/src/processor.rs | 32 +- stake-pool/program/src/state.rs | 68 +++- stake-pool/program/tests/helpers/mod.rs | 8 +- stake-pool/program/tests/set_epoch_fee.rs | 33 +- .../program/tests/set_withdrawal_fee.rs | 304 ++++++++++++++++-- 7 files changed, 409 insertions(+), 56 deletions(-) diff --git a/docs/src/stake-pool/cli.md b/docs/src/stake-pool/cli.md index d57a1cce8af..fbd95c2b7bc 100644 --- a/docs/src/stake-pool/cli.md +++ b/docs/src/stake-pool/cli.md @@ -169,18 +169,19 @@ Signature: 5yPXfVj5cbKBfZiEVi2UR5bXzVDuc2c3ruBwSjkAqpvxPHigwGHiS1mXQVE4qwok5moMW ``` In order to protect stake pool depositors from malicious managers, the program -applies the new fee for the following epoch. +applies the new fee after crossing two epoch boundaries, giving a minimum wait +time of one full epoch. For example, if the fee is 1% at epoch 100, and the manager sets it to 10%, the -manager will still gain 1% for the rewards earned during epoch 100. Starting -with epoch 101, the manager will earn 10%. +manager will still gain 1% for the rewards earned during epochs 100 and 101. Starting +with epoch 102, the manager will earn 10%. Additionally, to prevent a malicious manager from immediately setting the withdrawal fee to a very high amount, making it practically impossible for users to withdraw, the stake pool program currently enforces a limit of 1.5x increase per epoch. -For example, if the current withdrawal fee is 2.5%, the maximum that can be set -for the next epoch is 3.75%. +For example, if the current withdrawal fee is 2.5%, the maximum settable fee is +3.75%, and will take effect after two epoch boundaries. The possible options for the fee type are `epoch`, `sol-withdrawal`, `stake-withdrawal`, `sol-deposit`, and `stake-deposit`. diff --git a/stake-pool/cli/src/output.rs b/stake-pool/cli/src/output.rs index f9b0cd27772..097e93340cc 100644 --- a/stake-pool/cli/src/output.rs +++ b/stake-pool/cli/src/output.rs @@ -475,7 +475,8 @@ impl From<(Pubkey, StakePool, ValidatorList, Pubkey)> for CliStakePool { last_update_epoch: stake_pool.last_update_epoch, lockup: CliStakePoolLockup::from(stake_pool.lockup), epoch_fee: CliStakePoolFee::from(stake_pool.epoch_fee), - next_epoch_fee: stake_pool.next_epoch_fee.map(CliStakePoolFee::from), + next_epoch_fee: Option::::from(stake_pool.next_epoch_fee) + .map(CliStakePoolFee::from), preferred_deposit_validator_vote_address: stake_pool .preferred_deposit_validator_vote_address .map(|x| x.to_string()), @@ -484,8 +485,7 @@ impl From<(Pubkey, StakePool, ValidatorList, Pubkey)> for CliStakePool { .map(|x| x.to_string()), stake_deposit_fee: CliStakePoolFee::from(stake_pool.stake_deposit_fee), stake_withdrawal_fee: CliStakePoolFee::from(stake_pool.stake_withdrawal_fee), - next_stake_withdrawal_fee: stake_pool - .next_stake_withdrawal_fee + next_stake_withdrawal_fee: Option::::from(stake_pool.next_stake_withdrawal_fee) .map(CliStakePoolFee::from), stake_referral_fee: stake_pool.stake_referral_fee, sol_deposit_authority: stake_pool.sol_deposit_authority.map(|x| x.to_string()), @@ -493,8 +493,7 @@ impl From<(Pubkey, StakePool, ValidatorList, Pubkey)> for CliStakePool { sol_referral_fee: stake_pool.sol_referral_fee, sol_withdraw_authority: stake_pool.sol_withdraw_authority.map(|x| x.to_string()), sol_withdrawal_fee: CliStakePoolFee::from(stake_pool.sol_withdrawal_fee), - next_sol_withdrawal_fee: stake_pool - .next_sol_withdrawal_fee + next_sol_withdrawal_fee: Option::::from(stake_pool.next_sol_withdrawal_fee) .map(CliStakePoolFee::from), last_epoch_pool_token_supply: stake_pool.last_epoch_pool_token_supply, last_epoch_total_lamports: stake_pool.last_epoch_total_lamports, diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 33355dc0bec..9ffffd01df8 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -7,8 +7,9 @@ use { instruction::{FundingType, PreferredValidatorType, StakePoolInstruction}, minimum_delegation, minimum_reserve_lamports, minimum_stake_lamports, state::{ - is_extension_supported_for_mint, AccountType, Fee, FeeType, StakePool, StakeStatus, - StakeWithdrawSource, ValidatorList, ValidatorListHeader, ValidatorStakeInfo, + is_extension_supported_for_mint, AccountType, Fee, FeeType, FutureEpoch, StakePool, + StakeStatus, StakeWithdrawSource, ValidatorList, ValidatorListHeader, + ValidatorStakeInfo, }, AUTHORITY_DEPOSIT, AUTHORITY_WITHDRAW, EPHEMERAL_STAKE_SEED_PREFIX, TRANSIENT_STAKE_SEED_PREFIX, @@ -905,19 +906,19 @@ impl Processor { stake_pool.last_update_epoch = Clock::get()?.epoch; stake_pool.lockup = stake::state::Lockup::default(); stake_pool.epoch_fee = epoch_fee; - stake_pool.next_epoch_fee = None; + stake_pool.next_epoch_fee = FutureEpoch::None; stake_pool.preferred_deposit_validator_vote_address = None; stake_pool.preferred_withdraw_validator_vote_address = None; stake_pool.stake_deposit_fee = deposit_fee; stake_pool.stake_withdrawal_fee = withdrawal_fee; - stake_pool.next_stake_withdrawal_fee = None; + stake_pool.next_stake_withdrawal_fee = FutureEpoch::None; stake_pool.stake_referral_fee = referral_fee; stake_pool.sol_deposit_authority = sol_deposit_authority; stake_pool.sol_deposit_fee = deposit_fee; stake_pool.sol_referral_fee = referral_fee; stake_pool.sol_withdraw_authority = None; stake_pool.sol_withdrawal_fee = withdrawal_fee; - stake_pool.next_sol_withdrawal_fee = None; + stake_pool.next_sol_withdrawal_fee = FutureEpoch::None; stake_pool.last_epoch_pool_token_supply = 0; stake_pool.last_epoch_total_lamports = 0; @@ -2532,18 +2533,21 @@ impl Processor { } if stake_pool.last_update_epoch < clock.epoch { - if let Some(fee) = stake_pool.next_epoch_fee { - stake_pool.epoch_fee = fee; - stake_pool.next_epoch_fee = None; + if let Some(fee) = stake_pool.next_epoch_fee.get() { + stake_pool.epoch_fee = *fee; } - if let Some(fee) = stake_pool.next_stake_withdrawal_fee { - stake_pool.stake_withdrawal_fee = fee; - stake_pool.next_stake_withdrawal_fee = None; + stake_pool.next_epoch_fee.update_epoch(); + + if let Some(fee) = stake_pool.next_stake_withdrawal_fee.get() { + stake_pool.stake_withdrawal_fee = *fee; } - if let Some(fee) = stake_pool.next_sol_withdrawal_fee { - stake_pool.sol_withdrawal_fee = fee; - stake_pool.next_sol_withdrawal_fee = None; + stake_pool.next_stake_withdrawal_fee.update_epoch(); + + if let Some(fee) = stake_pool.next_sol_withdrawal_fee.get() { + stake_pool.sol_withdrawal_fee = *fee; } + stake_pool.next_sol_withdrawal_fee.update_epoch(); + stake_pool.last_update_epoch = clock.epoch; stake_pool.last_epoch_total_lamports = previous_lamports; stake_pool.last_epoch_pool_token_supply = previous_pool_token_supply; diff --git a/stake-pool/program/src/state.rs b/stake-pool/program/src/state.rs index 1a601d992f7..30b2ef43230 100644 --- a/stake-pool/program/src/state.rs +++ b/stake-pool/program/src/state.rs @@ -104,7 +104,7 @@ pub struct StakePool { pub epoch_fee: Fee, /// Fee for next epoch - pub next_epoch_fee: Option, + pub next_epoch_fee: FutureEpoch, /// Preferred deposit validator vote account pubkey pub preferred_deposit_validator_vote_address: Option, @@ -119,7 +119,7 @@ pub struct StakePool { pub stake_withdrawal_fee: Fee, /// Future stake withdrawal fee, to be set for the following epoch - pub next_stake_withdrawal_fee: Option, + pub next_stake_withdrawal_fee: FutureEpoch, /// Fees paid out to referrers on referred stake deposits. /// Expressed as a percentage (0 - 100) of deposit fees. @@ -148,7 +148,7 @@ pub struct StakePool { pub sol_withdrawal_fee: Fee, /// Future SOL withdrawal fee, to be set for the following epoch - pub next_sol_withdrawal_fee: Option, + pub next_sol_withdrawal_fee: FutureEpoch, /// Last epoch's total pool tokens, used only for APR estimation pub last_epoch_pool_token_supply: u64, @@ -483,14 +483,14 @@ impl StakePool { match fee { FeeType::SolReferral(new_fee) => self.sol_referral_fee = *new_fee, FeeType::StakeReferral(new_fee) => self.stake_referral_fee = *new_fee, - FeeType::Epoch(new_fee) => self.next_epoch_fee = Some(*new_fee), + FeeType::Epoch(new_fee) => self.next_epoch_fee = FutureEpoch::new(*new_fee), FeeType::StakeWithdrawal(new_fee) => { new_fee.check_withdrawal(&self.stake_withdrawal_fee)?; - self.next_stake_withdrawal_fee = Some(*new_fee) + self.next_stake_withdrawal_fee = FutureEpoch::new(*new_fee) } FeeType::SolWithdrawal(new_fee) => { new_fee.check_withdrawal(&self.sol_withdrawal_fee)?; - self.next_sol_withdrawal_fee = Some(*new_fee) + self.next_sol_withdrawal_fee = FutureEpoch::new(*new_fee) } FeeType::SolDeposit(new_fee) => self.sol_deposit_fee = *new_fee, FeeType::StakeDeposit(new_fee) => self.stake_deposit_fee = *new_fee, @@ -793,6 +793,62 @@ impl ValidatorListHeader { } } +/// Wrapper type that "counts down" epochs, which is Borsh-compatible with the +/// native `Option` +#[repr(C)] +#[derive(Clone, Copy, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)] +pub enum FutureEpoch { + /// Nothing is set + None, + /// Value is ready after the next epoch boundary + One(T), + /// Value is ready after two epoch boundaries + Two(T), +} +impl Default for FutureEpoch { + fn default() -> Self { + Self::None + } +} +impl FutureEpoch { + /// Create a new value to be unlocked in a two epochs + pub fn new(value: T) -> Self { + Self::Two(value) + } +} +impl FutureEpoch { + /// Update the epoch, to be done after `get`ting the underlying value + pub fn update_epoch(&mut self) { + match self { + Self::None => {} + Self::One(_) => { + // The value has waited its last epoch + *self = Self::None; + } + // The value still has to wait one more epoch after this + Self::Two(v) => { + *self = Self::One(v.clone()); + } + } + } + + /// Get the value if it's ready, which is only at `One` epoch remaining + pub fn get(&self) -> Option<&T> { + match self { + Self::None | Self::Two(_) => None, + Self::One(v) => Some(v), + } + } +} +impl From> for Option { + fn from(v: FutureEpoch) -> Option { + match v { + FutureEpoch::None => None, + FutureEpoch::One(inner) | FutureEpoch::Two(inner) => Some(inner), + } + } +} + /// Fee rate as a ratio, minted on `UpdateStakePoolBalance` as a proportion of /// the rewards /// If either the numerator or the denominator is 0, the fee is considered to be 0 diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 4a6842331c2..a7691f59d3c 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -30,7 +30,7 @@ use { find_stake_program_address, find_transient_stake_program_address, find_withdraw_authority_program_address, id, instruction, minimum_delegation, processor::Processor, - state::{self, FeeType, StakePool, ValidatorList}, + state::{self, FeeType, FutureEpoch, StakePool, ValidatorList}, MINIMUM_RESERVE_LAMPORTS, }, spl_token_2022::{ @@ -1776,19 +1776,19 @@ impl StakePoolAccounts { last_update_epoch: 0, lockup: stake::state::Lockup::default(), epoch_fee: self.epoch_fee, - next_epoch_fee: None, + next_epoch_fee: FutureEpoch::None, preferred_deposit_validator_vote_address: None, preferred_withdraw_validator_vote_address: None, stake_deposit_fee: state::Fee::default(), sol_deposit_fee: state::Fee::default(), stake_withdrawal_fee: state::Fee::default(), - next_stake_withdrawal_fee: None, + next_stake_withdrawal_fee: FutureEpoch::None, stake_referral_fee: 0, sol_referral_fee: 0, sol_deposit_authority: None, sol_withdraw_authority: None, sol_withdrawal_fee: state::Fee::default(), - next_sol_withdrawal_fee: None, + next_sol_withdrawal_fee: FutureEpoch::None, last_epoch_pool_token_supply: 0, last_epoch_total_lamports: 0, }; diff --git a/stake-pool/program/tests/set_epoch_fee.rs b/stake-pool/program/tests/set_epoch_fee.rs index aff29f1ed7c..57d183bdb38 100644 --- a/stake-pool/program/tests/set_epoch_fee.rs +++ b/stake-pool/program/tests/set_epoch_fee.rs @@ -14,7 +14,7 @@ use { }, spl_stake_pool::{ error, id, instruction, - state::{Fee, FeeType, StakePool}, + state::{Fee, FeeType, FutureEpoch, StakePool}, MINIMUM_RESERVE_LAMPORTS, }, }; @@ -76,7 +76,7 @@ async fn success() { let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); assert_eq!(stake_pool.epoch_fee, old_fee); - assert_eq!(stake_pool.next_epoch_fee, Some(new_fee)); + assert_eq!(stake_pool.next_epoch_fee, FutureEpoch::Two(new_fee)); let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; @@ -94,6 +94,33 @@ async fn success() { ) .await; + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.epoch_fee, old_fee); + assert_eq!(stake_pool.next_epoch_fee, FutureEpoch::One(new_fee)); + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + context + .warp_to_slot(first_normal_slot + 2 * slots_per_epoch) + .unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[], + false, + ) + .await; + let stake_pool = get_account( &mut context.banks_client, &stake_pool_accounts.stake_pool.pubkey(), @@ -101,7 +128,7 @@ async fn success() { .await; let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); assert_eq!(stake_pool.epoch_fee, new_fee); - assert_eq!(stake_pool.next_epoch_fee, None); + assert_eq!(stake_pool.next_epoch_fee, FutureEpoch::None); } #[tokio::test] diff --git a/stake-pool/program/tests/set_withdrawal_fee.rs b/stake-pool/program/tests/set_withdrawal_fee.rs index 1ca7df81a59..6ebf6ea384f 100644 --- a/stake-pool/program/tests/set_withdrawal_fee.rs +++ b/stake-pool/program/tests/set_withdrawal_fee.rs @@ -14,7 +14,7 @@ use { }, spl_stake_pool::{ error, id, instruction, - state::{Fee, FeeType, StakePool}, + state::{Fee, FeeType, FutureEpoch, StakePool}, MINIMUM_RESERVE_LAMPORTS, }, }; @@ -99,10 +99,13 @@ async fn success() { assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); assert_eq!( stake_pool.next_stake_withdrawal_fee, - Some(new_withdrawal_fee) + FutureEpoch::Two(new_withdrawal_fee) ); assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, Some(new_withdrawal_fee)); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::Two(new_withdrawal_fee) + ); let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; @@ -120,6 +123,41 @@ async fn success() { ) .await; + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); + assert_eq!( + stake_pool.next_stake_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + context + .warp_to_slot(first_normal_slot + 2 * slots_per_epoch) + .unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[], + false, + ) + .await; + let stake_pool = get_account( &mut context.banks_client, &stake_pool_accounts.stake_pool.pubkey(), @@ -127,9 +165,9 @@ async fn success() { .await; let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_stake_withdrawal_fee, None); + assert_eq!(stake_pool.next_stake_withdrawal_fee, FutureEpoch::None); assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, None); + assert_eq!(stake_pool.next_sol_withdrawal_fee, FutureEpoch::None); } #[tokio::test] @@ -189,10 +227,13 @@ async fn success_fee_cannot_increase_more_than_once() { assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); assert_eq!( stake_pool.next_stake_withdrawal_fee, - Some(new_withdrawal_fee) + FutureEpoch::Two(new_withdrawal_fee) ); assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, Some(new_withdrawal_fee)); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::Two(new_withdrawal_fee) + ); let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; @@ -216,10 +257,46 @@ async fn success_fee_cannot_increase_more_than_once() { ) .await; let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); + assert_eq!( + stake_pool.next_stake_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + context + .warp_to_slot(first_normal_slot + 2 * slots_per_epoch) + .unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[], + false, + ) + .await; + + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_stake_withdrawal_fee, None); + assert_eq!(stake_pool.next_stake_withdrawal_fee, FutureEpoch::None); assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, None); + assert_eq!(stake_pool.next_sol_withdrawal_fee, FutureEpoch::None); // try setting to the old fee in the same epoch let transaction = Transaction::new_signed_with_payer( @@ -231,7 +308,7 @@ async fn success_fee_cannot_increase_more_than_once() { )], Some(&context.payer.pubkey()), &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, + last_blockhash, ); context .banks_client @@ -247,7 +324,7 @@ async fn success_fee_cannot_increase_more_than_once() { )], Some(&context.payer.pubkey()), &[&context.payer, &stake_pool_accounts.manager], - context.last_blockhash, + last_blockhash, ); context .banks_client @@ -264,12 +341,12 @@ async fn success_fee_cannot_increase_more_than_once() { assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); assert_eq!( stake_pool.next_stake_withdrawal_fee, - Some(old_stake_withdrawal_fee) + FutureEpoch::Two(old_stake_withdrawal_fee) ); assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); assert_eq!( stake_pool.next_sol_withdrawal_fee, - Some(old_sol_withdrawal_fee) + FutureEpoch::Two(old_sol_withdrawal_fee) ); let error = stake_pool_accounts @@ -291,12 +368,163 @@ async fn success_fee_cannot_increase_more_than_once() { assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); assert_eq!( stake_pool.next_stake_withdrawal_fee, - Some(old_stake_withdrawal_fee) + FutureEpoch::Two(old_stake_withdrawal_fee) ); assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); assert_eq!( stake_pool.next_sol_withdrawal_fee, - Some(old_sol_withdrawal_fee) + FutureEpoch::Two(old_sol_withdrawal_fee) + ); +} + +#[tokio::test] +async fn success_reset_fee_after_one_epoch() { + let (mut context, stake_pool_accounts, new_withdrawal_fee) = setup(None).await; + + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + let old_stake_withdrawal_fee = stake_pool.stake_withdrawal_fee; + let old_sol_withdrawal_fee = stake_pool.sol_withdrawal_fee; + + let transaction = Transaction::new_signed_with_payer( + &[instruction::set_fee( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + FeeType::StakeWithdrawal(new_withdrawal_fee), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::set_fee( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + FeeType::SolWithdrawal(new_withdrawal_fee), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + + assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); + assert_eq!( + stake_pool.next_stake_withdrawal_fee, + FutureEpoch::Two(new_withdrawal_fee) + ); + assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::Two(new_withdrawal_fee) + ); + + let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; + let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; + + context + .warp_to_slot(first_normal_slot + slots_per_epoch) + .unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &context.last_blockhash, + &[], + false, + ) + .await; + + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); + assert_eq!( + stake_pool.next_stake_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + + // Flip the two fees, resets the counter to two future epochs + let transaction = Transaction::new_signed_with_payer( + &[instruction::set_fee( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + FeeType::StakeWithdrawal(old_sol_withdrawal_fee), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let transaction = Transaction::new_signed_with_payer( + &[instruction::set_fee( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.manager.pubkey(), + FeeType::SolWithdrawal(old_stake_withdrawal_fee), + )], + Some(&context.payer.pubkey()), + &[&context.payer, &stake_pool_accounts.manager], + context.last_blockhash, + ); + context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); + assert_eq!( + stake_pool.next_stake_withdrawal_fee, + FutureEpoch::Two(old_sol_withdrawal_fee) + ); + assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::Two(old_stake_withdrawal_fee) ); } @@ -365,10 +593,13 @@ async fn success_increase_fee_from_0() { assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); assert_eq!( stake_pool.next_stake_withdrawal_fee, - Some(new_withdrawal_fee) + FutureEpoch::Two(new_withdrawal_fee) ); assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, Some(new_withdrawal_fee)); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::Two(new_withdrawal_fee) + ); let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot; let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch; @@ -386,6 +617,41 @@ async fn success_increase_fee_from_0() { ) .await; + let stake_pool = get_account( + &mut context.banks_client, + &stake_pool_accounts.stake_pool.pubkey(), + ) + .await; + let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); + assert_eq!(stake_pool.stake_withdrawal_fee, old_stake_withdrawal_fee); + assert_eq!( + stake_pool.next_stake_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + assert_eq!(stake_pool.sol_withdrawal_fee, old_sol_withdrawal_fee); + assert_eq!( + stake_pool.next_sol_withdrawal_fee, + FutureEpoch::One(new_withdrawal_fee) + ); + + let last_blockhash = context + .banks_client + .get_new_latest_blockhash(&context.last_blockhash) + .await + .unwrap(); + context + .warp_to_slot(first_normal_slot + 2 * slots_per_epoch) + .unwrap(); + stake_pool_accounts + .update_all( + &mut context.banks_client, + &context.payer, + &last_blockhash, + &[], + false, + ) + .await; + let stake_pool = get_account( &mut context.banks_client, &stake_pool_accounts.stake_pool.pubkey(), @@ -393,9 +659,9 @@ async fn success_increase_fee_from_0() { .await; let stake_pool = try_from_slice_unchecked::(stake_pool.data.as_slice()).unwrap(); assert_eq!(stake_pool.stake_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_stake_withdrawal_fee, None); + assert_eq!(stake_pool.next_stake_withdrawal_fee, FutureEpoch::None); assert_eq!(stake_pool.sol_withdrawal_fee, new_withdrawal_fee); - assert_eq!(stake_pool.next_sol_withdrawal_fee, None); + assert_eq!(stake_pool.next_sol_withdrawal_fee, FutureEpoch::None); } #[tokio::test]