590 lines
21 KiB
Rust
590 lines
21 KiB
Rust
//! Program state processor
|
|
|
|
use crate::{
|
|
error::PoolError,
|
|
instruction::PoolInstruction,
|
|
state::{Decision, Pool, POOL_VERSION},
|
|
};
|
|
use borsh::{BorshDeserialize, BorshSerialize};
|
|
use solana_program::{
|
|
account_info::next_account_info,
|
|
account_info::AccountInfo,
|
|
clock::{Clock, Slot},
|
|
entrypoint::ProgramResult,
|
|
msg,
|
|
program::{invoke, invoke_signed},
|
|
program_error::ProgramError,
|
|
program_pack::{IsInitialized, Pack},
|
|
pubkey::Pubkey,
|
|
rent::Rent,
|
|
sysvar::Sysvar,
|
|
};
|
|
use spl_token::state::{Account, Mint};
|
|
|
|
/// Program state handler.
|
|
pub struct Processor {}
|
|
impl Processor {
|
|
/// Calculates the authority id by generating a program address.
|
|
pub fn authority_id(
|
|
program_id: &Pubkey,
|
|
my_info: &Pubkey,
|
|
bump_seed: u8,
|
|
) -> Result<Pubkey, ProgramError> {
|
|
Pubkey::create_program_address(&[&my_info.to_bytes()[..32], &[bump_seed]], program_id)
|
|
.map_err(|_| PoolError::InvalidAuthorityData.into())
|
|
}
|
|
|
|
/// Transfer tokens with authority
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn transfer<'a>(
|
|
token_program_id: AccountInfo<'a>,
|
|
source_account: AccountInfo<'a>,
|
|
destination_account: AccountInfo<'a>,
|
|
program_authority_account: AccountInfo<'a>,
|
|
user_authority_account: AccountInfo<'a>,
|
|
amount: u64,
|
|
pool_pub_key: &Pubkey,
|
|
bump_seed: u8,
|
|
) -> ProgramResult {
|
|
if program_authority_account.key == user_authority_account.key {
|
|
let me_bytes = pool_pub_key.to_bytes();
|
|
let authority_signature_seeds = [&me_bytes[..32], &[bump_seed]];
|
|
let signers = &[&authority_signature_seeds[..]];
|
|
|
|
invoke_signed(
|
|
&spl_token::instruction::transfer(
|
|
token_program_id.key,
|
|
source_account.key,
|
|
destination_account.key,
|
|
program_authority_account.key,
|
|
&[program_authority_account.key],
|
|
amount,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_id,
|
|
program_authority_account,
|
|
source_account,
|
|
destination_account,
|
|
],
|
|
signers,
|
|
)
|
|
} else {
|
|
invoke(
|
|
&spl_token::instruction::transfer(
|
|
token_program_id.key,
|
|
source_account.key,
|
|
destination_account.key,
|
|
user_authority_account.key,
|
|
&[user_authority_account.key],
|
|
amount,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_id,
|
|
user_authority_account,
|
|
source_account,
|
|
destination_account,
|
|
],
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Mint tokens
|
|
pub fn mint<'a>(
|
|
token_program_id: AccountInfo<'a>,
|
|
mint_account: AccountInfo<'a>,
|
|
destination_account: AccountInfo<'a>,
|
|
authority_account: AccountInfo<'a>,
|
|
amount: u64,
|
|
pool_pub_key: &Pubkey,
|
|
bump_seed: u8,
|
|
) -> ProgramResult {
|
|
let me_bytes = pool_pub_key.to_bytes();
|
|
let authority_signature_seeds = [&me_bytes[..32], &[bump_seed]];
|
|
let signers = &[&authority_signature_seeds[..]];
|
|
|
|
invoke_signed(
|
|
&spl_token::instruction::mint_to(
|
|
token_program_id.key,
|
|
mint_account.key,
|
|
destination_account.key,
|
|
authority_account.key,
|
|
&[authority_account.key],
|
|
amount,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_id,
|
|
mint_account,
|
|
destination_account,
|
|
authority_account,
|
|
],
|
|
signers,
|
|
)
|
|
}
|
|
|
|
/// Burn tokens
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn burn<'a>(
|
|
token_program_id: AccountInfo<'a>,
|
|
source_account: AccountInfo<'a>,
|
|
mint_account: AccountInfo<'a>,
|
|
program_authority_account: AccountInfo<'a>,
|
|
user_authority_account: AccountInfo<'a>,
|
|
amount: u64,
|
|
pool_pub_key: &Pubkey,
|
|
bump_seed: u8,
|
|
) -> ProgramResult {
|
|
if program_authority_account.key == user_authority_account.key {
|
|
let me_bytes = pool_pub_key.to_bytes();
|
|
let authority_signature_seeds = [&me_bytes[..32], &[bump_seed]];
|
|
let signers = &[&authority_signature_seeds[..]];
|
|
|
|
invoke_signed(
|
|
&spl_token::instruction::burn(
|
|
token_program_id.key,
|
|
source_account.key,
|
|
mint_account.key,
|
|
program_authority_account.key,
|
|
&[program_authority_account.key],
|
|
amount,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_id,
|
|
program_authority_account,
|
|
source_account,
|
|
mint_account,
|
|
],
|
|
signers,
|
|
)
|
|
} else {
|
|
invoke(
|
|
&spl_token::instruction::burn(
|
|
token_program_id.key,
|
|
source_account.key,
|
|
mint_account.key,
|
|
user_authority_account.key,
|
|
&[user_authority_account.key],
|
|
amount,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_id,
|
|
user_authority_account,
|
|
source_account,
|
|
mint_account,
|
|
],
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Initialize the pool
|
|
pub fn process_init_pool(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
mint_end_slot: Slot,
|
|
decide_end_slot: Slot,
|
|
bump_seed: u8,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let pool_account_info = next_account_info(account_info_iter)?;
|
|
let authority_info = next_account_info(account_info_iter)?;
|
|
let decider_info = next_account_info(account_info_iter)?;
|
|
let deposit_token_mint_info = next_account_info(account_info_iter)?;
|
|
let deposit_account_info = next_account_info(account_info_iter)?;
|
|
let token_pass_mint_info = next_account_info(account_info_iter)?;
|
|
let token_fail_mint_info = next_account_info(account_info_iter)?;
|
|
let rent_info = next_account_info(account_info_iter)?;
|
|
let rent = &Rent::from_account_info(rent_info)?;
|
|
let token_program_info = next_account_info(account_info_iter)?;
|
|
|
|
let mut pool = Pool::try_from_slice(&pool_account_info.data.borrow())?;
|
|
// Pool account should not be already initialized
|
|
if pool.is_initialized() {
|
|
return Err(PoolError::AlreadyInUse.into());
|
|
}
|
|
|
|
// Check if pool account is rent-exempt
|
|
if !rent.is_exempt(pool_account_info.lamports(), pool_account_info.data_len()) {
|
|
return Err(PoolError::NotRentExempt.into());
|
|
}
|
|
|
|
// Check if deposit token's mint owner is token program
|
|
if deposit_token_mint_info.owner != token_program_info.key {
|
|
return Err(PoolError::InvalidTokenMint.into());
|
|
}
|
|
|
|
// Check if deposit token mint is initialized
|
|
let deposit_token_mint = Mint::unpack(&deposit_token_mint_info.data.borrow())?;
|
|
|
|
// Check if bump seed is correct
|
|
let authority = Self::authority_id(program_id, pool_account_info.key, bump_seed)?;
|
|
if &authority != authority_info.key {
|
|
return Err(PoolError::InvalidAuthorityAccount.into());
|
|
}
|
|
|
|
let deposit_account = Account::unpack_unchecked(&deposit_account_info.data.borrow())?;
|
|
if deposit_account.is_initialized() {
|
|
return Err(PoolError::DepositAccountInUse.into());
|
|
}
|
|
|
|
let token_pass = Mint::unpack_unchecked(&token_pass_mint_info.data.borrow())?;
|
|
if token_pass.is_initialized() {
|
|
return Err(PoolError::TokenMintInUse.into());
|
|
}
|
|
|
|
let token_fail = Mint::unpack_unchecked(&token_fail_mint_info.data.borrow())?;
|
|
if token_fail.is_initialized() {
|
|
return Err(PoolError::TokenMintInUse.into());
|
|
}
|
|
|
|
invoke(
|
|
&spl_token::instruction::initialize_account(
|
|
token_program_info.key,
|
|
deposit_account_info.key,
|
|
deposit_token_mint_info.key,
|
|
authority_info.key,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_info.clone(),
|
|
deposit_account_info.clone(),
|
|
deposit_token_mint_info.clone(),
|
|
authority_info.clone(),
|
|
rent_info.clone(),
|
|
],
|
|
)?;
|
|
|
|
invoke(
|
|
&spl_token::instruction::initialize_mint(
|
|
&spl_token::id(),
|
|
token_pass_mint_info.key,
|
|
authority_info.key,
|
|
None,
|
|
deposit_token_mint.decimals,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_info.clone(),
|
|
token_pass_mint_info.clone(),
|
|
rent_info.clone(),
|
|
],
|
|
)?;
|
|
|
|
invoke(
|
|
&spl_token::instruction::initialize_mint(
|
|
&spl_token::id(),
|
|
token_fail_mint_info.key,
|
|
authority_info.key,
|
|
None,
|
|
deposit_token_mint.decimals,
|
|
)
|
|
.unwrap(),
|
|
&[
|
|
token_program_info.clone(),
|
|
token_fail_mint_info.clone(),
|
|
rent_info.clone(),
|
|
],
|
|
)?;
|
|
|
|
pool.version = POOL_VERSION;
|
|
pool.bump_seed = bump_seed;
|
|
pool.token_program_id = *token_program_info.key;
|
|
pool.deposit_account = *deposit_account_info.key;
|
|
pool.token_pass_mint = *token_pass_mint_info.key;
|
|
pool.token_fail_mint = *token_fail_mint_info.key;
|
|
pool.decider = *decider_info.key;
|
|
pool.mint_end_slot = mint_end_slot;
|
|
pool.decide_end_slot = decide_end_slot;
|
|
pool.decision = Decision::Undecided;
|
|
|
|
pool.serialize(&mut *pool_account_info.data.borrow_mut())
|
|
.map_err(|e| e.into())
|
|
}
|
|
|
|
/// Process Deposit instruction
|
|
pub fn process_deposit(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
amount: u64,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let pool_account_info = next_account_info(account_info_iter)?;
|
|
let authority_account_info = next_account_info(account_info_iter)?;
|
|
let user_transfer_authority_info = next_account_info(account_info_iter)?;
|
|
let user_token_account_info = next_account_info(account_info_iter)?;
|
|
let pool_deposit_token_account_info = next_account_info(account_info_iter)?;
|
|
let token_pass_mint_info = next_account_info(account_info_iter)?;
|
|
let token_fail_mint_info = next_account_info(account_info_iter)?;
|
|
let token_pass_destination_account_info = next_account_info(account_info_iter)?;
|
|
let token_fail_destination_account_info = next_account_info(account_info_iter)?;
|
|
let clock_info = next_account_info(account_info_iter)?;
|
|
let clock = &Clock::from_account_info(clock_info)?;
|
|
let token_program_id_info = next_account_info(account_info_iter)?;
|
|
|
|
if amount == 0 {
|
|
return Err(PoolError::InvalidAmount.into());
|
|
}
|
|
|
|
let pool = Pool::try_from_slice(&pool_account_info.data.borrow())?;
|
|
|
|
if clock.slot > pool.mint_end_slot {
|
|
return Err(PoolError::InvalidSlotForDeposit.into());
|
|
}
|
|
|
|
let authority_pub_key =
|
|
Self::authority_id(program_id, pool_account_info.key, pool.bump_seed)?;
|
|
if *authority_account_info.key != authority_pub_key {
|
|
return Err(PoolError::InvalidAuthorityAccount.into());
|
|
}
|
|
|
|
// Transfer deposit tokens from user's account to our deposit account
|
|
Self::transfer(
|
|
token_program_id_info.clone(),
|
|
user_token_account_info.clone(),
|
|
pool_deposit_token_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
user_transfer_authority_info.clone(),
|
|
amount,
|
|
pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
|
|
// Mint PASS tokens to user account
|
|
Self::mint(
|
|
token_program_id_info.clone(),
|
|
token_pass_mint_info.clone(),
|
|
token_pass_destination_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
amount,
|
|
pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
// Mint FAIL tokens to user account
|
|
Self::mint(
|
|
token_program_id_info.clone(),
|
|
token_fail_mint_info.clone(),
|
|
token_fail_destination_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
amount,
|
|
pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Process Withdraw instruction
|
|
pub fn process_withdraw(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
amount: u64,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let pool_account_info = next_account_info(account_info_iter)?;
|
|
let authority_account_info = next_account_info(account_info_iter)?;
|
|
let user_transfer_authority_info = next_account_info(account_info_iter)?;
|
|
let pool_deposit_token_account_info = next_account_info(account_info_iter)?;
|
|
let token_pass_user_account_info = next_account_info(account_info_iter)?;
|
|
let token_fail_user_account_info = next_account_info(account_info_iter)?;
|
|
let token_pass_mint_info = next_account_info(account_info_iter)?;
|
|
let token_fail_mint_info = next_account_info(account_info_iter)?;
|
|
let user_token_destination_account_info = next_account_info(account_info_iter)?;
|
|
let clock_info = next_account_info(account_info_iter)?;
|
|
let clock = &Clock::from_account_info(clock_info)?;
|
|
let token_program_id_info = next_account_info(account_info_iter)?;
|
|
|
|
if amount == 0 {
|
|
return Err(PoolError::InvalidAmount.into());
|
|
}
|
|
|
|
let user_pass_token_account = Account::unpack(&token_pass_user_account_info.data.borrow())?;
|
|
let user_fail_token_account = Account::unpack(&token_fail_user_account_info.data.borrow())?;
|
|
|
|
let pool = Pool::try_from_slice(&pool_account_info.data.borrow())?;
|
|
|
|
let authority_pub_key =
|
|
Self::authority_id(program_id, pool_account_info.key, pool.bump_seed)?;
|
|
if *authority_account_info.key != authority_pub_key {
|
|
return Err(PoolError::InvalidAuthorityAccount.into());
|
|
}
|
|
|
|
match pool.decision {
|
|
Decision::Pass => {
|
|
// Burn PASS tokens
|
|
Self::burn(
|
|
token_program_id_info.clone(),
|
|
token_pass_user_account_info.clone(),
|
|
token_pass_mint_info.clone(),
|
|
authority_account_info.clone(),
|
|
user_transfer_authority_info.clone(),
|
|
amount,
|
|
&pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
|
|
// Transfer deposit tokens from pool deposit account to user destination account
|
|
Self::transfer(
|
|
token_program_id_info.clone(),
|
|
pool_deposit_token_account_info.clone(),
|
|
user_token_destination_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
amount,
|
|
pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
}
|
|
Decision::Fail => {
|
|
// Burn FAIL tokens
|
|
Self::burn(
|
|
token_program_id_info.clone(),
|
|
token_fail_user_account_info.clone(),
|
|
token_fail_mint_info.clone(),
|
|
authority_account_info.clone(),
|
|
user_transfer_authority_info.clone(),
|
|
amount,
|
|
&pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
|
|
// Transfer deposit tokens from pool deposit account to user destination account
|
|
Self::transfer(
|
|
token_program_id_info.clone(),
|
|
pool_deposit_token_account_info.clone(),
|
|
user_token_destination_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
amount,
|
|
pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
}
|
|
Decision::Undecided => {
|
|
let current_slot = clock.slot;
|
|
if current_slot < pool.mint_end_slot || current_slot > pool.decide_end_slot {
|
|
let possible_withdraw_amount = amount
|
|
.min(user_pass_token_account.amount)
|
|
.min(user_fail_token_account.amount);
|
|
|
|
// Burn PASS tokens
|
|
Self::burn(
|
|
token_program_id_info.clone(),
|
|
token_pass_user_account_info.clone(),
|
|
token_pass_mint_info.clone(),
|
|
authority_account_info.clone(),
|
|
user_transfer_authority_info.clone(),
|
|
possible_withdraw_amount,
|
|
&pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
|
|
// Burn FAIL tokens
|
|
Self::burn(
|
|
token_program_id_info.clone(),
|
|
token_fail_user_account_info.clone(),
|
|
token_fail_mint_info.clone(),
|
|
authority_account_info.clone(),
|
|
user_transfer_authority_info.clone(),
|
|
amount,
|
|
&pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
|
|
// Transfer deposit tokens from pool deposit account to user destination account
|
|
Self::transfer(
|
|
token_program_id_info.clone(),
|
|
pool_deposit_token_account_info.clone(),
|
|
user_token_destination_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
authority_account_info.clone(),
|
|
amount,
|
|
pool_account_info.key,
|
|
pool.bump_seed,
|
|
)?;
|
|
} else {
|
|
return Err(PoolError::NoDecisionMadeYet.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Process Decide instruction
|
|
pub fn process_decide(
|
|
_program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
decision: bool,
|
|
) -> ProgramResult {
|
|
let account_info_iter = &mut accounts.iter();
|
|
let pool_account_info = next_account_info(account_info_iter)?;
|
|
let decider_account_info = next_account_info(account_info_iter)?;
|
|
let clock_info = next_account_info(account_info_iter)?;
|
|
let clock = &Clock::from_account_info(clock_info)?;
|
|
|
|
let mut pool = Pool::try_from_slice(&pool_account_info.data.borrow())?;
|
|
|
|
if *decider_account_info.key != pool.decider {
|
|
return Err(PoolError::WrongDeciderAccount.into());
|
|
}
|
|
|
|
if !decider_account_info.is_signer {
|
|
return Err(PoolError::SignatureMissing.into());
|
|
}
|
|
|
|
if pool.decision != Decision::Undecided {
|
|
return Err(PoolError::DecisionAlreadyMade.into());
|
|
}
|
|
|
|
let current_slot = clock.slot;
|
|
if current_slot < pool.mint_end_slot || current_slot > pool.decide_end_slot {
|
|
return Err(PoolError::InvalidSlotForDecision.into());
|
|
}
|
|
|
|
pool.decision = if decision {
|
|
Decision::Pass
|
|
} else {
|
|
Decision::Fail
|
|
};
|
|
|
|
pool.serialize(&mut *pool_account_info.data.borrow_mut())
|
|
.map_err(|e| e.into())
|
|
}
|
|
|
|
/// Processes an instruction
|
|
pub fn process_instruction(
|
|
program_id: &Pubkey,
|
|
accounts: &[AccountInfo],
|
|
input: &[u8],
|
|
) -> ProgramResult {
|
|
let instruction = PoolInstruction::try_from_slice(input)?;
|
|
match instruction {
|
|
PoolInstruction::InitPool(init_args) => {
|
|
msg!("Instruction: InitPool");
|
|
Self::process_init_pool(
|
|
program_id,
|
|
accounts,
|
|
init_args.mint_end_slot,
|
|
init_args.decide_end_slot,
|
|
init_args.bump_seed,
|
|
)
|
|
}
|
|
PoolInstruction::Deposit(amount) => {
|
|
msg!("Instruction: Deposit");
|
|
Self::process_deposit(program_id, accounts, amount)
|
|
}
|
|
PoolInstruction::Withdraw(amount) => {
|
|
msg!("Instruction: Withdraw");
|
|
Self::process_withdraw(program_id, accounts, amount)
|
|
}
|
|
PoolInstruction::Decide(decision) => {
|
|
msg!("Instruction: Decide");
|
|
Self::process_decide(program_id, accounts, decision)
|
|
}
|
|
}
|
|
}
|
|
}
|