stake-pool: Add borsh support and specify size on creation (#1505)

* lending: Update JS tests to solana-test-validator

* Add solana tools install

* Fix oopsie on the path

* Move where deployed programs go

* stake-pool: Add borsh support and size on creation

We can't specify the size in the instruction unfortunately, since we'd
only have 10kb max for the validator list. At roughly 50 bytes per
validator, that only gives us 200 validators.

On the flip side, using Borsh means we can allow the validator stake list
to be any size!

* Add AccountType enum

* Remove V1 everywhere

* Add max validators as parameter and get_instance_packed_len

* Add test for adding too many validators

* Clippy
This commit is contained in:
Jon Cinque 2021-03-27 13:42:29 +01:00 committed by GitHub
parent 34e66c6bad
commit d815ba9b8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 749 additions and 600 deletions

3
Cargo.lock generated
View File

@ -3856,9 +3856,11 @@ version = "0.1.0"
dependencies = [
"arrayref",
"bincode",
"borsh 0.8.2",
"num-derive",
"num-traits",
"num_enum",
"proptest",
"serde",
"serde_derive",
"solana-program",
@ -3875,6 +3877,7 @@ name = "spl-stake-pool-cli"
version = "2.0.1"
dependencies = [
"bincode",
"borsh 0.8.2",
"bs58 0.4.0",
"clap",
"lazy_static",

View File

@ -9,6 +9,7 @@ repository = "https://github.com/solana-labs/solana-program-library"
version = "2.0.1"
[dependencies]
borsh = "0.8"
clap = "2.33.3"
serde_json = "1.0.62"
solana-account-decoder = "1.6.1"

View File

@ -1,52 +1,58 @@
#[macro_use]
extern crate lazy_static;
use bincode::deserialize;
use clap::{
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings, Arg,
ArgGroup, SubCommand,
};
use solana_account_decoder::UiAccountEncoding;
use solana_clap_utils::{
input_parsers::pubkey_of,
input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_url},
keypair::signer_from_path,
};
use solana_client::{
rpc_client::RpcClient,
rpc_config::RpcAccountInfoConfig,
rpc_config::RpcProgramAccountsConfig,
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
};
use solana_program::{instruction::Instruction, program_pack::Pack, pubkey::Pubkey};
use solana_sdk::{
account::Account,
commitment_config::CommitmentConfig,
native_token::{self, Sol},
signature::{Keypair, Signer},
system_instruction,
transaction::Transaction,
};
use spl_stake_pool::{
instruction::{
add_validator_stake_account, create_validator_stake_account, deposit,
initialize as initialize_pool, remove_validator_stake_account, set_owner,
update_list_balance, update_pool_balance, withdraw, Fee as PoolFee,
InitArgs as PoolInitArgs,
use {
bincode::deserialize,
borsh::BorshDeserialize,
clap::{
crate_description, crate_name, crate_version, value_t, value_t_or_exit, App, AppSettings,
Arg, ArgGroup, SubCommand,
},
processor::Processor as PoolProcessor,
stake::authorize as authorize_stake,
stake::id as stake_program_id,
stake::StakeAuthorize,
stake::StakeState,
state::StakePool,
state::ValidatorStakeList,
solana_account_decoder::UiAccountEncoding,
solana_clap_utils::{
input_parsers::pubkey_of,
input_validators::{is_amount, is_keypair, is_parsable, is_pubkey, is_url},
keypair::signer_from_path,
},
solana_client::{
rpc_client::RpcClient,
rpc_config::RpcAccountInfoConfig,
rpc_config::RpcProgramAccountsConfig,
rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType},
},
solana_program::{
borsh::get_packed_len, instruction::Instruction, program_pack::Pack, pubkey::Pubkey,
},
solana_sdk::{
account::Account,
commitment_config::CommitmentConfig,
native_token::{self, Sol},
signature::{Keypair, Signer},
system_instruction,
transaction::Transaction,
},
spl_stake_pool::{
borsh::{get_instance_packed_len, try_from_slice_unchecked},
instruction::{
add_validator_stake_account, create_validator_stake_account, deposit,
initialize as initialize_pool, remove_validator_stake_account, set_owner,
update_list_balance, update_pool_balance, withdraw, Fee as PoolFee,
},
processor::Processor as PoolProcessor,
stake::authorize as authorize_stake,
stake::id as stake_program_id,
stake::StakeAuthorize,
stake::StakeState,
state::StakePool,
state::ValidatorStakeList,
},
spl_token::{
self, instruction::approve as approve_token, instruction::initialize_account,
instruction::initialize_mint, native_mint, state::Account as TokenAccount,
state::Mint as TokenMint,
},
std::process::exit,
};
use spl_token::{
self, instruction::approve as approve_token, instruction::initialize_account,
instruction::initialize_mint, native_mint, state::Account as TokenAccount,
state::Mint as TokenMint,
};
use std::process::exit;
struct Config {
rpc_client: RpcClient,
@ -109,7 +115,7 @@ fn get_authority_accounts(config: &Config, authority: &Pubkey) -> Vec<(Pubkey, A
.unwrap()
}
fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
fn command_create_pool(config: &Config, fee: PoolFee, max_validators: u32) -> CommandResult {
let mint_account = Keypair::new();
println!("Creating mint {}", mint_account.pubkey());
@ -132,10 +138,12 @@ 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(StakePool::LEN)?;
.get_minimum_balance_for_rent_exemption(get_packed_len::<StakePool>())?;
let empty_validator_list = ValidatorStakeList::new_with_max_validators(max_validators);
let validator_stake_list_size = get_instance_packed_len(&empty_validator_list)?;
let validator_stake_list_balance = config
.rpc_client
.get_minimum_balance_for_rent_exemption(ValidatorStakeList::LEN)?;
.get_minimum_balance_for_rent_exemption(validator_stake_list_size)?;
let total_rent_free_balances = mint_account_balance
+ pool_fee_account_balance
+ pool_account_balance
@ -177,7 +185,7 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
&config.fee_payer.pubkey(),
&pool_account.pubkey(),
pool_account_balance,
StakePool::LEN as u64,
get_packed_len::<StakePool>() as u64,
&spl_stake_pool::id(),
),
// Validator stake account list storage
@ -185,7 +193,7 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
&config.fee_payer.pubkey(),
&validator_stake_list.pubkey(),
validator_stake_list_balance,
ValidatorStakeList::LEN as u64,
validator_stake_list_size as u64,
&spl_stake_pool::id(),
),
// Initialize pool token mint account
@ -212,7 +220,8 @@ fn command_create_pool(config: &Config, fee: PoolFee) -> CommandResult {
&mint_account.pubkey(),
&pool_fee_account.pubkey(),
&spl_token::id(),
PoolInitArgs { fee },
fee,
max_validators,
)?,
],
Some(&config.fee_payer.pubkey()),
@ -274,7 +283,7 @@ fn command_vsa_add(
) -> CommandResult {
// Get stake pool state
let pool_data = config.rpc_client.get_account_data(&pool)?;
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
let mut total_rent_free_balances: u64 = 0;
@ -365,7 +374,7 @@ fn command_vsa_remove(
) -> CommandResult {
// Get stake pool state
let pool_data = config.rpc_client.get_account_data(&pool)?;
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
let pool_data: StakePool = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
let pool_withdraw_authority: Pubkey = PoolProcessor::authority_id(
&spl_stake_pool::id(),
@ -495,7 +504,7 @@ fn command_deposit(
) -> CommandResult {
// Get stake pool state
let pool_data = config.rpc_client.get_account_data(&pool)?;
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
let pool_data: StakePool = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
// Get stake account data
let stake_data = config.rpc_client.get_account_data(&stake)?;
@ -514,7 +523,7 @@ fn command_deposit(
.rpc_client
.get_account_data(&pool_data.validator_stake_list)?;
let validator_stake_list_data =
ValidatorStakeList::deserialize(&validator_stake_list_data.as_slice())?;
try_from_slice_unchecked::<ValidatorStakeList>(&validator_stake_list_data.as_slice())?;
if !validator_stake_list_data.contains(&validator) {
return Err("Stake account for this validator does not exist in the pool.".into());
}
@ -617,14 +626,14 @@ 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::deserialize(pool_data.as_slice()).unwrap();
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
if config.verbose {
let validator_list = config
.rpc_client
.get_account_data(&pool_data.validator_stake_list)?;
let validator_stake_list_data =
ValidatorStakeList::deserialize(&validator_list.as_slice())?;
try_from_slice_unchecked::<ValidatorStakeList>(&validator_list.as_slice())?;
println!("Current validator list");
for validator in validator_stake_list_data.validators {
println!(
@ -669,12 +678,12 @@ 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::deserialize(pool_data.as_slice()).unwrap();
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
let validator_stake_list_data = config
.rpc_client
.get_account_data(&pool_data.validator_stake_list)?;
let validator_stake_list_data =
ValidatorStakeList::deserialize(&validator_stake_list_data.as_slice())?;
try_from_slice_unchecked::<ValidatorStakeList>(&validator_stake_list_data.as_slice())?;
let epoch_info = config.rpc_client.get_epoch_info()?;
@ -815,7 +824,7 @@ fn command_withdraw(
) -> CommandResult {
// Get stake pool state
let pool_data = config.rpc_client.get_account_data(&pool)?;
let pool_data = StakePool::deserialize(pool_data.as_slice()).unwrap();
let pool_data = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
let pool_mint_data = config.rpc_client.get_account_data(&pool_data.pool_mint)?;
let pool_mint = TokenMint::unpack_from_slice(pool_mint_data.as_slice()).unwrap();
@ -954,7 +963,7 @@ fn command_set_owner(
new_fee_receiver: &Option<Pubkey>,
) -> CommandResult {
let pool_data = config.rpc_client.get_account_data(&pool)?;
let pool_data: StakePool = StakePool::deserialize(pool_data.as_slice()).unwrap();
let pool_data: StakePool = StakePool::try_from_slice(pool_data.as_slice()).unwrap();
// If new accounts are missing in the arguments use the old ones
let new_owner: Pubkey = match new_owner {
@ -1088,6 +1097,16 @@ fn main() {
.required(true)
.help("Fee denominator, fee amount is numerator divided by denominator."),
)
.arg(
Arg::with_name("max_validators")
.long("max-validators")
.short("m")
.validator(is_parsable::<u32>)
.value_name("NUMBER")
.takes_value(true)
.required(true)
.help("Max number of validators included in the stake pool"),
)
)
.subcommand(SubCommand::with_name("create-validator-stake").about("Create a new validator stake account to use with the pool")
.arg(
@ -1344,12 +1363,14 @@ fn main() {
("create-pool", Some(arg_matches)) => {
let numerator = value_t_or_exit!(arg_matches, "fee_numerator", u64);
let denominator = value_t_or_exit!(arg_matches, "fee_denominator", u64);
let max_validators = value_t_or_exit!(arg_matches, "max_validators", u32);
command_create_pool(
&config,
PoolFee {
denominator,
numerator,
},
max_validators,
)
}
("create-validator-stake", Some(arg_matches)) => {

View File

@ -13,6 +13,7 @@ test-bpf = []
[dependencies]
arrayref = "0.3.6"
borsh = "0.8"
num-derive = "0.3"
num-traits = "0.2"
num_enum = "0.5.1"
@ -25,9 +26,10 @@ thiserror = "1.0"
bincode = "1.3.1"
[dev-dependencies]
proptest = "0.10"
solana-program-test = "1.6.1"
solana-sdk = "1.6.1"
solana-vote-program = "1.5.11"
solana-vote-program = "1.6.1"
[lib]
crate-type = ["cdylib", "lib"]

View File

@ -0,0 +1,39 @@
//! Extra borsh utils
//! TODO delete once try_from_slice_unchecked has been published
use {
borsh::{maybestd::io::Error, BorshDeserialize, BorshSerialize},
std::io::{Result as IoResult, Write},
};
/// Deserializes something and allows for incomplete reading
pub fn try_from_slice_unchecked<T: BorshDeserialize>(data: &[u8]) -> Result<T, Error> {
let mut data_mut = data;
let result = T::deserialize(&mut data_mut)?;
Ok(result)
}
/// Helper struct which to count how much data would be written during serialization
#[derive(Default)]
struct WriteCounter {
count: usize,
}
impl Write for WriteCounter {
fn write(&mut self, data: &[u8]) -> IoResult<usize> {
let amount = data.len();
self.count += amount;
Ok(amount)
}
fn flush(&mut self) -> IoResult<()> {
Ok(())
}
}
/// Get the worst-case packed length for the given BorshSchema
pub fn get_instance_packed_len<T: BorshSerialize>(instance: &T) -> Result<usize, Error> {
let mut counter = WriteCounter::default();
instance.serialize(&mut counter)?;
Ok(counter.count)
}

View File

@ -37,8 +37,6 @@ pub enum StakePoolError {
/// Invalid validator stake list account.
#[error("InvalidValidatorStakeList")]
InvalidValidatorStakeList,
// 10.
/// Invalid owner fee account.
#[error("InvalidFeeAccount")]
InvalidFeeAccount,
@ -56,8 +54,6 @@ pub enum StakePoolError {
/// Stake account voting for this validator already exists in the pool.
#[error("ValidatorAlreadyAdded")]
ValidatorAlreadyAdded,
// 15.
/// Stake account for this validator not found in the pool.
#[error("ValidatorNotFound")]
ValidatorNotFound,
@ -75,16 +71,14 @@ pub enum StakePoolError {
/// Validator stake account is not found in the list storage.
#[error("UnknownValidatorStakeAccount")]
UnknownValidatorStakeAccount,
// 20.
/// Wrong minting authority set for mint pool account
#[error("WrongMintingAuthority")]
WrongMintingAuthority,
// 20.
/// Account is not rent-exempt
#[error("AccountNotRentExempt")]
AccountNotRentExempt,
/// The size of the given validator stake list does match the expected amount
#[error("UnexpectedValidatorListAccountSize")]
UnexpectedValidatorListAccountSize,
}
impl From<StakePoolError> for ProgramError {
fn from(e: StakePoolError) -> Self {

View File

@ -3,19 +3,18 @@
#![allow(clippy::too_many_arguments)]
use {
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
solana_program::{
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
sysvar,
},
std::mem::size_of,
};
/// Fee rate as a ratio
/// Fee is minted on deposit
/// Fee rate as a ratio, minted on deposit
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
pub struct Fee {
/// denominator of the fee ratio
pub denominator: u64,
@ -23,17 +22,9 @@ pub struct Fee {
pub numerator: u64,
}
/// Inital values for the Stake Pool
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct InitArgs {
/// Fee paid to the owner in pool tokens
pub fee: Fee,
}
/// Instructions supported by the StakePool program.
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, BorshSerialize, BorshDeserialize, BorshSchema)]
pub enum StakePoolInstruction {
/// Initializes a new StakePool.
///
@ -45,7 +36,14 @@ pub enum StakePoolInstruction {
/// 5. `[]` Clock sysvar
/// 6. `[]` Rent sysvar
/// 7. `[]` Token program id
Initialize(InitArgs),
Initialize {
/// Deposit fee assessed
#[allow(dead_code)] // but it's not
fee: Fee,
/// Maximum expected number of validators
#[allow(dead_code)] // but it's not
max_validators: u32,
},
/// Creates new program account for accumulating stakes for a particular validator
///
@ -149,86 +147,6 @@ pub enum StakePoolInstruction {
SetOwner,
}
impl StakePoolInstruction {
/// Deserializes a byte buffer into an [StakePoolInstruction](enum.StakePoolInstruction.html).
/// TODO efficient unpacking here
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
if input.len() < size_of::<u8>() {
return Err(ProgramError::InvalidAccountData);
}
Ok(match input[0] {
0 => {
let val: &InitArgs = unpack(input)?;
Self::Initialize(*val)
}
1 => Self::CreateValidatorStakeAccount,
2 => Self::AddValidatorStakeAccount,
3 => Self::RemoveValidatorStakeAccount,
4 => Self::UpdateListBalance,
5 => Self::UpdatePoolBalance,
6 => Self::Deposit,
7 => {
let val: &u64 = unpack(input)?;
Self::Withdraw(*val)
}
8 => Self::SetOwner,
_ => return Err(ProgramError::InvalidAccountData),
})
}
/// Serializes an [StakePoolInstruction](enum.StakePoolInstruction.html) into a byte buffer.
/// TODO efficient packing here
pub fn serialize(&self) -> Result<Vec<u8>, ProgramError> {
let mut output = vec![0u8; size_of::<StakePoolInstruction>()];
match self {
Self::Initialize(init) => {
output[0] = 0;
#[allow(clippy::cast_ptr_alignment)]
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut InitArgs) };
*value = *init;
}
Self::CreateValidatorStakeAccount => {
output[0] = 1;
}
Self::AddValidatorStakeAccount => {
output[0] = 2;
}
Self::RemoveValidatorStakeAccount => {
output[0] = 3;
}
Self::UpdateListBalance => {
output[0] = 4;
}
Self::UpdatePoolBalance => {
output[0] = 5;
}
Self::Deposit => {
output[0] = 6;
}
Self::Withdraw(val) => {
output[0] = 7;
#[allow(clippy::cast_ptr_alignment)]
let value = unsafe { &mut *(&mut output[1] as *mut u8 as *mut u64) };
*value = *val;
}
Self::SetOwner => {
output[0] = 8;
}
}
Ok(output)
}
}
/// Unpacks a reference from a bytes buffer.
pub fn unpack<T>(input: &[u8]) -> Result<&T, ProgramError> {
if input.len() < size_of::<u8>() + size_of::<T>() {
return Err(ProgramError::InvalidAccountData);
}
#[allow(clippy::cast_ptr_alignment)]
let val: &T = unsafe { &*(&input[1] as *const u8 as *const T) };
Ok(val)
}
/// Creates an 'initialize' instruction.
pub fn initialize(
program_id: &Pubkey,
@ -238,10 +156,14 @@ pub fn initialize(
pool_mint: &Pubkey,
owner_pool_account: &Pubkey,
token_program_id: &Pubkey,
init_args: InitArgs,
fee: Fee,
max_validators: u32,
) -> Result<Instruction, ProgramError> {
let init_data = StakePoolInstruction::Initialize(init_args);
let data = init_data.serialize()?;
let init_data = StakePoolInstruction::Initialize {
fee,
max_validators,
};
let data = init_data.try_to_vec()?;
let accounts = vec![
AccountMeta::new(*stake_pool, true),
AccountMeta::new_readonly(*owner, true),
@ -285,7 +207,7 @@ pub fn create_validator_stake_account(
Ok(Instruction {
program_id: *program_id,
accounts,
data: StakePoolInstruction::CreateValidatorStakeAccount.serialize()?,
data: StakePoolInstruction::CreateValidatorStakeAccount.try_to_vec()?,
})
}
@ -320,7 +242,7 @@ pub fn add_validator_stake_account(
Ok(Instruction {
program_id: *program_id,
accounts,
data: StakePoolInstruction::AddValidatorStakeAccount.serialize()?,
data: StakePoolInstruction::AddValidatorStakeAccount.try_to_vec()?,
})
}
@ -354,7 +276,7 @@ pub fn remove_validator_stake_account(
Ok(Instruction {
program_id: *program_id,
accounts,
data: StakePoolInstruction::RemoveValidatorStakeAccount.serialize()?,
data: StakePoolInstruction::RemoveValidatorStakeAccount.try_to_vec()?,
})
}
@ -373,7 +295,7 @@ pub fn update_list_balance(
Ok(Instruction {
program_id: *program_id,
accounts,
data: StakePoolInstruction::UpdateListBalance.serialize()?,
data: StakePoolInstruction::UpdateListBalance.try_to_vec()?,
})
}
@ -391,7 +313,7 @@ pub fn update_pool_balance(
Ok(Instruction {
program_id: *program_id,
accounts,
data: StakePoolInstruction::UpdatePoolBalance.serialize()?,
data: StakePoolInstruction::UpdatePoolBalance.try_to_vec()?,
})
}
@ -410,8 +332,6 @@ pub fn deposit(
token_program_id: &Pubkey,
stake_program_id: &Pubkey,
) -> Result<Instruction, ProgramError> {
let args = StakePoolInstruction::Deposit;
let data = args.serialize()?;
let accounts = vec![
AccountMeta::new(*stake_pool, false),
AccountMeta::new(*validator_stake_list_storage, false),
@ -430,7 +350,7 @@ pub fn deposit(
Ok(Instruction {
program_id: *program_id,
accounts,
data,
data: StakePoolInstruction::Deposit.try_to_vec()?,
})
}
@ -449,8 +369,6 @@ pub fn withdraw(
stake_program_id: &Pubkey,
amount: u64,
) -> Result<Instruction, ProgramError> {
let args = StakePoolInstruction::Withdraw(amount);
let data = args.serialize()?;
let accounts = vec![
AccountMeta::new(*stake_pool, false),
AccountMeta::new(*validator_stake_list_storage, false),
@ -467,7 +385,7 @@ pub fn withdraw(
Ok(Instruction {
program_id: *program_id,
accounts,
data,
data: StakePoolInstruction::Withdraw(amount).try_to_vec()?,
})
}
@ -479,8 +397,6 @@ pub fn set_owner(
stake_pool_new_owner: &Pubkey,
stake_pool_new_fee_receiver: &Pubkey,
) -> Result<Instruction, ProgramError> {
let args = StakePoolInstruction::SetOwner;
let data = args.serialize()?;
let accounts = vec![
AccountMeta::new(*stake_pool, false),
AccountMeta::new_readonly(*stake_pool_owner, true),
@ -490,6 +406,6 @@ pub fn set_owner(
Ok(Instruction {
program_id: *program_id,
accounts,
data,
data: StakePoolInstruction::SetOwner.try_to_vec()?,
})
}

View File

@ -2,15 +2,13 @@
//! A program for creating pools of Solana stakes managed by a Stake-o-Matic
pub mod borsh;
pub mod error;
pub mod instruction;
pub mod processor;
pub mod stake;
pub mod state;
/// Current program version
pub const PROGRAM_VERSION: u8 = 1;
#[cfg(not(feature = "no-entrypoint"))]
pub mod entrypoint;

View File

@ -1,33 +1,36 @@
//! Program state processor
use crate::{
error::StakePoolError,
instruction::{InitArgs, StakePoolInstruction},
stake,
state::{StakePool, ValidatorStakeInfo, ValidatorStakeList},
PROGRAM_VERSION,
use {
crate::{
borsh::try_from_slice_unchecked,
error::StakePoolError,
instruction::{Fee, StakePoolInstruction},
stake,
state::{AccountType, StakePool, ValidatorStakeInfo, ValidatorStakeList},
},
bincode::deserialize,
borsh::{BorshDeserialize, BorshSerialize},
num_traits::FromPrimitive,
solana_program::{
account_info::next_account_info,
account_info::AccountInfo,
clock::Clock,
decode_error::DecodeError,
entrypoint::ProgramResult,
msg,
native_token::sol_to_lamports,
program::{invoke, invoke_signed},
program_error::PrintProgramError,
program_error::ProgramError,
program_pack::Pack,
pubkey::Pubkey,
rent::Rent,
stake_history::StakeHistory,
system_instruction,
sysvar::Sysvar,
},
spl_token::state::Mint,
};
use bincode::deserialize;
use num_traits::FromPrimitive;
use solana_program::{
account_info::next_account_info,
account_info::AccountInfo,
clock::Clock,
decode_error::DecodeError,
entrypoint::ProgramResult,
msg,
native_token::sol_to_lamports,
program::{invoke, invoke_signed},
program_error::PrintProgramError,
program_error::ProgramError,
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 {}
@ -271,8 +274,9 @@ impl Processor {
/// Processes `Initialize` instruction.
pub fn process_initialize(
program_id: &Pubkey,
init: InitArgs,
accounts: &[AccountInfo],
fee: Fee,
max_validators: u32,
) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let stake_pool_info = next_account_info(account_info_iter)?;
@ -294,24 +298,33 @@ impl Processor {
return Err(StakePoolError::SignatureMissing.into());
}
let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?;
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
// Stake pool account should not be already initialized
if stake_pool.is_initialized() {
if !stake_pool.is_uninitialized() {
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() {
let mut validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_uninitialized() {
return Err(StakePoolError::AlreadyInUse.into());
}
validator_stake_list.version = ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION;
// Check validator list size
let data_length = validator_stake_list_info.data_len();
let expected_max_validators = ValidatorStakeList::calculate_max_validators(data_length);
if expected_max_validators != max_validators as usize || max_validators == 0 {
return Err(StakePoolError::UnexpectedValidatorListAccountSize.into());
}
validator_stake_list.account_type = AccountType::ValidatorStakeList;
validator_stake_list.validators.clear();
validator_stake_list.max_validators = max_validators;
// 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());
msg!("Stake pool not rent-exempt");
return Err(ProgramError::AccountNotRentExempt);
}
// Check if validator stake list account is rent-exempt
@ -319,11 +332,12 @@ impl Processor {
validator_stake_list_info.lamports(),
validator_stake_list_info.data_len(),
) {
return Err(StakePoolError::AccountNotRentExempt.into());
msg!("Validator stake list not rent-exempt");
return Err(ProgramError::AccountNotRentExempt);
}
// Numerator should be smaller than or equal to denominator (fee <= 1)
if init.fee.numerator > init.fee.denominator {
if fee.numerator > fee.denominator {
return Err(StakePoolError::FeeTooHigh.into());
}
@ -361,12 +375,12 @@ impl Processor {
return Err(StakePoolError::WrongMintingAuthority.into());
}
validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?;
validator_stake_list.serialize(&mut *validator_stake_list_info.data.borrow_mut())?;
msg!("Clock data: {:?}", clock_info.data.borrow());
msg!("Epoch: {}", clock.epoch);
stake_pool.version = PROGRAM_VERSION;
stake_pool.account_type = AccountType::StakePool;
stake_pool.owner = *owner_info.key;
stake_pool.deposit_bump_seed = deposit_bump_seed;
stake_pool.withdraw_bump_seed = withdraw_bump_seed;
@ -375,9 +389,11 @@ impl Processor {
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.fee = fee;
stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())
stake_pool
.serialize(&mut *stake_pool_info.data.borrow_mut())
.map_err(|e| e.into())
}
/// Processes `CreateValidatorStakeAccount` instruction.
@ -503,8 +519,8 @@ impl Processor {
}
// 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() {
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -533,11 +549,15 @@ 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() {
let mut validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
if validator_stake_list.max_validators as usize == validator_stake_list.validators.len() {
return Err(ProgramError::AccountDataTooSmall);
}
let validator_account =
Self::get_validator_checked(program_id, stake_pool_info, stake_account_info)?;
@ -589,13 +609,13 @@ impl Processor {
balance: stake_lamports,
last_update_epoch: clock.epoch,
});
validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?;
validator_stake_list.serialize(&mut *validator_stake_list_info.data.borrow_mut())?;
// Save amounts to the stake pool state
stake_pool.pool_total += token_amount;
// Only update stake total if the last state update epoch is current
stake_pool.stake_total += stake_lamports;
stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?;
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
Ok(())
}
@ -636,8 +656,8 @@ impl Processor {
}
// 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() {
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -665,9 +685,10 @@ 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() {
let mut validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -716,13 +737,13 @@ impl Processor {
validator_stake_list
.validators
.retain(|item| item.validator_account != validator_account);
validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?;
validator_stake_list.serialize(&mut *validator_stake_list_info.data.borrow_mut())?;
// Save amounts to the stake pool state
stake_pool.pool_total -= token_amount;
// Only update stake total if the last state update epoch is current
stake_pool.stake_total -= stake_lamports;
stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?;
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
Ok(())
}
@ -742,9 +763,10 @@ impl Processor {
let validator_stake_accounts = account_info_iter.as_slice();
// 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() {
let mut validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -775,7 +797,7 @@ impl Processor {
}
if changes {
validator_stake_list.serialize(&mut validator_stake_list_info.data.borrow_mut())?;
validator_stake_list.serialize(&mut *validator_stake_list_info.data.borrow_mut())?;
}
Ok(())
@ -796,8 +818,8 @@ impl Processor {
let clock = &Clock::from_account_info(clock_info)?;
// 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() {
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -807,9 +829,10 @@ 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() {
let validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -823,7 +846,7 @@ impl Processor {
stake_pool.stake_total = total_balance;
stake_pool.last_update_epoch = clock.epoch;
stake_pool.serialize(&mut stake_pool_info.data.borrow_mut())?;
stake_pool.serialize(&mut *stake_pool_info.data.borrow_mut())?;
Ok(())
}
@ -894,8 +917,8 @@ impl Processor {
return Err(ProgramError::IncorrectProgramId);
}
let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?;
if !stake_pool.is_initialized() {
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -924,9 +947,10 @@ 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() {
let mut validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -1009,10 +1033,10 @@ impl Processor {
)?;
stake_pool.pool_total += pool_amount;
stake_pool.stake_total += stake_lamports;
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())?;
validator_stake_list.serialize(&mut *validator_stake_list_info.data.borrow_mut())?;
Ok(())
}
@ -1053,8 +1077,8 @@ impl Processor {
return Err(ProgramError::IncorrectProgramId);
}
let mut stake_pool = StakePool::deserialize(&stake_pool_info.data.borrow())?;
if !stake_pool.is_initialized() {
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -1076,9 +1100,10 @@ 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() {
let mut validator_stake_list = try_from_slice_unchecked::<ValidatorStakeList>(
&validator_stake_list_info.data.borrow(),
)?;
if !validator_stake_list.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -1140,10 +1165,10 @@ impl Processor {
stake_pool.pool_total -= pool_amount;
stake_pool.stake_total -= stake_amount;
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())?;
validator_stake_list.serialize(&mut *validator_stake_list_info.data.borrow_mut())?;
Ok(())
}
@ -1156,8 +1181,8 @@ 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 = StakePool::deserialize(&stake_pool_info.data.borrow())?;
if !stake_pool.is_initialized() {
let mut stake_pool = StakePool::try_from_slice(&stake_pool_info.data.borrow())?;
if !stake_pool.is_valid() {
return Err(StakePoolError::InvalidState.into());
}
@ -1173,16 +1198,19 @@ impl Processor {
stake_pool.owner = *new_owner_info.key;
stake_pool.owner_fee_account = *new_owner_fee_info.key;
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).
pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult {
let instruction = StakePoolInstruction::deserialize(input)?;
let instruction = StakePoolInstruction::try_from_slice(input)?;
match instruction {
StakePoolInstruction::Initialize(init) => {
StakePoolInstruction::Initialize {
fee,
max_validators,
} => {
msg!("Instruction: Init");
Self::process_initialize(program_id, init, accounts)
Self::process_initialize(program_id, accounts, fee, max_validators)
}
StakePoolInstruction::CreateValidatorStakeAccount => {
msg!("Instruction: CreateValidatorStakeAccount");
@ -1248,7 +1276,7 @@ impl PrintProgramError for StakePoolError {
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"),
StakePoolError::UnexpectedValidatorListAccountSize=> msg!("Error: The size of the given validator stake list does match the expected amount"),
}
}
}

View File

@ -2,21 +2,35 @@
use {
crate::{error::StakePoolError, instruction::Fee, processor::Processor},
solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, program_error::ProgramError,
pubkey::Pubkey,
},
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
solana_program::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey},
spl_math::checked_ceil_div::CheckedCeilDiv,
std::convert::{TryFrom, TryInto},
std::mem::size_of,
std::convert::TryFrom,
};
/// Enum representing the account type managed by the program
#[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub enum AccountType {
/// If the account has not been initialized, the enum will be 0
Uninitialized,
/// Stake pool
StakePool,
/// Validator stake list
ValidatorStakeList,
}
impl Default for AccountType {
fn default() -> Self {
AccountType::Uninitialized
}
}
/// Initialized program details.
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct StakePool {
/// Pool version
pub version: u8,
/// Account type, must be StakePool currently
pub account_type: AccountType,
/// Owner authority
/// allows for updating the staking authority
pub owner: Pubkey,
@ -44,8 +58,6 @@ pub struct StakePool {
pub fee: Fee,
}
impl StakePool {
/// Length of state data when serialized
pub const LEN: usize = size_of::<StakePool>();
/// calculate the pool tokens that should be minted
pub fn calc_pool_deposit_amount(&self, stake_lamports: u64) -> Option<u64> {
if self.stake_total == 0 {
@ -129,50 +141,34 @@ impl StakePool {
Ok(())
}
/// Check if StakePool is initialized
pub fn is_initialized(&self) -> bool {
self.version > 0
/// Check if StakePool is actually initialized as a stake pool
pub fn is_valid(&self) -> bool {
self.account_type == AccountType::StakePool
}
/// Deserializes a byte buffer into a [StakePool](struct.StakePool.html).
pub fn deserialize(input: &[u8]) -> Result<StakePool, ProgramError> {
if input.len() < size_of::<StakePool>() {
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::<StakePool>() {
return Err(ProgramError::InvalidAccountData);
}
#[allow(clippy::cast_ptr_alignment)]
let value = unsafe { &mut *(&mut output[0] as *mut u8 as *mut StakePool) };
*value = *self;
Ok(())
/// Check if StakePool is currently uninitialized
pub fn is_uninitialized(&self) -> bool {
self.account_type == AccountType::Uninitialized
}
}
const MAX_VALIDATOR_STAKE_ACCOUNTS: usize = 1000;
/// Storage list for all validator stake accounts in the pool.
#[repr(C)]
#[derive(Clone, Debug, Default, PartialEq)]
#[derive(Clone, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ValidatorStakeList {
/// Validator stake list version
pub version: u8,
/// Account type, must be ValidatorStakeList currently
pub account_type: AccountType,
/// Maximum allowable number of validators
pub max_validators: u32,
/// List of all validator stake accounts and their info
pub validators: Vec<ValidatorStakeInfo>,
}
/// Information about the singe validator stake account
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq)]
#[derive(Clone, Copy, Debug, Default, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)]
pub struct ValidatorStakeInfo {
/// Validator account pubkey
pub validator_account: Pubkey,
@ -185,15 +181,19 @@ pub struct ValidatorStakeInfo {
}
impl ValidatorStakeList {
/// Length of ValidatorStakeList data when serialized
pub const LEN: usize =
Self::HEADER_LEN + ValidatorStakeInfo::LEN * MAX_VALIDATOR_STAKE_ACCOUNTS;
/// Create an empty instance containing space for `max_validators`
pub fn new_with_max_validators(max_validators: u32) -> Self {
Self {
account_type: AccountType::ValidatorStakeList,
max_validators,
validators: vec![ValidatorStakeInfo::default(); max_validators as usize],
}
}
/// Header length
pub const HEADER_LEN: usize = size_of::<u8>() + size_of::<u16>();
/// Version of validator stake list
pub const VALIDATOR_STAKE_LIST_VERSION: u8 = 1;
/// Calculate the number of validator entries that fit in the provided length
pub fn calculate_max_validators(buffer_length: usize) -> usize {
(buffer_length - 1 - 4 - 4) / 48
}
/// Check if contains validator with particular pubkey
pub fn contains(&self, validator: &Pubkey) -> bool {
@ -215,125 +215,62 @@ impl ValidatorStakeList {
.find(|x| x.validator_account == *validator)
}
/// Check if validator stake list is initialized
pub fn is_initialized(&self) -> bool {
self.version > 0
/// Check if validator stake list is actually initialized as a validator stake list
pub fn is_valid(&self) -> bool {
self.account_type == AccountType::ValidatorStakeList
}
/// Deserializes a byte buffer into a ValidatorStakeList.
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
if input.len() < Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
if input[0] == 0 {
return Ok(ValidatorStakeList {
version: 0,
validators: vec![],
});
}
let number_of_validators: usize = u16::from_le_bytes(
input[1..3]
.try_into()
.or(Err(ProgramError::InvalidAccountData))?,
) as usize;
if number_of_validators > MAX_VALIDATOR_STAKE_ACCOUNTS {
return Err(ProgramError::InvalidAccountData);
}
let mut validators: Vec<ValidatorStakeInfo> = Vec::with_capacity(number_of_validators);
let mut from = Self::HEADER_LEN;
let mut to = from + ValidatorStakeInfo::LEN;
for _ in 0..number_of_validators {
validators.push(ValidatorStakeInfo::deserialize(&input[from..to])?);
from += ValidatorStakeInfo::LEN;
to += ValidatorStakeInfo::LEN;
}
Ok(ValidatorStakeList {
version: input[0],
validators,
})
}
/// Serializes ValidatorStakeList into a byte buffer.
pub fn serialize(&self, output: &mut [u8]) -> ProgramResult {
if output.len() < Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
if self.validators.len() > MAX_VALIDATOR_STAKE_ACCOUNTS {
return Err(ProgramError::InvalidAccountData);
}
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;
for validator in &self.validators {
validator.serialize(&mut output[from..to])?;
from += ValidatorStakeInfo::LEN;
to += ValidatorStakeInfo::LEN;
}
Ok(())
}
}
impl ValidatorStakeInfo {
/// Length of ValidatorStakeInfo data when serialized
pub const LEN: usize = size_of::<ValidatorStakeInfo>();
/// Deserializes a byte buffer into a ValidatorStakeInfo.
pub fn deserialize(input: &[u8]) -> Result<Self, ProgramError> {
if input.len() < Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
#[allow(clippy::cast_ptr_alignment)]
let stake_info: &ValidatorStakeInfo =
unsafe { &*(&input[0] as *const u8 as *const ValidatorStakeInfo) };
Ok(*stake_info)
}
/// Serializes ValidatorStakeInfo into a byte buffer.
pub fn serialize(&self, output: &mut [u8]) -> ProgramResult {
if output.len() < Self::LEN {
return Err(ProgramError::InvalidAccountData);
}
#[allow(clippy::cast_ptr_alignment)]
let value = unsafe { &mut *(&mut output[0] as *mut u8 as *mut ValidatorStakeInfo) };
*value = *self;
Ok(())
/// Check if the validator stake list is uninitialized
pub fn is_uninitialized(&self) -> bool {
self.account_type == AccountType::Uninitialized
}
}
#[cfg(test)]
mod test {
use super::*;
use {
super::*,
crate::borsh::{get_instance_packed_len, try_from_slice_unchecked},
proptest::prelude::*,
solana_program::borsh::get_packed_len,
};
#[test]
fn test_state_packing() {
let max_validators = 10_000;
let size =
get_instance_packed_len(&ValidatorStakeList::new_with_max_validators(max_validators))
.unwrap();
// Not initialized
let stake_list = ValidatorStakeList {
version: 0,
account_type: AccountType::Uninitialized,
max_validators: 0,
validators: vec![],
};
let mut bytes: [u8; ValidatorStakeList::LEN] = [0; ValidatorStakeList::LEN];
let mut byte_vec = vec![0u8; size];
let mut bytes = byte_vec.as_mut_slice();
stake_list.serialize(&mut bytes).unwrap();
let stake_list_unpacked = ValidatorStakeList::deserialize(&bytes).unwrap();
let stake_list_unpacked =
try_from_slice_unchecked::<ValidatorStakeList>(&byte_vec).unwrap();
assert_eq!(stake_list_unpacked, stake_list);
// Empty
let stake_list = ValidatorStakeList {
version: ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION,
account_type: AccountType::ValidatorStakeList,
max_validators: 0,
validators: vec![],
};
let mut bytes: [u8; ValidatorStakeList::LEN] = [0; ValidatorStakeList::LEN];
let mut byte_vec = vec![0u8; size];
let mut bytes = byte_vec.as_mut_slice();
stake_list.serialize(&mut bytes).unwrap();
let stake_list_unpacked = ValidatorStakeList::deserialize(&bytes).unwrap();
let stake_list_unpacked =
try_from_slice_unchecked::<ValidatorStakeList>(&byte_vec).unwrap();
assert_eq!(stake_list_unpacked, stake_list);
// With several accounts
let stake_list = ValidatorStakeList {
version: ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION,
account_type: AccountType::ValidatorStakeList,
max_validators,
validators: vec![
ValidatorStakeInfo {
validator_account: Pubkey::new_from_array([1; 32]),
@ -352,9 +289,23 @@ mod test {
},
],
};
let mut bytes: [u8; ValidatorStakeList::LEN] = [0; ValidatorStakeList::LEN];
let mut byte_vec = vec![0u8; size];
let mut bytes = byte_vec.as_mut_slice();
stake_list.serialize(&mut bytes).unwrap();
let stake_list_unpacked = ValidatorStakeList::deserialize(&bytes).unwrap();
let stake_list_unpacked =
try_from_slice_unchecked::<ValidatorStakeList>(&byte_vec).unwrap();
assert_eq!(stake_list_unpacked, stake_list);
}
proptest! {
#[test]
fn stake_list_size_calculation(test_amount in 0..=100_000_u32) {
let validators = ValidatorStakeList::new_with_max_validators(test_amount);
let size = get_instance_packed_len(&validators).unwrap();
assert_eq!(ValidatorStakeList::calculate_max_validators(size), test_amount as usize);
assert_eq!(ValidatorStakeList::calculate_max_validators(size + 1), test_amount as usize);
assert_eq!(ValidatorStakeList::calculate_max_validators(size + get_packed_len::<ValidatorStakeInfo>()), (test_amount + 1)as usize);
assert_eq!(ValidatorStakeList::calculate_max_validators(size - 1), (test_amount - 1) as usize);
}
}
}

View File

@ -2,19 +2,21 @@
mod helpers;
use helpers::*;
use solana_program::hash::Hash;
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::Transaction,
transaction::TransactionError,
transport::TransportError,
use {
borsh::BorshDeserialize,
helpers::*,
solana_program::hash::Hash,
solana_program_test::*,
solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::Transaction,
transaction::TransactionError,
transport::TransportError,
},
spl_stake_pool::{borsh::try_from_slice_unchecked, error, id, instruction, stake, state},
spl_token::error as token_error,
};
use spl_stake_pool::*;
use spl_token::error as token_error;
async fn setup() -> (
BanksClient,
@ -129,7 +131,7 @@ async fn test_stake_pool_deposit() {
let stake_pool_before =
get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool_before =
state::StakePool::deserialize(&stake_pool_before.data.as_slice()).unwrap();
state::StakePool::try_from_slice(&stake_pool_before.data.as_slice()).unwrap();
// Save validator stake account record before depositing
let validator_stake_list = get_account(
@ -138,7 +140,8 @@ async fn test_stake_pool_deposit() {
)
.await;
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
let validator_stake_item_before = validator_stake_list
.find(&validator_stake_account.vote.pubkey())
.unwrap();
@ -167,7 +170,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::StakePool::deserialize(&stake_pool.data.as_slice()).unwrap();
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(
stake_pool.stake_total,
stake_pool_before.stake_total + stake_lamports
@ -195,7 +198,8 @@ async fn test_stake_pool_deposit() {
)
.await;
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
let validator_stake_item = validator_stake_list
.find(&validator_stake_account.vote.pubkey())
.unwrap();

View File

@ -1,17 +1,22 @@
#![allow(dead_code)]
use solana_program::{hash::Hash, program_pack::Pack, pubkey::Pubkey, system_instruction};
use solana_program_test::*;
use solana_sdk::{
account::Account,
signature::{Keypair, Signer},
transaction::Transaction,
transport::TransportError,
use {
solana_program::{
borsh::get_packed_len, hash::Hash, program_pack::Pack, pubkey::Pubkey, system_instruction,
},
solana_program_test::*,
solana_sdk::{
account::Account,
signature::{Keypair, Signer},
transaction::Transaction,
transport::TransportError,
},
solana_vote_program::{self, vote_state::VoteState},
spl_stake_pool::{borsh::get_instance_packed_len, id, instruction, processor, stake, state},
};
use solana_vote_program::{self, vote_state::VoteState};
use spl_stake_pool::*;
pub const TEST_STAKE_AMOUNT: u64 = 100;
pub const MAX_TEST_VALIDATORS: u32 = 10_000;
pub fn program_test() -> ProgramTest {
ProgramTest::new(
@ -165,11 +170,15 @@ pub async fn create_stake_pool(
pool_token_account: &Pubkey,
owner: &Keypair,
fee: &instruction::Fee,
max_validators: u32,
) -> Result<(), TransportError> {
let rent = banks_client.get_rent().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: *fee };
let rent_stake_pool = rent.minimum_balance(get_packed_len::<state::StakePool>());
let validator_stake_list_size = get_instance_packed_len(
&state::ValidatorStakeList::new_with_max_validators(max_validators),
)
.unwrap();
let rent_validator_stake_list = rent.minimum_balance(validator_stake_list_size);
let mut transaction = Transaction::new_with_payer(
&[
@ -177,14 +186,14 @@ pub async fn create_stake_pool(
&payer.pubkey(),
&stake_pool.pubkey(),
rent_stake_pool,
state::StakePool::LEN as u64,
get_packed_len::<state::StakePool>() as u64,
&id(),
),
system_instruction::create_account(
&payer.pubkey(),
&validator_stake_list.pubkey(),
rent_validator_stake_list,
state::ValidatorStakeList::LEN as u64,
validator_stake_list_size as u64,
&id(),
),
instruction::initialize(
@ -195,7 +204,8 @@ pub async fn create_stake_pool(
pool_mint,
pool_token_account,
&spl_token::id(),
init_args,
fee.clone(),
max_validators,
)
.unwrap(),
],
@ -431,6 +441,7 @@ pub struct StakePoolAccounts {
pub withdraw_authority: Pubkey,
pub deposit_authority: Pubkey,
pub fee: instruction::Fee,
pub max_validators: u32,
}
impl StakePoolAccounts {
@ -462,6 +473,7 @@ impl StakePoolAccounts {
numerator: 1,
denominator: 100,
},
max_validators: MAX_TEST_VALIDATORS,
}
}
@ -502,6 +514,7 @@ impl StakePoolAccounts {
&self.pool_fee_account.pubkey(),
&self.owner,
&self.fee,
self.max_validators,
)
.await?;
Ok(())

View File

@ -2,19 +2,26 @@
mod helpers;
use helpers::*;
use solana_program::hash::Hash;
use solana_program::{
instruction::{AccountMeta, Instruction},
program_pack::Pack,
system_instruction, sysvar,
use {
borsh::BorshSerialize,
helpers::*,
solana_program::{
borsh::get_packed_len,
hash::Hash,
instruction::{AccountMeta, Instruction},
program_pack::Pack,
system_instruction, sysvar,
},
solana_program_test::*,
solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer,
transaction::Transaction, transaction::TransactionError, transport::TransportError,
},
spl_stake_pool::{
borsh::{get_instance_packed_len, try_from_slice_unchecked},
error, id, instruction, state,
},
};
use solana_program_test::*;
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,
@ -55,7 +62,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::StakePool::LEN);
assert_eq!(stake_pool.data.len(), get_packed_len::<state::StakePool>());
assert_eq!(stake_pool.owner, id());
// Validator stake list storage initialized
@ -65,8 +72,9 @@ 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);
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
assert_eq!(validator_stake_list.is_valid(), true);
}
#[tokio::test]
@ -157,6 +165,85 @@ async fn test_initialize_stake_pool_with_high_fee() {
}
}
#[tokio::test]
async fn test_initialize_stake_pool_with_wrong_max_validators() {
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(get_packed_len::<state::StakePool>());
let validator_stake_list_size = get_instance_packed_len(
&state::ValidatorStakeList::new_with_max_validators(stake_pool_accounts.max_validators - 1),
)
.unwrap();
let rent_validator_stake_list = rent.minimum_balance(validator_stake_list_size);
let mut transaction = Transaction::new_with_payer(
&[
system_instruction::create_account(
&payer.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(),
rent_stake_pool,
get_packed_len::<state::StakePool>() as u64,
&id(),
),
system_instruction::create_account(
&payer.pubkey(),
&stake_pool_accounts.validator_stake_list.pubkey(),
rent_validator_stake_list,
validator_stake_list_size 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(),
stake_pool_accounts.fee.clone(),
stake_pool_accounts.max_validators,
)
.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::UnexpectedValidatorListAccountSize as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to initialize stake pool with high fee"),
}
}
#[tokio::test]
async fn test_initialize_stake_pool_with_wrong_mint_authority() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
@ -192,6 +279,7 @@ async fn test_initialize_stake_pool_with_wrong_mint_authority() {
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.owner,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
@ -245,11 +333,12 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
);
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 rent_stake_pool = rent.minimum_balance(get_packed_len::<state::StakePool>());
let validator_stake_list_size = get_instance_packed_len(
&state::ValidatorStakeList::new_with_max_validators(stake_pool_accounts.max_validators),
)
.unwrap();
let rent_validator_stake_list = rent.minimum_balance(validator_stake_list_size);
let mut transaction = Transaction::new_with_payer(
&[
@ -257,14 +346,14 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
&payer.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(),
rent_stake_pool,
state::StakePool::LEN as u64,
get_packed_len::<state::StakePool>() 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,
validator_stake_list_size as u64,
&id(),
),
instruction::initialize(
@ -275,7 +364,8 @@ async fn test_initialize_stake_pool_with_wrong_token_program_id() {
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&wrong_token_program.pubkey(),
init_args,
stake_pool_accounts.fee.clone(),
stake_pool_accounts.max_validators,
)
.unwrap(),
],
@ -349,6 +439,7 @@ async fn test_initialize_stake_pool_with_wrong_fee_accounts_owner() {
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.owner,
&stake_pool_accounts.fee,
stake_pool_accounts.max_validators,
)
.await
.err()
@ -409,10 +500,11 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
.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 validator_stake_list_size = get_instance_packed_len(
&state::ValidatorStakeList::new_with_max_validators(stake_pool_accounts.max_validators),
)
.unwrap();
let rent_validator_stake_list = rent.minimum_balance(validator_stake_list_size);
let mut transaction = Transaction::new_with_payer(
&[
@ -420,14 +512,14 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
&payer.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(),
1,
state::StakePool::LEN as u64,
get_packed_len::<state::StakePool>() 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,
validator_stake_list_size as u64,
&id(),
),
instruction::initialize(
@ -438,7 +530,8 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&spl_token::id(),
init_args,
stake_pool_accounts.fee.clone(),
stake_pool_accounts.max_validators,
)
.unwrap(),
],
@ -453,24 +546,19 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_pool() {
],
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"
),
}
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(
2,
InstructionError::InvalidError,
// should be InstructionError::AccountNotRentExempt, but the mapping
// is wrong
)
);
}
#[tokio::test]
@ -487,10 +575,11 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_stake_list()
.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 rent_stake_pool = rent.minimum_balance(get_packed_len::<state::StakePool>());
let validator_stake_list_size = get_instance_packed_len(
&state::ValidatorStakeList::new_with_max_validators(stake_pool_accounts.max_validators),
)
.unwrap();
let mut transaction = Transaction::new_with_payer(
&[
@ -498,14 +587,14 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_stake_list()
&payer.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(),
rent_stake_pool,
state::StakePool::LEN as u64,
get_packed_len::<state::StakePool>() as u64,
&id(),
),
system_instruction::create_account(
&payer.pubkey(),
&stake_pool_accounts.validator_stake_list.pubkey(),
1,
state::ValidatorStakeList::LEN as u64,
validator_stake_list_size as u64,
&id(),
),
instruction::initialize(
@ -516,7 +605,8 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_validator_stake_list()
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&spl_token::id(),
init_args,
stake_pool_accounts.fee.clone(),
stake_pool_accounts.max_validators,
)
.unwrap(),
],
@ -531,24 +621,20 @@ async fn test_initialize_stake_pool_with_not_rent_exempt_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::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"
),
}
assert_eq!(
banks_client
.process_transaction(transaction)
.await
.unwrap_err()
.unwrap(),
TransactionError::InstructionError(
2,
InstructionError::InvalidError,
// should be InstructionError::AccountNotRentExempt, but the mapping
// is wrong
)
);
}
#[tokio::test]
@ -565,13 +651,18 @@ async fn test_initialize_stake_pool_without_owner_signature() {
.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 rent_stake_pool = rent.minimum_balance(get_packed_len::<state::StakePool>());
let validator_stake_list_size = get_instance_packed_len(
&state::ValidatorStakeList::new_with_max_validators(stake_pool_accounts.max_validators),
)
.unwrap();
let rent_validator_stake_list = rent.minimum_balance(validator_stake_list_size);
let init_data = instruction::StakePoolInstruction::Initialize(init_args);
let data = init_data.serialize().unwrap();
let init_data = instruction::StakePoolInstruction::Initialize {
fee: stake_pool_accounts.fee.clone(),
max_validators: stake_pool_accounts.max_validators,
};
let data = init_data.try_to_vec().unwrap();
let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), true),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false),
@ -594,14 +685,14 @@ async fn test_initialize_stake_pool_without_owner_signature() {
&payer.pubkey(),
&stake_pool_accounts.stake_pool.pubkey(),
rent_stake_pool,
state::StakePool::LEN as u64,
get_packed_len::<state::StakePool>() 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,
rent_validator_stake_list,
validator_stake_list_size as u64,
&id(),
),
stake_pool_init_instruction,

View File

@ -2,16 +2,20 @@
mod helpers;
use helpers::*;
use solana_program::hash::Hash;
use solana_program::instruction::AccountMeta;
use solana_program::instruction::Instruction;
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer, transaction::Transaction,
transaction::TransactionError, transport::TransportError,
use {
borsh::{BorshDeserialize, BorshSerialize},
helpers::*,
solana_program::{
hash::Hash,
instruction::{AccountMeta, Instruction},
},
solana_program_test::*,
solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer,
transaction::Transaction, transaction::TransactionError, transport::TransportError,
},
spl_stake_pool::{error, id, instruction, state},
};
use spl_stake_pool::*;
async fn setup() -> (
BanksClient,
@ -71,7 +75,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::StakePool::deserialize(&stake_pool.data.as_slice()).unwrap();
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(stake_pool.owner, new_owner.pubkey());
}
@ -116,8 +120,9 @@ async fn test_set_owner_without_signature() {
let (mut banks_client, payer, recent_blockhash, stake_pool_accounts, new_pool_fee, new_owner) =
setup().await;
let args = instruction::StakePoolInstruction::SetOwner;
let data = args.serialize().unwrap();
let data = instruction::StakePoolInstruction::SetOwner
.try_to_vec()
.unwrap();
let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.owner.pubkey(), false),

View File

@ -8,7 +8,7 @@ use {
solana_program::{native_token, pubkey::Pubkey},
solana_program_test::*,
solana_sdk::signature::Signer,
spl_stake_pool::*,
spl_stake_pool::{borsh::try_from_slice_unchecked, stake, state},
};
async fn get_list_sum(banks_client: &mut BanksClient, validator_stake_list_key: &Pubkey) -> u64 {
@ -18,7 +18,8 @@ async fn get_list_sum(banks_client: &mut BanksClient, validator_stake_list_key:
.expect("get_account")
.expect("validator stake list not none");
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
validator_stake_list
.validators

View File

@ -2,13 +2,15 @@
mod helpers;
use helpers::*;
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer, transaction::Transaction,
transaction::TransactionError, transport::TransportError,
use {
helpers::*,
solana_program_test::*,
solana_sdk::{
instruction::InstructionError, signature::Keypair, signature::Signer,
transaction::Transaction, transaction::TransactionError, transport::TransportError,
},
spl_stake_pool::*,
};
use spl_stake_pool::*;
#[tokio::test]
async fn test_update_pool_balance() {

View File

@ -2,22 +2,24 @@
mod helpers;
use helpers::*;
use bincode::deserialize;
use solana_program::hash::Hash;
use solana_program::instruction::AccountMeta;
use solana_program::instruction::Instruction;
use solana_program::sysvar;
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::Transaction,
transaction::TransactionError,
transport::TransportError,
use {
bincode::deserialize,
borsh::BorshSerialize,
helpers::*,
solana_program::{
hash::Hash,
instruction::{AccountMeta, Instruction},
sysvar,
},
solana_program_test::*,
solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
transport::TransportError,
},
spl_stake_pool::{borsh::try_from_slice_unchecked, error, id, instruction, stake, state},
};
use spl_stake_pool::*;
async fn setup() -> (
BanksClient,
@ -113,11 +115,13 @@ async fn test_add_validator_stake_account() {
)
.await;
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
assert_eq!(
validator_stake_list,
state::ValidatorStakeList {
version: state::ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION,
account_type: state::AccountType::ValidatorStakeList,
max_validators: stake_pool_accounts.max_validators,
validators: vec![state::ValidatorStakeInfo {
validator_account: user_stake.vote.pubkey(),
last_update_epoch: 0,
@ -409,7 +413,7 @@ async fn test_not_owner_try_to_add_validator_stake_account_without_signature() {
program_id: id(),
accounts,
data: instruction::StakePoolInstruction::AddValidatorStakeAccount
.serialize()
.try_to_vec()
.unwrap(),
};
@ -543,6 +547,74 @@ async fn test_add_validator_stake_account_with_wrong_stake_program_id() {
}
}
#[tokio::test]
async fn test_add_too_many_validator_stake_accounts() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let mut stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts.max_validators = 1;
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash)
.await
.unwrap();
let user = Keypair::new();
let user_stake = ValidatorStakeAccount::new_with_target_authority(
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake
.create_and_delegate(&mut banks_client, &payer, &recent_blockhash)
.await;
// make pool token account
let user_pool_account = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_pool_account,
&stake_pool_accounts.pool_mint.pubkey(),
&user.pubkey(),
)
.await
.unwrap();
let error = stake_pool_accounts
.add_validator_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.stake_account,
&user_pool_account.pubkey(),
)
.await;
assert!(error.is_none());
let user_stake = ValidatorStakeAccount::new_with_target_authority(
&stake_pool_accounts.deposit_authority,
&stake_pool_accounts.stake_pool.pubkey(),
);
user_stake
.create_and_delegate(&mut banks_client, &payer, &recent_blockhash)
.await;
let error = stake_pool_accounts
.add_validator_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake.stake_account,
&user_pool_account.pubkey(),
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(0, InstructionError::AccountDataTooSmall),
);
}
#[tokio::test]
async fn test_add_validator_stake_account_to_unupdated_stake_pool() {} // TODO

View File

@ -2,22 +2,25 @@
mod helpers;
use bincode::deserialize;
use helpers::*;
use solana_program::hash::Hash;
use solana_program::instruction::AccountMeta;
use solana_program::instruction::Instruction;
use solana_program::pubkey::Pubkey;
use solana_program::sysvar;
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::Transaction,
transaction::TransactionError,
transport::TransportError,
use {
bincode::deserialize,
borsh::BorshSerialize,
helpers::*,
solana_program::{
hash::Hash,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
sysvar,
},
solana_program_test::*,
solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
transport::TransportError,
},
spl_stake_pool::{borsh::try_from_slice_unchecked, error, id, instruction, stake, state},
};
use spl_stake_pool::*;
async fn setup() -> (
BanksClient,
@ -128,11 +131,13 @@ async fn test_remove_validator_stake_account() {
)
.await;
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
assert_eq!(
validator_stake_list,
state::ValidatorStakeList {
version: state::ValidatorStakeList::VALIDATOR_STAKE_LIST_VERSION,
account_type: state::AccountType::ValidatorStakeList,
max_validators: stake_pool_accounts.max_validators,
validators: vec![]
}
);
@ -495,7 +500,7 @@ async fn test_not_owner_try_to_remove_validator_stake_account_without_signature(
program_id: id(),
accounts,
data: instruction::StakePoolInstruction::RemoveValidatorStakeAccount
.serialize()
.try_to_vec()
.unwrap(),
};

View File

@ -2,20 +2,21 @@
mod helpers;
use helpers::*;
use solana_program::pubkey::Pubkey;
use solana_program::hash::Hash;
use solana_program_test::*;
use solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::Transaction,
transaction::TransactionError,
transport::TransportError,
use {
borsh::BorshDeserialize,
helpers::*,
solana_program::hash::Hash,
solana_program::pubkey::Pubkey,
solana_program_test::*,
solana_sdk::{
instruction::InstructionError,
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
transport::TransportError,
},
spl_stake_pool::{borsh::try_from_slice_unchecked, error, id, instruction, stake, state},
spl_token::error::TokenError,
};
use spl_stake_pool::*;
use spl_token::error::TokenError;
async fn setup() -> (
BanksClient,
@ -101,7 +102,7 @@ async fn test_stake_pool_withdraw() {
let stake_pool_before =
get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool_before =
state::StakePool::deserialize(&stake_pool_before.data.as_slice()).unwrap();
state::StakePool::try_from_slice(&stake_pool_before.data.as_slice()).unwrap();
// Save validator stake account record before withdrawal
let validator_stake_list = get_account(
@ -110,7 +111,8 @@ async fn test_stake_pool_withdraw() {
)
.await;
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
let validator_stake_item_before = validator_stake_list
.find(&validator_stake_account.vote.pubkey())
.unwrap();
@ -136,7 +138,7 @@ async fn test_stake_pool_withdraw() {
// Check pool stats
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool = state::StakePool::deserialize(&stake_pool.data.as_slice()).unwrap();
let stake_pool = state::StakePool::try_from_slice(&stake_pool.data.as_slice()).unwrap();
assert_eq!(
stake_pool.stake_total,
stake_pool_before.stake_total - tokens_to_burn
@ -153,7 +155,8 @@ async fn test_stake_pool_withdraw() {
)
.await;
let validator_stake_list =
state::ValidatorStakeList::deserialize(validator_stake_list.data.as_slice()).unwrap();
try_from_slice_unchecked::<state::ValidatorStakeList>(validator_stake_list.data.as_slice())
.unwrap();
let validator_stake_item = validator_stake_list
.find(&validator_stake_account.vote.pubkey())
.unwrap();