SetLockup now requires the authorized withdrawer when the lockup is not in force

This commit is contained in:
Michael Vines 2021-05-20 14:04:07 -07:00
parent ff0e623d30
commit 96cde36784
3 changed files with 157 additions and 23 deletions

View File

@ -1133,7 +1133,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, keyed_account_at_index},
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.
@ -624,7 +627,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())
}
}
}
@ -635,7 +645,7 @@ mod tests {
use solana_sdk::{
account::{self, Account, AccountSharedData, WritableAccount},
keyed_account::KeyedAccount,
process_instruction::MockInvokeContext,
process_instruction::{mock_set_sysvar, MockInvokeContext},
rent::Rent,
sysvar::stake_history::StakeHistory,
};
@ -717,11 +727,15 @@ mod tests {
.zip(accounts.iter())
.map(|(meta, account)| KeyedAccount::new(&meta.pubkey, meta.is_signer, account))
.collect();
super::process_instruction(
&Pubkey::default(),
&instruction.data,
&mut MockInvokeContext::new(keyed_accounts),
let mut invoke_context = MockInvokeContext::new(keyed_accounts);
mock_set_sysvar(
&mut invoke_context,
sysvar::clock::id(),
sysvar::clock::Clock::default(),
)
.unwrap();
super::process_instruction(&Pubkey::default(), &instruction.data, &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),
@ -2987,7 +3008,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)
);
@ -3006,7 +3027,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)
);
@ -3017,7 +3038,8 @@ mod tests {
epoch: Some(1),
custodian: Some(custodian),
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@ -3054,6 +3076,7 @@ mod tests {
custodian: Some(custodian),
},
&HashSet::default(),
None
),
Err(InstructionError::MissingRequiredSignature)
);
@ -3064,14 +3087,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(
@ -3103,7 +3127,8 @@ mod tests {
epoch: None,
custodian: None,
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@ -3115,7 +3140,8 @@ mod tests {
epoch: None,
custodian: None,
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@ -3137,7 +3163,8 @@ mod tests {
epoch: Some(3),
custodian: None,
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@ -3160,7 +3187,8 @@ mod tests {
epoch: None,
custodian: Some(new_custodian),
},
&vec![custodian].into_iter().collect()
&vec![custodian].into_iter().collect(),
None
),
Ok(())
);
@ -3178,12 +3206,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();