Reject close of active vote accounts (#22651)

* 10461 Reject close of vote accounts unless it earned no credits in the previous epoch. This is checked by comparing current epoch (from clock sysvar) with the most recent epoch with credits in vote state.
This commit is contained in:
Will Hickey 2022-02-02 14:16:24 -06:00 committed by GitHub
parent 58a70d76a3
commit 75563f6c7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 385 additions and 17 deletions

View File

@ -119,7 +119,24 @@ pub fn process_instruction(
} else {
None
};
vote_state::withdraw(me, lamports, to, &signers, rent_sysvar.as_deref())
let clock_if_feature_active = if invoke_context
.feature_set
.is_active(&feature_set::reject_vote_account_close_unless_zero_credit_epoch::id())
{
Some(invoke_context.get_sysvar_cache().get_clock()?)
} else {
None
};
vote_state::withdraw(
me,
lamports,
to,
&signers,
rent_sysvar.as_deref(),
clock_if_feature_active.as_deref(),
)
}
VoteInstruction::AuthorizeChecked(vote_authorize) => {
if invoke_context

View File

@ -1151,6 +1151,7 @@ pub fn withdraw<S: std::hash::BuildHasher>(
to_account: &KeyedAccount,
signers: &HashSet<Pubkey, S>,
rent_sysvar: Option<&Rent>,
clock: Option<&Clock>,
) -> Result<(), InstructionError> {
let vote_state: VoteState =
State::<VoteStateVersions>::state(vote_account)?.convert_to_current();
@ -1163,8 +1164,23 @@ pub fn withdraw<S: std::hash::BuildHasher>(
.ok_or(InstructionError::InsufficientFunds)?;
if remaining_balance == 0 {
// Deinitialize upon zero-balance
vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?;
let reject_active_vote_account_close = clock
.zip(vote_state.epoch_credits.last())
.map(|(clock, (last_epoch_with_credits, _, _))| {
let current_epoch = clock.epoch;
// if current_epoch - last_epoch_with_credits < 2 then the validator has received credits
// either in the current epoch or the previous epoch. If it's >= 2 then it has been at least
// one full epoch since the validator has received credits.
current_epoch.saturating_sub(*last_epoch_with_credits) < 2
})
.unwrap_or(false);
if reject_active_vote_account_close {
return Err(InstructionError::ActiveVoteAccountClose);
} else {
// Deinitialize upon zero-balance
vote_account.set_state(&VoteStateVersions::new_current(VoteState::default()))?;
}
} else if let Some(rent_sysvar) = rent_sysvar {
let min_rent_exempt_balance = rent_sysvar.minimum_balance(vote_account.data_len()?);
if remaining_balance < min_rent_exempt_balance {
@ -1438,6 +1454,39 @@ mod tests {
)
}
fn create_test_account_with_epoch_credits(
credits_to_append: &[u64],
) -> (Pubkey, RefCell<AccountSharedData>) {
let (vote_pubkey, vote_account) = create_test_account();
let vote_account_space = vote_account.borrow().data().len();
let mut vote_state = VoteState::from(&*vote_account.borrow_mut()).unwrap();
vote_state.authorized_withdrawer = vote_pubkey;
vote_state.epoch_credits = Vec::new();
let mut current_epoch_credits = 0;
let mut previous_epoch_credits = 0;
for (epoch, credits) in credits_to_append.iter().enumerate() {
current_epoch_credits += credits;
vote_state.epoch_credits.push((
u64::try_from(epoch).unwrap(),
current_epoch_credits,
previous_epoch_credits,
));
previous_epoch_credits = current_epoch_credits;
}
let lamports = vote_account.borrow().lamports();
let mut vote_account_with_epoch_credits =
AccountSharedData::new(lamports, vote_account_space, &vote_pubkey);
let versioned = VoteStateVersions::new_current(vote_state);
VoteState::to(&versioned, &mut vote_account_with_epoch_credits);
let ref_vote_account_with_epoch_credits = RefCell::new(vote_account_with_epoch_credits);
(vote_pubkey, ref_vote_account_with_epoch_credits)
}
fn simulate_process_vote(
vote_pubkey: &Pubkey,
vote_account: &RefCell<AccountSharedData>,
@ -2222,6 +2271,13 @@ mod tests {
#[test]
fn test_vote_state_withdraw() {
let (vote_pubkey, vote_account) = create_test_account();
let credits_through_epoch_1: Vec<u64> = vec![2, 1];
let credits_through_epoch_2: Vec<u64> = vec![2, 1, 3];
let clock_epoch_3 = &Clock {
epoch: 3,
..Clock::default()
};
// unsigned request
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, false, &vote_account)];
@ -2236,6 +2292,7 @@ mod tests {
),
&signers,
None,
None,
);
assert_eq!(res, Err(InstructionError::MissingRequiredSignature));
@ -2253,17 +2310,24 @@ mod tests {
),
&signers,
None,
Some(&Clock::default()),
);
assert_eq!(res, Err(InstructionError::InsufficientFunds));
// non rent exempt withdraw, before feature activation
// non rent exempt withdraw, before 7txXZZD6 feature activation
// without 0 credit epoch, before ALBk3EWd feature activation
{
let (vote_pubkey, vote_account) = create_test_account();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
let lamports = vote_account.borrow().lamports();
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_2);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account.borrow().data().len())
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
@ -2277,18 +2341,121 @@ mod tests {
),
&signers,
None,
None,
);
assert_eq!(res, Ok(()));
}
// non rent exempt withdraw, after feature activation
// non rent exempt withdraw, before 7txXZZD6 feature activation
// with 0 credit epoch, before ALBk3EWd feature activation
{
let (vote_pubkey, vote_account) = create_test_account();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
let lamports = vote_account.borrow().lamports();
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_1);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account.borrow().data().len())
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports - minimum_balance + 1,
&KeyedAccount::new(
&solana_sdk::pubkey::new_rand(),
false,
&RefCell::new(AccountSharedData::default()),
),
&signers,
None,
None,
);
assert_eq!(res, Ok(()));
}
// non rent exempt withdraw, before 7txXZZD6 feature activation
// without 0 credit epoch, after ALBk3EWd feature activation
{
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_2);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports - minimum_balance + 1,
&KeyedAccount::new(
&solana_sdk::pubkey::new_rand(),
false,
&RefCell::new(AccountSharedData::default()),
),
&signers,
None,
Some(clock_epoch_3),
);
assert_eq!(res, Ok(()));
}
// non rent exempt withdraw, before 7txXZZD6 feature activation
// with 0 credit epoch, after ALBk3EWd activation
{
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_1);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports - minimum_balance + 1,
&KeyedAccount::new(
&solana_sdk::pubkey::new_rand(),
false,
&RefCell::new(AccountSharedData::default()),
),
&signers,
None,
Some(clock_epoch_3),
);
assert_eq!(res, Ok(()));
}
// non rent exempt withdraw, after 7txXZZD6 feature activation
// with 0 credit epoch, before ALBk3EWd feature activation
{
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_1);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
@ -2302,11 +2469,108 @@ mod tests {
),
&signers,
Some(&rent_sysvar),
None,
);
assert_eq!(res, Err(InstructionError::InsufficientFunds));
}
// partial valid withdraw, after feature activation
// non rent exempt withdraw, after 7txXZZD6 feature activation
// without 0 credit epoch, before ALBk3EWd feature activation
{
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_2);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports - minimum_balance + 1,
&KeyedAccount::new(
&solana_sdk::pubkey::new_rand(),
false,
&RefCell::new(AccountSharedData::default()),
),
&signers,
Some(&rent_sysvar),
None,
);
assert_eq!(res, Err(InstructionError::InsufficientFunds));
}
// non rent exempt withdraw, after 7txXZZD6 feature activation
// with 0 credit epoch, after ALBk3EWd feature activation
{
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_1);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports - minimum_balance + 1,
&KeyedAccount::new(
&solana_sdk::pubkey::new_rand(),
false,
&RefCell::new(AccountSharedData::default()),
),
&signers,
Some(&rent_sysvar),
Some(clock_epoch_3),
);
assert_eq!(res, Err(InstructionError::InsufficientFunds));
}
// non rent exempt withdraw, after 7txXZZD6 feature activation
// without 0 credit epoch, after ALBk3EWd feature activation
{
let (vote_pubkey, vote_account_with_epoch_credits) =
create_test_account_with_epoch_credits(&credits_through_epoch_2);
let keyed_accounts = &[KeyedAccount::new(
&vote_pubkey,
true,
&vote_account_with_epoch_credits,
)];
let lamports = vote_account_with_epoch_credits.borrow().lamports();
let rent_sysvar = Rent::default();
let minimum_balance = rent_sysvar
.minimum_balance(vote_account_with_epoch_credits.borrow().data().len())
.max(1);
assert!(minimum_balance <= lamports);
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports - minimum_balance + 1,
&KeyedAccount::new(
&solana_sdk::pubkey::new_rand(),
false,
&RefCell::new(AccountSharedData::default()),
),
&signers,
Some(&rent_sysvar),
Some(clock_epoch_3),
);
assert_eq!(res, Err(InstructionError::InsufficientFunds));
}
// partial valid withdraw, after 7txXZZD6 feature activation
{
let to_account = RefCell::new(AccountSharedData::default());
let (vote_pubkey, vote_account) = create_test_account();
@ -2325,6 +2589,7 @@ mod tests {
&KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account),
&signers,
Some(&rent_sysvar),
Some(&Clock::default()),
);
assert_eq!(res, Ok(()));
assert_eq!(
@ -2334,12 +2599,45 @@ mod tests {
assert_eq!(to_account.borrow().lamports(), withdraw_lamports);
}
// full withdraw, before/after activation
// full withdraw, before/after 7txXZZD6 feature activation
// with/without 0 credit epoch, before ALBk3EWd feature activation
{
let rent_sysvar = Rent::default();
for rent_sysvar in [None, Some(&rent_sysvar)] {
for credits in [&credits_through_epoch_1, &credits_through_epoch_2] {
let to_account = RefCell::new(AccountSharedData::default());
let (vote_pubkey, vote_account) =
create_test_account_with_epoch_credits(credits);
let lamports = vote_account.borrow().lamports();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports,
&KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account),
&signers,
rent_sysvar,
None,
);
assert_eq!(res, Ok(()));
assert_eq!(vote_account.borrow().lamports(), 0);
assert_eq!(to_account.borrow().lamports(), lamports);
let post_state: VoteStateVersions = vote_account.borrow().state().unwrap();
// State has been deinitialized since balance is zero
assert!(post_state.is_uninitialized());
}
}
}
// full withdraw, before/after 7txXZZD6 feature activation
// with 0 credit epoch, after ALBk3EWd feature activation
{
let rent_sysvar = Rent::default();
for rent_sysvar in [None, Some(&rent_sysvar)] {
let to_account = RefCell::new(AccountSharedData::default());
let (vote_pubkey, vote_account) = create_test_account();
// let (vote_pubkey, vote_account) = create_test_account();
let (vote_pubkey, vote_account) =
create_test_account_with_epoch_credits(&credits_through_epoch_1);
let lamports = vote_account.borrow().lamports();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
@ -2349,6 +2647,7 @@ mod tests {
&KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account),
&signers,
rent_sysvar,
Some(clock_epoch_3),
);
assert_eq!(res, Ok(()));
assert_eq!(vote_account.borrow().lamports(), 0);
@ -2359,6 +2658,35 @@ mod tests {
}
}
// full withdraw, before/after 7txXZZD6 feature activation
// without 0 credit epoch, after ALBk3EWd feature activation
{
let rent_sysvar = Rent::default();
for rent_sysvar in [None, Some(&rent_sysvar)] {
let to_account = RefCell::new(AccountSharedData::default());
// let (vote_pubkey, vote_account) = create_test_account();
let (vote_pubkey, vote_account) =
create_test_account_with_epoch_credits(&credits_through_epoch_2);
let lamports = vote_account.borrow().lamports();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
let signers: HashSet<Pubkey> = get_signers(keyed_accounts);
let res = withdraw(
&keyed_accounts[0],
lamports,
&KeyedAccount::new(&solana_sdk::pubkey::new_rand(), false, &to_account),
&signers,
rent_sysvar,
Some(clock_epoch_3),
);
assert_eq!(res, Err(InstructionError::ActiveVoteAccountClose));
assert_eq!(vote_account.borrow().lamports(), lamports);
assert_eq!(to_account.borrow().lamports(), 0);
let post_state: VoteStateVersions = vote_account.borrow().state().unwrap();
// State is still initialized
assert!(!post_state.is_uninitialized());
}
}
// authorize authorized_withdrawer
let authorized_withdrawer_pubkey = solana_sdk::pubkey::new_rand();
let keyed_accounts = &[KeyedAccount::new(&vote_pubkey, true, &vote_account)];
@ -2388,6 +2716,7 @@ mod tests {
withdrawer_keyed_account,
&signers,
None,
None,
);
assert_eq!(res, Ok(()));
assert_eq!(vote_account.borrow().lamports(), 0);

View File

@ -214,7 +214,7 @@ impl RentDebits {
}
type BankStatusCache = StatusCache<Result<()>>;
#[frozen_abi(digest = "FPLuTUU5MjwsijzDubxY6BvBEkWULhYNUyY6Puqejb4g")]
#[frozen_abi(digest = "6XkxpmzmKZguLZMS1KmU7N2dAcv8MmNhyobJCwRLkTdi")]
pub type BankSlotDelta = SlotDelta<Result<()>>;
// Eager rent collection repeats in cyclic manner.

View File

@ -252,6 +252,10 @@ pub enum InstructionError {
/// Accounts data budget exceeded
#[error("Requested account data allocation exceeded the accounts data budget")]
AccountsDataBudgetExceeded,
/// Active vote account close
#[error("Cannot close vote account unless it stopped voting at least one full epoch ago")]
ActiveVoteAccountClose,
// Note: For any new error added here an equivalent ProgramError and its
// conversions must also be added
}

View File

@ -51,6 +51,8 @@ pub enum ProgramError {
IllegalOwner,
#[error("Requested account data allocation exceeded the accounts data budget")]
AccountsDataBudgetExceeded,
#[error("Cannot close vote account unless it stopped voting at least one full epoch ago")]
ActiveVoteAccountClose,
}
pub trait PrintProgramError {
@ -90,6 +92,7 @@ impl PrintProgramError for ProgramError {
Self::UnsupportedSysvar => msg!("Error: UnsupportedSysvar"),
Self::IllegalOwner => msg!("Error: IllegalOwner"),
Self::AccountsDataBudgetExceeded => msg!("Error: AccountsDataBudgetExceeded"),
Self::ActiveVoteAccountClose => msg!("Error: ActiveVoteAccountClose"),
}
}
}
@ -121,6 +124,7 @@ pub const ACCOUNT_NOT_RENT_EXEMPT: u64 = to_builtin!(16);
pub const UNSUPPORTED_SYSVAR: u64 = to_builtin!(17);
pub const ILLEGAL_OWNER: u64 = to_builtin!(18);
pub const ACCOUNTS_DATA_BUDGET_EXCEEDED: u64 = to_builtin!(19);
pub const ACTIVE_VOTE_ACCOUNT_CLOSE: u64 = to_builtin!(20);
// Warning: Any new program errors added here must also be:
// - Added to the below conversions
// - Added as an equivilent to InstructionError
@ -148,6 +152,7 @@ impl From<ProgramError> for u64 {
ProgramError::UnsupportedSysvar => UNSUPPORTED_SYSVAR,
ProgramError::IllegalOwner => ILLEGAL_OWNER,
ProgramError::AccountsDataBudgetExceeded => ACCOUNTS_DATA_BUDGET_EXCEEDED,
ProgramError::ActiveVoteAccountClose => ACTIVE_VOTE_ACCOUNT_CLOSE,
ProgramError::Custom(error) => {
if error == 0 {
CUSTOM_ZERO
@ -181,6 +186,7 @@ impl From<u64> for ProgramError {
UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar,
ILLEGAL_OWNER => Self::IllegalOwner,
ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded,
ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose,
_ => Self::Custom(error as u32),
}
}
@ -210,6 +216,7 @@ impl TryFrom<InstructionError> for ProgramError {
Self::Error::UnsupportedSysvar => Ok(Self::UnsupportedSysvar),
Self::Error::IllegalOwner => Ok(Self::IllegalOwner),
Self::Error::AccountsDataBudgetExceeded => Ok(Self::AccountsDataBudgetExceeded),
Self::Error::ActiveVoteAccountClose => Ok(Self::ActiveVoteAccountClose),
_ => Err(error),
}
}
@ -241,6 +248,7 @@ where
UNSUPPORTED_SYSVAR => Self::UnsupportedSysvar,
ILLEGAL_OWNER => Self::IllegalOwner,
ACCOUNTS_DATA_BUDGET_EXCEEDED => Self::AccountsDataBudgetExceeded,
ACTIVE_VOTE_ACCOUNT_CLOSE => Self::ActiveVoteAccountClose,
_ => {
// A valid custom error has no bits set in the upper 32
if error >> BUILTIN_BIT_SHIFT == 0 {

View File

@ -307,6 +307,10 @@ pub mod spl_associated_token_account_v1_0_4 {
solana_sdk::declare_id!("FaTa4SpiaSNH44PGC4z8bnGVTkSRYaWvrBs3KTu8XQQq");
}
pub mod reject_vote_account_close_unless_zero_credit_epoch {
solana_sdk::declare_id!("ALBk3EWdeAg2WAGf6GPDUf1nynyNqCdEVmgouG7rpuCj");
}
lazy_static! {
/// Map of feature identifiers to user-visible description
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
@ -378,6 +382,7 @@ lazy_static! {
(update_syscall_base_costs::id(), "Update syscall base costs"),
(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"),
/*************** ADD NEW FEATURES HERE ***************/
]
.iter()

View File

@ -113,6 +113,7 @@ enum InstructionErrorType {
UNSUPPORTED_SYSVAR = 48;
ILLEGAL_OWNER = 49;
ACCOUNTS_DATA_BUDGET_EXCEEDED = 50;
ACTIVE_VOTE_ACCOUNT_CLOSE = 51;
}
message UnixTimestamp {

View File

@ -689,6 +689,7 @@ impl TryFrom<tx_by_addr::TransactionError> for TransactionError {
48 => InstructionError::UnsupportedSysvar,
49 => InstructionError::IllegalOwner,
50 => InstructionError::AccountsDataBudgetExceeded,
51 => InstructionError::ActiveVoteAccountClose,
_ => return Err("Invalid InstructionError"),
};
@ -979,6 +980,9 @@ impl From<TransactionError> for tx_by_addr::TransactionError {
InstructionError::AccountsDataBudgetExceeded => {
tx_by_addr::InstructionErrorType::AccountsDataBudgetExceeded
}
InstructionError::ActiveVoteAccountClose => {
tx_by_addr::InstructionErrorType::ActiveVoteAccountClose
}
} as i32,
custom: match instruction_error {
InstructionError::Custom(custom) => {