diff --git a/Cargo.lock b/Cargo.lock index 71534e13..528e2428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3694,6 +3694,22 @@ dependencies = [ "spl-token 3.1.0", ] +[[package]] +name = "spl-binary-oracle-pair" +version = "0.1.0" +dependencies = [ + "arbitrary", + "arrayref", + "num-derive", + "num-traits", + "proptest", + "solana-program", + "solana-sdk", + "spl-token 3.0.1", + "thiserror", + "uint", +] + [[package]] name = "spl-example-cross-program-invocation" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 2666b639..4cf12b50 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "associated-token-account/program", + "binary-oracle-pair/program", "examples/rust/cross-program-invocation", "examples/rust/custom-heap", "examples/rust/logging", diff --git a/binary-oracle-pair/README.md b/binary-oracle-pair/README.md new file mode 100644 index 00000000..9371114f --- /dev/null +++ b/binary-oracle-pair/README.md @@ -0,0 +1,12 @@ +Simple Oracle Pair Token + +1. pick a deposit token +2. pick the decider's pubkey +3. pick the mint term end slot +4. pick the decide term end slot, must be after 3 + +Each deposit token can mint one `Pass` and one `Fail` token up to +the mint term end slot. After the decide term end slot the `Pass` +token converts 1:1 with the deposit token if and only if the decider +had set `pass` before the end of the decide term, otherwise the `Fail` +token converts 1:1 with the deposit token. diff --git a/binary-oracle-pair/program/Cargo.toml b/binary-oracle-pair/program/Cargo.toml new file mode 100644 index 00000000..f3d1327d --- /dev/null +++ b/binary-oracle-pair/program/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "spl-binary-oracle-pair" +version = "0.1.0" +description = "Solana Program Library Binary Oracle Pair" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana-program-library" +license = "Apache-2.0" +edition = "2018" + +[features] +test-bpf = [] + +[dependencies] +num-derive = "0.3" +num-traits = "0.2" +solana-program = "1.5.14" +spl-token = { version = "3.0", path = "../../token/program", features = [ "no-entrypoint" ] } +thiserror = "1.0" +uint = "0.8" +arbitrary = { version = "0.4", features = ["derive"], optional = true } +borsh = "0.8.2" + +[dev-dependencies] +solana-program-test = "1.6.1" +solana-sdk = "1.5.14" +tokio = { version = "1.3.0", features = ["macros"]} + +[lib] +crate-type = ["cdylib", "lib"] diff --git a/binary-oracle-pair/program/Xargo.toml b/binary-oracle-pair/program/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/binary-oracle-pair/program/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/binary-oracle-pair/program/src/entrypoint.rs b/binary-oracle-pair/program/src/entrypoint.rs new file mode 100644 index 00000000..de7e13ad --- /dev/null +++ b/binary-oracle-pair/program/src/entrypoint.rs @@ -0,0 +1,25 @@ +//! Program entrypoint definitions + +#![cfg(all(target_arch = "bpf", not(feature = "no-entrypoint")))] + +use crate::{error::PoolError, processor}; +use solana_program::{ + account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, + program_error::PrintProgramError, pubkey::Pubkey, +}; + +entrypoint!(process_instruction); +fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if let Err(error) = + processor::Processor::process_instruction(program_id, accounts, instruction_data) + { + // catch the error so we can print it + error.print::(); + return Err(error); + } + Ok(()) +} diff --git a/binary-oracle-pair/program/src/error.rs b/binary-oracle-pair/program/src/error.rs new file mode 100644 index 00000000..17884848 --- /dev/null +++ b/binary-oracle-pair/program/src/error.rs @@ -0,0 +1,97 @@ +//! Error types + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; +use solana_program::{ + decode_error::DecodeError, msg, program_error::PrintProgramError, program_error::ProgramError, +}; +use thiserror::Error; + +/// Errors that may be returned by the Binary Oracle Pair program. +#[derive(Clone, Debug, Eq, Error, FromPrimitive, PartialEq)] +pub enum PoolError { + /// Pool account already in use + #[error("Pool account already in use")] + AlreadyInUse, + /// Deposit account already in use + #[error("Deposit account already in use")] + DepositAccountInUse, + /// Token mint account already in use + #[error("Token account already in use")] + TokenMintInUse, + /// Invalid seed or bump_seed was provided + #[error("Failed to generate program account because of invalid data")] + InvalidAuthorityData, + /// Invalid authority account provided + #[error("Invalid authority account provided")] + InvalidAuthorityAccount, + /// Lamport balance below rent-exempt threshold. + #[error("Lamport balance below rent-exempt threshold")] + NotRentExempt, + /// Expected an SPL Token mint + #[error("Input token mint account is not valid")] + InvalidTokenMint, + /// Amount should be more than zero + #[error("Amount should be more than zero")] + InvalidAmount, + /// Wrong decider account + #[error("Wrong decider account was sent")] + WrongDeciderAccount, + /// Signature missing in transaction + #[error("Signature missing in transaction")] + SignatureMissing, + /// Decision was already made for this pool + #[error("Decision was already made for this pool")] + DecisionAlreadyMade, + /// Decision can't be made in current slot + #[error("Decision can't be made in current slot")] + InvalidSlotForDecision, + /// Deposit can't be made in current slot + #[error("Deposit can't be made in current slot")] + InvalidSlotForDeposit, + /// No decision has been made yet + #[error("No decision has been made yet")] + NoDecisionMadeYet, +} + +impl From for ProgramError { + fn from(e: PoolError) -> Self { + ProgramError::Custom(e as u32) + } +} + +impl DecodeError for PoolError { + fn type_of() -> &'static str { + "Binary Oracle Pair Error" + } +} + +impl PrintProgramError for PoolError { + fn print(&self) + where + E: 'static + std::error::Error + DecodeError + PrintProgramError + FromPrimitive, + { + match self { + PoolError::AlreadyInUse => msg!("Error: Pool account already in use"), + PoolError::DepositAccountInUse => msg!("Error: Deposit account already in use"), + PoolError::TokenMintInUse => msg!("Error: Token account already in use"), + PoolError::InvalidAuthorityData => { + msg!("Error: Failed to generate program account because of invalid data") + } + PoolError::InvalidAuthorityAccount => msg!("Error: Invalid authority account provided"), + PoolError::NotRentExempt => msg!("Error: Lamport balance below rent-exempt threshold"), + PoolError::InvalidTokenMint => msg!("Error: Input token mint account is not valid"), + PoolError::InvalidAmount => msg!("Error: Amount should be more than zero"), + PoolError::WrongDeciderAccount => msg!("Error: Wrong decider account was sent"), + PoolError::SignatureMissing => msg!("Error: Signature missing in transaction"), + PoolError::DecisionAlreadyMade => { + msg!("Error: Decision was already made for this pool") + } + PoolError::InvalidSlotForDecision => { + msg!("Error: Decision can't be made in current slot") + } + PoolError::InvalidSlotForDeposit => msg!("Deposit can't be made in current slot"), + PoolError::NoDecisionMadeYet => msg!("Error: No decision has been made yet"), + } + } +} diff --git a/binary-oracle-pair/program/src/instruction.rs b/binary-oracle-pair/program/src/instruction.rs new file mode 100644 index 00000000..67c72881 --- /dev/null +++ b/binary-oracle-pair/program/src/instruction.rs @@ -0,0 +1,220 @@ +//! Instruction types + +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::{ + clock::Slot, + instruction::{AccountMeta, Instruction}, + program_error::ProgramError, + pubkey::Pubkey, + sysvar, +}; + +/// Initialize arguments for pool +#[repr(C)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub struct InitArgs { + /// mint end slot + pub mint_end_slot: Slot, + /// decide end slot + pub decide_end_slot: Slot, + /// authority nonce + pub bump_seed: u8, +} + +/// Instruction definition +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub enum PoolInstruction { + /// Initializes a new binary oracle pair pool. + /// + /// 0. `[w]` Pool account. + /// 1. `[]` Authority + /// 2. `[]` Decider authority + /// 3. `[]` Deposit currency SPL Token mint. Must be initialized. + /// 4. `[w]` Deposit token account. Should not be initialized + /// 5. `[w]` Token Pass mint. Should not be initialized + /// 6. `[w]` Token Fail mint. Should not be initialized + /// 7. `[]` Rent sysvar + /// 8. `[]` Token program id + InitPool(InitArgs), + + /// Deposit into the pool. + /// + /// 0. `[]` Pool + /// 1. `[]` Authority + /// 2. `[s]` User transfer authority + /// 3. `[w]` Token SOURCE Account, amount is transferable by pool authority with allowances. + /// 4. `[w]` Deposit token account + /// 5. `[w]` token_P PASS mint + /// 6. `[w]` token_F FAIL mint + /// 7. `[w]` token_P DESTINATION Account + /// 8. `[w]` token_F DESTINATION Account + /// 9. `[]` Sysvar Clock + /// 10. `[]` Token program id + Deposit(u64), + + /// Withdraw from the pool. + /// If current slot is < mint_end slot, 1 Pass AND 1 Fail token convert to 1 deposit + /// If current slot is > decide_end_slot slot && decide == Some(true), 1 Pass convert to 1 deposit + /// otherwise 1 Fail converts to 1 deposit + /// + /// Pass tokens convert 1:1 to the deposit token iff decision is set to Some(true) + /// AND current slot is > decide_end_slot. + /// + /// 0. `[]` Pool + /// 1. `[]` Authority + /// 2. `[s]` User transfer authority + /// 3. `[w]` Pool deposit token account + /// 4. `[w]` token_P PASS SOURCE Account + /// 5. `[w]` token_F FAIL SOURCE Account + /// 6. `[w]` token_P PASS mint + /// 7. `[w]` token_F FAIL mint + /// 8. `[w]` Deposit DESTINATION Account + /// 9. `[]` Sysvar Clock + /// 10. `[]` Token program id + Withdraw(u64), + + /// Trigger the decision. + /// Call only succeeds once and if current slot > mint_end slot AND < decide_end slot + /// 0. `[]` Pool + /// 1. `[s]` Decider pubkey + /// 2. `[]` Sysvar Clock + Decide(bool), +} + +/// Create `InitPool` instruction +#[allow(clippy::too_many_arguments)] +pub fn init_pool( + program_id: &Pubkey, + pool: &Pubkey, + authority: &Pubkey, + decider: &Pubkey, + deposit_token_mint: &Pubkey, + deposit_account: &Pubkey, + token_pass_mint: &Pubkey, + token_fail_mint: &Pubkey, + token_program_id: &Pubkey, + init_args: InitArgs, +) -> Result { + let init_data = PoolInstruction::InitPool(init_args); + let data = init_data.try_to_vec()?; + let accounts = vec![ + AccountMeta::new(*pool, false), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new_readonly(*decider, false), + AccountMeta::new_readonly(*deposit_token_mint, false), + AccountMeta::new(*deposit_account, false), + AccountMeta::new(*token_pass_mint, false), + AccountMeta::new(*token_fail_mint, false), + AccountMeta::new_readonly(sysvar::rent::id(), false), + AccountMeta::new_readonly(*token_program_id, false), + ]; + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) +} + +/// Create `Deposit` instruction +#[allow(clippy::too_many_arguments)] +pub fn deposit( + program_id: &Pubkey, + pool: &Pubkey, + authority: &Pubkey, + user_transfer_authority: &Pubkey, + user_token_account: &Pubkey, + pool_deposit_token_account: &Pubkey, + token_pass_mint: &Pubkey, + token_fail_mint: &Pubkey, + token_pass_destination_account: &Pubkey, + token_fail_destination_account: &Pubkey, + token_program_id: &Pubkey, + amount: u64, +) -> Result { + let init_data = PoolInstruction::Deposit(amount); + let data = init_data.try_to_vec()?; + + let accounts = vec![ + AccountMeta::new_readonly(*pool, false), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new_readonly( + *user_transfer_authority, + authority != user_transfer_authority, + ), + AccountMeta::new(*user_token_account, false), + AccountMeta::new(*pool_deposit_token_account, false), + AccountMeta::new(*token_pass_mint, false), + AccountMeta::new(*token_fail_mint, false), + AccountMeta::new(*token_pass_destination_account, false), + AccountMeta::new(*token_fail_destination_account, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*token_program_id, false), + ]; + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) +} + +/// Create `Withdraw` instruction +#[allow(clippy::too_many_arguments)] +pub fn withdraw( + program_id: &Pubkey, + pool: &Pubkey, + authority: &Pubkey, + user_transfer_authority: &Pubkey, + pool_deposit_token_account: &Pubkey, + token_pass_user_account: &Pubkey, + token_fail_user_account: &Pubkey, + token_pass_mint: &Pubkey, + token_fail_mint: &Pubkey, + user_token_destination_account: &Pubkey, + token_program_id: &Pubkey, + amount: u64, +) -> Result { + let init_data = PoolInstruction::Withdraw(amount); + let data = init_data.try_to_vec()?; + let accounts = vec![ + AccountMeta::new_readonly(*pool, false), + AccountMeta::new_readonly(*authority, false), + AccountMeta::new_readonly( + *user_transfer_authority, + authority != user_transfer_authority, + ), + AccountMeta::new(*pool_deposit_token_account, false), + AccountMeta::new(*token_pass_user_account, false), + AccountMeta::new(*token_fail_user_account, false), + AccountMeta::new(*token_pass_mint, false), + AccountMeta::new(*token_fail_mint, false), + AccountMeta::new(*user_token_destination_account, false), + AccountMeta::new_readonly(sysvar::clock::id(), false), + AccountMeta::new_readonly(*token_program_id, false), + ]; + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) +} + +/// Create `Decide` instruction +pub fn decide( + program_id: &Pubkey, + pool: &Pubkey, + decider: &Pubkey, + decision: bool, +) -> Result { + let init_data = PoolInstruction::Decide(decision); + let data = init_data.try_to_vec()?; + let accounts = vec![ + AccountMeta::new(*pool, false), + AccountMeta::new_readonly(*decider, true), + AccountMeta::new_readonly(sysvar::clock::id(), false), + ]; + Ok(Instruction { + program_id: *program_id, + accounts, + data, + }) +} diff --git a/binary-oracle-pair/program/src/lib.rs b/binary-oracle-pair/program/src/lib.rs new file mode 100644 index 00000000..7b20c3b9 --- /dev/null +++ b/binary-oracle-pair/program/src/lib.rs @@ -0,0 +1,16 @@ +//! binary oracle pair +#![deny(missing_docs)] + +pub mod error; +pub mod instruction; +pub mod processor; +pub mod state; + +#[cfg(not(feature = "no-entrypoint"))] +mod entrypoint; + +// Export current sdk types for downstream users building with a different sdk version +pub use solana_program; + +// Binary Oracle Pair id +solana_program::declare_id!("Fd7btgySsrjuo25CJCj7oE7VPMyezDhnx7pZkj2v69Nk"); diff --git a/binary-oracle-pair/program/src/processor.rs b/binary-oracle-pair/program/src/processor.rs new file mode 100644 index 00000000..1f30bc13 --- /dev/null +++ b/binary-oracle-pair/program/src/processor.rs @@ -0,0 +1,589 @@ +//! 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::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) + } + } + } +} diff --git a/binary-oracle-pair/program/src/state.rs b/binary-oracle-pair/program/src/state.rs new file mode 100644 index 00000000..9b8438f6 --- /dev/null +++ b/binary-oracle-pair/program/src/state.rs @@ -0,0 +1,92 @@ +//! State transition types + +use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; + +/// Uninitialized version value, all instances are at least version 1 +pub const UNINITIALIZED_VERSION: u8 = 0; +/// Initialized pool version +pub const POOL_VERSION: u8 = 1; + +/// Program states. +#[repr(C)] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub struct Pool { + /// Initialized state. + pub version: u8, + + /// Nonce used in program address. + pub bump_seed: u8, + + /// Program ID of the tokens + pub token_program_id: Pubkey, + + /// Account to deposit into + pub deposit_account: Pubkey, + + /// Mint information for token Pass + pub token_pass_mint: Pubkey, + + /// Mint information for token Fail + pub token_fail_mint: Pubkey, + + /// decider key + pub decider: Pubkey, + + /// mint end slot + pub mint_end_slot: u64, + + /// decide end slot + pub decide_end_slot: u64, + + /// decision status + pub decision: Decision, +} + +/// Decision status +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Debug, Clone)] +pub enum Decision { + /// Decision was not made + Undecided, + /// Decision set at Pass + Pass, + /// Decision set at Fail + Fail, +} + +impl Pool { + /// Length serialized data + pub const LEN: usize = 179; + + /// Check if Pool already initialized + pub fn is_initialized(&self) -> bool { + self.version != UNINITIALIZED_VERSION + } +} + +mod test { + #[cfg(test)] + use super::*; + + #[test] + pub fn test_pool_pack_unpack() { + let p = Pool { + version: 1, + bump_seed: 2, + token_program_id: Pubkey::new_unique(), + deposit_account: Pubkey::new_unique(), + token_pass_mint: Pubkey::new_unique(), + token_fail_mint: Pubkey::new_unique(), + decider: Pubkey::new_unique(), + mint_end_slot: 433, + decide_end_slot: 5546, + decision: Decision::Fail, + }; + + let packed = p.try_to_vec().unwrap(); + + let unpacked = Pool::try_from_slice(packed.as_slice()).unwrap(); + + assert_eq!(p, unpacked); + } +} diff --git a/binary-oracle-pair/program/tests/tests.rs b/binary-oracle-pair/program/tests/tests.rs new file mode 100644 index 00000000..d4a93f4f --- /dev/null +++ b/binary-oracle-pair/program/tests/tests.rs @@ -0,0 +1,1123 @@ +#![cfg(feature = "test-bpf")] + +use borsh::de::BorshDeserialize; +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 spl_binary_oracle_pair::*; + +pub fn program_test() -> ProgramTest { + ProgramTest::new( + "spl_binary_oracle_pair", + id(), + processor!(processor::Processor::process_instruction), + ) +} + +pub async fn create_token_account( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + account: &Keypair, + mint: &Pubkey, + owner: &Pubkey, +) -> Result<(), TransportError> { + let rent = banks_client.get_rent().await.unwrap(); + let account_rent = rent.minimum_balance(spl_token::state::Account::LEN); + + let mut transaction = Transaction::new_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &account.pubkey(), + account_rent, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_account( + &spl_token::id(), + &account.pubkey(), + mint, + owner, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + ); + transaction.sign(&[payer, account], *recent_blockhash); + banks_client.process_transaction(transaction).await?; + Ok(()) +} + +pub async fn mint_tokens_to( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + mint: &Pubkey, + destination: &Pubkey, + authority: &Keypair, + amount: u64, +) -> Result<(), TransportError> { + let mut transaction = Transaction::new_with_payer( + &[spl_token::instruction::mint_to( + &spl_token::id(), + mint, + destination, + &authority.pubkey(), + &[&authority.pubkey()], + amount, + ) + .unwrap()], + Some(&payer.pubkey()), + ); + transaction.sign(&[payer, authority], *recent_blockhash); + banks_client.process_transaction(transaction).await?; + Ok(()) +} + +pub async fn approve_delegate( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + source: &Pubkey, + delegate: &Pubkey, + source_owner: &Keypair, + amount: u64, +) -> Result<(), TransportError> { + let mut transaction = Transaction::new_with_payer( + &[spl_token::instruction::approve( + &spl_token::id(), + source, + delegate, + &source_owner.pubkey(), + &[&source_owner.pubkey()], + amount, + ) + .unwrap()], + Some(&payer.pubkey()), + ); + transaction.sign(&[payer, source_owner], *recent_blockhash); + banks_client.process_transaction(transaction).await?; + Ok(()) +} + +pub async fn make_decision( + program_context: &mut ProgramTestContext, + pool_account: &Pubkey, + decider: &Keypair, + decision: bool, +) -> Result<(), TransportError> { + let mut transaction = Transaction::new_with_payer( + &[instruction::decide(&id(), pool_account, &decider.pubkey(), decision).unwrap()], + Some(&program_context.payer.pubkey()), + ); + + transaction.sign( + &[&program_context.payer, decider], + program_context.last_blockhash, + ); + program_context + .banks_client + .process_transaction(transaction) + .await?; + Ok(()) +} + +pub async fn make_withdraw( + program_context: &mut ProgramTestContext, + pool_account: &Pubkey, + authority: &Pubkey, + user_transfer_authority: &Pubkey, + pool_deposit_account: &Pubkey, + user_pass_account: &Pubkey, + user_fail_account: &Pubkey, + token_pass_mint: &Pubkey, + token_fail_mint: &Pubkey, + user_account: &Pubkey, + withdraw_amount: u64, +) -> Result<(), TransportError> { + let mut transaction = Transaction::new_with_payer( + &[instruction::withdraw( + &id(), + pool_account, + authority, + user_transfer_authority, + pool_deposit_account, + user_pass_account, + user_fail_account, + token_pass_mint, + token_fail_mint, + user_account, + &spl_token::id(), + withdraw_amount, + ) + .unwrap()], + Some(&program_context.payer.pubkey()), + ); + transaction.sign(&[&program_context.payer], program_context.last_blockhash); + program_context + .banks_client + .process_transaction(transaction) + .await + .unwrap(); + Ok(()) +} + +pub async fn get_token_balance(banks_client: &mut BanksClient, token: &Pubkey) -> u64 { + let token_account = banks_client.get_account(*token).await.unwrap().unwrap(); + let account_info: spl_token::state::Account = + spl_token::state::Account::unpack_from_slice(token_account.data.as_slice()).unwrap(); + account_info.amount +} + +pub struct TestPool { + pub pool_account: Keypair, + pub authority: Pubkey, + pub bump_seed: u8, + pub deposit_token_mint: Keypair, + pub deposit_token_mint_owner: Keypair, + pub pool_deposit_account: Keypair, + pub token_pass_mint: Keypair, + pub token_fail_mint: Keypair, + pub decider: Keypair, + pub mint_end_slot: u64, + pub decide_end_slot: u64, +} + +impl TestPool { + pub fn new() -> Self { + let pool_account = Keypair::new(); + let (authority, bump_seed) = + Pubkey::find_program_address(&[&pool_account.pubkey().to_bytes()[..32]], &id()); + Self { + pool_account, + authority, + bump_seed, + deposit_token_mint: Keypair::new(), + deposit_token_mint_owner: Keypair::new(), + pool_deposit_account: Keypair::new(), + token_pass_mint: Keypair::new(), + token_fail_mint: Keypair::new(), + decider: Keypair::new(), + mint_end_slot: 2, + decide_end_slot: 2000, + } + } + + pub async fn init_pool( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + ) { + let rent = banks_client.get_rent().await.unwrap(); + let pool_rent = rent.minimum_balance(state::Pool::LEN); + let mint_rent = rent.minimum_balance(spl_token::state::Mint::LEN); + let account_rent = rent.minimum_balance(spl_token::state::Account::LEN); + + // create pool account + create_account( + banks_client, + payer, + recent_blockhash, + &self.pool_account, + pool_rent, + state::Pool::LEN as u64, + &id(), + ) + .await + .unwrap(); + + // create mint of deposit token + create_mint( + banks_client, + payer, + recent_blockhash, + &self.deposit_token_mint, + mint_rent, + &self.deposit_token_mint_owner.pubkey(), + ) + .await + .unwrap(); + + let init_args = instruction::InitArgs { + mint_end_slot: self.mint_end_slot, + decide_end_slot: self.decide_end_slot, + bump_seed: self.bump_seed, + }; + + let mut transaction = Transaction::new_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &self.pool_deposit_account.pubkey(), + account_rent, + spl_token::state::Account::LEN as u64, + &spl_token::id(), + ), + system_instruction::create_account( + &payer.pubkey(), + &self.token_pass_mint.pubkey(), + mint_rent, + spl_token::state::Mint::LEN as u64, + &spl_token::id(), + ), + system_instruction::create_account( + &payer.pubkey(), + &self.token_fail_mint.pubkey(), + mint_rent, + spl_token::state::Mint::LEN as u64, + &spl_token::id(), + ), + instruction::init_pool( + &id(), + &self.pool_account.pubkey(), + &self.authority, + &self.decider.pubkey(), + &self.deposit_token_mint.pubkey(), + &self.pool_deposit_account.pubkey(), + &self.token_pass_mint.pubkey(), + &self.token_fail_mint.pubkey(), + &spl_token::id(), + init_args, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + ); + + transaction.sign( + &[ + payer, + &self.pool_deposit_account, + &self.token_pass_mint, + &self.token_fail_mint, + ], + *recent_blockhash, + ); + banks_client.process_transaction(transaction).await.unwrap(); + } + + #[allow(clippy::too_many_arguments)] + pub async fn prepare_accounts_for_deposit( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + deposit_tokens_to_mint: u64, + deposit_tokens_for_allowance: u64, + user_account: &Keypair, + authority: &Pubkey, + user_account_owner: &Keypair, + user_pass_account: &Keypair, + user_fail_account: &Keypair, + ) { + // Create user account + create_token_account( + banks_client, + payer, + recent_blockhash, + user_account, + &self.deposit_token_mint.pubkey(), + &user_account_owner.pubkey(), + ) + .await + .unwrap(); + + // Mint to him some deposit tokens + mint_tokens_to( + banks_client, + payer, + recent_blockhash, + &self.deposit_token_mint.pubkey(), + &user_account.pubkey(), + &self.deposit_token_mint_owner, + deposit_tokens_to_mint, + ) + .await + .unwrap(); + + // Give allowance to pool authority + approve_delegate( + banks_client, + payer, + recent_blockhash, + &user_account.pubkey(), + authority, + user_account_owner, + deposit_tokens_for_allowance, + ) + .await + .unwrap(); + + // Create token accounts for PASS and FAIL tokens + create_token_account( + banks_client, + payer, + recent_blockhash, + user_pass_account, + &self.token_pass_mint.pubkey(), + &user_account_owner.pubkey(), + ) + .await + .unwrap(); + + create_token_account( + banks_client, + payer, + recent_blockhash, + user_fail_account, + &self.token_fail_mint.pubkey(), + &user_account_owner.pubkey(), + ) + .await + .unwrap(); + } + + #[allow(clippy::too_many_arguments)] + pub async fn make_deposit( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + user_account: &Keypair, + user_pass_account: &Keypair, + user_fail_account: &Keypair, + deposit_amount: u64, + ) { + let mut transaction = Transaction::new_with_payer( + &[instruction::deposit( + &id(), + &self.pool_account.pubkey(), + &self.authority, + &self.authority, + &user_account.pubkey(), + &self.pool_deposit_account.pubkey(), + &self.token_pass_mint.pubkey(), + &self.token_fail_mint.pubkey(), + &user_pass_account.pubkey(), + &user_fail_account.pubkey(), + &spl_token::id(), + deposit_amount, + ) + .unwrap()], + Some(&payer.pubkey()), + ); + transaction.sign(&[payer], *recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + } + + #[allow(clippy::too_many_arguments)] + pub async fn make_deposit_with_user_transfer_authority( + &self, + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + user_account: &Keypair, + user_authority: &Keypair, + user_pass_account: &Keypair, + user_fail_account: &Keypair, + deposit_amount: u64, + ) { + let mut transaction = Transaction::new_with_payer( + &[instruction::deposit( + &id(), + &self.pool_account.pubkey(), + &self.authority, + &user_authority.pubkey(), + &user_account.pubkey(), + &self.pool_deposit_account.pubkey(), + &self.token_pass_mint.pubkey(), + &self.token_fail_mint.pubkey(), + &user_pass_account.pubkey(), + &user_fail_account.pubkey(), + &spl_token::id(), + deposit_amount, + ) + .unwrap()], + Some(&payer.pubkey()), + ); + transaction.sign(&[payer, user_authority], *recent_blockhash); + banks_client.process_transaction(transaction).await.unwrap(); + } +} + +pub async fn create_mint( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + mint_account: &Keypair, + mint_rent: u64, + owner: &Pubkey, +) -> Result<(), TransportError> { + let mut transaction = Transaction::new_with_payer( + &[ + system_instruction::create_account( + &payer.pubkey(), + &mint_account.pubkey(), + mint_rent, + spl_token::state::Mint::LEN as u64, + &spl_token::id(), + ), + spl_token::instruction::initialize_mint( + &spl_token::id(), + &mint_account.pubkey(), + &owner, + None, + 0, + ) + .unwrap(), + ], + Some(&payer.pubkey()), + ); + transaction.sign(&[payer, mint_account], *recent_blockhash); + banks_client.process_transaction(transaction).await?; + Ok(()) +} + +pub async fn create_account( + banks_client: &mut BanksClient, + payer: &Keypair, + recent_blockhash: &Hash, + account: &Keypair, + rent: u64, + space: u64, + owner: &Pubkey, +) -> Result<(), TransportError> { + let mut transaction = Transaction::new_with_payer( + &[system_instruction::create_account( + &payer.pubkey(), + &account.pubkey(), + rent, + space, + owner, + )], + Some(&payer.pubkey()), + ); + + transaction.sign(&[payer, account], *recent_blockhash); + banks_client.process_transaction(transaction).await?; + Ok(()) +} + +async fn get_account(banks_client: &mut BanksClient, pubkey: &Pubkey) -> Account { + banks_client + .get_account(*pubkey) + .await + .expect("account not found") + .expect("account empty") +} + +#[tokio::test] +async fn test_init_pool() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + + let pool = TestPool::new(); + + pool.init_pool(&mut banks_client, &payer, &recent_blockhash) + .await; + + let pool_account_data = get_account(&mut banks_client, &pool.pool_account.pubkey()).await; + + assert_eq!(pool_account_data.data.len(), state::Pool::LEN); + assert_eq!(pool_account_data.owner, id()); + + // check if Pool is initialized + let pool = state::Pool::try_from_slice(pool_account_data.data.as_slice()).unwrap(); + assert!(pool.is_initialized()); +} + +#[tokio::test] +async fn test_deposit_with_program_authority() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + + let deposit_amount = 100; + + let pool = TestPool::new(); + + pool.init_pool(&mut banks_client, &payer, &recent_blockhash) + .await; + + let user_account = Keypair::new(); + let user_account_owner = Keypair::new(); + let user_pass_account = Keypair::new(); + let user_fail_account = Keypair::new(); + + pool.prepare_accounts_for_deposit( + &mut banks_client, + &payer, + &recent_blockhash, + deposit_amount, + deposit_amount, + &user_account, + &pool.authority, + &user_account_owner, + &user_pass_account, + &user_fail_account, + ) + .await; + + let user_balance_before = get_token_balance(&mut banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_before, deposit_amount); + + // Make deposit + pool.make_deposit( + &mut banks_client, + &payer, + &recent_blockhash, + &user_account, + &user_pass_account, + &user_fail_account, + deposit_amount, + ) + .await; + + // Check balance of user account + let user_balance_after = get_token_balance(&mut banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_after, 0); + + // Check balance of pool deposit account + let pool_deposit_account_balance = + get_token_balance(&mut banks_client, &pool.pool_deposit_account.pubkey()).await; + assert_eq!(pool_deposit_account_balance, deposit_amount); + + // Check if user has PASS and FAIL tokens + let user_pass_tokens = get_token_balance(&mut banks_client, &user_pass_account.pubkey()).await; + assert_eq!(user_pass_tokens, deposit_amount); + + let user_fail_tokens = get_token_balance(&mut banks_client, &user_fail_account.pubkey()).await; + assert_eq!(user_fail_tokens, deposit_amount); +} + +#[tokio::test] +async fn test_deposit_with_user_authority() { + let (mut banks_client, payer, recent_blockhash) = program_test().start().await; + + let deposit_amount = 100; + + let pool = TestPool::new(); + + pool.init_pool(&mut banks_client, &payer, &recent_blockhash) + .await; + + let user_account = Keypair::new(); + let user_account_owner = Keypair::new(); + let user_transfer_authority = Keypair::new(); + let user_pass_account = Keypair::new(); + let user_fail_account = Keypair::new(); + + pool.prepare_accounts_for_deposit( + &mut banks_client, + &payer, + &recent_blockhash, + deposit_amount, + deposit_amount, + &user_account, + &user_transfer_authority.pubkey(), + &user_account_owner, + &user_pass_account, + &user_fail_account, + ) + .await; + + let user_balance_before = get_token_balance(&mut banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_before, deposit_amount); + + // Make deposit + pool.make_deposit_with_user_transfer_authority( + &mut banks_client, + &payer, + &recent_blockhash, + &user_account, + &user_transfer_authority, + &user_pass_account, + &user_fail_account, + deposit_amount, + ) + .await; + + // Check balance of user account + let user_balance_after = get_token_balance(&mut banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_after, 0); + + // Check balance of pool deposit account + let pool_deposit_account_balance = + get_token_balance(&mut banks_client, &pool.pool_deposit_account.pubkey()).await; + assert_eq!(pool_deposit_account_balance, deposit_amount); + + // Check if user has PASS and FAIL tokens + let user_pass_tokens = get_token_balance(&mut banks_client, &user_pass_account.pubkey()).await; + assert_eq!(user_pass_tokens, deposit_amount); + + let user_fail_tokens = get_token_balance(&mut banks_client, &user_fail_account.pubkey()).await; + assert_eq!(user_fail_tokens, deposit_amount); +} + +#[tokio::test] +async fn test_withdraw_no_decision() { + let mut program_context = program_test().start_with_context().await; + + let deposit_amount = 100; + let withdraw_amount = 50; + + let pool = TestPool::new(); + + pool.init_pool( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + ) + .await; + + let user_account = Keypair::new(); + let user_account_owner = Keypair::new(); + let user_pass_account = Keypair::new(); + let user_fail_account = Keypair::new(); + + pool.prepare_accounts_for_deposit( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + deposit_amount, + deposit_amount, + &user_account, + &pool.authority, + &user_account_owner, + &user_pass_account, + &user_fail_account, + ) + .await; + + // Make deposit + pool.make_deposit( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_account, + &user_pass_account, + &user_fail_account, + deposit_amount, + ) + .await; + + // Set allowances to burn PASS and FAIL tokens + approve_delegate( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_pass_account.pubkey(), + &pool.authority, + &user_account_owner, + deposit_amount, + ) + .await + .unwrap(); + approve_delegate( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_fail_account.pubkey(), + &pool.authority, + &user_account_owner, + deposit_amount, + ) + .await + .unwrap(); + + let user_balance_before = + get_token_balance(&mut program_context.banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_before, 0); + + // Check balance of pool deposit account + let pool_deposit_account_balance = get_token_balance( + &mut program_context.banks_client, + &pool.pool_deposit_account.pubkey(), + ) + .await; + assert_eq!(pool_deposit_account_balance, deposit_amount); + + // Check if user has PASS and FAIL tokens + let user_pass_tokens = get_token_balance( + &mut program_context.banks_client, + &user_pass_account.pubkey(), + ) + .await; + assert_eq!(user_pass_tokens, deposit_amount); + + let user_fail_tokens = get_token_balance( + &mut program_context.banks_client, + &user_fail_account.pubkey(), + ) + .await; + assert_eq!(user_fail_tokens, deposit_amount); + + make_withdraw( + &mut program_context, + &pool.pool_account.pubkey(), + &pool.authority, + &pool.authority, + &pool.pool_deposit_account.pubkey(), + &user_pass_account.pubkey(), + &user_fail_account.pubkey(), + &pool.token_pass_mint.pubkey(), + &pool.token_fail_mint.pubkey(), + &user_account.pubkey(), + withdraw_amount, + ) + .await + .unwrap(); + + let user_balance_after = + get_token_balance(&mut program_context.banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_after, withdraw_amount); + + // Check balance of pool deposit account after withdraw + let pool_deposit_account_balance_after = get_token_balance( + &mut program_context.banks_client, + &pool.pool_deposit_account.pubkey(), + ) + .await; + assert_eq!( + pool_deposit_account_balance_after, + deposit_amount - withdraw_amount + ); + + // Check if program burned PASS and FAIL tokens + let user_pass_tokens_after = get_token_balance( + &mut program_context.banks_client, + &user_pass_account.pubkey(), + ) + .await; + assert_eq!(user_pass_tokens_after, deposit_amount - withdraw_amount); + + let user_fail_tokens_after = get_token_balance( + &mut program_context.banks_client, + &user_fail_account.pubkey(), + ) + .await; + assert_eq!(user_fail_tokens_after, deposit_amount - withdraw_amount); +} + +#[tokio::test] +async fn test_withdraw_pass_decision() { + let mut program_context = program_test().start_with_context().await; + + let deposit_amount = 100; + let withdraw_amount = 50; + + let pool = TestPool::new(); + + pool.init_pool( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + ) + .await; + + let user_account = Keypair::new(); + let user_account_owner = Keypair::new(); + let user_pass_account = Keypair::new(); + let user_fail_account = Keypair::new(); + + pool.prepare_accounts_for_deposit( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + deposit_amount, + deposit_amount, + &user_account, + &pool.authority, + &user_account_owner, + &user_pass_account, + &user_fail_account, + ) + .await; + + // Make deposit + pool.make_deposit( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_account, + &user_pass_account, + &user_fail_account, + deposit_amount, + ) + .await; + + // Set allowances to burn PASS and FAIL tokens + approve_delegate( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_pass_account.pubkey(), + &pool.authority, + &user_account_owner, + deposit_amount, + ) + .await + .unwrap(); + approve_delegate( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_fail_account.pubkey(), + &pool.authority, + &user_account_owner, + deposit_amount, + ) + .await + .unwrap(); + + let decision = true; + + program_context + .warp_to_slot(pool.mint_end_slot + 1) + .unwrap(); + + make_decision( + &mut program_context, + &pool.pool_account.pubkey(), + &pool.decider, + decision, + ) + .await + .unwrap(); + + make_withdraw( + &mut program_context, + &pool.pool_account.pubkey(), + &pool.authority, + &pool.authority, + &pool.pool_deposit_account.pubkey(), + &user_pass_account.pubkey(), + &user_fail_account.pubkey(), + &pool.token_pass_mint.pubkey(), + &pool.token_fail_mint.pubkey(), + &user_account.pubkey(), + withdraw_amount, + ) + .await + .unwrap(); + + let user_balance_after = + get_token_balance(&mut program_context.banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_after, withdraw_amount); + + // Check balance of pool deposit account after withdraw + let pool_deposit_account_balance_after = get_token_balance( + &mut program_context.banks_client, + &pool.pool_deposit_account.pubkey(), + ) + .await; + assert_eq!( + pool_deposit_account_balance_after, + deposit_amount - withdraw_amount + ); + + // Check if program burned PASS and FAIL tokens + let user_pass_tokens_after = get_token_balance( + &mut program_context.banks_client, + &user_pass_account.pubkey(), + ) + .await; + assert_eq!(user_pass_tokens_after, deposit_amount - withdraw_amount); + + let user_fail_tokens_after = get_token_balance( + &mut program_context.banks_client, + &user_fail_account.pubkey(), + ) + .await; + assert_eq!(user_fail_tokens_after, deposit_amount); +} + +#[tokio::test] +async fn test_withdraw_fail_decision() { + let mut program_context = program_test().start_with_context().await; + + let deposit_amount = 100; + let withdraw_amount = 50; + + let pool = TestPool::new(); + + pool.init_pool( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + ) + .await; + + let user_account = Keypair::new(); + let user_account_owner = Keypair::new(); + let user_pass_account = Keypair::new(); + let user_fail_account = Keypair::new(); + + pool.prepare_accounts_for_deposit( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + deposit_amount, + deposit_amount, + &user_account, + &pool.authority, + &user_account_owner, + &user_pass_account, + &user_fail_account, + ) + .await; + + // Make deposit + pool.make_deposit( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_account, + &user_pass_account, + &user_fail_account, + deposit_amount, + ) + .await; + + // Set allowances to burn PASS and FAIL tokens + approve_delegate( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_pass_account.pubkey(), + &pool.authority, + &user_account_owner, + deposit_amount, + ) + .await + .unwrap(); + approve_delegate( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + &user_fail_account.pubkey(), + &pool.authority, + &user_account_owner, + deposit_amount, + ) + .await + .unwrap(); + + let decision = false; + + program_context + .warp_to_slot(pool.mint_end_slot + 1) + .unwrap(); + + make_decision( + &mut program_context, + &pool.pool_account.pubkey(), + &pool.decider, + decision, + ) + .await + .unwrap(); + + make_withdraw( + &mut program_context, + &pool.pool_account.pubkey(), + &pool.authority, + &pool.authority, + &pool.pool_deposit_account.pubkey(), + &user_pass_account.pubkey(), + &user_fail_account.pubkey(), + &pool.token_pass_mint.pubkey(), + &pool.token_fail_mint.pubkey(), + &user_account.pubkey(), + withdraw_amount, + ) + .await + .unwrap(); + + let user_balance_after = + get_token_balance(&mut program_context.banks_client, &user_account.pubkey()).await; + assert_eq!(user_balance_after, withdraw_amount); + + // Check balance of pool deposit account after withdraw + let pool_deposit_account_balance_after = get_token_balance( + &mut program_context.banks_client, + &pool.pool_deposit_account.pubkey(), + ) + .await; + assert_eq!( + pool_deposit_account_balance_after, + deposit_amount - withdraw_amount + ); + + // Check if program burned PASS and FAIL tokens + let user_pass_tokens_after = get_token_balance( + &mut program_context.banks_client, + &user_pass_account.pubkey(), + ) + .await; + assert_eq!(user_pass_tokens_after, deposit_amount); + + let user_fail_tokens_after = get_token_balance( + &mut program_context.banks_client, + &user_fail_account.pubkey(), + ) + .await; + assert_eq!(user_fail_tokens_after, deposit_amount - withdraw_amount); +} + +#[tokio::test] +async fn test_decide() { + let mut program_context = program_test().start_with_context().await; + + let pool = TestPool::new(); + + pool.init_pool( + &mut program_context.banks_client, + &program_context.payer, + &program_context.last_blockhash, + ) + .await; + + let pool_account_data_before = program_context + .banks_client + .get_account(pool.pool_account.pubkey()) + .await + .unwrap() + .unwrap(); + + let pool_data_before = + state::Pool::try_from_slice(pool_account_data_before.data.as_slice()).unwrap(); + + assert_eq!(pool_data_before.decision, state::Decision::Undecided); + + let decision = true; + + program_context + .warp_to_slot(pool.mint_end_slot + 1) + .unwrap(); + + make_decision( + &mut program_context, + &pool.pool_account.pubkey(), + &pool.decider, + decision, + ) + .await + .unwrap(); + + let pool_account_data_after = program_context + .banks_client + .get_account(pool.pool_account.pubkey()) + .await + .unwrap() + .unwrap(); + + let pool_data_after = + state::Pool::try_from_slice(pool_account_data_after.data.as_slice()).unwrap(); + + assert_eq!(pool_data_after.decision, state::Decision::Pass); +}