//! Program state processor use crate::{ error::Error, instruction::{InitArgs, StakePoolInstruction}, stake, state::{StakePool, State}, }; use num_traits::FromPrimitive; #[cfg(not(target_arch = "bpf"))] use solana_program::instruction::Instruction; #[cfg(target_arch = "bpf")] use solana_program::program::invoke_signed; use solana_program::{ account_info::next_account_info, account_info::AccountInfo, decode_error::DecodeError, entrypoint::ProgramResult, info, program_error::PrintProgramError, program_error::ProgramError, pubkey::Pubkey, }; use std::convert::TryFrom; /// Program state handler. pub struct Processor {} impl Processor { /// Suffix for deposit authority seed pub const AUTHORITY_DEPOSIT: &'static [u8] = b"deposit"; /// Suffix for withdraw authority seed pub const AUTHORITY_WITHDRAW: &'static [u8] = b"withdraw"; /// Calculates the authority id by generating a program address. pub fn authority_id( program_id: &Pubkey, my_info: &Pubkey, authority_type: &[u8], bump_seed: u8, ) -> Result { Pubkey::create_program_address( &[&my_info.to_bytes()[..32], authority_type, &[bump_seed]], program_id, ) .or(Err(Error::InvalidProgramAddress)) } /// Generates seed bump for stake pool authorities pub fn find_authority_bump_seed( program_id: &Pubkey, my_info: &Pubkey, authority_type: &[u8], ) -> u8 { let (_pubkey, bump_seed) = Pubkey::find_program_address(&[&my_info.to_bytes()[..32], authority_type], program_id); bump_seed } /// Issue a stake_split instruction. pub fn stake_split<'a>( stake_pool: &Pubkey, stake_account: AccountInfo<'a>, authority: AccountInfo<'a>, authority_type: &[u8], bump_seed: u8, amount: u64, split_stake: AccountInfo<'a>, ) -> Result<(), ProgramError> { let me_bytes = stake_pool.to_bytes(); let authority_signature_seeds = [&me_bytes[..32], authority_type, &[bump_seed]]; let signers = &[&authority_signature_seeds[..]]; let ix = stake::split_only(stake_account.key, authority.key, amount, split_stake.key); invoke_signed(&ix, &[stake_account, authority, split_stake], signers) } /// Issue a stake_set_owner instruction. pub fn stake_authorize<'a>( stake_pool: &Pubkey, stake_account: AccountInfo<'a>, authority: AccountInfo<'a>, authority_type: &[u8], bump_seed: u8, new_staker: &Pubkey, staker_auth: stake::StakeAuthorize, ) -> Result<(), ProgramError> { let me_bytes = stake_pool.to_bytes(); let authority_signature_seeds = [&me_bytes[..32], authority_type, &[bump_seed]]; let signers = &[&authority_signature_seeds[..]]; let ix = stake::authorize(stake_account.key, authority.key, new_staker, staker_auth); invoke_signed(&ix, &[stake_account, authority], signers) } /// Issue a spl_token `Burn` instruction. #[allow(clippy::too_many_arguments)] pub fn token_burn<'a>( stake_pool: &Pubkey, token_program: AccountInfo<'a>, burn_account: AccountInfo<'a>, mint: AccountInfo<'a>, authority: AccountInfo<'a>, authority_type: &[u8], bump_seed: u8, amount: u64, ) -> Result<(), ProgramError> { let me_bytes = stake_pool.to_bytes(); let authority_signature_seeds = [&me_bytes[..32], authority_type, &[bump_seed]]; let signers = &[&authority_signature_seeds[..]]; let ix = spl_token::instruction::burn( token_program.key, burn_account.key, mint.key, authority.key, &[], amount, )?; invoke_signed( &ix, &[burn_account, mint, authority, token_program], signers, ) } /// Issue a spl_token `MintTo` instruction. #[allow(clippy::too_many_arguments)] pub fn token_mint_to<'a>( stake_pool: &Pubkey, token_program: AccountInfo<'a>, mint: AccountInfo<'a>, destination: AccountInfo<'a>, authority: AccountInfo<'a>, authority_type: &[u8], bump_seed: u8, amount: u64, ) -> Result<(), ProgramError> { let me_bytes = stake_pool.to_bytes(); let authority_signature_seeds = [&me_bytes[..32], authority_type, &[bump_seed]]; let signers = &[&authority_signature_seeds[..]]; let ix = spl_token::instruction::mint_to( token_program.key, mint.key, destination.key, authority.key, &[], amount, )?; invoke_signed(&ix, &[mint, destination, authority, token_program], signers) } /// Processes an [Initialize](enum.Instruction.html). pub fn process_initialize( program_id: &Pubkey, init: InitArgs, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; let owner_info = next_account_info(account_info_iter)?; let pool_mint_info = next_account_info(account_info_iter)?; let owner_fee_info = next_account_info(account_info_iter)?; let token_program_info = next_account_info(account_info_iter)?; // Stake pool account should not be already initialized if State::Unallocated != State::deserialize(&stake_pool_info.data.borrow())? { return Err(Error::AlreadyInUse.into()); } // Numerator should be smaller than or equal to denominator (fee <= 1) if init.fee.numerator > init.fee.denominator { return Err(Error::FeeTooHigh.into()); } let stake_pool = State::Init(StakePool { owner: *owner_info.key, deposit_bump_seed: Self::find_authority_bump_seed( program_id, stake_pool_info.key, Self::AUTHORITY_DEPOSIT, ), withdraw_bump_seed: Self::find_authority_bump_seed( program_id, stake_pool_info.key, Self::AUTHORITY_WITHDRAW, ), pool_mint: *pool_mint_info.key, owner_fee_account: *owner_fee_info.key, token_program_id: *token_program_info.key, stake_total: 0, pool_total: 0, fee: init.fee, }); stake_pool.serialize(&mut stake_pool_info.data.borrow_mut()) } /// Processes [Deposit](enum.Instruction.html). pub fn process_deposit(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); // Stake pool let stake_pool_info = next_account_info(account_info_iter)?; // Stake pool deposit authority let deposit_info = next_account_info(account_info_iter)?; // Stake pool withdraw authority let withdraw_info = next_account_info(account_info_iter)?; // Stake account to join the pool (withdraw should be set to stake pool deposit) let stake_info = next_account_info(account_info_iter)?; // User account to receive pool tokens let dest_user_info = next_account_info(account_info_iter)?; // Account to receive pool fee tokens let owner_fee_info = next_account_info(account_info_iter)?; // Pool token mint account let pool_mint_info = next_account_info(account_info_iter)?; // Pool token program id let token_program_info = next_account_info(account_info_iter)?; let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; if *withdraw_info.key != Self::authority_id( program_id, stake_pool_info.key, Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, )? { return Err(Error::InvalidProgramAddress.into()); } if *deposit_info.key != Self::authority_id( program_id, stake_pool_info.key, Self::AUTHORITY_DEPOSIT, stake_pool.deposit_bump_seed, )? { return Err(Error::InvalidProgramAddress.into()); } if stake_pool.owner_fee_account != *owner_fee_info.key { return Err(Error::InvalidInput.into()); } if stake_pool.token_program_id != *token_program_info.key { return Err(Error::InvalidInput.into()); } let stake_lamports = **stake_info.lamports.borrow(); let pool_amount = stake_pool .calc_pool_deposit_amount(stake_lamports) .ok_or(Error::CalculationFailure)?; let fee_amount = stake_pool .calc_fee_amount(pool_amount) .ok_or(Error::CalculationFailure)?; let user_amount = pool_amount .checked_sub(fee_amount) .ok_or(Error::CalculationFailure)?; Self::stake_authorize( stake_pool_info.key, stake_info.clone(), deposit_info.clone(), Self::AUTHORITY_DEPOSIT, stake_pool.deposit_bump_seed, withdraw_info.key, stake::StakeAuthorize::Withdrawer, )?; let user_amount = ::try_from(user_amount).or(Err(Error::CalculationFailure))?; Self::token_mint_to( stake_pool_info.key, token_program_info.clone(), pool_mint_info.clone(), dest_user_info.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, user_amount, )?; let fee_amount = ::try_from(fee_amount).or(Err(Error::CalculationFailure))?; Self::token_mint_to( stake_pool_info.key, token_program_info.clone(), pool_mint_info.clone(), owner_fee_info.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, fee_amount as u64, )?; let pool_amount = ::try_from(pool_amount).or(Err(Error::CalculationFailure))?; stake_pool.pool_total += pool_amount; stake_pool.stake_total += stake_lamports; State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } /// Processes [Withdraw](enum.Instruction.html). pub fn process_withdraw( program_id: &Pubkey, stake_amount: u64, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); // Stake pool let stake_pool_info = next_account_info(account_info_iter)?; // Stake pool withdraw authority let withdraw_info = next_account_info(account_info_iter)?; // Stake account to split let stake_split_from = next_account_info(account_info_iter)?; // Unitialized stake account to receive withdrawal let stake_split_to = next_account_info(account_info_iter)?; // User account to set as a new withdraw authority let user_stake_authority = next_account_info(account_info_iter)?; // User account with pool tokens to burn from let burn_from_info = next_account_info(account_info_iter)?; // Pool token mint account let pool_mint_info = next_account_info(account_info_iter)?; // Pool token program id let token_program_info = next_account_info(account_info_iter)?; let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; if *withdraw_info.key != Self::authority_id( program_id, stake_pool_info.key, Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, )? { return Err(Error::InvalidProgramAddress.into()); } if stake_pool.token_program_id != *token_program_info.key { return Err(Error::InvalidInput.into()); } let pool_amount = stake_pool .calc_pool_withdraw_amount(stake_amount) .ok_or(Error::CalculationFailure)?; let pool_amount = ::try_from(pool_amount).or(Err(Error::CalculationFailure))?; Self::stake_split( stake_pool_info.key, stake_split_from.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, stake_amount, stake_split_to.clone(), )?; Self::stake_authorize( stake_pool_info.key, stake_split_to.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, user_stake_authority.key, stake::StakeAuthorize::Withdrawer, )?; Self::token_burn( stake_pool_info.key, token_program_info.clone(), burn_from_info.clone(), pool_mint_info.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, pool_amount, )?; stake_pool.pool_total -= pool_amount; stake_pool.stake_total -= stake_amount; State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } /// Processes [Claim](enum.Instruction.html). pub fn process_claim(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); // Stake pool let stake_pool_info = next_account_info(account_info_iter)?; // Stake pool withdraw authority let withdraw_info = next_account_info(account_info_iter)?; // Stake account to claim let stake_to_claim = next_account_info(account_info_iter)?; // User account to set as a new withdraw authority let user_stake_authority = next_account_info(account_info_iter)?; // User account with pool tokens to burn from let burn_from_info = next_account_info(account_info_iter)?; // Pool token account let pool_mint_info = next_account_info(account_info_iter)?; // Pool token program id let token_program_info = next_account_info(account_info_iter)?; let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; if *withdraw_info.key != Self::authority_id( program_id, stake_pool_info.key, Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, )? { return Err(Error::InvalidProgramAddress.into()); } if stake_pool.token_program_id != *token_program_info.key { return Err(Error::InvalidInput.into()); } let stake_amount = **stake_to_claim.lamports.borrow(); let pool_amount = stake_pool .calc_pool_withdraw_amount(stake_amount) .ok_or(Error::CalculationFailure)?; let pool_amount = ::try_from(pool_amount).or(Err(Error::CalculationFailure))?; Self::stake_authorize( stake_pool_info.key, stake_to_claim.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, user_stake_authority.key, stake::StakeAuthorize::Withdrawer, )?; Self::token_burn( stake_pool_info.key, token_program_info.clone(), burn_from_info.clone(), pool_mint_info.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, pool_amount, )?; stake_pool.pool_total -= pool_amount; stake_pool.stake_total -= stake_amount; State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } /// Processes [SetStakeAuthority](enum.Instruction.html). pub fn process_set_staking_auth( program_id: &Pubkey, accounts: &[AccountInfo], ) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; let owner_info = next_account_info(account_info_iter)?; let withdraw_info = next_account_info(account_info_iter)?; let stake_info = next_account_info(account_info_iter)?; let staker_info = next_account_info(account_info_iter)?; let stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; if *owner_info.key != stake_pool.owner { return Err(Error::InvalidInput.into()); } if !owner_info.is_signer { return Err(Error::InvalidInput.into()); } if *withdraw_info.key != Self::authority_id( program_id, stake_pool_info.key, Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, )? { return Err(Error::InvalidProgramAddress.into()); } Self::stake_authorize( stake_pool_info.key, stake_info.clone(), withdraw_info.clone(), Self::AUTHORITY_WITHDRAW, stake_pool.withdraw_bump_seed, staker_info.key, stake::StakeAuthorize::Staker, )?; Ok(()) } /// Processes [SetOwner](enum.Instruction.html). pub fn process_set_owner(_program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult { let account_info_iter = &mut accounts.iter(); let stake_pool_info = next_account_info(account_info_iter)?; let owner_info = next_account_info(account_info_iter)?; let new_owner_info = next_account_info(account_info_iter)?; let new_owner_fee_info = next_account_info(account_info_iter)?; let mut stake_pool = State::deserialize(&stake_pool_info.data.borrow())?.stake_pool()?; if *owner_info.key != stake_pool.owner { return Err(Error::InvalidInput.into()); } if !owner_info.is_signer { return Err(Error::InvalidInput.into()); } stake_pool.owner = *new_owner_info.key; stake_pool.owner_fee_account = *new_owner_fee_info.key; State::Init(stake_pool).serialize(&mut stake_pool_info.data.borrow_mut())?; Ok(()) } /// Processes [Instruction](enum.Instruction.html). pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], input: &[u8]) -> ProgramResult { let instruction = StakePoolInstruction::deserialize(input)?; match instruction { StakePoolInstruction::Initialize(init) => { info!("Instruction: Init"); Self::process_initialize(program_id, init, accounts) } StakePoolInstruction::Deposit => { info!("Instruction: Deposit"); Self::process_deposit(program_id, accounts) } StakePoolInstruction::Withdraw(amount) => { info!("Instruction: Withdraw"); Self::process_withdraw(program_id, amount, accounts) } StakePoolInstruction::Claim => { info!("Instruction: Claim"); Self::process_claim(program_id, accounts) } StakePoolInstruction::SetStakingAuthority => { info!("Instruction: SetStakingAuthority"); Self::process_set_staking_auth(program_id, accounts) } StakePoolInstruction::SetOwner => { info!("Instruction: SetOwner"); Self::process_set_owner(program_id, accounts) } } } } // Test program id for the stake-pool program. #[cfg(not(target_arch = "bpf"))] const STAKE_POOL_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]); // Test program id for the token program. #[cfg(not(target_arch = "bpf"))] const TOKEN_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]); /// Routes invokes to the token program, used for testing. /// TODO add routing to stake program for testing #[cfg(not(target_arch = "bpf"))] pub fn invoke_signed<'a>( instruction: &Instruction, account_infos: &[AccountInfo<'a>], signers_seeds: &[&[&[u8]]], ) -> ProgramResult { // mimic check for token program in accounts if !account_infos.iter().any(|x| *x.key == TOKEN_PROGRAM_ID) { return Err(ProgramError::InvalidAccountData); } let mut new_account_infos = vec![]; for meta in instruction.accounts.iter() { for account_info in account_infos.iter() { if meta.pubkey == *account_info.key { let mut new_account_info = account_info.clone(); for seeds in signers_seeds.iter() { let signer = Pubkey::create_program_address(seeds, &STAKE_POOL_PROGRAM_ID).unwrap(); if *account_info.key == signer { new_account_info.is_signer = true; } } new_account_infos.push(new_account_info); } } } spl_token::processor::Processor::process( &instruction.program_id, &new_account_infos, &instruction.data, ) } impl PrintProgramError for Error { fn print(&self) where E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, { match self { Error::AlreadyInUse => info!("Error: AlreadyInUse"), Error::InvalidProgramAddress => info!("Error: InvalidProgramAddress"), Error::InvalidOwner => info!("Error: InvalidOwner"), Error::ExpectedToken => info!("Error: ExpectedToken"), Error::ExpectedAccount => info!("Error: ExpectedAccount"), Error::InvalidSupply => info!("Error: InvalidSupply"), Error::InvalidDelegate => info!("Error: InvalidDelegate"), Error::InvalidState => info!("Error: InvalidState"), Error::InvalidInput => info!("Error: InvalidInput"), Error::InvalidOutput => info!("Error: InvalidOutput"), Error::CalculationFailure => info!("Error: CalculationFailure"), Error::FeeTooHigh => info!("Error: FeeTooHigh"), } } } #[cfg(test)] mod tests { use super::*; use crate::instruction::initialize; use crate::instruction::Fee; use crate::instruction::InitArgs; use core::mem::size_of; use solana_program::{ account::Account, account_info::create_is_signer_account_infos, instruction::Instruction, program_pack::Pack, rent::Rent, sysvar::rent, }; use spl_token::{ instruction::{initialize_account, initialize_mint}, processor::Processor as SplProcessor, state::{Account as SplAccount, Mint as SplMint}, }; fn pubkey_rand() -> Pubkey { Pubkey::new(&rand::random::<[u8; 32]>()) } fn do_process_instruction( instruction: Instruction, accounts: Vec<&mut Account>, ) -> ProgramResult { let mut meta = instruction .accounts .iter() .zip(accounts) .map(|(account_meta, account)| (&account_meta.pubkey, account_meta.is_signer, account)) .collect::>(); let account_infos = create_is_signer_account_infos(&mut meta); if instruction.program_id == STAKE_POOL_PROGRAM_ID { Processor::process(&instruction.program_id, &account_infos, &instruction.data) } else { SplProcessor::process(&instruction.program_id, &account_infos, &instruction.data) } } fn account_minimum_balance() -> u64 { Rent::default().minimum_balance(SplAccount::get_packed_len()) } fn mint_minimum_balance() -> u64 { Rent::default().minimum_balance(SplMint::get_packed_len()) } fn mint_token( program_id: &Pubkey, mint_key: &Pubkey, mut mint_account: &mut Account, authority_key: &Pubkey, amount: u64, ) -> (Pubkey, Account) { let account_key = pubkey_rand(); let mut account_account = Account::new( account_minimum_balance(), SplAccount::get_packed_len(), &program_id, ); let mut authority_account = Account::default(); let mut rent_sysvar_account = rent::create_account(1, &Rent::free()); // create account do_process_instruction( initialize_account(&program_id, &account_key, &mint_key, authority_key).unwrap(), vec![ &mut account_account, &mut mint_account, &mut authority_account, &mut rent_sysvar_account, ], ) .unwrap(); do_process_instruction( spl_token::instruction::mint_to( &program_id, &mint_key, &account_key, &authority_key, &[], amount, ) .unwrap(), vec![ &mut mint_account, &mut account_account, &mut authority_account, ], ) .unwrap(); (account_key, account_account) } fn create_mint(program_id: &Pubkey, authority_key: &Pubkey) -> (Pubkey, Account) { let mint_key = pubkey_rand(); let mut mint_account = Account::new( mint_minimum_balance(), SplMint::get_packed_len(), &program_id, ); let mut rent_sysvar_account = rent::create_account(1, &Rent::free()); // create token mint do_process_instruction( initialize_mint(&program_id, &mint_key, authority_key, None, 2).unwrap(), vec![&mut mint_account, &mut rent_sysvar_account], ) .unwrap(); (mint_key, mint_account) } #[test] fn test_initialize() { let stake_pool_key = pubkey_rand(); let mut stake_pool_account = Account::new(0, size_of::(), &STAKE_POOL_PROGRAM_ID); let owner_key = pubkey_rand(); let mut owner_account = Account::default(); let authority_key = pubkey_rand(); let (pool_mint_key, mut pool_mint_account) = create_mint(&TOKEN_PROGRAM_ID, &authority_key); let (pool_token_key, mut pool_token_account) = mint_token( &TOKEN_PROGRAM_ID, &pool_mint_key, &mut pool_mint_account, &authority_key, 0, ); // StakePool Init do_process_instruction( initialize( &STAKE_POOL_PROGRAM_ID, &stake_pool_key, &owner_key, &pool_mint_key, &pool_token_key, &TOKEN_PROGRAM_ID, InitArgs { fee: Fee { denominator: 10, numerator: 2, }, }, ) .unwrap(), vec![ &mut stake_pool_account, &mut owner_account, &mut pool_mint_account, &mut pool_token_account, &mut Account::default(), ], ) .unwrap(); } }