From 992b313941e410404cb1cda8c92ef21ce2323795 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 15 Sep 2021 22:53:44 +0000 Subject: [PATCH] Cli: check current authorities before attempting to change them (backport #19853) (#19923) * Cli: check current authorities before attempting to change them (#19853) * Stake-authorize: check account current authority * Stake-set-lockup: check account current custodian * Make helper fn pub(crate) * Vote-authorize: check account current authority (cherry picked from commit 15144fc9236750bc604a1e6e8f653207c6fd2a78) # Conflicts: # cli/src/vote.rs * Fix conflict Co-authored-by: Tyera Eulberg Co-authored-by: Tyera Eulberg --- cli/src/cli.rs | 27 +++++++++++--- cli/src/stake.rs | 94 ++++++++++++++++++++++++++++++++++++++++++++++++ cli/src/vote.rs | 21 +++++++++++ 3 files changed, 138 insertions(+), 4 deletions(-) diff --git a/cli/src/cli.rs b/cli/src/cli.rs index b77321bd15..9cfde30a16 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1951,17 +1951,36 @@ mod tests { let result = process_command(&config); assert!(result.is_ok()); + let vote_account_info_response = json!(Response { + context: RpcResponseContext { slot: 1 }, + value: json!({ + "data": ["KLUv/QBYNQIAtAIBAAAAbnoc3Smwt4/ROvTFWY/v9O8qlxZuPKby5Pv8zYBQW/EFAAEAAB8ACQD6gx92zAiAAecDP4B2XeEBSIx7MQeung==", "base64+zstd"], + "lamports": 42, + "owner": "Vote111111111111111111111111111111111111111", + "executable": false, + "rentEpoch": 1, + }), + }); + let mut mocks = HashMap::new(); + mocks.insert(RpcRequest::GetAccountInfo, vote_account_info_response); + let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks); + let mut vote_config = CliConfig { + rpc_client: Some(Arc::new(rpc_client)), + json_rpc_url: "http://127.0.0.1:8899".to_string(), + ..CliConfig::default() + }; + let current_authority = keypair_from_seed(&[5; 32]).unwrap(); let new_authorized_pubkey = solana_sdk::pubkey::new_rand(); - config.signers = vec![&bob_keypair]; - config.command = CliCommand::VoteAuthorize { + vote_config.signers = vec![¤t_authority]; + vote_config.command = CliCommand::VoteAuthorize { vote_account_pubkey: bob_pubkey, new_authorized_pubkey, - vote_authorize: VoteAuthorize::Voter, + vote_authorize: VoteAuthorize::Withdrawer, memo: None, authorized: 0, new_authorized: None, }; - let result = process_command(&config); + let result = process_command(&vote_config); assert!(result.is_ok()); let new_identity_keypair = Keypair::new(); diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 4125b296da..5dc6b45f5f 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -32,6 +32,7 @@ use solana_sdk::{ account::from_account, account_utils::StateMut, clock::{Clock, UnixTimestamp, SECONDS_PER_DAY}, + commitment_config::CommitmentConfig, epoch_schedule::EpochSchedule, message::Message, pubkey::Pubkey, @@ -1354,6 +1355,15 @@ pub fn process_stake_authorize( ) -> ProcessResult { let mut ixs = Vec::new(); let custodian = custodian.map(|index| config.signers[index]); + let current_stake_account = if !sign_only { + Some(get_stake_account_state( + rpc_client, + stake_account_pubkey, + config.commitment, + )?) + } else { + None + }; for StakeAuthorizationIndexed { authorization_type, new_authority_pubkey, @@ -1366,6 +1376,29 @@ pub fn process_stake_authorize( (new_authority_pubkey, "new_authorized_pubkey".to_string()), )?; let authority = config.signers[*authority]; + if let Some(current_stake_account) = current_stake_account { + let authorized = match current_stake_account { + StakeState::Stake(Meta { authorized, .. }, ..) => Some(authorized), + StakeState::Initialized(Meta { authorized, .. }) => Some(authorized), + _ => None, + }; + if let Some(authorized) = authorized { + match authorization_type { + StakeAuthorize::Staker => { + check_current_authority(&authorized.staker, &authority.pubkey())?; + } + StakeAuthorize::Withdrawer => { + check_current_authority(&authorized.withdrawer, &authority.pubkey())?; + } + } + } else { + return Err(CliError::RpcRequestError(format!( + "{:?} is not an Initialized or Delegated stake account", + stake_account_pubkey, + )) + .into()); + } + } if new_authority_signer.is_some() { ixs.push(stake_instruction::authorize_checked( stake_account_pubkey, // stake account to update @@ -1899,6 +1932,26 @@ pub fn process_stake_set_lockup( let nonce_authority = config.signers[nonce_authority]; let fee_payer = config.signers[fee_payer]; + if !sign_only { + let state = get_stake_account_state(rpc_client, stake_account_pubkey, config.commitment)?; + let lockup = match state { + StakeState::Stake(Meta { lockup, .. }, ..) => Some(lockup), + StakeState::Initialized(Meta { lockup, .. }) => Some(lockup), + _ => None, + }; + if let Some(lockup) = lockup { + if lockup.custodian != Pubkey::default() { + check_current_authority(&lockup.custodian, &custodian.pubkey())?; + } + } else { + return Err(CliError::RpcRequestError(format!( + "{:?} is not an Initialized or Delegated stake account", + stake_account_pubkey, + )) + .into()); + } + } + let message = if let Some(nonce_account) = &nonce_account { Message::new_with_nonce( ixs, @@ -2041,6 +2094,47 @@ pub fn build_stake_state( } } +fn get_stake_account_state( + rpc_client: &RpcClient, + stake_account_pubkey: &Pubkey, + commitment_config: CommitmentConfig, +) -> Result> { + let stake_account = rpc_client + .get_account_with_commitment(stake_account_pubkey, commitment_config)? + .value + .ok_or_else(|| { + CliError::RpcRequestError(format!("{:?} account does not exist", stake_account_pubkey)) + })?; + if stake_account.owner != stake::program::id() { + return Err(CliError::RpcRequestError(format!( + "{:?} is not a stake account", + stake_account_pubkey, + )) + .into()); + } + stake_account.state().map_err(|err| { + CliError::RpcRequestError(format!( + "Account data could not be deserialized to stake state: {}", + err + )) + .into() + }) +} + +pub(crate) fn check_current_authority( + account_current_authority: &Pubkey, + provided_current_authority: &Pubkey, +) -> Result<(), CliError> { + if account_current_authority != provided_current_authority { + Err(CliError::RpcRequestError(format!( + "Invalid current authority provided: {:?}, expected {:?}", + provided_current_authority, account_current_authority + ))) + } else { + Ok(()) + } +} + pub fn get_epoch_boundary_timestamps( rpc_client: &RpcClient, reward: &RpcInflationReward, diff --git a/cli/src/vote.rs b/cli/src/vote.rs index 264d96bc7f..1dd1dbb142 100644 --- a/cli/src/vote.rs +++ b/cli/src/vote.rs @@ -6,6 +6,7 @@ use crate::{ }, memo::WithMemo, spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, + stake::check_current_authority, }; use clap::{value_t_or_exit, App, Arg, ArgMatches, SubCommand}; use solana_clap_utils::{ @@ -669,6 +670,26 @@ pub fn process_vote_authorize( (&authorized.pubkey(), "authorized_account".to_string()), (new_authorized_pubkey, "new_authorized_pubkey".to_string()), )?; + + let (_, vote_state) = get_vote_account(rpc_client, vote_account_pubkey, config.commitment)?; + match vote_authorize { + VoteAuthorize::Voter => { + let current_authorized_voter = vote_state + .authorized_voters() + .last() + .ok_or_else(|| { + CliError::RpcRequestError( + "Invalid vote account state; no authorized voters found".to_string(), + ) + })? + .1; + check_current_authority(current_authorized_voter, &authorized.pubkey())? + } + VoteAuthorize::Withdrawer => { + check_current_authority(&vote_state.authorized_withdrawer, &authorized.pubkey())? + } + } + let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?; let vote_ix = if new_authorized_signer.is_some() { vote_instruction::authorize_checked(