2019-04-01 16:45:53 -07:00
|
|
|
//! Stake state
|
|
|
|
//! * delegate stakes to vote accounts
|
|
|
|
//! * keep track of rewards
|
|
|
|
//! * own mining pools
|
|
|
|
|
2021-12-03 09:00:31 -08:00
|
|
|
#[deprecated(
|
|
|
|
since = "1.8.0",
|
|
|
|
note = "Please use `solana_sdk::stake::state` or `solana_program::stake::state` instead"
|
|
|
|
)]
|
|
|
|
pub use solana_sdk::stake::state::*;
|
2021-06-15 09:04:00 -07:00
|
|
|
use {
|
2021-11-17 10:35:07 -08:00
|
|
|
solana_program_runtime::{ic_msg, invoke_context::InvokeContext},
|
2021-06-15 09:04:00 -07:00
|
|
|
solana_sdk::{
|
|
|
|
account::{AccountSharedData, ReadableAccount, WritableAccount},
|
2022-04-10 00:55:37 -07:00
|
|
|
account_utils::StateMut,
|
2021-06-15 09:04:00 -07:00
|
|
|
clock::{Clock, Epoch},
|
2022-04-01 22:11:10 -07:00
|
|
|
feature_set::{
|
2023-06-23 13:57:18 -07:00
|
|
|
self, clean_up_delegation_errors, stake_merge_with_unmatched_credits_observed,
|
2023-07-07 07:01:39 -07:00
|
|
|
FeatureSet,
|
2022-04-01 22:11:10 -07:00
|
|
|
},
|
2021-06-15 09:04:00 -07:00
|
|
|
instruction::{checked_add, InstructionError},
|
|
|
|
pubkey::Pubkey,
|
2023-07-07 07:01:39 -07:00
|
|
|
rent::Rent,
|
2021-06-15 09:04:00 -07:00
|
|
|
stake::{
|
|
|
|
config::Config,
|
|
|
|
instruction::{LockupArgs, StakeError},
|
|
|
|
program::id,
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_flags::StakeFlags,
|
2022-03-25 09:11:51 -07:00
|
|
|
tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent},
|
2021-06-15 09:04:00 -07:00
|
|
|
},
|
|
|
|
stake_history::{StakeHistory, StakeHistoryEntry},
|
2022-09-06 02:31:40 -07:00
|
|
|
transaction_context::{
|
|
|
|
BorrowedAccount, IndexOfAccount, InstructionContext, TransactionContext,
|
|
|
|
},
|
2021-06-15 09:04:00 -07:00
|
|
|
},
|
2022-08-03 23:12:59 -07:00
|
|
|
solana_vote_program::vote_state::{self, VoteState, VoteStateVersions},
|
2023-06-24 09:26:27 -07:00
|
|
|
std::{cmp::Ordering, collections::HashSet, convert::TryFrom},
|
2019-08-12 20:59:57 -07:00
|
|
|
};
|
2021-06-15 09:04:00 -07:00
|
|
|
|
2021-02-11 22:24:23 -08:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum SkippedReason {
|
2022-02-01 05:42:02 -08:00
|
|
|
DisabledInflation,
|
|
|
|
JustActivated,
|
|
|
|
TooEarlyUnfairSplit,
|
2021-02-11 22:24:23 -08:00
|
|
|
ZeroPoints,
|
|
|
|
ZeroPointValue,
|
|
|
|
ZeroReward,
|
|
|
|
ZeroCreditsAndReturnZero,
|
|
|
|
ZeroCreditsAndReturnCurrent,
|
2022-04-26 14:49:35 -07:00
|
|
|
ZeroCreditsAndReturnRewinded,
|
2021-02-11 22:24:23 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
impl From<SkippedReason> for InflationPointCalculationEvent {
|
|
|
|
fn from(reason: SkippedReason) -> Self {
|
|
|
|
InflationPointCalculationEvent::Skipped(reason)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-07 23:43:50 -08:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum InflationPointCalculationEvent {
|
2020-11-26 20:20:47 -08:00
|
|
|
CalculatedPoints(u64, u128, u128, u128),
|
2020-11-07 23:43:50 -08:00
|
|
|
SplitRewards(u64, u64, u64, PointValue),
|
2020-11-26 20:20:47 -08:00
|
|
|
EffectiveStakeAtRewardedEpoch(u64),
|
2020-11-07 23:43:50 -08:00
|
|
|
RentExemptReserve(u64),
|
2020-11-15 11:38:46 -08:00
|
|
|
Delegation(Delegation, Pubkey),
|
2020-11-07 23:43:50 -08:00
|
|
|
Commission(u8),
|
2021-02-11 22:24:23 -08:00
|
|
|
CreditsObserved(u64, Option<u64>),
|
|
|
|
Skipped(SkippedReason),
|
2020-11-07 23:43:50 -08:00
|
|
|
}
|
|
|
|
|
2021-10-05 18:30:08 -07:00
|
|
|
pub(crate) fn null_tracer() -> Option<impl Fn(&InflationPointCalculationEvent)> {
|
2020-11-07 23:43:50 -08:00
|
|
|
None::<fn(&_)>
|
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
// utility function, used by Stakes, tests
|
|
|
|
pub fn from<T: ReadableAccount + StateMut<StakeState>>(account: &T) -> Option<StakeState> {
|
|
|
|
account.state().ok()
|
2019-06-21 20:43:24 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
pub fn stake_from<T: ReadableAccount + StateMut<StakeState>>(account: &T) -> Option<Stake> {
|
|
|
|
from(account).and_then(|state: StakeState| state.stake())
|
2019-09-26 13:29:29 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
pub fn delegation_from(account: &AccountSharedData) -> Option<Delegation> {
|
|
|
|
from(account).and_then(|state: StakeState| state.delegation())
|
2019-09-12 19:03:28 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
pub fn authorized_from(account: &AccountSharedData) -> Option<Authorized> {
|
|
|
|
from(account).and_then(|state: StakeState| state.authorized())
|
2019-12-19 14:37:47 -08:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
pub fn lockup_from<T: ReadableAccount + StateMut<StakeState>>(account: &T) -> Option<Lockup> {
|
|
|
|
from(account).and_then(|state: StakeState| state.lockup())
|
2019-09-26 13:29:29 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
pub fn meta_from(account: &AccountSharedData) -> Option<Meta> {
|
|
|
|
from(account).and_then(|state: StakeState| state.meta())
|
2019-10-31 11:07:27 -07:00
|
|
|
}
|
|
|
|
|
2022-06-28 10:39:00 -07:00
|
|
|
fn redelegate_stake(
|
|
|
|
invoke_context: &InvokeContext,
|
2021-06-15 09:04:00 -07:00
|
|
|
stake: &mut Stake,
|
|
|
|
stake_lamports: u64,
|
|
|
|
voter_pubkey: &Pubkey,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
clock: &Clock,
|
|
|
|
stake_history: &StakeHistory,
|
|
|
|
config: &Config,
|
|
|
|
) -> Result<(), StakeError> {
|
|
|
|
// If stake is currently active:
|
2021-08-19 22:08:44 -07:00
|
|
|
if stake.stake(clock.epoch, Some(stake_history)) != 0 {
|
2022-06-28 10:39:00 -07:00
|
|
|
let stake_lamports_ok = if invoke_context
|
|
|
|
.feature_set
|
|
|
|
.is_active(&feature_set::stake_redelegate_instruction::id())
|
|
|
|
{
|
|
|
|
// When a stake account is redelegated, the delegated lamports from the source stake
|
|
|
|
// account are transferred to a new stake account. Do not permit the deactivation of
|
|
|
|
// the source stake account to be rescinded, by more generally requiring the delegation
|
|
|
|
// be configured with the expected amount of stake lamports before rescinding.
|
|
|
|
stake_lamports >= stake.delegation.stake
|
|
|
|
} else {
|
|
|
|
true
|
|
|
|
};
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
// If pubkey of new voter is the same as current,
|
|
|
|
// and we are scheduled to start deactivating this epoch,
|
|
|
|
// we rescind deactivation
|
|
|
|
if stake.delegation.voter_pubkey == *voter_pubkey
|
|
|
|
&& clock.epoch == stake.delegation.deactivation_epoch
|
2022-06-28 10:39:00 -07:00
|
|
|
&& stake_lamports_ok
|
2021-06-15 09:04:00 -07:00
|
|
|
{
|
|
|
|
stake.delegation.deactivation_epoch = std::u64::MAX;
|
|
|
|
return Ok(());
|
2020-11-19 12:15:06 -08:00
|
|
|
} else {
|
2021-06-15 09:04:00 -07:00
|
|
|
// can't redelegate to another pubkey if stake is active.
|
|
|
|
return Err(StakeError::TooSoonToRedelegate);
|
2019-06-21 20:43:24 -07:00
|
|
|
}
|
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
// Either the stake is freshly activated, is active but has been
|
|
|
|
// deactivated this epoch, or has fully de-activated.
|
|
|
|
// Redelegation implies either re-activation or un-deactivation
|
|
|
|
|
|
|
|
stake.delegation.stake = stake_lamports;
|
|
|
|
stake.delegation.activation_epoch = clock.epoch;
|
|
|
|
stake.delegation.deactivation_epoch = std::u64::MAX;
|
|
|
|
stake.delegation.voter_pubkey = *voter_pubkey;
|
|
|
|
stake.delegation.warmup_cooldown_rate = config.warmup_cooldown_rate;
|
|
|
|
stake.credits_observed = vote_state.credits();
|
|
|
|
Ok(())
|
2019-06-21 20:43:24 -07:00
|
|
|
}
|
|
|
|
|
2022-03-25 09:11:51 -07:00
|
|
|
pub(crate) fn new_stake(
|
2021-06-15 09:04:00 -07:00
|
|
|
stake: u64,
|
|
|
|
voter_pubkey: &Pubkey,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
activation_epoch: Epoch,
|
|
|
|
config: &Config,
|
|
|
|
) -> Stake {
|
|
|
|
Stake {
|
|
|
|
delegation: Delegation::new(
|
|
|
|
voter_pubkey,
|
2019-11-25 13:14:32 -08:00
|
|
|
stake,
|
|
|
|
activation_epoch,
|
2021-06-15 09:04:00 -07:00
|
|
|
config.warmup_cooldown_rate,
|
|
|
|
),
|
|
|
|
credits_observed: vote_state.credits(),
|
2019-11-25 13:14:32 -08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-20 21:57:25 -07:00
|
|
|
/// captures a rewards round as lamports to be awarded
|
|
|
|
/// and the total points over which those lamports
|
|
|
|
/// are to be distributed
|
|
|
|
// basically read as rewards/points, but in integers instead of as an f64
|
2022-05-22 18:00:42 -07:00
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
2020-07-20 21:57:25 -07:00
|
|
|
pub struct PointValue {
|
|
|
|
pub rewards: u64, // lamports to split
|
|
|
|
pub points: u128, // over these points
|
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
fn redeem_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
rewarded_epoch: Epoch,
|
2021-06-15 09:04:00 -07:00
|
|
|
stake: &mut Stake,
|
|
|
|
point_value: &PointValue,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
stake_history: Option<&StakeHistory>,
|
2021-10-05 18:30:08 -07:00
|
|
|
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
|
2021-06-15 09:04:00 -07:00
|
|
|
) -> Option<(u64, u64)> {
|
2021-10-05 18:30:08 -07:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
2021-06-15 09:04:00 -07:00
|
|
|
inflation_point_calc_tracer(&InflationPointCalculationEvent::CreditsObserved(
|
|
|
|
stake.credits_observed,
|
|
|
|
None,
|
|
|
|
));
|
2019-11-25 13:14:32 -08:00
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
rewarded_epoch,
|
2021-06-15 09:04:00 -07:00
|
|
|
stake,
|
|
|
|
point_value,
|
|
|
|
vote_state,
|
|
|
|
stake_history,
|
2021-10-05 18:30:08 -07:00
|
|
|
inflation_point_calc_tracer.as_ref(),
|
2021-06-15 09:04:00 -07:00
|
|
|
)
|
2022-04-27 00:44:10 -07:00
|
|
|
.map(|calculated_stake_rewards| {
|
2021-02-11 22:24:23 -08:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer {
|
|
|
|
inflation_point_calc_tracer(&InflationPointCalculationEvent::CreditsObserved(
|
2021-06-15 09:04:00 -07:00
|
|
|
stake.credits_observed,
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(calculated_stake_rewards.new_credits_observed),
|
2021-02-11 22:24:23 -08:00
|
|
|
));
|
|
|
|
}
|
2022-04-27 00:44:10 -07:00
|
|
|
stake.credits_observed = calculated_stake_rewards.new_credits_observed;
|
|
|
|
stake.delegation.stake += calculated_stake_rewards.staker_rewards;
|
|
|
|
(
|
|
|
|
calculated_stake_rewards.staker_rewards,
|
|
|
|
calculated_stake_rewards.voter_rewards,
|
|
|
|
)
|
2021-06-15 09:04:00 -07:00
|
|
|
})
|
|
|
|
}
|
2020-01-22 12:21:31 -08:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
fn calculate_stake_points(
|
|
|
|
stake: &Stake,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
stake_history: Option<&StakeHistory>,
|
2021-10-05 18:30:08 -07:00
|
|
|
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
|
2021-06-15 09:04:00 -07:00
|
|
|
) -> u128 {
|
|
|
|
calculate_stake_points_and_credits(
|
|
|
|
stake,
|
|
|
|
vote_state,
|
|
|
|
stake_history,
|
|
|
|
inflation_point_calc_tracer,
|
|
|
|
)
|
2022-04-27 00:44:10 -07:00
|
|
|
.points
|
|
|
|
}
|
|
|
|
|
2022-05-22 18:00:42 -07:00
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
2022-04-27 00:44:10 -07:00
|
|
|
struct CalculatedStakePoints {
|
|
|
|
points: u128,
|
|
|
|
new_credits_observed: u64,
|
|
|
|
force_credits_update_with_skipped_reward: bool,
|
2021-06-15 09:04:00 -07:00
|
|
|
}
|
2020-07-20 21:57:25 -07:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
/// for a given stake and vote_state, calculate how many
|
|
|
|
/// points were earned (credits * stake) and new value
|
|
|
|
/// for credits_observed were the points paid
|
|
|
|
fn calculate_stake_points_and_credits(
|
|
|
|
stake: &Stake,
|
|
|
|
new_vote_state: &VoteState,
|
|
|
|
stake_history: Option<&StakeHistory>,
|
2021-10-05 18:30:08 -07:00
|
|
|
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
|
2022-04-27 00:44:10 -07:00
|
|
|
) -> CalculatedStakePoints {
|
2022-02-01 05:42:02 -08:00
|
|
|
let credits_in_stake = stake.credits_observed;
|
|
|
|
let credits_in_vote = new_vote_state.credits();
|
2021-06-15 09:04:00 -07:00
|
|
|
// if there is no newer credits since observed, return no point
|
2023-06-24 09:26:27 -07:00
|
|
|
match credits_in_vote.cmp(&credits_in_stake) {
|
|
|
|
Ordering::Less => {
|
2022-04-26 14:49:35 -07:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
|
|
|
inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnRewinded.into());
|
|
|
|
}
|
|
|
|
// Don't adjust stake.activation_epoch for simplicity:
|
|
|
|
// - generally fast-forwarding stake.activation_epoch forcibly (for
|
2022-06-10 16:02:47 -07:00
|
|
|
// artificial re-activation with re-warm-up) skews the stake
|
2022-04-26 14:49:35 -07:00
|
|
|
// history sysvar. And properly handling all the cases
|
|
|
|
// regarding deactivation epoch/warm-up/cool-down without
|
|
|
|
// introducing incentive skew is hard.
|
|
|
|
// - Conceptually, it should be acceptable for the staked SOLs at
|
|
|
|
// the recreated vote to receive rewards again immediately after
|
|
|
|
// rewind even if it looks like instant activation. That's
|
|
|
|
// because it must have passed the required warmed-up at least
|
|
|
|
// once in the past already
|
|
|
|
// - Also such a stake account remains to be a part of overall
|
|
|
|
// effective stake calculation even while the vote account is
|
|
|
|
// missing for (indefinite) time or remains to be pre-remove
|
|
|
|
// credits score. It should be treated equally to staking with
|
2022-06-10 16:02:47 -07:00
|
|
|
// delinquent validator with no differentiation.
|
2022-04-26 14:49:35 -07:00
|
|
|
|
|
|
|
// hint with true to indicate some exceptional credits handling is needed
|
2022-04-27 00:44:10 -07:00
|
|
|
return CalculatedStakePoints {
|
|
|
|
points: 0,
|
|
|
|
new_credits_observed: credits_in_vote,
|
|
|
|
force_credits_update_with_skipped_reward: true,
|
|
|
|
};
|
2023-06-24 09:26:27 -07:00
|
|
|
}
|
|
|
|
Ordering::Equal => {
|
2022-04-26 14:49:35 -07:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
|
|
|
inflation_point_calc_tracer(&SkippedReason::ZeroCreditsAndReturnCurrent.into());
|
|
|
|
}
|
2023-06-24 09:26:27 -07:00
|
|
|
// don't hint caller and return current value if credits remain unchanged (= delinquent)
|
2022-04-27 00:44:10 -07:00
|
|
|
return CalculatedStakePoints {
|
|
|
|
points: 0,
|
|
|
|
new_credits_observed: credits_in_stake,
|
|
|
|
force_credits_update_with_skipped_reward: false,
|
|
|
|
};
|
2023-06-24 09:26:27 -07:00
|
|
|
}
|
|
|
|
Ordering::Greater => {}
|
2020-07-20 21:57:25 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
let mut points = 0;
|
2022-02-01 05:42:02 -08:00
|
|
|
let mut new_credits_observed = credits_in_stake;
|
2021-06-15 09:04:00 -07:00
|
|
|
|
|
|
|
for (epoch, final_epoch_credits, initial_epoch_credits) in
|
|
|
|
new_vote_state.epoch_credits().iter().copied()
|
|
|
|
{
|
2021-08-19 22:08:44 -07:00
|
|
|
let stake_amount = u128::from(stake.delegation.stake(epoch, stake_history));
|
2020-11-18 17:34:51 -08:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
// figure out how much this stake has seen that
|
|
|
|
// for which the vote account has a record
|
2022-02-01 05:42:02 -08:00
|
|
|
let earned_credits = if credits_in_stake < initial_epoch_credits {
|
2021-06-15 09:04:00 -07:00
|
|
|
// the staker observed the entire epoch
|
|
|
|
final_epoch_credits - initial_epoch_credits
|
2022-02-01 05:42:02 -08:00
|
|
|
} else if credits_in_stake < final_epoch_credits {
|
2021-06-15 09:04:00 -07:00
|
|
|
// the staker registered sometime during the epoch, partial credit
|
|
|
|
final_epoch_credits - new_credits_observed
|
|
|
|
} else {
|
|
|
|
// the staker has already observed or been redeemed this epoch
|
|
|
|
// or was activated after this epoch
|
|
|
|
0
|
|
|
|
};
|
|
|
|
let earned_credits = u128::from(earned_credits);
|
2019-04-07 21:45:28 -07:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
// don't want to assume anything about order of the iterator...
|
|
|
|
new_credits_observed = new_credits_observed.max(final_epoch_credits);
|
2019-04-07 21:45:28 -07:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
// finally calculate points for this epoch
|
|
|
|
let earned_points = stake_amount * earned_credits;
|
|
|
|
points += earned_points;
|
2020-07-20 21:57:25 -07:00
|
|
|
|
2021-10-05 18:30:08 -07:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
2021-06-15 09:04:00 -07:00
|
|
|
inflation_point_calc_tracer(&InflationPointCalculationEvent::CalculatedPoints(
|
|
|
|
epoch,
|
|
|
|
stake_amount,
|
|
|
|
earned_credits,
|
|
|
|
earned_points,
|
2020-11-07 23:43:50 -08:00
|
|
|
));
|
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
}
|
2019-04-07 21:45:28 -07:00
|
|
|
|
2022-04-27 00:44:10 -07:00
|
|
|
CalculatedStakePoints {
|
|
|
|
points,
|
|
|
|
new_credits_observed,
|
|
|
|
force_credits_update_with_skipped_reward: false,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-05-22 18:00:42 -07:00
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
2022-04-27 00:44:10 -07:00
|
|
|
struct CalculatedStakeRewards {
|
|
|
|
staker_rewards: u64,
|
|
|
|
voter_rewards: u64,
|
|
|
|
new_credits_observed: u64,
|
2021-06-15 09:04:00 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
/// for a given stake and vote_state, calculate what distributions and what updates should be made
|
|
|
|
/// returns a tuple in the case of a payout of:
|
|
|
|
/// * staker_rewards to be distributed
|
|
|
|
/// * voter_rewards to be distributed
|
|
|
|
/// * new value for credits_observed in the stake
|
|
|
|
/// returns None if there's no payout or if any deserved payout is < 1 lamport
|
2021-08-19 22:08:44 -07:00
|
|
|
fn calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
rewarded_epoch: Epoch,
|
2021-06-15 09:04:00 -07:00
|
|
|
stake: &Stake,
|
|
|
|
point_value: &PointValue,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
stake_history: Option<&StakeHistory>,
|
2021-10-05 18:30:08 -07:00
|
|
|
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
|
2022-04-27 00:44:10 -07:00
|
|
|
) -> Option<CalculatedStakeRewards> {
|
2022-02-01 05:42:02 -08:00
|
|
|
// ensure to run to trigger (optional) inflation_point_calc_tracer
|
2022-04-27 00:44:10 -07:00
|
|
|
let CalculatedStakePoints {
|
|
|
|
points,
|
|
|
|
new_credits_observed,
|
|
|
|
mut force_credits_update_with_skipped_reward,
|
|
|
|
} = calculate_stake_points_and_credits(
|
|
|
|
stake,
|
|
|
|
vote_state,
|
|
|
|
stake_history,
|
|
|
|
inflation_point_calc_tracer.as_ref(),
|
|
|
|
);
|
2020-07-20 21:57:25 -07:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
// Drive credits_observed forward unconditionally when rewards are disabled
|
2021-09-03 14:20:56 -07:00
|
|
|
// or when this is the stake's activation epoch
|
2022-02-01 05:42:02 -08:00
|
|
|
if point_value.rewards == 0 {
|
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
|
|
|
inflation_point_calc_tracer(&SkippedReason::DisabledInflation.into());
|
|
|
|
}
|
2022-04-27 00:44:10 -07:00
|
|
|
force_credits_update_with_skipped_reward = true;
|
2022-02-01 05:42:02 -08:00
|
|
|
} else if stake.delegation.activation_epoch == rewarded_epoch {
|
|
|
|
// not assert!()-ed; but points should be zero
|
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
|
|
|
inflation_point_calc_tracer(&SkippedReason::JustActivated.into());
|
|
|
|
}
|
2022-04-27 00:44:10 -07:00
|
|
|
force_credits_update_with_skipped_reward = true;
|
2022-02-01 05:42:02 -08:00
|
|
|
}
|
|
|
|
|
2022-04-27 00:44:10 -07:00
|
|
|
if force_credits_update_with_skipped_reward {
|
|
|
|
return Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: 0,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed,
|
|
|
|
});
|
2019-04-07 21:45:28 -07:00
|
|
|
}
|
2019-06-17 19:34:21 -07:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
if points == 0 {
|
2022-02-01 05:42:02 -08:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
2021-06-15 09:04:00 -07:00
|
|
|
inflation_point_calc_tracer(&SkippedReason::ZeroPoints.into());
|
2019-09-11 09:48:29 -07:00
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
return None;
|
2019-09-11 09:48:29 -07:00
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
if point_value.points == 0 {
|
2022-02-01 05:42:02 -08:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
2021-06-15 09:04:00 -07:00
|
|
|
inflation_point_calc_tracer(&SkippedReason::ZeroPointValue.into());
|
2019-10-31 11:07:27 -07:00
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
return None;
|
2019-10-31 11:07:27 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
let rewards = points
|
|
|
|
.checked_mul(u128::from(point_value.rewards))
|
|
|
|
.unwrap()
|
|
|
|
.checked_div(point_value.points)
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let rewards = u64::try_from(rewards).unwrap();
|
|
|
|
|
|
|
|
// don't bother trying to split if fractional lamports got truncated
|
|
|
|
if rewards == 0 {
|
2022-02-01 05:42:02 -08:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
2021-06-15 09:04:00 -07:00
|
|
|
inflation_point_calc_tracer(&SkippedReason::ZeroReward.into());
|
2019-08-12 20:59:57 -07:00
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
return None;
|
|
|
|
}
|
|
|
|
let (voter_rewards, staker_rewards, is_split) = vote_state.commission_split(rewards);
|
2022-02-01 05:42:02 -08:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
2021-06-15 09:04:00 -07:00
|
|
|
inflation_point_calc_tracer(&InflationPointCalculationEvent::SplitRewards(
|
|
|
|
rewards,
|
|
|
|
voter_rewards,
|
|
|
|
staker_rewards,
|
|
|
|
(*point_value).clone(),
|
|
|
|
));
|
2019-06-17 19:34:21 -07:00
|
|
|
}
|
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
if (voter_rewards == 0 || staker_rewards == 0) && is_split {
|
|
|
|
// don't collect if we lose a whole lamport somewhere
|
|
|
|
// is_split means there should be tokens on both sides,
|
|
|
|
// uncool to move credits_observed if one side didn't get paid
|
2022-02-01 05:42:02 -08:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
|
|
|
inflation_point_calc_tracer(&SkippedReason::TooEarlyUnfairSplit.into());
|
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
return None;
|
2019-06-17 19:34:21 -07:00
|
|
|
}
|
2021-06-15 09:04:00 -07:00
|
|
|
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards,
|
|
|
|
voter_rewards,
|
|
|
|
new_credits_observed,
|
|
|
|
})
|
2019-06-17 19:34:21 -07:00
|
|
|
}
|
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
pub fn initialize(
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
authorized: &Authorized,
|
|
|
|
lockup: &Lockup,
|
|
|
|
rent: &Rent,
|
|
|
|
) -> Result<(), InstructionError> {
|
2022-04-19 10:04:12 -07:00
|
|
|
if stake_account.get_data().len() != StakeState::size_of() {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::InvalidAccountData);
|
|
|
|
}
|
2022-04-10 00:55:37 -07:00
|
|
|
if let StakeState::Uninitialized = stake_account.get_state()? {
|
|
|
|
let rent_exempt_reserve = rent.minimum_balance(stake_account.get_data().len());
|
2023-06-23 13:57:18 -07:00
|
|
|
if stake_account.get_lamports() >= rent_exempt_reserve {
|
2022-04-06 13:58:09 -07:00
|
|
|
stake_account.set_state(&StakeState::Initialized(Meta {
|
|
|
|
rent_exempt_reserve,
|
|
|
|
authorized: *authorized,
|
|
|
|
lockup: *lockup,
|
|
|
|
}))
|
|
|
|
} else {
|
|
|
|
Err(InstructionError::InsufficientFunds)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(InstructionError::InvalidAccountData)
|
|
|
|
}
|
2019-04-01 16:45:53 -07:00
|
|
|
}
|
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
/// Authorize the given pubkey to manage stake (deactivate, withdraw). This may be called
|
|
|
|
/// multiple times, but will implicitly withdraw authorization from the previously authorized
|
|
|
|
/// staker. The default staker is the owner of the stake account's pubkey.
|
|
|
|
pub fn authorize(
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
signers: &HashSet<Pubkey>,
|
|
|
|
new_authority: &Pubkey,
|
|
|
|
stake_authorize: StakeAuthorize,
|
|
|
|
require_custodian_for_locked_stake_authorize: bool,
|
|
|
|
clock: &Clock,
|
|
|
|
custodian: Option<&Pubkey>,
|
|
|
|
) -> Result<(), InstructionError> {
|
2022-04-10 00:55:37 -07:00
|
|
|
match stake_account.get_state()? {
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Stake(mut meta, stake, stake_flags) => {
|
2022-04-06 13:58:09 -07:00
|
|
|
meta.authorized.authorize(
|
|
|
|
signers,
|
|
|
|
new_authority,
|
|
|
|
stake_authorize,
|
|
|
|
if require_custodian_for_locked_stake_authorize {
|
|
|
|
Some((&meta.lockup, clock, custodian))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))
|
2020-11-23 12:13:38 -08:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Initialized(mut meta) => {
|
|
|
|
meta.authorized.authorize(
|
|
|
|
signers,
|
|
|
|
new_authority,
|
|
|
|
stake_authorize,
|
|
|
|
if require_custodian_for_locked_stake_authorize {
|
|
|
|
Some((&meta.lockup, clock, custodian))
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
},
|
|
|
|
)?;
|
|
|
|
stake_account.set_state(&StakeState::Initialized(meta))
|
2019-09-12 19:03:28 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
_ => Err(InstructionError::InvalidAccountData),
|
2019-09-12 19:03:28 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
}
|
2019-10-31 11:07:27 -07:00
|
|
|
|
2022-04-10 00:55:37 -07:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-04-06 13:58:09 -07:00
|
|
|
pub fn authorize_with_seed(
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-09-06 02:31:40 -07:00
|
|
|
authority_base_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
authority_seed: &str,
|
|
|
|
authority_owner: &Pubkey,
|
|
|
|
new_authority: &Pubkey,
|
|
|
|
stake_authorize: StakeAuthorize,
|
|
|
|
require_custodian_for_locked_stake_authorize: bool,
|
|
|
|
clock: &Clock,
|
|
|
|
custodian: Option<&Pubkey>,
|
|
|
|
) -> Result<(), InstructionError> {
|
|
|
|
let mut signers = HashSet::default();
|
2022-06-16 09:46:17 -07:00
|
|
|
if instruction_context.is_instruction_account_signer(authority_base_index)? {
|
2022-04-10 00:55:37 -07:00
|
|
|
let base_pubkey = transaction_context.get_key_of_account_at_index(
|
2022-06-16 09:46:17 -07:00
|
|
|
instruction_context
|
|
|
|
.get_index_of_instruction_account_in_transaction(authority_base_index)?,
|
2022-04-10 00:55:37 -07:00
|
|
|
)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
signers.insert(Pubkey::create_with_seed(
|
|
|
|
base_pubkey,
|
|
|
|
authority_seed,
|
|
|
|
authority_owner,
|
|
|
|
)?);
|
2019-09-04 13:34:09 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
authorize(
|
|
|
|
stake_account,
|
|
|
|
&signers,
|
|
|
|
new_authority,
|
|
|
|
stake_authorize,
|
|
|
|
require_custodian_for_locked_stake_authorize,
|
|
|
|
clock,
|
|
|
|
custodian,
|
|
|
|
)
|
|
|
|
}
|
2020-11-16 12:42:59 -08:00
|
|
|
|
2022-06-28 10:39:00 -07:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-04-06 13:58:09 -07:00
|
|
|
pub fn delegate(
|
2022-06-28 10:39:00 -07:00
|
|
|
invoke_context: &InvokeContext,
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
2022-09-06 02:31:40 -07:00
|
|
|
stake_account_index: IndexOfAccount,
|
|
|
|
vote_account_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
clock: &Clock,
|
|
|
|
stake_history: &StakeHistory,
|
|
|
|
config: &Config,
|
|
|
|
signers: &HashSet<Pubkey>,
|
2022-05-04 03:17:29 -07:00
|
|
|
feature_set: &FeatureSet,
|
2022-04-06 13:58:09 -07:00
|
|
|
) -> Result<(), InstructionError> {
|
2022-06-16 09:46:17 -07:00
|
|
|
let vote_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, vote_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
if *vote_account.get_owner() != solana_vote_program::id() {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
2019-04-01 16:45:53 -07:00
|
|
|
}
|
2022-04-10 00:55:37 -07:00
|
|
|
let vote_pubkey = *vote_account.get_key();
|
|
|
|
let vote_state = vote_account.get_state::<VoteStateVersions>();
|
|
|
|
drop(vote_account);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
match stake_account.get_state()? {
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Initialized(meta) => {
|
|
|
|
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
|
|
|
let ValidatedDelegatedInfo { stake_amount } =
|
2022-05-04 03:17:29 -07:00
|
|
|
validate_delegated_amount(&stake_account, &meta, feature_set)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
let stake = new_stake(
|
|
|
|
stake_amount,
|
2022-04-10 00:55:37 -07:00
|
|
|
&vote_pubkey,
|
|
|
|
&vote_state?.convert_to_current(),
|
2022-04-06 13:58:09 -07:00
|
|
|
clock.epoch,
|
|
|
|
config,
|
|
|
|
);
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, StakeFlags::empty()))
|
2019-06-21 20:43:24 -07:00
|
|
|
}
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Stake(meta, mut stake, stake_flags) => {
|
2022-04-06 13:58:09 -07:00
|
|
|
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
|
|
|
let ValidatedDelegatedInfo { stake_amount } =
|
2022-05-04 03:17:29 -07:00
|
|
|
validate_delegated_amount(&stake_account, &meta, feature_set)?;
|
2022-06-28 10:39:00 -07:00
|
|
|
redelegate_stake(
|
|
|
|
invoke_context,
|
2022-04-06 13:58:09 -07:00
|
|
|
&mut stake,
|
|
|
|
stake_amount,
|
2022-04-10 00:55:37 -07:00
|
|
|
&vote_pubkey,
|
|
|
|
&vote_state?.convert_to_current(),
|
2022-04-06 13:58:09 -07:00
|
|
|
clock,
|
|
|
|
stake_history,
|
|
|
|
config,
|
|
|
|
)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))
|
2020-01-28 20:59:53 -08:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
_ => Err(InstructionError::InvalidAccountData),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deactivate(
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
clock: &Clock,
|
|
|
|
signers: &HashSet<Pubkey>,
|
|
|
|
) -> Result<(), InstructionError> {
|
2023-07-24 07:09:40 -07:00
|
|
|
if let StakeState::Stake(meta, mut stake, stake_flags) = stake_account.get_state()? {
|
2022-04-06 13:58:09 -07:00
|
|
|
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
|
|
|
stake.deactivate(clock.epoch)?;
|
|
|
|
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))
|
2022-04-06 13:58:09 -07:00
|
|
|
} else {
|
|
|
|
Err(InstructionError::InvalidAccountData)
|
2020-01-28 20:59:53 -08:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
}
|
2019-10-31 11:07:27 -07:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
pub fn set_lockup(
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
lockup: &LockupArgs,
|
|
|
|
signers: &HashSet<Pubkey>,
|
|
|
|
clock: &Clock,
|
|
|
|
) -> Result<(), InstructionError> {
|
2022-04-10 00:55:37 -07:00
|
|
|
match stake_account.get_state()? {
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Initialized(mut meta) => {
|
|
|
|
meta.set_lockup(lockup, signers, clock)?;
|
|
|
|
stake_account.set_state(&StakeState::Initialized(meta))
|
2020-11-16 12:42:59 -08:00
|
|
|
}
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Stake(mut meta, stake, stake_flags) => {
|
2022-04-06 13:58:09 -07:00
|
|
|
meta.set_lockup(lockup, signers, clock)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))
|
2021-02-16 19:42:46 -08:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
_ => Err(InstructionError::InvalidAccountData),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn split(
|
|
|
|
invoke_context: &InvokeContext,
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
2022-09-06 02:31:40 -07:00
|
|
|
stake_account_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
lamports: u64,
|
2022-09-06 02:31:40 -07:00
|
|
|
split_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
signers: &HashSet<Pubkey>,
|
|
|
|
) -> Result<(), InstructionError> {
|
2022-06-16 09:46:17 -07:00
|
|
|
let split =
|
|
|
|
instruction_context.try_borrow_instruction_account(transaction_context, split_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
if *split.get_owner() != id() {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
|
|
|
}
|
2022-04-19 10:04:12 -07:00
|
|
|
if split.get_data().len() != StakeState::size_of() {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::InvalidAccountData);
|
|
|
|
}
|
2022-04-10 00:55:37 -07:00
|
|
|
if !matches!(split.get_state()?, StakeState::Uninitialized) {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::InvalidAccountData);
|
|
|
|
}
|
2022-04-10 00:55:37 -07:00
|
|
|
let split_lamport_balance = split.get_lamports();
|
|
|
|
drop(split);
|
2022-06-16 09:46:17 -07:00
|
|
|
let stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
if lamports > stake_account.get_lamports() {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
2022-04-10 00:55:37 -07:00
|
|
|
let stake_state = stake_account.get_state()?;
|
|
|
|
drop(stake_account);
|
2022-04-06 13:58:09 -07:00
|
|
|
|
2022-04-10 00:55:37 -07:00
|
|
|
match stake_state {
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Stake(meta, mut stake, stake_flags) => {
|
2022-04-06 13:58:09 -07:00
|
|
|
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
2022-05-04 03:17:29 -07:00
|
|
|
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
2022-04-06 13:58:09 -07:00
|
|
|
let validated_split_info = validate_split_amount(
|
|
|
|
invoke_context,
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context,
|
|
|
|
instruction_context,
|
|
|
|
stake_account_index,
|
|
|
|
split_index,
|
2022-04-06 13:58:09 -07:00
|
|
|
lamports,
|
|
|
|
&meta,
|
|
|
|
Some(&stake),
|
2022-05-04 03:17:29 -07:00
|
|
|
minimum_delegation,
|
2022-04-06 13:58:09 -07:00
|
|
|
)?;
|
|
|
|
|
|
|
|
// split the stake, subtract rent_exempt_balance unless
|
|
|
|
// the destination account already has those lamports
|
|
|
|
// in place.
|
|
|
|
// this means that the new stake account will have a stake equivalent to
|
|
|
|
// lamports minus rent_exempt_reserve if it starts out with a zero balance
|
|
|
|
let (remaining_stake_delta, split_stake_amount) =
|
|
|
|
if validated_split_info.source_remaining_balance == 0 {
|
|
|
|
// If split amount equals the full source stake (as implied by 0
|
|
|
|
// source_remaining_balance), the new split stake must equal the same
|
|
|
|
// amount, regardless of any current lamport balance in the split account.
|
|
|
|
// Since split accounts retain the state of their source account, this
|
|
|
|
// prevents any magic activation of stake by prefunding the split account.
|
|
|
|
//
|
|
|
|
// The new split stake also needs to ignore any positive delta between the
|
|
|
|
// original rent_exempt_reserve and the split_rent_exempt_reserve, in order
|
|
|
|
// to prevent magic activation of stake by splitting between accounts of
|
|
|
|
// different sizes.
|
|
|
|
let remaining_stake_delta = lamports.saturating_sub(meta.rent_exempt_reserve);
|
|
|
|
(remaining_stake_delta, remaining_stake_delta)
|
|
|
|
} else {
|
|
|
|
// Otherwise, the new split stake should reflect the entire split
|
|
|
|
// requested, less any lamports needed to cover the split_rent_exempt_reserve.
|
2023-04-14 15:30:59 -07:00
|
|
|
|
|
|
|
if invoke_context
|
|
|
|
.feature_set
|
|
|
|
.is_active(&clean_up_delegation_errors::id())
|
|
|
|
&& stake.delegation.stake.saturating_sub(lamports) < minimum_delegation
|
|
|
|
{
|
|
|
|
return Err(StakeError::InsufficientDelegation.into());
|
|
|
|
}
|
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
(
|
|
|
|
lamports,
|
|
|
|
lamports.saturating_sub(
|
|
|
|
validated_split_info
|
|
|
|
.destination_rent_exempt_reserve
|
2022-04-10 00:55:37 -07:00
|
|
|
.saturating_sub(split_lamport_balance),
|
2022-04-06 13:58:09 -07:00
|
|
|
),
|
|
|
|
)
|
|
|
|
};
|
2023-04-14 15:30:59 -07:00
|
|
|
|
|
|
|
if invoke_context
|
|
|
|
.feature_set
|
|
|
|
.is_active(&clean_up_delegation_errors::id())
|
|
|
|
&& split_stake_amount < minimum_delegation
|
|
|
|
{
|
|
|
|
return Err(StakeError::InsufficientDelegation.into());
|
|
|
|
}
|
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
let split_stake = stake.split(remaining_stake_delta, split_stake_amount)?;
|
|
|
|
let mut split_meta = meta;
|
|
|
|
split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve;
|
|
|
|
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))?;
|
2022-04-10 00:55:37 -07:00
|
|
|
drop(stake_account);
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut split = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, split_index)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
split.set_state(&StakeState::Stake(split_meta, split_stake, stake_flags))?;
|
2022-03-16 08:56:48 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Initialized(meta) => {
|
|
|
|
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
let validated_split_info = validate_split_amount(
|
|
|
|
invoke_context,
|
|
|
|
transaction_context,
|
|
|
|
instruction_context,
|
|
|
|
stake_account_index,
|
|
|
|
split_index,
|
|
|
|
lamports,
|
|
|
|
&meta,
|
|
|
|
None,
|
2023-06-23 13:57:18 -07:00
|
|
|
0, // additional_required_lamports
|
2022-04-10 00:55:37 -07:00
|
|
|
)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
let mut split_meta = meta;
|
|
|
|
split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve;
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut split = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, split_index)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
split.set_state(&StakeState::Initialized(split_meta))?;
|
2022-03-16 08:56:48 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Uninitialized => {
|
2022-04-10 00:55:37 -07:00
|
|
|
let stake_pubkey = transaction_context.get_key_of_account_at_index(
|
2022-06-16 09:46:17 -07:00
|
|
|
instruction_context
|
|
|
|
.get_index_of_instruction_account_in_transaction(stake_account_index)?,
|
2022-04-10 00:55:37 -07:00
|
|
|
)?;
|
|
|
|
if !signers.contains(stake_pubkey) {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::MissingRequiredSignature);
|
2019-10-31 11:07:27 -07:00
|
|
|
}
|
2022-03-16 08:56:48 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
_ => return Err(InstructionError::InvalidAccountData),
|
|
|
|
}
|
2020-12-21 18:54:49 -08:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
// Deinitialize state upon zero balance
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
if lamports == stake_account.get_lamports() {
|
2022-04-06 13:58:09 -07:00
|
|
|
stake_account.set_state(&StakeState::Uninitialized)?;
|
|
|
|
}
|
2022-04-10 00:55:37 -07:00
|
|
|
drop(stake_account);
|
2022-03-16 08:56:48 -07:00
|
|
|
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut split =
|
|
|
|
instruction_context.try_borrow_instruction_account(transaction_context, split_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
split.checked_add_lamports(lamports)?;
|
|
|
|
drop(split);
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_account.checked_sub_lamports(lamports)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
2020-11-16 12:42:59 -08:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
pub fn merge(
|
|
|
|
invoke_context: &InvokeContext,
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
2022-09-06 02:31:40 -07:00
|
|
|
stake_account_index: IndexOfAccount,
|
|
|
|
source_account_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
clock: &Clock,
|
|
|
|
stake_history: &StakeHistory,
|
|
|
|
signers: &HashSet<Pubkey>,
|
|
|
|
) -> Result<(), InstructionError> {
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut source_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, source_account_index)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
// Ensure source isn't spoofed
|
2022-04-10 00:55:37 -07:00
|
|
|
if *source_account.get_owner() != id() {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
|
|
|
}
|
|
|
|
// Close the stake_account-reference loophole
|
2022-06-16 09:46:17 -07:00
|
|
|
if instruction_context.get_index_of_instruction_account_in_transaction(stake_account_index)?
|
|
|
|
== instruction_context
|
|
|
|
.get_index_of_instruction_account_in_transaction(source_account_index)?
|
2022-04-10 00:55:37 -07:00
|
|
|
{
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::InvalidArgument);
|
|
|
|
}
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2020-11-19 11:53:05 -08:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
ic_msg!(invoke_context, "Checking if destination stake is mergeable");
|
2022-04-08 11:40:09 -07:00
|
|
|
let stake_merge_kind = MergeKind::get_if_mergeable(
|
|
|
|
invoke_context,
|
2022-04-10 00:55:37 -07:00
|
|
|
&stake_account.get_state()?,
|
|
|
|
stake_account.get_lamports(),
|
2022-04-08 11:40:09 -07:00
|
|
|
clock,
|
|
|
|
stake_history,
|
|
|
|
)?;
|
2020-06-10 17:22:47 -07:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
// Authorized staker is allowed to split/merge accounts
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_merge_kind
|
|
|
|
.meta()
|
|
|
|
.authorized
|
|
|
|
.check(signers, StakeAuthorize::Staker)?;
|
2020-06-10 17:22:47 -07:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
ic_msg!(invoke_context, "Checking if source stake is mergeable");
|
2022-04-08 11:40:09 -07:00
|
|
|
let source_merge_kind = MergeKind::get_if_mergeable(
|
|
|
|
invoke_context,
|
2022-04-10 00:55:37 -07:00
|
|
|
&source_account.get_state()?,
|
|
|
|
source_account.get_lamports(),
|
2022-04-08 11:40:09 -07:00
|
|
|
clock,
|
|
|
|
stake_history,
|
|
|
|
)?;
|
2020-06-10 17:22:47 -07:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
ic_msg!(invoke_context, "Merging stake accounts");
|
|
|
|
if let Some(merged_state) = stake_merge_kind.merge(invoke_context, source_merge_kind, clock)? {
|
|
|
|
stake_account.set_state(&merged_state)?;
|
|
|
|
}
|
2019-08-17 18:12:30 -07:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
// Source is about to be drained, deinitialize its state
|
|
|
|
source_account.set_state(&StakeState::Uninitialized)?;
|
|
|
|
|
|
|
|
// Drain the source stake account
|
2022-04-10 00:55:37 -07:00
|
|
|
let lamports = source_account.get_lamports();
|
|
|
|
source_account.checked_sub_lamports(lamports)?;
|
|
|
|
stake_account.checked_add_lamports(lamports)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
Ok(())
|
|
|
|
}
|
2019-09-16 17:47:42 -07:00
|
|
|
|
2022-06-28 10:39:00 -07:00
|
|
|
pub fn redelegate(
|
|
|
|
invoke_context: &InvokeContext,
|
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-09-06 02:31:40 -07:00
|
|
|
uninitialized_stake_account_index: IndexOfAccount,
|
|
|
|
vote_account_index: IndexOfAccount,
|
2022-06-28 10:39:00 -07:00
|
|
|
config: &Config,
|
|
|
|
signers: &HashSet<Pubkey>,
|
|
|
|
) -> Result<(), InstructionError> {
|
|
|
|
let clock = invoke_context.get_sysvar_cache().get_clock()?;
|
|
|
|
|
|
|
|
// ensure `uninitialized_stake_account_index` is in the uninitialized state
|
|
|
|
let mut uninitialized_stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, uninitialized_stake_account_index)?;
|
|
|
|
if *uninitialized_stake_account.get_owner() != id() {
|
|
|
|
ic_msg!(
|
|
|
|
invoke_context,
|
|
|
|
"expected uninitialized stake account owner to be {}, not {}",
|
|
|
|
id(),
|
|
|
|
*uninitialized_stake_account.get_owner()
|
|
|
|
);
|
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
|
|
|
}
|
|
|
|
if uninitialized_stake_account.get_data().len() != StakeState::size_of() {
|
|
|
|
ic_msg!(
|
|
|
|
invoke_context,
|
|
|
|
"expected uninitialized stake account data len to be {}, not {}",
|
|
|
|
StakeState::size_of(),
|
|
|
|
uninitialized_stake_account.get_data().len()
|
|
|
|
);
|
|
|
|
return Err(InstructionError::InvalidAccountData);
|
|
|
|
}
|
|
|
|
if !matches!(
|
|
|
|
uninitialized_stake_account.get_state()?,
|
|
|
|
StakeState::Uninitialized
|
|
|
|
) {
|
|
|
|
ic_msg!(
|
|
|
|
invoke_context,
|
|
|
|
"expected uninitialized stake account to be uninitialized",
|
|
|
|
);
|
|
|
|
return Err(InstructionError::AccountAlreadyInitialized);
|
|
|
|
}
|
|
|
|
|
|
|
|
// validate the provided vote account
|
|
|
|
let vote_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, vote_account_index)?;
|
|
|
|
if *vote_account.get_owner() != solana_vote_program::id() {
|
|
|
|
ic_msg!(
|
|
|
|
invoke_context,
|
|
|
|
"expected vote account owner to be {}, not {}",
|
|
|
|
solana_vote_program::id(),
|
|
|
|
*vote_account.get_owner()
|
|
|
|
);
|
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
|
|
|
}
|
|
|
|
let vote_pubkey = *vote_account.get_key();
|
|
|
|
let vote_state = vote_account.get_state::<VoteStateVersions>()?;
|
|
|
|
|
|
|
|
let (stake_meta, effective_stake) =
|
2023-07-24 07:09:40 -07:00
|
|
|
if let StakeState::Stake(meta, stake, _stake_flags) = stake_account.get_state()? {
|
2022-06-28 10:39:00 -07:00
|
|
|
let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?;
|
|
|
|
let status = stake
|
|
|
|
.delegation
|
|
|
|
.stake_activating_and_deactivating(clock.epoch, Some(&stake_history));
|
|
|
|
if status.effective == 0 || status.activating != 0 || status.deactivating != 0 {
|
|
|
|
ic_msg!(invoke_context, "stake is not active");
|
|
|
|
return Err(StakeError::RedelegateTransientOrInactiveStake.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Deny redelegating to the same vote account. This is nonsensical and could be used to
|
|
|
|
// grief the global stake warm-up/cool-down rate
|
|
|
|
if stake.delegation.voter_pubkey == vote_pubkey {
|
|
|
|
ic_msg!(
|
|
|
|
invoke_context,
|
|
|
|
"redelegating to the same vote account not permitted"
|
|
|
|
);
|
|
|
|
return Err(StakeError::RedelegateToSameVoteAccount.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
(meta, status.effective)
|
|
|
|
} else {
|
|
|
|
ic_msg!(invoke_context, "invalid stake account data",);
|
|
|
|
return Err(InstructionError::InvalidAccountData);
|
|
|
|
};
|
|
|
|
|
|
|
|
// deactivate `stake_account`
|
|
|
|
//
|
|
|
|
// Note: This function also ensures `signers` contains the `StakeAuthorize::Staker`
|
|
|
|
deactivate(stake_account, &clock, signers)?;
|
|
|
|
|
|
|
|
// transfer the effective stake to the uninitialized stake account
|
|
|
|
stake_account.checked_sub_lamports(effective_stake)?;
|
|
|
|
uninitialized_stake_account.checked_add_lamports(effective_stake)?;
|
|
|
|
|
|
|
|
// initialize and schedule `uninitialized_stake_account` for activation
|
|
|
|
let sysvar_cache = invoke_context.get_sysvar_cache();
|
|
|
|
let rent = sysvar_cache.get_rent()?;
|
|
|
|
let mut uninitialized_stake_meta = stake_meta;
|
|
|
|
uninitialized_stake_meta.rent_exempt_reserve =
|
|
|
|
rent.minimum_balance(uninitialized_stake_account.get_data().len());
|
|
|
|
|
|
|
|
let ValidatedDelegatedInfo { stake_amount } = validate_delegated_amount(
|
|
|
|
&uninitialized_stake_account,
|
|
|
|
&uninitialized_stake_meta,
|
|
|
|
&invoke_context.feature_set,
|
|
|
|
)?;
|
|
|
|
uninitialized_stake_account.set_state(&StakeState::Stake(
|
|
|
|
uninitialized_stake_meta,
|
|
|
|
new_stake(
|
|
|
|
stake_amount,
|
|
|
|
&vote_pubkey,
|
|
|
|
&vote_state.convert_to_current(),
|
|
|
|
clock.epoch,
|
|
|
|
config,
|
|
|
|
),
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeFlags::empty(),
|
2022-06-28 10:39:00 -07:00
|
|
|
))?;
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-04-10 00:55:37 -07:00
|
|
|
#[allow(clippy::too_many_arguments)]
|
2022-04-06 13:58:09 -07:00
|
|
|
pub fn withdraw(
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
2022-09-06 02:31:40 -07:00
|
|
|
stake_account_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
lamports: u64,
|
2022-09-06 02:31:40 -07:00
|
|
|
to_index: IndexOfAccount,
|
2022-04-06 13:58:09 -07:00
|
|
|
clock: &Clock,
|
|
|
|
stake_history: &StakeHistory,
|
2022-09-06 02:31:40 -07:00
|
|
|
withdraw_authority_index: IndexOfAccount,
|
|
|
|
custodian_index: Option<IndexOfAccount>,
|
2022-04-06 13:58:09 -07:00
|
|
|
) -> Result<(), InstructionError> {
|
2022-04-10 00:55:37 -07:00
|
|
|
let withdraw_authority_pubkey = transaction_context.get_key_of_account_at_index(
|
2022-06-16 09:46:17 -07:00
|
|
|
instruction_context
|
|
|
|
.get_index_of_instruction_account_in_transaction(withdraw_authority_index)?,
|
2022-04-10 00:55:37 -07:00
|
|
|
)?;
|
2022-06-16 09:46:17 -07:00
|
|
|
if !instruction_context.is_instruction_account_signer(withdraw_authority_index)? {
|
2022-04-10 00:55:37 -07:00
|
|
|
return Err(InstructionError::MissingRequiredSignature);
|
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
let mut signers = HashSet::new();
|
|
|
|
signers.insert(*withdraw_authority_pubkey);
|
|
|
|
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut stake_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, stake_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
let (lockup, reserve, is_staked) = match stake_account.get_state()? {
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Stake(meta, stake, _stake_flag) => {
|
2022-04-06 13:58:09 -07:00
|
|
|
meta.authorized
|
|
|
|
.check(&signers, StakeAuthorize::Withdrawer)?;
|
|
|
|
// if we have a deactivation epoch and we're in cooldown
|
|
|
|
let staked = if clock.epoch >= stake.delegation.deactivation_epoch {
|
|
|
|
stake.delegation.stake(clock.epoch, Some(stake_history))
|
|
|
|
} else {
|
|
|
|
// Assume full stake if the stake account hasn't been
|
|
|
|
// de-activated, because in the future the exposed stake
|
|
|
|
// might be higher than stake.stake() due to warmup
|
|
|
|
stake.delegation.stake
|
|
|
|
};
|
|
|
|
|
|
|
|
let staked_and_reserve = checked_add(staked, meta.rent_exempt_reserve)?;
|
|
|
|
(meta.lockup, staked_and_reserve, staked != 0)
|
2019-06-21 22:28:34 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Initialized(meta) => {
|
|
|
|
meta.authorized
|
|
|
|
.check(&signers, StakeAuthorize::Withdrawer)?;
|
2022-05-04 03:17:29 -07:00
|
|
|
// stake accounts must have a balance >= rent_exempt_reserve
|
2023-06-23 13:57:18 -07:00
|
|
|
(meta.lockup, meta.rent_exempt_reserve, false)
|
2019-10-31 11:07:27 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
StakeState::Uninitialized => {
|
2022-04-10 00:55:37 -07:00
|
|
|
if !signers.contains(stake_account.get_key()) {
|
2022-04-06 13:58:09 -07:00
|
|
|
return Err(InstructionError::MissingRequiredSignature);
|
|
|
|
}
|
|
|
|
(Lockup::default(), 0, false) // no lockup, no restrictions
|
2019-09-16 17:47:42 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
_ => return Err(InstructionError::InvalidAccountData),
|
|
|
|
};
|
|
|
|
|
|
|
|
// verify that lockup has expired or that the withdrawal is signed by
|
|
|
|
// the custodian, both epoch and unix_timestamp must have passed
|
2022-04-10 00:55:37 -07:00
|
|
|
let custodian_pubkey = if let Some(custodian_index) = custodian_index {
|
2022-06-16 09:46:17 -07:00
|
|
|
if instruction_context.is_instruction_account_signer(custodian_index)? {
|
|
|
|
Some(
|
|
|
|
transaction_context.get_key_of_account_at_index(
|
|
|
|
instruction_context
|
|
|
|
.get_index_of_instruction_account_in_transaction(custodian_index)?,
|
|
|
|
)?,
|
|
|
|
)
|
2022-04-10 00:55:37 -07:00
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
};
|
2022-04-06 13:58:09 -07:00
|
|
|
if lockup.is_in_force(clock, custodian_pubkey) {
|
|
|
|
return Err(StakeError::LockupInForce.into());
|
|
|
|
}
|
2019-09-16 17:47:42 -07:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
let lamports_and_reserve = checked_add(lamports, reserve)?;
|
|
|
|
// if the stake is active, we mustn't allow the account to go away
|
|
|
|
if is_staked // line coverage for branch coverage
|
2022-04-10 00:55:37 -07:00
|
|
|
&& lamports_and_reserve > stake_account.get_lamports()
|
2022-04-06 13:58:09 -07:00
|
|
|
{
|
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
|
|
|
|
2022-04-10 00:55:37 -07:00
|
|
|
if lamports != stake_account.get_lamports() // not a full withdrawal
|
|
|
|
&& lamports_and_reserve > stake_account.get_lamports()
|
2022-04-06 13:58:09 -07:00
|
|
|
{
|
|
|
|
assert!(!is_staked);
|
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
2020-12-21 18:54:49 -08:00
|
|
|
|
2022-04-06 13:58:09 -07:00
|
|
|
// Deinitialize state upon zero balance
|
2022-04-10 00:55:37 -07:00
|
|
|
if lamports == stake_account.get_lamports() {
|
2022-04-06 13:58:09 -07:00
|
|
|
stake_account.set_state(&StakeState::Uninitialized)?;
|
2019-06-21 22:28:34 -07:00
|
|
|
}
|
2022-04-06 13:58:09 -07:00
|
|
|
|
2022-04-10 00:55:37 -07:00
|
|
|
stake_account.checked_sub_lamports(lamports)?;
|
|
|
|
drop(stake_account);
|
2022-06-16 09:46:17 -07:00
|
|
|
let mut to =
|
|
|
|
instruction_context.try_borrow_instruction_account(transaction_context, to_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
to.checked_add_lamports(lamports)?;
|
2022-04-06 13:58:09 -07:00
|
|
|
Ok(())
|
2019-04-01 16:45:53 -07:00
|
|
|
}
|
|
|
|
|
2022-03-25 09:11:51 -07:00
|
|
|
pub(crate) fn deactivate_delinquent(
|
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
|
|
|
stake_account: &mut BorrowedAccount,
|
2022-09-06 02:31:40 -07:00
|
|
|
delinquent_vote_account_index: IndexOfAccount,
|
|
|
|
reference_vote_account_index: IndexOfAccount,
|
2022-03-25 09:11:51 -07:00
|
|
|
current_epoch: Epoch,
|
|
|
|
) -> Result<(), InstructionError> {
|
|
|
|
let delinquent_vote_account_pubkey = transaction_context.get_key_of_account_at_index(
|
2022-06-16 09:46:17 -07:00
|
|
|
instruction_context
|
|
|
|
.get_index_of_instruction_account_in_transaction(delinquent_vote_account_index)?,
|
2022-03-25 09:11:51 -07:00
|
|
|
)?;
|
|
|
|
let delinquent_vote_account = instruction_context
|
2022-06-16 09:46:17 -07:00
|
|
|
.try_borrow_instruction_account(transaction_context, delinquent_vote_account_index)?;
|
2022-03-25 09:11:51 -07:00
|
|
|
if *delinquent_vote_account.get_owner() != solana_vote_program::id() {
|
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
|
|
|
}
|
|
|
|
let delinquent_vote_state = delinquent_vote_account
|
|
|
|
.get_state::<VoteStateVersions>()?
|
|
|
|
.convert_to_current();
|
|
|
|
|
|
|
|
let reference_vote_account = instruction_context
|
2022-06-16 09:46:17 -07:00
|
|
|
.try_borrow_instruction_account(transaction_context, reference_vote_account_index)?;
|
2022-03-25 09:11:51 -07:00
|
|
|
if *reference_vote_account.get_owner() != solana_vote_program::id() {
|
|
|
|
return Err(InstructionError::IncorrectProgramId);
|
|
|
|
}
|
|
|
|
let reference_vote_state = reference_vote_account
|
|
|
|
.get_state::<VoteStateVersions>()?
|
|
|
|
.convert_to_current();
|
|
|
|
|
|
|
|
if !acceptable_reference_epoch_credits(&reference_vote_state.epoch_credits, current_epoch) {
|
|
|
|
return Err(StakeError::InsufficientReferenceVotes.into());
|
|
|
|
}
|
|
|
|
|
2023-07-24 07:09:40 -07:00
|
|
|
if let StakeState::Stake(meta, mut stake, stake_flags) = stake_account.get_state()? {
|
2022-03-25 09:11:51 -07:00
|
|
|
if stake.delegation.voter_pubkey != *delinquent_vote_account_pubkey {
|
|
|
|
return Err(StakeError::VoteAddressMismatch.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
// Deactivate the stake account if its delegated vote account has never voted or has not
|
|
|
|
// voted in the last `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`
|
|
|
|
if eligible_for_deactivate_delinquent(&delinquent_vote_state.epoch_credits, current_epoch) {
|
|
|
|
stake.deactivate(current_epoch)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))
|
2022-03-25 09:11:51 -07:00
|
|
|
} else {
|
|
|
|
Err(StakeError::MinimumDelinquentEpochsForDeactivationNotMet.into())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(InstructionError::InvalidAccountData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-16 08:56:48 -07:00
|
|
|
/// After calling `validate_delegated_amount()`, this struct contains calculated values that are used
|
|
|
|
/// by the caller.
|
|
|
|
struct ValidatedDelegatedInfo {
|
|
|
|
stake_amount: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ensure the stake delegation amount is valid. This checks that the account meets the minimum
|
|
|
|
/// balance requirements of delegated stake. If not, return an error.
|
|
|
|
fn validate_delegated_amount(
|
2022-04-10 00:55:37 -07:00
|
|
|
account: &BorrowedAccount,
|
2022-03-16 08:56:48 -07:00
|
|
|
meta: &Meta,
|
2022-05-04 03:17:29 -07:00
|
|
|
feature_set: &FeatureSet,
|
2022-03-16 08:56:48 -07:00
|
|
|
) -> Result<ValidatedDelegatedInfo, InstructionError> {
|
2022-04-10 00:55:37 -07:00
|
|
|
let stake_amount = account
|
|
|
|
.get_lamports()
|
|
|
|
.saturating_sub(meta.rent_exempt_reserve); // can't stake the rent
|
2022-05-04 03:17:29 -07:00
|
|
|
|
2022-05-07 16:01:05 -07:00
|
|
|
// Stake accounts may be initialized with a stake amount below the minimum delegation so check
|
|
|
|
// that the minimum is met before delegation.
|
2023-06-23 13:57:18 -07:00
|
|
|
if stake_amount < crate::get_minimum_delegation(feature_set) {
|
2022-06-01 18:29:57 -07:00
|
|
|
return Err(StakeError::InsufficientDelegation.into());
|
2022-05-04 03:17:29 -07:00
|
|
|
}
|
2022-03-16 08:56:48 -07:00
|
|
|
Ok(ValidatedDelegatedInfo { stake_amount })
|
|
|
|
}
|
|
|
|
|
|
|
|
/// After calling `validate_split_amount()`, this struct contains calculated values that are used
|
|
|
|
/// by the caller.
|
|
|
|
#[derive(Copy, Clone, Debug, Default)]
|
|
|
|
struct ValidatedSplitInfo {
|
|
|
|
source_remaining_balance: u64,
|
|
|
|
destination_rent_exempt_reserve: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Ensure the split amount is valid. This checks the source and destination accounts meet the
|
|
|
|
/// minimum balance requirements, which is the rent exempt reserve plus the minimum stake
|
|
|
|
/// delegation, and that the source account has enough lamports for the request split amount. If
|
|
|
|
/// not, return an error.
|
|
|
|
fn validate_split_amount(
|
2022-03-31 01:46:35 -07:00
|
|
|
invoke_context: &InvokeContext,
|
2022-04-10 00:55:37 -07:00
|
|
|
transaction_context: &TransactionContext,
|
|
|
|
instruction_context: &InstructionContext,
|
2022-09-06 02:31:40 -07:00
|
|
|
source_account_index: IndexOfAccount,
|
|
|
|
destination_account_index: IndexOfAccount,
|
2022-03-16 08:56:48 -07:00
|
|
|
lamports: u64,
|
|
|
|
source_meta: &Meta,
|
|
|
|
source_stake: Option<&Stake>,
|
2022-05-04 03:17:29 -07:00
|
|
|
additional_required_lamports: u64,
|
2022-03-16 08:56:48 -07:00
|
|
|
) -> Result<ValidatedSplitInfo, InstructionError> {
|
2022-06-16 09:46:17 -07:00
|
|
|
let source_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, source_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
let source_lamports = source_account.get_lamports();
|
|
|
|
drop(source_account);
|
2022-06-16 09:46:17 -07:00
|
|
|
let destination_account = instruction_context
|
|
|
|
.try_borrow_instruction_account(transaction_context, destination_account_index)?;
|
2022-04-10 00:55:37 -07:00
|
|
|
let destination_lamports = destination_account.get_lamports();
|
|
|
|
let destination_data_len = destination_account.get_data().len();
|
|
|
|
drop(destination_account);
|
2022-03-16 08:56:48 -07:00
|
|
|
|
|
|
|
// Split amount has to be something
|
|
|
|
if lamports == 0 {
|
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Obviously cannot split more than what the source account has
|
|
|
|
if lamports > source_lamports {
|
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify that the source account still has enough lamports left after splitting:
|
|
|
|
// EITHER at least the minimum balance, OR zero (in this case the source
|
|
|
|
// account is transferring all lamports to new destination account, and the source
|
|
|
|
// account will be closed)
|
|
|
|
let source_minimum_balance = source_meta
|
|
|
|
.rent_exempt_reserve
|
2022-05-04 03:17:29 -07:00
|
|
|
.saturating_add(additional_required_lamports);
|
2022-03-16 08:56:48 -07:00
|
|
|
let source_remaining_balance = source_lamports.saturating_sub(lamports);
|
|
|
|
if source_remaining_balance == 0 {
|
|
|
|
// full amount is a withdrawal
|
|
|
|
// nothing to do here
|
|
|
|
} else if source_remaining_balance < source_minimum_balance {
|
|
|
|
// the remaining balance is too low to do the split
|
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
} else {
|
|
|
|
// all clear!
|
|
|
|
// nothing to do here
|
|
|
|
}
|
|
|
|
|
|
|
|
// Verify the destination account meets the minimum balance requirements
|
|
|
|
// This must handle:
|
|
|
|
// 1. The destination account having a different rent exempt reserve due to data size changes
|
|
|
|
// 2. The destination account being prefunded, which would lower the minimum split amount
|
2023-07-07 07:01:39 -07:00
|
|
|
let rent = invoke_context.get_sysvar_cache().get_rent()?;
|
|
|
|
let destination_rent_exempt_reserve = rent.minimum_balance(destination_data_len);
|
2022-03-16 08:56:48 -07:00
|
|
|
let destination_minimum_balance =
|
2022-05-04 03:17:29 -07:00
|
|
|
destination_rent_exempt_reserve.saturating_add(additional_required_lamports);
|
2022-03-16 08:56:48 -07:00
|
|
|
let destination_balance_deficit =
|
|
|
|
destination_minimum_balance.saturating_sub(destination_lamports);
|
|
|
|
if lamports < destination_balance_deficit {
|
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
|
|
|
|
|
|
|
// If the source account is already staked, the destination will end up staked as well. Verify
|
2022-04-01 22:11:10 -07:00
|
|
|
// the destination account's delegation amount is at least the minimum delegation.
|
2022-03-16 08:56:48 -07:00
|
|
|
//
|
|
|
|
// The *delegation* requirements are different than the *balance* requirements. If the
|
|
|
|
// destination account is prefunded with a balance of `rent exempt reserve + minimum stake
|
|
|
|
// delegation - 1`, the minimum split amount to satisfy the *balance* requirements is 1
|
|
|
|
// lamport. And since *only* the split amount is immediately staked in the destination
|
|
|
|
// account, the split amount must be at least the minimum stake delegation. So if the minimum
|
|
|
|
// stake delegation was 10 lamports, then a split amount of 1 lamport would not meet the
|
|
|
|
// *delegation* requirements.
|
2023-04-14 15:30:59 -07:00
|
|
|
if !invoke_context
|
|
|
|
.feature_set
|
|
|
|
.is_active(&clean_up_delegation_errors::id())
|
|
|
|
&& source_stake.is_some()
|
|
|
|
&& lamports < additional_required_lamports
|
|
|
|
{
|
2022-03-16 08:56:48 -07:00
|
|
|
return Err(InstructionError::InsufficientFunds);
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(ValidatedSplitInfo {
|
|
|
|
source_remaining_balance,
|
|
|
|
destination_rent_exempt_reserve,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-11-19 13:44:16 -08:00
|
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
|
|
enum MergeKind {
|
2023-07-24 07:09:40 -07:00
|
|
|
Inactive(Meta, u64, StakeFlags),
|
|
|
|
ActivationEpoch(Meta, Stake, StakeFlags),
|
2020-11-19 13:44:16 -08:00
|
|
|
FullyActive(Meta, Stake),
|
|
|
|
}
|
|
|
|
|
|
|
|
impl MergeKind {
|
|
|
|
fn meta(&self) -> &Meta {
|
|
|
|
match self {
|
2023-07-24 07:09:40 -07:00
|
|
|
Self::Inactive(meta, _, _) => meta,
|
|
|
|
Self::ActivationEpoch(meta, _, _) => meta,
|
2020-11-19 13:44:16 -08:00
|
|
|
Self::FullyActive(meta, _) => meta,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn active_stake(&self) -> Option<&Stake> {
|
|
|
|
match self {
|
2023-07-24 07:09:40 -07:00
|
|
|
Self::Inactive(_, _, _) => None,
|
|
|
|
Self::ActivationEpoch(_, stake, _) => Some(stake),
|
2020-11-19 13:44:16 -08:00
|
|
|
Self::FullyActive(_, stake) => Some(stake),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn get_if_mergeable(
|
2021-12-02 09:47:16 -08:00
|
|
|
invoke_context: &InvokeContext,
|
2022-04-08 11:40:09 -07:00
|
|
|
stake_state: &StakeState,
|
|
|
|
stake_lamports: u64,
|
2020-11-19 13:44:16 -08:00
|
|
|
clock: &Clock,
|
|
|
|
stake_history: &StakeHistory,
|
|
|
|
) -> Result<Self, InstructionError> {
|
2022-04-08 11:40:09 -07:00
|
|
|
match stake_state {
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Stake(meta, stake, stake_flags) => {
|
2020-11-19 13:44:16 -08:00
|
|
|
// stake must not be in a transient state. Transient here meaning
|
|
|
|
// activating or deactivating with non-zero effective stake.
|
2021-10-04 15:59:11 -07:00
|
|
|
let status = stake
|
2021-08-19 22:08:44 -07:00
|
|
|
.delegation
|
2021-10-04 15:59:11 -07:00
|
|
|
.stake_activating_and_deactivating(clock.epoch, Some(stake_history));
|
|
|
|
|
|
|
|
match (status.effective, status.activating, status.deactivating) {
|
2023-07-24 07:09:40 -07:00
|
|
|
(0, 0, 0) => Ok(Self::Inactive(*meta, stake_lamports, *stake_flags)),
|
|
|
|
(0, _, _) => Ok(Self::ActivationEpoch(*meta, *stake, *stake_flags)),
|
2022-04-08 11:40:09 -07:00
|
|
|
(_, 0, 0) => Ok(Self::FullyActive(*meta, *stake)),
|
2021-01-21 09:59:24 -08:00
|
|
|
_ => {
|
|
|
|
let err = StakeError::MergeTransientStake;
|
|
|
|
ic_msg!(invoke_context, "{}", err);
|
|
|
|
Err(err.into())
|
|
|
|
}
|
2020-11-19 13:44:16 -08:00
|
|
|
}
|
2020-11-19 11:53:05 -08:00
|
|
|
}
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeState::Initialized(meta) => {
|
|
|
|
Ok(Self::Inactive(*meta, stake_lamports, StakeFlags::empty()))
|
|
|
|
}
|
2020-11-19 13:44:16 -08:00
|
|
|
_ => Err(InstructionError::InvalidAccountData),
|
2020-11-19 11:53:05 -08:00
|
|
|
}
|
2020-11-19 13:44:16 -08:00
|
|
|
}
|
|
|
|
|
2021-01-21 09:59:24 -08:00
|
|
|
fn metas_can_merge(
|
2021-12-02 09:47:16 -08:00
|
|
|
invoke_context: &InvokeContext,
|
2021-01-21 09:59:24 -08:00
|
|
|
stake: &Meta,
|
|
|
|
source: &Meta,
|
2022-01-21 17:27:53 -08:00
|
|
|
clock: &Clock,
|
2021-01-21 09:59:24 -08:00
|
|
|
) -> Result<(), InstructionError> {
|
2022-01-21 17:27:53 -08:00
|
|
|
// lockups may mismatch so long as both have expired
|
|
|
|
let can_merge_lockups = stake.lockup == source.lockup
|
|
|
|
|| (!stake.lockup.is_in_force(clock, None) && !source.lockup.is_in_force(clock, None));
|
2020-11-19 13:44:16 -08:00
|
|
|
// `rent_exempt_reserve` has no bearing on the mergeability of accounts,
|
|
|
|
// as the source account will be culled by runtime once the operation
|
|
|
|
// succeeds. Considering it here would needlessly prevent merging stake
|
|
|
|
// accounts with differing data lengths, which already exist in the wild
|
|
|
|
// due to an SDK bug
|
2021-05-20 22:58:16 -07:00
|
|
|
if stake.authorized == source.authorized && can_merge_lockups {
|
2020-11-19 13:44:16 -08:00
|
|
|
Ok(())
|
|
|
|
} else {
|
2021-01-21 09:59:24 -08:00
|
|
|
ic_msg!(invoke_context, "Unable to merge due to metadata mismatch");
|
2020-11-19 13:44:16 -08:00
|
|
|
Err(StakeError::MergeMismatch.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn active_delegations_can_merge(
|
2021-12-02 09:47:16 -08:00
|
|
|
invoke_context: &InvokeContext,
|
2020-11-19 13:44:16 -08:00
|
|
|
stake: &Delegation,
|
|
|
|
source: &Delegation,
|
|
|
|
) -> Result<(), InstructionError> {
|
2021-01-21 09:59:24 -08:00
|
|
|
if stake.voter_pubkey != source.voter_pubkey {
|
|
|
|
ic_msg!(invoke_context, "Unable to merge due to voter mismatch");
|
|
|
|
Err(StakeError::MergeMismatch.into())
|
|
|
|
} else if (stake.warmup_cooldown_rate - source.warmup_cooldown_rate).abs() < f64::EPSILON
|
2020-11-19 13:44:16 -08:00
|
|
|
&& stake.deactivation_epoch == Epoch::MAX
|
|
|
|
&& source.deactivation_epoch == Epoch::MAX
|
|
|
|
{
|
|
|
|
Ok(())
|
|
|
|
} else {
|
2021-01-21 09:59:24 -08:00
|
|
|
ic_msg!(invoke_context, "Unable to merge due to stake deactivation");
|
2020-11-19 13:44:16 -08:00
|
|
|
Err(StakeError::MergeMismatch.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-08-04 07:43:34 -07:00
|
|
|
// Remove this when the `stake_merge_with_unmatched_credits_observed` feature is removed
|
2021-01-21 09:59:24 -08:00
|
|
|
fn active_stakes_can_merge(
|
2021-12-02 09:47:16 -08:00
|
|
|
invoke_context: &InvokeContext,
|
2021-01-21 09:59:24 -08:00
|
|
|
stake: &Stake,
|
|
|
|
source: &Stake,
|
|
|
|
) -> Result<(), InstructionError> {
|
|
|
|
Self::active_delegations_can_merge(invoke_context, &stake.delegation, &source.delegation)?;
|
2020-11-19 13:44:16 -08:00
|
|
|
// `credits_observed` MUST match to prevent earning multiple rewards
|
|
|
|
// from a stake account by merging it into another stake account that
|
|
|
|
// is small enough to not be paid out every epoch. This would effectively
|
|
|
|
// reset the larger stake accounts `credits_observed` to that of the
|
|
|
|
// smaller account.
|
|
|
|
if stake.credits_observed == source.credits_observed {
|
|
|
|
Ok(())
|
|
|
|
} else {
|
2021-01-21 09:59:24 -08:00
|
|
|
ic_msg!(
|
|
|
|
invoke_context,
|
|
|
|
"Unable to merge due to credits observed mismatch"
|
|
|
|
);
|
2020-11-19 13:44:16 -08:00
|
|
|
Err(StakeError::MergeMismatch.into())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-01-21 09:59:24 -08:00
|
|
|
fn merge(
|
|
|
|
self,
|
2021-12-02 09:47:16 -08:00
|
|
|
invoke_context: &InvokeContext,
|
2021-01-21 09:59:24 -08:00
|
|
|
source: Self,
|
2022-01-21 17:27:53 -08:00
|
|
|
clock: &Clock,
|
2021-01-21 09:59:24 -08:00
|
|
|
) -> Result<Option<StakeState>, InstructionError> {
|
2021-05-20 19:23:36 -07:00
|
|
|
Self::metas_can_merge(invoke_context, self.meta(), source.meta(), clock)?;
|
2020-11-19 13:44:16 -08:00
|
|
|
self.active_stake()
|
|
|
|
.zip(source.active_stake())
|
2021-08-04 07:43:34 -07:00
|
|
|
.map(|(stake, source)| {
|
|
|
|
if invoke_context
|
2021-12-03 03:15:22 -08:00
|
|
|
.feature_set
|
|
|
|
.is_active(&stake_merge_with_unmatched_credits_observed::id())
|
2021-08-04 07:43:34 -07:00
|
|
|
{
|
|
|
|
Self::active_delegations_can_merge(
|
|
|
|
invoke_context,
|
|
|
|
&stake.delegation,
|
|
|
|
&source.delegation,
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
Self::active_stakes_can_merge(invoke_context, stake, source)
|
|
|
|
}
|
|
|
|
})
|
2020-11-19 13:44:16 -08:00
|
|
|
.unwrap_or(Ok(()))?;
|
|
|
|
let merged_state = match (self, source) {
|
2023-07-24 07:09:40 -07:00
|
|
|
(Self::Inactive(_, _, _), Self::Inactive(_, _, _)) => None,
|
|
|
|
(Self::Inactive(_, _, _), Self::ActivationEpoch(_, _, _)) => None,
|
|
|
|
(
|
|
|
|
Self::ActivationEpoch(meta, mut stake, stake_flags),
|
|
|
|
Self::Inactive(_, source_lamports, source_stake_flags),
|
|
|
|
) => {
|
2021-02-16 19:42:46 -08:00
|
|
|
stake.delegation.stake = checked_add(stake.delegation.stake, source_lamports)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
Some(StakeState::Stake(
|
|
|
|
meta,
|
|
|
|
stake,
|
|
|
|
stake_flags.union(source_stake_flags),
|
|
|
|
))
|
2020-11-19 13:44:16 -08:00
|
|
|
}
|
|
|
|
(
|
2023-07-24 07:09:40 -07:00
|
|
|
Self::ActivationEpoch(meta, mut stake, stake_flags),
|
|
|
|
Self::ActivationEpoch(source_meta, source_stake, source_stake_flags),
|
2020-11-19 13:44:16 -08:00
|
|
|
) => {
|
2021-02-16 19:42:46 -08:00
|
|
|
let source_lamports = checked_add(
|
|
|
|
source_meta.rent_exempt_reserve,
|
|
|
|
source_stake.delegation.stake,
|
|
|
|
)?;
|
2021-08-04 07:43:34 -07:00
|
|
|
merge_delegation_stake_and_credits_observed(
|
|
|
|
invoke_context,
|
|
|
|
&mut stake,
|
|
|
|
source_lamports,
|
|
|
|
source_stake.credits_observed,
|
|
|
|
)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
Some(StakeState::Stake(
|
|
|
|
meta,
|
|
|
|
stake,
|
|
|
|
stake_flags.union(source_stake_flags),
|
|
|
|
))
|
2020-11-19 13:44:16 -08:00
|
|
|
}
|
|
|
|
(Self::FullyActive(meta, mut stake), Self::FullyActive(_, source_stake)) => {
|
|
|
|
// Don't stake the source account's `rent_exempt_reserve` to
|
|
|
|
// protect against the magic activation loophole. It will
|
|
|
|
// instead be moved into the destination account as extra,
|
|
|
|
// withdrawable `lamports`
|
2021-08-04 07:43:34 -07:00
|
|
|
merge_delegation_stake_and_credits_observed(
|
|
|
|
invoke_context,
|
|
|
|
&mut stake,
|
|
|
|
source_stake.delegation.stake,
|
|
|
|
source_stake.credits_observed,
|
|
|
|
)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
Some(StakeState::Stake(meta, stake, StakeFlags::empty()))
|
2020-11-19 13:44:16 -08:00
|
|
|
}
|
|
|
|
_ => return Err(StakeError::MergeMismatch.into()),
|
|
|
|
};
|
|
|
|
Ok(merged_state)
|
2020-11-19 11:53:05 -08:00
|
|
|
}
|
|
|
|
}
|
2020-11-19 13:44:16 -08:00
|
|
|
|
2021-08-04 07:43:34 -07:00
|
|
|
fn merge_delegation_stake_and_credits_observed(
|
2021-12-02 09:47:16 -08:00
|
|
|
invoke_context: &InvokeContext,
|
2021-08-04 07:43:34 -07:00
|
|
|
stake: &mut Stake,
|
|
|
|
absorbed_lamports: u64,
|
|
|
|
absorbed_credits_observed: u64,
|
|
|
|
) -> Result<(), InstructionError> {
|
2021-12-03 03:15:22 -08:00
|
|
|
if invoke_context
|
|
|
|
.feature_set
|
|
|
|
.is_active(&stake_merge_with_unmatched_credits_observed::id())
|
|
|
|
{
|
2021-08-04 07:43:34 -07:00
|
|
|
stake.credits_observed =
|
|
|
|
stake_weighted_credits_observed(stake, absorbed_lamports, absorbed_credits_observed)
|
|
|
|
.ok_or(InstructionError::ArithmeticOverflow)?;
|
|
|
|
}
|
|
|
|
stake.delegation.stake = checked_add(stake.delegation.stake, absorbed_lamports)?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Calculate the effective credits observed for two stakes when merging
|
|
|
|
///
|
|
|
|
/// When merging two `ActivationEpoch` or `FullyActive` stakes, the credits
|
|
|
|
/// observed of the merged stake is the weighted average of the two stakes'
|
|
|
|
/// credits observed.
|
|
|
|
///
|
|
|
|
/// This is because we can derive the effective credits_observed by reversing the staking
|
|
|
|
/// rewards equation, _while keeping the rewards unchanged after merge (i.e. strong
|
|
|
|
/// requirement)_, like below:
|
|
|
|
///
|
|
|
|
/// a(N) => account, r => rewards, s => stake, c => credits:
|
|
|
|
/// assume:
|
|
|
|
/// a3 = merge(a1, a2)
|
|
|
|
/// then:
|
|
|
|
/// a3.s = a1.s + a2.s
|
|
|
|
///
|
|
|
|
/// Next, given:
|
|
|
|
/// aN.r = aN.c * aN.s (for every N)
|
|
|
|
/// finally:
|
|
|
|
/// a3.r = a1.r + a2.r
|
|
|
|
/// a3.c * a3.s = a1.c * a1.s + a2.c * a2.s
|
|
|
|
/// a3.c = (a1.c * a1.s + a2.c * a2.s) / (a1.s + a2.s) // QED
|
|
|
|
///
|
|
|
|
/// (For this discussion, we omitted irrelevant variables, including distance
|
|
|
|
/// calculation against vote_account and point indirection.)
|
|
|
|
fn stake_weighted_credits_observed(
|
|
|
|
stake: &Stake,
|
|
|
|
absorbed_lamports: u64,
|
|
|
|
absorbed_credits_observed: u64,
|
|
|
|
) -> Option<u64> {
|
|
|
|
if stake.credits_observed == absorbed_credits_observed {
|
|
|
|
Some(stake.credits_observed)
|
|
|
|
} else {
|
|
|
|
let total_stake = u128::from(stake.delegation.stake.checked_add(absorbed_lamports)?);
|
|
|
|
let stake_weighted_credits =
|
|
|
|
u128::from(stake.credits_observed).checked_mul(u128::from(stake.delegation.stake))?;
|
|
|
|
let absorbed_weighted_credits =
|
|
|
|
u128::from(absorbed_credits_observed).checked_mul(u128::from(absorbed_lamports))?;
|
|
|
|
// Discard fractional credits as a merge side-effect friction by taking
|
|
|
|
// the ceiling, done by adding `denominator - 1` to the numerator.
|
|
|
|
let total_weighted_credits = stake_weighted_credits
|
|
|
|
.checked_add(absorbed_weighted_credits)?
|
|
|
|
.checked_add(total_stake)?
|
|
|
|
.checked_sub(1)?;
|
|
|
|
u64::try_from(total_weighted_credits.checked_div(total_stake)?).ok()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-05 21:53:26 -07:00
|
|
|
// utility function, used by runtime
|
|
|
|
// returns a tuple of (stakers_reward,voters_reward)
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub fn redeem_rewards(
|
|
|
|
rewarded_epoch: Epoch,
|
|
|
|
stake_state: StakeState,
|
|
|
|
stake_account: &mut AccountSharedData,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
point_value: &PointValue,
|
|
|
|
stake_history: Option<&StakeHistory>,
|
|
|
|
inflation_point_calc_tracer: Option<impl Fn(&InflationPointCalculationEvent)>,
|
|
|
|
) -> Result<(u64, u64), InstructionError> {
|
2023-07-24 07:09:40 -07:00
|
|
|
if let StakeState::Stake(meta, mut stake, stake_flags) = stake_state {
|
2021-10-05 21:53:26 -07:00
|
|
|
if let Some(inflation_point_calc_tracer) = inflation_point_calc_tracer.as_ref() {
|
|
|
|
inflation_point_calc_tracer(
|
|
|
|
&InflationPointCalculationEvent::EffectiveStakeAtRewardedEpoch(
|
|
|
|
stake.stake(rewarded_epoch, stake_history),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
inflation_point_calc_tracer(&InflationPointCalculationEvent::RentExemptReserve(
|
|
|
|
meta.rent_exempt_reserve,
|
|
|
|
));
|
|
|
|
inflation_point_calc_tracer(&InflationPointCalculationEvent::Commission(
|
|
|
|
vote_state.commission,
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
if let Some((stakers_reward, voters_reward)) = redeem_stake_rewards(
|
|
|
|
rewarded_epoch,
|
|
|
|
&mut stake,
|
|
|
|
point_value,
|
|
|
|
vote_state,
|
|
|
|
stake_history,
|
|
|
|
inflation_point_calc_tracer,
|
|
|
|
) {
|
|
|
|
stake_account.checked_add_lamports(stakers_reward)?;
|
2023-07-24 07:09:40 -07:00
|
|
|
stake_account.set_state(&StakeState::Stake(meta, stake, stake_flags))?;
|
2021-10-05 21:53:26 -07:00
|
|
|
|
|
|
|
Ok((stakers_reward, voters_reward))
|
|
|
|
} else {
|
|
|
|
Err(StakeError::NoCreditsToRedeem.into())
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
Err(InstructionError::InvalidAccountData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// utility function, used by runtime
|
|
|
|
#[doc(hidden)]
|
|
|
|
pub fn calculate_points(
|
|
|
|
stake_state: &StakeState,
|
|
|
|
vote_state: &VoteState,
|
|
|
|
stake_history: Option<&StakeHistory>,
|
|
|
|
) -> Result<u128, InstructionError> {
|
2023-07-24 07:09:40 -07:00
|
|
|
if let StakeState::Stake(_meta, stake, _stake_flags) = stake_state {
|
2021-10-05 21:53:26 -07:00
|
|
|
Ok(calculate_stake_points(
|
|
|
|
stake,
|
|
|
|
vote_state,
|
|
|
|
stake_history,
|
|
|
|
null_tracer(),
|
|
|
|
))
|
|
|
|
} else {
|
2020-07-20 21:57:25 -07:00
|
|
|
Err(InstructionError::InvalidAccountData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-19 12:15:06 -08:00
|
|
|
pub type RewriteStakeStatus = (&'static str, (u64, u64), (u64, u64));
|
|
|
|
|
2019-08-12 20:59:57 -07:00
|
|
|
// utility function, used by runtime::Stakes, tests
|
|
|
|
pub fn new_stake_history_entry<'a, I>(
|
|
|
|
epoch: Epoch,
|
|
|
|
stakes: I,
|
|
|
|
history: Option<&StakeHistory>,
|
|
|
|
) -> StakeHistoryEntry
|
|
|
|
where
|
2019-11-25 13:14:32 -08:00
|
|
|
I: Iterator<Item = &'a Delegation>,
|
2019-08-12 20:59:57 -07:00
|
|
|
{
|
2021-10-04 15:59:11 -07:00
|
|
|
stakes.fold(StakeHistoryEntry::default(), |sum, stake| {
|
|
|
|
sum + stake.stake_activating_and_deactivating(epoch, history)
|
|
|
|
})
|
2019-08-12 20:59:57 -07:00
|
|
|
}
|
|
|
|
|
2022-03-09 14:48:10 -08:00
|
|
|
// utility function, used by tests
|
|
|
|
pub fn create_stake_history_from_delegations(
|
|
|
|
bootstrap: Option<u64>,
|
|
|
|
epochs: std::ops::Range<Epoch>,
|
|
|
|
delegations: &[Delegation],
|
|
|
|
) -> StakeHistory {
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
|
|
|
|
let bootstrap_delegation = if let Some(bootstrap) = bootstrap {
|
|
|
|
vec![Delegation {
|
|
|
|
activation_epoch: std::u64::MAX,
|
|
|
|
stake: bootstrap,
|
|
|
|
..Delegation::default()
|
|
|
|
}]
|
|
|
|
} else {
|
|
|
|
vec![]
|
|
|
|
};
|
|
|
|
|
|
|
|
for epoch in epochs {
|
|
|
|
let entry = new_stake_history_entry(
|
|
|
|
epoch,
|
|
|
|
delegations.iter().chain(bootstrap_delegation.iter()),
|
|
|
|
Some(&stake_history),
|
|
|
|
);
|
|
|
|
stake_history.add(epoch, entry);
|
|
|
|
}
|
|
|
|
|
|
|
|
stake_history
|
|
|
|
}
|
|
|
|
|
2019-11-25 15:11:55 -08:00
|
|
|
// genesis investor accounts
|
|
|
|
pub fn create_lockup_stake_account(
|
|
|
|
authorized: &Authorized,
|
|
|
|
lockup: &Lockup,
|
|
|
|
rent: &Rent,
|
|
|
|
lamports: u64,
|
2021-03-09 13:06:07 -08:00
|
|
|
) -> AccountSharedData {
|
2022-04-19 10:04:12 -07:00
|
|
|
let mut stake_account = AccountSharedData::new(lamports, StakeState::size_of(), &id());
|
2019-11-25 15:11:55 -08:00
|
|
|
|
2021-03-09 14:31:33 -08:00
|
|
|
let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len());
|
2020-01-02 13:15:31 -08:00
|
|
|
assert!(
|
|
|
|
lamports >= rent_exempt_reserve,
|
2022-12-06 06:30:06 -08:00
|
|
|
"lamports: {lamports} is less than rent_exempt_reserve {rent_exempt_reserve}"
|
2020-01-02 13:15:31 -08:00
|
|
|
);
|
2019-11-25 15:11:55 -08:00
|
|
|
|
|
|
|
stake_account
|
|
|
|
.set_state(&StakeState::Initialized(Meta {
|
|
|
|
authorized: *authorized,
|
|
|
|
lockup: *lockup,
|
|
|
|
rent_exempt_reserve,
|
|
|
|
}))
|
|
|
|
.expect("set_state");
|
|
|
|
|
|
|
|
stake_account
|
|
|
|
}
|
|
|
|
|
|
|
|
// utility function, used by Bank, tests, genesis for bootstrap
|
2019-09-26 13:29:29 -07:00
|
|
|
pub fn create_account(
|
|
|
|
authorized: &Pubkey,
|
|
|
|
voter_pubkey: &Pubkey,
|
2021-03-09 13:06:07 -08:00
|
|
|
vote_account: &AccountSharedData,
|
2019-11-12 12:33:40 -08:00
|
|
|
rent: &Rent,
|
2019-09-26 13:29:29 -07:00
|
|
|
lamports: u64,
|
2021-03-09 13:06:07 -08:00
|
|
|
) -> AccountSharedData {
|
2020-07-20 21:57:25 -07:00
|
|
|
do_create_account(
|
|
|
|
authorized,
|
|
|
|
voter_pubkey,
|
|
|
|
vote_account,
|
|
|
|
rent,
|
|
|
|
lamports,
|
|
|
|
Epoch::MAX,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// utility function, used by tests
|
|
|
|
pub fn create_account_with_activation_epoch(
|
|
|
|
authorized: &Pubkey,
|
|
|
|
voter_pubkey: &Pubkey,
|
2021-03-09 13:06:07 -08:00
|
|
|
vote_account: &AccountSharedData,
|
2020-07-20 21:57:25 -07:00
|
|
|
rent: &Rent,
|
|
|
|
lamports: u64,
|
|
|
|
activation_epoch: Epoch,
|
2021-03-09 13:06:07 -08:00
|
|
|
) -> AccountSharedData {
|
2020-07-20 21:57:25 -07:00
|
|
|
do_create_account(
|
|
|
|
authorized,
|
|
|
|
voter_pubkey,
|
|
|
|
vote_account,
|
|
|
|
rent,
|
|
|
|
lamports,
|
|
|
|
activation_epoch,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn do_create_account(
|
|
|
|
authorized: &Pubkey,
|
|
|
|
voter_pubkey: &Pubkey,
|
2021-03-09 13:06:07 -08:00
|
|
|
vote_account: &AccountSharedData,
|
2020-07-20 21:57:25 -07:00
|
|
|
rent: &Rent,
|
|
|
|
lamports: u64,
|
|
|
|
activation_epoch: Epoch,
|
2021-03-09 13:06:07 -08:00
|
|
|
) -> AccountSharedData {
|
2022-04-19 10:04:12 -07:00
|
|
|
let mut stake_account = AccountSharedData::new(lamports, StakeState::size_of(), &id());
|
2019-05-07 17:08:49 -07:00
|
|
|
|
2022-08-03 23:12:59 -07:00
|
|
|
let vote_state = vote_state::from(vote_account).expect("vote_state");
|
2019-11-25 15:11:55 -08:00
|
|
|
|
2021-03-09 14:31:33 -08:00
|
|
|
let rent_exempt_reserve = rent.minimum_balance(stake_account.data().len());
|
2019-11-25 15:11:55 -08:00
|
|
|
|
2019-05-07 17:08:49 -07:00
|
|
|
stake_account
|
2019-09-26 13:29:29 -07:00
|
|
|
.set_state(&StakeState::Stake(
|
2019-10-31 11:07:27 -07:00
|
|
|
Meta {
|
2019-11-25 15:11:55 -08:00
|
|
|
authorized: Authorized::auto(authorized),
|
2019-11-12 12:33:40 -08:00
|
|
|
rent_exempt_reserve,
|
2019-11-25 15:11:55 -08:00
|
|
|
..Meta::default()
|
2019-09-26 13:29:29 -07:00
|
|
|
},
|
2021-06-15 09:04:00 -07:00
|
|
|
new_stake(
|
2019-11-25 15:11:55 -08:00
|
|
|
lamports - rent_exempt_reserve, // underflow is an error, is basically: assert!(lamports > rent_exempt_reserve);
|
2019-11-12 12:33:40 -08:00
|
|
|
voter_pubkey,
|
|
|
|
&vote_state,
|
2020-07-20 21:57:25 -07:00
|
|
|
activation_epoch,
|
2019-11-12 12:33:40 -08:00
|
|
|
&Config::default(),
|
|
|
|
),
|
2023-07-24 07:09:40 -07:00
|
|
|
StakeFlags::empty(),
|
2019-09-26 13:29:29 -07:00
|
|
|
))
|
2019-05-07 17:08:49 -07:00
|
|
|
.expect("set_state");
|
|
|
|
|
|
|
|
stake_account
|
|
|
|
}
|
|
|
|
|
2019-04-01 16:45:53 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2021-12-03 09:00:31 -08:00
|
|
|
use {
|
|
|
|
super::*,
|
|
|
|
proptest::prelude::*,
|
2023-04-03 08:23:24 -07:00
|
|
|
solana_program_runtime::with_mock_invoke_context,
|
2021-12-03 09:00:31 -08:00
|
|
|
solana_sdk::{
|
2022-04-05 03:36:01 -07:00
|
|
|
account::{create_account_shared_data_for_test, AccountSharedData},
|
2021-12-03 09:00:31 -08:00
|
|
|
native_token,
|
|
|
|
pubkey::Pubkey,
|
2022-03-31 01:46:35 -07:00
|
|
|
sysvar::SysvarId,
|
2021-12-03 09:00:31 -08:00
|
|
|
},
|
2021-01-21 09:59:24 -08:00
|
|
|
};
|
2019-04-01 16:45:53 -07:00
|
|
|
|
2020-01-01 11:03:29 -08:00
|
|
|
#[test]
|
2020-04-21 21:05:49 -07:00
|
|
|
fn test_authorized_authorize() {
|
2020-10-19 12:12:08 -07:00
|
|
|
let staker = solana_sdk::pubkey::new_rand();
|
2020-04-21 21:05:49 -07:00
|
|
|
let mut authorized = Authorized::auto(&staker);
|
|
|
|
let mut signers = HashSet::new();
|
|
|
|
assert_eq!(
|
2021-02-05 22:40:07 -08:00
|
|
|
authorized.authorize(&signers, &staker, StakeAuthorize::Staker, None),
|
2020-04-21 21:05:49 -07:00
|
|
|
Err(InstructionError::MissingRequiredSignature)
|
|
|
|
);
|
|
|
|
signers.insert(staker);
|
|
|
|
assert_eq!(
|
2021-02-05 22:40:07 -08:00
|
|
|
authorized.authorize(&signers, &staker, StakeAuthorize::Staker, None),
|
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_authorized_authorize_with_custodian() {
|
|
|
|
let staker = solana_sdk::pubkey::new_rand();
|
|
|
|
let custodian = solana_sdk::pubkey::new_rand();
|
|
|
|
let invalid_custodian = solana_sdk::pubkey::new_rand();
|
|
|
|
let mut authorized = Authorized::auto(&staker);
|
|
|
|
let mut signers = HashSet::new();
|
|
|
|
signers.insert(staker);
|
|
|
|
|
|
|
|
let lockup = Lockup {
|
|
|
|
epoch: 1,
|
|
|
|
unix_timestamp: 1,
|
|
|
|
custodian,
|
|
|
|
};
|
|
|
|
let clock = Clock {
|
|
|
|
epoch: 0,
|
|
|
|
unix_timestamp: 0,
|
|
|
|
..Clock::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
// Legacy behaviour when the `require_custodian_for_locked_stake_authorize` feature is
|
|
|
|
// inactive
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(&signers, &staker, StakeAuthorize::Withdrawer, None),
|
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
|
|
|
|
// No lockup, no custodian
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&Lockup::default(), &clock, None))
|
|
|
|
),
|
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
|
|
|
|
// No lockup, invalid custodian not a signer
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&Lockup::default(), &clock, Some(&invalid_custodian)))
|
|
|
|
),
|
|
|
|
Ok(()) // <== invalid custodian doesn't matter, there's no lockup
|
|
|
|
);
|
|
|
|
|
|
|
|
// Lockup active, invalid custodian not a signer
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&lockup, &clock, Some(&invalid_custodian)))
|
|
|
|
),
|
|
|
|
Err(StakeError::CustodianSignatureMissing.into()),
|
|
|
|
);
|
|
|
|
|
|
|
|
signers.insert(invalid_custodian);
|
|
|
|
|
|
|
|
// No lockup, invalid custodian is a signer
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&Lockup::default(), &clock, Some(&invalid_custodian)))
|
|
|
|
),
|
|
|
|
Ok(()) // <== invalid custodian doesn't matter, there's no lockup
|
|
|
|
);
|
|
|
|
|
|
|
|
// Lockup active, invalid custodian is a signer
|
|
|
|
signers.insert(invalid_custodian);
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&lockup, &clock, Some(&invalid_custodian)))
|
|
|
|
),
|
|
|
|
Err(StakeError::LockupInForce.into()), // <== invalid custodian rejected
|
|
|
|
);
|
|
|
|
|
|
|
|
signers.remove(&invalid_custodian);
|
|
|
|
|
|
|
|
// Lockup active, no custodian
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&lockup, &clock, None))
|
|
|
|
),
|
|
|
|
Err(StakeError::CustodianMissing.into()),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Lockup active, custodian not a signer
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&lockup, &clock, Some(&custodian)))
|
|
|
|
),
|
|
|
|
Err(StakeError::CustodianSignatureMissing.into()),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Lockup active, custodian is a signer
|
|
|
|
signers.insert(custodian);
|
|
|
|
assert_eq!(
|
|
|
|
authorized.authorize(
|
|
|
|
&signers,
|
|
|
|
&staker,
|
|
|
|
StakeAuthorize::Withdrawer,
|
|
|
|
Some((&lockup, &clock, Some(&custodian)))
|
|
|
|
),
|
2020-04-21 21:05:49 -07:00
|
|
|
Ok(())
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-08-18 15:41:49 -07:00
|
|
|
#[test]
|
|
|
|
fn test_stake_state_stake_from_fail() {
|
2022-04-19 10:04:12 -07:00
|
|
|
let mut stake_account = AccountSharedData::new(0, StakeState::size_of(), &id());
|
2019-08-12 20:59:57 -07:00
|
|
|
|
2019-08-18 15:41:49 -07:00
|
|
|
stake_account
|
|
|
|
.set_state(&StakeState::default())
|
|
|
|
.expect("set_state");
|
2019-08-12 20:59:57 -07:00
|
|
|
|
2021-06-15 09:04:00 -07:00
|
|
|
assert_eq!(stake_from(&stake_account), None);
|
2019-08-18 15:41:49 -07:00
|
|
|
}
|
2019-08-12 20:59:57 -07:00
|
|
|
|
2019-08-18 15:41:49 -07:00
|
|
|
#[test]
|
|
|
|
fn test_stake_is_bootstrap() {
|
2021-05-19 07:31:47 -07:00
|
|
|
assert!(Delegation {
|
|
|
|
activation_epoch: std::u64::MAX,
|
|
|
|
..Delegation::default()
|
|
|
|
}
|
|
|
|
.is_bootstrap());
|
|
|
|
assert!(!Delegation {
|
|
|
|
activation_epoch: 0,
|
|
|
|
..Delegation::default()
|
|
|
|
}
|
|
|
|
.is_bootstrap());
|
2019-08-12 20:59:57 -07:00
|
|
|
}
|
|
|
|
|
2019-08-18 15:41:49 -07:00
|
|
|
#[test]
|
|
|
|
fn test_stake_activating_and_deactivating() {
|
2019-11-25 13:14:32 -08:00
|
|
|
let stake = Delegation {
|
2019-08-18 15:41:49 -07:00
|
|
|
stake: 1_000,
|
|
|
|
activation_epoch: 0, // activating at zero
|
|
|
|
deactivation_epoch: 5,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-18 15:41:49 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
// save this off so stake.config.warmup_rate changes don't break this test
|
2020-05-15 09:35:43 -07:00
|
|
|
let increment = (1_000_f64 * stake.warmup_cooldown_rate) as u64;
|
2019-08-18 15:41:49 -07:00
|
|
|
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
// assert that this stake follows step function if there's no history
|
|
|
|
assert_eq!(
|
2021-08-19 22:08:44 -07:00
|
|
|
stake.stake_activating_and_deactivating(stake.activation_epoch, Some(&stake_history),),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_effective_and_activating(0, stake.stake),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
for epoch in stake.activation_epoch + 1..stake.deactivation_epoch {
|
|
|
|
assert_eq!(
|
2021-08-19 22:08:44 -07:00
|
|
|
stake.stake_activating_and_deactivating(epoch, Some(&stake_history)),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_effective(stake.stake),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
// assert that this stake is full deactivating
|
|
|
|
assert_eq!(
|
2021-08-19 22:08:44 -07:00
|
|
|
stake
|
|
|
|
.stake_activating_and_deactivating(stake.deactivation_epoch, Some(&stake_history),),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_deactivating(stake.stake),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
// assert that this stake is fully deactivated if there's no history
|
|
|
|
assert_eq!(
|
|
|
|
stake.stake_activating_and_deactivating(
|
|
|
|
stake.deactivation_epoch + 1,
|
2020-11-11 13:11:57 -08:00
|
|
|
Some(&stake_history),
|
2019-08-18 15:41:49 -07:00
|
|
|
),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::default(),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
stake_history.add(
|
|
|
|
0u64, // entry for zero doesn't have my activating amount
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: 1_000,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
// assert that this stake is broken, because above setup is broken
|
|
|
|
assert_eq!(
|
2021-08-19 22:08:44 -07:00
|
|
|
stake.stake_activating_and_deactivating(1, Some(&stake_history)),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_effective_and_activating(0, stake.stake),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
stake_history.add(
|
|
|
|
0u64, // entry for zero has my activating amount
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: 1_000,
|
|
|
|
activating: 1_000,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
// no entry for 1, so this stake gets shorted
|
|
|
|
);
|
|
|
|
// assert that this stake is broken, because above setup is broken
|
|
|
|
assert_eq!(
|
2021-08-19 22:08:44 -07:00
|
|
|
stake.stake_activating_and_deactivating(2, Some(&stake_history)),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_effective_and_activating(
|
|
|
|
increment,
|
|
|
|
stake.stake - increment
|
|
|
|
),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
// start over, test deactivation edge cases
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
|
|
|
|
stake_history.add(
|
|
|
|
stake.deactivation_epoch, // entry for zero doesn't have my de-activating amount
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: 1_000,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
// assert that this stake is broken, because above setup is broken
|
|
|
|
assert_eq!(
|
|
|
|
stake.stake_activating_and_deactivating(
|
|
|
|
stake.deactivation_epoch + 1,
|
2020-11-11 13:11:57 -08:00
|
|
|
Some(&stake_history),
|
2019-08-18 15:41:49 -07:00
|
|
|
),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_deactivating(stake.stake),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
// put in my initial deactivating amount, but don't put in an entry for next
|
|
|
|
stake_history.add(
|
|
|
|
stake.deactivation_epoch, // entry for zero has my de-activating amount
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: 1_000,
|
|
|
|
deactivating: 1_000,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
// assert that this stake is broken, because above setup is broken
|
|
|
|
assert_eq!(
|
|
|
|
stake.stake_activating_and_deactivating(
|
|
|
|
stake.deactivation_epoch + 2,
|
2020-11-11 13:11:57 -08:00
|
|
|
Some(&stake_history),
|
2019-08-18 15:41:49 -07:00
|
|
|
),
|
2021-10-04 15:59:11 -07:00
|
|
|
// hung, should be lower
|
|
|
|
StakeActivationStatus::with_deactivating(stake.stake - increment),
|
2019-08-18 15:41:49 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-11 13:11:57 -08:00
|
|
|
mod same_epoch_activation_then_deactivation {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
enum OldDeactivationBehavior {
|
|
|
|
Stuck,
|
|
|
|
Slow,
|
|
|
|
}
|
|
|
|
|
2021-10-04 15:59:11 -07:00
|
|
|
fn do_test(
|
|
|
|
old_behavior: OldDeactivationBehavior,
|
|
|
|
expected_stakes: &[StakeActivationStatus],
|
|
|
|
) {
|
2020-11-11 13:11:57 -08:00
|
|
|
let cluster_stake = 1_000;
|
|
|
|
let activating_stake = 10_000;
|
|
|
|
let some_stake = 700;
|
|
|
|
let some_epoch = 0;
|
|
|
|
|
|
|
|
let stake = Delegation {
|
|
|
|
stake: some_stake,
|
|
|
|
activation_epoch: some_epoch,
|
|
|
|
deactivation_epoch: some_epoch,
|
|
|
|
..Delegation::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
let cluster_deactivation_at_stake_modified_epoch = match old_behavior {
|
|
|
|
OldDeactivationBehavior::Stuck => 0,
|
|
|
|
OldDeactivationBehavior::Slow => 1000,
|
|
|
|
};
|
|
|
|
|
|
|
|
let stake_history_entries = vec![
|
|
|
|
(
|
|
|
|
cluster_stake,
|
|
|
|
activating_stake,
|
|
|
|
cluster_deactivation_at_stake_modified_epoch,
|
|
|
|
),
|
|
|
|
(cluster_stake, activating_stake, 1000),
|
|
|
|
(cluster_stake, activating_stake, 1000),
|
|
|
|
(cluster_stake, activating_stake, 100),
|
|
|
|
(cluster_stake, activating_stake, 100),
|
|
|
|
(cluster_stake, activating_stake, 100),
|
|
|
|
(cluster_stake, activating_stake, 100),
|
|
|
|
];
|
|
|
|
|
|
|
|
for (epoch, (effective, activating, deactivating)) in
|
|
|
|
stake_history_entries.into_iter().enumerate()
|
|
|
|
{
|
|
|
|
stake_history.add(
|
|
|
|
epoch as Epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
expected_stakes,
|
|
|
|
(0..expected_stakes.len())
|
2021-08-19 22:08:44 -07:00
|
|
|
.map(|epoch| stake
|
|
|
|
.stake_activating_and_deactivating(epoch as u64, Some(&stake_history),))
|
2020-11-11 13:11:57 -08:00
|
|
|
.collect::<Vec<_>>()
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_new_behavior_previously_slow() {
|
|
|
|
// any stake accounts activated and deactivated at the same epoch
|
|
|
|
// shouldn't been activated (then deactivated) at all!
|
|
|
|
|
|
|
|
do_test(
|
|
|
|
OldDeactivationBehavior::Slow,
|
|
|
|
&[
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
2020-11-11 13:11:57 -08:00
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_new_behavior_previously_stuck() {
|
|
|
|
// any stake accounts activated and deactivated at the same epoch
|
|
|
|
// shouldn't been activated (then deactivated) at all!
|
|
|
|
|
|
|
|
do_test(
|
|
|
|
OldDeactivationBehavior::Stuck,
|
|
|
|
&[
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
|
|
|
StakeActivationStatus::default(),
|
2020-11-11 13:11:57 -08:00
|
|
|
],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-24 19:49:35 -08:00
|
|
|
#[test]
|
|
|
|
fn test_inflation_and_slashing_with_activating_and_deactivating_stake() {
|
|
|
|
// some really boring delegation and stake_history setup
|
|
|
|
let (delegated_stake, mut stake, stake_history) = {
|
|
|
|
let cluster_stake = 1_000;
|
|
|
|
let delegated_stake = 700;
|
|
|
|
|
|
|
|
let stake = Delegation {
|
|
|
|
stake: delegated_stake,
|
|
|
|
activation_epoch: 0,
|
|
|
|
deactivation_epoch: 4,
|
|
|
|
..Delegation::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
stake_history.add(
|
|
|
|
0,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: cluster_stake,
|
|
|
|
activating: delegated_stake,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
let newly_effective_at_epoch1 = (cluster_stake as f64 * 0.25) as u64;
|
|
|
|
assert_eq!(newly_effective_at_epoch1, 250);
|
|
|
|
stake_history.add(
|
|
|
|
1,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: cluster_stake + newly_effective_at_epoch1,
|
|
|
|
activating: delegated_stake - newly_effective_at_epoch1,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
let newly_effective_at_epoch2 =
|
|
|
|
((cluster_stake + newly_effective_at_epoch1) as f64 * 0.25) as u64;
|
|
|
|
assert_eq!(newly_effective_at_epoch2, 312);
|
|
|
|
stake_history.add(
|
|
|
|
2,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: cluster_stake
|
|
|
|
+ newly_effective_at_epoch1
|
|
|
|
+ newly_effective_at_epoch2,
|
|
|
|
activating: delegated_stake
|
|
|
|
- newly_effective_at_epoch1
|
|
|
|
- newly_effective_at_epoch2,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
stake_history.add(
|
|
|
|
3,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: cluster_stake + delegated_stake,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
stake_history.add(
|
|
|
|
4,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: cluster_stake + delegated_stake,
|
|
|
|
deactivating: delegated_stake,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
let newly_not_effective_stake_at_epoch5 =
|
|
|
|
((cluster_stake + delegated_stake) as f64 * 0.25) as u64;
|
|
|
|
assert_eq!(newly_not_effective_stake_at_epoch5, 425);
|
|
|
|
stake_history.add(
|
|
|
|
5,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective: cluster_stake + delegated_stake
|
|
|
|
- newly_not_effective_stake_at_epoch5,
|
|
|
|
deactivating: delegated_stake - newly_not_effective_stake_at_epoch5,
|
|
|
|
..StakeHistoryEntry::default()
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
(delegated_stake, stake, stake_history)
|
|
|
|
};
|
|
|
|
|
|
|
|
// helper closures
|
|
|
|
let calculate_each_staking_status = |stake: &Delegation, epoch_count: usize| -> Vec<_> {
|
|
|
|
(0..epoch_count)
|
|
|
|
.map(|epoch| {
|
2021-08-19 22:08:44 -07:00
|
|
|
stake.stake_activating_and_deactivating(epoch as u64, Some(&stake_history))
|
2020-11-24 19:49:35 -08:00
|
|
|
})
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
};
|
2021-10-04 15:59:11 -07:00
|
|
|
let adjust_staking_status = |rate: f64, status: &[StakeActivationStatus]| {
|
2020-11-24 19:49:35 -08:00
|
|
|
status
|
2021-10-04 15:59:11 -07:00
|
|
|
.iter()
|
|
|
|
.map(|entry| StakeActivationStatus {
|
|
|
|
effective: (entry.effective as f64 * rate) as u64,
|
|
|
|
activating: (entry.activating as f64 * rate) as u64,
|
|
|
|
deactivating: (entry.deactivating as f64 * rate) as u64,
|
2020-11-24 19:49:35 -08:00
|
|
|
})
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
};
|
|
|
|
|
|
|
|
let expected_staking_status_transition = vec![
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_effective_and_activating(0, 700),
|
|
|
|
StakeActivationStatus::with_effective_and_activating(250, 450),
|
|
|
|
StakeActivationStatus::with_effective_and_activating(562, 138),
|
|
|
|
StakeActivationStatus::with_effective(700),
|
|
|
|
StakeActivationStatus::with_deactivating(700),
|
|
|
|
StakeActivationStatus::with_deactivating(275),
|
|
|
|
StakeActivationStatus::default(),
|
2020-11-24 19:49:35 -08:00
|
|
|
];
|
|
|
|
let expected_staking_status_transition_base = vec![
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus::with_effective_and_activating(0, 700),
|
|
|
|
StakeActivationStatus::with_effective_and_activating(250, 450),
|
|
|
|
StakeActivationStatus::with_effective_and_activating(562, 138 + 1), // +1 is needed for rounding
|
|
|
|
StakeActivationStatus::with_effective(700),
|
|
|
|
StakeActivationStatus::with_deactivating(700),
|
|
|
|
StakeActivationStatus::with_deactivating(275 + 1), // +1 is needed for rounding
|
|
|
|
StakeActivationStatus::default(),
|
2020-11-24 19:49:35 -08:00
|
|
|
];
|
|
|
|
|
|
|
|
// normal stake activating and deactivating transition test, just in case
|
|
|
|
assert_eq!(
|
|
|
|
expected_staking_status_transition,
|
|
|
|
calculate_each_staking_status(&stake, expected_staking_status_transition.len())
|
|
|
|
);
|
|
|
|
|
|
|
|
// 10% inflation rewards assuming some sizable epochs passed!
|
|
|
|
let rate = 1.10;
|
|
|
|
stake.stake = (delegated_stake as f64 * rate) as u64;
|
|
|
|
let expected_staking_status_transition =
|
|
|
|
adjust_staking_status(rate, &expected_staking_status_transition_base);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
expected_staking_status_transition,
|
|
|
|
calculate_each_staking_status(&stake, expected_staking_status_transition_base.len()),
|
|
|
|
);
|
|
|
|
|
|
|
|
// 50% slashing!!!
|
|
|
|
let rate = 0.5;
|
|
|
|
stake.stake = (delegated_stake as f64 * rate) as u64;
|
|
|
|
let expected_staking_status_transition =
|
|
|
|
adjust_staking_status(rate, &expected_staking_status_transition_base);
|
|
|
|
|
|
|
|
assert_eq!(
|
|
|
|
expected_staking_status_transition,
|
|
|
|
calculate_each_staking_status(&stake, expected_staking_status_transition_base.len()),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-10-14 15:40:24 -07:00
|
|
|
#[test]
|
|
|
|
fn test_stop_activating_after_deactivation() {
|
2019-11-25 13:14:32 -08:00
|
|
|
let stake = Delegation {
|
2019-10-14 15:40:24 -07:00
|
|
|
stake: 1_000,
|
|
|
|
activation_epoch: 0,
|
|
|
|
deactivation_epoch: 3,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-10-14 15:40:24 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
let base_stake = 1_000;
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
let mut effective = base_stake;
|
|
|
|
let other_activation = 100;
|
|
|
|
let mut other_activations = vec![0];
|
|
|
|
|
|
|
|
// Build a stake history where the test staker always consumes all of the available warm
|
|
|
|
// up and cool down stake. However, simulate other stakers beginning to activate during
|
|
|
|
// the test staker's deactivation.
|
|
|
|
for epoch in 0..=stake.deactivation_epoch + 1 {
|
|
|
|
let (activating, deactivating) = if epoch < stake.deactivation_epoch {
|
|
|
|
(stake.stake + base_stake - effective, 0)
|
|
|
|
} else {
|
|
|
|
let other_activation_sum: u64 = other_activations.iter().sum();
|
|
|
|
let deactivating = effective - base_stake - other_activation_sum;
|
|
|
|
(other_activation, deactivating)
|
|
|
|
};
|
|
|
|
|
|
|
|
stake_history.add(
|
|
|
|
epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
2021-04-18 10:27:36 -07:00
|
|
|
let effective_rate_limited = (effective as f64 * stake.warmup_cooldown_rate) as u64;
|
2019-10-14 15:40:24 -07:00
|
|
|
if epoch < stake.deactivation_epoch {
|
2021-04-18 10:27:36 -07:00
|
|
|
effective += effective_rate_limited.min(activating);
|
2019-10-14 15:40:24 -07:00
|
|
|
other_activations.push(0);
|
|
|
|
} else {
|
2021-04-18 10:27:36 -07:00
|
|
|
effective -= effective_rate_limited.min(deactivating);
|
2019-10-14 15:40:24 -07:00
|
|
|
effective += other_activation;
|
|
|
|
other_activations.push(other_activation);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
for epoch in 0..=stake.deactivation_epoch + 1 {
|
2021-12-01 06:57:29 -08:00
|
|
|
let history = stake_history.get(epoch).unwrap();
|
2019-10-14 15:40:24 -07:00
|
|
|
let other_activations: u64 = other_activations[..=epoch as usize].iter().sum();
|
|
|
|
let expected_stake = history.effective - base_stake - other_activations;
|
|
|
|
let (expected_activating, expected_deactivating) = if epoch < stake.deactivation_epoch {
|
|
|
|
(history.activating, 0)
|
|
|
|
} else {
|
|
|
|
(0, history.deactivating)
|
|
|
|
};
|
|
|
|
assert_eq!(
|
2021-08-19 22:08:44 -07:00
|
|
|
stake.stake_activating_and_deactivating(epoch, Some(&stake_history)),
|
2021-10-04 15:59:11 -07:00
|
|
|
StakeActivationStatus {
|
|
|
|
effective: expected_stake,
|
|
|
|
activating: expected_activating,
|
|
|
|
deactivating: expected_deactivating,
|
|
|
|
},
|
2019-10-14 15:40:24 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-08-18 15:41:49 -07:00
|
|
|
#[test]
|
|
|
|
fn test_stake_warmup_cooldown_sub_integer_moves() {
|
2019-11-25 13:14:32 -08:00
|
|
|
let delegations = [Delegation {
|
2019-08-18 15:41:49 -07:00
|
|
|
stake: 2,
|
|
|
|
activation_epoch: 0, // activating at zero
|
|
|
|
deactivation_epoch: 5,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-18 15:41:49 -07:00
|
|
|
}];
|
|
|
|
// give 2 epochs of cooldown
|
|
|
|
let epochs = 7;
|
|
|
|
// make boostrap stake smaller than warmup so warmup/cooldownn
|
|
|
|
// increment is always smaller than 1
|
2019-11-25 13:14:32 -08:00
|
|
|
let bootstrap = (delegations[0].warmup_cooldown_rate * 100.0 / 2.0) as u64;
|
|
|
|
let stake_history =
|
|
|
|
create_stake_history_from_delegations(Some(bootstrap), 0..epochs, &delegations);
|
2019-08-18 15:41:49 -07:00
|
|
|
let mut max_stake = 0;
|
|
|
|
let mut min_stake = 2;
|
|
|
|
|
|
|
|
for epoch in 0..epochs {
|
2019-11-25 13:14:32 -08:00
|
|
|
let stake = delegations
|
2019-08-18 15:41:49 -07:00
|
|
|
.iter()
|
2021-08-19 22:08:44 -07:00
|
|
|
.map(|delegation| delegation.stake(epoch, Some(&stake_history)))
|
2019-08-18 15:41:49 -07:00
|
|
|
.sum::<u64>();
|
|
|
|
max_stake = max_stake.max(stake);
|
|
|
|
min_stake = min_stake.min(stake);
|
|
|
|
}
|
|
|
|
assert_eq!(max_stake, 2);
|
|
|
|
assert_eq!(min_stake, 0);
|
|
|
|
}
|
|
|
|
|
2019-06-17 19:34:21 -07:00
|
|
|
#[test]
|
2019-08-18 15:41:49 -07:00
|
|
|
fn test_stake_warmup_cooldown() {
|
2019-11-25 13:14:32 -08:00
|
|
|
let delegations = [
|
|
|
|
Delegation {
|
2019-08-17 18:12:30 -07:00
|
|
|
// never deactivates
|
2019-08-12 20:59:57 -07:00
|
|
|
stake: 1_000,
|
2019-08-17 18:12:30 -07:00
|
|
|
activation_epoch: std::u64::MAX,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-12 20:59:57 -07:00
|
|
|
},
|
2019-11-25 13:14:32 -08:00
|
|
|
Delegation {
|
2019-08-12 20:59:57 -07:00
|
|
|
stake: 1_000,
|
2019-08-17 18:12:30 -07:00
|
|
|
activation_epoch: 0,
|
|
|
|
deactivation_epoch: 9,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-12 20:59:57 -07:00
|
|
|
},
|
2019-11-25 13:14:32 -08:00
|
|
|
Delegation {
|
2019-08-12 20:59:57 -07:00
|
|
|
stake: 1_000,
|
2019-08-17 18:12:30 -07:00
|
|
|
activation_epoch: 1,
|
|
|
|
deactivation_epoch: 6,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-12 20:59:57 -07:00
|
|
|
},
|
2019-11-25 13:14:32 -08:00
|
|
|
Delegation {
|
2019-08-12 20:59:57 -07:00
|
|
|
stake: 1_000,
|
2019-08-17 18:12:30 -07:00
|
|
|
activation_epoch: 2,
|
|
|
|
deactivation_epoch: 5,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-12 20:59:57 -07:00
|
|
|
},
|
2019-11-25 13:14:32 -08:00
|
|
|
Delegation {
|
2019-08-12 20:59:57 -07:00
|
|
|
stake: 1_000,
|
2019-08-17 18:12:30 -07:00
|
|
|
activation_epoch: 2,
|
|
|
|
deactivation_epoch: 4,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-12 20:59:57 -07:00
|
|
|
},
|
2019-11-25 13:14:32 -08:00
|
|
|
Delegation {
|
2019-08-12 20:59:57 -07:00
|
|
|
stake: 1_000,
|
2019-08-17 18:12:30 -07:00
|
|
|
activation_epoch: 4,
|
|
|
|
deactivation_epoch: 4,
|
2019-11-25 13:14:32 -08:00
|
|
|
..Delegation::default()
|
2019-08-12 20:59:57 -07:00
|
|
|
},
|
|
|
|
];
|
2019-08-17 18:12:30 -07:00
|
|
|
// chosen to ensure that the last activated stake (at 4) finishes
|
|
|
|
// warming up and cooling down
|
|
|
|
// a stake takes 2.0f64.log(1.0 + STAKE_WARMUP_RATE) epochs to warm up or cool down
|
|
|
|
// when all alone, but the above overlap a lot
|
2019-08-12 20:59:57 -07:00
|
|
|
let epochs = 20;
|
|
|
|
|
2019-11-25 13:14:32 -08:00
|
|
|
let stake_history = create_stake_history_from_delegations(None, 0..epochs, &delegations);
|
2019-08-12 20:59:57 -07:00
|
|
|
|
2019-11-25 13:14:32 -08:00
|
|
|
let mut prev_total_effective_stake = delegations
|
2019-08-12 20:59:57 -07:00
|
|
|
.iter()
|
2021-08-19 22:08:44 -07:00
|
|
|
.map(|delegation| delegation.stake(0, Some(&stake_history)))
|
2019-08-12 20:59:57 -07:00
|
|
|
.sum::<u64>();
|
|
|
|
|
2019-08-17 18:12:30 -07:00
|
|
|
// uncomment and add ! for fun with graphing
|
|
|
|
// eprintln("\n{:8} {:8} {:8}", " epoch", " total", " delta");
|
|
|
|
for epoch in 1..epochs {
|
2019-11-25 13:14:32 -08:00
|
|
|
let total_effective_stake = delegations
|
2019-08-12 20:59:57 -07:00
|
|
|
.iter()
|
2021-08-19 22:08:44 -07:00
|
|
|
.map(|delegation| delegation.stake(epoch, Some(&stake_history)))
|
2019-08-12 20:59:57 -07:00
|
|
|
.sum::<u64>();
|
|
|
|
|
2019-08-17 18:12:30 -07:00
|
|
|
let delta = if total_effective_stake > prev_total_effective_stake {
|
|
|
|
total_effective_stake - prev_total_effective_stake
|
|
|
|
} else {
|
|
|
|
prev_total_effective_stake - total_effective_stake
|
|
|
|
};
|
|
|
|
|
|
|
|
// uncomment and add ! for fun with graphing
|
|
|
|
//eprint("{:8} {:8} {:8} ", epoch, total_effective_stake, delta);
|
|
|
|
//(0..(total_effective_stake as usize / (stakes.len() * 5))).for_each(|_| eprint("#"));
|
|
|
|
//eprintln();
|
2019-08-12 20:59:57 -07:00
|
|
|
|
2019-08-15 14:35:48 -07:00
|
|
|
assert!(
|
2019-08-17 18:12:30 -07:00
|
|
|
delta
|
2019-11-25 13:14:32 -08:00
|
|
|
<= ((prev_total_effective_stake as f64 * Config::default().warmup_cooldown_rate) as u64)
|
2019-08-17 18:12:30 -07:00
|
|
|
.max(1)
|
2019-08-15 14:35:48 -07:00
|
|
|
);
|
2019-08-17 18:12:30 -07:00
|
|
|
|
2019-08-12 20:59:57 -07:00
|
|
|
prev_total_effective_stake = total_effective_stake;
|
2019-06-17 19:34:21 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-06-21 23:45:03 -07:00
|
|
|
#[test]
|
2022-03-09 14:48:10 -08:00
|
|
|
fn test_stake_state_redeem_rewards() {
|
|
|
|
let mut vote_state = VoteState::default();
|
|
|
|
// assume stake.stake() is right
|
|
|
|
// bootstrap means fully-vested stake at epoch 0
|
|
|
|
let stake_lamports = 1;
|
|
|
|
let mut stake = new_stake(
|
|
|
|
stake_lamports,
|
|
|
|
&Pubkey::default(),
|
|
|
|
&vote_state,
|
|
|
|
std::u64::MAX,
|
|
|
|
&Config::default(),
|
2019-09-26 13:29:29 -07:00
|
|
|
);
|
2019-09-04 13:34:09 -07:00
|
|
|
|
2022-03-09 14:48:10 -08:00
|
|
|
// this one can't collect now, credits_observed == vote_state.credits()
|
2019-10-31 11:07:27 -07:00
|
|
|
assert_eq!(
|
2022-03-09 14:48:10 -08:00
|
|
|
None,
|
|
|
|
redeem_stake_rewards(
|
|
|
|
0,
|
|
|
|
&mut stake,
|
|
|
|
&PointValue {
|
|
|
|
rewards: 1_000_000_000,
|
|
|
|
points: 1
|
2019-11-25 15:11:55 -08:00
|
|
|
},
|
2022-03-09 14:48:10 -08:00
|
|
|
&vote_state,
|
|
|
|
None,
|
|
|
|
null_tracer(),
|
|
|
|
)
|
2019-10-31 11:07:27 -07:00
|
|
|
);
|
2022-03-09 14:48:10 -08:00
|
|
|
|
|
|
|
// put 2 credits in at epoch 0
|
2022-06-06 16:37:03 -07:00
|
|
|
vote_state.increment_credits(0, 1);
|
|
|
|
vote_state.increment_credits(0, 1);
|
2022-03-09 14:48:10 -08:00
|
|
|
|
|
|
|
// this one should be able to collect exactly 2
|
2019-09-04 13:34:09 -07:00
|
|
|
assert_eq!(
|
2022-03-09 14:48:10 -08:00
|
|
|
Some((stake_lamports * 2, 0)),
|
|
|
|
redeem_stake_rewards(
|
|
|
|
0,
|
|
|
|
&mut stake,
|
|
|
|
&PointValue {
|
|
|
|
rewards: 1,
|
|
|
|
points: 1
|
2019-11-25 15:11:55 -08:00
|
|
|
},
|
2022-03-09 14:48:10 -08:00
|
|
|
&vote_state,
|
|
|
|
None,
|
|
|
|
null_tracer(),
|
|
|
|
)
|
2019-09-04 13:34:09 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2022-03-09 14:48:10 -08:00
|
|
|
stake.delegation.stake,
|
|
|
|
stake_lamports + (stake_lamports * 2)
|
2019-09-04 13:34:09 -07:00
|
|
|
);
|
2022-03-09 14:48:10 -08:00
|
|
|
assert_eq!(stake.credits_observed, 2);
|
2019-09-04 13:34:09 -07:00
|
|
|
}
|
|
|
|
|
2020-11-23 12:13:38 -08:00
|
|
|
#[test]
|
2022-03-09 14:48:10 -08:00
|
|
|
fn test_stake_state_calculate_points_with_typical_values() {
|
|
|
|
let mut vote_state = VoteState::default();
|
|
|
|
|
|
|
|
// bootstrap means fully-vested stake at epoch 0 with
|
|
|
|
// 10_000_000 SOL is a big but not unreasaonable stake
|
|
|
|
let stake = new_stake(
|
|
|
|
native_token::sol_to_lamports(10_000_000f64),
|
|
|
|
&Pubkey::default(),
|
|
|
|
&vote_state,
|
|
|
|
std::u64::MAX,
|
|
|
|
&Config::default(),
|
2021-03-09 13:06:07 -08:00
|
|
|
);
|
2020-11-23 12:13:38 -08:00
|
|
|
|
2022-03-09 14:48:10 -08:00
|
|
|
// this one can't collect now, credits_observed == vote_state.credits()
|
2020-11-23 12:13:38 -08:00
|
|
|
assert_eq!(
|
2022-03-09 14:48:10 -08:00
|
|
|
None,
|
|
|
|
calculate_stake_rewards(
|
|
|
|
0,
|
|
|
|
&stake,
|
|
|
|
&PointValue {
|
|
|
|
rewards: 1_000_000_000,
|
|
|
|
points: 1
|
2020-11-23 12:13:38 -08:00
|
|
|
},
|
2022-03-09 14:48:10 -08:00
|
|
|
&vote_state,
|
|
|
|
None,
|
|
|
|
null_tracer(),
|
|
|
|
)
|
2020-11-23 12:13:38 -08:00
|
|
|
);
|
|
|
|
|
2022-03-09 14:48:10 -08:00
|
|
|
let epoch_slots: u128 = 14 * 24 * 3600 * 160;
|
|
|
|
// put 193,536,000 credits in at epoch 0, typical for a 14-day epoch
|
|
|
|
// this loop takes a few seconds...
|
|
|
|
for _ in 0..epoch_slots {
|
2022-06-06 16:37:03 -07:00
|
|
|
vote_state.increment_credits(0, 1);
|
2022-03-09 14:48:10 -08:00
|
|
|
}
|
2020-11-23 12:13:38 -08:00
|
|
|
|
2022-03-09 14:48:10 -08:00
|
|
|
// no overflow on points
|
2020-11-23 12:13:38 -08:00
|
|
|
assert_eq!(
|
2022-03-09 14:48:10 -08:00
|
|
|
u128::from(stake.delegation.stake) * epoch_slots,
|
|
|
|
calculate_stake_points(&stake, &vote_state, None, null_tracer())
|
2020-07-20 21:57:25 -07:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-04-07 21:45:28 -07:00
|
|
|
#[test]
|
|
|
|
fn test_stake_state_calculate_rewards() {
|
|
|
|
let mut vote_state = VoteState::default();
|
2019-08-12 20:59:57 -07:00
|
|
|
// assume stake.stake() is right
|
|
|
|
// bootstrap means fully-vested stake at epoch 0
|
2021-06-15 09:04:00 -07:00
|
|
|
let mut stake = new_stake(
|
2019-11-12 12:33:40 -08:00
|
|
|
1,
|
|
|
|
&Pubkey::default(),
|
|
|
|
&vote_state,
|
|
|
|
std::u64::MAX,
|
|
|
|
&Config::default(),
|
|
|
|
);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
|
|
|
// this one can't collect now, credits_observed == vote_state.credits()
|
2019-08-12 20:59:57 -07:00
|
|
|
assert_eq!(
|
|
|
|
None,
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
0,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 1_000_000_000,
|
|
|
|
points: 1
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-08-12 20:59:57 -07:00
|
|
|
);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
|
|
|
// put 2 credits in at epoch 0
|
2022-06-06 16:37:03 -07:00
|
|
|
vote_state.increment_credits(0, 1);
|
|
|
|
vote_state.increment_credits(0, 1);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
|
|
|
// this one should be able to collect exactly 2
|
2019-04-07 21:45:28 -07:00
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: stake.delegation.stake * 2,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 2,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
0,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 2,
|
|
|
|
points: 2 // all his
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-04-07 21:45:28 -07:00
|
|
|
);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
|
|
|
stake.credits_observed = 1;
|
2020-01-21 19:08:40 -08:00
|
|
|
// this one should be able to collect exactly 1 (already observed one)
|
2019-04-07 21:45:28 -07:00
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: stake.delegation.stake,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 2,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
0,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 1,
|
|
|
|
points: 1
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-04-07 21:45:28 -07:00
|
|
|
);
|
|
|
|
|
2020-01-21 19:08:40 -08:00
|
|
|
// put 1 credit in epoch 1
|
2022-06-06 16:37:03 -07:00
|
|
|
vote_state.increment_credits(1, 1);
|
2020-01-21 19:08:40 -08:00
|
|
|
|
2019-06-21 20:43:24 -07:00
|
|
|
stake.credits_observed = 2;
|
2020-01-21 19:08:40 -08:00
|
|
|
// this one should be able to collect the one just added
|
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: stake.delegation.stake,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 3,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
1,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 2,
|
|
|
|
points: 2
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2020-01-21 19:08:40 -08:00
|
|
|
);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
2020-01-21 19:08:40 -08:00
|
|
|
// put 1 credit in epoch 2
|
2022-06-06 16:37:03 -07:00
|
|
|
vote_state.increment_credits(2, 1);
|
2020-01-21 19:08:40 -08:00
|
|
|
// this one should be able to collect 2 now
|
2019-04-07 21:45:28 -07:00
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: stake.delegation.stake * 2,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
2,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 2,
|
|
|
|
points: 2
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-04-07 21:45:28 -07:00
|
|
|
);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
|
|
|
stake.credits_observed = 0;
|
|
|
|
// this one should be able to collect everything from t=0 a warmed up stake of 2
|
|
|
|
// (2 credits at stake of 1) + (1 credit at a stake of 2)
|
2019-04-07 21:45:28 -07:00
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: stake.delegation.stake * 2 // epoch 0
|
2020-05-15 09:35:43 -07:00
|
|
|
+ stake.delegation.stake // epoch 1
|
|
|
|
+ stake.delegation.stake, // epoch 2
|
2022-04-27 00:44:10 -07:00
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
2,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 4,
|
|
|
|
points: 4
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-04-07 21:45:28 -07:00
|
|
|
);
|
2019-06-21 20:43:24 -07:00
|
|
|
|
|
|
|
// same as above, but is a really small commission out of 32 bits,
|
|
|
|
// verify that None comes back on small redemptions where no one gets paid
|
|
|
|
vote_state.commission = 1;
|
2019-04-07 21:45:28 -07:00
|
|
|
assert_eq!(
|
2020-01-21 19:08:40 -08:00
|
|
|
None, // would be Some((0, 2 * 1 + 1 * 2, 4)),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
2,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 4,
|
|
|
|
points: 4
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-06-21 20:43:24 -07:00
|
|
|
);
|
2019-12-03 20:55:04 -08:00
|
|
|
vote_state.commission = 99;
|
2019-06-21 20:43:24 -07:00
|
|
|
assert_eq!(
|
2020-01-21 19:08:40 -08:00
|
|
|
None, // would be Some((0, 2 * 1 + 1 * 2, 4)),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
2,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-07-20 21:57:25 -07:00
|
|
|
&PointValue {
|
|
|
|
rewards: 4,
|
|
|
|
points: 4
|
|
|
|
},
|
|
|
|
&vote_state,
|
2020-11-07 23:43:50 -08:00
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-07-20 21:57:25 -07:00
|
|
|
)
|
2019-04-07 21:45:28 -07:00
|
|
|
);
|
2020-11-18 17:34:51 -08:00
|
|
|
|
|
|
|
// now one with inflation disabled. no one gets paid, but we still need
|
2020-11-30 05:47:34 -08:00
|
|
|
// to advance the stake state's credits_observed field to prevent back-
|
2020-11-18 17:34:51 -08:00
|
|
|
// paying rewards when inflation is turned on.
|
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: 0,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
2,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-11-18 17:34:51 -08:00
|
|
|
&PointValue {
|
|
|
|
rewards: 0,
|
|
|
|
points: 4
|
|
|
|
},
|
|
|
|
&vote_state,
|
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-11-18 17:34:51 -08:00
|
|
|
)
|
|
|
|
);
|
2020-11-30 05:47:34 -08:00
|
|
|
|
|
|
|
// credits_observed remains at previous level when vote_state credits are
|
|
|
|
// not advancing and inflation is disabled
|
|
|
|
stake.credits_observed = 4;
|
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: 0,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
}),
|
2021-06-15 09:04:00 -07:00
|
|
|
calculate_stake_rewards(
|
2021-09-03 14:20:56 -07:00
|
|
|
2,
|
2021-06-15 09:04:00 -07:00
|
|
|
&stake,
|
2020-11-30 05:47:34 -08:00
|
|
|
&PointValue {
|
|
|
|
rewards: 0,
|
|
|
|
points: 4
|
|
|
|
},
|
|
|
|
&vote_state,
|
|
|
|
None,
|
2021-10-05 18:30:08 -07:00
|
|
|
null_tracer(),
|
2020-11-30 05:47:34 -08:00
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
CalculatedStakePoints {
|
|
|
|
points: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
force_credits_update_with_skipped_reward: false,
|
|
|
|
},
|
2023-06-24 09:26:27 -07:00
|
|
|
calculate_stake_points_and_credits(&stake, &vote_state, None, null_tracer())
|
2022-04-26 14:49:35 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
// credits_observed is auto-rewinded when vote_state credits are assumed to have been
|
|
|
|
// recreated
|
|
|
|
stake.credits_observed = 1000;
|
|
|
|
// this is new behavior 1; return the post-recreation rewinded credits from the vote account
|
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
CalculatedStakePoints {
|
|
|
|
points: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
force_credits_update_with_skipped_reward: true,
|
|
|
|
},
|
2023-06-24 09:26:27 -07:00
|
|
|
calculate_stake_points_and_credits(&stake, &vote_state, None, null_tracer())
|
2022-04-26 14:49:35 -07:00
|
|
|
);
|
|
|
|
// this is new behavior 2; don't hint when credits both from stake and vote are identical
|
|
|
|
stake.credits_observed = 4;
|
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
CalculatedStakePoints {
|
|
|
|
points: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
force_credits_update_with_skipped_reward: false,
|
|
|
|
},
|
2023-06-24 09:26:27 -07:00
|
|
|
calculate_stake_points_and_credits(&stake, &vote_state, None, null_tracer())
|
2020-11-30 05:47:34 -08:00
|
|
|
);
|
2021-09-03 14:20:56 -07:00
|
|
|
|
|
|
|
// get rewards and credits observed when not the activation epoch
|
2022-03-09 14:48:10 -08:00
|
|
|
vote_state.commission = 0;
|
|
|
|
stake.credits_observed = 3;
|
|
|
|
stake.delegation.activation_epoch = 1;
|
2020-02-25 18:03:26 -08:00
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: stake.delegation.stake, // epoch 2
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
}),
|
2022-03-09 14:48:10 -08:00
|
|
|
calculate_stake_rewards(
|
|
|
|
2,
|
|
|
|
&stake,
|
|
|
|
&PointValue {
|
|
|
|
rewards: 1,
|
|
|
|
points: 1
|
|
|
|
},
|
|
|
|
&vote_state,
|
|
|
|
None,
|
|
|
|
null_tracer(),
|
|
|
|
)
|
2020-02-25 18:03:26 -08:00
|
|
|
);
|
|
|
|
|
2022-03-09 14:48:10 -08:00
|
|
|
// credits_observed is moved forward for the stake's activation epoch,
|
|
|
|
// and no rewards are perceived
|
|
|
|
stake.delegation.activation_epoch = 2;
|
|
|
|
stake.credits_observed = 3;
|
2020-02-25 18:03:26 -08:00
|
|
|
assert_eq!(
|
2022-04-27 00:44:10 -07:00
|
|
|
Some(CalculatedStakeRewards {
|
|
|
|
staker_rewards: 0,
|
|
|
|
voter_rewards: 0,
|
|
|
|
new_credits_observed: 4,
|
|
|
|
}),
|
2022-03-09 14:48:10 -08:00
|
|
|
calculate_stake_rewards(
|
|
|
|
2,
|
|
|
|
&stake,
|
|
|
|
&PointValue {
|
|
|
|
rewards: 1,
|
|
|
|
points: 1
|
|
|
|
},
|
|
|
|
&vote_state,
|
|
|
|
None,
|
|
|
|
null_tracer(),
|
|
|
|
)
|
2020-02-25 18:03:26 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2019-12-19 14:37:47 -08:00
|
|
|
#[test]
|
|
|
|
fn test_lockup_is_expired() {
|
2020-10-19 12:12:08 -07:00
|
|
|
let custodian = solana_sdk::pubkey::new_rand();
|
2019-12-19 14:37:47 -08:00
|
|
|
let lockup = Lockup {
|
|
|
|
epoch: 1,
|
|
|
|
unix_timestamp: 1,
|
|
|
|
custodian,
|
|
|
|
};
|
|
|
|
// neither time
|
2021-05-19 07:31:47 -07:00
|
|
|
assert!(lockup.is_in_force(
|
|
|
|
&Clock {
|
|
|
|
epoch: 0,
|
|
|
|
unix_timestamp: 0,
|
|
|
|
..Clock::default()
|
|
|
|
},
|
|
|
|
None
|
|
|
|
));
|
2019-12-19 14:37:47 -08:00
|
|
|
// not timestamp
|
2021-05-19 07:31:47 -07:00
|
|
|
assert!(lockup.is_in_force(
|
|
|
|
&Clock {
|
|
|
|
epoch: 2,
|
|
|
|
unix_timestamp: 0,
|
|
|
|
..Clock::default()
|
|
|
|
},
|
|
|
|
None
|
|
|
|
));
|
2019-12-19 14:37:47 -08:00
|
|
|
// not epoch
|
2021-05-19 07:31:47 -07:00
|
|
|
assert!(lockup.is_in_force(
|
|
|
|
&Clock {
|
|
|
|
epoch: 0,
|
|
|
|
unix_timestamp: 2,
|
|
|
|
..Clock::default()
|
|
|
|
},
|
|
|
|
None
|
|
|
|
));
|
2019-12-19 14:37:47 -08:00
|
|
|
// both, no custodian
|
2021-05-19 07:31:47 -07:00
|
|
|
assert!(!lockup.is_in_force(
|
|
|
|
&Clock {
|
|
|
|
epoch: 1,
|
|
|
|
unix_timestamp: 1,
|
|
|
|
..Clock::default()
|
|
|
|
},
|
|
|
|
None
|
|
|
|
));
|
2019-12-19 14:37:47 -08:00
|
|
|
// neither, but custodian
|
2021-05-19 07:31:47 -07:00
|
|
|
assert!(!lockup.is_in_force(
|
|
|
|
&Clock {
|
|
|
|
epoch: 0,
|
|
|
|
unix_timestamp: 0,
|
|
|
|
..Clock::default()
|
|
|
|
},
|
|
|
|
Some(&custodian),
|
|
|
|
));
|
2019-12-19 14:37:47 -08:00
|
|
|
}
|
|
|
|
|
2020-01-20 12:33:27 -08:00
|
|
|
#[test]
|
|
|
|
#[ignore]
|
|
|
|
#[should_panic]
|
|
|
|
fn test_dbg_stake_minimum_balance() {
|
2022-04-19 10:04:12 -07:00
|
|
|
let minimum_balance = Rent::default().minimum_balance(StakeState::size_of());
|
2020-01-20 12:33:27 -08:00
|
|
|
panic!(
|
|
|
|
"stake minimum_balance: {} lamports, {} SOL",
|
|
|
|
minimum_balance,
|
|
|
|
minimum_balance as f64 / solana_sdk::native_token::LAMPORTS_PER_SOL as f64
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-11-19 13:44:16 -08:00
|
|
|
#[test]
|
|
|
|
fn test_things_can_merge() {
|
2023-04-03 08:23:24 -07:00
|
|
|
with_mock_invoke_context!(invoke_context, transaction_context, Vec::new());
|
2020-11-19 13:44:16 -08:00
|
|
|
let good_stake = Stake {
|
|
|
|
credits_observed: 4242,
|
|
|
|
delegation: Delegation {
|
|
|
|
voter_pubkey: Pubkey::new_unique(),
|
|
|
|
stake: 424242424242,
|
|
|
|
activation_epoch: 42,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
let identical = good_stake;
|
2021-01-21 09:59:24 -08:00
|
|
|
assert!(
|
|
|
|
MergeKind::active_stakes_can_merge(&invoke_context, &good_stake, &identical).is_ok()
|
|
|
|
);
|
2020-11-19 13:44:16 -08:00
|
|
|
|
|
|
|
let bad_credits_observed = Stake {
|
|
|
|
credits_observed: good_stake.credits_observed + 1,
|
|
|
|
..good_stake
|
|
|
|
};
|
2021-01-21 09:59:24 -08:00
|
|
|
assert!(MergeKind::active_stakes_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&good_stake,
|
|
|
|
&bad_credits_observed
|
|
|
|
)
|
|
|
|
.is_err());
|
2020-11-19 13:44:16 -08:00
|
|
|
|
|
|
|
let good_delegation = good_stake.delegation;
|
|
|
|
let different_stake_ok = Delegation {
|
|
|
|
stake: good_delegation.stake + 1,
|
|
|
|
..good_delegation
|
|
|
|
};
|
2021-01-21 09:59:24 -08:00
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&good_delegation,
|
|
|
|
&different_stake_ok
|
|
|
|
)
|
|
|
|
.is_ok());
|
2020-11-19 13:44:16 -08:00
|
|
|
|
|
|
|
let different_activation_epoch_ok = Delegation {
|
|
|
|
activation_epoch: good_delegation.activation_epoch + 1,
|
|
|
|
..good_delegation
|
|
|
|
};
|
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
2021-01-21 09:59:24 -08:00
|
|
|
&invoke_context,
|
2020-11-19 13:44:16 -08:00
|
|
|
&good_delegation,
|
|
|
|
&different_activation_epoch_ok
|
|
|
|
)
|
|
|
|
.is_ok());
|
|
|
|
|
|
|
|
let bad_voter = Delegation {
|
|
|
|
voter_pubkey: Pubkey::new_unique(),
|
|
|
|
..good_delegation
|
|
|
|
};
|
2021-01-21 09:59:24 -08:00
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&good_delegation,
|
|
|
|
&bad_voter
|
|
|
|
)
|
|
|
|
.is_err());
|
2020-11-19 13:44:16 -08:00
|
|
|
|
|
|
|
let bad_warmup_cooldown_rate = Delegation {
|
|
|
|
warmup_cooldown_rate: good_delegation.warmup_cooldown_rate + f64::EPSILON,
|
|
|
|
..good_delegation
|
|
|
|
};
|
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
2021-01-21 09:59:24 -08:00
|
|
|
&invoke_context,
|
2020-11-19 13:44:16 -08:00
|
|
|
&good_delegation,
|
|
|
|
&bad_warmup_cooldown_rate
|
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
2021-01-21 09:59:24 -08:00
|
|
|
&invoke_context,
|
2020-11-19 13:44:16 -08:00
|
|
|
&bad_warmup_cooldown_rate,
|
|
|
|
&good_delegation
|
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
|
|
|
|
let bad_deactivation_epoch = Delegation {
|
|
|
|
deactivation_epoch: 43,
|
|
|
|
..good_delegation
|
|
|
|
};
|
2021-01-21 09:59:24 -08:00
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&good_delegation,
|
|
|
|
&bad_deactivation_epoch
|
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
assert!(MergeKind::active_delegations_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&bad_deactivation_epoch,
|
|
|
|
&good_delegation
|
|
|
|
)
|
|
|
|
.is_err());
|
2021-05-20 19:23:36 -07:00
|
|
|
}
|
2020-11-19 13:44:16 -08:00
|
|
|
|
2021-05-20 19:23:36 -07:00
|
|
|
#[test]
|
2022-01-21 17:27:53 -08:00
|
|
|
fn test_metas_can_merge() {
|
2023-04-03 08:23:24 -07:00
|
|
|
with_mock_invoke_context!(invoke_context, transaction_context, Vec::new());
|
2020-11-19 13:44:16 -08:00
|
|
|
// Identical Metas can merge
|
2021-05-20 19:23:36 -07:00
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&Meta::default(),
|
|
|
|
&Meta::default(),
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_ok());
|
|
|
|
|
|
|
|
let mismatched_rent_exempt_reserve_ok = Meta {
|
|
|
|
rent_exempt_reserve: 42,
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
assert_ne!(
|
|
|
|
mismatched_rent_exempt_reserve_ok.rent_exempt_reserve,
|
|
|
|
Meta::default().rent_exempt_reserve,
|
|
|
|
);
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&Meta::default(),
|
|
|
|
&mismatched_rent_exempt_reserve_ok,
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_ok());
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&mismatched_rent_exempt_reserve_ok,
|
|
|
|
&Meta::default(),
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_ok());
|
|
|
|
|
|
|
|
let mismatched_authorized_fails = Meta {
|
|
|
|
authorized: Authorized {
|
|
|
|
staker: Pubkey::new_unique(),
|
|
|
|
withdrawer: Pubkey::new_unique(),
|
|
|
|
},
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
assert_ne!(
|
|
|
|
mismatched_authorized_fails.authorized,
|
|
|
|
Meta::default().authorized,
|
|
|
|
);
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&Meta::default(),
|
|
|
|
&mismatched_authorized_fails,
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&mismatched_authorized_fails,
|
|
|
|
&Meta::default(),
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
|
|
|
|
let lockup1_timestamp = 42;
|
|
|
|
let lockup2_timestamp = 4242;
|
|
|
|
let lockup1_epoch = 4;
|
|
|
|
let lockup2_epoch = 42;
|
|
|
|
let metas_with_lockup1 = Meta {
|
|
|
|
lockup: Lockup {
|
|
|
|
unix_timestamp: lockup1_timestamp,
|
|
|
|
epoch: lockup1_epoch,
|
|
|
|
custodian: Pubkey::new_unique(),
|
|
|
|
},
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
let metas_with_lockup2 = Meta {
|
|
|
|
lockup: Lockup {
|
|
|
|
unix_timestamp: lockup2_timestamp,
|
|
|
|
epoch: lockup2_epoch,
|
|
|
|
custodian: Pubkey::new_unique(),
|
|
|
|
},
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
// Mismatched lockups fail when both in force
|
|
|
|
assert_ne!(metas_with_lockup1.lockup, Meta::default().lockup);
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&metas_with_lockup1,
|
|
|
|
&metas_with_lockup2,
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&metas_with_lockup2,
|
|
|
|
&metas_with_lockup1,
|
2022-01-21 17:27:53 -08:00
|
|
|
&Clock::default()
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
|
|
|
|
let clock = Clock {
|
|
|
|
epoch: lockup1_epoch + 1,
|
|
|
|
unix_timestamp: lockup1_timestamp + 1,
|
|
|
|
..Clock::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
// Mismatched lockups fail when either in force
|
|
|
|
assert_ne!(metas_with_lockup1.lockup, Meta::default().lockup);
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&metas_with_lockup1,
|
|
|
|
&metas_with_lockup2,
|
2022-01-21 17:27:53 -08:00
|
|
|
&clock
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&metas_with_lockup2,
|
|
|
|
&metas_with_lockup1,
|
2022-01-21 17:27:53 -08:00
|
|
|
&clock
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_err());
|
|
|
|
|
|
|
|
let clock = Clock {
|
|
|
|
epoch: lockup2_epoch + 1,
|
|
|
|
unix_timestamp: lockup2_timestamp + 1,
|
|
|
|
..Clock::default()
|
|
|
|
};
|
|
|
|
|
|
|
|
// Mismatched lockups succeed when both expired
|
|
|
|
assert_ne!(metas_with_lockup1.lockup, Meta::default().lockup);
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&metas_with_lockup1,
|
|
|
|
&metas_with_lockup2,
|
2022-01-21 17:27:53 -08:00
|
|
|
&clock
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_ok());
|
|
|
|
assert!(MergeKind::metas_can_merge(
|
|
|
|
&invoke_context,
|
|
|
|
&metas_with_lockup2,
|
|
|
|
&metas_with_lockup1,
|
2022-01-21 17:27:53 -08:00
|
|
|
&clock
|
2021-05-20 22:58:16 -07:00
|
|
|
)
|
|
|
|
.is_ok());
|
|
|
|
}
|
|
|
|
|
2020-11-19 13:44:16 -08:00
|
|
|
#[test]
|
|
|
|
fn test_merge_kind_get_if_mergeable() {
|
2023-04-03 08:23:24 -07:00
|
|
|
with_mock_invoke_context!(invoke_context, transaction_context, Vec::new());
|
2020-11-19 13:44:16 -08:00
|
|
|
let authority_pubkey = Pubkey::new_unique();
|
|
|
|
let initial_lamports = 4242424242;
|
|
|
|
let rent = Rent::default();
|
2022-04-19 10:04:12 -07:00
|
|
|
let rent_exempt_reserve = rent.minimum_balance(StakeState::size_of());
|
2020-11-19 13:44:16 -08:00
|
|
|
let stake_lamports = rent_exempt_reserve + initial_lamports;
|
|
|
|
|
|
|
|
let meta = Meta {
|
|
|
|
rent_exempt_reserve,
|
|
|
|
..Meta::auto(&authority_pubkey)
|
|
|
|
};
|
2022-04-08 11:40:09 -07:00
|
|
|
let mut stake_account = AccountSharedData::new_data_with_space(
|
2020-11-19 13:44:16 -08:00
|
|
|
stake_lamports,
|
|
|
|
&StakeState::Uninitialized,
|
2022-04-19 10:04:12 -07:00
|
|
|
StakeState::size_of(),
|
2020-11-19 13:44:16 -08:00
|
|
|
&id(),
|
|
|
|
)
|
|
|
|
.expect("stake_account");
|
|
|
|
let mut clock = Clock::default();
|
|
|
|
let mut stake_history = StakeHistory::default();
|
|
|
|
|
|
|
|
// Uninitialized state fails
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap_err(),
|
2020-11-19 13:44:16 -08:00
|
|
|
InstructionError::InvalidAccountData
|
|
|
|
);
|
|
|
|
|
|
|
|
// RewardsPool state fails
|
2022-04-08 11:40:09 -07:00
|
|
|
stake_account.set_state(&StakeState::RewardsPool).unwrap();
|
2020-11-19 13:44:16 -08:00
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap_err(),
|
2020-11-19 13:44:16 -08:00
|
|
|
InstructionError::InvalidAccountData
|
|
|
|
);
|
|
|
|
|
|
|
|
// Initialized state succeeds
|
2022-04-08 11:40:09 -07:00
|
|
|
stake_account
|
2020-11-19 13:44:16 -08:00
|
|
|
.set_state(&StakeState::Initialized(meta))
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2023-07-24 07:09:40 -07:00
|
|
|
MergeKind::Inactive(meta, stake_lamports, StakeFlags::empty())
|
2020-11-19 13:44:16 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
clock.epoch = 0;
|
|
|
|
let mut effective = 2 * initial_lamports;
|
|
|
|
let mut activating = 0;
|
|
|
|
let mut deactivating = 0;
|
|
|
|
stake_history.add(
|
|
|
|
clock.epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
clock.epoch += 1;
|
|
|
|
activating = initial_lamports;
|
|
|
|
stake_history.add(
|
|
|
|
clock.epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
|
|
|
|
let stake = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: initial_lamports,
|
|
|
|
activation_epoch: 1,
|
|
|
|
deactivation_epoch: 5,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
..Stake::default()
|
|
|
|
};
|
2022-04-08 11:40:09 -07:00
|
|
|
stake_account
|
2023-07-24 07:09:40 -07:00
|
|
|
.set_state(&StakeState::Stake(meta, stake, StakeFlags::empty()))
|
2020-11-19 13:44:16 -08:00
|
|
|
.unwrap();
|
|
|
|
// activation_epoch succeeds
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2023-07-24 07:09:40 -07:00
|
|
|
MergeKind::ActivationEpoch(meta, stake, StakeFlags::empty()),
|
2020-11-19 13:44:16 -08:00
|
|
|
);
|
|
|
|
|
|
|
|
// all paritially activated, transient epochs fail
|
|
|
|
loop {
|
|
|
|
clock.epoch += 1;
|
|
|
|
let delta =
|
|
|
|
activating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64);
|
|
|
|
effective += delta;
|
|
|
|
activating -= delta;
|
|
|
|
stake_history.add(
|
|
|
|
clock.epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if activating == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap_err(),
|
2020-11-19 13:44:16 -08:00
|
|
|
InstructionError::from(StakeError::MergeTransientStake),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// all epochs for which we're fully active succeed
|
|
|
|
while clock.epoch < stake.delegation.deactivation_epoch - 1 {
|
|
|
|
clock.epoch += 1;
|
|
|
|
stake_history.add(
|
|
|
|
clock.epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2020-11-19 13:44:16 -08:00
|
|
|
MergeKind::FullyActive(meta, stake),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
clock.epoch += 1;
|
|
|
|
deactivating = stake.delegation.stake;
|
|
|
|
stake_history.add(
|
|
|
|
clock.epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
// deactivation epoch fails, fully transient/deactivating
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap_err(),
|
2020-11-19 13:44:16 -08:00
|
|
|
InstructionError::from(StakeError::MergeTransientStake),
|
|
|
|
);
|
|
|
|
|
|
|
|
// all transient, deactivating epochs fail
|
|
|
|
loop {
|
|
|
|
clock.epoch += 1;
|
|
|
|
let delta =
|
|
|
|
deactivating.min((effective as f64 * stake.delegation.warmup_cooldown_rate) as u64);
|
|
|
|
effective -= delta;
|
|
|
|
deactivating -= delta;
|
|
|
|
stake_history.add(
|
|
|
|
clock.epoch,
|
|
|
|
StakeHistoryEntry {
|
|
|
|
effective,
|
|
|
|
activating,
|
|
|
|
deactivating,
|
|
|
|
},
|
|
|
|
);
|
|
|
|
if deactivating == 0 {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap_err(),
|
2020-11-19 13:44:16 -08:00
|
|
|
InstructionError::from(StakeError::MergeTransientStake),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
// first fully-deactivated epoch succeeds
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
MergeKind::get_if_mergeable(
|
|
|
|
&invoke_context,
|
2022-04-08 11:40:09 -07:00
|
|
|
&stake_account.state().unwrap(),
|
|
|
|
stake_account.lamports(),
|
2021-01-21 09:59:24 -08:00
|
|
|
&clock,
|
|
|
|
&stake_history
|
|
|
|
)
|
|
|
|
.unwrap(),
|
2023-07-24 07:09:40 -07:00
|
|
|
MergeKind::Inactive(meta, stake_lamports, StakeFlags::empty()),
|
2020-11-19 13:44:16 -08:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_merge_kind_merge() {
|
2023-04-03 08:23:24 -07:00
|
|
|
with_mock_invoke_context!(invoke_context, transaction_context, Vec::new());
|
2022-01-21 17:27:53 -08:00
|
|
|
let clock = Clock::default();
|
2020-11-19 13:44:16 -08:00
|
|
|
let lamports = 424242;
|
|
|
|
let meta = Meta {
|
|
|
|
rent_exempt_reserve: 42,
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
let stake = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: 4242,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
..Stake::default()
|
|
|
|
};
|
2023-07-24 07:09:40 -07:00
|
|
|
let inactive = MergeKind::Inactive(Meta::default(), lamports, StakeFlags::empty());
|
|
|
|
let activation_epoch = MergeKind::ActivationEpoch(meta, stake, StakeFlags::empty());
|
2020-11-19 13:44:16 -08:00
|
|
|
let fully_active = MergeKind::FullyActive(meta, stake);
|
|
|
|
|
|
|
|
assert_eq!(
|
2021-01-21 09:59:24 -08:00
|
|
|
inactive
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, inactive.clone(), &clock)
|
2021-01-21 09:59:24 -08:00
|
|
|
.unwrap(),
|
|
|
|
None
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
inactive
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, activation_epoch.clone(), &clock)
|
2021-01-21 09:59:24 -08:00
|
|
|
.unwrap(),
|
2020-11-19 13:44:16 -08:00
|
|
|
None
|
|
|
|
);
|
2021-01-21 09:59:24 -08:00
|
|
|
assert!(inactive
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, fully_active.clone(), &clock)
|
2021-01-21 09:59:24 -08:00
|
|
|
.is_err());
|
2020-11-19 13:44:16 -08:00
|
|
|
assert!(activation_epoch
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, fully_active.clone(), &clock)
|
2021-01-21 09:59:24 -08:00
|
|
|
.is_err());
|
|
|
|
assert!(fully_active
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, inactive.clone(), &clock)
|
2020-11-19 13:44:16 -08:00
|
|
|
.is_err());
|
|
|
|
assert!(fully_active
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, activation_epoch.clone(), &clock)
|
2020-11-19 13:44:16 -08:00
|
|
|
.is_err());
|
|
|
|
|
2021-01-21 09:59:24 -08:00
|
|
|
let new_state = activation_epoch
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, inactive, &clock)
|
2021-01-21 09:59:24 -08:00
|
|
|
.unwrap()
|
|
|
|
.unwrap();
|
2020-11-19 13:44:16 -08:00
|
|
|
let delegation = new_state.delegation().unwrap();
|
|
|
|
assert_eq!(delegation.stake, stake.delegation.stake + lamports);
|
|
|
|
|
|
|
|
let new_state = activation_epoch
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, activation_epoch, &clock)
|
2020-11-19 13:44:16 -08:00
|
|
|
.unwrap()
|
|
|
|
.unwrap();
|
|
|
|
let delegation = new_state.delegation().unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
delegation.stake,
|
|
|
|
2 * stake.delegation.stake + meta.rent_exempt_reserve
|
|
|
|
);
|
|
|
|
|
2021-01-21 09:59:24 -08:00
|
|
|
let new_state = fully_active
|
|
|
|
.clone()
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, fully_active, &clock)
|
2021-01-21 09:59:24 -08:00
|
|
|
.unwrap()
|
|
|
|
.unwrap();
|
2020-11-19 13:44:16 -08:00
|
|
|
let delegation = new_state.delegation().unwrap();
|
|
|
|
assert_eq!(delegation.stake, 2 * stake.delegation.stake);
|
|
|
|
}
|
2021-08-04 07:43:34 -07:00
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn test_active_stake_merge() {
|
2023-04-03 08:23:24 -07:00
|
|
|
let transaction_accounts = vec![(
|
|
|
|
Rent::id(),
|
|
|
|
create_account_shared_data_for_test(&Rent::default()),
|
|
|
|
)];
|
|
|
|
with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts);
|
2022-01-21 17:27:53 -08:00
|
|
|
let clock = Clock::default();
|
2021-08-04 07:43:34 -07:00
|
|
|
let delegation_a = 4_242_424_242u64;
|
|
|
|
let delegation_b = 6_200_000_000u64;
|
|
|
|
let credits_a = 124_521_000u64;
|
|
|
|
let rent_exempt_reserve = 227_000_000u64;
|
|
|
|
let meta = Meta {
|
|
|
|
rent_exempt_reserve,
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
let stake_a = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: delegation_a,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
credits_observed: credits_a,
|
|
|
|
};
|
|
|
|
let stake_b = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: delegation_b,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
credits_observed: credits_a,
|
|
|
|
};
|
|
|
|
|
|
|
|
// activating stake merge, match credits observed
|
2023-07-24 07:09:40 -07:00
|
|
|
let activation_epoch_a = MergeKind::ActivationEpoch(meta, stake_a, StakeFlags::empty());
|
|
|
|
let activation_epoch_b = MergeKind::ActivationEpoch(meta, stake_b, StakeFlags::empty());
|
2021-08-04 07:43:34 -07:00
|
|
|
let new_stake = activation_epoch_a
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, activation_epoch_b, &clock)
|
2021-08-04 07:43:34 -07:00
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.stake()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(new_stake.credits_observed, credits_a);
|
|
|
|
assert_eq!(
|
|
|
|
new_stake.delegation.stake,
|
|
|
|
delegation_a + delegation_b + rent_exempt_reserve
|
|
|
|
);
|
|
|
|
|
|
|
|
// active stake merge, match credits observed
|
|
|
|
let fully_active_a = MergeKind::FullyActive(meta, stake_a);
|
|
|
|
let fully_active_b = MergeKind::FullyActive(meta, stake_b);
|
|
|
|
let new_stake = fully_active_a
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, fully_active_b, &clock)
|
2021-08-04 07:43:34 -07:00
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.stake()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(new_stake.credits_observed, credits_a);
|
|
|
|
assert_eq!(new_stake.delegation.stake, delegation_a + delegation_b);
|
|
|
|
|
|
|
|
// activating stake merge, unmatched credits observed
|
|
|
|
let credits_b = 125_124_521u64;
|
|
|
|
let stake_b = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: delegation_b,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
credits_observed: credits_b,
|
|
|
|
};
|
2023-07-24 07:09:40 -07:00
|
|
|
let activation_epoch_a = MergeKind::ActivationEpoch(meta, stake_a, StakeFlags::empty());
|
|
|
|
let activation_epoch_b = MergeKind::ActivationEpoch(meta, stake_b, StakeFlags::empty());
|
2021-08-04 07:43:34 -07:00
|
|
|
let new_stake = activation_epoch_a
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, activation_epoch_b, &clock)
|
2021-08-04 07:43:34 -07:00
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.stake()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
new_stake.credits_observed,
|
|
|
|
(credits_a * delegation_a + credits_b * (delegation_b + rent_exempt_reserve))
|
|
|
|
/ (delegation_a + delegation_b + rent_exempt_reserve)
|
|
|
|
+ 1
|
|
|
|
);
|
|
|
|
assert_eq!(
|
|
|
|
new_stake.delegation.stake,
|
|
|
|
delegation_a + delegation_b + rent_exempt_reserve
|
|
|
|
);
|
|
|
|
|
|
|
|
// active stake merge, unmatched credits observed
|
|
|
|
let fully_active_a = MergeKind::FullyActive(meta, stake_a);
|
|
|
|
let fully_active_b = MergeKind::FullyActive(meta, stake_b);
|
|
|
|
let new_stake = fully_active_a
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, fully_active_b, &clock)
|
2021-08-04 07:43:34 -07:00
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.stake()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
new_stake.credits_observed,
|
|
|
|
(credits_a * delegation_a + credits_b * delegation_b) / (delegation_a + delegation_b)
|
|
|
|
+ 1
|
|
|
|
);
|
|
|
|
assert_eq!(new_stake.delegation.stake, delegation_a + delegation_b);
|
|
|
|
|
|
|
|
// active stake merge, unmatched credits observed, no need to ceiling the calculation
|
|
|
|
let delegation = 1_000_000u64;
|
|
|
|
let credits_a = 200_000_000u64;
|
|
|
|
let credits_b = 100_000_000u64;
|
|
|
|
let rent_exempt_reserve = 227_000_000u64;
|
|
|
|
let meta = Meta {
|
|
|
|
rent_exempt_reserve,
|
|
|
|
..Meta::default()
|
|
|
|
};
|
|
|
|
let stake_a = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: delegation,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
credits_observed: credits_a,
|
|
|
|
};
|
|
|
|
let stake_b = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: delegation,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
credits_observed: credits_b,
|
|
|
|
};
|
|
|
|
let fully_active_a = MergeKind::FullyActive(meta, stake_a);
|
|
|
|
let fully_active_b = MergeKind::FullyActive(meta, stake_b);
|
|
|
|
let new_stake = fully_active_a
|
2022-01-21 17:27:53 -08:00
|
|
|
.merge(&invoke_context, fully_active_b, &clock)
|
2021-08-04 07:43:34 -07:00
|
|
|
.unwrap()
|
|
|
|
.unwrap()
|
|
|
|
.stake()
|
|
|
|
.unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
new_stake.credits_observed,
|
|
|
|
(credits_a * delegation + credits_b * delegation) / (delegation + delegation)
|
|
|
|
);
|
|
|
|
assert_eq!(new_stake.delegation.stake, delegation * 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
prop_compose! {
|
|
|
|
pub fn sum_within(max: u64)(total in 1..max)
|
|
|
|
(intermediate in 1..total, total in Just(total))
|
|
|
|
-> (u64, u64) {
|
|
|
|
(intermediate, total - intermediate)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
proptest! {
|
|
|
|
#[test]
|
|
|
|
fn test_stake_weighted_credits_observed(
|
|
|
|
(credits_a, credits_b) in sum_within(u64::MAX),
|
|
|
|
(delegation_a, delegation_b) in sum_within(u64::MAX),
|
|
|
|
) {
|
|
|
|
let stake = Stake {
|
|
|
|
delegation: Delegation {
|
|
|
|
stake: delegation_a,
|
|
|
|
..Delegation::default()
|
|
|
|
},
|
|
|
|
credits_observed: credits_a
|
|
|
|
};
|
|
|
|
let credits_observed = stake_weighted_credits_observed(
|
|
|
|
&stake,
|
|
|
|
delegation_b,
|
|
|
|
credits_b,
|
|
|
|
).unwrap();
|
|
|
|
|
|
|
|
// calculated credits observed should always be between the credits of a and b
|
|
|
|
if credits_a < credits_b {
|
|
|
|
assert!(credits_a < credits_observed);
|
|
|
|
assert!(credits_observed <= credits_b);
|
|
|
|
} else {
|
|
|
|
assert!(credits_b <= credits_observed);
|
|
|
|
assert!(credits_observed <= credits_a);
|
|
|
|
}
|
|
|
|
|
|
|
|
// the difference of the combined weighted credits and the separate weighted credits
|
|
|
|
// should be 1 or 0
|
|
|
|
let weighted_credits_total = credits_observed as u128 * (delegation_a + delegation_b) as u128;
|
|
|
|
let weighted_credits_a = credits_a as u128 * delegation_a as u128;
|
|
|
|
let weighted_credits_b = credits_b as u128 * delegation_b as u128;
|
|
|
|
let raw_diff = weighted_credits_total - (weighted_credits_a + weighted_credits_b);
|
|
|
|
let credits_observed_diff = raw_diff / (delegation_a + delegation_b) as u128;
|
|
|
|
assert!(credits_observed_diff <= 1);
|
|
|
|
}
|
|
|
|
}
|
2019-04-01 16:45:53 -07:00
|
|
|
}
|