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:
parent
d0bf7157cf
commit
cd57d1cf10
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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.
|
|
@ -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"]
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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(())
|
||||
}
|
|
@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
|
@ -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");
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue