Make active stake consistent in split (#33295)
* Add feature gate * Add helper fn * Require split destination to be rent-exempt if it is active * Update cli to prefund split accounts * cli: require rent param with sign-only * Update tokens to prefund split accounts * Update split tests with sysvar accounts * Fix test_split_to_account_with_rent_exempt_reserve * Fix test_staked_split_destination_minimum_balance * Fix test_split_more_than_staked * Fix test_split_minimum_stake_delegation and remove misleading StakeState::Initialized case * Fix test_split_from_larger_sized_account * Add test for pre-/post-activation behavior splitting some or all of stake account * Assert active stake * Fix runtime test * Ignore stake-pool downstream * Review comments * Feature gate sysvar reads
This commit is contained in:
parent
7a8a492d4c
commit
bca41edf20
|
@ -128,7 +128,7 @@ jobs:
|
||||||
- [governance/addin-mock/program, governance/program]
|
- [governance/addin-mock/program, governance/program]
|
||||||
- [memo/program]
|
- [memo/program]
|
||||||
- [name-service/program]
|
- [name-service/program]
|
||||||
- [stake-pool/program]
|
# - [stake-pool/program]
|
||||||
- [single-pool/program]
|
- [single-pool/program]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
|
@ -238,6 +238,7 @@ pub enum CliCommand {
|
||||||
lamports: u64,
|
lamports: u64,
|
||||||
fee_payer: SignerIndex,
|
fee_payer: SignerIndex,
|
||||||
compute_unit_price: Option<u64>,
|
compute_unit_price: Option<u64>,
|
||||||
|
rent_exempt_reserve: Option<u64>,
|
||||||
},
|
},
|
||||||
MergeStake {
|
MergeStake {
|
||||||
stake_account_pubkey: Pubkey,
|
stake_account_pubkey: Pubkey,
|
||||||
|
@ -1226,6 +1227,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||||
lamports,
|
lamports,
|
||||||
fee_payer,
|
fee_payer,
|
||||||
compute_unit_price,
|
compute_unit_price,
|
||||||
|
rent_exempt_reserve,
|
||||||
} => process_split_stake(
|
} => process_split_stake(
|
||||||
&rpc_client,
|
&rpc_client,
|
||||||
config,
|
config,
|
||||||
|
@ -1242,6 +1244,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||||
*lamports,
|
*lamports,
|
||||||
*fee_payer,
|
*fee_payer,
|
||||||
compute_unit_price.as_ref(),
|
compute_unit_price.as_ref(),
|
||||||
|
rent_exempt_reserve.as_ref(),
|
||||||
),
|
),
|
||||||
CliCommand::MergeStake {
|
CliCommand::MergeStake {
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
|
@ -2243,6 +2246,7 @@ mod tests {
|
||||||
lamports: 30,
|
lamports: 30,
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
compute_unit_price: None,
|
compute_unit_price: None,
|
||||||
|
rent_exempt_reserve: None,
|
||||||
};
|
};
|
||||||
config.signers = vec![&keypair, &split_stake_account];
|
config.signers = vec![&keypair, &split_stake_account];
|
||||||
let result = process_command(&config);
|
let result = process_command(&config);
|
||||||
|
|
|
@ -55,7 +55,7 @@ use {
|
||||||
tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent},
|
tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent},
|
||||||
},
|
},
|
||||||
stake_history::{Epoch, StakeHistory},
|
stake_history::{Epoch, StakeHistory},
|
||||||
system_instruction::SystemError,
|
system_instruction::{self, SystemError},
|
||||||
sysvar::{clock, stake_history},
|
sysvar::{clock, stake_history},
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
},
|
},
|
||||||
|
@ -121,6 +121,13 @@ pub struct StakeAuthorizationIndexed {
|
||||||
pub new_authority_signer: Option<SignerIndex>,
|
pub new_authority_signer: Option<SignerIndex>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct SignOnlySplitNeedsRent {}
|
||||||
|
impl ArgsConfig for SignOnlySplitNeedsRent {
|
||||||
|
fn sign_only_arg<'a, 'b>(&self, arg: Arg<'a, 'b>) -> Arg<'a, 'b> {
|
||||||
|
arg.requires("rent_exempt_reserve_sol")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait StakeSubCommands {
|
pub trait StakeSubCommands {
|
||||||
fn stake_subcommands(self) -> Self;
|
fn stake_subcommands(self) -> Self;
|
||||||
}
|
}
|
||||||
|
@ -493,11 +500,21 @@ impl StakeSubCommands for App<'_, '_> {
|
||||||
will be at a derived address of SPLIT_STAKE_ACCOUNT")
|
will be at a derived address of SPLIT_STAKE_ACCOUNT")
|
||||||
)
|
)
|
||||||
.arg(stake_authority_arg())
|
.arg(stake_authority_arg())
|
||||||
.offline_args()
|
.offline_args_config(&SignOnlySplitNeedsRent{})
|
||||||
.nonce_args(false)
|
.nonce_args(false)
|
||||||
.arg(fee_payer_arg())
|
.arg(fee_payer_arg())
|
||||||
.arg(memo_arg())
|
.arg(memo_arg())
|
||||||
.arg(compute_unit_price_arg())
|
.arg(compute_unit_price_arg())
|
||||||
|
.arg(
|
||||||
|
Arg::with_name("rent_exempt_reserve_sol")
|
||||||
|
.long("rent-exempt-reserve-sol")
|
||||||
|
.value_name("AMOUNT")
|
||||||
|
.takes_value(true)
|
||||||
|
.validator(is_amount)
|
||||||
|
.requires("sign_only")
|
||||||
|
.help("Offline signing only: the rent-exempt amount to move into the new \
|
||||||
|
stake account, in SOL")
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.subcommand(
|
.subcommand(
|
||||||
SubCommand::with_name("merge-stake")
|
SubCommand::with_name("merge-stake")
|
||||||
|
@ -1027,6 +1044,7 @@ pub fn parse_split_stake(
|
||||||
let signer_info =
|
let signer_info =
|
||||||
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
|
default_signer.generate_unique_signers(bulk_signers, matches, wallet_manager)?;
|
||||||
let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
|
let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
|
||||||
|
let rent_exempt_reserve = lamports_of_sol(matches, "rent_exempt_reserve_sol");
|
||||||
|
|
||||||
Ok(CliCommandInfo {
|
Ok(CliCommandInfo {
|
||||||
command: CliCommand::SplitStake {
|
command: CliCommand::SplitStake {
|
||||||
|
@ -1043,6 +1061,7 @@ pub fn parse_split_stake(
|
||||||
lamports,
|
lamports,
|
||||||
fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
|
fee_payer: signer_info.index_of(fee_payer_pubkey).unwrap(),
|
||||||
compute_unit_price,
|
compute_unit_price,
|
||||||
|
rent_exempt_reserve,
|
||||||
},
|
},
|
||||||
signers: signer_info.signers,
|
signers: signer_info.signers,
|
||||||
})
|
})
|
||||||
|
@ -1852,6 +1871,7 @@ pub fn process_split_stake(
|
||||||
lamports: u64,
|
lamports: u64,
|
||||||
fee_payer: SignerIndex,
|
fee_payer: SignerIndex,
|
||||||
compute_unit_price: Option<&u64>,
|
compute_unit_price: Option<&u64>,
|
||||||
|
rent_exempt_reserve: Option<&u64>,
|
||||||
) -> ProcessResult {
|
) -> ProcessResult {
|
||||||
let split_stake_account = config.signers[split_stake_account];
|
let split_stake_account = config.signers[split_stake_account];
|
||||||
let fee_payer = config.signers[fee_payer];
|
let fee_payer = config.signers[fee_payer];
|
||||||
|
@ -1885,7 +1905,7 @@ pub fn process_split_stake(
|
||||||
split_stake_account.pubkey()
|
split_stake_account.pubkey()
|
||||||
};
|
};
|
||||||
|
|
||||||
if !sign_only {
|
let rent_exempt_reserve = if !sign_only {
|
||||||
if let Ok(stake_account) = rpc_client.get_account(&split_stake_account_address) {
|
if let Ok(stake_account) = rpc_client.get_account(&split_stake_account_address) {
|
||||||
let err_msg = if stake_account.owner == stake::program::id() {
|
let err_msg = if stake_account.owner == stake::program::id() {
|
||||||
format!("Stake account {split_stake_account_address} already exists")
|
format!("Stake account {split_stake_account_address} already exists")
|
||||||
|
@ -1906,12 +1926,23 @@ pub fn process_split_stake(
|
||||||
))
|
))
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
}
|
minimum_balance
|
||||||
|
} else {
|
||||||
|
rent_exempt_reserve
|
||||||
|
.cloned()
|
||||||
|
.expect("rent_exempt_reserve_sol is required with sign_only")
|
||||||
|
};
|
||||||
|
|
||||||
let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;
|
let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;
|
||||||
|
|
||||||
let ixs = if let Some(seed) = split_stake_account_seed {
|
let mut ixs = vec![system_instruction::transfer(
|
||||||
stake_instruction::split_with_seed(
|
&fee_payer.pubkey(),
|
||||||
|
&split_stake_account_address,
|
||||||
|
rent_exempt_reserve,
|
||||||
|
)];
|
||||||
|
if let Some(seed) = split_stake_account_seed {
|
||||||
|
ixs.append(
|
||||||
|
&mut stake_instruction::split_with_seed(
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
&stake_authority.pubkey(),
|
&stake_authority.pubkey(),
|
||||||
lamports,
|
lamports,
|
||||||
|
@ -1920,16 +1951,19 @@ pub fn process_split_stake(
|
||||||
seed,
|
seed,
|
||||||
)
|
)
|
||||||
.with_memo(memo)
|
.with_memo(memo)
|
||||||
.with_compute_unit_price(compute_unit_price)
|
.with_compute_unit_price(compute_unit_price),
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
stake_instruction::split(
|
ixs.append(
|
||||||
|
&mut stake_instruction::split(
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
&stake_authority.pubkey(),
|
&stake_authority.pubkey(),
|
||||||
lamports,
|
lamports,
|
||||||
&split_stake_account_address,
|
&split_stake_account_address,
|
||||||
)
|
)
|
||||||
.with_memo(memo)
|
.with_memo(memo)
|
||||||
.with_compute_unit_price(compute_unit_price)
|
.with_compute_unit_price(compute_unit_price),
|
||||||
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let nonce_authority = config.signers[nonce_authority];
|
let nonce_authority = config.signers[nonce_authority];
|
||||||
|
@ -4848,6 +4882,7 @@ mod tests {
|
||||||
lamports: 50_000_000_000,
|
lamports: 50_000_000_000,
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
compute_unit_price: None,
|
compute_unit_price: None,
|
||||||
|
rent_exempt_reserve: None,
|
||||||
},
|
},
|
||||||
signers: vec![
|
signers: vec![
|
||||||
read_keypair_file(&default_keypair_file).unwrap().into(),
|
read_keypair_file(&default_keypair_file).unwrap().into(),
|
||||||
|
@ -4915,6 +4950,7 @@ mod tests {
|
||||||
lamports: 50_000_000_000,
|
lamports: 50_000_000_000,
|
||||||
fee_payer: 1,
|
fee_payer: 1,
|
||||||
compute_unit_price: None,
|
compute_unit_price: None,
|
||||||
|
rent_exempt_reserve: None,
|
||||||
},
|
},
|
||||||
signers: vec![
|
signers: vec![
|
||||||
Presigner::new(&stake_auth_pubkey, &stake_sig).into(),
|
Presigner::new(&stake_auth_pubkey, &stake_sig).into(),
|
||||||
|
|
|
@ -1469,6 +1469,10 @@ fn test_stake_split() {
|
||||||
config.json_rpc_url = test_validator.rpc_url();
|
config.json_rpc_url = test_validator.rpc_url();
|
||||||
config.signers = vec![&default_signer];
|
config.signers = vec![&default_signer];
|
||||||
|
|
||||||
|
let minimum_balance = rpc_client
|
||||||
|
.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut config_offline = CliConfig::recent_for_tests();
|
let mut config_offline = CliConfig::recent_for_tests();
|
||||||
config_offline.json_rpc_url = String::default();
|
config_offline.json_rpc_url = String::default();
|
||||||
config_offline.signers = vec![&offline_signer];
|
config_offline.signers = vec![&offline_signer];
|
||||||
|
@ -1496,10 +1500,7 @@ fn test_stake_split() {
|
||||||
check_balance!(1_000_000_000_000, &rpc_client, &offline_pubkey);
|
check_balance!(1_000_000_000_000, &rpc_client, &offline_pubkey);
|
||||||
|
|
||||||
// Create stake account, identity is authority
|
// Create stake account, identity is authority
|
||||||
let stake_balance = rpc_client
|
let stake_balance = minimum_balance + 10_000_000_000;
|
||||||
.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())
|
|
||||||
.unwrap()
|
|
||||||
+ 10_000_000_000;
|
|
||||||
let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
let stake_keypair = keypair_from_seed(&[0u8; 32]).unwrap();
|
||||||
let stake_account_pubkey = stake_keypair.pubkey();
|
let stake_account_pubkey = stake_keypair.pubkey();
|
||||||
config.signers.push(&stake_keypair);
|
config.signers.push(&stake_keypair);
|
||||||
|
@ -1569,6 +1570,7 @@ fn test_stake_split() {
|
||||||
lamports: 2 * stake_balance,
|
lamports: 2 * stake_balance,
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
compute_unit_price: None,
|
compute_unit_price: None,
|
||||||
|
rent_exempt_reserve: Some(minimum_balance),
|
||||||
};
|
};
|
||||||
config_offline.output_format = OutputFormat::JsonCompact;
|
config_offline.output_format = OutputFormat::JsonCompact;
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
|
@ -1593,10 +1595,15 @@ fn test_stake_split() {
|
||||||
lamports: 2 * stake_balance,
|
lamports: 2 * stake_balance,
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
compute_unit_price: None,
|
compute_unit_price: None,
|
||||||
|
rent_exempt_reserve: None,
|
||||||
};
|
};
|
||||||
process_command(&config).unwrap();
|
process_command(&config).unwrap();
|
||||||
check_balance!(8 * stake_balance, &rpc_client, &stake_account_pubkey,);
|
check_balance!(8 * stake_balance, &rpc_client, &stake_account_pubkey);
|
||||||
check_balance!(2 * stake_balance, &rpc_client, &split_account.pubkey(),);
|
check_balance!(
|
||||||
|
2 * stake_balance + minimum_balance,
|
||||||
|
&rpc_client,
|
||||||
|
&split_account.pubkey()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -103,6 +103,19 @@ pub(crate) fn new_warmup_cooldown_rate_epoch(invoke_context: &InvokeContext) ->
|
||||||
.new_warmup_cooldown_rate_epoch(epoch_schedule.as_ref())
|
.new_warmup_cooldown_rate_epoch(epoch_schedule.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_stake_status(
|
||||||
|
invoke_context: &InvokeContext,
|
||||||
|
stake: &Stake,
|
||||||
|
clock: &Clock,
|
||||||
|
) -> Result<StakeActivationStatus, InstructionError> {
|
||||||
|
let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?;
|
||||||
|
Ok(stake.delegation.stake_activating_and_deactivating(
|
||||||
|
clock.epoch,
|
||||||
|
Some(&stake_history),
|
||||||
|
new_warmup_cooldown_rate_epoch(invoke_context),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn redelegate_stake(
|
fn redelegate_stake(
|
||||||
invoke_context: &InvokeContext,
|
invoke_context: &InvokeContext,
|
||||||
stake: &mut Stake,
|
stake: &mut Stake,
|
||||||
|
@ -688,6 +701,16 @@ pub fn split(
|
||||||
StakeStateV2::Stake(meta, mut stake, stake_flags) => {
|
StakeStateV2::Stake(meta, mut stake, stake_flags) => {
|
||||||
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
meta.authorized.check(signers, StakeAuthorize::Staker)?;
|
||||||
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
let minimum_delegation = crate::get_minimum_delegation(&invoke_context.feature_set);
|
||||||
|
let is_active = if invoke_context
|
||||||
|
.feature_set
|
||||||
|
.is_active(&feature_set::require_rent_exempt_split_destination::id())
|
||||||
|
{
|
||||||
|
let clock = invoke_context.get_sysvar_cache().get_clock()?;
|
||||||
|
let status = get_stake_status(invoke_context, &stake, &clock)?;
|
||||||
|
status.effective > 0
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
let validated_split_info = validate_split_amount(
|
let validated_split_info = validate_split_amount(
|
||||||
invoke_context,
|
invoke_context,
|
||||||
transaction_context,
|
transaction_context,
|
||||||
|
@ -697,6 +720,7 @@ pub fn split(
|
||||||
lamports,
|
lamports,
|
||||||
&meta,
|
&meta,
|
||||||
minimum_delegation,
|
minimum_delegation,
|
||||||
|
is_active,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// split the stake, subtract rent_exempt_balance unless
|
// split the stake, subtract rent_exempt_balance unless
|
||||||
|
@ -763,6 +787,7 @@ pub fn split(
|
||||||
lamports,
|
lamports,
|
||||||
&meta,
|
&meta,
|
||||||
0, // additional_required_lamports
|
0, // additional_required_lamports
|
||||||
|
false,
|
||||||
)?;
|
)?;
|
||||||
let mut split_meta = meta;
|
let mut split_meta = meta;
|
||||||
split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve;
|
split_meta.rent_exempt_reserve = validated_split_info.destination_rent_exempt_reserve;
|
||||||
|
@ -925,12 +950,7 @@ pub fn redelegate(
|
||||||
|
|
||||||
let (stake_meta, effective_stake) =
|
let (stake_meta, effective_stake) =
|
||||||
if let StakeStateV2::Stake(meta, stake, _stake_flags) = stake_account.get_state()? {
|
if let StakeStateV2::Stake(meta, stake, _stake_flags) = stake_account.get_state()? {
|
||||||
let stake_history = invoke_context.get_sysvar_cache().get_stake_history()?;
|
let status = get_stake_status(invoke_context, &stake, &clock)?;
|
||||||
let status = stake.delegation.stake_activating_and_deactivating(
|
|
||||||
clock.epoch,
|
|
||||||
Some(&stake_history),
|
|
||||||
new_warmup_cooldown_rate_epoch(invoke_context),
|
|
||||||
);
|
|
||||||
if status.effective == 0 || status.activating != 0 || status.deactivating != 0 {
|
if status.effective == 0 || status.activating != 0 || status.deactivating != 0 {
|
||||||
ic_msg!(invoke_context, "stake is not active");
|
ic_msg!(invoke_context, "stake is not active");
|
||||||
return Err(StakeError::RedelegateTransientOrInactiveStake.into());
|
return Err(StakeError::RedelegateTransientOrInactiveStake.into());
|
||||||
|
@ -1192,6 +1212,7 @@ fn validate_split_amount(
|
||||||
lamports: u64,
|
lamports: u64,
|
||||||
source_meta: &Meta,
|
source_meta: &Meta,
|
||||||
additional_required_lamports: u64,
|
additional_required_lamports: u64,
|
||||||
|
source_is_active: bool,
|
||||||
) -> Result<ValidatedSplitInfo, InstructionError> {
|
) -> Result<ValidatedSplitInfo, InstructionError> {
|
||||||
let source_account = instruction_context
|
let source_account = instruction_context
|
||||||
.try_borrow_instruction_account(transaction_context, source_account_index)?;
|
.try_borrow_instruction_account(transaction_context, source_account_index)?;
|
||||||
|
@ -1232,12 +1253,27 @@ fn validate_split_amount(
|
||||||
// nothing to do here
|
// nothing to do here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let rent = invoke_context.get_sysvar_cache().get_rent()?;
|
||||||
|
let destination_rent_exempt_reserve = rent.minimum_balance(destination_data_len);
|
||||||
|
|
||||||
|
// As of feature `require_rent_exempt_split_destination`, if the source is active stake, one of
|
||||||
|
// these criteria must be met:
|
||||||
|
// 1. the destination account must be prefunded with at least the rent-exempt reserve, or
|
||||||
|
// 2. the split must consume 100% of the source
|
||||||
|
if invoke_context
|
||||||
|
.feature_set
|
||||||
|
.is_active(&feature_set::require_rent_exempt_split_destination::id())
|
||||||
|
&& source_is_active
|
||||||
|
&& source_remaining_balance != 0
|
||||||
|
&& destination_lamports < destination_rent_exempt_reserve
|
||||||
|
{
|
||||||
|
return Err(InstructionError::InsufficientFunds);
|
||||||
|
}
|
||||||
|
|
||||||
// Verify the destination account meets the minimum balance requirements
|
// Verify the destination account meets the minimum balance requirements
|
||||||
// This must handle:
|
// This must handle:
|
||||||
// 1. The destination account having a different rent exempt reserve due to data size changes
|
// 1. The destination account having a different rent exempt reserve due to data size changes
|
||||||
// 2. The destination account being prefunded, which would lower the minimum split amount
|
// 2. The destination account being prefunded, which would lower the minimum split amount
|
||||||
let rent = invoke_context.get_sysvar_cache().get_rent()?;
|
|
||||||
let destination_rent_exempt_reserve = rent.minimum_balance(destination_data_len);
|
|
||||||
let destination_minimum_balance =
|
let destination_minimum_balance =
|
||||||
destination_rent_exempt_reserve.saturating_add(additional_required_lamports);
|
destination_rent_exempt_reserve.saturating_add(additional_required_lamports);
|
||||||
let destination_balance_deficit =
|
let destination_balance_deficit =
|
||||||
|
|
|
@ -428,15 +428,21 @@ fn test_stake_account_lifetime() {
|
||||||
let split_stake_keypair = Keypair::new();
|
let split_stake_keypair = Keypair::new();
|
||||||
let split_stake_pubkey = split_stake_keypair.pubkey();
|
let split_stake_pubkey = split_stake_keypair.pubkey();
|
||||||
|
|
||||||
|
bank.transfer(
|
||||||
|
stake_rent_exempt_reserve,
|
||||||
|
&mint_keypair,
|
||||||
|
&split_stake_pubkey,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let bank_client = BankClient::new_shared(bank.clone());
|
let bank_client = BankClient::new_shared(bank.clone());
|
||||||
|
|
||||||
// Test split
|
// Test split
|
||||||
let split_starting_delegation = stake_minimum_delegation + bonus_delegation;
|
let split_starting_delegation = stake_minimum_delegation + bonus_delegation;
|
||||||
let split_starting_balance = split_starting_delegation + stake_rent_exempt_reserve;
|
|
||||||
let message = Message::new(
|
let message = Message::new(
|
||||||
&stake_instruction::split(
|
&stake_instruction::split(
|
||||||
&stake_pubkey,
|
&stake_pubkey,
|
||||||
&stake_pubkey,
|
&stake_pubkey,
|
||||||
split_starting_balance,
|
split_starting_delegation,
|
||||||
&split_stake_pubkey,
|
&split_stake_pubkey,
|
||||||
),
|
),
|
||||||
Some(&mint_pubkey),
|
Some(&mint_pubkey),
|
||||||
|
@ -451,7 +457,7 @@ fn test_stake_account_lifetime() {
|
||||||
get_staked(&bank, &split_stake_pubkey),
|
get_staked(&bank, &split_stake_pubkey),
|
||||||
split_starting_delegation,
|
split_starting_delegation,
|
||||||
);
|
);
|
||||||
let stake_remaining_balance = balance - split_starting_balance;
|
let stake_remaining_balance = balance - split_starting_delegation;
|
||||||
|
|
||||||
// Deactivate the split
|
// Deactivate the split
|
||||||
let message = Message::new(
|
let message = Message::new(
|
||||||
|
|
|
@ -689,6 +689,10 @@ pub mod enable_program_runtime_v2_and_loader_v4 {
|
||||||
solana_sdk::declare_id!("8oBxsYqnCvUTGzgEpxPcnVf7MLbWWPYddE33PftFeBBd");
|
solana_sdk::declare_id!("8oBxsYqnCvUTGzgEpxPcnVf7MLbWWPYddE33PftFeBBd");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub mod require_rent_exempt_split_destination {
|
||||||
|
solana_sdk::declare_id!("D2aip4BBr8NPWtU9vLrwrBvbuaQ8w1zV38zFLxx4pfBV");
|
||||||
|
}
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
/// Map of feature identifiers to user-visible description
|
/// Map of feature identifiers to user-visible description
|
||||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||||
|
@ -856,6 +860,7 @@ lazy_static! {
|
||||||
(timely_vote_credits::id(), "use timeliness of votes in determining credits to award"),
|
(timely_vote_credits::id(), "use timeliness of votes in determining credits to award"),
|
||||||
(remaining_compute_units_syscall_enabled::id(), "enable the remaining_compute_units syscall"),
|
(remaining_compute_units_syscall_enabled::id(), "enable the remaining_compute_units syscall"),
|
||||||
(enable_program_runtime_v2_and_loader_v4::id(), "Enable Program-Runtime-v2 and Loader-v4 #33293"),
|
(enable_program_runtime_v2_and_loader_v4::id(), "Enable Program-Runtime-v2 and Loader-v4 #33293"),
|
||||||
|
(require_rent_exempt_split_destination::id(), "Require stake split destination account to be rent exempt"),
|
||||||
/*************** ADD NEW FEATURES HERE ***************/
|
/*************** ADD NEW FEATURES HERE ***************/
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
@ -559,6 +559,7 @@ fn parse_distribute_stake_args(
|
||||||
stake_authority,
|
stake_authority,
|
||||||
withdraw_authority,
|
withdraw_authority,
|
||||||
lockup_authority,
|
lockup_authority,
|
||||||
|
rent_exempt_reserve: None,
|
||||||
};
|
};
|
||||||
let stake_args = StakeArgs {
|
let stake_args = StakeArgs {
|
||||||
unlocked_sol: sol_to_lamports(value_t_or_exit!(matches, "unlocked_sol", f64)),
|
unlocked_sol: sol_to_lamports(value_t_or_exit!(matches, "unlocked_sol", f64)),
|
||||||
|
|
|
@ -5,6 +5,7 @@ pub struct SenderStakeArgs {
|
||||||
pub stake_authority: Box<dyn Signer>,
|
pub stake_authority: Box<dyn Signer>,
|
||||||
pub withdraw_authority: Box<dyn Signer>,
|
pub withdraw_authority: Box<dyn Signer>,
|
||||||
pub lockup_authority: Option<Box<dyn Signer>>,
|
pub lockup_authority: Option<Box<dyn Signer>>,
|
||||||
|
pub rent_exempt_reserve: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct StakeArgs {
|
pub struct StakeArgs {
|
||||||
|
|
|
@ -31,7 +31,7 @@ use {
|
||||||
signature::{unique_signers, Signature, Signer},
|
signature::{unique_signers, Signature, Signer},
|
||||||
stake::{
|
stake::{
|
||||||
instruction::{self as stake_instruction, LockupArgs},
|
instruction::{self as stake_instruction, LockupArgs},
|
||||||
state::{Authorized, Lockup, StakeAuthorize},
|
state::{Authorized, Lockup, StakeAuthorize, StakeStateV2},
|
||||||
},
|
},
|
||||||
system_instruction,
|
system_instruction,
|
||||||
transaction::Transaction,
|
transaction::Transaction,
|
||||||
|
@ -234,12 +234,24 @@ fn distribution_instructions(
|
||||||
Some(sender_stake_args) => {
|
Some(sender_stake_args) => {
|
||||||
let stake_authority = sender_stake_args.stake_authority.pubkey();
|
let stake_authority = sender_stake_args.stake_authority.pubkey();
|
||||||
let withdraw_authority = sender_stake_args.withdraw_authority.pubkey();
|
let withdraw_authority = sender_stake_args.withdraw_authority.pubkey();
|
||||||
let mut instructions = stake_instruction::split(
|
let rent_exempt_reserve = sender_stake_args
|
||||||
|
.rent_exempt_reserve
|
||||||
|
.expect("SenderStakeArgs.rent_exempt_reserve should be populated");
|
||||||
|
|
||||||
|
// Transfer some tokens to stake account to cover rent-exempt reserve.
|
||||||
|
let mut instructions = vec![system_instruction::transfer(
|
||||||
|
&sender_pubkey,
|
||||||
|
new_stake_account_address,
|
||||||
|
rent_exempt_reserve,
|
||||||
|
)];
|
||||||
|
|
||||||
|
// Split to stake account
|
||||||
|
instructions.append(&mut stake_instruction::split(
|
||||||
&sender_stake_args.stake_account_address,
|
&sender_stake_args.stake_account_address,
|
||||||
&stake_authority,
|
&stake_authority,
|
||||||
allocation.amount - unlocked_sol,
|
allocation.amount - unlocked_sol - rent_exempt_reserve,
|
||||||
new_stake_account_address,
|
new_stake_account_address,
|
||||||
);
|
));
|
||||||
|
|
||||||
// Make the recipient the new stake authority
|
// Make the recipient the new stake authority
|
||||||
instructions.push(stake_instruction::authorize(
|
instructions.push(stake_instruction::authorize(
|
||||||
|
@ -1174,11 +1186,15 @@ pub fn test_process_distribute_stake_with_client(client: &RpcClient, sender_keyp
|
||||||
let output_file = NamedTempFile::new().unwrap();
|
let output_file = NamedTempFile::new().unwrap();
|
||||||
let output_path = output_file.path().to_str().unwrap().to_string();
|
let output_path = output_file.path().to_str().unwrap().to_string();
|
||||||
|
|
||||||
|
let rent_exempt_reserve = client
|
||||||
|
.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())
|
||||||
|
.unwrap();
|
||||||
let sender_stake_args = SenderStakeArgs {
|
let sender_stake_args = SenderStakeArgs {
|
||||||
stake_account_address,
|
stake_account_address,
|
||||||
stake_authority: Box::new(stake_authority),
|
stake_authority: Box::new(stake_authority),
|
||||||
withdraw_authority: Box::new(withdraw_authority),
|
withdraw_authority: Box::new(withdraw_authority),
|
||||||
lockup_authority: None,
|
lockup_authority: None,
|
||||||
|
rent_exempt_reserve: Some(rent_exempt_reserve),
|
||||||
};
|
};
|
||||||
let stake_args = StakeArgs {
|
let stake_args = StakeArgs {
|
||||||
unlocked_sol: sol_to_lamports(1.0),
|
unlocked_sol: sol_to_lamports(1.0),
|
||||||
|
@ -1529,14 +1545,14 @@ mod tests {
|
||||||
)); // Same recipient, same lockups
|
)); // Same recipient, same lockups
|
||||||
}
|
}
|
||||||
|
|
||||||
const SET_LOCKUP_INDEX: usize = 5;
|
const SET_LOCKUP_INDEX: usize = 6;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_set_split_stake_lockup() {
|
fn test_set_split_stake_lockup() {
|
||||||
let lockup_date_str = "2021-01-07T00:00:00Z";
|
let lockup_date_str = "2021-01-07T00:00:00Z";
|
||||||
let allocation = Allocation {
|
let allocation = Allocation {
|
||||||
recipient: Pubkey::default().to_string(),
|
recipient: Pubkey::default().to_string(),
|
||||||
amount: sol_to_lamports(1.0),
|
amount: sol_to_lamports(1.002_282_880),
|
||||||
lockup_date: lockup_date_str.to_string(),
|
lockup_date: lockup_date_str.to_string(),
|
||||||
};
|
};
|
||||||
let stake_account_address = solana_sdk::pubkey::new_rand();
|
let stake_account_address = solana_sdk::pubkey::new_rand();
|
||||||
|
@ -1548,6 +1564,7 @@ mod tests {
|
||||||
stake_authority: Box::new(Keypair::new()),
|
stake_authority: Box::new(Keypair::new()),
|
||||||
withdraw_authority: Box::new(Keypair::new()),
|
withdraw_authority: Box::new(Keypair::new()),
|
||||||
lockup_authority: Some(Box::new(lockup_authority)),
|
lockup_authority: Some(Box::new(lockup_authority)),
|
||||||
|
rent_exempt_reserve: Some(2_282_880),
|
||||||
};
|
};
|
||||||
let stake_args = StakeArgs {
|
let stake_args = StakeArgs {
|
||||||
lockup_authority: Some(lockup_authority_address),
|
lockup_authority: Some(lockup_authority_address),
|
||||||
|
@ -1821,6 +1838,7 @@ mod tests {
|
||||||
stake_authority: Box::new(stake_authority),
|
stake_authority: Box::new(stake_authority),
|
||||||
withdraw_authority: Box::new(withdraw_authority),
|
withdraw_authority: Box::new(withdraw_authority),
|
||||||
lockup_authority: None,
|
lockup_authority: None,
|
||||||
|
rent_exempt_reserve: Some(2_282_880),
|
||||||
};
|
};
|
||||||
|
|
||||||
StakeArgs {
|
StakeArgs {
|
||||||
|
|
|
@ -4,4 +4,5 @@ pub mod args;
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
mod db;
|
mod db;
|
||||||
pub mod spl_token;
|
pub mod spl_token;
|
||||||
|
pub mod stake;
|
||||||
pub mod token_display;
|
pub mod token_display;
|
||||||
|
|
|
@ -2,7 +2,7 @@ use {
|
||||||
solana_clap_utils::input_validators::normalize_to_url_if_moniker,
|
solana_clap_utils::input_validators::normalize_to_url_if_moniker,
|
||||||
solana_cli_config::{Config, CONFIG_FILE},
|
solana_cli_config::{Config, CONFIG_FILE},
|
||||||
solana_rpc_client::rpc_client::RpcClient,
|
solana_rpc_client::rpc_client::RpcClient,
|
||||||
solana_tokens::{arg_parser::parse_args, args::Command, commands, spl_token},
|
solana_tokens::{arg_parser::parse_args, args::Command, commands, spl_token, stake},
|
||||||
std::{
|
std::{
|
||||||
env,
|
env,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
@ -43,6 +43,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||||
match command_args.command {
|
match command_args.command {
|
||||||
Command::DistributeTokens(mut args) => {
|
Command::DistributeTokens(mut args) => {
|
||||||
spl_token::update_token_args(&client, &mut args.spl_token_args)?;
|
spl_token::update_token_args(&client, &mut args.spl_token_args)?;
|
||||||
|
stake::update_stake_args(&client, &mut args.stake_args)?;
|
||||||
commands::process_allocations(&client, &args, exit)?;
|
commands::process_allocations(&client, &args, exit)?;
|
||||||
}
|
}
|
||||||
Command::Balances(mut args) => {
|
Command::Balances(mut args) => {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
use {
|
||||||
|
crate::{args::StakeArgs, commands::Error},
|
||||||
|
solana_rpc_client::rpc_client::RpcClient,
|
||||||
|
solana_sdk::stake::state::StakeStateV2,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn update_stake_args(client: &RpcClient, args: &mut Option<StakeArgs>) -> Result<(), Error> {
|
||||||
|
if let Some(stake_args) = args {
|
||||||
|
if let Some(sender_args) = &mut stake_args.sender_stake_args {
|
||||||
|
let rent = client.get_minimum_balance_for_rent_exemption(StakeStateV2::size_of())?;
|
||||||
|
sender_args.rent_exempt_reserve = Some(rent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
Loading…
Reference in New Issue