Binary oracle implementation (#1347)

* oracle pair init

* update

* updates

* progress

* update

* update

* progress

* builds

* update

* progress

* update

* copy pasta

* Refactor and add Instruction serializing/deserializing

* Add pack/unpack for Pool struct

* Implement InitPool instruction

* Add unit test and refactor InitPool instruction

* Minor changes

* Add using deposing token mint decimals in pass and fail mints

* Add Deposit instruction processing

* Add test for Deposit instruction

* Add Withdraw instruction processing

* Add test for Withdraw instruction

* Add Decide instruction with test

* Changes in Withdraw instruciton and add time travel to Decide instruction test

* Fix clippy warning

* Fix warning with if operator

* Fix clippy warnings

* Update libs version and minor fixes

* Minor changes

* Add user_transfer_authority to withdraw instruction and other minor changes

* Fix clippy warns

* Change return value after serialization

* Update tokio and solana-program-test libs version

Co-authored-by: Anatoly Yakovenko <anatoly@solana.com>
This commit is contained in:
Vadim 2021-03-18 19:18:20 +02:00 committed by GitHub
parent d0bf7157cf
commit cd57d1cf10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 2222 additions and 0 deletions

16
Cargo.lock generated
View File

@ -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"

View File

@ -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",

View File

@ -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.

View File

@ -0,0 +1,29 @@
[package]
name = "spl-binary-oracle-pair"
version = "0.1.0"
description = "Solana Program Library Binary Oracle Pair"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
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"]

View File

@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []

View File

@ -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::<PoolError>();
return Err(error);
}
Ok(())
}

View File

@ -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<PoolError> for ProgramError {
fn from(e: PoolError) -> Self {
ProgramError::Custom(e as u32)
}
}
impl<T> DecodeError<T> for PoolError {
fn type_of() -> &'static str {
"Binary Oracle Pair Error"
}
}
impl PrintProgramError for PoolError {
fn print<E>(&self)
where
E: 'static + std::error::Error + DecodeError<E> + 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"),
}
}
}

View File

@ -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<Instruction, ProgramError> {
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<Instruction, ProgramError> {
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<Instruction, ProgramError> {
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<Instruction, ProgramError> {
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,
})
}

View File

@ -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");

View File

@ -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, 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)
}
}
}
}

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff