fix(amm): disambiguate signed swap input holdings

Use token_definition_id_in as the swap direction selector while allowing callers to authorize only the selected input holding.

Update AMM IDL metadata and integration coverage for input signatures, token-id mismatches, swapped holding slots, and exact-output swaps.
This commit is contained in:
Ricardo Guilherme Schmidt 2026-06-30 12:06:02 -03:00
parent fe4c7a96da
commit bea6d6437c
No known key found for this signature in database
GPG Key ID: 1396EA17DE132FFE
4 changed files with 780 additions and 54 deletions

View File

@ -425,13 +425,13 @@
{ {
"name": "user_holding_a", "name": "user_holding_a",
"writable": true, "writable": true,
"signer": true, "signer": false,
"init": false "init": false
}, },
{ {
"name": "user_holding_b", "name": "user_holding_b",
"writable": true, "writable": true,
"signer": true, "signer": false,
"init": false "init": false
}, },
{ {
@ -496,13 +496,13 @@
{ {
"name": "user_holding_a", "name": "user_holding_a",
"writable": true, "writable": true,
"signer": true, "signer": false,
"init": false "init": false
}, },
{ {
"name": "user_holding_b", "name": "user_holding_b",
"writable": true, "writable": true,
"signer": true, "signer": false,
"init": false "init": false
}, },
{ {

View File

@ -172,15 +172,17 @@ pub enum Instruction {
deadline: u64, deadline: u64,
}, },
/// Swap some quantity of Tokens (either Token A or Token B) /// Swap some quantity of tokens while maintaining the Pool constant product.
/// while maintaining the Pool constant product. ///
/// Swap direction is selected by `token_definition_id_in`; the selected input holding must be
/// authorized so the downstream token transfer can debit it.
/// ///
/// Required accounts: /// Required accounts:
/// - AMM Pool (initialized) /// - AMM Pool (initialized)
/// - Vault Holding Account for Token A (initialized) /// - Vault Holding Account for Token A (initialized)
/// - Vault Holding Account for Token B (initialized) /// - Vault Holding Account for Token B (initialized)
/// - User Holding Account for Token A /// - User Holding Account for Token A (initialized)
/// - User Holding Account for Token B; either is authorized. /// - User Holding Account for Token B (initialized); the input holding is authorized.
/// - Current Tick Account, the pool's TWAP PDA derived as /// - Current Tick Account, the pool's TWAP PDA derived as
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed /// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
/// with the new spot price /// with the new spot price
@ -193,15 +195,18 @@ pub enum Instruction {
deadline: u64, deadline: u64,
}, },
/// Swap tokens specifying the exact desired output amount, /// Swap tokens specifying the exact desired output amount while maintaining the Pool constant
/// while maintaining the Pool constant product. /// product.
///
/// Swap direction is selected by `token_definition_id_in`; the selected input holding must be
/// authorized so the downstream token transfer can debit it.
/// ///
/// Required accounts: /// Required accounts:
/// - AMM Pool (initialized) /// - AMM Pool (initialized)
/// - Vault Holding Account for Token A (initialized) /// - Vault Holding Account for Token A (initialized)
/// - Vault Holding Account for Token B (initialized) /// - Vault Holding Account for Token B (initialized)
/// - User Holding Account for Token A /// - User Holding Account for Token A (initialized)
/// - User Holding Account for Token B; either is authorized. /// - User Holding Account for Token B (initialized); the input holding is authorized.
/// - Current Tick Account, the pool's TWAP PDA derived as /// - Current Tick Account, the pool's TWAP PDA derived as
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed /// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
/// with the new spot price /// with the new spot price

View File

@ -298,6 +298,9 @@ mod amm {
} }
/// Swap some quantity of tokens while maintaining the pool constant product. /// Swap some quantity of tokens while maintaining the pool constant product.
///
/// `token_definition_id_in` selects the swap input token; the selected input holding must be
/// authorized for the downstream token transfer.
#[expect( #[expect(
clippy::too_many_arguments, clippy::too_many_arguments,
reason = "instruction interface requires explicit pool, vault, user accounts, and bounds" reason = "instruction interface requires explicit pool, vault, user accounts, and bounds"
@ -312,9 +315,9 @@ mod amm {
vault_a: AccountWithMetadata, vault_a: AccountWithMetadata,
#[account(mut)] #[account(mut)]
vault_b: AccountWithMetadata, vault_b: AccountWithMetadata,
#[account(mut, signer)] #[account(mut)]
user_holding_a: AccountWithMetadata, user_holding_a: AccountWithMetadata,
#[account(mut, signer)] #[account(mut)]
user_holding_b: AccountWithMetadata, user_holding_b: AccountWithMetadata,
#[account(mut)] #[account(mut)]
current_tick_account: AccountWithMetadata, current_tick_account: AccountWithMetadata,
@ -343,6 +346,9 @@ mod amm {
} }
/// Swap tokens specifying the exact desired output amount. /// Swap tokens specifying the exact desired output amount.
///
/// `token_definition_id_in` selects the swap input token; the selected input holding must be
/// authorized for the downstream token transfer.
#[expect( #[expect(
clippy::too_many_arguments, clippy::too_many_arguments,
reason = "instruction interface requires explicit pool, vault, user accounts, and bounds" reason = "instruction interface requires explicit pool, vault, user accounts, and bounds"
@ -357,9 +363,9 @@ mod amm {
vault_a: AccountWithMetadata, vault_a: AccountWithMetadata,
#[account(mut)] #[account(mut)]
vault_b: AccountWithMetadata, vault_b: AccountWithMetadata,
#[account(mut, signer)] #[account(mut)]
user_holding_a: AccountWithMetadata, user_holding_a: AccountWithMetadata,
#[account(mut, signer)] #[account(mut)]
user_holding_b: AccountWithMetadata, user_holding_b: AccountWithMetadata,
#[account(mut)] #[account(mut)]
current_tick_account: AccountWithMetadata, current_tick_account: AccountWithMetadata,

View File

@ -257,6 +257,46 @@ impl Balances {
10_415 10_415
} }
fn exact_output_a_to_b_amount_in() -> u128 {
437
}
fn reserve_a_swap_exact_output_a_to_b() -> u128 {
Self::vault_a_init() + Self::exact_output_a_to_b_amount_in()
}
fn reserve_b_swap_exact_output_a_to_b() -> u128 {
Self::vault_b_init() - Self::swap_min_out()
}
fn user_a_swap_exact_output_a_to_b() -> u128 {
Self::user_a_init() - Self::exact_output_a_to_b_amount_in()
}
fn user_b_swap_exact_output_a_to_b() -> u128 {
Self::user_b_init() + Self::swap_min_out()
}
fn exact_output_b_to_a_amount_in() -> u128 {
106
}
fn reserve_a_swap_exact_output_b_to_a() -> u128 {
Self::vault_a_init() - Self::swap_min_out()
}
fn reserve_b_swap_exact_output_b_to_a() -> u128 {
Self::vault_b_init() + Self::exact_output_b_to_a_amount_in()
}
fn user_a_swap_exact_output_b_to_a() -> u128 {
Self::user_a_init() + Self::swap_min_out()
}
fn user_b_swap_exact_output_b_to_a() -> u128 {
Self::user_b_init() - Self::exact_output_b_to_a_amount_in()
}
fn vault_a_add() -> u128 { fn vault_a_add() -> u128 {
7_000 7_000
} }
@ -536,8 +576,7 @@ impl Accounts {
definition_id: Ids::token_a_definition(), definition_id: Ids::token_a_definition(),
balance: Balances::user_a_swap_1(), balance: Balances::user_a_swap_1(),
}), }),
// Both user holdings are now swap signers, so this holding's nonce increments too. nonce: Nonce(0),
nonce: Nonce(1),
} }
} }
@ -616,7 +655,140 @@ impl Accounts {
definition_id: Ids::token_b_definition(), definition_id: Ids::token_b_definition(),
balance: Balances::user_b_swap_2(), balance: Balances::user_b_swap_2(),
}), }),
// Both user holdings are now swap signers, so this holding's nonce increments too. nonce: Nonce(0),
}
}
fn pool_definition_swap_exact_output_a_to_b() -> Account {
Account {
program_owner: Ids::amm_program(),
balance: 0_u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: Ids::token_a_definition(),
definition_token_b_id: Ids::token_b_definition(),
vault_a_id: Ids::vault_a(),
vault_b_id: Ids::vault_b(),
liquidity_pool_id: Ids::token_lp_definition(),
liquidity_pool_supply: Balances::pool_lp_supply_init(),
reserve_a: Balances::reserve_a_swap_exact_output_a_to_b(),
reserve_b: Balances::reserve_b_swap_exact_output_a_to_b(),
fees: Balances::fee_tier(),
}),
nonce: Nonce(0),
}
}
fn vault_a_swap_exact_output_a_to_b() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_a_definition(),
balance: Balances::reserve_a_swap_exact_output_a_to_b(),
}),
nonce: Nonce(0),
}
}
fn vault_b_swap_exact_output_a_to_b() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_b_definition(),
balance: Balances::reserve_b_swap_exact_output_a_to_b(),
}),
nonce: Nonce(0),
}
}
fn user_a_holding_swap_exact_output_a_to_b() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_a_definition(),
balance: Balances::user_a_swap_exact_output_a_to_b(),
}),
nonce: Nonce(1),
}
}
fn user_b_holding_swap_exact_output_a_to_b() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_b_definition(),
balance: Balances::user_b_swap_exact_output_a_to_b(),
}),
nonce: Nonce(0),
}
}
fn pool_definition_swap_exact_output_b_to_a() -> Account {
Account {
program_owner: Ids::amm_program(),
balance: 0_u128,
data: Data::from(&PoolDefinition {
definition_token_a_id: Ids::token_a_definition(),
definition_token_b_id: Ids::token_b_definition(),
vault_a_id: Ids::vault_a(),
vault_b_id: Ids::vault_b(),
liquidity_pool_id: Ids::token_lp_definition(),
liquidity_pool_supply: Balances::pool_lp_supply_init(),
reserve_a: Balances::reserve_a_swap_exact_output_b_to_a(),
reserve_b: Balances::reserve_b_swap_exact_output_b_to_a(),
fees: Balances::fee_tier(),
}),
nonce: Nonce(0),
}
}
fn vault_a_swap_exact_output_b_to_a() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_a_definition(),
balance: Balances::reserve_a_swap_exact_output_b_to_a(),
}),
nonce: Nonce(0),
}
}
fn vault_b_swap_exact_output_b_to_a() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_b_definition(),
balance: Balances::reserve_b_swap_exact_output_b_to_a(),
}),
nonce: Nonce(0),
}
}
fn user_a_holding_swap_exact_output_b_to_a() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_a_definition(),
balance: Balances::user_a_swap_exact_output_b_to_a(),
}),
nonce: Nonce(0),
}
}
fn user_b_holding_swap_exact_output_b_to_a() -> Account {
Account {
program_owner: Ids::token_program(),
balance: 0_u128,
data: Data::from(&TokenHolding::Fungible {
definition_id: Ids::token_b_definition(),
balance: Balances::user_b_swap_exact_output_b_to_a(),
}),
nonce: Nonce(1), nonce: Nonce(1),
} }
} }
@ -990,13 +1162,9 @@ fn state_for_amm_tests() -> V03State {
state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init()); state.force_insert_account(Ids::pool_definition(), Accounts::pool_definition_init());
// Seed the pool's current-tick account so swaps and syncs can refresh it. Its initial value is // Seed the pool's current-tick account so swaps and syncs can refresh it. Its initial value is
// the tick of the opening reserves; swap/sync tests assert it is updated to the new price. // the tick of the opening reserves; swap/sync tests assert it is updated to the new price.
let initial_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
Balances::vault_a_init(),
Balances::vault_b_init(),
));
state.force_insert_account( state.force_insert_account(
Ids::current_tick_account(), Ids::current_tick_account(),
Accounts::current_tick_account(initial_tick), Accounts::current_tick_account(initial_pool_tick()),
); );
state.force_insert_account( state.force_insert_account(
Ids::token_a_definition(), Ids::token_a_definition(),
@ -1134,16 +1302,12 @@ fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_ou
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![ vec![current_nonce(state, Ids::user_a())],
current_nonce(state, Ids::user_a()),
current_nonce(state, Ids::user_b()),
],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap(); state.transition_from_public_transaction(&tx, 0, 0).unwrap();
@ -1170,16 +1334,12 @@ fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_ou
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![ vec![current_nonce(state, Ids::user_b())],
current_nonce(state, Ids::user_a()),
current_nonce(state, Ids::user_b()),
],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap(); state.transition_from_public_transaction(&tx, 0, 0).unwrap();
@ -1391,6 +1551,54 @@ fn pool_definition(account: &Account) -> PoolDefinition {
PoolDefinition::try_from(&account.data).expect("expected pool definition") PoolDefinition::try_from(&account.data).expect("expected pool definition")
} }
fn initial_pool_tick() -> i32 {
twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
Balances::vault_a_init(),
Balances::vault_b_init(),
))
}
fn assert_initial_swap_state(state: &V03State) {
assert_eq!(state.get_account_by_id(Ids::config()), Accounts::config());
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_init()
);
assert_eq!(
state.get_account_by_id(Ids::vault_a()),
Accounts::vault_a_init()
);
assert_eq!(
state.get_account_by_id(Ids::vault_b()),
Accounts::vault_b_init()
);
assert_eq!(
state.get_account_by_id(Ids::user_a()),
Accounts::user_a_holding()
);
assert_eq!(
state.get_account_by_id(Ids::user_b()),
Accounts::user_b_holding()
);
assert_eq!(
state.get_account_by_id(Ids::current_tick_account()),
Accounts::current_tick_account(initial_pool_tick())
);
}
fn assert_current_tick_matches_pool(state: &V03State) {
let pool = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
let tick_account = twap_oracle_core::CurrentTickAccount::try_from(
&state.get_account_by_id(Ids::current_tick_account()).data,
)
.expect("current tick account must hold a valid CurrentTickAccount");
let expected_tick = twap_oracle_core::price_to_tick(amm_core::spot_price_q64_64(
pool.reserve_a,
pool.reserve_b,
));
assert_eq!(tick_account.tick, expected_tick);
}
fn fungible_total_supply(account: &Account) -> u128 { fn fungible_total_supply(account: &Account) -> u128 {
let definition = TokenDefinition::try_from(&account.data).expect("expected token definition"); let definition = TokenDefinition::try_from(&account.data).expect("expected token definition");
let TokenDefinition::Fungible { let TokenDefinition::Fungible {
@ -2533,13 +2741,12 @@ fn amm_swap_b_to_a() {
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![Nonce(0), Nonce(0)], vec![Nonce(0)],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap(); state.transition_from_public_transaction(&tx, 0, 0).unwrap();
@ -2589,13 +2796,12 @@ fn amm_swap_a_to_b() {
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![Nonce(0), Nonce(0)], vec![Nonce(0)],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap(); state.transition_from_public_transaction(&tx, 0, 0).unwrap();
@ -2662,16 +2868,36 @@ fn amm_swap_exact_output_refreshes_current_tick() {
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![Nonce(0), Nonce(0)], vec![Nonce(0)],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap(); state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_swap_exact_output_a_to_b()
);
assert_eq!(
state.get_account_by_id(Ids::vault_a()),
Accounts::vault_a_swap_exact_output_a_to_b()
);
assert_eq!(
state.get_account_by_id(Ids::vault_b()),
Accounts::vault_b_swap_exact_output_a_to_b()
);
assert_eq!(
state.get_account_by_id(Ids::user_a()),
Accounts::user_a_holding_swap_exact_output_a_to_b()
);
assert_eq!(
state.get_account_by_id(Ids::user_b()),
Accounts::user_b_holding_swap_exact_output_a_to_b()
);
// The swap refreshed the pool's TWAP current tick to the post-swap spot price, computed from // The swap refreshed the pool's TWAP current tick to the post-swap spot price, computed from
// the reserves the swap actually settled on. // the reserves the swap actually settled on.
let pool = pool_definition(&state.get_account_by_id(Ids::pool_definition())); let pool = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
@ -2690,6 +2916,500 @@ fn amm_swap_exact_output_refreshes_current_tick() {
); );
} }
#[test]
fn amm_swap_exact_output_b_to_a_signs_only_input() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out: Balances::swap_min_out(),
max_amount_in: Balances::swap_amount_in(),
token_definition_id_in: Ids::token_b_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_swap_exact_output_b_to_a()
);
assert_eq!(
state.get_account_by_id(Ids::vault_a()),
Accounts::vault_a_swap_exact_output_b_to_a()
);
assert_eq!(
state.get_account_by_id(Ids::vault_b()),
Accounts::vault_b_swap_exact_output_b_to_a()
);
assert_eq!(
state.get_account_by_id(Ids::user_a()),
Accounts::user_a_holding_swap_exact_output_b_to_a()
);
assert_eq!(
state.get_account_by_id(Ids::user_b()),
Accounts::user_b_holding_swap_exact_output_b_to_a()
);
let pool = pool_definition(&state.get_account_by_id(Ids::pool_definition()));
let required_input = pool
.reserve_b
.checked_sub(Balances::vault_b_init())
.expect("swap should increase input-token reserve");
let user_a_balance = fungible_balance(&state.get_account_by_id(Ids::user_a()));
let user_b_balance = fungible_balance(&state.get_account_by_id(Ids::user_b()));
assert_eq!(
pool.reserve_a,
Balances::vault_a_init() - Balances::swap_min_out()
);
assert_ne!(required_input, 0);
assert!(required_input <= Balances::swap_amount_in());
assert_eq!(
Balances::user_b_init() - user_b_balance,
required_input,
"user debit must equal pool input reserve increase"
);
assert_eq!(
user_a_balance,
Balances::user_a_init() + Balances::swap_min_out()
);
assert_current_tick_matches_pool(&state);
}
#[test]
fn amm_swap_exact_output_dual_signed_uses_token_definition_id_in() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out: Balances::swap_min_out(),
max_amount_in: Balances::swap_amount_in(),
token_definition_id_in: Ids::token_b_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_swap_exact_output_b_to_a()
);
assert_eq!(
state.get_account_by_id(Ids::vault_a()),
Accounts::vault_a_swap_exact_output_b_to_a()
);
assert_eq!(
state.get_account_by_id(Ids::vault_b()),
Accounts::vault_b_swap_exact_output_b_to_a()
);
let mut expected_user_a = Accounts::user_a_holding_swap_exact_output_b_to_a();
expected_user_a.nonce = Nonce(1);
assert_eq!(state.get_account_by_id(Ids::user_a()), expected_user_a);
assert_eq!(
state.get_account_by_id(Ids::user_b()),
Accounts::user_b_holding_swap_exact_output_b_to_a()
);
assert_current_tick_matches_pool(&state);
}
#[test]
fn amm_swap_exact_input_requires_input_signature() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: Balances::swap_amount_in(),
min_amount_out: Balances::swap_min_out(),
token_definition_id_in: Ids::token_a_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_input_rejects_token_definition_id_in_mismatch() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: Balances::swap_amount_in(),
min_amount_out: Balances::swap_min_out(),
token_definition_id_in: Ids::token_a_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_input_rejects_swapped_user_holding_slots() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: Balances::swap_amount_in(),
min_amount_out: Balances::swap_min_out(),
token_definition_id_in: Ids::token_b_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_b(),
Ids::user_a(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_input_dual_signed_uses_token_definition_id_in() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: Balances::swap_amount_in(),
min_amount_out: Balances::swap_min_out(),
token_definition_id_in: Ids::token_b_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
state.transition_from_public_transaction(&tx, 0, 0).unwrap();
assert_eq!(
state.get_account_by_id(Ids::pool_definition()),
Accounts::pool_definition_swap_1()
);
assert_eq!(
state.get_account_by_id(Ids::vault_a()),
Accounts::vault_a_swap_1()
);
assert_eq!(
state.get_account_by_id(Ids::vault_b()),
Accounts::vault_b_swap_1()
);
let mut expected_user_a = Accounts::user_a_holding_swap_1();
expected_user_a.nonce = Nonce(1);
assert_eq!(state.get_account_by_id(Ids::user_a()), expected_user_a);
assert_eq!(
state.get_account_by_id(Ids::user_b()),
Accounts::user_b_holding_swap_1()
);
}
#[test]
fn amm_swap_exact_input_rejects_dual_signed_unknown_token_definition_id_in() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in: Balances::swap_amount_in(),
min_amount_out: Balances::swap_min_out(),
token_definition_id_in: Ids::token_lp_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_output_requires_input_signature() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out: Balances::swap_min_out(),
max_amount_in: Balances::swap_amount_in(),
token_definition_id_in: Ids::token_a_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_output_rejects_token_definition_id_in_mismatch() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out: Balances::swap_min_out(),
max_amount_in: Balances::swap_amount_in(),
token_definition_id_in: Ids::token_a_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_output_rejects_swapped_user_holding_slots() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out: Balances::swap_min_out(),
max_amount_in: Balances::swap_amount_in(),
token_definition_id_in: Ids::token_b_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_b(),
Ids::user_a(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0)],
instruction,
)
.unwrap();
let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test]
fn amm_swap_exact_output_rejects_dual_signed_unknown_token_definition_id_in() {
let mut state = state_for_amm_tests();
let instruction = amm_core::Instruction::SwapExactOutput {
exact_amount_out: Balances::swap_min_out(),
max_amount_in: Balances::swap_amount_in(),
token_definition_id_in: Ids::token_lp_definition(),
deadline: u64::MAX,
};
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
Ids::config(),
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
vec![Nonce(0), Nonce(0)],
instruction,
)
.unwrap();
let witness_set =
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set);
assert!(matches!(
state.transition_from_public_transaction(&tx, 0, 0),
Err(LeeError::ProgramExecutionFailed(_))
));
assert_initial_swap_state(&state);
}
#[test] #[test]
fn amm_sync_reserves_updates_pool_and_current_tick() { fn amm_sync_reserves_updates_pool_and_current_tick() {
let mut state = state_for_amm_tests(); let mut state = state_for_amm_tests();
@ -2799,13 +3519,12 @@ fn amm_swap_rejects_expired_deadline() {
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![Nonce(0), Nonce(0)], vec![Nonce(0)],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
assert!(matches!( assert!(matches!(
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms), state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),
@ -2839,16 +3558,12 @@ fn amm_swap_exact_output_rejects_expired_deadline() {
Ids::current_tick_account(), Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID, CLOCK_01_PROGRAM_ACCOUNT_ID,
], ],
vec![ vec![current_nonce(&state, Ids::user_a())],
current_nonce(&state, Ids::user_a()),
current_nonce(&state, Ids::user_b()),
],
instruction, instruction,
) )
.unwrap(); .unwrap();
let witness_set = let witness_set = public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a()]);
public_transaction::WitnessSet::for_message(&message, &[&Keys::user_a(), &Keys::user_b()]);
let tx = PublicTransaction::new(message, witness_set); let tx = PublicTransaction::new(message, witness_set);
assert!(matches!( assert!(matches!(
state.transition_from_public_transaction(&tx, 0, block_timestamp_ms), state.transition_from_public_transaction(&tx, 0, block_timestamp_ms),