solana-program-library/stake-pool/program/tests/withdraw.rs

1547 lines
45 KiB
Rust

#![cfg(feature = "test-bpf")]
mod helpers;
use {
bincode::deserialize,
borsh::BorshSerialize,
helpers::*,
solana_program::{
borsh::try_from_slice_unchecked,
hash::Hash,
instruction::{AccountMeta, Instruction, InstructionError},
pubkey::Pubkey,
stake, sysvar,
},
solana_program_test::*,
solana_sdk::{
signature::{Keypair, Signer},
transaction::{Transaction, TransactionError},
transport::TransportError,
},
spl_stake_pool::{error::StakePoolError, id, instruction, minimum_stake_lamports, state},
spl_token::error::TokenError,
};
async fn setup() -> (
BanksClient,
Keypair,
Hash,
StakePoolAccounts,
ValidatorStakeAccount,
DepositStakeAccount,
Keypair,
Keypair,
u64,
) {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_info = simple_deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake_account,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
let tokens_to_withdraw = deposit_info.pool_tokens / 4;
// Delegate tokens for withdrawing
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
tokens_to_withdraw,
)
.await;
// Create stake account to withdraw to
let user_stake_recipient = Keypair::new();
create_blank_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient,
)
.await;
(
banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
)
}
#[tokio::test]
async fn success() {
_success(SuccessTestType::Success).await;
}
#[tokio::test]
async fn success_with_closed_manager_fee_account() {
_success(SuccessTestType::UninitializedManagerFee).await;
}
enum SuccessTestType {
Success,
UninitializedManagerFee,
}
async fn _success(test_type: SuccessTestType) {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
) = setup().await;
// 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 =
try_from_slice_unchecked::<state::StakePool>(stake_pool_before.data.as_slice()).unwrap();
// Check user recipient stake account balance
let initial_stake_lamports = get_account(&mut banks_client, &user_stake_recipient.pubkey())
.await
.lamports;
// Save validator stake account record before withdrawal
let validator_list = get_account(
&mut banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
let validator_stake_item_before = validator_list
.find(&validator_stake_account.vote.pubkey())
.unwrap();
// Save user token balance
let user_token_balance_before =
get_token_balance(&mut banks_client, &deposit_info.pool_account.pubkey()).await;
// Save pool fee token balance
let pool_fee_balance_before = get_token_balance(
&mut banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
let destination_keypair = Keypair::new();
create_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&destination_keypair,
&stake_pool_accounts.pool_mint.pubkey(),
&Keypair::new().pubkey(),
)
.await
.unwrap();
if let SuccessTestType::UninitializedManagerFee = test_type {
transfer_spl_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_fee_account.pubkey(),
&destination_keypair.pubkey(),
&stake_pool_accounts.manager,
pool_fee_balance_before,
)
.await;
// Check that the account cannot be frozen due to lack of
// freeze authority.
let transaction_error = freeze_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&stake_pool_accounts.manager,
)
.await
.unwrap_err();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::Custom(0x10));
}
_ => panic!("Wrong error occurs while try to withdraw with wrong stake program ID"),
}
close_token_account(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_fee_account.pubkey(),
&destination_keypair.pubkey(),
&stake_pool_accounts.manager,
)
.await
.unwrap();
}
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_withdraw,
)
.await;
assert!(error.is_none());
// Check pool stats
let stake_pool = get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool =
try_from_slice_unchecked::<state::StakePool>(stake_pool.data.as_slice()).unwrap();
// first and only deposit, lamports:pool 1:1
let tokens_withdrawal_fee = match test_type {
SuccessTestType::Success => {
stake_pool_accounts.calculate_withdrawal_fee(tokens_to_withdraw)
}
_ => 0,
};
let tokens_burnt = tokens_to_withdraw - tokens_withdrawal_fee;
assert_eq!(
stake_pool.total_lamports,
stake_pool_before.total_lamports - tokens_burnt
);
assert_eq!(
stake_pool.pool_token_supply,
stake_pool_before.pool_token_supply - tokens_burnt
);
if let SuccessTestType::Success = test_type {
// Check manager received withdrawal fee
let pool_fee_balance = get_token_balance(
&mut banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
assert_eq!(
pool_fee_balance,
pool_fee_balance_before + tokens_withdrawal_fee,
);
}
// Check validator stake list storage
let validator_list = get_account(
&mut banks_client,
&stake_pool_accounts.validator_list.pubkey(),
)
.await;
let validator_list =
try_from_slice_unchecked::<state::ValidatorList>(validator_list.data.as_slice()).unwrap();
let validator_stake_item = validator_list
.find(&validator_stake_account.vote.pubkey())
.unwrap();
assert_eq!(
validator_stake_item.stake_lamports(),
validator_stake_item_before.stake_lamports() - tokens_burnt
);
assert_eq!(
validator_stake_item.active_stake_lamports,
validator_stake_item.stake_lamports(),
);
// Check tokens used
let user_token_balance =
get_token_balance(&mut banks_client, &deposit_info.pool_account.pubkey()).await;
assert_eq!(
user_token_balance,
user_token_balance_before - tokens_to_withdraw
);
// Check validator stake account balance
let validator_stake_account =
get_account(&mut banks_client, &validator_stake_account.stake_account).await;
let stake_state =
deserialize::<stake::state::StakeState>(&validator_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
assert_eq!(
validator_stake_account.lamports - minimum_stake_lamports(&meta),
validator_stake_item.active_stake_lamports
);
// Check user recipient stake account balance
let user_stake_recipient_account =
get_account(&mut banks_client, &user_stake_recipient.pubkey()).await;
assert_eq!(
user_stake_recipient_account.lamports,
initial_stake_lamports + tokens_burnt
);
}
#[tokio::test]
async fn fail_with_wrong_stake_program() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let new_authority = Pubkey::new_unique();
let wrong_stake_program = Pubkey::new_unique();
let accounts = vec![
AccountMeta::new(stake_pool_accounts.stake_pool.pubkey(), false),
AccountMeta::new(stake_pool_accounts.validator_list.pubkey(), false),
AccountMeta::new_readonly(stake_pool_accounts.withdraw_authority, false),
AccountMeta::new(validator_stake_account.stake_account, false),
AccountMeta::new(user_stake_recipient.pubkey(), false),
AccountMeta::new_readonly(new_authority, false),
AccountMeta::new_readonly(user_transfer_authority.pubkey(), true),
AccountMeta::new(deposit_info.pool_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_fee_account.pubkey(), false),
AccountMeta::new(stake_pool_accounts.pool_mint.pubkey(), false),
AccountMeta::new_readonly(sysvar::clock::id(), false),
AccountMeta::new_readonly(spl_token::id(), false),
AccountMeta::new_readonly(wrong_stake_program, false),
];
let instruction = Instruction {
program_id: id(),
accounts,
data: instruction::StakePoolInstruction::WithdrawStake(tokens_to_burn)
.try_to_vec()
.unwrap(),
};
let transaction = Transaction::new_signed_with_payer(
&[instruction],
Some(&payer.pubkey()),
&[&payer, &user_transfer_authority],
recent_blockhash,
);
#[allow(clippy::useless_conversion)] // Remove during upgrade to 1.10
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.into();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::IncorrectProgramId);
}
_ => panic!("Wrong error occurs while try to withdraw with wrong stake program ID"),
}
}
#[tokio::test]
async fn fail_with_wrong_withdraw_authority() {
let (
mut banks_client,
payer,
recent_blockhash,
mut stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let new_authority = Pubkey::new_unique();
stake_pool_accounts.withdraw_authority = Keypair::new().pubkey();
let transaction_error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = StakePoolError::InvalidProgramAddress as u32;
assert_eq!(error_index, program_error);
}
_ => panic!("Wrong error occurs while try to withdraw with wrong withdraw authority"),
}
}
#[tokio::test]
async fn fail_with_wrong_token_program_id() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let new_authority = Pubkey::new_unique();
let wrong_token_program = Keypair::new();
let transaction = Transaction::new_signed_with_payer(
&[instruction::withdraw_stake(
&id(),
&stake_pool_accounts.stake_pool.pubkey(),
&stake_pool_accounts.validator_list.pubkey(),
&stake_pool_accounts.withdraw_authority,
&validator_stake_account.stake_account,
&user_stake_recipient.pubkey(),
&new_authority,
&user_transfer_authority.pubkey(),
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.pool_mint.pubkey(),
&wrong_token_program.pubkey(),
tokens_to_burn,
)],
Some(&payer.pubkey()),
&[&payer, &user_transfer_authority],
recent_blockhash,
);
#[allow(clippy::useless_conversion)] // Remove during upgrade to 1.10
let transaction_error = banks_client
.process_transaction(transaction)
.await
.err()
.unwrap()
.into();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::IncorrectProgramId);
}
_ => panic!("Wrong error occurs while try to withdraw with wrong token program ID"),
}
}
#[tokio::test]
async fn fail_with_wrong_validator_list() {
let (
mut banks_client,
payer,
recent_blockhash,
mut stake_pool_accounts,
validator_stake,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let new_authority = Pubkey::new_unique();
stake_pool_accounts.validator_list = Keypair::new();
let transaction_error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = StakePoolError::InvalidValidatorStakeList as u32;
assert_eq!(error_index, program_error);
}
_ => panic!(
"Wrong error occurs while try to withdraw with wrong validator stake list account"
),
}
}
#[tokio::test]
async fn fail_with_unknown_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
_,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
) = setup().await;
let unknown_stake = create_unknown_validator_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&unknown_stake.stake_account,
&new_authority,
tokens_to_withdraw,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::ValidatorNotFound as u32)
)
);
}
#[tokio::test]
async fn fail_double_withdraw_to_the_same_account() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await;
assert!(error.is_none());
let latest_blockhash = banks_client.get_latest_blockhash().await.unwrap();
// Delegate tokens for burning
delegate_tokens(
&mut banks_client,
&payer,
&latest_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
tokens_to_burn,
)
.await;
let transaction_error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&latest_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(_, error)) => {
assert_eq!(error, InstructionError::InvalidAccountData);
}
_ => panic!("Wrong error occurs while try to do double withdraw"),
}
}
#[tokio::test]
async fn fail_without_token_approval() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_info = simple_deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake_account,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
let tokens_to_burn = deposit_info.pool_tokens / 4;
// Create stake account to withdraw to
let user_stake_recipient = Keypair::new();
create_blank_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient,
)
.await;
let new_authority = Pubkey::new_unique();
let user_transfer_authority = Keypair::new();
let transaction_error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = TokenError::OwnerMismatch as u32;
assert_eq!(error_index, program_error);
}
_ => panic!(
"Wrong error occurs while try to do withdraw without token delegation for burn before"
),
}
}
#[tokio::test]
async fn fail_with_low_delegation() {
let (mut banks_client, payer, recent_blockhash) = program_test().start().await;
let stake_pool_accounts = StakePoolAccounts::new();
stake_pool_accounts
.initialize_stake_pool(&mut banks_client, &payer, &recent_blockhash, 1)
.await
.unwrap();
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_info = simple_deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&validator_stake_account,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
let tokens_to_burn = deposit_info.pool_tokens / 4;
let user_transfer_authority = Keypair::new();
// Delegate tokens for burning
delegate_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
1,
)
.await;
// Create stake account to withdraw to
let user_stake_recipient = Keypair::new();
create_blank_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient,
)
.await;
let new_authority = Pubkey::new_unique();
let transaction_error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap();
match transaction_error {
TransportError::TransactionError(TransactionError::InstructionError(
_,
InstructionError::Custom(error_index),
)) => {
let program_error = TokenError::InsufficientFunds as u32;
assert_eq!(error_index, program_error);
}
_ => panic!(
"Wrong error occurs while try to do withdraw with not enough delegated tokens to burn"
),
}
}
#[tokio::test]
async fn fail_overdraw_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
_validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
),
);
}
#[tokio::test]
async fn success_with_reserve() {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new();
let initial_reserve_lamports = 1;
stake_pool_accounts
.initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
initial_reserve_lamports,
)
.await
.unwrap();
let validator_stake = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_lamports = TEST_STAKE_AMOUNT;
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeState>());
let deposit_info = simple_deposit_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
&validator_stake,
deposit_lamports,
)
.await
.unwrap();
// decrease some stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
deposit_lamports - 1,
validator_stake.transient_stake_seed,
)
.await;
assert!(error.is_none());
// warp forward to deactivation
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
context
.warp_to_slot(first_normal_slot + slots_per_epoch)
.unwrap();
// update to merge deactivated stake into reserve
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&[validator_stake.vote.pubkey()],
false,
)
.await;
// Delegate tokens for using for withdrawal
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
deposit_info.pool_tokens,
)
.await;
// Withdraw directly from reserve, fail because some stake left
let withdraw_destination = Keypair::new();
let withdraw_destination_authority = Pubkey::new_unique();
let initial_stake_lamports = create_blank_stake_account(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination,
)
.await;
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&withdraw_destination_authority,
deposit_info.pool_tokens,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::StakeLamportsNotEqualToMinimum as u32)
)
);
// decrease rest of stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
stake_rent + 1,
validator_stake.transient_stake_seed,
)
.await;
assert!(error.is_none());
// warp forward to deactivation
context
.warp_to_slot(first_normal_slot + 2 * slots_per_epoch)
.unwrap();
// update to merge deactivated stake into reserve
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&[validator_stake.vote.pubkey()],
false,
)
.await;
// now it works
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.reserve_stake.pubkey(),
&withdraw_destination_authority,
deposit_info.pool_tokens,
)
.await;
assert!(error.is_none());
// first and only deposit, lamports:pool 1:1
let stake_pool = get_account(
&mut context.banks_client,
&stake_pool_accounts.stake_pool.pubkey(),
)
.await;
let stake_pool =
try_from_slice_unchecked::<state::StakePool>(stake_pool.data.as_slice()).unwrap();
// the entire deposit is actually stake since it isn't activated, so only
// the stake deposit fee is charged
let deposit_fee = stake_pool
.calc_pool_tokens_stake_deposit_fee(stake_rent + deposit_info.stake_lamports)
.unwrap();
assert_eq!(
deposit_info.stake_lamports + stake_rent - deposit_fee,
deposit_info.pool_tokens,
"stake {} rent {} deposit fee {} pool tokens {}",
deposit_info.stake_lamports,
stake_rent,
deposit_fee,
deposit_info.pool_tokens
);
let withdrawal_fee = stake_pool_accounts.calculate_withdrawal_fee(deposit_info.pool_tokens);
// Check tokens used
let user_token_balance = get_token_balance(
&mut context.banks_client,
&deposit_info.pool_account.pubkey(),
)
.await;
assert_eq!(user_token_balance, 0);
// Check reserve stake account balance
let reserve_stake_account = get_account(
&mut context.banks_client,
&stake_pool_accounts.reserve_stake.pubkey(),
)
.await;
let stake_state = deserialize::<stake::state::StakeState>(&reserve_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
assert_eq!(
initial_reserve_lamports + meta.rent_exempt_reserve + withdrawal_fee + deposit_fee,
reserve_stake_account.lamports
);
// Check user recipient stake account balance
let user_stake_recipient_account =
get_account(&mut context.banks_client, &withdraw_destination.pubkey()).await;
assert_eq!(
user_stake_recipient_account.lamports,
initial_stake_lamports + deposit_info.stake_lamports + stake_rent
- withdrawal_fee
- deposit_fee
);
}
#[tokio::test]
async fn success_with_preferred_validator() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
stake_pool_accounts
.set_preferred_validator(
&mut banks_client,
&payer,
&recent_blockhash,
instruction::PreferredValidatorType::Withdraw,
Some(validator_stake.vote.pubkey()),
)
.await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
tokens_to_burn,
)
.await;
assert!(error.is_none());
}
#[tokio::test]
async fn fail_with_wrong_preferred_withdraw() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_burn,
) = setup().await;
let preferred_validator = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
stake_pool_accounts
.set_preferred_validator(
&mut banks_client,
&payer,
&recent_blockhash,
instruction::PreferredValidatorType::Withdraw,
Some(preferred_validator.vote.pubkey()),
)
.await;
// preferred is empty, this works
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
tokens_to_burn,
)
.await;
assert!(error.is_none());
// deposit into preferred, then fail
let _preferred_deposit = simple_deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&preferred_validator,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
// Create stake account to withdraw to
let user_stake_recipient = Keypair::new();
create_blank_stake_account(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient,
)
.await;
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.stake_account,
&new_authority,
tokens_to_burn,
)
.await
.unwrap()
.unwrap();
match error {
TransactionError::InstructionError(_, InstructionError::Custom(error_index)) => {
assert_eq!(
error_index,
StakePoolError::IncorrectWithdrawVoteAddress as u32
);
}
_ => panic!("Wrong error occurs while try to make a deposit with wrong stake program ID"),
}
}
#[tokio::test]
async fn success_withdraw_from_transient() {
let mut context = program_test().start_with_context().await;
let stake_pool_accounts = StakePoolAccounts::new();
let initial_reserve_lamports = 1;
stake_pool_accounts
.initialize_stake_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
initial_reserve_lamports,
)
.await
.unwrap();
// add a preferred withdraw validator, keep it empty, to be sure that this works
let preferred_validator = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
stake_pool_accounts
.set_preferred_validator(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
instruction::PreferredValidatorType::Withdraw,
Some(preferred_validator.vote.pubkey()),
)
.await;
let validator_stake = simple_add_validator_to_pool(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
)
.await;
let deposit_lamports = TEST_STAKE_AMOUNT;
let rent = context.banks_client.get_rent().await.unwrap();
let stake_rent = rent.minimum_balance(std::mem::size_of::<stake::state::StakeState>());
let deposit_info = simple_deposit_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&stake_pool_accounts,
&validator_stake,
deposit_lamports,
)
.await
.unwrap();
// Delegate tokens for burning during withdraw
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&deposit_info.pool_account.pubkey(),
&deposit_info.authority,
&user_transfer_authority.pubkey(),
deposit_info.pool_tokens,
)
.await;
// decrease minimum stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
stake_rent + 1,
validator_stake.transient_stake_seed,
)
.await;
assert!(error.is_none());
let withdraw_destination = Keypair::new();
let withdraw_destination_authority = Pubkey::new_unique();
let _initial_stake_lamports = create_blank_stake_account(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination,
)
.await;
// fail withdrawing from transient, still a lamport in the validator stake account
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.transient_stake_account,
&withdraw_destination_authority,
deposit_info.pool_tokens / 2,
)
.await
.unwrap()
.unwrap();
assert_eq!(
error,
TransactionError::InstructionError(
0,
InstructionError::Custom(StakePoolError::InvalidStakeAccountAddress as u32)
)
);
// warp forward to deactivation
let first_normal_slot = context.genesis_config().epoch_schedule.first_normal_slot;
let slots_per_epoch = context.genesis_config().epoch_schedule.slots_per_epoch;
context
.warp_to_slot(first_normal_slot + slots_per_epoch)
.unwrap();
// update to merge deactivated stake into reserve
stake_pool_accounts
.update_all(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&[
preferred_validator.vote.pubkey(),
validator_stake.vote.pubkey(),
],
false,
)
.await;
// decrease rest of stake
let error = stake_pool_accounts
.decrease_validator_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&validator_stake.stake_account,
&validator_stake.transient_stake_account,
deposit_lamports - 1,
validator_stake.transient_stake_seed,
)
.await;
assert!(error.is_none());
// nothing left in the validator stake account (or any others), so withdrawing
// from the transient account is ok!
let error = stake_pool_accounts
.withdraw_stake(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&withdraw_destination.pubkey(),
&user_transfer_authority,
&deposit_info.pool_account.pubkey(),
&validator_stake.transient_stake_account,
&withdraw_destination_authority,
deposit_info.pool_tokens / 4,
)
.await;
assert!(error.is_none());
}
#[tokio::test]
async fn success_withdraw_all_fee_tokens() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
validator_stake_account,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
) = setup().await;
// move tokens to fee account
transfer_spl_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&deposit_info.pool_account.pubkey(),
&stake_pool_accounts.pool_fee_account.pubkey(),
&user_transfer_authority,
tokens_to_withdraw,
)
.await;
let fee_tokens = get_token_balance(
&mut banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts.pool_fee_account.pubkey(),
&stake_pool_accounts.manager,
&user_transfer_authority.pubkey(),
fee_tokens,
)
.await;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&stake_pool_accounts.pool_fee_account.pubkey(),
&validator_stake_account.stake_account,
&new_authority,
fee_tokens,
)
.await;
assert!(error.is_none());
// Check balance is 0
let fee_tokens = get_token_balance(
&mut banks_client,
&stake_pool_accounts.pool_fee_account.pubkey(),
)
.await;
assert_eq!(fee_tokens, 0);
}
#[tokio::test]
async fn success_empty_out_stake_with_fee() {
let (
mut banks_client,
payer,
recent_blockhash,
stake_pool_accounts,
_,
deposit_info,
user_transfer_authority,
user_stake_recipient,
tokens_to_withdraw,
) = setup().await;
// add another validator and deposit into it
let other_validator_stake_account = simple_add_validator_to_pool(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
)
.await;
let other_deposit_info = simple_deposit_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&stake_pool_accounts,
&other_validator_stake_account,
TEST_STAKE_AMOUNT,
)
.await
.unwrap();
// move tokens to new account
transfer_spl_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&deposit_info.pool_account.pubkey(),
&other_deposit_info.pool_account.pubkey(),
&user_transfer_authority,
tokens_to_withdraw,
)
.await;
let user_tokens =
get_token_balance(&mut banks_client, &other_deposit_info.pool_account.pubkey()).await;
let user_transfer_authority = Keypair::new();
delegate_tokens(
&mut banks_client,
&payer,
&recent_blockhash,
&other_deposit_info.pool_account.pubkey(),
&other_deposit_info.authority,
&user_transfer_authority.pubkey(),
user_tokens,
)
.await;
// calculate exactly how much to withdraw, given the fee, to get the account
// down to 0, using an inverse fee calculation
let validator_stake_account = get_account(
&mut banks_client,
&other_validator_stake_account.stake_account,
)
.await;
let stake_state =
deserialize::<stake::state::StakeState>(&validator_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
let lamports_to_withdraw = validator_stake_account.lamports - minimum_stake_lamports(&meta);
let stake_pool_account =
get_account(&mut banks_client, &stake_pool_accounts.stake_pool.pubkey()).await;
let stake_pool =
try_from_slice_unchecked::<state::StakePool>(stake_pool_account.data.as_slice()).unwrap();
let fee = stake_pool.stake_withdrawal_fee;
let inverse_fee = state::Fee {
numerator: fee.denominator - fee.numerator,
denominator: fee.denominator,
};
let pool_tokens_to_withdraw =
lamports_to_withdraw * inverse_fee.denominator / inverse_fee.numerator;
let new_authority = Pubkey::new_unique();
let error = stake_pool_accounts
.withdraw_stake(
&mut banks_client,
&payer,
&recent_blockhash,
&user_stake_recipient.pubkey(),
&user_transfer_authority,
&other_deposit_info.pool_account.pubkey(),
&other_validator_stake_account.stake_account,
&new_authority,
pool_tokens_to_withdraw,
)
.await;
assert!(error.is_none());
// Check balance of validator stake account is MINIMUM + rent-exemption
let validator_stake_account = get_account(
&mut banks_client,
&other_validator_stake_account.stake_account,
)
.await;
let stake_state =
deserialize::<stake::state::StakeState>(&validator_stake_account.data).unwrap();
let meta = stake_state.meta().unwrap();
assert_eq!(
validator_stake_account.lamports,
minimum_stake_lamports(&meta)
);
}