diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 6ad72e25b..35507f301 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -260,6 +260,7 @@ pub enum CliCommand { nonce_account: Option, nonce_authority: SignerIndex, fee_payer: SignerIndex, + custodian: Option, }, StakeSetLockup { stake_account_pubkey: Pubkey, @@ -1520,11 +1521,13 @@ pub fn process_command(config: &CliConfig) -> ProcessResult { nonce_account, nonce_authority, fee_payer, + custodian, } => process_stake_authorize( &rpc_client, config, &stake_account_pubkey, new_authorizations, + *custodian, *sign_only, blockhash_query, *nonce_account, diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 4f25b5221..8bf220c3f 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -64,6 +64,12 @@ pub const WITHDRAW_AUTHORITY_ARG: ArgConstant<'static> = ArgConstant { help: "Authorized withdrawer [default: cli config keypair]", }; +pub const CUSTODIAN_ARG: ArgConstant<'static> = ArgConstant { + name: "custodian", + long: "custodian", + help: "Authority to override account lockup", +}; + fn stake_authority_arg<'a, 'b>() -> Arg<'a, 'b> { Arg::with_name(STAKE_AUTHORITY_ARG.name) .long(STAKE_AUTHORITY_ARG.long) @@ -82,6 +88,15 @@ fn withdraw_authority_arg<'a, 'b>() -> Arg<'a, 'b> { .help(WITHDRAW_AUTHORITY_ARG.help) } +fn custodian_arg<'a, 'b>() -> Arg<'a, 'b> { + Arg::with_name(CUSTODIAN_ARG.name) + .long(CUSTODIAN_ARG.long) + .takes_value(true) + .value_name("KEYPAIR") + .validator(is_valid_signer) + .help(CUSTODIAN_ARG.help) +} + pub trait StakeSubCommands { fn stake_subcommands(self) -> Self; } @@ -223,6 +238,7 @@ impl StakeSubCommands for App<'_, '_> { .offline_args() .nonce_args(false) .arg(fee_payer_arg()) + .arg(custodian_arg()) ) .subcommand( SubCommand::with_name("deactivate-stake") @@ -331,14 +347,7 @@ impl StakeSubCommands for App<'_, '_> { .offline_args() .nonce_args(false) .arg(fee_payer_arg()) - .arg( - Arg::with_name("custodian") - .long("custodian") - .takes_value(true) - .value_name("KEYPAIR") - .validator(is_valid_signer) - .help("Authority to override account lockup") - ) + .arg(custodian_arg()) ) .subcommand( SubCommand::with_name("stake-set-lockup") @@ -561,11 +570,15 @@ pub fn parse_stake_authorize( let (nonce_authority, nonce_authority_pubkey) = signer_of(matches, NONCE_AUTHORITY_ARG.name, wallet_manager)?; let (fee_payer, fee_payer_pubkey) = signer_of(matches, FEE_PAYER_ARG.name, wallet_manager)?; + let (custodian, custodian_pubkey) = signer_of(matches, "custodian", wallet_manager)?; bulk_signers.push(fee_payer); if nonce_account.is_some() { bulk_signers.push(nonce_authority); } + if custodian.is_some() { + bulk_signers.push(custodian); + } let signer_info = default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?; @@ -591,6 +604,7 @@ pub fn parse_stake_authorize( nonce_account, nonce_authority: signer_info.index_of(nonce_authority_pubkey).unwrap(), fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(), + custodian: custodian_pubkey.and_then(|_| signer_info.index_of(custodian_pubkey)), }, signers: signer_info.signers, }) @@ -970,6 +984,7 @@ pub fn process_stake_authorize( config: &CliConfig, stake_account_pubkey: &Pubkey, new_authorizations: &[(StakeAuthorize, Pubkey, SignerIndex)], + custodian: Option, sign_only: bool, blockhash_query: &BlockhashQuery, nonce_account: Option, @@ -977,6 +992,7 @@ pub fn process_stake_authorize( fee_payer: SignerIndex, ) -> ProcessResult { let mut ixs = Vec::new(); + let custodian = custodian.map(|index| config.signers[index]); for (stake_authorize, authorized_pubkey, authority) in new_authorizations.iter() { check_unique_pubkeys( (stake_account_pubkey, "stake_account_pubkey".to_string()), @@ -988,6 +1004,7 @@ pub fn process_stake_authorize( &authority.pubkey(), // currently authorized authorized_pubkey, // new stake signer *stake_authorize, // stake or withdraw + custodian.map(|signer| signer.pubkey()).as_ref(), )); } @@ -1939,6 +1956,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),], }, @@ -1973,6 +1991,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2011,6 +2030,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2038,6 +2058,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),], }, @@ -2062,6 +2083,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2092,6 +2114,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2123,6 +2146,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into(),], }, @@ -2151,6 +2175,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2185,6 +2210,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } @@ -2221,6 +2247,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 1, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2267,6 +2294,7 @@ mod tests { nonce_account: Some(nonce_account), nonce_authority: 2, fee_payer: 1, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2299,6 +2327,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }, signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()], } @@ -2336,6 +2365,7 @@ mod tests { nonce_account: Some(nonce_account_pubkey), nonce_authority: 1, fee_payer: 0, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2369,6 +2399,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 1, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), @@ -2406,6 +2437,7 @@ mod tests { nonce_account: None, nonce_authority: 0, fee_payer: 1, + custodian: None, }, signers: vec![ read_keypair_file(&default_keypair_file).unwrap().into(), diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 8a1c91f51..b9af88ef3 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -592,6 +592,7 @@ fn test_stake_authorize() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -619,6 +620,7 @@ fn test_stake_authorize() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -641,6 +643,7 @@ fn test_stake_authorize() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -663,6 +666,7 @@ fn test_stake_authorize() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; config_offline.output_format = OutputFormat::JsonCompact; let sign_reply = process_command(&config_offline).unwrap(); @@ -678,6 +682,7 @@ fn test_stake_authorize() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -724,6 +729,7 @@ fn test_stake_authorize() { nonce_account: Some(nonce_account.pubkey()), nonce_authority: 0, fee_payer: 0, + custodian: None, }; let sign_reply = process_command(&config_offline).unwrap(); let sign_only = parse_sign_only_reply_string(&sign_reply); @@ -743,6 +749,7 @@ fn test_stake_authorize() { nonce_account: Some(nonce_account.pubkey()), nonce_authority: 0, fee_payer: 0, + custodian: None, }; process_command(&config).unwrap(); let stake_account = rpc_client.get_account(&stake_account_pubkey).unwrap(); @@ -845,6 +852,7 @@ fn test_stake_authorize_with_fee_payer() { nonce_account: None, nonce_authority: 0, fee_payer: 1, + custodian: None, }; process_command(&config).unwrap(); // `config` balance has not changed, despite submitting the TX @@ -863,6 +871,7 @@ fn test_stake_authorize_with_fee_payer() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; config_offline.output_format = OutputFormat::JsonCompact; let sign_reply = process_command(&config_offline).unwrap(); @@ -878,6 +887,7 @@ fn test_stake_authorize_with_fee_payer() { nonce_account: None, nonce_authority: 0, fee_payer: 0, + custodian: None, }; process_command(&config).unwrap(); // `config`'s balance again has not changed diff --git a/core/src/rpc_pubsub.rs b/core/src/rpc_pubsub.rs index 10dac56f2..877116a4f 100644 --- a/core/src/rpc_pubsub.rs +++ b/core/src/rpc_pubsub.rs @@ -781,6 +781,7 @@ mod tests { &stake_authority.pubkey(), &new_stake_authority, StakeAuthorize::Staker, + None, ); let message = Message::new(&[ix], Some(&stake_authority.pubkey())); let tx = Transaction::new(&[&stake_authority], message, blockhash); diff --git a/programs/stake/src/stake_instruction.rs b/programs/stake/src/stake_instruction.rs index 19568f762..11586cd8b 100644 --- a/programs/stake/src/stake_instruction.rs +++ b/programs/stake/src/stake_instruction.rs @@ -68,6 +68,8 @@ pub enum StakeInstruction { /// 0. [WRITE] Stake account to be updated /// 1. [] Clock sysvar /// 2. [SIGNER] The stake or withdraw authority + /// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration Authorize(Pubkey, StakeAuthorize), /// Delegate a stake to a particular vote account @@ -139,6 +141,8 @@ pub enum StakeInstruction { /// 0. [WRITE] Stake account to be updated /// 1. [SIGNER] Base key of stake or withdraw authority /// 2. [] Clock sysvar + /// 3. Optional: [SIGNER] Lockup authority, if updating StakeAuthorize::Withdrawer before + /// lockup expiration AuthorizeWithSeed(AuthorizeWithSeedArgs), } @@ -344,13 +348,18 @@ pub fn authorize( authorized_pubkey: &Pubkey, new_authorized_pubkey: &Pubkey, stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, ) -> Instruction { - let account_metas = vec![ + let mut account_metas = vec![ AccountMeta::new(*stake_pubkey, false), AccountMeta::new_readonly(sysvar::clock::id(), false), AccountMeta::new_readonly(*authorized_pubkey, true), ]; + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + Instruction::new( id(), &StakeInstruction::Authorize(*new_authorized_pubkey, stake_authorize), @@ -365,13 +374,18 @@ pub fn authorize_with_seed( authority_owner: &Pubkey, new_authorized_pubkey: &Pubkey, stake_authorize: StakeAuthorize, + custodian_pubkey: Option<&Pubkey>, ) -> Instruction { - let account_metas = vec![ + let mut account_metas = vec![ AccountMeta::new(*stake_pubkey, false), AccountMeta::new_readonly(*authority_base, true), AccountMeta::new_readonly(sysvar::clock::id(), false), ]; + if let Some(custodian_pubkey) = custodian_pubkey { + account_metas.push(AccountMeta::new_readonly(*custodian_pubkey, true)); + } + let args = AuthorizeWithSeedArgs { new_authorized_pubkey: *new_authorized_pubkey, stake_authorize, @@ -637,7 +651,8 @@ mod tests { &Pubkey::default(), &Pubkey::default(), &Pubkey::default(), - StakeAuthorize::Staker + StakeAuthorize::Staker, + None, )), Err(InstructionError::InvalidAccountData), ); @@ -722,7 +737,8 @@ mod tests { &spoofed_stake_state_pubkey(), &Pubkey::default(), &Pubkey::default(), - StakeAuthorize::Staker + StakeAuthorize::Staker, + None, )), Err(InstructionError::IncorrectProgramId), ); diff --git a/stake-accounts/src/stake_accounts.rs b/stake-accounts/src/stake_accounts.rs index ff81a2677..65fdf7334 100644 --- a/stake-accounts/src/stake_accounts.rs +++ b/stake-accounts/src/stake_accounts.rs @@ -66,12 +66,14 @@ fn authorize_stake_accounts_instructions( stake_authority_pubkey, new_stake_authority_pubkey, StakeAuthorize::Staker, + None, ); let instruction1 = stake_instruction::authorize( &stake_account_address, withdraw_authority_pubkey, new_withdraw_authority_pubkey, StakeAuthorize::Withdrawer, + None, ); vec![instruction0, instruction1] } diff --git a/tokens/src/commands.rs b/tokens/src/commands.rs index e76816ee1..d2a6187a5 100644 --- a/tokens/src/commands.rs +++ b/tokens/src/commands.rs @@ -211,6 +211,7 @@ fn distribution_instructions( &stake_authority, &recipient, StakeAuthorize::Staker, + None, )); // Make the recipient the new withdraw authority @@ -219,6 +220,7 @@ fn distribution_instructions( &withdraw_authority, &recipient, StakeAuthorize::Withdrawer, + None, )); // Add lockup diff --git a/transaction-status/src/parse_stake.rs b/transaction-status/src/parse_stake.rs index fa60ce018..a0f4ed7d8 100644 --- a/transaction-status/src/parse_stake.rs +++ b/transaction-status/src/parse_stake.rs @@ -45,15 +45,23 @@ pub fn parse_stake( } StakeInstruction::Authorize(new_authorized, authority_type) => { check_num_stake_accounts(&instruction.accounts, 3)?; + let mut value = json!({ + "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), + "clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(), + "authority": account_keys[instruction.accounts[2] as usize].to_string(), + "newAuthority": new_authorized.to_string(), + "authorityType": authority_type, + }); + let map = value.as_object_mut().unwrap(); + if instruction.accounts.len() >= 4 { + map.insert( + "custodian".to_string(), + json!(account_keys[instruction.accounts[3] as usize].to_string()), + ); + } Ok(ParsedInstructionEnum { instruction_type: "authorize".to_string(), - info: json!({ - "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), - "clockSysvar": account_keys[instruction.accounts[1] as usize].to_string(), - "authority": account_keys[instruction.accounts[2] as usize].to_string(), - "newAuthority": new_authorized.to_string(), - "authorityType": authority_type, - }), + info: value, }) } StakeInstruction::DelegateStake => { @@ -93,7 +101,7 @@ pub fn parse_stake( "lamports": lamports, }); let map = value.as_object_mut().unwrap(); - if instruction.accounts.len() == 6 { + if instruction.accounts.len() >= 6 { map.insert( "custodian".to_string(), json!(account_keys[instruction.accounts[5] as usize].to_string()), @@ -151,16 +159,30 @@ pub fn parse_stake( } StakeInstruction::AuthorizeWithSeed(args) => { check_num_stake_accounts(&instruction.accounts, 2)?; - Ok(ParsedInstructionEnum { - instruction_type: "authorizeWithSeed".to_string(), - info: json!({ + let mut value = json!({ "stakeAccount": account_keys[instruction.accounts[0] as usize].to_string(), "authorityBase": account_keys[instruction.accounts[1] as usize].to_string(), "newAuthorized": args.new_authorized_pubkey.to_string(), "authorityType": args.stake_authorize, "authoritySeed": args.authority_seed, "authorityOwner": args.authority_owner.to_string(), - }), + }); + let map = value.as_object_mut().unwrap(); + if instruction.accounts.len() >= 3 { + map.insert( + "clockSysvar".to_string(), + json!(account_keys[instruction.accounts[2] as usize].to_string()), + ); + } + if instruction.accounts.len() >= 4 { + map.insert( + "custodian".to_string(), + json!(account_keys[instruction.accounts[3] as usize].to_string()), + ); + } + Ok(ParsedInstructionEnum { + instruction_type: "authorizeWithSeed".to_string(), + info: value, }) } } @@ -184,17 +206,17 @@ mod test { fn test_parse_stake_instruction() { let mut keys: Vec = vec![]; for _ in 0..6 { - keys.push(solana_sdk::pubkey::new_rand()); + keys.push(Pubkey::new_unique()); } let authorized = Authorized { - staker: solana_sdk::pubkey::new_rand(), - withdrawer: solana_sdk::pubkey::new_rand(), + staker: Pubkey::new_unique(), + withdrawer: Pubkey::new_unique(), }; let lockup = Lockup { unix_timestamp: 1_234_567_890, epoch: 11, - custodian: solana_sdk::pubkey::new_rand(), + custodian: Pubkey::new_unique(), }; let lamports = 55; @@ -222,9 +244,13 @@ mod test { ); assert!(parse_stake(&message.instructions[1], &keys[0..2]).is_err()); - let authority_type = StakeAuthorize::Staker; - let instruction = - stake_instruction::authorize(&keys[1], &keys[0], &keys[3], authority_type); + let instruction = stake_instruction::authorize( + &keys[1], + &keys[0], + &keys[3], + StakeAuthorize::Staker, + None, + ); let message = Message::new(&[instruction], None); assert_eq!( parse_stake(&message.instructions[0], &keys[0..3]).unwrap(), @@ -235,7 +261,31 @@ mod test { "clockSysvar": keys[2].to_string(), "authority": keys[0].to_string(), "newAuthority": keys[3].to_string(), - "authorityType": authority_type, + "authorityType": StakeAuthorize::Staker, + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..2]).is_err()); + + let instruction = stake_instruction::authorize( + &keys[1], + &keys[0], + &keys[3], + StakeAuthorize::Withdrawer, + Some(&keys[1]), + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..3]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorize".to_string(), + info: json!({ + "stakeAccount": keys[1].to_string(), + "clockSysvar": keys[2].to_string(), + "authority": keys[0].to_string(), + "newAuthority": keys[3].to_string(), + "authorityType": StakeAuthorize::Withdrawer, + "custodian": keys[1].to_string(), }), } ); @@ -350,7 +400,8 @@ mod test { seed.to_string(), &keys[2], &keys[3], - authority_type, + StakeAuthorize::Staker, + None, ); let message = Message::new(&[instruction], None); assert_eq!( @@ -363,7 +414,36 @@ mod test { "newAuthorized": keys[3].to_string(), "authorityBase": keys[0].to_string(), "authoritySeed": seed, - "authorityType": authority_type, + "authorityType": StakeAuthorize::Staker, + "clockSysvar": keys[2].to_string(), + }), + } + ); + assert!(parse_stake(&message.instructions[0], &keys[0..1]).is_err()); + + let instruction = stake_instruction::authorize_with_seed( + &keys[1], + &keys[0], + seed.to_string(), + &keys[2], + &keys[3], + StakeAuthorize::Withdrawer, + Some(&keys[4]), + ); + let message = Message::new(&[instruction], None); + assert_eq!( + parse_stake(&message.instructions[0], &keys[0..5]).unwrap(), + ParsedInstructionEnum { + instruction_type: "authorizeWithSeed".to_string(), + info: json!({ + "stakeAccount": keys[2].to_string(), + "authorityOwner": keys[2].to_string(), + "newAuthorized": keys[3].to_string(), + "authorityBase": keys[0].to_string(), + "authoritySeed": seed, + "authorityType": StakeAuthorize::Withdrawer, + "clockSysvar": keys[3].to_string(), + "custodian": keys[1].to_string(), }), } ); @@ -375,11 +455,11 @@ mod test { fn test_parse_set_lockup() { let mut keys: Vec = vec![]; for _ in 0..2 { - keys.push(solana_sdk::pubkey::new_rand()); + keys.push(Pubkey::new_unique()); } let unix_timestamp = 1_234_567_890; let epoch = 11; - let custodian = solana_sdk::pubkey::new_rand(); + let custodian = Pubkey::new_unique(); let lockup = LockupArgs { unix_timestamp: Some(unix_timestamp),