token-swap: Add fuzzer for swap / withdraw / deposit (#875)

* token-swap: Add fuzzer for swap / withdraw / deposit

* Run cargo fmt

* Make end-to-end testing work

* Fix test failures

* Cleanup for review

* Remove Clone trait for Initialize

* Fix building fuzz targets

* Fix fuzz withdraw logic to avoid ZeroTradingTokens error

* Cargo fmt

* Clippy / fmt again

* Integrate fuzzer in main workspace to share clippy / fmt

* Fix clippy

* Switch to stable honggfuzz that could work with BPF

* Update to most recent honggfuzz

* Add build / run requirements

* Add fuzz CI and cleanup for honggfuzz

* Revert to using fuzz feature to integrate with workspace

* Make fuzz script executable

* Refactor and cleanup

* Add nightly run

* Fix workflow typo

* Add runtime for pull request fuzz workflow

* Fix unrelated new clippy errors
This commit is contained in:
Jon Cinque 2020-11-30 13:01:31 +01:00 committed by GitHub
parent ac20c5d59a
commit b40e0dd3fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 1350 additions and 180 deletions

64
.github/workflows/fuzz-nightly.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: Fuzz Nightly
on:
schedule:
- cron: '0 3 * * *'
jobs:
fuzz-nightly:
runs-on: ubuntu-latest
strategy:
matrix:
fuzz_target: [token-swap-instructions]
fail-fast: false
steps:
- uses: actions/checkout@v2
- name: Set env vars
run: |
source ci/rust-version.sh
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
source ci/solana-version.sh
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_STABLE }}
override: true
profile: minimal
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-fuzz-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-fuzz-
- uses: actions/cache@v2
with:
path: |
~/.cargo/bin/cargo-hfuzz
~/.cargo/bin/cargo-honggfuzz
key: cargo-fuzz-bins-${{ runner.os }}
restore-keys: |
cargo-fuzz-bins-${{ runner.os }}-
- uses: actions/cache@v2
with:
path: |
~/.cache
key: solana-${{ env.SOLANA_VERSION }}
restore-keys: |
solana-
- name: Install dependencies
run: |
./ci/install-build-deps.sh
./ci/install-program-deps.sh
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
- name: Run fuzz target
run: ./ci/fuzz.sh ${{ matrix.fuzz_target }} 18000 # 5 hours, jobs max out at 6

View File

@ -15,6 +15,7 @@ jobs:
- js-test-token
- js-test-token-swap
- js-test-token-lending
- fuzz
steps:
- run: echo "Done"
@ -200,3 +201,60 @@ jobs:
name: programs
path: target/bpfel-unknown-unknown/release
- run: ./ci/js-test-token-lending.sh
fuzz:
runs-on: ubuntu-latest
strategy:
matrix:
fuzz_target: [token-swap-instructions]
steps:
- uses: actions/checkout@v2
- name: Set env vars
run: |
source ci/rust-version.sh
echo "RUST_STABLE=$rust_stable" >> $GITHUB_ENV
source ci/solana-version.sh
echo "SOLANA_VERSION=$solana_version" >> $GITHUB_ENV
- uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_STABLE }}
override: true
profile: minimal
- uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: cargo-fuzz-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-fuzz-
- uses: actions/cache@v2
with:
path: |
~/.cargo/bin/cargo-hfuzz
~/.cargo/bin/cargo-honggfuzz
key: cargo-fuzz-bins-${{ runner.os }}
restore-keys: |
cargo-fuzz-bins-${{ runner.os }}-
- uses: actions/cache@v2
with:
path: |
~/.cache
key: solana-${{ env.SOLANA_VERSION }}
restore-keys: |
solana-
- name: Install dependencies
run: |
./ci/install-build-deps.sh
./ci/install-program-deps.sh
echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH
- name: Run fuzz target
run: ./ci/fuzz.sh ${{ matrix.fuzz_target }} 30 # 30 seconds, just to check everything is ok

2
.gitignore vendored
View File

@ -5,3 +5,5 @@ bin
config.json
node_modules
./package-lock.json
hfuzz_target
hfuzz_workspace

53
Cargo.lock generated
View File

@ -58,6 +58,15 @@ version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf8dcb5b4bbaa28653b647d8c77bd4ed40183b48882e130c1f1ffb73de069fd7"
[[package]]
name = "arbitrary"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db55d72333851e17d572bec876e390cd3b11eb1ef53ae821dd9f3b653d2b4569"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arrayref"
version = "0.3.6"
@ -690,13 +699,12 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "2.1.0"
source = "git+https://github.com/garious/curve25519-dalek?rev=60efef3553d6bf3d7f3b09b5f97acd54d72529ff#60efef3553d6bf3d7f3b09b5f97acd54d72529ff"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d85653f070353a16313d0046f173f70d1aadd5b42600a14de626f0dfb3473a5"
dependencies = [
"borsh",
"byteorder",
"digest 0.8.1",
"rand_core",
"serde",
"subtle 2.2.3",
"zeroize",
]
@ -704,12 +712,13 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d85653f070353a16313d0046f173f70d1aadd5b42600a14de626f0dfb3473a5"
source = "git+https://github.com/garious/curve25519-dalek?rev=60efef3553d6bf3d7f3b09b5f97acd54d72529ff#60efef3553d6bf3d7f3b09b5f97acd54d72529ff"
dependencies = [
"borsh",
"byteorder",
"digest 0.8.1",
"rand_core",
"serde",
"subtle 2.2.3",
"zeroize",
]
@ -749,6 +758,17 @@ dependencies = [
"syn 1.0.48",
]
[[package]]
name = "derive_arbitrary"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a012b5e473dc912f0db0546a1c9c6a194ce8494feb66fa0237160926f9e0e6"
dependencies = [
"proc-macro2 1.0.24",
"quote 1.0.7",
"syn 1.0.48",
]
[[package]]
name = "dialoguer"
version = "0.6.2"
@ -1258,6 +1278,17 @@ dependencies = [
"hmac",
]
[[package]]
name = "honggfuzz"
version = "0.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f085725a5828d7e959f014f624773094dfe20acc91be310ef106923c30594bc"
dependencies = [
"arbitrary",
"lazy_static",
"memmap",
]
[[package]]
name = "http"
version = "0.2.1"
@ -3490,6 +3521,7 @@ dependencies = [
name = "spl-token-swap"
version = "0.1.0"
dependencies = [
"arbitrary",
"arrayref",
"num-derive",
"num-traits",
@ -3499,6 +3531,17 @@ dependencies = [
"thiserror",
]
[[package]]
name = "spl-token-swap-fuzz"
version = "0.0.1"
dependencies = [
"arbitrary",
"honggfuzz",
"solana-program",
"spl-token 3.0.0",
"spl-token-swap",
]
[[package]]
name = "spl-token-v3"
version = "3.0.0"

View File

@ -15,6 +15,7 @@ members = [
"themis/program_ristretto",
"token-lending/program",
"token-swap/program",
"token-swap/program/fuzz",
"token/cli",
"token/perf-monitor",
"token/program",

39
ci/fuzz.sh Executable file
View File

@ -0,0 +1,39 @@
#!/usr/bin/env bash
set -e
usage() {
exitcode=0
if [[ -n "$1" ]]; then
exitcode=1
echo "Error: $*"
fi
echo "Usage: $0 [fuzz-target] [run-time-in-seconds]"
exit $exitcode
}
fuzz_target=$1
if [[ -z $fuzz_target ]]; then
usage "No fuzz target provided"
fi
run_time=$2
if [[ -z $2 ]]; then
usage "No runtime provided"
fi
set -x
HFUZZ_RUN_ARGS="--run_time $run_time --exit_upon_crash" cargo hfuzz run $fuzz_target
# Until https://github.com/rust-fuzz/honggfuzz-rs/issues/16 is resolved,
# hfuzz does not return an error code on crash, so look for a crash artifact
for crash_file in ./hfuzz_workspace/"$fuzz_target"/*.fuzz; do
# Check if the glob gets expanded to existing files.
if [[ -e "$crash_file" ]]; then
echo ".fuzz file $crash_file found, meaning some error occurred, exiting"
exit 1
fi
# Break early -- we just need one iteration to see if a failure occurred
break
done

View File

@ -10,4 +10,6 @@ sudo apt-get install -y openssl --allow-unauthenticated
sudo apt-get install -y libssl-dev --allow-unauthenticated
sudo apt-get install -y libssl1.1 --allow-unauthenticated
sudo apt-get install -y libudev-dev
sudo apt-get install -y binutils-dev
sudo apt-get install -y libunwind-dev
clang-7 --version

View File

@ -9,6 +9,7 @@ set -x
cargo --version
cargo install rustfilt || true
cargo install honggfuzz || true
export PATH="$HOME"/.local/share/solana/install/active_release/bin:"$PATH"
solana --version

View File

@ -10,6 +10,7 @@ edition = "2018"
[features]
no-entrypoint = []
production = []
fuzz = ["arbitrary"]
[dependencies]
arrayref = "0.3.6"
@ -19,6 +20,7 @@ solana-program = "1.4.9"
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 }
[dev-dependencies]
solana-sdk = "1.4.9"

View File

@ -0,0 +1,22 @@
[package]
name = "spl-token-swap-fuzz"
version = "0.0.1"
description = "Solana Program Library Token Swap Fuzzer"
authors = ["Solana Maintainers <maintainers@solana.foundation>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2018"
publish = false
[dependencies]
honggfuzz = { version = "0.5" }
arbitrary = { version = "0.4", features = ["derive"] }
solana-program = "1.4.8"
spl-token = { version = "3.0", path = "../../../token/program", features = [ "no-entrypoint" ] }
spl-token-swap = { path = "..", features = ["fuzz", "no-entrypoint"] }
[[bin]]
name = "token-swap-instructions"
path = "src/instructions.rs"
test = false
doc = false

View File

@ -0,0 +1,288 @@
use spl_token_swap_fuzz::{
native_account_data::NativeAccountData, native_token::get_token_balance,
native_token_swap::NativeTokenSwap,
};
use spl_token_swap::{
curve::{
base::{CurveType, SwapCurve},
constant_product::ConstantProductCurve,
},
error::SwapError,
instruction::{Deposit, Swap, Withdraw},
};
use spl_token::error::TokenError;
use honggfuzz::fuzz;
use arbitrary::Arbitrary;
use std::collections::{HashMap, HashSet};
#[derive(Debug, Arbitrary, Clone)]
enum FuzzInstruction {
Swap {
token_a_id: AccountId,
token_b_id: AccountId,
trade_direction: TradeDirection,
instruction: Swap,
},
Deposit {
token_a_id: AccountId,
token_b_id: AccountId,
pool_token_id: AccountId,
instruction: Deposit,
},
Withdraw {
token_a_id: AccountId,
token_b_id: AccountId,
pool_token_id: AccountId,
instruction: Withdraw,
},
}
/// Helper enum to tell which direction a swap is meant to go.
#[derive(Debug, Arbitrary, Clone)]
enum TradeDirection {
AtoB,
BtoA,
}
/// Use u8 as an account id to simplify the address space and re-use accounts
/// more often.
type AccountId = u8;
const INITIAL_SWAP_TOKEN_A_AMOUNT: u64 = 100_000_000_000;
const INITIAL_SWAP_TOKEN_B_AMOUNT: u64 = 300_000_000_000;
const INITIAL_USER_TOKEN_A_AMOUNT: u64 = 1_000_000_000;
const INITIAL_USER_TOKEN_B_AMOUNT: u64 = 3_000_000_000;
fn main() {
loop {
fuzz!(|fuzz_instructions: Vec<FuzzInstruction>| {
run_fuzz_instructions(fuzz_instructions)
});
}
}
fn run_fuzz_instructions(fuzz_instructions: Vec<FuzzInstruction>) {
let trade_fee_numerator = 25;
let trade_fee_denominator = 10000;
let owner_trade_fee_numerator = 5;
let owner_trade_fee_denominator = 10000;
let owner_withdraw_fee_numerator = 30;
let owner_withdraw_fee_denominator = 10000;
let host_fee_numerator = 1;
let host_fee_denominator = 5;
let swap_curve = SwapCurve {
curve_type: CurveType::ConstantProduct,
calculator: Box::new(ConstantProductCurve {
trade_fee_numerator,
trade_fee_denominator,
owner_trade_fee_numerator,
owner_trade_fee_denominator,
owner_withdraw_fee_numerator,
owner_withdraw_fee_denominator,
host_fee_numerator,
host_fee_denominator,
}),
};
let mut token_swap = NativeTokenSwap::new(
swap_curve,
INITIAL_SWAP_TOKEN_A_AMOUNT,
INITIAL_SWAP_TOKEN_B_AMOUNT,
);
// keep track of all accounts, including swap accounts
let mut token_a_accounts: HashMap<AccountId, NativeAccountData> = HashMap::new();
let mut token_b_accounts: HashMap<AccountId, NativeAccountData> = HashMap::new();
let mut pool_accounts: HashMap<AccountId, NativeAccountData> = HashMap::new();
// to ensure that we never create or remove base tokens
let before_total_token_a =
INITIAL_SWAP_TOKEN_A_AMOUNT + get_total_token_a_amount(&fuzz_instructions);
let before_total_token_b =
INITIAL_SWAP_TOKEN_B_AMOUNT + get_total_token_b_amount(&fuzz_instructions);
for fuzz_instruction in fuzz_instructions {
run_fuzz_instruction(
fuzz_instruction,
&mut token_swap,
&mut token_a_accounts,
&mut token_b_accounts,
&mut pool_accounts,
);
}
// check total token a and b amounts
let after_total_token_a = token_a_accounts
.values()
.map(get_token_balance)
.sum::<u64>()
+ get_token_balance(&token_swap.token_a_account);
assert_eq!(before_total_token_a, after_total_token_a);
let after_total_token_b = token_b_accounts
.values()
.map(get_token_balance)
.sum::<u64>()
+ get_token_balance(&token_swap.token_b_account);
assert_eq!(before_total_token_b, after_total_token_b);
// final check to make sure that withdrawing everything works
let mut withdrawn_token_a_account = token_swap.create_token_a_account(0);
let mut withdrawn_token_b_account = token_swap.create_token_b_account(0);
for mut pool_account in pool_accounts.values_mut() {
token_swap
.withdraw_all(
&mut pool_account,
&mut withdrawn_token_a_account,
&mut withdrawn_token_b_account,
)
.unwrap();
}
let mut pool_account = token_swap.pool_token_account.clone();
token_swap
.withdraw_all(
&mut pool_account,
&mut withdrawn_token_a_account,
&mut withdrawn_token_b_account,
)
.unwrap();
let mut fee_account = token_swap.pool_fee_account.clone();
token_swap
.withdraw_all(
&mut fee_account,
&mut withdrawn_token_a_account,
&mut withdrawn_token_b_account,
)
.unwrap();
let after_total_token_a = token_a_accounts
.values()
.map(get_token_balance)
.sum::<u64>()
+ get_token_balance(&withdrawn_token_a_account)
+ get_token_balance(&token_swap.token_a_account);
assert_eq!(before_total_token_a, after_total_token_a);
let after_total_token_b = token_b_accounts
.values()
.map(get_token_balance)
.sum::<u64>()
+ get_token_balance(&withdrawn_token_b_account)
+ get_token_balance(&token_swap.token_b_account);
assert_eq!(before_total_token_b, after_total_token_b);
}
fn run_fuzz_instruction(
fuzz_instruction: FuzzInstruction,
token_swap: &mut NativeTokenSwap,
token_a_accounts: &mut HashMap<AccountId, NativeAccountData>,
token_b_accounts: &mut HashMap<AccountId, NativeAccountData>,
pool_accounts: &mut HashMap<AccountId, NativeAccountData>,
) {
let result = match fuzz_instruction {
FuzzInstruction::Swap {
token_a_id,
token_b_id,
trade_direction,
instruction,
} => {
let mut token_a_account = token_a_accounts
.entry(token_a_id)
.or_insert_with(|| token_swap.create_token_a_account(INITIAL_USER_TOKEN_A_AMOUNT));
let mut token_b_account = token_b_accounts
.entry(token_b_id)
.or_insert_with(|| token_swap.create_token_b_account(INITIAL_USER_TOKEN_B_AMOUNT));
match trade_direction {
TradeDirection::AtoB => {
token_swap.swap_a_to_b(&mut token_a_account, &mut token_b_account, instruction)
}
TradeDirection::BtoA => {
token_swap.swap_b_to_a(&mut token_b_account, &mut token_a_account, instruction)
}
}
}
FuzzInstruction::Deposit {
token_a_id,
token_b_id,
pool_token_id,
instruction,
} => {
let mut token_a_account = token_a_accounts
.entry(token_a_id)
.or_insert_with(|| token_swap.create_token_a_account(INITIAL_USER_TOKEN_A_AMOUNT));
let mut token_b_account = token_b_accounts
.entry(token_b_id)
.or_insert_with(|| token_swap.create_token_b_account(INITIAL_USER_TOKEN_B_AMOUNT));
let mut pool_account = pool_accounts
.entry(pool_token_id)
.or_insert_with(|| token_swap.create_pool_account());
token_swap.deposit(
&mut token_a_account,
&mut token_b_account,
&mut pool_account,
instruction,
)
}
FuzzInstruction::Withdraw {
token_a_id,
token_b_id,
pool_token_id,
instruction,
} => {
let mut token_a_account = token_a_accounts
.entry(token_a_id)
.or_insert_with(|| token_swap.create_token_a_account(INITIAL_USER_TOKEN_A_AMOUNT));
let mut token_b_account = token_b_accounts
.entry(token_b_id)
.or_insert_with(|| token_swap.create_token_b_account(INITIAL_USER_TOKEN_B_AMOUNT));
let mut pool_account = pool_accounts
.entry(pool_token_id)
.or_insert_with(|| token_swap.create_pool_account());
token_swap.withdraw(
&mut pool_account,
&mut token_a_account,
&mut token_b_account,
instruction,
)
}
};
result
.map_err(|e| {
if !(e == SwapError::CalculationFailure.into()
|| e == SwapError::ConversionFailure.into()
|| e == SwapError::FeeCalculationFailure.into()
|| e == SwapError::ExceededSlippage.into()
|| e == SwapError::ZeroTradingTokens.into()
|| e == TokenError::InsufficientFunds.into())
{
Err(e).unwrap()
}
})
.ok();
}
fn get_total_token_a_amount(fuzz_instructions: &[FuzzInstruction]) -> u64 {
let mut token_a_ids = HashSet::new();
for fuzz_instruction in fuzz_instructions.iter() {
match fuzz_instruction {
FuzzInstruction::Swap { token_a_id, .. } => token_a_ids.insert(token_a_id),
FuzzInstruction::Deposit { token_a_id, .. } => token_a_ids.insert(token_a_id),
FuzzInstruction::Withdraw { token_a_id, .. } => token_a_ids.insert(token_a_id),
};
}
(token_a_ids.len() as u64) * INITIAL_USER_TOKEN_A_AMOUNT
}
fn get_total_token_b_amount(fuzz_instructions: &[FuzzInstruction]) -> u64 {
let mut token_b_ids = HashSet::new();
for fuzz_instruction in fuzz_instructions.iter() {
match fuzz_instruction {
FuzzInstruction::Swap { token_b_id, .. } => token_b_ids.insert(token_b_id),
FuzzInstruction::Deposit { token_b_id, .. } => token_b_ids.insert(token_b_id),
FuzzInstruction::Withdraw { token_b_id, .. } => token_b_ids.insert(token_b_id),
};
}
(token_b_ids.len() as u64) * INITIAL_USER_TOKEN_B_AMOUNT
}

View File

@ -0,0 +1,4 @@
pub mod native_account_data;
pub mod native_processor;
pub mod native_token;
pub mod native_token_swap;

View File

@ -0,0 +1,45 @@
use solana_program::{account_info::AccountInfo, clock::Epoch, pubkey::Pubkey};
#[derive(Clone)]
pub struct NativeAccountData {
pub key: Pubkey,
pub lamports: u64,
pub data: Vec<u8>,
pub program_id: Pubkey,
pub is_signer: bool,
}
impl NativeAccountData {
pub fn new(size: usize, program_id: Pubkey) -> Self {
Self {
key: Pubkey::new_unique(),
lamports: 0,
data: vec![0; size],
program_id,
is_signer: false,
}
}
pub fn new_from_account_info(account_info: &AccountInfo) -> Self {
Self {
key: *account_info.key,
lamports: **account_info.lamports.borrow(),
data: account_info.data.borrow().to_vec(),
program_id: *account_info.owner,
is_signer: account_info.is_signer,
}
}
pub fn as_account_info(&mut self) -> AccountInfo {
AccountInfo::new(
&self.key,
self.is_signer,
false,
&mut self.lamports,
&mut self.data[..],
&self.program_id,
false,
Epoch::default(),
)
}
}

View File

@ -0,0 +1,103 @@
use crate::native_account_data::NativeAccountData;
use solana_program::{
account_info::AccountInfo, entrypoint::ProgramResult, instruction::Instruction,
program_error::ProgramError, program_stubs, pubkey::Pubkey,
};
struct TestSyscallStubs {}
impl program_stubs::SyscallStubs for TestSyscallStubs {
fn sol_invoke_signed(
&self,
instruction: &Instruction,
account_infos: &[AccountInfo],
signers_seeds: &[&[&[u8]]],
) -> ProgramResult {
let mut new_account_infos = vec![];
// mimic check for token program in accounts
if !account_infos.iter().any(|x| *x.key == spl_token::id()) {
return Err(ProgramError::InvalidAccountData);
}
for meta in instruction.accounts.iter() {
for account_info in account_infos.iter() {
if meta.pubkey == *account_info.key {
let mut new_account_info = account_info.clone();
for seeds in signers_seeds.iter() {
let signer =
Pubkey::create_program_address(&seeds, &spl_token_swap::id()).unwrap();
if *account_info.key == signer {
new_account_info.is_signer = true;
}
}
new_account_infos.push(new_account_info);
}
}
}
spl_token::processor::Processor::process(
&instruction.program_id,
&new_account_infos,
&instruction.data,
)
}
}
fn test_syscall_stubs() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
program_stubs::set_syscall_stubs(Box::new(TestSyscallStubs {}));
});
}
pub fn do_process_instruction(instruction: Instruction, accounts: &[AccountInfo]) -> ProgramResult {
test_syscall_stubs();
// approximate the logic in the actual runtime which runs the instruction
// and only updates accounts if the instruction is successful
let mut account_data = accounts
.iter()
.map(NativeAccountData::new_from_account_info)
.collect::<Vec<_>>();
let account_infos = account_data
.iter_mut()
.map(NativeAccountData::as_account_info)
.collect::<Vec<_>>();
let res = if instruction.program_id == spl_token_swap::id() {
spl_token_swap::processor::Processor::process(
&instruction.program_id,
&account_infos,
&instruction.data,
)
} else {
spl_token::processor::Processor::process(
&instruction.program_id,
&account_infos,
&instruction.data,
)
};
if res.is_ok() {
let mut account_metas = instruction
.accounts
.iter()
.zip(accounts)
.map(|(account_meta, account)| (&account_meta.pubkey, account))
.collect::<Vec<_>>();
for account_info in account_infos.iter() {
for account_meta in account_metas.iter_mut() {
if account_info.key == account_meta.0 {
let account = &mut account_meta.1;
let mut lamports = account.lamports.borrow_mut();
**lamports = **account_info.lamports.borrow();
let mut data = account.data.borrow_mut();
data.clone_from_slice(*account_info.data.borrow());
}
}
}
}
res
}

View File

@ -0,0 +1,41 @@
use crate::native_account_data::NativeAccountData;
use spl_token::state::{Account as TokenAccount, AccountState as TokenAccountState, Mint};
use solana_program::{program_option::COption, program_pack::Pack, pubkey::Pubkey};
pub fn create_mint(owner: &Pubkey) -> NativeAccountData {
let mut account_data = NativeAccountData::new(Mint::LEN, spl_token::id());
let mint = Mint {
is_initialized: true,
mint_authority: COption::Some(*owner),
..Default::default()
};
Mint::pack(mint, &mut account_data.data[..]).unwrap();
account_data
}
pub fn create_token_account(
mint_account: &mut NativeAccountData,
owner: &Pubkey,
amount: u64,
) -> NativeAccountData {
let mut mint = Mint::unpack(&mint_account.data).unwrap();
let mut account_data = NativeAccountData::new(TokenAccount::LEN, spl_token::id());
let account = TokenAccount {
state: TokenAccountState::Initialized,
mint: mint_account.key,
owner: *owner,
amount,
..Default::default()
};
mint.supply += amount;
Mint::pack(mint, &mut mint_account.data[..]).unwrap();
TokenAccount::pack(account, &mut account_data.data[..]).unwrap();
account_data
}
pub fn get_token_balance(account_data: &NativeAccountData) -> u64 {
let account = TokenAccount::unpack(&account_data.data).unwrap();
account.amount
}

View File

@ -0,0 +1,418 @@
//! Helpers for working with swaps in a fuzzing environment
use crate::native_account_data::NativeAccountData;
use crate::native_processor::do_process_instruction;
use crate::native_token;
use spl_token_swap::{
curve::base::SwapCurve,
instruction::{self, Deposit, Swap, Withdraw},
state::SwapInfo,
};
use spl_token::instruction::approve;
use solana_program::{
bpf_loader, entrypoint::ProgramResult, program_pack::Pack, pubkey::Pubkey, system_program,
};
pub struct NativeTokenSwap {
pub user_account: NativeAccountData,
pub nonce: u8,
pub authority_account: NativeAccountData,
pub swap_curve: SwapCurve,
pub swap_account: NativeAccountData,
pub pool_mint_account: NativeAccountData,
pub pool_fee_account: NativeAccountData,
pub pool_token_account: NativeAccountData,
pub token_a_account: NativeAccountData,
pub token_a_mint_account: NativeAccountData,
pub token_b_account: NativeAccountData,
pub token_b_mint_account: NativeAccountData,
pub token_program_account: NativeAccountData,
}
pub fn create_program_account(program_id: Pubkey) -> NativeAccountData {
let mut account_data = NativeAccountData::new(0, bpf_loader::id());
account_data.key = program_id;
account_data
}
impl NativeTokenSwap {
pub fn new(swap_curve: SwapCurve, token_a_amount: u64, token_b_amount: u64) -> Self {
let mut user_account = NativeAccountData::new(0, system_program::id());
user_account.is_signer = true;
let mut swap_account = NativeAccountData::new(SwapInfo::LEN, spl_token_swap::id());
let (authority_key, nonce) = Pubkey::find_program_address(
&[&swap_account.key.to_bytes()[..]],
&spl_token_swap::id(),
);
let mut authority_account = create_program_account(authority_key);
let mut token_program_account = create_program_account(spl_token::id());
let mut pool_mint_account = native_token::create_mint(&authority_account.key);
let mut pool_token_account =
native_token::create_token_account(&mut pool_mint_account, &user_account.key, 0);
let mut pool_fee_account =
native_token::create_token_account(&mut pool_mint_account, &user_account.key, 0);
let mut token_a_mint_account = native_token::create_mint(&user_account.key);
let mut token_a_account = native_token::create_token_account(
&mut token_a_mint_account,
&authority_account.key,
token_a_amount,
);
let mut token_b_mint_account = native_token::create_mint(&user_account.key);
let mut token_b_account = native_token::create_token_account(
&mut token_b_mint_account,
&authority_account.key,
token_b_amount,
);
let init_instruction = instruction::initialize(
&spl_token_swap::id(),
&spl_token::id(),
&swap_account.key,
&authority_account.key,
&token_a_account.key,
&token_b_account.key,
&pool_mint_account.key,
&pool_fee_account.key,
&pool_token_account.key,
nonce,
swap_curve.clone(),
)
.unwrap();
do_process_instruction(
init_instruction,
&[
swap_account.as_account_info(),
authority_account.as_account_info(),
token_a_account.as_account_info(),
token_b_account.as_account_info(),
pool_mint_account.as_account_info(),
pool_fee_account.as_account_info(),
pool_token_account.as_account_info(),
token_program_account.as_account_info(),
],
)
.unwrap();
Self {
user_account,
nonce,
authority_account,
swap_curve,
swap_account,
pool_mint_account,
pool_fee_account,
pool_token_account,
token_a_account,
token_a_mint_account,
token_b_account,
token_b_mint_account,
token_program_account,
}
}
pub fn create_pool_account(&mut self) -> NativeAccountData {
native_token::create_token_account(&mut self.pool_mint_account, &self.user_account.key, 0)
}
pub fn create_token_a_account(&mut self, amount: u64) -> NativeAccountData {
native_token::create_token_account(
&mut self.token_a_mint_account,
&self.user_account.key,
amount,
)
}
pub fn create_token_b_account(&mut self, amount: u64) -> NativeAccountData {
native_token::create_token_account(
&mut self.token_b_mint_account,
&self.user_account.key,
amount,
)
}
pub fn swap_a_to_b(
&mut self,
token_a_account: &mut NativeAccountData,
token_b_account: &mut NativeAccountData,
instruction: Swap,
) -> ProgramResult {
do_process_instruction(
approve(
&self.token_program_account.key,
&token_a_account.key,
&self.authority_account.key,
&self.user_account.key,
&[],
instruction.amount_in,
)
.unwrap(),
&[
token_a_account.as_account_info(),
self.authority_account.as_account_info(),
self.user_account.as_account_info(),
],
)
.unwrap();
let swap_instruction = instruction::swap(
&spl_token_swap::id(),
&spl_token::id(),
&self.swap_account.key,
&self.authority_account.key,
&token_a_account.key,
&self.token_a_account.key,
&self.token_b_account.key,
&token_b_account.key,
&self.pool_mint_account.key,
&self.pool_fee_account.key,
Some(&self.pool_token_account.key),
instruction,
)
.unwrap();
do_process_instruction(
swap_instruction,
&[
self.swap_account.as_account_info(),
self.authority_account.as_account_info(),
token_a_account.as_account_info(),
self.token_a_account.as_account_info(),
self.token_b_account.as_account_info(),
token_b_account.as_account_info(),
self.pool_mint_account.as_account_info(),
self.pool_fee_account.as_account_info(),
self.token_program_account.as_account_info(),
self.pool_token_account.as_account_info(),
],
)
}
pub fn swap_b_to_a(
&mut self,
token_b_account: &mut NativeAccountData,
token_a_account: &mut NativeAccountData,
instruction: Swap,
) -> ProgramResult {
do_process_instruction(
approve(
&self.token_program_account.key,
&token_b_account.key,
&self.authority_account.key,
&self.user_account.key,
&[],
instruction.amount_in,
)
.unwrap(),
&[
token_b_account.as_account_info(),
self.authority_account.as_account_info(),
self.user_account.as_account_info(),
],
)
.unwrap();
let swap_instruction = instruction::swap(
&spl_token_swap::id(),
&spl_token::id(),
&self.swap_account.key,
&self.authority_account.key,
&token_b_account.key,
&self.token_b_account.key,
&self.token_a_account.key,
&token_a_account.key,
&self.pool_mint_account.key,
&self.pool_fee_account.key,
Some(&self.pool_token_account.key),
instruction,
)
.unwrap();
do_process_instruction(
swap_instruction,
&[
self.swap_account.as_account_info(),
self.authority_account.as_account_info(),
token_b_account.as_account_info(),
self.token_b_account.as_account_info(),
self.token_a_account.as_account_info(),
token_a_account.as_account_info(),
self.pool_mint_account.as_account_info(),
self.pool_fee_account.as_account_info(),
self.token_program_account.as_account_info(),
self.pool_token_account.as_account_info(),
],
)
}
pub fn deposit(
&mut self,
token_a_account: &mut NativeAccountData,
token_b_account: &mut NativeAccountData,
pool_account: &mut NativeAccountData,
mut instruction: Deposit,
) -> ProgramResult {
do_process_instruction(
approve(
&self.token_program_account.key,
&token_a_account.key,
&self.authority_account.key,
&self.user_account.key,
&[],
instruction.maximum_token_a_amount,
)
.unwrap(),
&[
token_a_account.as_account_info(),
self.authority_account.as_account_info(),
self.user_account.as_account_info(),
],
)
.unwrap();
do_process_instruction(
approve(
&self.token_program_account.key,
&token_b_account.key,
&self.authority_account.key,
&self.user_account.key,
&[],
instruction.maximum_token_b_amount,
)
.unwrap(),
&[
token_b_account.as_account_info(),
self.authority_account.as_account_info(),
self.user_account.as_account_info(),
],
)
.unwrap();
// special logic: if we only deposit 1 pool token, we can't withdraw it
// because we incur a withdrawal fee, so we hack it to not be 1
if instruction.pool_token_amount == 1 {
instruction.pool_token_amount = 2;
}
let deposit_instruction = instruction::deposit(
&spl_token_swap::id(),
&spl_token::id(),
&self.swap_account.key,
&self.authority_account.key,
&token_a_account.key,
&token_b_account.key,
&self.token_a_account.key,
&self.token_b_account.key,
&self.pool_mint_account.key,
&pool_account.key,
instruction,
)
.unwrap();
do_process_instruction(
deposit_instruction,
&[
self.swap_account.as_account_info(),
self.authority_account.as_account_info(),
token_a_account.as_account_info(),
token_b_account.as_account_info(),
self.token_a_account.as_account_info(),
self.token_b_account.as_account_info(),
self.pool_mint_account.as_account_info(),
pool_account.as_account_info(),
self.token_program_account.as_account_info(),
],
)
}
pub fn withdraw(
&mut self,
pool_account: &mut NativeAccountData,
token_a_account: &mut NativeAccountData,
token_b_account: &mut NativeAccountData,
mut instruction: Withdraw,
) -> ProgramResult {
let pool_token_amount = native_token::get_token_balance(&pool_account);
// special logic to avoid withdrawing down to 1 pool token, which
// eventually causes an error on withdrawing all
if pool_token_amount.saturating_sub(instruction.pool_token_amount) == 1 {
instruction.pool_token_amount = pool_token_amount;
}
do_process_instruction(
approve(
&self.token_program_account.key,
&pool_account.key,
&self.authority_account.key,
&self.user_account.key,
&[],
instruction.pool_token_amount,
)
.unwrap(),
&[
pool_account.as_account_info(),
self.authority_account.as_account_info(),
self.user_account.as_account_info(),
],
)
.unwrap();
let withdraw_instruction = instruction::withdraw(
&spl_token_swap::id(),
&spl_token::id(),
&self.swap_account.key,
&self.authority_account.key,
&self.pool_mint_account.key,
&self.pool_fee_account.key,
&pool_account.key,
&self.token_a_account.key,
&self.token_b_account.key,
&token_a_account.key,
&token_b_account.key,
instruction,
)
.unwrap();
do_process_instruction(
withdraw_instruction,
&[
self.swap_account.as_account_info(),
self.authority_account.as_account_info(),
self.pool_mint_account.as_account_info(),
pool_account.as_account_info(),
self.token_a_account.as_account_info(),
self.token_b_account.as_account_info(),
token_a_account.as_account_info(),
token_b_account.as_account_info(),
self.pool_fee_account.as_account_info(),
self.token_program_account.as_account_info(),
],
)
}
pub fn withdraw_all(
&mut self,
mut pool_account: &mut NativeAccountData,
mut token_a_account: &mut NativeAccountData,
mut token_b_account: &mut NativeAccountData,
) -> ProgramResult {
let pool_token_amount = native_token::get_token_balance(&pool_account);
if pool_token_amount > 0 {
let instruction = Withdraw {
pool_token_amount,
minimum_token_a_amount: 0,
minimum_token_b_amount: 0,
};
self.withdraw(
&mut pool_account,
&mut token_a_account,
&mut token_b_account,
instruction,
)
} else {
Ok(())
}
}
}

View File

@ -53,7 +53,7 @@ impl Default for SwapCurve {
/// Clone takes advantage of pack / unpack to get around the difficulty of
/// cloning dynamic objects.
/// Note that this is only to be used for testing.
#[cfg(test)]
#[cfg(any(test, feature = "fuzz"))]
impl Clone for SwapCurve {
fn clone(&self) -> Self {
let mut packed_self = [0u8; Self::LEN];

View File

@ -13,6 +13,59 @@ use solana_program::{
use std::convert::TryInto;
use std::mem::size_of;
#[cfg(feature = "fuzz")]
use arbitrary::Arbitrary;
/// Initialize instruction data
#[repr(C)]
#[derive(Debug, PartialEq)]
pub struct Initialize {
/// nonce used to create valid program address
pub nonce: u8,
/// swap curve info for pool, including CurveType, fees, and anything
/// else that may be required
pub swap_curve: SwapCurve,
}
/// Swap instruction data
#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub struct Swap {
/// SOURCE amount to transfer, output to DESTINATION is based on the exchange rate
pub amount_in: u64,
/// Minimum amount of DESTINATION token to output, prevents excessive slippage
pub minimum_amount_out: u64,
}
/// Deposit instruction data
#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub struct Deposit {
/// Pool token amount to transfer. token_a and token_b amount are set by
/// the current exchange rate and size of the pool
pub pool_token_amount: u64,
/// Maximum token A amount to deposit, prevents excessive slippage
pub maximum_token_a_amount: u64,
/// Maximum token B amount to deposit, prevents excessive slippage
pub maximum_token_b_amount: u64,
}
/// Withdraw instruction data
#[cfg_attr(feature = "fuzz", derive(Arbitrary))]
#[repr(C)]
#[derive(Clone, Debug, PartialEq)]
pub struct Withdraw {
/// Amount of pool tokens to burn. User receives an output of token a
/// and b based on the percentage of the pool tokens that are returned.
pub pool_token_amount: u64,
/// Minimum amount of token A to receive, prevents excessive slippage
pub minimum_token_a_amount: u64,
/// Minimum amount of token B to receive, prevents excessive slippage
pub minimum_token_b_amount: u64,
}
/// Instructions supported by the SwapInfo program.
#[repr(C)]
#[derive(Debug, PartialEq)]
@ -29,13 +82,7 @@ pub enum SwapInstruction {
/// 6. `[writable]` Pool Token Account to deposit the initial pool token
/// supply. Must be empty, not owned by $authority.
/// 7. '[]` Token program id
Initialize {
/// nonce used to create valid program address
nonce: u8,
/// swap curve info for pool, including CurveType, fees, and anything
/// else that may be required
swap_curve: SwapCurve,
},
Initialize(Initialize),
/// Swap the tokens in the pool.
///
@ -49,12 +96,7 @@ pub enum SwapInstruction {
/// 7. `[writable]` Fee account, to receive trading fees
/// 8. '[]` Token program id
/// 9. `[optional, writable]` Host fee account to receive additional trading fees
Swap {
/// SOURCE amount to transfer, output to DESTINATION is based on the exchange rate
amount_in: u64,
/// Minimum amount of DESTINATION token to output, prevents excessive slippage
minimum_amount_out: u64,
},
Swap(Swap),
/// Deposit some tokens into the pool. The output is a "pool" token representing ownership
/// into the pool. Inputs are converted to the current ratio.
@ -68,15 +110,7 @@ pub enum SwapInstruction {
/// 6. `[writable]` Pool MINT account, $authority is the owner.
/// 7. `[writable]` Pool Account to deposit the generated tokens, user is the owner.
/// 8. '[]` Token program id
Deposit {
/// Pool token amount to transfer. token_a and token_b amount are set by
/// the current exchange rate and size of the pool
pool_token_amount: u64,
/// Maximum token A amount to deposit, prevents excessive slippage
maximum_token_a_amount: u64,
/// Maximum token B amount to deposit, prevents excessive slippage
maximum_token_b_amount: u64,
},
Deposit(Deposit),
/// Withdraw the token from the pool at the current ratio.
///
@ -90,15 +124,7 @@ pub enum SwapInstruction {
/// 7. `[writable]` token_b user Account to credit.
/// 8. `[writable]` Fee account, to receive withdrawal fees
/// 9. '[]` Token program id
Withdraw {
/// Amount of pool tokens to burn. User receives an output of token a
/// and b based on the percentage of the pool tokens that are returned.
pool_token_amount: u64,
/// Minimum amount of token A to receive, prevents excessive slippage
minimum_token_a_amount: u64,
/// Minimum amount of token B to receive, prevents excessive slippage
minimum_token_b_amount: u64,
},
Withdraw(Withdraw),
}
impl SwapInstruction {
@ -109,35 +135,35 @@ impl SwapInstruction {
0 => {
let (&nonce, rest) = rest.split_first().ok_or(SwapError::InvalidInstruction)?;
let swap_curve = SwapCurve::unpack_unchecked(rest)?;
Self::Initialize { nonce, swap_curve }
Self::Initialize(Initialize { nonce, swap_curve })
}
1 => {
let (amount_in, rest) = Self::unpack_u64(rest)?;
let (minimum_amount_out, _rest) = Self::unpack_u64(rest)?;
Self::Swap {
Self::Swap(Swap {
amount_in,
minimum_amount_out,
}
})
}
2 => {
let (pool_token_amount, rest) = Self::unpack_u64(rest)?;
let (maximum_token_a_amount, rest) = Self::unpack_u64(rest)?;
let (maximum_token_b_amount, _rest) = Self::unpack_u64(rest)?;
Self::Deposit {
Self::Deposit(Deposit {
pool_token_amount,
maximum_token_a_amount,
maximum_token_b_amount,
}
})
}
3 => {
let (pool_token_amount, rest) = Self::unpack_u64(rest)?;
let (minimum_token_a_amount, rest) = Self::unpack_u64(rest)?;
let (minimum_token_b_amount, _rest) = Self::unpack_u64(rest)?;
Self::Withdraw {
Self::Withdraw(Withdraw {
pool_token_amount,
minimum_token_a_amount,
minimum_token_b_amount,
}
})
}
_ => return Err(SwapError::InvalidInstruction.into()),
})
@ -161,36 +187,36 @@ impl SwapInstruction {
pub fn pack(&self) -> Vec<u8> {
let mut buf = Vec::with_capacity(size_of::<Self>());
match &*self {
Self::Initialize { nonce, swap_curve } => {
Self::Initialize(Initialize { nonce, swap_curve }) => {
buf.push(0);
buf.push(*nonce);
let mut swap_curve_slice = [0u8; SwapCurve::LEN];
Pack::pack_into_slice(swap_curve, &mut swap_curve_slice[..]);
buf.extend_from_slice(&swap_curve_slice);
}
Self::Swap {
Self::Swap(Swap {
amount_in,
minimum_amount_out,
} => {
}) => {
buf.push(1);
buf.extend_from_slice(&amount_in.to_le_bytes());
buf.extend_from_slice(&minimum_amount_out.to_le_bytes());
}
Self::Deposit {
Self::Deposit(Deposit {
pool_token_amount,
maximum_token_a_amount,
maximum_token_b_amount,
} => {
}) => {
buf.push(2);
buf.extend_from_slice(&pool_token_amount.to_le_bytes());
buf.extend_from_slice(&maximum_token_a_amount.to_le_bytes());
buf.extend_from_slice(&maximum_token_b_amount.to_le_bytes());
}
Self::Withdraw {
Self::Withdraw(Withdraw {
pool_token_amount,
minimum_token_a_amount,
minimum_token_b_amount,
} => {
}) => {
buf.push(3);
buf.extend_from_slice(&pool_token_amount.to_le_bytes());
buf.extend_from_slice(&minimum_token_a_amount.to_le_bytes());
@ -215,7 +241,7 @@ pub fn initialize(
nonce: u8,
swap_curve: SwapCurve,
) -> Result<Instruction, ProgramError> {
let init_data = SwapInstruction::Initialize { nonce, swap_curve };
let init_data = SwapInstruction::Initialize(Initialize { nonce, swap_curve });
let data = init_data.pack();
let accounts = vec![
@ -248,16 +274,9 @@ pub fn deposit(
swap_token_b_pubkey: &Pubkey,
pool_mint_pubkey: &Pubkey,
destination_pubkey: &Pubkey,
pool_token_amount: u64,
maximum_token_a_amount: u64,
maximum_token_b_amount: u64,
instruction: Deposit,
) -> Result<Instruction, ProgramError> {
let data = SwapInstruction::Deposit {
pool_token_amount,
maximum_token_a_amount,
maximum_token_b_amount,
}
.pack();
let data = SwapInstruction::Deposit(instruction).pack();
let accounts = vec![
AccountMeta::new_readonly(*swap_pubkey, false),
@ -291,16 +310,9 @@ pub fn withdraw(
swap_token_b_pubkey: &Pubkey,
destination_token_a_pubkey: &Pubkey,
destination_token_b_pubkey: &Pubkey,
pool_token_amount: u64,
minimum_token_a_amount: u64,
minimum_token_b_amount: u64,
instruction: Withdraw,
) -> Result<Instruction, ProgramError> {
let data = SwapInstruction::Withdraw {
pool_token_amount,
minimum_token_a_amount,
minimum_token_b_amount,
}
.pack();
let data = SwapInstruction::Withdraw(instruction).pack();
let accounts = vec![
AccountMeta::new_readonly(*swap_pubkey, false),
@ -335,14 +347,9 @@ pub fn swap(
pool_mint_pubkey: &Pubkey,
pool_fee_pubkey: &Pubkey,
host_fee_pubkey: Option<&Pubkey>,
amount_in: u64,
minimum_amount_out: u64,
instruction: Swap,
) -> Result<Instruction, ProgramError> {
let data = SwapInstruction::Swap {
amount_in,
minimum_amount_out,
}
.pack();
let data = SwapInstruction::Swap(instruction).pack();
let mut accounts = vec![
AccountMeta::new_readonly(*swap_pubkey, false),
@ -411,7 +418,7 @@ mod tests {
curve_type,
calculator,
};
let check = SwapInstruction::Initialize { nonce, swap_curve };
let check = SwapInstruction::Initialize(Initialize { nonce, swap_curve });
let packed = check.pack();
let mut expect = vec![];
expect.push(0u8);
@ -432,10 +439,10 @@ mod tests {
let amount_in: u64 = 2;
let minimum_amount_out: u64 = 10;
let check = SwapInstruction::Swap {
let check = SwapInstruction::Swap(Swap {
amount_in,
minimum_amount_out,
};
});
let packed = check.pack();
let mut expect = vec![1];
expect.extend_from_slice(&amount_in.to_le_bytes());
@ -447,11 +454,11 @@ mod tests {
let pool_token_amount: u64 = 5;
let maximum_token_a_amount: u64 = 10;
let maximum_token_b_amount: u64 = 20;
let check = SwapInstruction::Deposit {
let check = SwapInstruction::Deposit(Deposit {
pool_token_amount,
maximum_token_a_amount,
maximum_token_b_amount,
};
});
let packed = check.pack();
let mut expect = vec![2];
expect.extend_from_slice(&pool_token_amount.to_le_bytes());
@ -464,11 +471,11 @@ mod tests {
let pool_token_amount: u64 = 1212438012089;
let minimum_token_a_amount: u64 = 102198761982612;
let minimum_token_b_amount: u64 = 2011239855213;
let check = SwapInstruction::Withdraw {
let check = SwapInstruction::Withdraw(Withdraw {
pool_token_amount,
minimum_token_a_amount,
minimum_token_b_amount,
};
});
let packed = check.pack();
let mut expect = vec![3];
expect.extend_from_slice(&pool_token_amount.to_le_bytes());

View File

@ -2,7 +2,10 @@
use crate::constraints::{FeeConstraints, FEE_CONSTRAINTS};
use crate::{
curve::base::SwapCurve, error::SwapError, instruction::SwapInstruction, state::SwapInfo,
curve::base::SwapCurve,
error::SwapError,
instruction::{Deposit, Initialize, Swap, SwapInstruction, Withdraw},
state::SwapInfo,
};
use num_traits::FromPrimitive;
use solana_program::{
@ -550,7 +553,8 @@ impl Processor {
to_u128(token_a.amount)?,
)
.ok_or(SwapError::ZeroTradingTokens)?;
if a_amount < to_u128(minimum_token_a_amount)? {
let a_amount = to_u64(a_amount)?;
if a_amount < minimum_token_a_amount {
return Err(SwapError::ExceededSlippage.into());
}
let b_amount = calculator
@ -572,7 +576,7 @@ impl Processor {
dest_token_a_info.clone(),
authority_info.clone(),
token_swap.nonce,
to_u64(a_amount)?,
a_amount,
)?;
Self::token_transfer(
swap_info.key,
@ -620,22 +624,22 @@ impl Processor {
) -> ProgramResult {
let instruction = SwapInstruction::unpack(input)?;
match instruction {
SwapInstruction::Initialize { nonce, swap_curve } => {
SwapInstruction::Initialize(Initialize { nonce, swap_curve }) => {
info!("Instruction: Init");
Self::process_initialize(program_id, nonce, swap_curve, accounts, fee_constraints)
}
SwapInstruction::Swap {
SwapInstruction::Swap(Swap {
amount_in,
minimum_amount_out,
} => {
}) => {
info!("Instruction: Swap");
Self::process_swap(program_id, amount_in, minimum_amount_out, accounts)
}
SwapInstruction::Deposit {
SwapInstruction::Deposit(Deposit {
pool_token_amount,
maximum_token_a_amount,
maximum_token_b_amount,
} => {
}) => {
info!("Instruction: Deposit");
Self::process_deposit(
program_id,
@ -645,11 +649,11 @@ impl Processor {
accounts,
)
}
SwapInstruction::Withdraw {
SwapInstruction::Withdraw(Withdraw {
pool_token_amount,
minimum_token_a_amount,
minimum_token_b_amount,
} => {
}) => {
info!("Instruction: Withdraw");
Self::process_withdraw(
program_id,
@ -1039,8 +1043,10 @@ mod tests {
&self.pool_mint_key,
&self.pool_fee_key,
None,
amount_in,
minimum_amount_out,
Swap {
amount_in,
minimum_amount_out,
},
)
.unwrap(),
vec![
@ -1072,9 +1078,9 @@ mod tests {
mut depositor_token_b_account: &mut Account,
depositor_pool_key: &Pubkey,
mut depositor_pool_account: &mut Account,
pool_amount: u64,
amount_a: u64,
amount_b: u64,
pool_token_amount: u64,
maximum_token_a_amount: u64,
maximum_token_b_amount: u64,
) -> ProgramResult {
do_process_instruction(
approve(
@ -1083,7 +1089,7 @@ mod tests {
&self.authority_key,
&depositor_key,
&[],
amount_a,
maximum_token_a_amount,
)
.unwrap(),
vec![
@ -1101,7 +1107,7 @@ mod tests {
&self.authority_key,
&depositor_key,
&[],
amount_b,
maximum_token_b_amount,
)
.unwrap(),
vec![
@ -1124,9 +1130,11 @@ mod tests {
&self.token_b_key,
&self.pool_mint_key,
&depositor_pool_key,
pool_amount,
amount_a,
amount_b,
Deposit {
pool_token_amount,
maximum_token_a_amount,
maximum_token_b_amount,
},
)
.unwrap(),
vec![
@ -1153,9 +1161,9 @@ mod tests {
mut token_a_account: &mut Account,
token_b_key: &Pubkey,
mut token_b_account: &mut Account,
pool_amount: u64,
minimum_a_amount: u64,
minimum_b_amount: u64,
pool_token_amount: u64,
minimum_token_a_amount: u64,
minimum_token_b_amount: u64,
) -> ProgramResult {
// approve swap program to take out pool tokens
do_process_instruction(
@ -1165,7 +1173,7 @@ mod tests {
&self.authority_key,
&user_key,
&[],
pool_amount,
pool_token_amount,
)
.unwrap(),
vec![
@ -1190,9 +1198,11 @@ mod tests {
&self.token_b_key,
&token_a_key,
&token_b_key,
pool_amount,
minimum_a_amount,
minimum_b_amount,
Withdraw {
pool_token_amount,
minimum_token_a_amount,
minimum_token_b_amount,
},
)
.unwrap(),
vec![
@ -2357,9 +2367,11 @@ mod tests {
&accounts.token_b_key,
&accounts.pool_mint_key,
&pool_key,
pool_amount.try_into().unwrap(),
deposit_a,
deposit_b,
Deposit {
pool_token_amount: pool_amount.try_into().unwrap(),
maximum_token_a_amount: deposit_a,
maximum_token_b_amount: deposit_b,
},
)
.unwrap(),
vec![
@ -2402,9 +2414,11 @@ mod tests {
&accounts.token_b_key,
&accounts.pool_mint_key,
&pool_key,
pool_amount.try_into().unwrap(),
deposit_a,
deposit_b,
Deposit {
pool_token_amount: pool_amount.try_into().unwrap(),
maximum_token_a_amount: deposit_a,
maximum_token_b_amount: deposit_b,
},
)
.unwrap(),
vec![
@ -2705,8 +2719,8 @@ mod tests {
let initial_b = token_b_amount / 10;
let initial_pool = swap_curve.calculator.new_pool_supply() / 10;
let withdraw_amount = initial_pool / 4;
let minimum_a_amount = initial_a / 40;
let minimum_b_amount = initial_b / 40;
let minimum_token_a_amount = initial_a / 40;
let minimum_token_b_amount = initial_b / 40;
let mut accounts =
SwapAccountInfo::new(&user_key, swap_curve, token_a_amount, token_b_amount);
@ -2732,8 +2746,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
}
@ -2767,8 +2781,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
accounts.authority_key = old_authority;
@ -2801,8 +2815,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount / 2,
minimum_b_amount / 2,
minimum_token_a_amount / 2,
minimum_token_b_amount / 2,
)
);
}
@ -2834,8 +2848,8 @@ mod tests {
&token_a_key,
&mut token_a_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
}
@ -2881,8 +2895,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
}
@ -2932,8 +2946,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
),
);
accounts.pool_fee_account = old_pool_fee_account;
@ -2971,9 +2985,11 @@ mod tests {
&accounts.token_b_key,
&token_a_key,
&token_b_key,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
Withdraw {
pool_token_amount: withdraw_amount.try_into().unwrap(),
minimum_token_a_amount,
minimum_token_b_amount,
}
)
.unwrap(),
vec![
@ -3024,9 +3040,11 @@ mod tests {
&accounts.token_b_key,
&token_a_key,
&token_b_key,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
Withdraw {
pool_token_amount: withdraw_amount.try_into().unwrap(),
minimum_token_a_amount,
minimum_token_b_amount,
},
)
.unwrap(),
vec![
@ -3080,8 +3098,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
@ -3106,8 +3124,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
@ -3149,8 +3167,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
@ -3185,8 +3203,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
1,
minimum_a_amount * 10,
minimum_b_amount,
minimum_token_a_amount * 10,
minimum_token_b_amount,
)
);
}
@ -3219,8 +3237,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount * 10,
minimum_b_amount,
minimum_token_a_amount * 10,
minimum_token_b_amount,
)
);
// minimum B amount out too high
@ -3235,8 +3253,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount * 10,
minimum_token_a_amount,
minimum_token_b_amount * 10,
)
);
}
@ -3270,8 +3288,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
let swap_token_b_key = accounts.token_b_key;
@ -3287,8 +3305,8 @@ mod tests {
&swap_token_b_key,
&mut swap_token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
);
}
@ -3320,8 +3338,8 @@ mod tests {
&token_b_key,
&mut token_b_account,
withdraw_amount.try_into().unwrap(),
minimum_a_amount,
minimum_b_amount,
minimum_token_a_amount,
minimum_token_b_amount,
)
.unwrap();
@ -3479,7 +3497,7 @@ mod tests {
) = accounts.setup_token_accounts(&user_key, &swapper_key, initial_a, initial_b, 0);
// swap one way
let a_to_b_amount = initial_a / 10;
let minimum_b_amount = 0;
let minimum_token_b_amount = 0;
let pool_mint = Processor::unpack_mint(&accounts.pool_mint_account.data).unwrap();
let initial_supply = pool_mint.supply;
accounts
@ -3492,7 +3510,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
a_to_b_amount,
minimum_b_amount,
minimum_token_b_amount,
)
.unwrap();
@ -3772,8 +3790,10 @@ mod tests {
&accounts.pool_mint_key,
&accounts.pool_fee_key,
Some(&pool_key),
amount_in,
minimum_amount_out,
Swap {
amount_in,
minimum_amount_out,
},
)
.unwrap(),
vec![
@ -3837,7 +3857,7 @@ mod tests {
let initial_a = token_a_amount / 5;
let initial_b = token_b_amount / 5;
let minimum_b_amount = initial_b / 2;
let minimum_token_b_amount = initial_b / 2;
let swap_token_a_key = accounts.token_a_key;
let swap_token_b_key = accounts.token_b_key;
@ -3863,7 +3883,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
}
@ -3897,7 +3917,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
accounts.authority_key = old_authority;
@ -3929,8 +3949,10 @@ mod tests {
&accounts.pool_mint_key,
&accounts.pool_fee_key,
None,
initial_a,
minimum_b_amount,
Swap {
amount_in: initial_a,
minimum_amount_out: minimum_token_b_amount,
},
)
.unwrap(),
vec![
@ -3969,7 +3991,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a * 2,
minimum_b_amount * 2,
minimum_token_b_amount * 2,
)
);
}
@ -3999,8 +4021,10 @@ mod tests {
&accounts.pool_mint_key,
&accounts.pool_fee_key,
None,
initial_a,
minimum_b_amount,
Swap {
amount_in: initial_a,
minimum_amount_out: minimum_token_b_amount,
},
)
.unwrap(),
vec![
@ -4039,7 +4063,7 @@ mod tests {
&token_a_key,
&mut token_a_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
}
@ -4065,7 +4089,7 @@ mod tests {
&token_a_key,
&mut token_a_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
}
@ -4098,7 +4122,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
@ -4131,7 +4155,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
accounts.pool_fee_account = old_pool_fee_account;
@ -4163,8 +4187,10 @@ mod tests {
&accounts.pool_mint_key,
&accounts.pool_fee_key,
None,
initial_a,
minimum_b_amount,
Swap {
amount_in: initial_a,
minimum_amount_out: minimum_token_b_amount,
},
)
.unwrap(),
vec![
@ -4229,7 +4255,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a,
minimum_b_amount * 2,
minimum_token_b_amount * 2,
)
);
}
@ -4257,7 +4283,7 @@ mod tests {
&token_b_key,
&mut token_b_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
let mut swap_token_b_account = accounts.get_token_account(&swap_token_b_key).clone();
@ -4272,7 +4298,7 @@ mod tests {
&swap_token_b_key,
&mut swap_token_b_account,
initial_a,
minimum_b_amount,
minimum_token_b_amount,
)
);
}
@ -4308,8 +4334,10 @@ mod tests {
&accounts.pool_mint_key,
&accounts.pool_fee_key,
None,
initial_a,
minimum_b_amount,
Swap {
amount_in: initial_a,
minimum_amount_out: minimum_token_b_amount,
},
)
.unwrap(),
vec![
@ -4369,8 +4397,10 @@ mod tests {
&accounts.pool_mint_key,
&accounts.pool_fee_key,
Some(&bad_token_a_key),
initial_a,
0,
Swap {
amount_in: initial_a,
minimum_amount_out: 0,
},
)
.unwrap(),
vec![