#![allow(clippy::arithmetic_side_effects)]
#![allow(clippy::items_after_test_module)]
mod helpers;
use {
helpers::*,
solana_instruction::Instruction,
solana_program_error::ProgramError,
solana_program_test::*,
solana_pubkey::pubkey,
solana_pubkey::Pubkey,
solana_signer::Signer,
solana_stake_interface::program as stake_program,
solana_system_interface::program as system_program,
solana_transaction::Transaction,
spl_single_pool::{
error::SinglePoolError,
id,
instruction::{self, SinglePoolInstruction},
},
spl_token_interface as spl_token,
test_case::test_matrix,
};
#[derive(Clone, Debug, PartialEq, Eq)]
enum TestMode {
InitializePool,
DepositStake,
WithdrawStake,
DepositSol,
}
async fn build_instructions(
context: &mut ProgramTestContext,
accounts: &SinglePoolAccounts,
test_mode: TestMode,
) -> (Vec<Instruction>, usize) {
let initialize_instructions = if test_mode == TestMode::InitializePool {
let slot = context.genesis_config().epoch_schedule.first_normal_slot + 1;
context.warp_to_slot(slot).unwrap();
create_vote(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&accounts.validator,
&accounts.voter.pubkey(),
&accounts.withdrawer.pubkey(),
&accounts.vote_account,
)
.await;
transfer(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
&accounts.alice.pubkey(),
USER_STARTING_LAMPORTS,
)
.await;
let rent = context.banks_client.get_rent().await.unwrap();
let minimum_pool_balance = get_minimum_pool_balance(
&mut context.banks_client,
&context.payer,
&context.last_blockhash,
)
.await;
instruction::initialize(
&id(),
&accounts.vote_account.pubkey(),
&accounts.alice.pubkey(),
&rent,
minimum_pool_balance,
)
} else {
accounts
.initialize_for_deposit(context, TEST_STAKE_AMOUNT, None)
.await;
advance_epoch(context).await;
vec![]
};
let deposit_stake_instructions = instruction::deposit(
&id(),
&accounts.pool,
&accounts.alice_stake.pubkey(),
&accounts.alice_token,
&accounts.alice.pubkey(),
&accounts.alice.pubkey(),
);
let withdraw_stake_instructions = if test_mode == TestMode::WithdrawStake {
let transaction = Transaction::new_signed_with_payer(
&deposit_stake_instructions,
Some(&accounts.alice.pubkey()),
&[&accounts.alice],
context.last_blockhash,
);
context
.banks_client
.process_transaction(transaction)
.await
.unwrap();
create_blank_stake_account(
&mut context.banks_client,
&context.payer,
&accounts.alice,
&context.last_blockhash,
&accounts.alice_stake,
)
.await;
instruction::withdraw(
&id(),
&accounts.pool,
&accounts.alice_stake.pubkey(),
&accounts.alice.pubkey(),
&accounts.alice_token,
&accounts.alice.pubkey(),
get_token_balance(&mut context.banks_client, &accounts.alice_token).await,
)
} else {
vec![]
};
let deposit_sol_instructions = instruction::deposit_liquid(
&id(),
&accounts.vote_account.pubkey(),
&accounts.alice.pubkey(),
&accounts.alice.pubkey(),
&accounts.alice_token,
TEST_STAKE_AMOUNT,
);
let (instructions, index, enum_tag) = match test_mode {
TestMode::InitializePool => (initialize_instructions, 4, 0),
TestMode::DepositStake => (deposit_stake_instructions, 2, 2),
TestMode::WithdrawStake => (withdraw_stake_instructions, 1, 3),
TestMode::DepositSol => (deposit_sol_instructions, 1, 7),
};
assert_eq!(instructions[index].program_id, id());
assert_eq!(instructions[index].data[0], enum_tag);
(instructions, index)
}
#[test_matrix(
[StakeProgramVersion::Stable, StakeProgramVersion::Beta, StakeProgramVersion::Edge],
[TestMode::InitializePool, TestMode::DepositStake, TestMode::WithdrawStake, TestMode::DepositSol]
)]
#[tokio::test]
async fn fail_account_checks(stake_version: StakeProgramVersion, test_mode: TestMode) {
let Some(program_test) = program_test(stake_version) else {
return;
};
let mut context = program_test.start_with_context().await;
let accounts = SinglePoolAccounts::default();
let (instructions, i) = build_instructions(&mut context, &accounts, test_mode).await;
let bad_pubkey = pubkey!("BAD1111111111111111111111111111111111111111");
for j in 0..instructions[i].accounts.len() {
let mut instructions = instructions.clone();
let instruction_pubkey = instructions[i].accounts[j].pubkey;
if instruction_pubkey == accounts.alice.pubkey() {
continue;
}
instructions[i].accounts[j].pubkey = bad_pubkey;
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&accounts.alice.pubkey()),
&[&accounts.alice],
context.last_blockhash,
);
let e = context
.banks_client
.process_transaction(transaction)
.await
.unwrap_err();
if instruction_pubkey == accounts.vote_account.pubkey() {
check_error(e, ProgramError::IncorrectProgramId)
} else if instruction_pubkey == accounts.pool {
check_error(e, SinglePoolError::InvalidPoolAccount)
} else if instruction_pubkey == accounts.stake_account {
check_error(e, SinglePoolError::InvalidPoolStakeAccount)
} else if instruction_pubkey == accounts.onramp_account {
check_error(e, SinglePoolError::InvalidPoolOnRampAccount)
} else if instruction_pubkey == accounts.stake_authority {
check_error(e, SinglePoolError::InvalidPoolStakeAuthority)
} else if instruction_pubkey == accounts.mint_authority {
check_error(e, SinglePoolError::InvalidPoolMintAuthority)
} else if instruction_pubkey == accounts.mpl_authority {
check_error(e, SinglePoolError::InvalidPoolMplAuthority)
} else if instruction_pubkey == accounts.mint {
check_error(e, SinglePoolError::InvalidPoolMint)
} else if [system_program::id(), spl_token::id(), stake_program::id()]
.contains(&instruction_pubkey)
{
check_error(e, ProgramError::IncorrectProgramId)
}
}
let transaction = Transaction::new_signed_with_payer(
&instructions,
Some(&accounts.alice.pubkey()),
&[&accounts.alice],
context.last_blockhash,
);
context
.banks_client
.process_transaction(transaction)
.await
.unwrap();
}
fn make_basic_instruction(
accounts: &SinglePoolAccounts,
instruction_type: SinglePoolInstruction,
) -> Instruction {
match instruction_type {
SinglePoolInstruction::InitializePool => {
instruction::initialize_pool(&id(), &accounts.vote_account.pubkey())
}
SinglePoolInstruction::ReplenishPool => {
instruction::replenish_pool(&id(), &accounts.vote_account.pubkey())
}
SinglePoolInstruction::DepositStake => instruction::deposit_stake(
&id(),
&accounts.pool,
&Pubkey::default(),
&Pubkey::default(),
&Pubkey::default(),
),
SinglePoolInstruction::WithdrawStake { .. } => instruction::withdraw_stake(
&id(),
&accounts.pool,
&Pubkey::default(),
&Pubkey::default(),
&Pubkey::default(),
0,
),
SinglePoolInstruction::CreateTokenMetadata => {
instruction::create_token_metadata(&id(), &accounts.pool, &Pubkey::default())
}
SinglePoolInstruction::UpdateTokenMetadata { .. } => instruction::update_token_metadata(
&id(),
&accounts.vote_account.pubkey(),
&accounts.withdrawer.pubkey(),
"".to_string(),
"".to_string(),
"".to_string(),
),
SinglePoolInstruction::InitializePoolOnRamp => {
instruction::initialize_pool_onramp(&id(), &accounts.pool)
}
SinglePoolInstruction::DepositSol { .. } => instruction::deposit_sol(
&id(),
&accounts.vote_account.pubkey(),
&Pubkey::default(),
&Pubkey::default(),
0,
),
}
}
fn is_sorted<T>(data: &[T]) -> bool
where
T: Ord,
{
data.windows(2).all(|w| w[0] <= w[1])
}
#[test]
fn consistent_account_order() {
let accounts = SinglePoolAccounts::default();
let ordering = vec![
accounts.vote_account.pubkey(),
accounts.pool,
accounts.stake_account,
accounts.onramp_account,
accounts.mint,
accounts.stake_authority,
accounts.mint_authority,
accounts.mpl_authority,
];
let instructions = vec![
make_basic_instruction(&accounts, SinglePoolInstruction::InitializePool),
make_basic_instruction(&accounts, SinglePoolInstruction::ReplenishPool),
make_basic_instruction(&accounts, SinglePoolInstruction::DepositStake),
make_basic_instruction(
&accounts,
SinglePoolInstruction::WithdrawStake {
user_stake_authority: Pubkey::default(),
token_amount: 0,
},
),
make_basic_instruction(&accounts, SinglePoolInstruction::CreateTokenMetadata),
make_basic_instruction(
&accounts,
SinglePoolInstruction::UpdateTokenMetadata {
name: "".to_string(),
symbol: "".to_string(),
uri: "".to_string(),
},
),
make_basic_instruction(&accounts, SinglePoolInstruction::InitializePoolOnRamp),
make_basic_instruction(&accounts, SinglePoolInstruction::DepositSol { lamports: 0 }),
];
for instruction in instructions {
let mut indexes = vec![];
for target in &ordering {
if let Some(i) = instruction
.accounts
.iter()
.position(|meta| meta.pubkey == *target)
{
indexes.push(i);
}
}
assert!(is_sorted(&indexes));
}
}