Refactor stake program into solana_program (#17906)
* Move stake state / instructions into solana_program * Update account-decoder * Update cli and runtime * Update all other parts * Commit Cargo.lock changes in programs/bpf * Update cli stake instruction import * Allow integer arithmetic * Update ABI digest * Bump rust mem instruction count * Remove useless structs * Move stake::id() -> stake::program::id() * Re-export from solana_sdk and mark deprecated * Address feedback * Run cargo fmt
This commit is contained in:
@ -43,6 +43,7 @@ pub mod serialize_utils;
|
||||
pub mod short_vec;
|
||||
pub mod slot_hashes;
|
||||
pub mod slot_history;
|
||||
pub mod stake;
|
||||
pub mod stake_history;
|
||||
pub mod system_instruction;
|
||||
pub mod system_program;
|
||||
|
28
sdk/program/src/stake/config.rs
Normal file
28
sdk/program/src/stake/config.rs
Normal file
@ -0,0 +1,28 @@
|
||||
//! config for staking
|
||||
//! carries variables that the stake program cares about
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
|
||||
// stake config ID
|
||||
crate::declare_id!("StakeConfig11111111111111111111111111111111");
|
||||
|
||||
// means that no more than RATE of current effective stake may be added or subtracted per
|
||||
// epoch
|
||||
pub const DEFAULT_WARMUP_COOLDOWN_RATE: f64 = 0.25;
|
||||
pub const DEFAULT_SLASH_PENALTY: u8 = ((5 * std::u8::MAX as usize) / 100) as u8;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
|
||||
pub struct Config {
|
||||
/// how much stake we can activate/deactivate per-epoch as a fraction of currently effective stake
|
||||
pub warmup_cooldown_rate: f64,
|
||||
/// percentage of stake lost when slash, expressed as a portion of std::u8::MAX
|
||||
pub slash_penalty: u8,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
warmup_cooldown_rate: DEFAULT_WARMUP_COOLDOWN_RATE,
|
||||
slash_penalty: DEFAULT_SLASH_PENALTY,
|
||||
}
|
||||
}
|
||||
}
|
508
sdk/program/src/stake/instruction.rs
Normal file
508
sdk/program/src/stake/instruction.rs
Normal file
@ -0,0 +1,508 @@
|
||||
use {
|
||||
crate::stake::{
|
||||
config,
|
||||
program::id,
|
||||
state::{Authorized, Lockup, StakeAuthorize, StakeState},
|
||||
},
|
||||
crate::{
|
||||
clock::{Epoch, UnixTimestamp},
|
||||
decode_error::DecodeError,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
pubkey::Pubkey,
|
||||
system_instruction, sysvar,
|
||||
},
|
||||
log::*,
|
||||
num_derive::{FromPrimitive, ToPrimitive},
|
||||
serde_derive::{Deserialize, Serialize},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
/// Reasons the stake might have had an error
|
||||
#[derive(Error, Debug, Clone, PartialEq, FromPrimitive, ToPrimitive)]
|
||||
pub enum StakeError {
|
||||
#[error("not enough credits to redeem")]
|
||||
NoCreditsToRedeem,
|
||||
|
||||
#[error("lockup has not yet expired")]
|
||||
LockupInForce,
|
||||
|
||||
#[error("stake already deactivated")]
|
||||
AlreadyDeactivated,
|
||||
|
||||
#[error("one re-delegation permitted per epoch")]
|
||||
TooSoonToRedelegate,
|
||||
|
||||
#[error("split amount is more than is staked")]
|
||||
InsufficientStake,
|
||||
|
||||
#[error("stake account with transient stake cannot be merged")]
|
||||
MergeTransientStake,
|
||||
|
||||
#[error("stake account merge failed due to different authority, lockups or state")]
|
||||
MergeMismatch,
|
||||
|
||||
#[error("custodian address not present")]
|
||||
CustodianMissing,
|
||||
|
||||
#[error("custodian signature not present")]
|
||||
CustodianSignatureMissing,
|
||||
}
|
||||
|
||||
impl<E> DecodeError<E> for StakeError {
|
||||
fn type_of() -> &'static str {
|
||||
"StakeError"
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
|
||||
pub enum StakeInstruction {
|
||||
/// Initialize a stake with lockup and authorization information
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Uninitialized stake account
|
||||
/// 1. [] Rent sysvar
|
||||
///
|
||||
/// Authorized carries pubkeys that must sign staker transactions
|
||||
/// and withdrawer transactions.
|
||||
/// Lockup carries information about withdrawal restrictions
|
||||
Initialize(Authorized, Lockup),
|
||||
|
||||
/// Authorize a key to manage stake or withdrawal
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Stake account to be updated
|
||||
/// 1. [] Clock sysvar
|
||||
/// 2. [SIGNER] The stake or withdraw authority
|
||||
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
|
||||
/// lockup expiration
|
||||
Authorize(Pubkey, StakeAuthorize),
|
||||
|
||||
/// Delegate a stake to a particular vote account
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Initialized stake account to be delegated
|
||||
/// 1. [] Vote account to which this stake will be delegated
|
||||
/// 2. [] Clock sysvar
|
||||
/// 3. [] Stake history sysvar that carries stake warmup/cooldown history
|
||||
/// 4. [] Address of config account that carries stake config
|
||||
/// 5. [SIGNER] Stake authority
|
||||
///
|
||||
/// The entire balance of the staking account is staked. DelegateStake
|
||||
/// can be called multiple times, but re-delegation is delayed
|
||||
/// by one epoch
|
||||
DelegateStake,
|
||||
|
||||
/// Split u64 tokens and stake off a stake account into another stake account.
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Stake account to be split; must be in the Initialized or Stake state
|
||||
/// 1. [WRITE] Uninitialized stake account that will take the split-off amount
|
||||
/// 2. [SIGNER] Stake authority
|
||||
Split(u64),
|
||||
|
||||
/// Withdraw unstaked lamports from the stake account
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Stake account from which to withdraw
|
||||
/// 1. [WRITE] Recipient account
|
||||
/// 2. [] Clock sysvar
|
||||
/// 3. [] Stake history sysvar that carries stake warmup/cooldown history
|
||||
/// 4. [SIGNER] Withdraw authority
|
||||
/// 5. Optional: [SIGNER] Lockup authority, if before lockup expiration
|
||||
///
|
||||
/// The u64 is the portion of the stake account balance to be withdrawn,
|
||||
/// must be `<= StakeAccount.lamports - staked_lamports`.
|
||||
Withdraw(u64),
|
||||
|
||||
/// Deactivates the stake in the account
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Delegated stake account
|
||||
/// 1. [] Clock sysvar
|
||||
/// 2. [SIGNER] Stake authority
|
||||
Deactivate,
|
||||
|
||||
/// Set stake lockup
|
||||
///
|
||||
/// If a lockup is not active, the withdraw authority may set a new lockup
|
||||
/// If a lockup is active, the lockup custodian may update the lockup parameters
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Initialized stake account
|
||||
/// 1. [SIGNER] Lockup authority or withdraw authority
|
||||
SetLockup(LockupArgs),
|
||||
|
||||
/// Merge two stake accounts.
|
||||
///
|
||||
/// Both accounts must have identical lockup and authority keys. A merge
|
||||
/// is possible between two stakes in the following states with no additional
|
||||
/// conditions:
|
||||
///
|
||||
/// * two deactivated stakes
|
||||
/// * an inactive stake into an activating stake during its activation epoch
|
||||
///
|
||||
/// For the following cases, the voter pubkey and vote credits observed must match:
|
||||
///
|
||||
/// * two activated stakes
|
||||
/// * two activating accounts that share an activation epoch, during the activation epoch
|
||||
///
|
||||
/// All other combinations of stake states will fail to merge, including all
|
||||
/// "transient" states, where a stake is activating or deactivating with a
|
||||
/// non-zero effective stake.
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Destination stake account for the merge
|
||||
/// 1. [WRITE] Source stake account for to merge. This account will be drained
|
||||
/// 2. [] Clock sysvar
|
||||
/// 3. [] Stake history sysvar that carries stake warmup/cooldown history
|
||||
/// 4. [SIGNER] Stake authority
|
||||
Merge,
|
||||
|
||||
/// Authorize a key to manage stake or withdrawal with a derived key
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. [WRITE] Stake account to be updated
|
||||
/// 1. [SIGNER] Base key of stake or withdraw authority
|
||||
/// 2. [] Clock sysvar
|
||||
/// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before
|
||||
/// lockup expiration
|
||||
AuthorizeWithSeed(AuthorizeWithSeedArgs),
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
pub struct LockupArgs {
|
||||
pub unix_timestamp: Option<UnixTimestamp>,
|
||||
pub epoch: Option<Epoch>,
|
||||
pub custodian: Option<Pubkey>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
pub struct AuthorizeWithSeedArgs {
|
||||
pub new_authorized_pubkey: Pubkey,
|
||||
pub stake_authorize: StakeAuthorize,
|
||||
pub authority_seed: String,
|
||||
pub authority_owner: Pubkey,
|
||||
}
|
||||
|
||||
pub fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction {
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&StakeInstruction::Initialize(*authorized, *lockup),
|
||||
vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(sysvar::rent::id(), false),
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_account_with_seed(
|
||||
from_pubkey: &Pubkey,
|
||||
stake_pubkey: &Pubkey,
|
||||
base: &Pubkey,
|
||||
seed: &str,
|
||||
authorized: &Authorized,
|
||||
lockup: &Lockup,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
vec![
|
||||
system_instruction::create_account_with_seed(
|
||||
from_pubkey,
|
||||
stake_pubkey,
|
||||
base,
|
||||
seed,
|
||||
lamports,
|
||||
std::mem::size_of::<StakeState>() as u64,
|
||||
&id(),
|
||||
),
|
||||
initialize(stake_pubkey, authorized, lockup),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn create_account(
|
||||
from_pubkey: &Pubkey,
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized: &Authorized,
|
||||
lockup: &Lockup,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
vec![
|
||||
system_instruction::create_account(
|
||||
from_pubkey,
|
||||
stake_pubkey,
|
||||
lamports,
|
||||
std::mem::size_of::<StakeState>() as u64,
|
||||
&id(),
|
||||
),
|
||||
initialize(stake_pubkey, authorized, lockup),
|
||||
]
|
||||
}
|
||||
|
||||
fn _split(
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
split_stake_pubkey: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new(*split_stake_pubkey, false),
|
||||
AccountMeta::new_readonly(*authorized_pubkey, true),
|
||||
];
|
||||
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::Split(lamports), account_metas)
|
||||
}
|
||||
|
||||
pub fn split(
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
split_stake_pubkey: &Pubkey,
|
||||
) -> Vec<Instruction> {
|
||||
vec![
|
||||
system_instruction::allocate(split_stake_pubkey, std::mem::size_of::<StakeState>() as u64),
|
||||
system_instruction::assign(split_stake_pubkey, &id()),
|
||||
_split(
|
||||
stake_pubkey,
|
||||
authorized_pubkey,
|
||||
lamports,
|
||||
split_stake_pubkey,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn split_with_seed(
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
split_stake_pubkey: &Pubkey, // derived using create_with_seed()
|
||||
base: &Pubkey, // base
|
||||
seed: &str, // seed
|
||||
) -> Vec<Instruction> {
|
||||
vec![
|
||||
system_instruction::allocate_with_seed(
|
||||
split_stake_pubkey,
|
||||
base,
|
||||
seed,
|
||||
std::mem::size_of::<StakeState>() as u64,
|
||||
&id(),
|
||||
),
|
||||
_split(
|
||||
stake_pubkey,
|
||||
authorized_pubkey,
|
||||
lamports,
|
||||
split_stake_pubkey,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
pub fn merge(
|
||||
destination_stake_pubkey: &Pubkey,
|
||||
source_stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
) -> Vec<Instruction> {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*destination_stake_pubkey, false),
|
||||
AccountMeta::new(*source_stake_pubkey, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(*authorized_pubkey, true),
|
||||
];
|
||||
|
||||
vec![Instruction::new_with_bincode(
|
||||
id(),
|
||||
&StakeInstruction::Merge,
|
||||
account_metas,
|
||||
)]
|
||||
}
|
||||
|
||||
pub fn create_account_and_delegate_stake(
|
||||
from_pubkey: &Pubkey,
|
||||
stake_pubkey: &Pubkey,
|
||||
vote_pubkey: &Pubkey,
|
||||
authorized: &Authorized,
|
||||
lockup: &Lockup,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
let mut instructions = create_account(from_pubkey, stake_pubkey, authorized, lockup, lamports);
|
||||
instructions.push(delegate_stake(
|
||||
stake_pubkey,
|
||||
&authorized.staker,
|
||||
vote_pubkey,
|
||||
));
|
||||
instructions
|
||||
}
|
||||
|
||||
pub fn create_account_with_seed_and_delegate_stake(
|
||||
from_pubkey: &Pubkey,
|
||||
stake_pubkey: &Pubkey,
|
||||
base: &Pubkey,
|
||||
seed: &str,
|
||||
vote_pubkey: &Pubkey,
|
||||
authorized: &Authorized,
|
||||
lockup: &Lockup,
|
||||
lamports: u64,
|
||||
) -> Vec<Instruction> {
|
||||
let mut instructions = create_account_with_seed(
|
||||
from_pubkey,
|
||||
stake_pubkey,
|
||||
base,
|
||||
seed,
|
||||
authorized,
|
||||
lockup,
|
||||
lamports,
|
||||
);
|
||||
instructions.push(delegate_stake(
|
||||
stake_pubkey,
|
||||
&authorized.staker,
|
||||
vote_pubkey,
|
||||
));
|
||||
instructions
|
||||
}
|
||||
|
||||
pub fn authorize(
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
new_authorized_pubkey: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
custodian_pubkey: Option<&Pubkey>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(*authorized_pubkey, true),
|
||||
];
|
||||
|
||||
if let Some(custodian_pubkey) = custodian_pubkey {
|
||||
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
|
||||
}
|
||||
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&StakeInstruction::Authorize(*new_authorized_pubkey, stake_authorize),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn authorize_with_seed(
|
||||
stake_pubkey: &Pubkey,
|
||||
authority_base: &Pubkey,
|
||||
authority_seed: String,
|
||||
authority_owner: &Pubkey,
|
||||
new_authorized_pubkey: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
custodian_pubkey: Option<&Pubkey>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(*authority_base, true),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
];
|
||||
|
||||
if let Some(custodian_pubkey) = custodian_pubkey {
|
||||
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
|
||||
}
|
||||
|
||||
let args = AuthorizeWithSeedArgs {
|
||||
new_authorized_pubkey: *new_authorized_pubkey,
|
||||
stake_authorize,
|
||||
authority_seed,
|
||||
authority_owner: *authority_owner,
|
||||
};
|
||||
|
||||
Instruction::new_with_bincode(
|
||||
id(),
|
||||
&StakeInstruction::AuthorizeWithSeed(args),
|
||||
account_metas,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delegate_stake(
|
||||
stake_pubkey: &Pubkey,
|
||||
authorized_pubkey: &Pubkey,
|
||||
vote_pubkey: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(*vote_pubkey, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(config::id(), false),
|
||||
AccountMeta::new_readonly(*authorized_pubkey, true),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::DelegateStake, account_metas)
|
||||
}
|
||||
|
||||
pub fn withdraw(
|
||||
stake_pubkey: &Pubkey,
|
||||
withdrawer_pubkey: &Pubkey,
|
||||
to_pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
custodian_pubkey: Option<&Pubkey>,
|
||||
) -> Instruction {
|
||||
let mut account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new(*to_pubkey, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(sysvar::stake_history::id(), false),
|
||||
AccountMeta::new_readonly(*withdrawer_pubkey, true),
|
||||
];
|
||||
|
||||
if let Some(custodian_pubkey) = custodian_pubkey {
|
||||
account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true));
|
||||
}
|
||||
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::Withdraw(lamports), account_metas)
|
||||
}
|
||||
|
||||
pub fn deactivate_stake(stake_pubkey: &Pubkey, authorized_pubkey: &Pubkey) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(sysvar::clock::id(), false),
|
||||
AccountMeta::new_readonly(*authorized_pubkey, true),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::Deactivate, account_metas)
|
||||
}
|
||||
|
||||
pub fn set_lockup(
|
||||
stake_pubkey: &Pubkey,
|
||||
lockup: &LockupArgs,
|
||||
custodian_pubkey: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*stake_pubkey, false),
|
||||
AccountMeta::new_readonly(*custodian_pubkey, true),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::SetLockup(*lockup), account_metas)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::instruction::InstructionError;
|
||||
|
||||
#[test]
|
||||
fn test_custom_error_decode() {
|
||||
use num_traits::FromPrimitive;
|
||||
fn pretty_err<T>(err: InstructionError) -> String
|
||||
where
|
||||
T: 'static + std::error::Error + DecodeError<T> + FromPrimitive,
|
||||
{
|
||||
if let InstructionError::Custom(code) = err {
|
||||
let specific_error: T = T::decode_custom_error_to_enum(code).unwrap();
|
||||
format!(
|
||||
"{:?}: {}::{:?} - {}",
|
||||
err,
|
||||
T::type_of(),
|
||||
specific_error,
|
||||
specific_error,
|
||||
)
|
||||
} else {
|
||||
"".to_string()
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
"Custom(0): StakeError::NoCreditsToRedeem - not enough credits to redeem",
|
||||
pretty_err::<StakeError>(StakeError::NoCreditsToRedeem.into())
|
||||
)
|
||||
}
|
||||
}
|
7
sdk/program/src/stake/mod.rs
Normal file
7
sdk/program/src/stake/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod config;
|
||||
pub mod instruction;
|
||||
pub mod state;
|
||||
|
||||
pub mod program {
|
||||
crate::declare_id!("Stake11111111111111111111111111111111111111");
|
||||
}
|
533
sdk/program/src/stake/state.rs
Normal file
533
sdk/program/src/stake/state.rs
Normal file
@ -0,0 +1,533 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
use {
|
||||
crate::{
|
||||
clock::{Clock, Epoch, UnixTimestamp},
|
||||
instruction::InstructionError,
|
||||
pubkey::Pubkey,
|
||||
rent::Rent,
|
||||
stake::{
|
||||
config::Config,
|
||||
instruction::{LockupArgs, StakeError},
|
||||
},
|
||||
stake_history::StakeHistory,
|
||||
},
|
||||
std::collections::HashSet,
|
||||
};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum StakeState {
|
||||
Uninitialized,
|
||||
Initialized(Meta),
|
||||
Stake(Meta, Stake),
|
||||
RewardsPool,
|
||||
}
|
||||
|
||||
impl Default for StakeState {
|
||||
fn default() -> Self {
|
||||
StakeState::Uninitialized
|
||||
}
|
||||
}
|
||||
|
||||
impl StakeState {
|
||||
pub fn get_rent_exempt_reserve(rent: &Rent) -> u64 {
|
||||
rent.minimum_balance(std::mem::size_of::<StakeState>())
|
||||
}
|
||||
|
||||
pub fn stake(&self) -> Option<Stake> {
|
||||
match self {
|
||||
StakeState::Stake(_meta, stake) => Some(*stake),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delegation(&self) -> Option<Delegation> {
|
||||
match self {
|
||||
StakeState::Stake(_meta, stake) => Some(stake.delegation),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authorized(&self) -> Option<Authorized> {
|
||||
match self {
|
||||
StakeState::Stake(meta, _stake) => Some(meta.authorized),
|
||||
StakeState::Initialized(meta) => Some(meta.authorized),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lockup(&self) -> Option<Lockup> {
|
||||
self.meta().map(|meta| meta.lockup)
|
||||
}
|
||||
|
||||
pub fn meta(&self) -> Option<Meta> {
|
||||
match self {
|
||||
StakeState::Stake(meta, _stake) => Some(*meta),
|
||||
StakeState::Initialized(meta) => Some(*meta),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
pub enum StakeAuthorize {
|
||||
Staker,
|
||||
Withdrawer,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
pub struct Lockup {
|
||||
/// UnixTimestamp at which this stake will allow withdrawal, unless the
|
||||
/// transaction is signed by the custodian
|
||||
pub unix_timestamp: UnixTimestamp,
|
||||
/// epoch height at which this stake will allow withdrawal, unless the
|
||||
/// transaction is signed by the custodian
|
||||
pub epoch: Epoch,
|
||||
/// custodian signature on a transaction exempts the operation from
|
||||
/// lockup constraints
|
||||
pub custodian: Pubkey,
|
||||
}
|
||||
|
||||
impl Lockup {
|
||||
pub fn is_in_force(&self, clock: &Clock, custodian: Option<&Pubkey>) -> bool {
|
||||
if custodian == Some(&self.custodian) {
|
||||
return false;
|
||||
}
|
||||
self.unix_timestamp > clock.unix_timestamp || self.epoch > clock.epoch
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
pub struct Authorized {
|
||||
pub staker: Pubkey,
|
||||
pub withdrawer: Pubkey,
|
||||
}
|
||||
|
||||
impl Authorized {
|
||||
pub fn auto(authorized: &Pubkey) -> Self {
|
||||
Self {
|
||||
staker: *authorized,
|
||||
withdrawer: *authorized,
|
||||
}
|
||||
}
|
||||
pub fn check(
|
||||
&self,
|
||||
signers: &HashSet<Pubkey>,
|
||||
stake_authorize: StakeAuthorize,
|
||||
) -> Result<(), InstructionError> {
|
||||
match stake_authorize {
|
||||
StakeAuthorize::Staker if signers.contains(&self.staker) => Ok(()),
|
||||
StakeAuthorize::Withdrawer if signers.contains(&self.withdrawer) => Ok(()),
|
||||
_ => Err(InstructionError::MissingRequiredSignature),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn authorize(
|
||||
&mut self,
|
||||
signers: &HashSet<Pubkey>,
|
||||
new_authorized: &Pubkey,
|
||||
stake_authorize: StakeAuthorize,
|
||||
lockup_custodian_args: Option<(&Lockup, &Clock, Option<&Pubkey>)>,
|
||||
) -> Result<(), InstructionError> {
|
||||
match stake_authorize {
|
||||
StakeAuthorize::Staker => {
|
||||
// Allow either the staker or the withdrawer to change the staker key
|
||||
if !signers.contains(&self.staker) && !signers.contains(&self.withdrawer) {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
self.staker = *new_authorized
|
||||
}
|
||||
StakeAuthorize::Withdrawer => {
|
||||
if let Some((lockup, clock, custodian)) = lockup_custodian_args {
|
||||
if lockup.is_in_force(&clock, None) {
|
||||
match custodian {
|
||||
None => {
|
||||
return Err(StakeError::CustodianMissing.into());
|
||||
}
|
||||
Some(custodian) => {
|
||||
if !signers.contains(custodian) {
|
||||
return Err(StakeError::CustodianSignatureMissing.into());
|
||||
}
|
||||
|
||||
if lockup.is_in_force(&clock, Some(custodian)) {
|
||||
return Err(StakeError::LockupInForce.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.check(signers, stake_authorize)?;
|
||||
self.withdrawer = *new_authorized
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
pub struct Meta {
|
||||
pub rent_exempt_reserve: u64,
|
||||
pub authorized: Authorized,
|
||||
pub lockup: Lockup,
|
||||
}
|
||||
|
||||
impl Meta {
|
||||
pub fn set_lockup(
|
||||
&mut self,
|
||||
lockup: &LockupArgs,
|
||||
signers: &HashSet<Pubkey>,
|
||||
clock: Option<&Clock>,
|
||||
) -> Result<(), InstructionError> {
|
||||
match clock {
|
||||
None => {
|
||||
// pre-stake_program_v4 behavior: custodian can set lockups at any time
|
||||
if !signers.contains(&self.lockup.custodian) {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
}
|
||||
Some(clock) => {
|
||||
// post-stake_program_v4 behavior:
|
||||
// * custodian can update the lockup while in force
|
||||
// * withdraw authority can set a new lockup
|
||||
//
|
||||
if self.lockup.is_in_force(clock, None) {
|
||||
if !signers.contains(&self.lockup.custodian) {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
} else if !signers.contains(&self.authorized.withdrawer) {
|
||||
return Err(InstructionError::MissingRequiredSignature);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(unix_timestamp) = lockup.unix_timestamp {
|
||||
self.lockup.unix_timestamp = unix_timestamp;
|
||||
}
|
||||
if let Some(epoch) = lockup.epoch {
|
||||
self.lockup.epoch = epoch;
|
||||
}
|
||||
if let Some(custodian) = lockup.custodian {
|
||||
self.lockup.custodian = custodian;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn rewrite_rent_exempt_reserve(
|
||||
&mut self,
|
||||
rent: &Rent,
|
||||
data_len: usize,
|
||||
) -> Option<(u64, u64)> {
|
||||
let corrected_rent_exempt_reserve = rent.minimum_balance(data_len);
|
||||
if corrected_rent_exempt_reserve != self.rent_exempt_reserve {
|
||||
// We forcibly update rent_excempt_reserve even
|
||||
// if rent_exempt_reserve > account_balance, hoping user might restore
|
||||
// rent_exempt status by depositing.
|
||||
let (old, new) = (self.rent_exempt_reserve, corrected_rent_exempt_reserve);
|
||||
self.rent_exempt_reserve = corrected_rent_exempt_reserve;
|
||||
Some((old, new))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto(authorized: &Pubkey) -> Self {
|
||||
Self {
|
||||
authorized: Authorized::auto(authorized),
|
||||
..Meta::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
pub struct Delegation {
|
||||
/// to whom the stake is delegated
|
||||
pub voter_pubkey: Pubkey,
|
||||
/// activated stake amount, set at delegate() time
|
||||
pub stake: u64,
|
||||
/// epoch at which this stake was activated, std::Epoch::MAX if is a bootstrap stake
|
||||
pub activation_epoch: Epoch,
|
||||
/// epoch the stake was deactivated, std::Epoch::MAX if not deactivated
|
||||
pub deactivation_epoch: Epoch,
|
||||
/// how much stake we can activate per-epoch as a fraction of currently effective stake
|
||||
pub warmup_cooldown_rate: f64,
|
||||
}
|
||||
|
||||
impl Default for Delegation {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
voter_pubkey: Pubkey::default(),
|
||||
stake: 0,
|
||||
activation_epoch: 0,
|
||||
deactivation_epoch: std::u64::MAX,
|
||||
warmup_cooldown_rate: Config::default().warmup_cooldown_rate,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Delegation {
|
||||
pub fn new(
|
||||
voter_pubkey: &Pubkey,
|
||||
stake: u64,
|
||||
activation_epoch: Epoch,
|
||||
warmup_cooldown_rate: f64,
|
||||
) -> Self {
|
||||
Self {
|
||||
voter_pubkey: *voter_pubkey,
|
||||
stake,
|
||||
activation_epoch,
|
||||
warmup_cooldown_rate,
|
||||
..Delegation::default()
|
||||
}
|
||||
}
|
||||
pub fn is_bootstrap(&self) -> bool {
|
||||
self.activation_epoch == std::u64::MAX
|
||||
}
|
||||
|
||||
pub fn stake(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
history: Option<&StakeHistory>,
|
||||
fix_stake_deactivate: bool,
|
||||
) -> u64 {
|
||||
self.stake_activating_and_deactivating(epoch, history, fix_stake_deactivate)
|
||||
.0
|
||||
}
|
||||
|
||||
// returned tuple is (effective, activating, deactivating) stake
|
||||
#[allow(clippy::comparison_chain)]
|
||||
pub fn stake_activating_and_deactivating(
|
||||
&self,
|
||||
target_epoch: Epoch,
|
||||
history: Option<&StakeHistory>,
|
||||
fix_stake_deactivate: bool,
|
||||
) -> (u64, u64, u64) {
|
||||
let delegated_stake = self.stake;
|
||||
|
||||
// first, calculate an effective and activating stake
|
||||
let (effective_stake, activating_stake) =
|
||||
self.stake_and_activating(target_epoch, history, fix_stake_deactivate);
|
||||
|
||||
// then de-activate some portion if necessary
|
||||
if target_epoch < self.deactivation_epoch {
|
||||
// not deactivated
|
||||
(effective_stake, activating_stake, 0)
|
||||
} else if target_epoch == self.deactivation_epoch {
|
||||
// can only deactivate what's activated
|
||||
(effective_stake, 0, effective_stake.min(delegated_stake))
|
||||
} else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) =
|
||||
history.and_then(|history| {
|
||||
history
|
||||
.get(&self.deactivation_epoch)
|
||||
.map(|cluster_stake_at_deactivation_epoch| {
|
||||
(
|
||||
history,
|
||||
self.deactivation_epoch,
|
||||
cluster_stake_at_deactivation_epoch,
|
||||
)
|
||||
})
|
||||
})
|
||||
{
|
||||
// target_epoch > self.deactivation_epoch
|
||||
|
||||
// loop from my deactivation epoch until the target epoch
|
||||
// current effective stake is updated using its previous epoch's cluster stake
|
||||
let mut current_epoch;
|
||||
let mut current_effective_stake = effective_stake;
|
||||
loop {
|
||||
current_epoch = prev_epoch + 1;
|
||||
// if there is no deactivating stake at prev epoch, we should have been
|
||||
// fully undelegated at this moment
|
||||
if prev_cluster_stake.deactivating == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// I'm trying to get to zero, how much of the deactivation in stake
|
||||
// this account is entitled to take
|
||||
let weight =
|
||||
current_effective_stake as f64 / prev_cluster_stake.deactivating as f64;
|
||||
|
||||
// portion of newly not-effective cluster stake I'm entitled to at current epoch
|
||||
let newly_not_effective_cluster_stake =
|
||||
prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate;
|
||||
let newly_not_effective_stake =
|
||||
((weight * newly_not_effective_cluster_stake) as u64).max(1);
|
||||
|
||||
current_effective_stake =
|
||||
current_effective_stake.saturating_sub(newly_not_effective_stake);
|
||||
if current_effective_stake == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
if current_epoch >= target_epoch {
|
||||
break;
|
||||
}
|
||||
if let Some(current_cluster_stake) = history.get(¤t_epoch) {
|
||||
prev_epoch = current_epoch;
|
||||
prev_cluster_stake = current_cluster_stake;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// deactivating stake should equal to all of currently remaining effective stake
|
||||
(current_effective_stake, 0, current_effective_stake)
|
||||
} else {
|
||||
// no history or I've dropped out of history, so assume fully deactivated
|
||||
(0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// returned tuple is (effective, activating) stake
|
||||
fn stake_and_activating(
|
||||
&self,
|
||||
target_epoch: Epoch,
|
||||
history: Option<&StakeHistory>,
|
||||
fix_stake_deactivate: bool,
|
||||
) -> (u64, u64) {
|
||||
let delegated_stake = self.stake;
|
||||
|
||||
if self.is_bootstrap() {
|
||||
// fully effective immediately
|
||||
(delegated_stake, 0)
|
||||
} else if fix_stake_deactivate && self.activation_epoch == self.deactivation_epoch {
|
||||
// activated but instantly deactivated; no stake at all regardless of target_epoch
|
||||
// this must be after the bootstrap check and before all-is-activating check
|
||||
(0, 0)
|
||||
} else if target_epoch == self.activation_epoch {
|
||||
// all is activating
|
||||
(0, delegated_stake)
|
||||
} else if target_epoch < self.activation_epoch {
|
||||
// not yet enabled
|
||||
(0, 0)
|
||||
} else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) =
|
||||
history.and_then(|history| {
|
||||
history
|
||||
.get(&self.activation_epoch)
|
||||
.map(|cluster_stake_at_activation_epoch| {
|
||||
(
|
||||
history,
|
||||
self.activation_epoch,
|
||||
cluster_stake_at_activation_epoch,
|
||||
)
|
||||
})
|
||||
})
|
||||
{
|
||||
// target_epoch > self.activation_epoch
|
||||
|
||||
// loop from my activation epoch until the target epoch summing up my entitlement
|
||||
// current effective stake is updated using its previous epoch's cluster stake
|
||||
let mut current_epoch;
|
||||
let mut current_effective_stake = 0;
|
||||
loop {
|
||||
current_epoch = prev_epoch + 1;
|
||||
// if there is no activating stake at prev epoch, we should have been
|
||||
// fully effective at this moment
|
||||
if prev_cluster_stake.activating == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
// how much of the growth in stake this account is
|
||||
// entitled to take
|
||||
let remaining_activating_stake = delegated_stake - current_effective_stake;
|
||||
let weight =
|
||||
remaining_activating_stake as f64 / prev_cluster_stake.activating as f64;
|
||||
|
||||
// portion of newly effective cluster stake I'm entitled to at current epoch
|
||||
let newly_effective_cluster_stake =
|
||||
prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate;
|
||||
let newly_effective_stake =
|
||||
((weight * newly_effective_cluster_stake) as u64).max(1);
|
||||
|
||||
current_effective_stake += newly_effective_stake;
|
||||
if current_effective_stake >= delegated_stake {
|
||||
current_effective_stake = delegated_stake;
|
||||
break;
|
||||
}
|
||||
|
||||
if current_epoch >= target_epoch || current_epoch >= self.deactivation_epoch {
|
||||
break;
|
||||
}
|
||||
if let Some(current_cluster_stake) = history.get(¤t_epoch) {
|
||||
prev_epoch = current_epoch;
|
||||
prev_cluster_stake = current_cluster_stake;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
current_effective_stake,
|
||||
delegated_stake - current_effective_stake,
|
||||
)
|
||||
} else {
|
||||
// no history or I've dropped out of history, so assume fully effective
|
||||
(delegated_stake, 0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rewrite_stake(
|
||||
&mut self,
|
||||
account_balance: u64,
|
||||
rent_exempt_balance: u64,
|
||||
) -> Option<(u64, u64)> {
|
||||
// note that this will intentionally overwrite innocent
|
||||
// deactivated-then-immeditealy-withdrawn stake accounts as well
|
||||
// this is chosen to minimize the risks from complicated logic,
|
||||
// over some unneeded rewrites
|
||||
let corrected_stake = account_balance.saturating_sub(rent_exempt_balance);
|
||||
if self.stake != corrected_stake {
|
||||
// this could result in creating a 0-staked account;
|
||||
// rewards and staking calc can handle it.
|
||||
let (old, new) = (self.stake, corrected_stake);
|
||||
self.stake = corrected_stake;
|
||||
Some((old, new))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Clone, Copy, AbiExample)]
|
||||
pub struct Stake {
|
||||
pub delegation: Delegation,
|
||||
/// credits observed is credits from vote account state when delegated or redeemed
|
||||
pub credits_observed: u64,
|
||||
}
|
||||
|
||||
impl Stake {
|
||||
pub fn stake(
|
||||
&self,
|
||||
epoch: Epoch,
|
||||
history: Option<&StakeHistory>,
|
||||
fix_stake_deactivate: bool,
|
||||
) -> u64 {
|
||||
self.delegation.stake(epoch, history, fix_stake_deactivate)
|
||||
}
|
||||
|
||||
pub fn split(
|
||||
&mut self,
|
||||
remaining_stake_delta: u64,
|
||||
split_stake_amount: u64,
|
||||
) -> Result<Self, StakeError> {
|
||||
if remaining_stake_delta > self.delegation.stake {
|
||||
return Err(StakeError::InsufficientStake);
|
||||
}
|
||||
self.delegation.stake -= remaining_stake_delta;
|
||||
let new = Self {
|
||||
delegation: Delegation {
|
||||
stake: split_stake_amount,
|
||||
..self.delegation
|
||||
},
|
||||
..*self
|
||||
};
|
||||
Ok(new)
|
||||
}
|
||||
|
||||
pub fn deactivate(&mut self, epoch: Epoch) -> Result<(), StakeError> {
|
||||
if self.delegation.deactivation_epoch != std::u64::MAX {
|
||||
Err(StakeError::AlreadyDeactivated)
|
||||
} else {
|
||||
self.delegation.deactivation_epoch = epoch;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user