SetLockup now requires the authorized withdrawer when the lockup is not in force (#17394)

(cherry picked from commit 96cde36784)

Co-authored-by: Michael Vines <mvines@gmail.com>
This commit is contained in:
mergify[bot]
2021-05-21 21:17:06 +00:00
committed by GitHub
parent 72d038ecd8
commit df08ba5dcd
3 changed files with 158 additions and 20 deletions

View File

@@ -1123,7 +1123,7 @@ fn test_stake_set_lockup() {
stake_account: 1,
seed: None,
staker: Some(offline_pubkey),
withdrawer: Some(offline_pubkey),
withdrawer: Some(config.signers[0].pubkey()),
lockup,
amount: SpendAmount::Some(10 * minimum_stake_balance),
sign_only: false,

View File

@@ -11,7 +11,7 @@ use solana_sdk::{
feature_set,
instruction::{AccountMeta, Instruction, InstructionError},
keyed_account::{from_keyed_account, get_signers, next_keyed_account, KeyedAccount},
process_instruction::InvokeContext,
process_instruction::{get_sysvar, InvokeContext},
program_utils::limited_deserialize,
pubkey::Pubkey,
system_instruction,
@@ -126,9 +126,12 @@ pub enum StakeInstruction {
/// 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
/// 1. [SIGNER] Lockup authority or withdraw authority
SetLockup(LockupArgs),
/// Merge two stake accounts.
@@ -620,7 +623,14 @@ pub fn process_instruction(
&signers,
),
StakeInstruction::SetLockup(lockup) => me.set_lockup(&lockup, &signers),
StakeInstruction::SetLockup(lockup) => {
let clock = if invoke_context.is_feature_active(&feature_set::stake_program_v4::id()) {
Some(get_sysvar::<Clock>(invoke_context, &sysvar::clock::id())?)
} else {
None
};
me.set_lockup(&lockup, &signers, clock.as_ref())
}
}
}
@@ -630,7 +640,7 @@ mod tests {
use bincode::serialize;
use solana_sdk::{
account::{self, Account, AccountSharedData},
process_instruction::MockInvokeContext,
process_instruction::{mock_set_sysvar, MockInvokeContext},
rent::Rent,
sysvar::stake_history::StakeHistory,
};
@@ -712,11 +722,19 @@ mod tests {
.zip(accounts.iter())
.map(|(meta, account)| KeyedAccount::new(&meta.pubkey, meta.is_signer, account))
.collect();
let mut invoke_context = MockInvokeContext::default();
mock_set_sysvar(
&mut invoke_context,
sysvar::clock::id(),
sysvar::clock::Clock::default(),
)
.unwrap();
super::process_instruction(
&Pubkey::default(),
&keyed_accounts,
&instruction.data,
&mut MockInvokeContext::default(),
&mut invoke_context,
)
}
}

View File

@@ -179,9 +179,28 @@ impl Meta {
&mut self,
lockup: &LockupArgs,
signers: &HashSet<Pubkey>,
clock: Option<&Clock>,
) -> Result<(), InstructionError> {
if !signers.contains(&self.lockup.custodian) {
return Err(InstructionError::MissingRequiredSignature);
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;
@@ -874,6 +893,7 @@ pub trait StakeAccount {
&self,
lockup: &LockupArgs,
signers: &HashSet<Pubkey>,
clock: Option<&Clock>,
) -> Result<(), InstructionError>;
fn split(
&self,
@@ -1054,14 +1074,15 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
&self,
lockup: &LockupArgs,
signers: &HashSet<Pubkey>,
clock: Option<&Clock>,
) -> Result<(), InstructionError> {
match self.state()? {
StakeState::Initialized(mut meta) => {
meta.set_lockup(lockup, signers)?;
meta.set_lockup(lockup, signers, clock)?;
self.set_state(&StakeState::Initialized(meta))
}
StakeState::Stake(mut meta, stake) => {
meta.set_lockup(lockup, signers)?;
meta.set_lockup(lockup, signers, clock)?;
self.set_state(&StakeState::Stake(meta, stake))
}
_ => Err(InstructionError::InvalidAccountData),
@@ -2984,7 +3005,7 @@ mod tests {
// wrong state, should fail
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account);
assert_eq!(
stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default()),
stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default(), None),
Err(InstructionError::InvalidAccountData)
);
@@ -3003,7 +3024,7 @@ mod tests {
.unwrap();
assert_eq!(
stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default()),
stake_keyed_account.set_lockup(&LockupArgs::default(), &HashSet::default(), None),
Err(InstructionError::MissingRequiredSignature)
);
@@ -3014,7 +3035,8 @@ mod tests {
epoch: Some(1),
custodian: Some(custodian),
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@@ -3051,6 +3073,7 @@ mod tests {
custodian: Some(custodian),
},
&HashSet::default(),
None
),
Err(InstructionError::MissingRequiredSignature)
);
@@ -3061,14 +3084,15 @@ mod tests {
epoch: Some(1),
custodian: Some(custodian),
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
}
#[test]
fn test_optional_lockup() {
fn test_optional_lockup_for_stake_program_v3_and_earlier() {
let stake_pubkey = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let stake_account = AccountSharedData::new_ref_data_with_space(
@@ -3100,7 +3124,8 @@ mod tests {
epoch: None,
custodian: None,
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@@ -3112,7 +3137,8 @@ mod tests {
epoch: None,
custodian: None,
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@@ -3134,7 +3160,8 @@ mod tests {
epoch: Some(3),
custodian: None,
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@@ -3157,7 +3184,8 @@ mod tests {
epoch: None,
custodian: Some(new_custodian),
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@@ -3175,12 +3203,104 @@ mod tests {
assert_eq!(
stake_keyed_account.set_lockup(
&LockupArgs::default(),
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Err(InstructionError::MissingRequiredSignature)
);
}
#[test]
fn test_optional_lockup_for_stake_program_v4() {
let stake_pubkey = solana_sdk::pubkey::new_rand();
let stake_lamports = 42;
let stake_account = AccountSharedData::new_ref_data_with_space(
stake_lamports,
&StakeState::Uninitialized,
std::mem::size_of::<StakeState>(),
&id(),
)
.expect("stake_account");
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, false, &stake_account);
let custodian = solana_sdk::pubkey::new_rand();
stake_keyed_account
.initialize(
&Authorized::auto(&stake_pubkey),
&Lockup {
unix_timestamp: 1,
epoch: 1,
custodian,
},
&Rent::free(),
)
.unwrap();
// Lockup in force: authorized withdrawer cannot change it
assert_eq!(
stake_keyed_account.set_lockup(
&LockupArgs {
unix_timestamp: Some(2),
epoch: None,
custodian: None
},
&vec![stake_pubkey].into_iter().collect(),
Some(&Clock::default())
),
Err(InstructionError::MissingRequiredSignature)
);
// Lockup in force: custodian can change it
assert_eq!(
stake_keyed_account.set_lockup(
&LockupArgs {
unix_timestamp: Some(2),
epoch: None,
custodian: None
},
&vec![custodian].into_iter().collect(),
Some(&Clock::default())
),
Ok(())
);
// Lockup expired: custodian cannot change it
assert_eq!(
stake_keyed_account.set_lockup(
&LockupArgs {
unix_timestamp: Some(3),
epoch: None,
custodian: None,
},
&vec![custodian].into_iter().collect(),
Some(&Clock {
unix_timestamp: UnixTimestamp::MAX,
epoch: Epoch::MAX,
..Clock::default()
})
),
Err(InstructionError::MissingRequiredSignature)
);
// Lockup expired: authorized withdrawer can change it
assert_eq!(
stake_keyed_account.set_lockup(
&LockupArgs {
unix_timestamp: Some(3),
epoch: None,
custodian: None,
},
&vec![stake_pubkey].into_iter().collect(),
Some(&Clock {
unix_timestamp: UnixTimestamp::MAX,
epoch: Epoch::MAX,
..Clock::default()
})
),
Ok(())
);
}
#[test]
fn test_withdraw_stake() {
let stake_pubkey = solana_sdk::pubkey::new_rand();