Add StakeInstruction::DeactivateDelinquent
This commit is contained in:
@ -46,6 +46,17 @@ pub enum StakeError {
|
||||
|
||||
#[error("custodian signature not present")]
|
||||
CustodianSignatureMissing,
|
||||
|
||||
#[error("insufficient voting activity in the reference vote account")]
|
||||
InsufficientReferenceVotes,
|
||||
|
||||
#[error("stake account is not delegated to the provided vote account")]
|
||||
VoteAddressMismatch,
|
||||
|
||||
#[error(
|
||||
"stake account has not been delinquent for the minimum epochs required for deactivation"
|
||||
)]
|
||||
MinimumDelinquentEpochsForDeactivationNotMet,
|
||||
}
|
||||
|
||||
impl<E> DecodeError<E> for StakeError {
|
||||
@ -234,6 +245,19 @@ pub enum StakeInstruction {
|
||||
///
|
||||
/// [`get_minimum_delegation()`]: super::tools::get_minimum_delegation
|
||||
GetMinimumDelegation,
|
||||
|
||||
/// Deactivate stake delegated to a vote account that has been delinquent for at least
|
||||
/// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs.
|
||||
///
|
||||
/// No signer is required for this instruction as it is a common good to deactivate abandoned
|
||||
/// stake.
|
||||
///
|
||||
/// # Account references
|
||||
/// 0. `[WRITE]` Delegated stake account
|
||||
/// 1. `[]` Delinquent vote account for the delegated stake account
|
||||
/// 2. `[]` Reference vote account that has voted at least once in the last
|
||||
/// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION` epochs
|
||||
DeactivateDelinquent,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||
@ -698,6 +722,19 @@ pub fn get_minimum_delegation() -> Instruction {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn deactivate_delinquent_stake(
|
||||
stake_account: &Pubkey,
|
||||
delinquent_vote_account: &Pubkey,
|
||||
reference_vote_account: &Pubkey,
|
||||
) -> Instruction {
|
||||
let account_metas = vec![
|
||||
AccountMeta::new(*stake_account, false),
|
||||
AccountMeta::new_readonly(*delinquent_vote_account, false),
|
||||
AccountMeta::new_readonly(*reference_vote_account, false),
|
||||
];
|
||||
Instruction::new_with_bincode(id(), &StakeInstruction::DeactivateDelinquent, account_metas)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use {super::*, crate::instruction::InstructionError};
|
||||
|
@ -10,3 +10,7 @@ pub mod program {
|
||||
// NOTE: This constant will be deprecated soon; if possible, use
|
||||
// `solana_stake_program::get_minimum_delegation()` instead.
|
||||
pub const MINIMUM_STAKE_DELEGATION: u64 = 1;
|
||||
|
||||
/// The minimum number of epochs before stake account that is delegated to a delinquent vote
|
||||
/// account may be unstaked with `StakeInstruction::DeactivateDelinquent`
|
||||
pub const MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION: usize = 5;
|
||||
|
@ -1,5 +1,7 @@
|
||||
//! Utility functions
|
||||
use crate::program_error::ProgramError;
|
||||
use crate::{
|
||||
clock::Epoch, program_error::ProgramError, stake::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION,
|
||||
};
|
||||
|
||||
/// Helper function for programs to call [`GetMinimumDelegation`] and then fetch the return data
|
||||
///
|
||||
@ -36,3 +38,117 @@ fn get_minimum_delegation_return_data() -> Result<u64, ProgramError> {
|
||||
})
|
||||
.map(u64::from_le_bytes)
|
||||
}
|
||||
|
||||
// Check if the provided `epoch_credits` demonstrate active voting over the previous
|
||||
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`
|
||||
pub fn acceptable_reference_epoch_credits(
|
||||
epoch_credits: &[(Epoch, u64, u64)],
|
||||
current_epoch: Epoch,
|
||||
) -> bool {
|
||||
if let Some(epoch_index) = epoch_credits
|
||||
.len()
|
||||
.checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION)
|
||||
{
|
||||
let mut epoch = current_epoch;
|
||||
for (vote_epoch, ..) in epoch_credits[epoch_index..].iter().rev() {
|
||||
if *vote_epoch != epoch {
|
||||
return false;
|
||||
}
|
||||
epoch = epoch.saturating_sub(1);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the provided `epoch_credits` demonstrate delinquency over the previous
|
||||
// `MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION`
|
||||
pub fn eligible_for_deactivate_delinquent(
|
||||
epoch_credits: &[(Epoch, u64, u64)],
|
||||
current_epoch: Epoch,
|
||||
) -> bool {
|
||||
match epoch_credits.last() {
|
||||
None => true,
|
||||
Some((epoch, ..)) => {
|
||||
if let Some(minimum_epoch) =
|
||||
current_epoch.checked_sub(MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch)
|
||||
{
|
||||
*epoch <= minimum_epoch
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_acceptable_reference_epoch_credits() {
|
||||
let epoch_credits = [];
|
||||
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 0));
|
||||
|
||||
let epoch_credits = [(0, 42, 42), (1, 42, 42), (2, 42, 42), (3, 42, 42)];
|
||||
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3));
|
||||
|
||||
let epoch_credits = [
|
||||
(0, 42, 42),
|
||||
(1, 42, 42),
|
||||
(2, 42, 42),
|
||||
(3, 42, 42),
|
||||
(4, 42, 42),
|
||||
];
|
||||
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 3));
|
||||
assert!(acceptable_reference_epoch_credits(&epoch_credits, 4));
|
||||
|
||||
let epoch_credits = [
|
||||
(1, 42, 42),
|
||||
(2, 42, 42),
|
||||
(3, 42, 42),
|
||||
(4, 42, 42),
|
||||
(5, 42, 42),
|
||||
];
|
||||
assert!(acceptable_reference_epoch_credits(&epoch_credits, 5));
|
||||
|
||||
let epoch_credits = [
|
||||
(0, 42, 42),
|
||||
(2, 42, 42),
|
||||
(3, 42, 42),
|
||||
(4, 42, 42),
|
||||
(5, 42, 42),
|
||||
];
|
||||
assert!(!acceptable_reference_epoch_credits(&epoch_credits, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_eligible_for_deactivate_delinquent() {
|
||||
let epoch_credits = [];
|
||||
assert!(eligible_for_deactivate_delinquent(&epoch_credits, 42));
|
||||
|
||||
let epoch_credits = [(0, 42, 42)];
|
||||
assert!(!eligible_for_deactivate_delinquent(&epoch_credits, 0));
|
||||
|
||||
let epoch_credits = [(0, 42, 42)];
|
||||
assert!(!eligible_for_deactivate_delinquent(
|
||||
&epoch_credits,
|
||||
MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1
|
||||
));
|
||||
assert!(eligible_for_deactivate_delinquent(
|
||||
&epoch_credits,
|
||||
MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch
|
||||
));
|
||||
|
||||
let epoch_credits = [(100, 42, 42)];
|
||||
assert!(!eligible_for_deactivate_delinquent(
|
||||
&epoch_credits,
|
||||
100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch - 1
|
||||
));
|
||||
assert!(eligible_for_deactivate_delinquent(
|
||||
&epoch_credits,
|
||||
100 + MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION as Epoch
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -271,6 +271,10 @@ pub mod update_syscall_base_costs {
|
||||
solana_sdk::declare_id!("2h63t332mGCCsWK2nqqqHhN4U9ayyqhLVFvczznHDoTZ");
|
||||
}
|
||||
|
||||
pub mod stake_deactivate_delinquent_instruction {
|
||||
solana_sdk::declare_id!("437r62HoAdUb63amq3D7ENnBLDhHT2xY8eFkLJYVKK4x");
|
||||
}
|
||||
|
||||
pub mod vote_withdraw_authority_may_change_authorized_voter {
|
||||
solana_sdk::declare_id!("AVZS3ZsN4gi6Rkx2QUibYuSJG3S6QHib7xCYhG6vGJxU");
|
||||
}
|
||||
@ -401,6 +405,7 @@ lazy_static! {
|
||||
(require_rent_exempt_accounts::id(), "require all new transaction accounts with data to be rent-exempt"),
|
||||
(filter_votes_outside_slot_hashes::id(), "filter vote slots older than the slot hashes history"),
|
||||
(update_syscall_base_costs::id(), "update syscall base costs"),
|
||||
(stake_deactivate_delinquent_instruction::id(), "enable the deactivate delinquent stake instruction #23932"),
|
||||
(vote_withdraw_authority_may_change_authorized_voter::id(), "vote account withdraw authority may change the authorized voter #22521"),
|
||||
(spl_associated_token_account_v1_0_4::id(), "SPL Associated Token Account Program release version 1.0.4, tied to token 3.3.0 #22648"),
|
||||
(reject_vote_account_close_unless_zero_credit_epoch::id(), "fail vote account withdraw to 0 unless account earned 0 credits in last completed epoch"),
|
||||
|
Reference in New Issue
Block a user