use super::*;
use solana_instruction::Instruction;
use spl_token_2022_interface::extension::ExtensionType;
use crate::constants::{TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID};
use crate::utils::{BuildError, MintInfo, TradeMints, append_hooks};
const MINT: Address = Address::new_from_array([21; 32]);
const PROGRAM: Address = Address::new_from_array([22; 32]);
const SOURCE: Address = Address::new_from_array([23; 32]);
const DESTINATION: Address = Address::new_from_array([24; 32]);
const USER: Address = Address::new_from_array([5; 32]);
const MARKET: Address = Address::new_from_array([6; 32]);
const RENT: Address = Address::new_from_array([7; 32]);
const OTHER_MINT: Address = Address::new_from_array([8; 32]);
const STATE: Address = Address::new_from_array([25; 32]);
const NAMED: Address = Address::new_from_array([26; 32]);
fn list(extras: &[ExtraAccountMeta]) -> Vec<u8> {
let mut data =
vec![0; ExtraAccountMetaList::size_of(extras.len()).unwrap()];
ExtraAccountMetaList::init::<Execute>(&mut data, extras).unwrap();
data
}
fn fixed(pubkey: Address, writable: bool) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_pubkey(&pubkey, false, writable).unwrap()
}
fn signer(pubkey: Address) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_pubkey(&pubkey, true, true).unwrap()
}
fn pda(seeds: &[Seed], writable: bool) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_seeds(seeds, false, writable).unwrap()
}
fn external_pda(program: u8, seeds: &[Seed]) -> ExtraAccountMeta {
ExtraAccountMeta::new_external_pda_with_seeds(program, seeds, false, false)
.unwrap()
}
fn pubkey_data(account: u8, offset: u8, writable: bool) -> ExtraAccountMeta {
ExtraAccountMeta::new_with_pubkey_data(
&PubkeyData::AccountData {
account_index: account,
data_index: offset,
},
false,
writable,
)
.unwrap()
}
fn raw(
discriminator: u8,
address_config: [u8; 32],
signer: bool,
) -> ExtraAccountMeta {
ExtraAccountMeta {
discriminator,
address_config,
is_signer: signer.into(),
is_writable: false.into(),
}
}
fn literal(bytes: &[u8]) -> Seed {
Seed::Literal {
bytes: bytes.to_vec(),
}
}
const fn account_key(index: u8) -> Seed {
Seed::AccountKey { index }
}
const fn account_data(account_index: u8, data_index: u8, length: u8) -> Seed {
Seed::AccountData {
account_index,
data_index,
length,
}
}
const fn instruction_data(index: u8, length: u8) -> Seed {
Seed::InstructionData { index, length }
}
fn validation(program: &Address) -> Address {
find_validation_address(&MINT, program)
}
const fn mint_state(transfer_hook: Option<Address>) -> MintState {
MintState {
token_program: TOKEN_2022_PROGRAM_ID,
decimals: TOKEN_2022_DECIMALS,
transfer_fee: None,
transfer_hook,
}
}
type Accounts = [(Address, Address, Vec<u8>)];
fn lookup<'a>(accounts: &'a Accounts) -> impl Fn(&Address) -> Fetched<'a> {
move |address: &Address| {
accounts
.iter()
.find(|(key, ..)| key == address)
.map_or(Fetched::NotFetched, |(_, owner, data)| {
Fetched::Account(AccountView { owner, data })
})
}
}
fn loaded(data: Vec<u8>) -> Hook {
Hook::load(
&MINT,
Some(PROGRAM),
lookup(&[(validation(&PROGRAM), PROGRAM, data)]),
)
.unwrap()
}
fn unresolvable(data: Vec<u8>) -> String {
let Err(HookError::Unresolvable(reason)) =
resolve(&loaded(data), &one(USER_LEG), &[])
else {
panic!("the list resolves");
};
reason.to_string()
}
const USER_LEG: HookLeg = HookLeg {
source: SOURCE,
mint: MINT,
destination: DESTINATION,
authority: USER,
destination_owner: MARKET,
};
const VAULT_LEG: HookLeg = HookLeg {
source: DESTINATION,
mint: MINT,
destination: SOURCE,
authority: MARKET,
destination_owner: USER,
};
fn one(leg: HookLeg) -> HookLegs {
HookLegs {
legs: vec![leg],
written: Vec::new(),
}
}
#[derive(Debug, PartialEq, Eq)]
enum Resolution {
Accounts(Vec<AccountMeta>),
Fetch(Vec<Address>),
}
fn hook_accounts<'a>(
hooks: &[&Hook],
legs: &HookLegs,
accounts: impl Fn(&Address) -> Fetched<'a>,
) -> Result<Resolution, HookError> {
Ok(match resolve_legs(hooks, legs, &accounts)? {
Step::Done(resolved) => Resolution::Accounts(resolved.accounts),
Step::Fetch(addresses) => Resolution::Fetch(addresses),
})
}
fn resolve(
hook: &Hook,
legs: &HookLegs,
accounts: &Accounts,
) -> Result<Resolution, HookError> {
hook_accounts(&[hook], legs, |address| {
accounts
.iter()
.find(|(key, ..)| key == address)
.map_or(Fetched::Missing, |(_, owner, data)| {
Fetched::Account(AccountView { owner, data })
})
})
}
fn accounts_of(hook: &Hook, legs: &HookLegs) -> Vec<AccountMeta> {
match resolve(hook, legs, &[]).unwrap() {
Resolution::Accounts(accounts) => accounts,
Resolution::Fetch(addresses) => panic!("fetch {addresses:?}"),
}
}
fn hook_pda(seeds: &[&[u8]]) -> Address {
Address::find_program_address(seeds, &PROGRAM).0
}
fn tail() -> [AccountMeta; 2] {
[
AccountMeta::new_readonly(PROGRAM, false),
AccountMeta::new_readonly(validation(&PROGRAM), false),
]
}
fn with_tail(extras: Vec<AccountMeta>) -> Vec<AccountMeta> {
[extras, tail().to_vec()].concat()
}
fn programs_test_list() -> Vec<u8> {
list(&[
fixed(RENT, false),
pda(&[literal(b"ledger"), account_key(0)], true),
pda(&[literal(b"owner"), account_data(0, 32, 32)], false),
])
}
#[test]
fn resolves_the_programs_test_hook() {
let hook = loaded(programs_test_list());
assert_eq!(hook.validation, validation(&PROGRAM));
for (leg, source, source_owner) in
[(USER_LEG, SOURCE, USER), (VAULT_LEG, DESTINATION, MARKET)]
{
assert_eq!(
accounts_of(&hook, &one(leg)),
with_tail(vec![
AccountMeta::new_readonly(RENT, false),
AccountMeta::new(
hook_pda(&[b"ledger", source.as_ref()]),
false
),
AccountMeta::new_readonly(
hook_pda(&[b"owner", source_owner.as_ref()]),
false
),
])
);
}
}
#[test]
fn resolves_prior_extras_data_and_external_programs() {
let first = hook_pda(&[DESTINATION.as_ref()]);
let hook = loaded(list(&[
pda(&[account_key(2)], false),
external_pda(5, &[account_data(2, 32, 32), instruction_data(0, 8)]),
pubkey_data(2, 0, false),
pda(&[account_data(4, 0, 8)], false),
]));
assert_eq!(
accounts_of(&hook, &one(USER_LEG)),
with_tail(vec![
AccountMeta::new_readonly(first, false),
AccountMeta::new_readonly(
Address::find_program_address(
&[MARKET.as_ref(), &EXECUTE_DISCRIMINATOR],
&first
)
.0,
false
),
AccountMeta::new_readonly(MINT, false),
AccountMeta::new_readonly(
hook_pda(&[&EXECUTE_DISCRIMINATOR]),
false
),
])
);
}
#[test]
fn refuses_what_no_resolver_can_resolve() {
for (extra, reason) in [
(
pda(&[instruction_data(9, 8)], false),
"an extra reads past the end of the hook's instruction data",
),
(
pda(&[account_key(5)], false),
"an extra names an account not yet resolved",
),
(
pubkey_data(5, 0, false),
"an extra names an account not yet resolved",
),
(
pda(&[account_data(5, 0, 8)], false),
"an extra names an account not yet resolved",
),
(
external_pda(5, &[]),
"an extra names an account not yet resolved",
),
(
raw(META_PUBKEY_DATA, [1; 32], false),
"an extra reads past the end of the hook's instruction data",
),
(raw(3, [0; 32], false), "malformed extra"),
(raw(META_PUBKEY_DATA, [0; 32], false), "malformed extra"),
(raw(META_HOOK_PDA, [9; 32], false), "malformed seed"),
(
pda(&[account_data(4, 0, 33)], false),
"seed longer than 32 bytes",
),
] {
assert_eq!(unresolvable(list(&[extra])), reason);
}
assert_eq!(unresolvable(vec![9; 20]), "malformed list");
let mut other = [9; 8].to_vec();
other.extend_from_slice(&0_u32.to_le_bytes());
assert_eq!(unresolvable(other.clone()), "malformed list");
other.extend_from_slice(&[0; 12]);
assert_eq!(unresolvable(other), "no Execute entry in the list");
assert_eq!(unresolvable(Vec::new()), "malformed list");
let mut short = list(&[fixed(RENT, false)]);
short.truncate(short.len() - 1);
assert_eq!(unresolvable(short), "malformed list");
let mut overcounted = list(&[fixed(RENT, false)]);
overcounted[12] = 2;
assert_eq!(unresolvable(overcounted), "malformed list");
let mut partial = list(&[fixed(RENT, false)]);
partial[8] += 1;
partial.push(0);
assert_eq!(unresolvable(partial), "malformed list");
let mut trailing = list(&[fixed(RENT, false)]);
trailing.extend_from_slice(&[9; 8]);
trailing.extend_from_slice(&100_u32.to_le_bytes());
assert_eq!(unresolvable(trailing), "malformed list");
let refused = resolve(
&loaded(list(&[pda(&[account_key(9)], false)])),
&one(USER_LEG),
&[],
);
assert_eq!(
refused,
Err(HookError::Unresolvable(
Unresolvable::NamesUnresolvedAccount
))
);
assert_eq!(
refused.unwrap_err().to_string(),
"transfer hook cannot be resolved ahead of the instruction: an \
extra names an account not yet resolved"
);
}
#[test]
fn a_pda_past_the_seed_limit_is_refused_before_it_can_panic() {
let seeds = |count: usize| vec![account_key(0); count];
assert_eq!(
unresolvable(list(&[pda(&seeds(16), false)])),
"a PDA has more seeds than an address allows"
);
assert_eq!(
unresolvable(list(&[external_pda(0, &seeds(16))])),
"a PDA has more seeds than an address allows"
);
let hook = loaded(list(&[pda(&seeds(15), false)]));
let source = SOURCE.to_bytes();
assert_eq!(
accounts_of(&hook, &one(USER_LEG))[0],
AccountMeta::new_readonly(hook_pda(&[source.as_slice(); 15]), false)
);
}
#[test]
fn a_list_longer_than_an_index_can_name_is_refused() {
let extras = vec![fixed(RENT, false); 252];
assert_eq!(unresolvable(list(&extras)), "list too long");
assert_eq!(
accounts_of(&loaded(list(&extras[..251])), &one(USER_LEG)),
with_tail(vec![AccountMeta::new_readonly(RENT, false)])
);
}
#[test]
fn finds_the_execute_entry_among_others() {
let mut data = [1; 8].to_vec();
data.extend_from_slice(&3_u32.to_le_bytes());
data.extend_from_slice(&[0; 3]);
data.extend_from_slice(&list(&[fixed(RENT, true)]));
data.extend_from_slice(&[0; 40]);
let hook = loaded(data);
assert_eq!(
accounts_of(&hook, &one(USER_LEG))[0],
AccountMeta::new(RENT, false)
);
}
#[test]
fn extras_take_spl_resolver_roles() {
for (extras, expected) in [
(
vec![
fixed(SOURCE, true),
fixed(MINT, true),
fixed(DESTINATION, true),
fixed(USER, true),
fixed(validation(&PROGRAM), true),
],
vec![
AccountMeta::new_readonly(SOURCE, false),
AccountMeta::new_readonly(MINT, false),
AccountMeta::new_readonly(DESTINATION, false),
AccountMeta::new_readonly(USER, false),
AccountMeta::new_readonly(validation(&PROGRAM), false),
],
),
(
vec![pubkey_data(0, 0, true)],
vec![AccountMeta::new_readonly(MINT, false)],
),
(
vec![fixed(RENT, false), fixed(RENT, true)],
vec![
AccountMeta::new_readonly(RENT, false),
AccountMeta::new_readonly(RENT, false),
],
),
(
vec![fixed(RENT, true), fixed(RENT, false), fixed(RENT, true)],
vec![
AccountMeta::new(RENT, false),
AccountMeta::new_readonly(RENT, false),
AccountMeta::new(RENT, false),
],
),
(
vec![signer(MARKET), raw(META_FIXED, RENT.to_bytes(), true)],
vec![
AccountMeta::new(MARKET, false),
AccountMeta::new_readonly(RENT, false),
],
),
] {
let hook = loaded(list(&extras));
let execute =
match resolve_legs(&[&hook], &one(USER_LEG), &|_| Fetched::Missing)
.unwrap()
{
Step::Done(resolved) => resolved.executes,
Step::Fetch(addresses) => panic!("fetch {addresses:?}"),
};
assert_eq!(
execute[0].metas[EXECUTE_ACCOUNTS..].to_vec(),
expected,
"{extras:?}"
);
}
}
#[test]
fn the_trailing_accounts_merge_each_role() {
let hook = loaded(list(&[
fixed(RENT, true),
fixed(RENT, false),
fixed(SOURCE, true),
]));
assert_eq!(
accounts_of(&hook, &one(USER_LEG)),
with_tail(vec![
AccountMeta::new(RENT, false),
AccountMeta::new_readonly(SOURCE, false),
])
);
}
#[test]
fn legs_merge_by_key() {
let hook = loaded(list(&[fixed(RENT, false), fixed(DESTINATION, true)]));
let other = Hook::load(
&OTHER_MINT,
Some(PROGRAM),
lookup(&[(
find_validation_address(&OTHER_MINT, &PROGRAM),
PROGRAM,
list(&[fixed(RENT, true), fixed(MARKET, false)]),
)]),
)
.unwrap();
let legs = HookLegs {
legs: vec![
USER_LEG,
HookLeg {
mint: OTHER_MINT,
source: Address::new_from_array([30; 32]),
destination: Address::new_from_array([31; 32]),
..USER_LEG
},
],
written: Vec::new(),
};
assert_eq!(
hook_accounts(&[&hook, &other], &legs, |_| Fetched::Missing),
Ok(Resolution::Accounts(vec![
AccountMeta::new(RENT, false),
AccountMeta::new_readonly(DESTINATION, false),
AccountMeta::new_readonly(PROGRAM, false),
AccountMeta::new_readonly(validation(&PROGRAM), false),
AccountMeta::new_readonly(MARKET, false),
AccountMeta::new_readonly(
find_validation_address(&OTHER_MINT, &PROGRAM),
false
),
]))
);
assert_eq!(
hook_accounts(
&[&hook],
&one(HookLeg {
mint: OTHER_MINT,
..USER_LEG
}),
|_| Fetched::Missing
),
Ok(Resolution::Accounts(Vec::new()))
);
}
#[test]
fn an_amount_seed_refuses_the_list() {
let refused = HookError::Unresolvable(Unresolvable::ReadsTransferAmount);
for seed in [
instruction_data(8, 8),
instruction_data(0, 16),
instruction_data(7, 2),
] {
let hook = loaded(list(&[pda(&[seed], false)]));
for leg in [USER_LEG, VAULT_LEG] {
assert_eq!(resolve(&hook, &one(leg), &[]), Err(refused.clone()));
}
}
assert_eq!(
unresolvable(list(&[pda(&[instruction_data(8, 8)], false)])),
"a seed reads the transfer amount"
);
let discriminator = loaded(list(&[pda(&[instruction_data(0, 8)], false)]));
assert_eq!(
accounts_of(&discriminator, &one(VAULT_LEG))[0],
AccountMeta::new_readonly(hook_pda(&[&EXECUTE_DISCRIMINATOR]), false)
);
}
#[test]
fn a_list_reads_account_data_through_the_lookup() {
let mut state = vec![0; 8];
state.extend_from_slice(NAMED.as_ref());
let hook = loaded(list(&[
fixed(STATE, false),
pubkey_data(5, 8, false),
pda(&[account_data(1, 0, 4)], false),
]));
let mint_data = vec![3; 82];
let accounts = [
(STATE, PROGRAM, state.clone()),
(MINT, TOKEN_2022_PROGRAM_ID, mint_data),
];
assert_eq!(
resolve(&hook, &one(USER_LEG), &accounts),
Ok(Resolution::Accounts(with_tail(vec![
AccountMeta::new_readonly(STATE, false),
AccountMeta::new_readonly(NAMED, false),
AccountMeta::new_readonly(hook_pda(&[&[3; 4]]), false),
])))
);
assert_eq!(
hook_accounts(&[&hook], &one(USER_LEG), lookup(&[])),
Ok(Resolution::Fetch(vec![STATE]))
);
assert_eq!(
hook_accounts(
&[&hook],
&one(USER_LEG),
lookup(&[(STATE, PROGRAM, state)])
),
Ok(Resolution::Fetch(vec![MINT]))
);
assert_eq!(
resolve(&hook, &one(USER_LEG), &[]),
Err(HookError::Unresolvable(Unresolvable::ExtraUnresolved))
);
}
#[test]
fn a_read_the_instruction_changes_first_is_refused() {
let swap_time =
Err(HookError::Unresolvable(Unresolvable::ReadsSwapTimeData));
for extra in [
pda(&[account_data(0, 64, 8)], false),
pda(&[account_data(2, 60, 8)], false),
] {
assert_eq!(
resolve(&loaded(list(&[extra])), &one(USER_LEG), &[]),
swap_time
);
}
let named = loaded(list(&[
fixed(DESTINATION, false),
pubkey_data(5, 32, false),
]));
assert_eq!(
accounts_of(&named, &one(USER_LEG))[1],
AccountMeta::new_readonly(MARKET, false)
);
let written = HookLegs {
legs: vec![USER_LEG],
written: vec![MARKET],
};
let reads_market = loaded(list(&[
fixed(MARKET, false),
pda(&[account_data(5, 0, 8)], false),
]));
assert_eq!(
resolve(&reads_market, &written, &[(MARKET, PROGRAM, vec![0; 8])]),
swap_time
);
let reads_authority = loaded(list(&[pda(&[account_data(3, 0, 8)], false)]));
assert_eq!(
resolve(
&reads_authority,
&one(VAULT_LEG),
&[(MARKET, PROGRAM, vec![1; 8])]
),
Ok(Resolution::Accounts(with_tail(vec![
AccountMeta::new_readonly(hook_pda(&[&[1; 8]]), false)
])))
);
assert_eq!(
resolve(
&reads_authority,
&HookLegs {
legs: vec![VAULT_LEG],
written: vec![MARKET],
},
&[(MARKET, PROGRAM, vec![1; 8])]
),
swap_time
);
}
#[test]
fn a_missing_or_unfetched_list() {
let missing =
Hook::load(&MINT, Some(PROGRAM), |_| Fetched::Missing).unwrap();
assert_eq!(missing.list(), None);
assert_eq!(
resolve(&missing, &one(USER_LEG), &[]),
Err(HookError::NoList)
);
let unfetched =
Hook::load(&MINT, Some(PROGRAM), |_| Fetched::NotFetched).unwrap();
let not_loaded = resolve(&unfetched, &one(USER_LEG), &[]);
assert_eq!(
not_loaded,
Err(HookError::NotLoaded(vec![validation(&PROGRAM)]))
);
assert_eq!(
not_loaded.unwrap_err().to_string(),
"transfer hook accounts not loaded"
);
let squatted = Hook::load(
&MINT,
Some(PROGRAM),
lookup(&[(validation(&PROGRAM), Address::default(), Vec::new())]),
)
.unwrap();
assert_eq!(
resolve(&squatted, &one(USER_LEG), &[]),
Err(HookError::NoList)
);
assert_eq!(Hook::load(&MINT, None, |_| Fetched::NotFetched), None);
}
fn hooked_info(
mint: Address,
program: Address,
accounts: &Accounts,
) -> MintInfo {
MintInfo {
hook_program: Some(program),
hook: Some(snapshot(accounts)),
..plain_info(mint)
}
}
const fn plain_info(mint: Address) -> MintInfo {
MintInfo::plain(mint, TOKEN_2022_PROGRAM_ID, TOKEN_2022_DECIMALS)
}
#[test]
fn market_hooks_load_each_armed_mint() {
let other = Address::new_from_array([9; 32]);
let other_validation = find_validation_address(&OTHER_MINT, &other);
let data = list(&[fixed(RENT, false)]);
let hooks = MarketHooks::load(
&hooked_info(
MINT,
PROGRAM,
&[(validation(&PROGRAM), PROGRAM, data.clone())],
),
&hooked_info(OTHER_MINT, other, &[]),
);
assert_eq!(
hooks.hooks(),
vec![
&Hook {
mint: MINT,
program: PROGRAM,
validation: validation(&PROGRAM),
list: List::Loaded(data.clone()),
},
&Hook {
mint: OTHER_MINT,
program: other,
validation: other_validation,
list: List::NotFetched,
},
]
);
let elsewhere = MarketHooks::load(
&hooked_info(MINT, PROGRAM, &[(other_validation, other, data.clone())]),
&hooked_info(OTHER_MINT, other, &[]),
);
assert_eq!(elsewhere.hooks()[1].list, List::NotFetched);
let mut missing = hooked_info(OTHER_MINT, other, &[]);
missing.insert(other_validation, None);
let squatted =
hooked_info(OTHER_MINT, other, &[(other_validation, PROGRAM, data)]);
for quote in [missing, squatted] {
let hooks = MarketHooks::load(&plain_info(MINT), "e);
assert_eq!(hooks.hooks().len(), 1);
assert_eq!(hooks.hooks()[0].list, List::Missing);
}
let bare = MintInfo {
hook_program: Some(other),
..plain_info(OTHER_MINT)
};
assert_eq!(
MarketHooks::load(&plain_info(MINT), &bare).hooks()[0].list,
List::NotFetched
);
let unhooked =
MarketHooks::load(&plain_info(MINT), &plain_info(OTHER_MINT));
assert_eq!(unhooked.hooks(), Vec::<&Hook>::new());
}
#[test]
fn a_build_appends_after_every_account() {
let instruction = Instruction {
program_id: MARKET,
accounts: vec![AccountMeta::new(USER, true)],
data: vec![1, 2],
};
assert_eq!(
append(&plain_info(MINT), instruction.clone()),
Ok(instruction.clone())
);
assert_eq!(
append(&hooked_info(MINT, PROGRAM, &[]), instruction.clone()),
Err(BuildError::Hook(HookError::NotLoaded(vec![validation(
&PROGRAM
)])))
);
let hooked = hooked_info(
MINT,
PROGRAM,
&[(validation(&PROGRAM), PROGRAM, list(&[fixed(RENT, false)]))],
);
assert_eq!(
append(&hooked, instruction.clone()),
Ok(Instruction {
accounts: [
instruction.accounts.clone(),
with_tail(vec![AccountMeta::new_readonly(RENT, false)]),
]
.concat(),
..instruction
})
);
let spl = snapshot(&[(MINT, TOKEN_PROGRAM_ID, spl_mint())]);
assert_eq!(spl.fetched(&RENT), Fetched::NotFetched);
assert_eq!(
spl.fetched(&MINT),
Fetched::Account(AccountView {
owner: &TOKEN_PROGRAM_ID,
data: &spl_mint(),
})
);
}
fn append(
info: &MintInfo,
instruction: Instruction,
) -> Result<Instruction, BuildError> {
append_hooks(
instruction,
TradeMints {
base: &plain_info(OTHER_MINT),
quote: info,
},
&one(USER_LEG),
)
}
fn snapshot(accounts: &Accounts) -> HookState {
let mut state = HookState::default();
for (address, owner, data) in accounts {
state.insert(
*address,
Some(FetchedAccount {
owner: *owner,
data: data.clone(),
}),
);
}
state
}
const TOKEN_2022_DECIMALS: u8 = 6;
const SPL_DECIMALS: u8 = 9;
fn spl_mint() -> Vec<u8> {
let mut data = vec![0; 82];
data[44] = SPL_DECIMALS;
data[45] = 1;
data
}
fn token_2022_mint(extensions: &[(u16, Vec<u8>)]) -> Vec<u8> {
let mut data = vec![0; 165];
data[44] = TOKEN_2022_DECIMALS;
data[45] = 1;
data.push(1);
for (extension_type, payload) in extensions {
data.extend_from_slice(&extension_type.to_le_bytes());
data.extend_from_slice(
&u16::try_from(payload.len()).unwrap().to_le_bytes(),
);
data.extend_from_slice(payload);
}
data
}
fn load(owner: &Address, data: &[u8]) -> Result<MintState, ProgramError> {
MintState::load(AccountView { owner, data })
}
fn hook_extension(program_id: [u8; 32]) -> (u16, Vec<u8>) {
(
ExtensionType::TransferHook.into(),
[[7; 32], program_id].concat(),
)
}
fn fee_extension(bps: u16, maximum_fee: u64) -> (u16, Vec<u8>) {
let fee = [
0_u64.to_le_bytes().as_slice(),
&maximum_fee.to_le_bytes(),
&bps.to_le_bytes(),
]
.concat();
(
ExtensionType::TransferFeeConfig.into(),
[[7; 32].as_slice(), &[7; 32], &[0; 8], &fee, &fee].concat(),
)
}
fn metadata() -> [(u16, Vec<u8>); 2] {
[(18, vec![0; 64]), (19, vec![0; 90])]
}
#[test]
fn a_launchpad_base_mint_passes() {
assert_eq!(
load(&TOKEN_2022_PROGRAM_ID, &token_2022_mint(&metadata())),
Ok(mint_state(None))
);
}
#[test]
fn an_spl_mint_passes() {
assert_eq!(
load(&TOKEN_PROGRAM_ID, &spl_mint()),
Ok(MintState {
token_program: TOKEN_PROGRAM_ID,
decimals: SPL_DECIMALS,
..mint_state(None)
})
);
}
#[test]
fn the_decimals_come_from_the_base_mint() {
let [pointer, metadata] = metadata();
for decimals in [0, 6, 9, u8::MAX] {
let mut spl = spl_mint();
spl[44] = decimals;
assert_eq!(load(&TOKEN_PROGRAM_ID, &spl).unwrap().decimals, decimals);
let mut token_2022 =
token_2022_mint(&[pointer.clone(), metadata.clone()]);
token_2022[44] = decimals;
assert_eq!(
load(&TOKEN_2022_PROGRAM_ID, &token_2022).unwrap().decimals,
decimals
);
}
}
#[test]
fn an_spl_mint_is_the_initialized_base_mint_alone() {
let mut uninitialized = spl_mint();
uninitialized[45] = 0;
assert_eq!(
load(&TOKEN_PROGRAM_ID, &uninitialized),
Err(ProgramError::UninitializedAccount)
);
for length in [0, 44, 81, 83, 165, 355] {
let mut data = spl_mint();
data.resize(length, 0);
assert_eq!(
load(&TOKEN_PROGRAM_ID, &data),
Err(ProgramError::InvalidAccountData),
"{length} bytes"
);
}
}
#[test]
fn an_unset_hook_is_no_hook() {
let [pointer, metadata] = metadata();
let data = token_2022_mint(&[pointer, hook_extension([0; 32]), metadata]);
assert_eq!(
load(&TOKEN_2022_PROGRAM_ID, &data).unwrap().transfer_hook,
None
);
}
#[test]
fn a_set_hook_reads_its_program() {
let [pointer, metadata] = metadata();
let data = token_2022_mint(&[pointer, hook_extension([9; 32]), metadata]);
assert_eq!(
load(&TOKEN_2022_PROGRAM_ID, &data),
Ok(mint_state(Some(Address::new_from_array([9; 32]))))
);
}
#[test]
fn a_cut_extension_is_malformed() {
let mut data = token_2022_mint(&[hook_extension([9; 32])]);
data.truncate(data.len() - 1);
assert_eq!(
load(&TOKEN_2022_PROGRAM_ID, &data),
Err(ProgramError::InvalidAccountData)
);
for (extension_type, mut payload) in
[hook_extension([9; 32]), fee_extension(100, 1_000)]
{
payload.pop();
assert_eq!(
load(
&TOKEN_2022_PROGRAM_ID,
&token_2022_mint(&[(extension_type, payload)])
),
Err(ProgramError::InvalidArgument),
"type {extension_type}"
);
}
assert_eq!(
load(&MINT, &token_2022_mint(&[])),
Err(ProgramError::IncorrectProgramId)
);
}
#[test]
fn a_future_extension_type_loads_as_no_extension() {
let [pointer, _] = metadata();
let data = token_2022_mint(&[pointer, (250, vec![0; 16])]);
assert_eq!(load(&TOKEN_2022_PROGRAM_ID, &data), Ok(mint_state(None)));
}
mod builders {
use super::*;
use crate::launchpad::trade::{
BuildBuyExactInParams, BuildSellExactOutParams, WRITES_MARKET,
build_buy_exact_in_instruction, build_sell_exact_out_instruction,
};
use crate::utils::{
TradeAccounts, TradeDirection, TradeLegInput, legs_of_trade,
};
const PARTNER: Address = Address::new_from_array([3; 32]);
const PLATFORM: Address = Address::new_from_array([4; 32]);
const BASE: Address = Address::new_from_array([9; 32]);
fn info_of(address: Address, data: &[u8]) -> MintInfo {
let sysvar = solana_address::address!(
"Sysvar1111111111111111111111111111111111111"
);
MintInfo::load(
address,
AccountView {
owner: &TOKEN_2022_PROGRAM_ID,
data,
},
AccountView {
owner: &sysvar,
data: &[0; 40],
},
)
.unwrap()
}
fn base() -> MintInfo {
info_of(BASE, &token_2022_mint(&metadata()))
}
fn hook_free_quote() -> MintInfo {
info_of(MINT, &token_2022_mint(&metadata()))
}
fn buy<'a>(
base_mint_info: &'a MintInfo,
quote_mint_info: &'a MintInfo,
) -> BuildBuyExactInParams<'a> {
BuildBuyExactInParams {
user: &USER,
payer: None,
base_mint_info,
quote_mint_info,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_in: 7_000,
min_amount_out: 1,
}
}
fn build_buy(quote: &MintInfo) -> Result<Instruction, BuildError> {
build_buy_exact_in_instruction(&buy(&base(), quote))
}
fn quote_hooked_with(
extensions: &[(u16, Vec<u8>)],
data: Vec<u8>,
) -> MintInfo {
let mut mint = extensions.to_vec();
mint.push(hook_extension(PROGRAM.to_bytes()));
let mut info = info_of(MINT, &token_2022_mint(&mint));
info.insert(
validation(&PROGRAM),
Some(FetchedAccount {
owner: PROGRAM,
data,
}),
);
info
}
fn quote_hooked(data: Vec<u8>) -> MintInfo {
quote_hooked_with(&[], data)
}
fn bare() -> Instruction {
build_buy(&hook_free_quote()).unwrap()
}
fn bare_accounts(bare: &Instruction) -> TradeAccounts {
TradeAccounts {
user: USER,
payer: USER,
market: bare.accounts[2].pubkey,
base_mint: BASE,
quote_mint: MINT,
base_vault: bare.accounts[5].pubkey,
quote_vault: bare.accounts[6].pubkey,
user_base_account: bare.accounts[7].pubkey,
user_quote_account: bare.accounts[8].pubkey,
partner: PARTNER,
partner_config: bare.accounts[10].pubkey,
quote_token_program: TOKEN_2022_PROGRAM_ID,
}
}
fn bare_legs(bare: &Instruction) -> HookLegs {
legs_of_trade(
&TradeLegInput {
accounts: bare_accounts(bare),
direction: TradeDirection::Buy,
},
WRITES_MARKET,
)
}
#[test]
fn a_hook_free_build_appends_nothing() {
let bare = bare();
assert_eq!(bare.accounts.len(), 17);
assert_eq!(
bare,
crate::launchpad::trade::trade_instruction(
&bare_accounts(&bare),
crate::launchpad::trade::TradeArgs {
direction: TradeDirection::Buy,
mode: crate::utils::TradeMode::ExactIn,
amount: 7_000,
limit: 1,
},
)
);
let plain = |address| {
MintInfo::plain(address, TOKEN_2022_PROGRAM_ID, TOKEN_2022_DECIMALS)
};
assert_eq!(
build_buy_exact_in_instruction(&buy(&plain(BASE), &plain(MINT))),
Ok(bare)
);
}
#[test]
fn a_hooked_build_appends_after_the_plain_accounts() {
let hooked =
build_buy("e_hooked(list(&[fixed(RENT, false)]))).unwrap();
assert_eq!(hooked.accounts.len(), 20);
let plain = bare();
assert_eq!(plain.accounts.len(), 17);
assert_eq!(plain.accounts[..], hooked.accounts[..17]);
assert_eq!(plain.data, hooked.data);
}
#[test]
fn a_build_from_a_filled_info_resolves_the_hook() {
let mut quote = info_of(
MINT,
&token_2022_mint(&[hook_extension(PROGRAM.to_bytes())]),
);
assert_eq!(quote.missing(), Ok(vec![validation(&PROGRAM)]));
assert_eq!(
build_buy("e),
Err(BuildError::Hook(HookError::NotLoaded(vec![validation(
&PROGRAM
)])))
);
quote.insert(
validation(&PROGRAM),
Some(FetchedAccount {
owner: PROGRAM,
data: list(&[fixed(RENT, true)]),
}),
);
assert_eq!(quote.missing(), Ok(Vec::new()));
let built = build_buy("e).unwrap();
assert_eq!(built.accounts[..17], bare().accounts[..]);
assert_eq!(
built.accounts[17..],
with_tail(vec![AccountMeta::new(RENT, false)])
);
}
#[test]
fn a_base_mint_the_transaction_creates_is_hook_free() {
let quote = quote_hooked(list(&[fixed(RENT, false)]));
let launch =
MintInfo::plain(BASE, TOKEN_2022_PROGRAM_ID, TOKEN_2022_DECIMALS);
let built =
build_buy_exact_in_instruction(&buy(&launch, "e)).unwrap();
assert_eq!(
built.accounts[17..],
with_tail(vec![AccountMeta::new_readonly(RENT, false)])
);
}
#[test]
fn a_hooked_build_appends_the_resolved_accounts() {
let quote = quote_hooked(list(&[pda(&[account_key(0)], true)]));
let bare = bare();
let built = build_buy("e).unwrap();
let user_quote = bare.accounts[8].pubkey;
assert_eq!(built.accounts[..17], bare.accounts[..]);
assert_eq!(
built.accounts[17..],
with_tail(vec![AccountMeta::new(
hook_pda(&[user_quote.as_ref()]),
false
)])
);
let legs = bare_legs(&bare);
assert_eq!(
append_hooks(
bare,
TradeMints {
base: &base(),
quote: "e,
},
&legs
),
Ok(built)
);
}
#[test]
fn an_amount_seed_refuses_every_build() {
use crate::{dex, launchpad};
let refused =
HookError::Unresolvable(Unresolvable::ReadsTransferAmount);
let base_mint_info = base();
let quote =
quote_hooked(list(&[pda(&[instruction_data(8, 8)], false)]));
assert_eq!(build_buy("e), Err(BuildError::Hook(refused.clone())));
assert_eq!(
build_sell_exact_out_instruction(&BuildSellExactOutParams {
user: &USER,
payer: None,
base_mint_info: &base_mint_info,
quote_mint_info: "e,
partner: &PARTNER,
platform_config: &PLATFORM,
amount_out: 1_000_000,
max_amount_in: 9,
}),
Err(BuildError::Hook(refused.clone()))
);
assert_eq!(
dex::trade::sell_exact_out(&dex::trade::SellExactOutParams {
user: &USER,
payer: None,
base_mint_info: &base_mint_info,
quote_mint_info: "e,
base_reserves: 1_000_000_000_000,
quote_reserves: 30_000_000_000,
fee_bps: 100,
slippage_bps: 100,
partner: &PARTNER,
platform_config: &PLATFORM,
quote_amount: 1_000_000,
}),
Err(BuildError::Hook(refused.clone()))
);
assert_eq!(
launchpad::migrate::build_migrate_instruction(
&launchpad::migrate::BuildMigrateParams {
caller: &USER,
base_mint_info: &base(),
quote_mint_info: "e,
}
),
Err(BuildError::Hook(refused.clone()))
);
let bare = bare();
let legs = bare_legs(&bare);
assert_eq!(
append_hooks(
bare,
TradeMints {
base: &base_mint_info,
quote: "e,
},
&legs
),
Err(BuildError::Hook(refused))
);
}
#[test]
fn a_build_refuses_what_the_info_lacks_until_it_holds_it() {
let data = list(&[literal_extra(STATE), pubkey_data(5, 8, false)]);
let mut quote = quote_hooked(data.clone());
assert_eq!(
build_buy("e),
Err(BuildError::Hook(HookError::NotLoaded(vec![STATE])))
);
assert_eq!(quote.missing(), Ok(vec![STATE]));
quote.insert(
STATE,
Some(FetchedAccount {
owner: PROGRAM,
data: [[0; 8].as_slice(), NAMED.as_ref()].concat(),
}),
);
assert_eq!(quote.missing(), Ok(Vec::new()));
let built = build_buy("e).unwrap();
assert_eq!(
built.accounts[17..],
with_tail(vec![
AccountMeta::new_readonly(STATE, false),
AccountMeta::new_readonly(NAMED, false),
])
);
let mut unloaded = quote.clone();
unloaded
.hook
.as_mut()
.unwrap()
.accounts
.remove(&validation(&PROGRAM));
assert_eq!(
build_buy(&unloaded),
Err(BuildError::Hook(HookError::NotLoaded(vec![validation(
&PROGRAM
)])))
);
unloaded.insert(
validation(&PROGRAM),
Some(FetchedAccount {
owner: PROGRAM,
data,
}),
);
assert_eq!(build_buy(&unloaded), Ok(built));
}
#[test]
fn a_trade_specific_read_refuses() {
let quote = quote_hooked(list(&[
pda(&[account_key(0)], false),
pubkey_data(5, 0, false),
]));
assert_eq!(quote.missing(), Ok(Vec::new()));
let user_quote = bare().accounts[8].pubkey;
assert_eq!(
build_buy("e),
Err(BuildError::Hook(HookError::NotLoaded(vec![hook_pda(&[
user_quote.as_ref()
])])))
);
}
#[test]
fn a_hooked_mint_with_a_fee_resolves_as_one_without() {
let data = list(&[fixed(RENT, false)]);
assert_eq!(
build_buy("e_hooked_with(
&[fee_extension(100, 1_000)],
data.clone()
)),
build_buy("e_hooked(data))
);
}
fn literal_extra(pubkey: Address) -> ExtraAccountMeta {
fixed(pubkey, false)
}
}
mod fill {
use super::*;
fn hooked(data: Vec<u8>) -> HookState {
snapshot(&[
(
MINT,
TOKEN_2022_PROGRAM_ID,
token_2022_mint(&[hook_extension(PROGRAM.to_bytes())]),
),
(validation(&PROGRAM), PROGRAM, data),
])
}
fn stored(state: &mut HookState, address: Address, data: Vec<u8>) {
state.insert(
address,
Some(FetchedAccount {
owner: PROGRAM,
data,
}),
);
}
fn empty() -> Instruction {
Instruction {
program_id: MARKET,
accounts: Vec::new(),
data: Vec::new(),
}
}
fn build(state: &HookState) -> Result<Instruction, BuildError> {
let info = MintInfo {
hook_program: Some(PROGRAM),
hook: Some(state.clone()),
..plain_info(MINT)
};
append(&info, empty())
}
fn naming(key: Address) -> Vec<u8> {
[[0; 8].as_slice(), key.as_ref()].concat()
}
#[test]
fn the_mints_come_first_then_the_lists() {
let empty = HookState::default();
assert_eq!(
empty.missing(&[MINT, OTHER_MINT, MINT]),
Ok(vec![MINT, OTHER_MINT])
);
let mut mints = snapshot(&[
(
MINT,
TOKEN_2022_PROGRAM_ID,
token_2022_mint(&[hook_extension(PROGRAM.to_bytes())]),
),
(OTHER_MINT, TOKEN_PROGRAM_ID, spl_mint()),
]);
assert_eq!(
mints.missing(&[MINT, OTHER_MINT]),
Ok(vec![validation(&PROGRAM)])
);
mints.insert(validation(&PROGRAM), None);
assert_eq!(mints.missing(&[MINT, OTHER_MINT]), Ok(Vec::new()));
}
#[test]
fn a_missing_mint_runs_no_hook_and_a_malformed_one_refuses() {
let mut state = HookState::default();
state.insert(MINT, None);
assert_eq!(state.missing(&[MINT]), Ok(Vec::new()));
let malformed = snapshot(&[(MINT, PROGRAM, token_2022_mint(&[]))]);
assert_eq!(malformed.missing(&[MINT]), Err(HookError::MalformedMint));
assert_eq!(
HookError::MalformedMint.to_string(),
"a transfer's mint does not decode as a mint"
);
}
#[test]
fn a_list_no_resolver_resolves_names_nothing() {
let state = hooked(vec![1, 2, 3]);
assert_eq!(state.missing(&[MINT]), Ok(Vec::new()));
}
#[test]
fn a_static_config_account_is_fetched() {
let mut state =
hooked(list(&[fixed(STATE, false), pubkey_data(5, 8, false)]));
assert_eq!(state.missing(&[MINT]), Ok(vec![STATE]));
stored(&mut state, STATE, naming(NAMED));
assert_eq!(state.missing(&[MINT]), Ok(Vec::new()));
let unread = hooked(list(&[fixed(STATE, false)]));
assert_eq!(unread.missing(&[MINT]), Ok(Vec::new()));
}
#[test]
fn a_chain_of_fixed_reads_takes_a_round_each() {
let mut state = hooked(list(&[
fixed(STATE, false),
pubkey_data(5, 8, false),
pda(&[account_data(6, 0, 4)], false),
]));
assert_eq!(state.missing(&[MINT]), Ok(vec![STATE]));
stored(&mut state, STATE, naming(NAMED));
assert_eq!(state.missing(&[MINT]), Ok(vec![NAMED]));
stored(&mut state, NAMED, vec![4, 3, 2, 1]);
assert_eq!(state.missing(&[MINT]), Ok(Vec::new()));
assert_eq!(
build(&state),
Ok(Instruction {
accounts: with_tail(vec![
AccountMeta::new_readonly(STATE, false),
AccountMeta::new_readonly(NAMED, false),
AccountMeta::new_readonly(
hook_pda(&[&[4, 3, 2, 1]]),
false
),
]),
..empty()
})
);
}
#[test]
fn only_fixed_inputs_make_a_fixed_address() {
let fixed_pda = |seeds: &[Seed]| {
Address::find_program_address(
&seeds
.iter()
.map(|seed| match seed {
Seed::Literal { bytes } => bytes.clone(),
Seed::InstructionData { .. } => {
EXECUTE_DISCRIMINATOR.to_vec()
}
Seed::AccountKey { index: 1 } => {
MINT.to_bytes().to_vec()
}
_ => validation(&PROGRAM).to_bytes().to_vec(),
})
.collect::<Vec<_>>()
.iter()
.map(Vec::as_slice)
.collect::<Vec<_>>(),
&PROGRAM,
)
.0
};
for seeds in [
vec![literal(b"config")],
vec![instruction_data(0, 8)],
vec![account_key(1)],
vec![account_key(4)],
] {
let state =
hooked(list(&[pda(&seeds, false), pubkey_data(5, 0, false)]));
assert_eq!(
state.missing(&[MINT]),
Ok(vec![fixed_pda(&seeds)]),
"{seeds:?}"
);
}
for seeds in [
vec![instruction_data(8, 8)],
vec![instruction_data(4, 8)],
vec![account_key(0)],
vec![account_key(2)],
vec![account_key(3)],
vec![account_data(3, 0, 8)],
] {
let state =
hooked(list(&[pda(&seeds, false), pubkey_data(5, 0, false)]));
assert_eq!(state.missing(&[MINT]), Ok(Vec::new()), "{seeds:?}");
}
}
#[test]
fn a_trade_extra_reading_a_fixed_account_fetches_it() {
let state = hooked(list(&[
fixed(STATE, false),
pda(&[account_key(0), account_data(5, 0, 8)], false),
]));
assert_eq!(state.missing(&[MINT]), Ok(vec![STATE]));
}
#[test]
fn a_missing_fixed_account_reads_as_no_data() {
let mut state = hooked(list(&[
fixed(STATE, false),
pubkey_data(5, 8, false),
pda(&[account_data(6, 0, 4)], false),
]));
state.insert(STATE, None);
assert_eq!(state.missing(&[MINT]), Ok(Vec::new()));
assert_eq!(
build(&state),
Err(BuildError::Hook(HookError::Unresolvable(
Unresolvable::ExtraUnresolved
)))
);
}
}