Add StakeInstruction::AuthorizeWithSeed (#11700)
* Add StakeInstruction::AuthorizeWithSeed * chore: add authorize-with-seed to web.js * fix: add address_owner * Add SystemInstruction::TransferWithSeed * Update ABI hash * chore: better variable names * Add AuthorizeWithSeedArgs * Reorder and rename arguments for clarity
This commit is contained in:
parent
247f27af37
commit
f02a78d8ff
|
@ -131,6 +131,13 @@ pub enum StakeInstruction {
|
||||||
/// 3. [] Stake history sysvar that carries stake warmup/cooldown history
|
/// 3. [] Stake history sysvar that carries stake warmup/cooldown history
|
||||||
/// 4. [SIGNER] Stake authority
|
/// 4. [SIGNER] Stake authority
|
||||||
Merge,
|
Merge,
|
||||||
|
|
||||||
|
/// Authorize a key to manage stake or withdrawal with a derived key
|
||||||
|
///
|
||||||
|
/// # Account references
|
||||||
|
/// 0. [WRITE] Stake account to be updated
|
||||||
|
/// 1. [SIGNER] Base key of stake or withdraw authority
|
||||||
|
AuthorizeWithSeed(AuthorizeWithSeedArgs),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Clone, Copy)]
|
||||||
|
@ -140,6 +147,14 @@ pub struct LockupArgs {
|
||||||
pub custodian: Option<Pubkey>,
|
pub custodian: Option<Pubkey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||||
|
pub struct AuthorizeWithSeedArgs {
|
||||||
|
pub new_authorized_pubkey: Pubkey,
|
||||||
|
pub stake_authorize: StakeAuthorize,
|
||||||
|
pub authority_seed: String,
|
||||||
|
pub authority_owner: Pubkey,
|
||||||
|
}
|
||||||
|
|
||||||
fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction {
|
fn initialize(stake_pubkey: &Pubkey, authorized: &Authorized, lockup: &Lockup) -> Instruction {
|
||||||
Instruction::new(
|
Instruction::new(
|
||||||
id(),
|
id(),
|
||||||
|
@ -341,6 +356,33 @@ pub fn authorize(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn authorize_with_seed(
|
||||||
|
stake_pubkey: &Pubkey,
|
||||||
|
authority_base: &Pubkey,
|
||||||
|
authority_seed: String,
|
||||||
|
authority_owner: &Pubkey,
|
||||||
|
new_authorized_pubkey: &Pubkey,
|
||||||
|
stake_authorize: StakeAuthorize,
|
||||||
|
) -> Instruction {
|
||||||
|
let account_metas = vec![
|
||||||
|
AccountMeta::new(*stake_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*authority_base, true),
|
||||||
|
];
|
||||||
|
|
||||||
|
let args = AuthorizeWithSeedArgs {
|
||||||
|
new_authorized_pubkey: *new_authorized_pubkey,
|
||||||
|
stake_authorize,
|
||||||
|
authority_seed,
|
||||||
|
authority_owner: *authority_owner,
|
||||||
|
};
|
||||||
|
|
||||||
|
Instruction::new(
|
||||||
|
id(),
|
||||||
|
&StakeInstruction::AuthorizeWithSeed(args),
|
||||||
|
account_metas,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn delegate_stake(
|
pub fn delegate_stake(
|
||||||
stake_pubkey: &Pubkey,
|
stake_pubkey: &Pubkey,
|
||||||
authorized_pubkey: &Pubkey,
|
authorized_pubkey: &Pubkey,
|
||||||
|
@ -420,7 +462,17 @@ pub fn process_instruction(
|
||||||
&Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?,
|
&Rent::from_keyed_account(next_keyed_account(keyed_accounts)?)?,
|
||||||
),
|
),
|
||||||
StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => {
|
StakeInstruction::Authorize(authorized_pubkey, stake_authorize) => {
|
||||||
me.authorize(&authorized_pubkey, stake_authorize, &signers)
|
me.authorize(&signers, &authorized_pubkey, stake_authorize)
|
||||||
|
}
|
||||||
|
StakeInstruction::AuthorizeWithSeed(args) => {
|
||||||
|
let authority_base = next_keyed_account(keyed_accounts)?;
|
||||||
|
me.authorize_with_seed(
|
||||||
|
&authority_base,
|
||||||
|
&args.authority_seed,
|
||||||
|
&args.authority_owner,
|
||||||
|
&args.new_authorized_pubkey,
|
||||||
|
args.stake_authorize,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
StakeInstruction::DelegateStake => {
|
StakeInstruction::DelegateStake => {
|
||||||
let vote = next_keyed_account(keyed_accounts)?;
|
let vote = next_keyed_account(keyed_accounts)?;
|
||||||
|
|
|
@ -556,9 +556,17 @@ pub trait StakeAccount {
|
||||||
) -> Result<(), InstructionError>;
|
) -> Result<(), InstructionError>;
|
||||||
fn authorize(
|
fn authorize(
|
||||||
&self,
|
&self,
|
||||||
authority: &Pubkey,
|
|
||||||
stake_authorize: StakeAuthorize,
|
|
||||||
signers: &HashSet<Pubkey>,
|
signers: &HashSet<Pubkey>,
|
||||||
|
new_authority: &Pubkey,
|
||||||
|
stake_authorize: StakeAuthorize,
|
||||||
|
) -> Result<(), InstructionError>;
|
||||||
|
fn authorize_with_seed(
|
||||||
|
&self,
|
||||||
|
authority_base: &KeyedAccount,
|
||||||
|
authority_seed: &str,
|
||||||
|
authority_owner: &Pubkey,
|
||||||
|
new_authority: &Pubkey,
|
||||||
|
stake_authorize: StakeAuthorize,
|
||||||
) -> Result<(), InstructionError>;
|
) -> Result<(), InstructionError>;
|
||||||
fn delegate(
|
fn delegate(
|
||||||
&self,
|
&self,
|
||||||
|
@ -627,24 +635,42 @@ impl<'a> StakeAccount for KeyedAccount<'a> {
|
||||||
/// staker. The default staker is the owner of the stake account's pubkey.
|
/// staker. The default staker is the owner of the stake account's pubkey.
|
||||||
fn authorize(
|
fn authorize(
|
||||||
&self,
|
&self,
|
||||||
authority: &Pubkey,
|
|
||||||
stake_authorize: StakeAuthorize,
|
|
||||||
signers: &HashSet<Pubkey>,
|
signers: &HashSet<Pubkey>,
|
||||||
|
new_authority: &Pubkey,
|
||||||
|
stake_authorize: StakeAuthorize,
|
||||||
) -> Result<(), InstructionError> {
|
) -> Result<(), InstructionError> {
|
||||||
match self.state()? {
|
match self.state()? {
|
||||||
StakeState::Stake(mut meta, stake) => {
|
StakeState::Stake(mut meta, stake) => {
|
||||||
meta.authorized
|
meta.authorized
|
||||||
.authorize(signers, authority, stake_authorize)?;
|
.authorize(signers, new_authority, stake_authorize)?;
|
||||||
self.set_state(&StakeState::Stake(meta, stake))
|
self.set_state(&StakeState::Stake(meta, stake))
|
||||||
}
|
}
|
||||||
StakeState::Initialized(mut meta) => {
|
StakeState::Initialized(mut meta) => {
|
||||||
meta.authorized
|
meta.authorized
|
||||||
.authorize(signers, authority, stake_authorize)?;
|
.authorize(signers, new_authority, stake_authorize)?;
|
||||||
self.set_state(&StakeState::Initialized(meta))
|
self.set_state(&StakeState::Initialized(meta))
|
||||||
}
|
}
|
||||||
_ => Err(InstructionError::InvalidAccountData),
|
_ => Err(InstructionError::InvalidAccountData),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fn authorize_with_seed(
|
||||||
|
&self,
|
||||||
|
authority_base: &KeyedAccount,
|
||||||
|
authority_seed: &str,
|
||||||
|
authority_owner: &Pubkey,
|
||||||
|
new_authority: &Pubkey,
|
||||||
|
stake_authorize: StakeAuthorize,
|
||||||
|
) -> Result<(), InstructionError> {
|
||||||
|
let mut signers = HashSet::default();
|
||||||
|
if let Some(base_pubkey) = authority_base.signer_key() {
|
||||||
|
signers.insert(Pubkey::create_with_seed(
|
||||||
|
base_pubkey,
|
||||||
|
authority_seed,
|
||||||
|
authority_owner,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
self.authorize(&signers, &new_authority, stake_authorize)
|
||||||
|
}
|
||||||
fn delegate(
|
fn delegate(
|
||||||
&self,
|
&self,
|
||||||
vote_account: &KeyedAccount,
|
vote_account: &KeyedAccount,
|
||||||
|
@ -2560,7 +2586,7 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_authorize_uninit() {
|
fn test_authorize_uninit() {
|
||||||
let stake_pubkey = Pubkey::new_rand();
|
let new_authority = Pubkey::new_rand();
|
||||||
let stake_lamports = 42;
|
let stake_lamports = 42;
|
||||||
let stake_account = Account::new_ref_data_with_space(
|
let stake_account = Account::new_ref_data_with_space(
|
||||||
stake_lamports,
|
stake_lamports,
|
||||||
|
@ -2570,21 +2596,21 @@ mod tests {
|
||||||
)
|
)
|
||||||
.expect("stake_account");
|
.expect("stake_account");
|
||||||
|
|
||||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
let stake_keyed_account = KeyedAccount::new(&new_authority, true, &stake_account);
|
||||||
let signers = vec![stake_pubkey].into_iter().collect();
|
let signers = vec![new_authority].into_iter().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker),
|
||||||
Err(InstructionError::InvalidAccountData)
|
Err(InstructionError::InvalidAccountData)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_authorize_lockup() {
|
fn test_authorize_lockup() {
|
||||||
let stake_pubkey = Pubkey::new_rand();
|
let stake_authority = Pubkey::new_rand();
|
||||||
let stake_lamports = 42;
|
let stake_lamports = 42;
|
||||||
let stake_account = Account::new_ref_data_with_space(
|
let stake_account = Account::new_ref_data_with_space(
|
||||||
stake_lamports,
|
stake_lamports,
|
||||||
&StakeState::Initialized(Meta::auto(&stake_pubkey)),
|
&StakeState::Initialized(Meta::auto(&stake_authority)),
|
||||||
std::mem::size_of::<StakeState>(),
|
std::mem::size_of::<StakeState>(),
|
||||||
&id(),
|
&id(),
|
||||||
)
|
)
|
||||||
|
@ -2595,16 +2621,16 @@ mod tests {
|
||||||
let to_keyed_account = KeyedAccount::new(&to, false, &to_account);
|
let to_keyed_account = KeyedAccount::new(&to, false, &to_account);
|
||||||
|
|
||||||
let clock = Clock::default();
|
let clock = Clock::default();
|
||||||
let stake_keyed_account = KeyedAccount::new(&stake_pubkey, true, &stake_account);
|
let stake_keyed_account = KeyedAccount::new(&stake_authority, true, &stake_account);
|
||||||
|
|
||||||
let stake_pubkey0 = Pubkey::new_rand();
|
let stake_pubkey0 = Pubkey::new_rand();
|
||||||
let signers = vec![stake_pubkey].into_iter().collect();
|
let signers = vec![stake_authority].into_iter().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Staker),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey0, StakeAuthorize::Withdrawer, &signers),
|
stake_keyed_account.authorize(&signers, &stake_pubkey0, StakeAuthorize::Withdrawer),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
if let StakeState::Initialized(Meta { authorized, .. }) =
|
if let StakeState::Initialized(Meta { authorized, .. }) =
|
||||||
|
@ -2619,7 +2645,7 @@ mod tests {
|
||||||
// A second authorization signed by the stake_keyed_account should fail
|
// A second authorization signed by the stake_keyed_account should fail
|
||||||
let stake_pubkey1 = Pubkey::new_rand();
|
let stake_pubkey1 = Pubkey::new_rand();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey1, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &stake_pubkey1, StakeAuthorize::Staker),
|
||||||
Err(InstructionError::MissingRequiredSignature)
|
Err(InstructionError::MissingRequiredSignature)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2628,7 +2654,7 @@ mod tests {
|
||||||
// Test a second authorization by the newly authorized pubkey
|
// Test a second authorization by the newly authorized pubkey
|
||||||
let stake_pubkey2 = Pubkey::new_rand();
|
let stake_pubkey2 = Pubkey::new_rand();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Staker, &signers0),
|
stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Staker),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
if let StakeState::Initialized(Meta { authorized, .. }) =
|
if let StakeState::Initialized(Meta { authorized, .. }) =
|
||||||
|
@ -2638,7 +2664,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey2, StakeAuthorize::Withdrawer, &signers0),
|
stake_keyed_account.authorize(&signers0, &stake_pubkey2, StakeAuthorize::Withdrawer),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
if let StakeState::Initialized(Meta { authorized, .. }) =
|
if let StakeState::Initialized(Meta { authorized, .. }) =
|
||||||
|
@ -2677,6 +2703,88 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_authorize_with_seed() {
|
||||||
|
let base_pubkey = Pubkey::new_rand();
|
||||||
|
let seed = "42";
|
||||||
|
let withdrawer_pubkey = Pubkey::create_with_seed(&base_pubkey, &seed, &id()).unwrap();
|
||||||
|
let stake_lamports = 42;
|
||||||
|
let stake_account = Account::new_ref_data_with_space(
|
||||||
|
stake_lamports,
|
||||||
|
&StakeState::Initialized(Meta::auto(&withdrawer_pubkey)),
|
||||||
|
std::mem::size_of::<StakeState>(),
|
||||||
|
&id(),
|
||||||
|
)
|
||||||
|
.expect("stake_account");
|
||||||
|
|
||||||
|
let base_account = Account::new_ref(1, 0, &id());
|
||||||
|
let base_keyed_account = KeyedAccount::new(&base_pubkey, true, &base_account);
|
||||||
|
|
||||||
|
let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account);
|
||||||
|
|
||||||
|
let new_authority = Pubkey::new_rand();
|
||||||
|
|
||||||
|
// Wrong seed
|
||||||
|
assert_eq!(
|
||||||
|
stake_keyed_account.authorize_with_seed(
|
||||||
|
&base_keyed_account,
|
||||||
|
&"",
|
||||||
|
&id(),
|
||||||
|
&new_authority,
|
||||||
|
StakeAuthorize::Staker,
|
||||||
|
),
|
||||||
|
Err(InstructionError::MissingRequiredSignature)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Wrong base
|
||||||
|
assert_eq!(
|
||||||
|
stake_keyed_account.authorize_with_seed(
|
||||||
|
&stake_keyed_account,
|
||||||
|
&seed,
|
||||||
|
&id(),
|
||||||
|
&new_authority,
|
||||||
|
StakeAuthorize::Staker,
|
||||||
|
),
|
||||||
|
Err(InstructionError::MissingRequiredSignature)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set stake authority
|
||||||
|
assert_eq!(
|
||||||
|
stake_keyed_account.authorize_with_seed(
|
||||||
|
&base_keyed_account,
|
||||||
|
&seed,
|
||||||
|
&id(),
|
||||||
|
&new_authority,
|
||||||
|
StakeAuthorize::Staker,
|
||||||
|
),
|
||||||
|
Ok(())
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set withdraw authority
|
||||||
|
assert_eq!(
|
||||||
|
stake_keyed_account.authorize_with_seed(
|
||||||
|
&base_keyed_account,
|
||||||
|
&seed,
|
||||||
|
&id(),
|
||||||
|
&new_authority,
|
||||||
|
StakeAuthorize::Withdrawer,
|
||||||
|
),
|
||||||
|
Ok(())
|
||||||
|
);
|
||||||
|
|
||||||
|
// No longer withdraw authority
|
||||||
|
assert_eq!(
|
||||||
|
stake_keyed_account.authorize_with_seed(
|
||||||
|
&stake_keyed_account,
|
||||||
|
&seed,
|
||||||
|
&id(),
|
||||||
|
&new_authority,
|
||||||
|
StakeAuthorize::Withdrawer,
|
||||||
|
),
|
||||||
|
Err(InstructionError::MissingRequiredSignature)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_authorize_override() {
|
fn test_authorize_override() {
|
||||||
let withdrawer_pubkey = Pubkey::new_rand();
|
let withdrawer_pubkey = Pubkey::new_rand();
|
||||||
|
@ -2692,39 +2800,39 @@ mod tests {
|
||||||
let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account);
|
let stake_keyed_account = KeyedAccount::new(&withdrawer_pubkey, true, &stake_account);
|
||||||
|
|
||||||
// Authorize a staker pubkey and move the withdrawer key into cold storage.
|
// Authorize a staker pubkey and move the withdrawer key into cold storage.
|
||||||
let stake_pubkey = Pubkey::new_rand();
|
let new_authority = Pubkey::new_rand();
|
||||||
let signers = vec![withdrawer_pubkey].into_iter().collect();
|
let signers = vec![withdrawer_pubkey].into_iter().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&stake_pubkey, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &new_authority, StakeAuthorize::Staker),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attack! The stake key (a hot key) is stolen and used to authorize a new staker.
|
// Attack! The stake key (a hot key) is stolen and used to authorize a new staker.
|
||||||
let mallory_pubkey = Pubkey::new_rand();
|
let mallory_pubkey = Pubkey::new_rand();
|
||||||
let signers = vec![stake_pubkey].into_iter().collect();
|
let signers = vec![new_authority].into_iter().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&mallory_pubkey, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Staker),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the original staker no longer has access.
|
// Verify the original staker no longer has access.
|
||||||
let new_stake_pubkey = Pubkey::new_rand();
|
let new_stake_pubkey = Pubkey::new_rand();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&new_stake_pubkey, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Staker),
|
||||||
Err(InstructionError::MissingRequiredSignature)
|
Err(InstructionError::MissingRequiredSignature)
|
||||||
);
|
);
|
||||||
|
|
||||||
// Verify the withdrawer (pulled from cold storage) can save the day.
|
// Verify the withdrawer (pulled from cold storage) can save the day.
|
||||||
let signers = vec![withdrawer_pubkey].into_iter().collect();
|
let signers = vec![withdrawer_pubkey].into_iter().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&new_stake_pubkey, StakeAuthorize::Withdrawer, &signers),
|
stake_keyed_account.authorize(&signers, &new_stake_pubkey, StakeAuthorize::Withdrawer),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
|
|
||||||
// Attack! Verify the staker cannot be used to authorize a withdraw.
|
// Attack! Verify the staker cannot be used to authorize a withdraw.
|
||||||
let signers = vec![new_stake_pubkey].into_iter().collect();
|
let signers = vec![new_stake_pubkey].into_iter().collect();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&mallory_pubkey, StakeAuthorize::Withdrawer, &signers),
|
stake_keyed_account.authorize(&signers, &mallory_pubkey, StakeAuthorize::Withdrawer),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -3517,7 +3625,7 @@ mod tests {
|
||||||
|
|
||||||
let new_staker_pubkey = Pubkey::new_rand();
|
let new_staker_pubkey = Pubkey::new_rand();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
stake_keyed_account.authorize(&new_staker_pubkey, StakeAuthorize::Staker, &signers),
|
stake_keyed_account.authorize(&signers, &new_staker_pubkey, StakeAuthorize::Staker),
|
||||||
Ok(())
|
Ok(())
|
||||||
);
|
);
|
||||||
let authorized =
|
let authorized =
|
||||||
|
|
|
@ -145,16 +145,11 @@ fn create_account(
|
||||||
transfer(from, to, lamports)
|
transfer(from, to, lamports)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn transfer(from: &KeyedAccount, to: &KeyedAccount, lamports: u64) -> Result<(), InstructionError> {
|
fn transfer_verified(
|
||||||
if lamports == 0 {
|
from: &KeyedAccount,
|
||||||
return Ok(());
|
to: &KeyedAccount,
|
||||||
}
|
lamports: u64,
|
||||||
|
) -> Result<(), InstructionError> {
|
||||||
if from.signer_key().is_none() {
|
|
||||||
debug!("Transfer: from must sign");
|
|
||||||
return Err(InstructionError::MissingRequiredSignature);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !from.data_is_empty()? {
|
if !from.data_is_empty()? {
|
||||||
debug!("Transfer: `from` must not carry data");
|
debug!("Transfer: `from` must not carry data");
|
||||||
return Err(InstructionError::InvalidArgument);
|
return Err(InstructionError::InvalidArgument);
|
||||||
|
@ -173,6 +168,45 @@ fn transfer(from: &KeyedAccount, to: &KeyedAccount, lamports: u64) -> Result<(),
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn transfer(from: &KeyedAccount, to: &KeyedAccount, lamports: u64) -> Result<(), InstructionError> {
|
||||||
|
if lamports == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if from.signer_key().is_none() {
|
||||||
|
debug!("Transfer: from must sign");
|
||||||
|
return Err(InstructionError::MissingRequiredSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer_verified(from, to, lamports)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transfer_with_seed(
|
||||||
|
from: &KeyedAccount,
|
||||||
|
from_base: &KeyedAccount,
|
||||||
|
from_seed: &str,
|
||||||
|
from_owner: &Pubkey,
|
||||||
|
to: &KeyedAccount,
|
||||||
|
lamports: u64,
|
||||||
|
) -> Result<(), InstructionError> {
|
||||||
|
if lamports == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if from_base.signer_key().is_none() {
|
||||||
|
debug!("Transfer: from must sign");
|
||||||
|
return Err(InstructionError::MissingRequiredSignature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if *from.unsigned_key()
|
||||||
|
!= Pubkey::create_with_seed(from_base.unsigned_key(), from_seed, from_owner)?
|
||||||
|
{
|
||||||
|
return Err(SystemError::AddressWithSeedMismatch.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
transfer_verified(from, to, lamports)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process_instruction(
|
pub fn process_instruction(
|
||||||
_owner: &Pubkey,
|
_owner: &Pubkey,
|
||||||
keyed_accounts: &[KeyedAccount],
|
keyed_accounts: &[KeyedAccount],
|
||||||
|
@ -220,6 +254,16 @@ pub fn process_instruction(
|
||||||
let to = next_keyed_account(keyed_accounts_iter)?;
|
let to = next_keyed_account(keyed_accounts_iter)?;
|
||||||
transfer(from, to, lamports)
|
transfer(from, to, lamports)
|
||||||
}
|
}
|
||||||
|
SystemInstruction::TransferWithSeed {
|
||||||
|
lamports,
|
||||||
|
from_seed,
|
||||||
|
from_owner,
|
||||||
|
} => {
|
||||||
|
let from = next_keyed_account(keyed_accounts_iter)?;
|
||||||
|
let base = next_keyed_account(keyed_accounts_iter)?;
|
||||||
|
let to = next_keyed_account(keyed_accounts_iter)?;
|
||||||
|
transfer_with_seed(from, base, &from_seed, &from_owner, to, lamports)
|
||||||
|
}
|
||||||
SystemInstruction::AdvanceNonceAccount => {
|
SystemInstruction::AdvanceNonceAccount => {
|
||||||
let me = &mut next_keyed_account(keyed_accounts_iter)?;
|
let me = &mut next_keyed_account(keyed_accounts_iter)?;
|
||||||
me.advance_nonce_account(
|
me.advance_nonce_account(
|
||||||
|
@ -864,6 +908,62 @@ mod tests {
|
||||||
assert_eq!(to_keyed_account.account.borrow().lamports, 51);
|
assert_eq!(to_keyed_account.account.borrow().lamports, 51);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_transfer_with_seed() {
|
||||||
|
let base = Pubkey::new_rand();
|
||||||
|
let base_account = Account::new_ref(100, 0, &Pubkey::new(&[2; 32])); // account owner should not matter
|
||||||
|
let from_base_keyed_account = KeyedAccount::new(&base, true, &base_account);
|
||||||
|
let from_seed = "42";
|
||||||
|
let from_owner = system_program::id();
|
||||||
|
let from = Pubkey::create_with_seed(&base, from_seed, &from_owner).unwrap();
|
||||||
|
let from_account = Account::new_ref(100, 0, &Pubkey::new(&[2; 32])); // account owner should not matter
|
||||||
|
let to = Pubkey::new(&[3; 32]);
|
||||||
|
let to_account = Account::new_ref(1, 0, &to); // account owner should not matter
|
||||||
|
let from_keyed_account = KeyedAccount::new(&from, true, &from_account);
|
||||||
|
let to_keyed_account = KeyedAccount::new(&to, false, &to_account);
|
||||||
|
transfer_with_seed(
|
||||||
|
&from_keyed_account,
|
||||||
|
&from_base_keyed_account,
|
||||||
|
&from_seed,
|
||||||
|
&from_owner,
|
||||||
|
&to_keyed_account,
|
||||||
|
50,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let from_lamports = from_keyed_account.account.borrow().lamports;
|
||||||
|
let to_lamports = to_keyed_account.account.borrow().lamports;
|
||||||
|
assert_eq!(from_lamports, 50);
|
||||||
|
assert_eq!(to_lamports, 51);
|
||||||
|
|
||||||
|
// Attempt to move more lamports than remaining in from_account
|
||||||
|
let from_keyed_account = KeyedAccount::new(&from, true, &from_account);
|
||||||
|
let result = transfer_with_seed(
|
||||||
|
&from_keyed_account,
|
||||||
|
&from_base_keyed_account,
|
||||||
|
&from_seed,
|
||||||
|
&from_owner,
|
||||||
|
&to_keyed_account,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
assert_eq!(result, Err(SystemError::ResultWithNegativeLamports.into()));
|
||||||
|
assert_eq!(from_keyed_account.account.borrow().lamports, 50);
|
||||||
|
assert_eq!(to_keyed_account.account.borrow().lamports, 51);
|
||||||
|
|
||||||
|
// test unsigned transfer of zero
|
||||||
|
let from_keyed_account = KeyedAccount::new(&from, false, &from_account);
|
||||||
|
assert!(transfer_with_seed(
|
||||||
|
&from_keyed_account,
|
||||||
|
&from_base_keyed_account,
|
||||||
|
&from_seed,
|
||||||
|
&from_owner,
|
||||||
|
&to_keyed_account,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
.is_ok(),);
|
||||||
|
assert_eq!(from_keyed_account.account.borrow().lamports, 50);
|
||||||
|
assert_eq!(to_keyed_account.account.borrow().lamports, 51);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_transfer_lamports_from_nonce_account_fail() {
|
fn test_transfer_lamports_from_nonce_account_fail() {
|
||||||
let from = Pubkey::new_rand();
|
let from = Pubkey::new_rand();
|
||||||
|
|
|
@ -52,7 +52,7 @@ impl<E> DecodeError<E> for NonceError {
|
||||||
/// maximum permitted size of data: 10 MB
|
/// maximum permitted size of data: 10 MB
|
||||||
pub const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024;
|
pub const MAX_PERMITTED_DATA_LENGTH: u64 = 10 * 1024 * 1024;
|
||||||
|
|
||||||
#[frozen_abi(digest = "E343asdJPd3aEbHSsmeeGzvztc9X2maaHhWxVS4P6hvW")]
|
#[frozen_abi(digest = "EpsptsKTYzMoQGSdoWRfPbwT3odGNfK3imEUTrxpLF1i")]
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, AbiExample, AbiEnumVisitor)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, AbiExample, AbiEnumVisitor)]
|
||||||
pub enum SystemInstruction {
|
pub enum SystemInstruction {
|
||||||
/// Create a new account
|
/// Create a new account
|
||||||
|
@ -197,6 +197,23 @@ pub enum SystemInstruction {
|
||||||
/// Owner program account
|
/// Owner program account
|
||||||
owner: Pubkey,
|
owner: Pubkey,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Transfer lamports from a derived address
|
||||||
|
///
|
||||||
|
/// # Account references
|
||||||
|
/// 0. [WRITE] Funding account
|
||||||
|
/// 1. [SIGNER] Base for funding account
|
||||||
|
/// 2. [WRITE] Recipient account
|
||||||
|
TransferWithSeed {
|
||||||
|
/// Amount to transfer
|
||||||
|
lamports: u64,
|
||||||
|
|
||||||
|
/// Seed to use to derive the funding account address
|
||||||
|
from_seed: String,
|
||||||
|
|
||||||
|
/// Owner to use to derive the funding account address
|
||||||
|
from_owner: Pubkey,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_account(
|
pub fn create_account(
|
||||||
|
@ -293,6 +310,30 @@ pub fn transfer(from_pubkey: &Pubkey, to_pubkey: &Pubkey, lamports: u64) -> Inst
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn transfer_with_seed(
|
||||||
|
from_pubkey: &Pubkey, // must match create_address_with_seed(base, seed, owner)
|
||||||
|
from_base: &Pubkey,
|
||||||
|
from_seed: String,
|
||||||
|
from_owner: &Pubkey,
|
||||||
|
to_pubkey: &Pubkey,
|
||||||
|
lamports: u64,
|
||||||
|
) -> Instruction {
|
||||||
|
let account_metas = vec![
|
||||||
|
AccountMeta::new(*from_pubkey, false),
|
||||||
|
AccountMeta::new_readonly(*from_base, true),
|
||||||
|
AccountMeta::new(*to_pubkey, false),
|
||||||
|
];
|
||||||
|
Instruction::new(
|
||||||
|
system_program::id(),
|
||||||
|
&SystemInstruction::TransferWithSeed {
|
||||||
|
lamports,
|
||||||
|
from_seed,
|
||||||
|
from_owner: *from_owner,
|
||||||
|
},
|
||||||
|
account_metas,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction {
|
pub fn allocate(pubkey: &Pubkey, space: u64) -> Instruction {
|
||||||
let account_metas = vec![AccountMeta::new(*pubkey, true)];
|
let account_metas = vec![AccountMeta::new(*pubkey, true)];
|
||||||
Instruction::new(
|
Instruction::new(
|
||||||
|
|
|
@ -133,6 +133,7 @@ fn process_transaction(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
StakeInstruction::Authorize(_, _)
|
StakeInstruction::Authorize(_, _)
|
||||||
|
| StakeInstruction::AuthorizeWithSeed(_)
|
||||||
| StakeInstruction::DelegateStake
|
| StakeInstruction::DelegateStake
|
||||||
| StakeInstruction::Deactivate => {
|
| StakeInstruction::Deactivate => {
|
||||||
// These instructions are always permitted
|
// These instructions are always permitted
|
||||||
|
|
|
@ -689,6 +689,15 @@ declare module '@solana/web3.js' {
|
||||||
stakeAuthorizationType: StakeAuthorizationType;
|
stakeAuthorizationType: StakeAuthorizationType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthorizeWithSeedStakeParams = {
|
||||||
|
stakePubkey: PublicKey;
|
||||||
|
authorityBase: PublicKey;
|
||||||
|
authoritySeed: string;
|
||||||
|
authorityOwner: PublicKey;
|
||||||
|
newAuthorizedPubkey: PublicKey;
|
||||||
|
stakeAuthorizationType: StakeAuthorizationType;
|
||||||
|
};
|
||||||
|
|
||||||
export type SplitStakeParams = {
|
export type SplitStakeParams = {
|
||||||
stakePubkey: PublicKey;
|
stakePubkey: PublicKey;
|
||||||
authorizedPubkey: PublicKey;
|
authorizedPubkey: PublicKey;
|
||||||
|
@ -725,6 +734,7 @@ declare module '@solana/web3.js' {
|
||||||
export type StakeInstructionType =
|
export type StakeInstructionType =
|
||||||
| 'Initialize'
|
| 'Initialize'
|
||||||
| 'Authorize'
|
| 'Authorize'
|
||||||
|
| 'AuthorizeWithSeed'
|
||||||
| 'Delegate'
|
| 'Delegate'
|
||||||
| 'Split'
|
| 'Split'
|
||||||
| 'Withdraw'
|
| 'Withdraw'
|
||||||
|
|
|
@ -704,6 +704,15 @@ declare module '@solana/web3.js' {
|
||||||
stakeAuthorizationType: StakeAuthorizationType,
|
stakeAuthorizationType: StakeAuthorizationType,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
declare export type AuthorizeWithSeedStakeParams = {|
|
||||||
|
stakePubkey: PublicKey,
|
||||||
|
authorityBase: PublicKey,
|
||||||
|
authoritySeed: string,
|
||||||
|
authorityOwner: PublicKey;
|
||||||
|
newAuthorizedPubkey: PublicKey,
|
||||||
|
stakeAuthorizationType: StakeAuthorizationType,
|
||||||
|
|};
|
||||||
|
|
||||||
declare export type SplitStakeParams = {|
|
declare export type SplitStakeParams = {|
|
||||||
stakePubkey: PublicKey,
|
stakePubkey: PublicKey,
|
||||||
authorizedPubkey: PublicKey,
|
authorizedPubkey: PublicKey,
|
||||||
|
@ -740,6 +749,7 @@ declare module '@solana/web3.js' {
|
||||||
declare export type StakeInstructionType =
|
declare export type StakeInstructionType =
|
||||||
| 'Initialize'
|
| 'Initialize'
|
||||||
| 'Authorize'
|
| 'Authorize'
|
||||||
|
| 'AuthorizeWithSeed'
|
||||||
| 'Delegate'
|
| 'Delegate'
|
||||||
| 'Split'
|
| 'Split'
|
||||||
| 'Withdraw'
|
| 'Withdraw'
|
||||||
|
|
|
@ -124,6 +124,25 @@ export type AuthorizeStakeParams = {|
|
||||||
stakeAuthorizationType: StakeAuthorizationType,
|
stakeAuthorizationType: StakeAuthorizationType,
|
||||||
|};
|
|};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorize stake instruction params using a derived key
|
||||||
|
* @typedef {Object} AuthorizeWithSeedStakeParams
|
||||||
|
* @property {PublicKey} stakePubkey
|
||||||
|
* @property {PublicKey} authorityBase
|
||||||
|
* @property {string} authoritySeed
|
||||||
|
* @property {PublicKey} authorityOwner
|
||||||
|
* @property {PublicKey} newAuthorizedPubkey
|
||||||
|
* @property {StakeAuthorizationType} stakeAuthorizationType
|
||||||
|
*/
|
||||||
|
export type AuthorizeWithSeedStakeParams = {|
|
||||||
|
stakePubkey: PublicKey,
|
||||||
|
authorityBase: PublicKey,
|
||||||
|
authoritySeed: string,
|
||||||
|
authorityOwner: PublicKey,
|
||||||
|
newAuthorizedPubkey: PublicKey,
|
||||||
|
stakeAuthorizationType: StakeAuthorizationType,
|
||||||
|
|};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Split stake instruction params
|
* Split stake instruction params
|
||||||
* @typedef {Object} SplitStakeParams
|
* @typedef {Object} SplitStakeParams
|
||||||
|
@ -262,6 +281,31 @@ export class StakeInstruction {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode an authorize-with-seed stake instruction and retrieve the instruction params.
|
||||||
|
*/
|
||||||
|
static decodeAuthorizeWithSeed(
|
||||||
|
instruction: TransactionInstruction,
|
||||||
|
): AuthorizeWithSeedStakeParams {
|
||||||
|
this.checkProgramId(instruction.programId);
|
||||||
|
this.checkKeyLength(instruction.keys, 2);
|
||||||
|
const {newAuthorized, stakeAuthorizationType, authoritySeed, authorityOwner} = decodeData(
|
||||||
|
STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed,
|
||||||
|
instruction.data,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
stakePubkey: instruction.keys[0].pubkey,
|
||||||
|
authorityBase: instruction.keys[1].pubkey,
|
||||||
|
authoritySeed: authoritySeed,
|
||||||
|
authorityOwner: new PublicKey(authorityOwner),
|
||||||
|
newAuthorizedPubkey: new PublicKey(newAuthorized),
|
||||||
|
stakeAuthorizationType: {
|
||||||
|
index: stakeAuthorizationType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode a split stake instruction and retrieve the instruction params.
|
* Decode a split stake instruction and retrieve the instruction params.
|
||||||
*/
|
*/
|
||||||
|
@ -341,7 +385,7 @@ export class StakeInstruction {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An enumeration of valid StakeInstructionType's
|
* An enumeration of valid StakeInstructionType's
|
||||||
* @typedef { 'Initialize' | 'Authorize' | 'Delegate' | 'Split' | 'Withdraw'
|
* @typedef { 'Initialize' | 'Authorize' | 'AuthorizeWithSeed' | 'Delegate' | 'Split' | 'Withdraw'
|
||||||
| 'Deactivate' } StakeInstructionType
|
| 'Deactivate' } StakeInstructionType
|
||||||
*/
|
*/
|
||||||
export type StakeInstructionType = $Keys<typeof STAKE_INSTRUCTION_LAYOUTS>;
|
export type StakeInstructionType = $Keys<typeof STAKE_INSTRUCTION_LAYOUTS>;
|
||||||
|
@ -388,6 +432,16 @@ export const STAKE_INSTRUCTION_LAYOUTS = Object.freeze({
|
||||||
index: 5,
|
index: 5,
|
||||||
layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
|
layout: BufferLayout.struct([BufferLayout.u32('instruction')]),
|
||||||
},
|
},
|
||||||
|
AuthorizeWithSeed: {
|
||||||
|
index: 8,
|
||||||
|
layout: BufferLayout.struct([
|
||||||
|
BufferLayout.u32('instruction'),
|
||||||
|
Layout.publicKey('newAuthorized'),
|
||||||
|
BufferLayout.u32('stakeAuthorizationType'),
|
||||||
|
Layout.rustString('authoritySeed'),
|
||||||
|
Layout.publicKey('authorityOwner'),
|
||||||
|
]),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -551,6 +605,38 @@ export class StakeProgram {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a Transaction that authorizes a new PublicKey as Staker
|
||||||
|
* or Withdrawer on the Stake account.
|
||||||
|
*/
|
||||||
|
static authorizeWithSeed(params: AuthorizeWithSeedStakeParams): Transaction {
|
||||||
|
const {
|
||||||
|
stakePubkey,
|
||||||
|
authorityBase,
|
||||||
|
authoritySeed,
|
||||||
|
authorityOwner,
|
||||||
|
newAuthorizedPubkey,
|
||||||
|
stakeAuthorizationType,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const type = STAKE_INSTRUCTION_LAYOUTS.AuthorizeWithSeed;
|
||||||
|
const data = encodeData(type, {
|
||||||
|
newAuthorized: newAuthorizedPubkey.toBuffer(),
|
||||||
|
stakeAuthorizationType: stakeAuthorizationType.index,
|
||||||
|
authoritySeed: authoritySeed,
|
||||||
|
authorityOwner: authorityOwner.toBuffer(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Transaction().add({
|
||||||
|
keys: [
|
||||||
|
{pubkey: stakePubkey, isSigner: false, isWritable: true},
|
||||||
|
{pubkey: authorityBase, isSigner: true, isWritable: false},
|
||||||
|
],
|
||||||
|
programId: this.programId,
|
||||||
|
data,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a Transaction that splits Stake tokens into another stake account
|
* Generate a Transaction that splits Stake tokens into another stake account
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -128,6 +128,27 @@ test('authorize', () => {
|
||||||
expect(params).toEqual(StakeInstruction.decodeAuthorize(stakeInstruction));
|
expect(params).toEqual(StakeInstruction.decodeAuthorize(stakeInstruction));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('authorizeWithSeed', () => {
|
||||||
|
const stakePubkey = new Account().publicKey;
|
||||||
|
const authorityBase = new Account().publicKey;
|
||||||
|
const authoritySeed = 'test string';
|
||||||
|
const authorityOwner = new Account().publicKey;
|
||||||
|
const newAuthorizedPubkey = new Account().publicKey;
|
||||||
|
const stakeAuthorizationType = StakeAuthorizationLayout.Staker;
|
||||||
|
const params = {
|
||||||
|
stakePubkey,
|
||||||
|
authorityBase,
|
||||||
|
authoritySeed,
|
||||||
|
authorityOwner,
|
||||||
|
newAuthorizedPubkey,
|
||||||
|
stakeAuthorizationType,
|
||||||
|
};
|
||||||
|
const transaction = StakeProgram.authorizeWithSeed(params);
|
||||||
|
expect(transaction.instructions).toHaveLength(1);
|
||||||
|
const [stakeInstruction] = transaction.instructions;
|
||||||
|
expect(params).toEqual(StakeInstruction.decodeAuthorizeWithSeed(stakeInstruction));
|
||||||
|
});
|
||||||
|
|
||||||
test('split', () => {
|
test('split', () => {
|
||||||
const stakePubkey = new Account().publicKey;
|
const stakePubkey = new Account().publicKey;
|
||||||
const authorizedPubkey = new Account().publicKey;
|
const authorizedPubkey = new Account().publicKey;
|
||||||
|
|
Loading…
Reference in New Issue