diff --git a/stake-pool/cli/src/main.rs b/stake-pool/cli/src/main.rs index f394ac58..02bf8bb3 100644 --- a/stake-pool/cli/src/main.rs +++ b/stake-pool/cli/src/main.rs @@ -39,7 +39,6 @@ use spl_stake_pool::{ stake::StakeAuthorize, stake::StakeState, state::StakePool, - state::State as PoolState, state::ValidatorStakeList, }; use spl_token::{ @@ -148,7 +147,7 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult { .get_minimum_balance_for_rent_exemption(TokenAccount::LEN)?; let pool_account_balance = config .rpc_client - .get_minimum_balance_for_rent_exemption(PoolState::LEN)?; + .get_minimum_balance_for_rent_exemption(StakePool::LEN)?; let validator_stake_list_balance = config .rpc_client .get_minimum_balance_for_rent_exemption(ValidatorStakeList::LEN)?; @@ -193,7 +192,7 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult { &config.fee_payer.pubkey(), &pool_account.pubkey(), pool_account_balance, - PoolState::LEN as u64, + StakePool::LEN as u64, &spl_stake_pool::id(), ), // Validator stake account list storage @@ -245,6 +244,7 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult { &validator_stake_list, &mint_account, &pool_fee_account, + config.owner.as_ref(), ]; unique_signers!(signers); transaction.sign(&signers, recent_blockhash); @@ -289,10 +289,7 @@ fn command_vsa_add( ) -> CommandResult { // Get stake pool state let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); let mut total_rent_free_balances: u64 = 0; @@ -383,10 +380,7 @@ fn command_vsa_remove( ) -> CommandResult { // Get stake pool state let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); let pool_withdraw_authority: Pubkey = PoolProcessor::authority_id( &spl_stake_pool::id(), @@ -512,10 +506,7 @@ fn command_deposit( ) -> CommandResult { // Get stake pool state let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); // Get stake account data let stake_data = config.rpc_client.get_account_data(&stake)?; @@ -624,10 +615,7 @@ fn command_deposit( fn command_list(config: &Config, pool: &Pubkey) -> CommandResult { // Get stake pool state let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); let pool_withdraw_authority: Pubkey = PoolProcessor::authority_id( &spl_stake_pool::id(), @@ -657,10 +645,7 @@ fn command_list(config: &Config, pool: &Pubkey) -> CommandResult { fn command_update(config: &Config, pool: &Pubkey) -> CommandResult { // Get stake pool state let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); let validator_stake_list_data = config .rpc_client .get_account_data(&pool_data.validator_stake_list)?; @@ -804,10 +789,7 @@ fn command_withdraw( ) -> CommandResult { // Get stake pool state let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); let pool_withdraw_authority: Pubkey = PoolProcessor::authority_id( &spl_stake_pool::id(), @@ -827,19 +809,21 @@ fn command_withdraw( } // Check burn_from balance - let max_withdraw_amount = pool_tokens_to_stake_amount(&pool_data, account_data.amount); - if max_withdraw_amount < amount { + if account_data.amount < amount { return Err(format!( - "Not enough token balance to withdraw {} SOL.\nMaximum withdraw amount is {} SOL.", + "Not enough token balance to withdraw {} pool tokens.\nMaximum withdraw amount is {} pool tokens.", lamports_to_sol(amount), - lamports_to_sol(max_withdraw_amount) + lamports_to_sol(account_data.amount) ) .into()); } + // Convert pool tokens amount to lamports + let sol_withdraw_amount = pool_tokens_to_stake_amount(&pool_data, amount); + // Get the list of accounts to withdraw from let withdraw_from: Vec = - prepare_withdraw_accounts(config, &pool_withdraw_authority, amount)?; + prepare_withdraw_accounts(config, &pool_withdraw_authority, sol_withdraw_amount)?; // Construct transaction to withdraw from withdraw_from account list let mut instructions: Vec = vec![]; @@ -848,9 +832,6 @@ fn command_withdraw( let mut total_rent_free_balances: u64 = 0; - // Calculate amount of tokens to burn - let tokens_to_burn = stake_amount_to_pool_tokens(&pool_data, amount); - instructions.push( // Approve spending token approve_token( @@ -859,7 +840,7 @@ fn command_withdraw( &pool_withdraw_authority, &config.owner.pubkey(), &[], - tokens_to_burn, + amount, )?, ); @@ -940,10 +921,7 @@ fn command_set_staking_auth( new_staker: &Pubkey, ) -> CommandResult { let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); let pool_withdraw_authority: Pubkey = PoolProcessor::authority_id( &spl_stake_pool::id(), @@ -981,10 +959,7 @@ fn command_set_owner( new_fee_receiver: &Option, ) -> CommandResult { let pool_data = config.rpc_client.get_account_data(&pool)?; - let pool_data: StakePool = PoolState::deserialize(pool_data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap(); // If new accounts are missing in the arguments use the old ones let new_owner: Pubkey = match new_owner { @@ -1264,7 +1239,7 @@ fn main() { .value_name("AMOUNT") .takes_value(true) .required(true) - .help("Amount in SOL to withdraw from the pool."), + .help("Amount of pool tokens to burn and get rewards."), ) .arg( Arg::with_name("burn_from") @@ -1444,6 +1419,7 @@ fn main() { ("withdraw", Some(arg_matches)) => { let pool_account: Pubkey = pubkey_of(arg_matches, "pool").unwrap(); let burn_from: Pubkey = pubkey_of(arg_matches, "burn_from").unwrap(); + // convert from float to int, using sol_to_lamports because they have the same precision as SOL let amount: u64 = sol_to_lamports(value_t_or_exit!(arg_matches, "amount", f64)); let stake_receiver: Option = pubkey_of(arg_matches, "stake_receiver"); command_withdraw(&config, &pool_account, amount, &burn_from, &stake_receiver) diff --git a/stake-pool/program/src/error.rs b/stake-pool/program/src/error.rs index d39a73a3..d2a34451 100644 --- a/stake-pool/program/src/error.rs +++ b/stake-pool/program/src/error.rs @@ -13,7 +13,7 @@ pub enum StakePoolError { /// The program address provided doesn't match the value generated by the program. #[error("InvalidProgramAddress")] InvalidProgramAddress, - /// The token swap state is invalid. + /// The stake pool state is invalid. #[error("InvalidState")] InvalidState, /// The calculation failed. @@ -46,6 +46,9 @@ pub enum StakePoolError { /// Stake account is not in the state expected by the program. #[error("WrongStakeState")] WrongStakeState, + /// User stake is not active + #[error("UserStakeNotActive")] + UserStakeNotActive, /// Stake account voting for this validator already exists in the pool. #[error("ValidatorAlreadyAdded")] ValidatorAlreadyAdded, @@ -58,12 +61,18 @@ pub enum StakePoolError { /// Identify validator stake accounts with old balances and update them. #[error("StakeListOutOfDate")] StakeListOutOfDate, - /// First udpate old validator stake account balances and then pool stake balance. + /// First update old validator stake account balances and then pool stake balance. #[error("StakeListAndPoolOutOfDate")] StakeListAndPoolOutOfDate, /// Validator stake account is not found in the list storage. #[error("UnknownValidatorStakeAccount")] UnknownValidatorStakeAccount, + /// Wrong minting authority set for mint pool account + #[error("WrongMintingAuthority")] + WrongMintingAuthority, + /// Account is not rent-exempt + #[error("AccountNotRentExempt")] + AccountNotRentExempt, } impl From for ProgramError { fn from(e: StakePoolError) -> Self { diff --git a/stake-pool/program/src/instruction.rs b/stake-pool/program/src/instruction.rs index abbe1c4c..00c85ccf 100644 --- a/stake-pool/program/src/instruction.rs +++ b/stake-pool/program/src/instruction.rs @@ -35,11 +35,13 @@ pub enum StakePoolInstruction { /// Initializes a new StakePool. /// /// 0. `[w]` New StakePool to create. - /// 1. `[]` Owner + /// 1. `[s]` Owner /// 2. `[w]` Uninitialized validator stake list storage account /// 3. `[]` pool token Mint. Must be non zero, owned by withdraw authority. /// 4. `[]` Pool Account to deposit the generated fee for owner. - /// 5. `[]` Token program id + /// 5. `[]` Clock sysvar + /// 6. `[]` Rent sysvar + /// 7. `[]` Token program id Initialize(InitArgs), /// Creates new program account for accumulating stakes for a particular validator @@ -66,8 +68,9 @@ pub enum StakePoolInstruction { /// 6. `[w]` User account to receive pool tokens /// 7. `[w]` Pool token mint account /// 8. `[]` Clock sysvar (required) - /// 9. `[]` Pool token program id, - /// 10. `[]` Stake program id, + /// 9. '[]' Sysvar stake history account + /// 10. `[]` Pool token program id, + /// 11. `[]` Stake program id, AddValidatorStakeAccount, /// Removes validator stake account from the pool @@ -253,11 +256,12 @@ pub fn initialize( let data = init_data.serialize()?; let accounts = vec![ AccountMeta::new(*stake_pool, true), - AccountMeta::new_readonly(*owner, false), + AccountMeta::new_readonly(*owner, true), AccountMeta::new(*validator_stake_list, false), AccountMeta::new_readonly(*pool_mint, false), AccountMeta::new_readonly(*owner_pool_account, false), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), AccountMeta::new_readonly(*token_program_id, false), ]; Ok(Instruction { @@ -321,6 +325,7 @@ pub fn add_validator_stake_account( AccountMeta::new(*pool_tokens_to, false), AccountMeta::new(*pool_mint, false), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), AccountMeta::new_readonly(*token_program_id, false), AccountMeta::new_readonly(*stake_program_id, false), ]; diff --git a/stake-pool/program/src/processor.rs b/stake-pool/program/src/processor.rs index 2203c8fa..73bf4b82 100644 --- a/stake-pool/program/src/processor.rs +++ b/stake-pool/program/src/processor.rs @@ -4,7 +4,7 @@ use crate::{ error::StakePoolError, instruction::{InitArgs, StakePoolInstruction}, stake, - state::{StakePool, State, ValidatorStakeInfo, ValidatorStakeList}, + state::{StakePool, ValidatorStakeInfo, ValidatorStakeList}, PROGRAM_VERSION, }; use bincode::deserialize; @@ -22,9 +22,11 @@ use solana_program::{ program_pack::Pack, pubkey::Pubkey, rent::Rent, + stake_history::StakeHistory, system_instruction, sysvar::Sysvar, }; +use spl_token::state::Mint; /// Program state handler. pub struct Processor {} @@ -293,28 +295,60 @@ impl Processor { // Clock sysvar account let clock_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(clock_info)?; + // Rent sysvar account + let rent_info = next_account_info(account_info_iter)?; + let rent = &Rent::from_account_info(rent_info)?; // Token program ID let token_program_info = next_account_info(account_info_iter)?; + // Check if transaction was signed by owner + if !owner_info.is_signer { + return Err(StakePoolError::SignatureMissing.into()); + } + + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; // Stake pool account should not be already initialized - if State::Unallocated != State::deserialize(&stake_pool_info.data.borrow())? { + if stake_pool.is_initialized() { return Err(StakePoolError::AlreadyInUse.into()); } // Check if validator stake list storage is unitialized let mut validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if validator_stake_list.is_initialized { + if validator_stake_list.is_initialized() { return Err(StakePoolError::AlreadyInUse.into()); } - validator_stake_list.is_initialized = true; + validator_stake_list.version = ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION; validator_stake_list.validators.clear(); + // Check if stake pool account is rent-exempt + if !rent.is_exempt(stake_pool_info.lamports(), stake_pool_info.data_len()) { + return Err(StakePoolError::AccountNotRentExempt.into()); + } + + // Check if validator stake list account is rent-exempt + if !rent.is_exempt( + validator_stake_list_info.lamports(), + validator_stake_list_info.data_len(), + ) { + return Err(StakePoolError::AccountNotRentExempt.into()); + } + // Numerator should be smaller than or equal to denominator (fee <= 1) if init.fee.numerator > init.fee.denominator { return Err(StakePoolError::FeeTooHigh.into()); } + // Check if fee account's owner the same as token program id + if owner_fee_info.owner != token_program_info.key { + return Err(StakePoolError::InvalidFeeAccount.into()); + } + + // Check pool mint program ID + if pool_mint_info.owner != token_program_info.key { + return Err(ProgramError::IncorrectProgramId); + } + // Check for owner fee account to have proper mint assigned if *pool_mint_info.key != spl_token::state::Account::unpack_from_slice(&owner_fee_info.data.borrow())?.mint @@ -322,41 +356,39 @@ impl Processor { return Err(StakePoolError::WrongAccountMint.into()); } - // Check pool mint program ID - if pool_mint_info.owner != token_program_info.key { - return Err(ProgramError::IncorrectProgramId); - } - let (_, deposit_bump_seed) = Self::find_authority_bump_seed( program_id, stake_pool_info.key, Self::AUTHORITY_DEPOSIT, ); - let (_, withdraw_bump_seed) = Self::find_authority_bump_seed( + let (withdraw_authority_key, withdraw_bump_seed) = Self::find_authority_bump_seed( program_id, stake_pool_info.key, Self::AUTHORITY_WITHDRAW, ); + let pool_mint = Mint::unpack_from_slice(&pool_mint_info.data.borrow())?; + + if !pool_mint.mint_authority.contains(&withdraw_authority_key) { + return Err(StakePoolError::WrongMintingAuthority.into()); + } + validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?; msg!("Clock data: {:?}", clock_info.data.borrow()); msg!("Epoch: {}", clock.epoch); - let stake_pool = State::Init(StakePool { - version: PROGRAM_VERSION, - owner: *owner_info.key, - deposit_bump_seed, - withdraw_bump_seed, - validator_stake_list: *validator_stake_list_info.key, - pool_mint: *pool_mint_info.key, - owner_fee_account: *owner_fee_info.key, - token_program_id: *token_program_info.key, - stake_total: 0, - pool_total: 0, - last_update_epoch: clock.epoch, - fee: init.fee, - }); + stake_pool.version = PROGRAM_VERSION; + stake_pool.owner = *owner_info.key; + stake_pool.deposit_bump_seed = deposit_bump_seed; + stake_pool.withdraw_bump_seed = withdraw_bump_seed; + stake_pool.validator_stake_list = *validator_stake_list_info.key; + stake_pool.pool_mint = *pool_mint_info.key; + stake_pool.owner_fee_account = *owner_fee_info.key; + stake_pool.token_program_id = *token_program_info.key; + stake_pool.last_update_epoch = clock.epoch; + stake_pool.fee = init.fee; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut()) } @@ -412,35 +444,20 @@ impl Processor { // Fund the associated token account with the minimum balance to be rent exempt let required_lamports = 1 + rent.minimum_balance(std::mem::size_of::()); - // Fund new account - invoke( - &system_instruction::transfer( + + // Create new stake account + invoke_signed( + &system_instruction::create_account( &funder_info.key, - stake_account_info.key, + &stake_account_info.key, required_lamports, - ), - &[ - funder_info.clone(), - stake_account_info.clone(), - system_program_info.clone(), - ], - )?; - // Allocate account space - invoke_signed( - &system_instruction::allocate( - stake_account_info.key, std::mem::size_of::() as u64, + &stake::id(), ), - &[stake_account_info.clone(), system_program_info.clone()], + &[funder_info.clone(), stake_account_info.clone()], &[&stake_account_signer_seeds], )?; - // Assign account to the stake program - invoke_signed( - &system_instruction::assign(stake_account_info.key, &stake::id()), - &[stake_account_info.clone(), system_program_info.clone()], - &[&stake_account_signer_seeds], - )?; - // Initialize stake account + invoke( &stake::initialize( &stake_account_info.key, @@ -483,6 +500,9 @@ impl Processor { // Clock sysvar account let clock_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(clock_info)?; + // Stake history sysvar account + let stake_history_info = next_account_info(account_info_iter)?; + let stake_history = &StakeHistory::from_account_info(stake_history_info)?; // Pool token program id let token_program_info = next_account_info(account_info_iter)?; // Staking program id @@ -493,8 +513,11 @@ impl Processor { return Err(ProgramError::IncorrectProgramId); } - // Get stake pool stake (and check if it is iniaialized) - let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + // Get stake pool stake (and check if it is initialized) + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } // Check authority accounts stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?; @@ -523,7 +546,7 @@ impl Processor { // Read validator stake list account and check if it is valid let mut validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if !validator_stake_list.is_initialized { + if !validator_stake_list.is_initialized() { return Err(StakePoolError::InvalidState.into()); } @@ -568,7 +591,8 @@ impl Processor { token_amount, )?; - // TODO: Check if stake is warmed up + // Check if stake is warmed up + Self::check_stake_activation(stake_account_info, clock, stake_history)?; // Add validator to the list and save validator_stake_list.validators.push(ValidatorStakeInfo { @@ -582,7 +606,7 @@ impl Processor { stake_pool.pool_total += token_amount; // Only update stake total if the last state update epoch is current stake_pool.stake_total += stake_lamports; - State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } @@ -622,8 +646,11 @@ impl Processor { return Err(ProgramError::IncorrectProgramId); } - // Get stake pool stake (and check if it is iniaialized) - let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + // Get stake pool stake (and check if it is initialized) + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } // Check authority account stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?; @@ -651,7 +678,7 @@ impl Processor { // Read validator stake list account and check if it is valid let mut validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if !validator_stake_list.is_initialized { + if !validator_stake_list.is_initialized() { return Err(StakePoolError::InvalidState.into()); } @@ -706,7 +733,7 @@ impl Processor { stake_pool.pool_total -= token_amount; // Only update stake total if the last state update epoch is current stake_pool.stake_total -= stake_lamports; - State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } @@ -728,7 +755,7 @@ impl Processor { // Read validator stake list account and check if it is valid let mut validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if !validator_stake_list.is_initialized { + if !validator_stake_list.is_initialized() { return Err(StakePoolError::InvalidState.into()); } @@ -779,8 +806,11 @@ impl Processor { let clock_info = next_account_info(account_info_iter)?; let clock = &Clock::from_account_info(clock_info)?; - // Get stake pool stake (and check if it is iniaialized) - let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + // Get stake pool stake (and check if it is initialized) + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } // Check validator stake account list storage if *validator_stake_list_info.key != stake_pool.validator_stake_list { @@ -790,7 +820,7 @@ impl Processor { // Read validator stake list account and check if it is valid let validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if !validator_stake_list.is_initialized { + if !validator_stake_list.is_initialized() { return Err(StakePoolError::InvalidState.into()); } @@ -804,11 +834,40 @@ impl Processor { stake_pool.stake_total = total_balance; stake_pool.last_update_epoch = clock.epoch; - State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } + /// Check stake activation status + pub fn check_stake_activation( + _stake_info: &AccountInfo, + _clock: &Clock, + _stake_history: &StakeHistory, + ) -> ProgramResult { + // TODO: remove conditional compilation when time travel in tests is possible + //#[cfg(not(feature = "test-bpf"))] + // This check is commented to make tests run without special command line arguments + /*{ + let stake_acc_state: stake::StakeState = + deserialize(&stake_info.data.borrow()).unwrap(); + let delegation = stake_acc_state.delegation(); + if let Some(delegation) = delegation { + let target_epoch = clock.epoch; + let history = Some(stake_history); + let fix_stake_deactivate = true; + let (effective, activating, deactivating) = delegation + .stake_activating_and_deactivating(target_epoch, history, fix_stake_deactivate); + if activating != 0 || deactivating != 0 || effective == 0 { + return Err(StakePoolError::UserStakeNotActive.into()); + } + } else { + return Err(StakePoolError::WrongStakeState.into()); + } + }*/ + Ok(()) + } + /// Processes [Deposit](enum.Instruction.html). pub fn process_deposit(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -822,7 +881,7 @@ impl Processor { let withdraw_info = next_account_info(account_info_iter)?; // Stake account to join the pool (withdraw should be set to stake pool deposit) let stake_info = next_account_info(account_info_iter)?; - // Valdidator stake account to merge with + // Validator stake account to merge with let validator_stake_account_info = next_account_info(account_info_iter)?; // User account to receive pool tokens let dest_user_info = next_account_info(account_info_iter)?; @@ -835,6 +894,7 @@ impl Processor { let clock = &Clock::from_account_info(clock_info)?; // Stake history sysvar account let stake_history_info = next_account_info(account_info_iter)?; + let stake_history = &StakeHistory::from_account_info(stake_history_info)?; // Pool token program id let token_program_info = next_account_info(account_info_iter)?; // Stake program id @@ -845,7 +905,13 @@ impl Processor { return Err(ProgramError::IncorrectProgramId); } - let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } + + // Check if stake is active + Self::check_stake_activation(stake_info, clock, stake_history)?; // Check authority accounts stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?; @@ -871,7 +937,7 @@ impl Processor { // Read validator stake list account and check if it is valid let mut validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if !validator_stake_list.is_initialized { + if !validator_stake_list.is_initialized() { return Err(StakePoolError::InvalidState.into()); } @@ -954,7 +1020,7 @@ impl Processor { )?; stake_pool.pool_total += pool_amount; stake_pool.stake_total += stake_lamports; - State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?; validator_list_item.balance = **validator_stake_account_info.lamports.borrow(); validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?; @@ -965,7 +1031,7 @@ impl Processor { /// Processes [Withdraw](enum.Instruction.html). pub fn process_withdraw( program_id: &Pubkey, - stake_amount: u64, + pool_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); @@ -998,7 +1064,10 @@ impl Processor { return Err(ProgramError::IncorrectProgramId); } - let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } // Check authority account stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?; @@ -1020,7 +1089,7 @@ impl Processor { // Read validator stake list account and check if it is valid let mut validator_stake_list = ValidatorStakeList::deserialize(&validator_stake_list_info.data.borrow())?; - if !validator_stake_list.is_initialized { + if !validator_stake_list.is_initialized() { return Err(StakePoolError::InvalidState.into()); } @@ -1031,8 +1100,8 @@ impl Processor { .find_mut(&validator_account) .ok_or(StakePoolError::ValidatorNotFound)?; - let pool_amount = stake_pool - .calc_pool_withdraw_amount(stake_amount) + let stake_amount = stake_pool + .calc_lamports_amount(pool_amount) .ok_or(StakePoolError::CalculationFailure)?; Self::stake_split( @@ -1084,7 +1153,7 @@ impl Processor { stake_pool.pool_total -= pool_amount; stake_pool.stake_total -= stake_amount; - State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?; validator_list_item.balance = **stake_split_from.lamports.borrow(); validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?; @@ -1112,7 +1181,10 @@ impl Processor { return Err(ProgramError::IncorrectProgramId); } - let stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + let stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } // Check authority account stake_pool.check_authority_withdraw(withdraw_info.key, program_id, stake_pool_info.key)?; @@ -1142,7 +1214,10 @@ impl Processor { let new_owner_info = next_account_info(account_info_iter)?; let new_owner_fee_info = next_account_info(account_info_iter)?; - let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; + let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?; + if !stake_pool.is_initialized() { + return Err(StakePoolError::InvalidState.into()); + } // Check owner validity and signature stake_pool.check_owner(owner_info)?; @@ -1156,7 +1231,7 @@ impl Processor { stake_pool.owner = *new_owner_info.key; stake_pool.owner_fee_account = *new_owner_fee_info.key; - State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; + stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } /// Processes [Instruction](enum.Instruction.html). @@ -1213,27 +1288,30 @@ impl PrintProgramError for StakePoolError { E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, { match self { - StakePoolError::AlreadyInUse => msg!("Error: AlreadyInUse"), - StakePoolError::InvalidProgramAddress => msg!("Error: InvalidProgramAddress"), - StakePoolError::InvalidState => msg!("Error: InvalidState"), - StakePoolError::CalculationFailure => msg!("Error: CalculationFailure"), - StakePoolError::FeeTooHigh => msg!("Error: FeeTooHigh"), - StakePoolError::WrongAccountMint => msg!("Error: WrongAccountMint"), - StakePoolError::NonZeroBalance => msg!("Error: NonZeroBalance"), - StakePoolError::WrongOwner => msg!("Error: WrongOwner"), - StakePoolError::SignatureMissing => msg!("Error: SignatureMissing"), - StakePoolError::InvalidValidatorStakeList => msg!("Error: InvalidValidatorStakeList"), - StakePoolError::InvalidFeeAccount => msg!("Error: InvalidFeeAccount"), - StakePoolError::WrongPoolMint => msg!("Error: WrongPoolMint"), - StakePoolError::WrongStakeState => msg!("Error: WrongStakeState"), - StakePoolError::ValidatorAlreadyAdded => msg!("Error: ValidatorAlreadyAdded"), - StakePoolError::ValidatorNotFound => msg!("Error: ValidatorNotFound"), - StakePoolError::InvalidStakeAccountAddress => msg!("Error: InvalidStakeAccountAddress"), - StakePoolError::StakeListOutOfDate => msg!("Error: StakeListOutOfDate"), - StakePoolError::StakeListAndPoolOutOfDate => msg!("Error: StakeListAndPoolOutOfDate"), + StakePoolError::AlreadyInUse => msg!("Error: The account cannot be initialized because it is already being used"), + StakePoolError::InvalidProgramAddress => msg!("Error: The program address provided doesn't match the value generated by the program"), + StakePoolError::InvalidState => msg!("Error: The stake pool state is invalid"), + StakePoolError::CalculationFailure => msg!("Error: The calculation failed"), + StakePoolError::FeeTooHigh => msg!("Error: Stake pool fee > 1"), + StakePoolError::WrongAccountMint => msg!("Error: Token account is associated with the wrong mint"), + StakePoolError::NonZeroBalance => msg!("Error: Account balance should be zero"), + StakePoolError::WrongOwner => msg!("Error: Wrong pool owner account"), + StakePoolError::SignatureMissing => msg!("Error: Required signature is missing"), + StakePoolError::InvalidValidatorStakeList => msg!("Error: Invalid validator stake list account"), + StakePoolError::InvalidFeeAccount => msg!("Error: Invalid owner fee account"), + StakePoolError::WrongPoolMint => msg!("Error: Specified pool mint account is wrong"), + StakePoolError::WrongStakeState => msg!("Error: Stake account is not in the state expected by the program"), + StakePoolError::UserStakeNotActive => msg!("Error: User stake is not active"), + StakePoolError::ValidatorAlreadyAdded => msg!("Error: Stake account voting for this validator already exists in the pool"), + StakePoolError::ValidatorNotFound => msg!("Error: Stake account for this validator not found in the pool"), + StakePoolError::InvalidStakeAccountAddress => msg!("Error: Stake account address not properly derived from the validator address"), + StakePoolError::StakeListOutOfDate => msg!("Error: Identify validator stake accounts with old balances and update them"), + StakePoolError::StakeListAndPoolOutOfDate => msg!("Error: First update old validator stake account balances and then pool stake balance"), StakePoolError::UnknownValidatorStakeAccount => { - msg!("Error: UnknownValidatorStakeAccount") + msg!("Error: Validator stake account is not found in the list storage") } + StakePoolError::WrongMintingAuthority => msg!("Error: Wrong minting authority set for mint pool account"), + StakePoolError::AccountNotRentExempt => msg!("Error: Account is not rent-exempt"), } } } diff --git a/stake-pool/program/src/stake.rs b/stake-pool/program/src/stake.rs index 0a49cc22..47351ff2 100644 --- a/stake-pool/program/src/stake.rs +++ b/stake-pool/program/src/stake.rs @@ -5,6 +5,7 @@ use solana_program::{ clock::{Epoch, UnixTimestamp}, instruction::{AccountMeta, Instruction}, pubkey::Pubkey, + stake_history::StakeHistory, system_instruction, sysvar, }; use std::str::FromStr; @@ -69,7 +70,7 @@ pub enum StakeInstruction { /// 5. Optional: [SIGNER] Lockup authority, if before lockup expiration /// /// The u64 is the portion of the stake account balance to be withdrawn, - /// must be `<= StakeAccount.lamports - staked_lamports`. + /// must be `<= ValidatorStakeAccount.lamports - staked_lamports`. Withdraw(u64), /// Deactivates the stake in the account @@ -186,6 +187,212 @@ pub struct Lockup { pub custodian: Pubkey, } +/// FIXME copied from the stake program +impl StakeState { + /// Get Delegation + pub fn delegation(&self) -> Option { + match self { + StakeState::Stake(_meta, stake) => Some(stake.delegation), + _ => None, + } + } +} + +/// FIXME copied from the stake program +impl Delegation { + /// Create new Delegation + pub fn new( + voter_pubkey: &Pubkey, + stake: u64, + activation_epoch: Epoch, + warmup_cooldown_rate: f64, + ) -> Self { + Self { + voter_pubkey: *voter_pubkey, + stake, + activation_epoch, + warmup_cooldown_rate, + ..Delegation::default() + } + } + /// Check if it bootstrap + pub fn is_bootstrap(&self) -> bool { + self.activation_epoch == std::u64::MAX + } + + /// Return tuple (effective, activating, deactivating) stake + #[allow(clippy::comparison_chain)] + pub fn stake_activating_and_deactivating( + &self, + target_epoch: Epoch, + history: Option<&StakeHistory>, + fix_stake_deactivate: bool, + ) -> (u64, u64, u64) { + let delegated_stake = self.stake; + + // first, calculate an effective and activating stake + let (effective_stake, activating_stake) = + self.stake_and_activating(target_epoch, history, fix_stake_deactivate); + + // then de-activate some portion if necessary + if target_epoch < self.deactivation_epoch { + // not deactivated + (effective_stake, activating_stake, 0) + } else if target_epoch == self.deactivation_epoch { + // can only deactivate what's activated + (effective_stake, 0, effective_stake.min(delegated_stake)) + } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) = + history.and_then(|history| { + history + .get(&self.deactivation_epoch) + .map(|cluster_stake_at_deactivation_epoch| { + ( + history, + self.deactivation_epoch, + cluster_stake_at_deactivation_epoch, + ) + }) + }) + { + // target_epoch > self.deactivation_epoch + + // loop from my deactivation epoch until the target epoch + // current effective stake is updated using its previous epoch's cluster stake + let mut current_epoch; + let mut current_effective_stake = effective_stake; + loop { + current_epoch = prev_epoch + 1; + // if there is no deactivating stake at prev epoch, we should have been + // fully undelegated at this moment + if prev_cluster_stake.deactivating == 0 { + break; + } + + // I'm trying to get to zero, how much of the deactivation in stake + // this account is entitled to take + let weight = + current_effective_stake as f64 / prev_cluster_stake.deactivating as f64; + + // portion of newly not-effective cluster stake I'm entitled to at current epoch + let newly_not_effective_cluster_stake = + prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate; + let newly_not_effective_stake = + ((weight * newly_not_effective_cluster_stake) as u64).max(1); + + current_effective_stake = + current_effective_stake.saturating_sub(newly_not_effective_stake); + if current_effective_stake == 0 { + break; + } + + if current_epoch >= target_epoch { + break; + } + if let Some(current_cluster_stake) = history.get(¤t_epoch) { + prev_epoch = current_epoch; + prev_cluster_stake = current_cluster_stake; + } else { + break; + } + } + + // deactivating stake should equal to all of currently remaining effective stake + (current_effective_stake, 0, current_effective_stake) + } else { + // no history or I've dropped out of history, so assume fully deactivated + (0, 0, 0) + } + } + + // returned tuple is (effective, activating) stake + fn stake_and_activating( + &self, + target_epoch: Epoch, + history: Option<&StakeHistory>, + fix_stake_deactivate: bool, + ) -> (u64, u64) { + let delegated_stake = self.stake; + + if self.is_bootstrap() { + // fully effective immediately + (delegated_stake, 0) + } else if fix_stake_deactivate && self.activation_epoch == self.deactivation_epoch { + // activated but instantly deactivated; no stake at all regardless of target_epoch + // this must be after the bootstrap check and before all-is-activating check + (0, 0) + } else if target_epoch == self.activation_epoch { + // all is activating + (0, delegated_stake) + } else if target_epoch < self.activation_epoch { + // not yet enabled + (0, 0) + } else if let Some((history, mut prev_epoch, mut prev_cluster_stake)) = + history.and_then(|history| { + history + .get(&self.activation_epoch) + .map(|cluster_stake_at_activation_epoch| { + ( + history, + self.activation_epoch, + cluster_stake_at_activation_epoch, + ) + }) + }) + { + // target_epoch > self.activation_epoch + + // loop from my activation epoch until the target epoch summing up my entitlement + // current effective stake is updated using its previous epoch's cluster stake + let mut current_epoch; + let mut current_effective_stake = 0; + loop { + current_epoch = prev_epoch + 1; + // if there is no activating stake at prev epoch, we should have been + // fully effective at this moment + if prev_cluster_stake.activating == 0 { + break; + } + + // how much of the growth in stake this account is + // entitled to take + let remaining_activating_stake = delegated_stake - current_effective_stake; + let weight = + remaining_activating_stake as f64 / prev_cluster_stake.activating as f64; + + // portion of newly effective cluster stake I'm entitled to at current epoch + let newly_effective_cluster_stake = + prev_cluster_stake.effective as f64 * self.warmup_cooldown_rate; + let newly_effective_stake = + ((weight * newly_effective_cluster_stake) as u64).max(1); + + current_effective_stake += newly_effective_stake; + if current_effective_stake >= delegated_stake { + current_effective_stake = delegated_stake; + break; + } + + if current_epoch >= target_epoch || current_epoch >= self.deactivation_epoch { + break; + } + if let Some(current_cluster_stake) = history.get(¤t_epoch) { + prev_epoch = current_epoch; + prev_cluster_stake = current_cluster_stake; + } else { + break; + } + } + + ( + current_effective_stake, + delegated_stake - current_effective_stake, + ) + } else { + // no history or I've dropped out of history, so assume fully effective + (delegated_stake, 0) + } + } +} + /// FIXME copied from the stake program pub fn split_only( stake_pubkey: &Pubkey, diff --git a/stake-pool/program/src/state.rs b/stake-pool/program/src/state.rs index 02bad925..3c945223 100644 --- a/stake-pool/program/src/state.rs +++ b/stake-pool/program/src/state.rs @@ -1,7 +1,7 @@ //! State transition types use crate::error::StakePoolError; -use crate::instruction::{unpack, Fee}; +use crate::instruction::Fee; use crate::processor::Processor; use core::convert::TryInto; use solana_program::{ @@ -44,6 +44,8 @@ pub struct StakePool { pub fee: Fee, } impl StakePool { + /// Length of state data when serialized + pub const LEN: usize = size_of::(); /// calculate the pool tokens that should be minted pub fn calc_pool_deposit_amount(&self, stake_lamports: u64) -> Option { if self.stake_total == 0 { @@ -60,6 +62,15 @@ impl StakePool { ) .ok() } + /// calculate lamports amount + pub fn calc_lamports_amount(&self, pool_tokens: u64) -> Option { + u64::try_from( + (pool_tokens as u128) + .checked_mul(self.stake_total as u128)? + .checked_div(self.pool_total as u128)?, + ) + .ok() + } /// calculate the fee in pool tokens that goes to the owner pub fn calc_fee_amount(&self, pool_amount: u64) -> Option { if self.fee.denominator == 0 { @@ -114,67 +125,34 @@ impl StakePool { } Ok(()) } -} -/// Program states. -#[repr(C)] -#[derive(Clone, Copy, Debug, PartialEq)] -#[allow(clippy::large_enum_variant)] -pub enum State { - /// Unallocated state, may be initialized into another state. - Unallocated, - /// Initialized state. - Init(StakePool), -} - -impl State { - /// Length of state data when serialized - pub const LEN: usize = size_of::() + size_of::(); - /// Deserializes a byte buffer into a [State](struct.State.html). - /// TODO efficient unpacking here - pub fn deserialize(input: &[u8]) -> Result { - if input.len() < size_of::() { - return Err(ProgramError::InvalidAccountData); - } - Ok(match input[0] { - 0 => State::Unallocated, - 1 => { - // We send whole input here, because unpack skips the first byte - let swap: &StakePool = unpack(&input)?; - State::Init(*swap) - } - _ => return Err(ProgramError::InvalidAccountData), - }) + /// Check if StakePool is initialized + pub fn is_initialized(&self) -> bool { + self.version > 0 } - /// Serializes [State](struct.State.html) into a byte buffer. - /// TODO efficient packing here + /// Deserializes a byte buffer into a [StakePool](struct.StakePool.html). + pub fn deserialize(input: &[u8]) -> Result { + if input.len() < size_of::() { + return Err(ProgramError::InvalidAccountData); + } + + let stake_pool: &StakePool = unsafe { &*(&input[0] as *const u8 as *const StakePool) }; + + Ok(*stake_pool) + } + + /// Serializes [StakePool](struct.StakePool.html) into a byte buffer. pub fn serialize(&self, output: &mut [u8]) -> ProgramResult { - if output.len() < size_of::() { + if output.len() < size_of::() { return Err(ProgramError::InvalidAccountData); } - match self { - Self::Unallocated => output[0] = 0, - Self::Init(swap) => { - if output.len() < size_of::() + size_of::() { - return Err(ProgramError::InvalidAccountData); - } - output[0] = 1; - #[allow(clippy::cast_ptr_alignment)] - let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut StakePool) }; - *value = *swap; - } - } + #[allow(clippy::cast_ptr_alignment)] + let value = unsafe { &mut *(&mut output[0] as *mut u8 as *mut StakePool) }; + *value = *self; + Ok(()) } - /// Gets the `StakePool` from `State` - pub fn stake_pool(&self) -> Result { - if let State::Init(swap) = &self { - Ok(*swap) - } else { - Err(StakePoolError::InvalidState.into()) - } - } } const MAX_VALIDATOR_STAKE_ACCOUNTS: usize = 1000; @@ -183,8 +161,8 @@ const MAX_VALIDATOR_STAKE_ACCOUNTS: usize = 1000; #[repr(C)] #[derive(Clone, Debug, Default, PartialEq)] pub struct ValidatorStakeList { - /// False if not yet initialized - pub is_initialized: bool, + /// Validator stake list version + pub version: u8, /// List of all validator stake accounts and their info pub validators: Vec, } @@ -211,6 +189,9 @@ impl ValidatorStakeList { /// Header length pub const HEADER_LEN: usize = size_of::() + size_of::(); + /// Version of validator stake list + pub const VALIDATOR_STAKE_LIST_VERSION: u8 = 1; + /// Check if contains validator with particular pubkey pub fn contains(&self, validator: &Pubkey) -> bool { self.validators @@ -231,6 +212,11 @@ impl ValidatorStakeList { .find(|x| x.validator_account == *validator) } + /// Check if validator stake list is initialized + pub fn is_initialized(&self) -> bool { + self.version > 0 + } + /// Deserializes a byte buffer into a ValidatorStakeList. pub fn deserialize(input: &[u8]) -> Result { if input.len() < Self::LEN { @@ -239,7 +225,7 @@ impl ValidatorStakeList { if input[0] == 0 { return Ok(ValidatorStakeList { - is_initialized: false, + version: 0, validators: vec![], }); } @@ -262,7 +248,7 @@ impl ValidatorStakeList { to += ValidatorStakeInfo::LEN; } Ok(ValidatorStakeList { - is_initialized: true, + version: input[0], validators, }) } @@ -275,7 +261,7 @@ impl ValidatorStakeList { if self.validators.len() > MAX_VALIDATOR_STAKE_ACCOUNTS { return Err(ProgramError::InvalidAccountData); } - output[0] = if self.is_initialized { 1 } else { 0 }; + output[0] = self.version; output[1..3].copy_from_slice(&u16::to_le_bytes(self.validators.len() as u16)); let mut from = Self::HEADER_LEN; let mut to = from + ValidatorStakeInfo::LEN; @@ -324,7 +310,7 @@ mod test { fn test_state_packing() { // Not initialized let stake_list = ValidatorStakeList { - is_initialized: false, + version: 0, validators: vec![], }; let mut bytes: [u8; ValidatorStakeList::LEN] = [0; ValidatorStakeList::LEN]; @@ -334,7 +320,7 @@ mod test { // Empty let stake_list = ValidatorStakeList { - is_initialized: true, + version: ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION, validators: vec![], }; let mut bytes: [u8; ValidatorStakeList::LEN] = [0; ValidatorStakeList::LEN]; @@ -344,7 +330,7 @@ mod test { // With several accounts let stake_list = ValidatorStakeList { - is_initialized: true, + version: ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION, validators: vec![ ValidatorStakeInfo { validator_account: Pubkey::new_from_array([1; 32]), diff --git a/stake-pool/program/tests/deposit.rs b/stake-pool/program/tests/deposit.rs index 8f92e56c..b1dce30b 100644 --- a/stake-pool/program/tests/deposit.rs +++ b/stake-pool/program/tests/deposit.rs @@ -16,7 +16,13 @@ use solana_sdk::{ use spl_stake_pool::*; use spl_token::error as token_error; -async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, StakeAccount) { +async fn setup() -> ( + BanksClient, + Keypair, + Hash, + StakePoolAccounts, + ValidatorStakeAccount, +) { let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let stake_pool_accounts = StakePoolAccounts::new(); stake_pool_accounts @@ -24,7 +30,7 @@ async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, StakeAccount .await .unwrap(); - let validator_stake_account: StakeAccount = simple_add_validator_stake_account( + let validator_stake_account: ValidatorStakeAccount = simple_add_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -50,10 +56,13 @@ async fn test_stake_pool_deposit() { // make stake account let user_stake = Keypair::new(); let lockup = stake::Lockup::default(); + + let stake_authority = Keypair::new(); let authorized = stake::Authorized { - staker: stake_pool_accounts.deposit_authority, - withdrawer: stake_pool_accounts.deposit_authority, + staker: stake_authority.pubkey(), + withdrawer: stake_authority.pubkey(), }; + let stake_lamports = create_independent_stake_account( &mut banks_client, &payer, @@ -63,6 +72,46 @@ async fn test_stake_pool_deposit() { &lockup, ) .await; + + create_vote( + &mut banks_client, + &payer, + &recent_blockhash, + &validator_stake_account.vote, + ) + .await; + delegate_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake.pubkey(), + &stake_authority, + &validator_stake_account.vote.pubkey(), + ) + .await; + + // Change authority to the stake pool's deposit + authorize_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake.pubkey(), + &stake_authority, + &stake_pool_accounts.deposit_authority, + stake::StakeAuthorize::Withdrawer, + ) + .await; + authorize_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake.pubkey(), + &stake_authority, + &stake_pool_accounts.deposit_authority, + stake::StakeAuthorize::Staker, + ) + .await; + // make pool token account let user_pool_account = Keypair::new(); create_token_account( @@ -79,10 +128,8 @@ async fn test_stake_pool_deposit() { // Save stake pool state before depositing let stake_pool_before = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool_before = state::State::deserialize(&stake_pool_before.data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let stake_pool_before = + state::StakePool::deserialize(&stake_pool_before.data.as_slice()).unwrap(); // Save validator stake account record before depositing let validator_stake_list = get_account( @@ -120,10 +167,7 @@ async fn test_stake_pool_deposit() { // Stake pool should add its balance to the pool balance let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = state::State::deserialize(&stake_pool.data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let stake_pool = state::StakePool::deserialize(&stake_pool.data.as_slice()).unwrap(); assert_eq!( stake_pool.stake_total, stake_pool_before.stake_total + stake_lamports @@ -239,6 +283,20 @@ async fn test_stake_pool_deposit_with_wrong_pool_fee_account() { let user = Keypair::new(); // make stake account let user_stake = Keypair::new(); + let lockup = stake::Lockup::default(); + let authorized = stake::Authorized { + staker: stake_pool_accounts.deposit_authority, + withdrawer: stake_pool_accounts.deposit_authority, + }; + create_independent_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake, + &authorized, + &lockup, + ) + .await; // make pool token account let user_pool_account = Keypair::new(); @@ -289,6 +347,20 @@ async fn test_stake_pool_deposit_with_wrong_token_program_id() { let user = Keypair::new(); // make stake account let user_stake = Keypair::new(); + let lockup = stake::Lockup::default(); + let authorized = stake::Authorized { + staker: stake_pool_accounts.deposit_authority, + withdrawer: stake_pool_accounts.deposit_authority, + }; + create_independent_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake, + &authorized, + &lockup, + ) + .await; // make pool token account let user_pool_account = Keypair::new(); @@ -351,6 +423,20 @@ async fn test_stake_pool_deposit_with_wrong_validator_stake_list_account() { let user = Keypair::new(); // make stake account let user_stake = Keypair::new(); + let lockup = stake::Lockup::default(); + let authorized = stake::Authorized { + staker: stake_pool_accounts.deposit_authority, + withdrawer: stake_pool_accounts.deposit_authority, + }; + create_independent_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake, + &authorized, + &lockup, + ) + .await; // make pool token account let user_pool_account = Keypair::new(); @@ -402,13 +488,13 @@ async fn test_stake_pool_deposit_where_stake_acc_not_in_stake_state() { .await .unwrap(); - let validator_stake_account = StakeAccount::new_with_target_authority( + let validator_stake_account = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); let user_stake_authority = Keypair::new(); - create_stake_account( + create_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -434,6 +520,20 @@ async fn test_stake_pool_deposit_where_stake_acc_not_in_stake_state() { .unwrap(); let user_stake_acc = Keypair::new(); + let lockup = stake::Lockup::default(); + let authorized = stake::Authorized { + staker: stake_pool_accounts.deposit_authority, + withdrawer: stake_pool_accounts.deposit_authority, + }; + create_independent_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake_acc, + &authorized, + &lockup, + ) + .await; let transaction_error = stake_pool_accounts .deposit_stake( &mut banks_client, @@ -470,7 +570,7 @@ async fn test_stake_pool_deposit_to_unknown_validator() { .await .unwrap(); - let validator_stake_account = StakeAccount::new_with_target_authority( + let validator_stake_account = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); @@ -548,6 +648,20 @@ async fn test_stake_pool_deposit_with_wrong_deposit_authority() { let user = Keypair::new(); // make stake account let user_stake = Keypair::new(); + let lockup = stake::Lockup::default(); + let authorized = stake::Authorized { + staker: stake_pool_accounts.deposit_authority, + withdrawer: stake_pool_accounts.deposit_authority, + }; + create_independent_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake, + &authorized, + &lockup, + ) + .await; // make pool token account let user_pool_account = Keypair::new(); @@ -602,6 +716,20 @@ async fn test_stake_pool_deposit_with_wrong_withdraw_authority() { let user = Keypair::new(); // make stake account let user_stake = Keypair::new(); + let lockup = stake::Lockup::default(); + let authorized = stake::Authorized { + staker: stake_pool_accounts.deposit_authority, + withdrawer: stake_pool_accounts.deposit_authority, + }; + create_independent_stake_account( + &mut banks_client, + &payer, + &recent_blockhash, + &user_stake, + &authorized, + &lockup, + ) + .await; // make pool token account let user_pool_account = Keypair::new(); diff --git a/stake-pool/program/tests/helpers/mod.rs b/stake-pool/program/tests/helpers/mod.rs index 18cca7f8..35863756 100644 --- a/stake-pool/program/tests/helpers/mod.rs +++ b/stake-pool/program/tests/helpers/mod.rs @@ -163,11 +163,11 @@ pub async fn create_stake_pool( validator_stake_list: &Keypair, pool_mint: &Pubkey, pool_token_account: &Pubkey, - owner: &Pubkey, + owner: &Keypair, fee: &instruction::Fee, ) -> Result<(), TransportError> { let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(state::State::LEN); + let rent_stake_pool = rent.minimum_balance(state::StakePool::LEN); let rent_validator_stake_list = rent.minimum_balance(state::ValidatorStakeList::LEN); let init_args = instruction::InitArgs { fee: *fee }; @@ -177,7 +177,7 @@ pub async fn create_stake_pool( &payer.pubkey(), &stake_pool.pubkey(), rent_stake_pool, - state::State::LEN as u64, + state::StakePool::LEN as u64, &id(), ), system_instruction::create_account( @@ -190,7 +190,7 @@ pub async fn create_stake_pool( instruction::initialize( &id(), &stake_pool.pubkey(), - owner, + &owner.pubkey(), &validator_stake_list.pubkey(), pool_mint, pool_token_account, @@ -202,7 +202,7 @@ pub async fn create_stake_pool( Some(&payer.pubkey()), ); transaction.sign( - &[payer, stake_pool, validator_stake_list], + &[payer, stake_pool, validator_stake_list, owner], *recent_blockhash, ); banks_client.process_transaction(transaction).await?; @@ -285,7 +285,7 @@ pub async fn create_blank_stake_account( lamports } -pub async fn create_stake_account( +pub async fn create_validator_stake_account( banks_client: &mut BanksClient, payer: &Keypair, recent_blockhash: &Hash, @@ -355,14 +355,14 @@ pub async fn authorize_stake_account( banks_client.process_transaction(transaction).await.unwrap(); } -pub struct StakeAccount { +pub struct ValidatorStakeAccount { pub stake_account: Pubkey, pub target_authority: Pubkey, pub vote: Keypair, pub stake_pool: Pubkey, } -impl StakeAccount { +impl ValidatorStakeAccount { pub fn new_with_target_authority(authority: &Pubkey, stake_pool: &Pubkey) -> Self { let validator = Keypair::new(); let (stake_account, _) = processor::Processor::find_stake_address_for_validator( @@ -370,7 +370,7 @@ impl StakeAccount { &validator.pubkey(), stake_pool, ); - StakeAccount { + ValidatorStakeAccount { stake_account, target_authority: *authority, vote: validator, @@ -386,7 +386,7 @@ impl StakeAccount { ) { // make stake account let user_stake_authority = Keypair::new(); - create_stake_account( + create_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -500,7 +500,7 @@ impl StakePoolAccounts { &self.validator_stake_list, &self.pool_mint.pubkey(), &self.pool_fee_account.pubkey(), - &self.owner.pubkey(), + &self.owner, &self.fee, ) .await?; @@ -638,8 +638,8 @@ pub async fn simple_add_validator_stake_account( payer: &Keypair, recent_blockhash: &Hash, stake_pool_accounts: &StakePoolAccounts, -) -> StakeAccount { - let user_stake = StakeAccount::new_with_target_authority( +) -> ValidatorStakeAccount { + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); @@ -685,7 +685,7 @@ pub async fn simple_deposit( payer: &Keypair, recent_blockhash: &Hash, stake_pool_accounts: &StakePoolAccounts, - validator_stake_account: &StakeAccount, + validator_stake_account: &ValidatorStakeAccount, ) -> DepositInfo { let user = Keypair::new(); // make stake account diff --git a/stake-pool/program/tests/initialize.rs b/stake-pool/program/tests/initialize.rs index c15f2f04..79bcaca3 100644 --- a/stake-pool/program/tests/initialize.rs +++ b/stake-pool/program/tests/initialize.rs @@ -3,13 +3,47 @@ mod helpers; use helpers::*; -use solana_program::system_instruction; +use solana_program::hash::Hash; +use solana_program::{ + instruction::{AccountMeta, Instruction}, + program_pack::Pack, + system_instruction, sysvar, +}; +use solana_program_test::BanksClient; use solana_sdk::{ instruction::InstructionError, signature::Keypair, signature::Signer, transaction::Transaction, transaction::TransactionError, transport::TransportError, }; use spl_stake_pool::*; +async fn create_mint_and_token_account( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + stake_pool_accounts: &StakePoolAccounts, +) { + create_mint( + banks_client, + payer, + recent_blockhash, + &stake_pool_accounts.pool_mint, + &stake_pool_accounts.withdraw_authority, + ) + .await + .unwrap(); + + create_token_account( + banks_client, + payer, + recent_blockhash, + &stake_pool_accounts.pool_fee_account, + &stake_pool_accounts.pool_mint.pubkey(), + &stake_pool_accounts.owner.pubkey(), + ) + .await + .unwrap(); +} + #[tokio::test] async fn test_stake_pool_initialize() { let (mut banks_client, payer, recent_blockhash) = program_test().start().await; @@ -21,7 +55,7 @@ async fn test_stake_pool_initialize() { // Stake pool now exists let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - assert_eq!(stake_pool.data.len(), state::State::LEN); + assert_eq!(stake_pool.data.len(), state::StakePool::LEN); assert_eq!(stake_pool.owner, id()); // Validator stake list storage initialized @@ -32,7 +66,7 @@ async fn test_stake_pool_initialize() { .await; let validator_stake_list = state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap(); - assert_eq!(validator_stake_list.is_initialized, true); + assert_eq!(validator_stake_list.is_initialized(), true); } #[tokio::test] @@ -129,27 +163,25 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() { let stake_pool_accounts = StakePoolAccounts::new(); let wrong_mint = Keypair::new(); + create_mint_and_token_account( + &mut banks_client, + &payer, + &recent_blockhash, + &stake_pool_accounts, + ) + .await; + + // create wrong mint create_mint( &mut banks_client, &payer, &recent_blockhash, - &stake_pool_accounts.pool_mint, + &wrong_mint, &stake_pool_accounts.withdraw_authority, ) .await .unwrap(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.owner.pubkey(), - ) - .await - .unwrap(); - let transaction_error = create_stake_pool( &mut banks_client, &payer, @@ -158,7 +190,7 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() { &stake_pool_accounts.validator_stake_list, &wrong_mint.pubkey(), &stake_pool_accounts.pool_fee_account.pubkey(), - &stake_pool_accounts.owner.pubkey(), + &stake_pool_accounts.owner, &stake_pool_accounts.fee, ) .await @@ -182,6 +214,8 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() { let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let stake_pool_accounts = StakePoolAccounts::new(); + let wrong_token_program = Keypair::new(); + create_mint( &mut banks_client, &payer, @@ -192,24 +226,30 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() { .await .unwrap(); - create_token_account( - &mut banks_client, - &payer, - &recent_blockhash, - &stake_pool_accounts.pool_fee_account, - &stake_pool_accounts.pool_mint.pubkey(), - &stake_pool_accounts.owner.pubkey(), - ) - .await - .unwrap(); - let rent = banks_client.get_rent().await.unwrap(); - let rent_stake_pool = rent.minimum_balance(state::State::LEN); + + let account_rent = rent.minimum_balance(spl_token::state::Account::LEN); + let mut transaction = Transaction::new_with_payer( + &[system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.pool_fee_account.pubkey(), + account_rent, + spl_token::state::Account::LEN as u64, + &wrong_token_program.pubkey(), + )], + Some(&payer.pubkey()), + ); + transaction.sign( + &[&payer, &stake_pool_accounts.pool_fee_account], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + let rent_stake_pool = rent.minimum_balance(state::StakePool::LEN); let rent_validator_stake_list = rent.minimum_balance(state::ValidatorStakeList::LEN); let init_args = instruction::InitArgs { fee: stake_pool_accounts.fee, }; - let wrong_token_program = Keypair::new(); let mut transaction = Transaction::new_with_payer( &[ @@ -217,7 +257,7 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() { &payer.pubkey(), &stake_pool_accounts.stake_pool.pubkey(), rent_stake_pool, - state::State::LEN as u64, + state::StakePool::LEN as u64, &id(), ), system_instruction::create_account( @@ -246,6 +286,7 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() { &payer, &stake_pool_accounts.stake_pool, &stake_pool_accounts.validator_stake_list, + &stake_pool_accounts.owner, ], recent_blockhash, ); @@ -264,3 +305,333 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() { ), } } + +#[tokio::test] +async fn test_initialize_stake_pool_with_wrong_fee_accounts_owner() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + let stake_pool_accounts = StakePoolAccounts::new(); + + create_mint( + &mut banks_client, + &payer, + &recent_blockhash, + &stake_pool_accounts.pool_mint, + &stake_pool_accounts.withdraw_authority, + ) + .await + .unwrap(); + let rent = banks_client.get_rent().await.unwrap(); + let account_rent = rent.minimum_balance(spl_token::state::Account::LEN); + + let mut transaction = Transaction::new_with_payer( + &[system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.pool_fee_account.pubkey(), + account_rent, + spl_token::state::Account::LEN as u64, + &Keypair::new().pubkey(), + )], + Some(&payer.pubkey()), + ); + transaction.sign( + &[&payer, &stake_pool_accounts.pool_fee_account], + recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + + let transaction_error = create_stake_pool( + &mut banks_client, + &payer, + &recent_blockhash, + &stake_pool_accounts.stake_pool, + &stake_pool_accounts.validator_stake_list, + &stake_pool_accounts.pool_mint.pubkey(), + &stake_pool_accounts.pool_fee_account.pubkey(), + &stake_pool_accounts.owner, + &stake_pool_accounts.fee, + ) + .await + .err() + .unwrap(); + + match transaction_error { + TransportError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(error_index), + )) => { + let program_error = error::StakePoolError::InvalidFeeAccount as u32; + assert_eq!(error_index, program_error); + } + _ => panic!( + "Wrong error occurs while try to initialize stake pool with wrong fee account's owner" + ), + } +} + +#[tokio::test] +async fn test_initialize_stake_pool_with_wrong_withdraw_authority() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + let mut stake_pool_accounts = StakePoolAccounts::new(); + + stake_pool_accounts.withdraw_authority = Keypair::new().pubkey(); + + let transaction_error = stake_pool_accounts + .initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash) + .await + .err() + .unwrap(); + + match transaction_error { + TransportError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(error_index), + )) => { + let program_error = error::StakePoolError::WrongMintingAuthority as u32; + assert_eq!(error_index, program_error); + } + _ => panic!( + "Wrong error occurs while try to initialize stake pool with wrong withdraw authority" + ), + } +} + +#[tokio::test] +async fn test_initialize_stake_pool_with_not_rent_exempt_pool() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + let stake_pool_accounts = StakePoolAccounts::new(); + + create_mint_and_token_account( + &mut banks_client, + &payer, + &recent_blockhash, + &stake_pool_accounts, + ) + .await; + + let rent = banks_client.get_rent().await.unwrap(); + let rent_validator_stake_list = rent.minimum_balance(state::ValidatorStakeList::LEN); + let init_args = instruction::InitArgs { + fee: stake_pool_accounts.fee, + }; + + let mut transaction = Transaction::new_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.stake_pool.pubkey(), + 1, + state::StakePool::LEN as u64, + &id(), + ), + system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.validator_stake_list.pubkey(), + rent_validator_stake_list, + state::ValidatorStakeList::LEN as u64, + &id(), + ), + instruction::initialize( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.owner.pubkey(), + &stake_pool_accounts.validator_stake_list.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &stake_pool_accounts.pool_fee_account.pubkey(), + &spl_token::id(), + init_args, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + ); + transaction.sign( + &[ + &payer, + &stake_pool_accounts.stake_pool, + &stake_pool_accounts.validator_stake_list, + &stake_pool_accounts.owner, + ], + recent_blockhash, + ); + let transaction_error = banks_client + .process_transaction(transaction) + .await + .err() + .unwrap(); + + match transaction_error { + TransportError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(error_index), + )) => { + let program_error = error::StakePoolError::AccountNotRentExempt as u32; + assert_eq!(error_index, program_error); + } + _ => panic!( + "Wrong error occurs while try to initialize stake pool with not rent exempt stake pool account" + ), + } +} + +#[tokio::test] +async fn test_initialize_stake_pool_with_not_rent_exempt_validator_stake_list() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + let stake_pool_accounts = StakePoolAccounts::new(); + + create_mint_and_token_account( + &mut banks_client, + &payer, + &recent_blockhash, + &stake_pool_accounts, + ) + .await; + + let rent = banks_client.get_rent().await.unwrap(); + let rent_stake_pool = rent.minimum_balance(state::StakePool::LEN); + let init_args = instruction::InitArgs { + fee: stake_pool_accounts.fee, + }; + + let mut transaction = Transaction::new_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.stake_pool.pubkey(), + rent_stake_pool, + state::StakePool::LEN as u64, + &id(), + ), + system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.validator_stake_list.pubkey(), + 1, + state::ValidatorStakeList::LEN as u64, + &id(), + ), + instruction::initialize( + &id(), + &stake_pool_accounts.stake_pool.pubkey(), + &stake_pool_accounts.owner.pubkey(), + &stake_pool_accounts.validator_stake_list.pubkey(), + &stake_pool_accounts.pool_mint.pubkey(), + &stake_pool_accounts.pool_fee_account.pubkey(), + &spl_token::id(), + init_args, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + ); + transaction.sign( + &[ + &payer, + &stake_pool_accounts.stake_pool, + &stake_pool_accounts.validator_stake_list, + &stake_pool_accounts.owner, + ], + recent_blockhash, + ); + let transaction_error = banks_client + .process_transaction(transaction) + .await + .err() + .unwrap(); + + match transaction_error { + TransportError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(error_index), + )) => { + let program_error = error::StakePoolError::AccountNotRentExempt as u32; + assert_eq!(error_index, program_error); + } + _ => panic!( + "Wrong error occurs while try to initialize stake pool with not rent exempt validator stake list account" + ), + } +} + +#[tokio::test] +async fn test_initialize_stake_pool_without_owner_signature() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + let stake_pool_accounts = StakePoolAccounts::new(); + + create_mint_and_token_account( + &mut banks_client, + &payer, + &recent_blockhash, + &stake_pool_accounts, + ) + .await; + + let rent = banks_client.get_rent().await.unwrap(); + let rent_stake_pool = rent.minimum_balance(state::StakePool::LEN); + let init_args = instruction::InitArgs { + fee: stake_pool_accounts.fee, + }; + + let init_data = instruction::StakePoolInstruction::Initialize(init_args); + let data = init_data.serialize().unwrap(); + let accounts = vec![ + AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), true), + AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false), + AccountMeta::new(stake_pool_accounts.validator_stake_list.pubkey(), false), + AccountMeta::new_readonly(stake_pool_accounts.pool_mint.pubkey(), false), + AccountMeta::new_readonly(stake_pool_accounts.pool_fee_account.pubkey(), false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(spl_token::id(), false), + ]; + let stake_pool_init_instruction = Instruction { + program_id: id(), + accounts, + data, + }; + + let mut transaction = Transaction::new_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.stake_pool.pubkey(), + rent_stake_pool, + state::StakePool::LEN as u64, + &id(), + ), + system_instruction::create_account( + &payer.pubkey(), + &stake_pool_accounts.validator_stake_list.pubkey(), + state::ValidatorStakeList::LEN as u64, + state::ValidatorStakeList::LEN as u64, + &id(), + ), + stake_pool_init_instruction, + ], + Some(&payer.pubkey()), + ); + transaction.sign( + &[ + &payer, + &stake_pool_accounts.stake_pool, + &stake_pool_accounts.validator_stake_list, + ], + recent_blockhash, + ); + let transaction_error = banks_client + .process_transaction(transaction) + .await + .err() + .unwrap(); + + match transaction_error { + TransportError::TransactionError(TransactionError::InstructionError( + _, + InstructionError::Custom(error_index), + )) => { + let program_error = error::StakePoolError::SignatureMissing as u32; + assert_eq!(error_index, program_error); + } + _ => panic!( + "Wrong error occurs while try to initialize stake pool without owner's signature" + ), + } +} diff --git a/stake-pool/program/tests/set_owner.rs b/stake-pool/program/tests/set_owner.rs index 8c2321b8..f0897e23 100644 --- a/stake-pool/program/tests/set_owner.rs +++ b/stake-pool/program/tests/set_owner.rs @@ -71,10 +71,7 @@ async fn test_set_owner() { banks_client.process_transaction(transaction).await.unwrap(); let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = state::State::deserialize(&stake_pool.data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let stake_pool = state::StakePool::deserialize(&stake_pool.data.as_slice()).unwrap(); assert_eq!(stake_pool.owner, new_owner.pubkey()); } diff --git a/stake-pool/program/tests/set_staking_authority.rs b/stake-pool/program/tests/set_staking_authority.rs index 4972e506..ab45a88e 100644 --- a/stake-pool/program/tests/set_staking_authority.rs +++ b/stake-pool/program/tests/set_staking_authority.rs @@ -15,7 +15,13 @@ use solana_sdk::{ }; use spl_stake_pool::*; -async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, StakeAccount) { +async fn setup() -> ( + BanksClient, + Keypair, + Hash, + StakePoolAccounts, + ValidatorStakeAccount, +) { let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let stake_pool_accounts = StakePoolAccounts::new(); stake_pool_accounts @@ -25,7 +31,7 @@ async fn setup() -> (BanksClient, Keypair, Hash, StakePoolAccounts, StakeAccount let user = Keypair::new(); - let user_stake = StakeAccount::new_with_target_authority( + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); diff --git a/stake-pool/program/tests/update_list_balance.rs b/stake-pool/program/tests/update_list_balance.rs index f834ed64..a9a22f31 100644 --- a/stake-pool/program/tests/update_list_balance.rs +++ b/stake-pool/program/tests/update_list_balance.rs @@ -35,7 +35,7 @@ async fn test_update_list_balance() { .unwrap(); // Add several accounts - let mut stake_accounts: Vec = vec![]; + let mut stake_accounts: Vec = vec![]; const STAKE_ACCOUNTS: u64 = 3; for _ in 0..STAKE_ACCOUNTS { stake_accounts.push( diff --git a/stake-pool/program/tests/vsa_add.rs b/stake-pool/program/tests/vsa_add.rs index da933cb1..bd86a6ca 100644 --- a/stake-pool/program/tests/vsa_add.rs +++ b/stake-pool/program/tests/vsa_add.rs @@ -24,7 +24,7 @@ async fn setup() -> ( Keypair, Hash, StakePoolAccounts, - StakeAccount, + ValidatorStakeAccount, Keypair, ) { let (mut banks_client, payer, recent_blockhash) = program_test().start().await; @@ -36,7 +36,7 @@ async fn setup() -> ( let user = Keypair::new(); - let user_stake = StakeAccount::new_with_target_authority( + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); @@ -117,7 +117,7 @@ async fn test_add_validator_stake_account() { assert_eq!( validator_stake_list, state::ValidatorStakeList { - is_initialized: true, + version: state::ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION, validators: vec![state::ValidatorStakeInfo { validator_account: user_stake.vote.pubkey(), last_update_epoch: 0, @@ -401,6 +401,7 @@ async fn test_not_owner_try_to_add_validator_stake_account_without_signature() { AccountMeta::new(user_pool_account.pubkey(), false), AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false), AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(sysvar::stake_history::id(), false), AccountMeta::new_readonly(spl_token::id(), false), AccountMeta::new_readonly(stake::id(), false), ]; @@ -443,12 +444,12 @@ async fn test_add_validator_stake_account_when_stake_acc_not_in_stake_state() { let user = Keypair::new(); - let user_stake = StakeAccount::new_with_target_authority( + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); let user_stake_authority = Keypair::new(); - create_stake_account( + create_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, diff --git a/stake-pool/program/tests/vsa_remove.rs b/stake-pool/program/tests/vsa_remove.rs index 99aaeee1..61aa3843 100644 --- a/stake-pool/program/tests/vsa_remove.rs +++ b/stake-pool/program/tests/vsa_remove.rs @@ -24,7 +24,7 @@ async fn setup() -> ( Keypair, Hash, StakePoolAccounts, - StakeAccount, + ValidatorStakeAccount, Keypair, Keypair, ) { @@ -37,7 +37,7 @@ async fn setup() -> ( let user = Keypair::new(); - let user_stake = StakeAccount::new_with_target_authority( + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); @@ -132,7 +132,7 @@ async fn test_remove_validator_stake_account() { assert_eq!( validator_stake_list, state::ValidatorStakeList { - is_initialized: true, + version: state::ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION, validators: vec![] } ); @@ -530,12 +530,12 @@ async fn test_remove_validator_stake_account_when_stake_acc_not_in_stake_state() let user = Keypair::new(); - let user_stake = StakeAccount::new_with_target_authority( + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); let user_stake_authority = Keypair::new(); - create_stake_account( + create_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, diff --git a/stake-pool/program/tests/withdraw.rs b/stake-pool/program/tests/withdraw.rs index ed805da1..17cc7274 100644 --- a/stake-pool/program/tests/withdraw.rs +++ b/stake-pool/program/tests/withdraw.rs @@ -22,10 +22,9 @@ async fn setup() -> ( Keypair, Hash, StakePoolAccounts, - StakeAccount, + ValidatorStakeAccount, DepositInfo, u64, - u64, ) { let (mut banks_client, payer, recent_blockhash) = program_test().start().await; let stake_pool_accounts = StakePoolAccounts::new(); @@ -34,7 +33,7 @@ async fn setup() -> ( .await .unwrap(); - let validator_stake_account: StakeAccount = simple_add_validator_stake_account( + let validator_stake_account: ValidatorStakeAccount = simple_add_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -52,7 +51,6 @@ async fn setup() -> ( .await; let tokens_to_burn = deposit_info.pool_tokens / 4; - let lamports_to_withdraw = tokens_to_burn; // For now math is 1:1 // Delegate tokens for burning delegate_tokens( @@ -74,7 +72,6 @@ async fn setup() -> ( validator_stake_account, deposit_info, tokens_to_burn, - lamports_to_withdraw, ) } @@ -88,7 +85,6 @@ async fn test_stake_pool_withdraw() { validator_stake_account, deposit_info, tokens_to_burn, - lamports_to_withdraw, ) = setup().await; // Create stake account to withdraw to @@ -104,10 +100,8 @@ async fn test_stake_pool_withdraw() { // Save stake pool state before withdrawal let stake_pool_before = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool_before = state::State::deserialize(&stake_pool_before.data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let stake_pool_before = + state::StakePool::deserialize(&stake_pool_before.data.as_slice()).unwrap(); // Save validator stake account record before withdrawal let validator_stake_list = get_account( @@ -135,20 +129,17 @@ async fn test_stake_pool_withdraw() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .unwrap(); // Check pool stats let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await; - let stake_pool = state::State::deserialize(&stake_pool.data.as_slice()) - .unwrap() - .stake_pool() - .unwrap(); + let stake_pool = state::StakePool::deserialize(&stake_pool.data.as_slice()).unwrap(); assert_eq!( stake_pool.stake_total, - stake_pool_before.stake_total - lamports_to_withdraw + stake_pool_before.stake_total - tokens_to_burn ); assert_eq!( stake_pool.pool_total, @@ -168,7 +159,7 @@ async fn test_stake_pool_withdraw() { .unwrap(); assert_eq!( validator_stake_item.balance, - validator_stake_item_before.balance - lamports_to_withdraw + validator_stake_item_before.balance - tokens_to_burn ); // Check tokens burned @@ -192,7 +183,7 @@ async fn test_stake_pool_withdraw() { get_account(&mut banks_client, &user_stake_recipient.pubkey()).await; assert_eq!( user_stake_recipient_account.lamports, - initial_stake_lamports + lamports_to_withdraw + initial_stake_lamports + tokens_to_burn ); } @@ -205,8 +196,7 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() { stake_pool_accounts, validator_stake_account, deposit_info, - _, - lamports_to_withdraw, + tokens_to_burn, ) = setup().await; // Create stake account to withdraw to @@ -228,7 +218,7 @@ async fn test_stake_pool_withdraw_with_wrong_stake_program() { &stake_pool_accounts.pool_mint.pubkey(), &spl_token::id(), &wrong_stake_program.pubkey(), - lamports_to_withdraw, + tokens_to_burn, ) .unwrap()], Some(&payer.pubkey()), @@ -257,8 +247,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() { mut stake_pool_accounts, validator_stake_account, deposit_info, - _, - lamports_to_withdraw, + tokens_to_burn, ) = setup().await; // Create stake account to withdraw to @@ -276,7 +265,7 @@ async fn test_stake_pool_withdraw_with_wrong_withdraw_authority() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err() @@ -303,8 +292,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() { stake_pool_accounts, validator_stake_account, deposit_info, - _, - lamports_to_withdraw, + tokens_to_burn, ) = setup().await; // Create stake account to withdraw to @@ -326,7 +314,7 @@ async fn test_stake_pool_withdraw_with_wrong_token_program_id() { &stake_pool_accounts.pool_mint.pubkey(), &wrong_token_program.pubkey(), &stake::id(), - lamports_to_withdraw, + tokens_to_burn, ) .unwrap()], Some(&payer.pubkey()), @@ -355,8 +343,7 @@ async fn test_stake_pool_withdraw_with_wrong_validator_stake_list() { mut stake_pool_accounts, validator_stake_account, deposit_info, - _, - lamports_to_withdraw, + tokens_to_burn, ) = setup().await; // Create stake account to withdraw to @@ -374,7 +361,7 @@ async fn test_stake_pool_withdraw_with_wrong_validator_stake_list() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err() @@ -403,13 +390,13 @@ async fn test_stake_pool_withdraw_when_stake_acc_not_in_stake_state() { .await .unwrap(); - let validator_stake_account = StakeAccount::new_with_target_authority( + let validator_stake_account = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); let user_stake_authority = Keypair::new(); - create_stake_account( + create_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -455,7 +442,6 @@ async fn test_stake_pool_withdraw_when_stake_acc_not_in_stake_state() { let pool_tokens = get_token_balance(&mut banks_client, &user_pool_account).await; let tokens_to_burn = pool_tokens / 4; - let lamports_to_withdraw = tokens_to_burn; // For now math is 1:1 // Delegate tokens for burning delegate_tokens( @@ -483,7 +469,7 @@ async fn test_stake_pool_withdraw_when_stake_acc_not_in_stake_state() { &user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err() @@ -510,7 +496,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() { .await .unwrap(); - let validator_stake_account = StakeAccount::new_with_target_authority( + let validator_stake_account = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); @@ -518,7 +504,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() { .create_and_delegate(&mut banks_client, &payer, &recent_blockhash) .await; - let user_stake = StakeAccount::new_with_target_authority( + let user_stake = ValidatorStakeAccount::new_with_target_authority( &stake_pool_accounts.deposit_authority, &stake_pool_accounts.stake_pool.pubkey(), ); @@ -573,7 +559,6 @@ async fn test_stake_pool_withdraw_from_unknown_validator() { let pool_tokens = get_token_balance(&mut banks_client, &user_pool_account).await; let tokens_to_burn = pool_tokens / 4; - let lamports_to_withdraw = tokens_to_burn; // For now math is 1:1 // Delegate tokens for burning delegate_tokens( @@ -601,7 +586,7 @@ async fn test_stake_pool_withdraw_from_unknown_validator() { &user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err() @@ -628,8 +613,7 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() { stake_pool_accounts, validator_stake_account, deposit_info, - _, - lamports_to_withdraw, + tokens_to_burn, ) = setup().await; // Create stake account to withdraw to @@ -652,7 +636,7 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .unwrap(); @@ -668,7 +652,7 @@ async fn test_stake_pool_double_withdraw_to_the_same_account() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err() @@ -691,7 +675,7 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() { .await .unwrap(); - let validator_stake_account: StakeAccount = simple_add_validator_stake_account( + let validator_stake_account: ValidatorStakeAccount = simple_add_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -709,7 +693,6 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() { .await; let tokens_to_burn = deposit_info.pool_tokens / 4; - let lamports_to_withdraw = tokens_to_burn; // For now math is 1:1 // Create stake account to withdraw to let user_stake_recipient = Keypair::new(); @@ -731,7 +714,7 @@ async fn test_stake_pool_withdraw_token_delegate_was_not_setup() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err() @@ -760,7 +743,7 @@ async fn test_stake_pool_withdraw_with_low_delegation() { .await .unwrap(); - let validator_stake_account: StakeAccount = simple_add_validator_stake_account( + let validator_stake_account: ValidatorStakeAccount = simple_add_validator_stake_account( &mut banks_client, &payer, &recent_blockhash, @@ -778,7 +761,6 @@ async fn test_stake_pool_withdraw_with_low_delegation() { .await; let tokens_to_burn = deposit_info.pool_tokens / 4; - let lamports_to_withdraw = tokens_to_burn; // For now math is 1:1 // Delegate tokens for burning delegate_tokens( @@ -812,7 +794,7 @@ async fn test_stake_pool_withdraw_with_low_delegation() { &deposit_info.user_pool_account, &validator_stake_account.stake_account, &new_authority, - lamports_to_withdraw, + tokens_to_burn, ) .await .err()