refactor(amm)!: select swap direction by input holding, sign only the input

Replace the `token_definition_id_in` argument with a role-based account
interface: swaps now take a `user_input_holding` and a `user_output_holding`
instead of positional token-A/token-B holdings. Direction is derived from the
input holding's own token definition, and the input slot is a framework-level
`#[account(signer)]` so authorization is enforced before execution rather than
delegated solely to the downstream token transfer. The output holding only
receives and needs no signature.

This removes the ambiguity the previous arg-based model carried (the arg could
disagree with the signed holding) and makes the IDL express the signing rule:
`user_input_holding` is `signer: true`, `user_output_holding` is `signer: false`.

The two user-holding post-states are echoed in the guest's declared slot order
(input, then output); the framework matches post-states to accounts by
position, so the internal A/B mapping used for reserve bookkeeping must not leak
into the returned order.

BREAKING CHANGE: The AMM swap instruction interface changed and the guest
ImageID/ProgramId changes as a result.
This commit is contained in:
r4bbit 2026-07-20 16:18:07 +02:00
parent 7d1ed57de1
commit adff15b6ef
No known key found for this signature in database
GPG Key ID: E95F1E9447DC91A9
6 changed files with 108 additions and 376 deletions

View File

@ -423,13 +423,13 @@
"init": false
},
{
"name": "user_holding_a",
"name": "user_input_holding",
"writable": true,
"signer": false,
"signer": true,
"init": false
},
{
"name": "user_holding_b",
"name": "user_output_holding",
"writable": true,
"signer": false,
"init": false
@ -456,10 +456,6 @@
"name": "min_amount_out",
"type": "u128"
},
{
"name": "token_definition_id_in",
"type": "account_id"
},
{
"name": "deadline",
"type": "u64"
@ -494,13 +490,13 @@
"init": false
},
{
"name": "user_holding_a",
"name": "user_input_holding",
"writable": true,
"signer": false,
"signer": true,
"init": false
},
{
"name": "user_holding_b",
"name": "user_output_holding",
"writable": true,
"signer": false,
"init": false
@ -527,10 +523,6 @@
"name": "max_amount_in",
"type": "u128"
},
{
"name": "token_definition_id_in",
"type": "account_id"
},
{
"name": "deadline",
"type": "u64"

View File

@ -174,15 +174,16 @@ pub enum Instruction {
/// Swap some quantity of tokens 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.
/// Swap direction is determined by the input holding: `user_input_holding`'s token definition
/// selects which pool token is sold. That holding must be signed so the downstream token
/// transfer can debit it; `user_output_holding` only receives and needs no signature.
///
/// Required accounts:
/// - AMM Pool (initialized)
/// - Vault Holding Account for Token A (initialized)
/// - Vault Holding Account for Token B (initialized)
/// - User Holding Account for Token A (initialized)
/// - User Holding Account for Token B (initialized); the input holding is authorized.
/// - User Input Holding Account (initialized, signed) — the token being sold
/// - User Output Holding Account (initialized) — receives the token being bought
/// - Current Tick Account, the pool's TWAP PDA derived as
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
/// with the new spot price
@ -190,7 +191,6 @@ pub enum Instruction {
SwapExactInput {
swap_amount_in: u128,
min_amount_out: u128,
token_definition_id_in: AccountId,
/// Unix timestamp (milliseconds) after which this transaction is invalid.
deadline: u64,
},
@ -198,15 +198,16 @@ pub enum Instruction {
/// Swap tokens specifying the exact desired output amount 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.
/// Swap direction is determined by the input holding: `user_input_holding`'s token definition
/// selects which pool token is sold. That holding must be signed so the downstream token
/// transfer can debit it; `user_output_holding` only receives and needs no signature.
///
/// Required accounts:
/// - AMM Pool (initialized)
/// - Vault Holding Account for Token A (initialized)
/// - Vault Holding Account for Token B (initialized)
/// - User Holding Account for Token A (initialized)
/// - User Holding Account for Token B (initialized); the input holding is authorized.
/// - User Input Holding Account (initialized, signed) — the token being sold
/// - User Output Holding Account (initialized) — receives the token being bought
/// - Current Tick Account, the pool's TWAP PDA derived as
/// `compute_current_tick_account_pda(twap_oracle_program_id, pool.account_id)`; refreshed
/// with the new spot price
@ -214,7 +215,6 @@ pub enum Instruction {
SwapExactOutput {
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
/// Unix timestamp (milliseconds) after which this transaction is invalid.
deadline: u64,
},

View File

@ -299,8 +299,8 @@ mod amm {
/// 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.
/// The swap direction is the input holding's own token; `user_input_holding` must be signed so
/// the downstream token transfer can debit it. `user_output_holding` only receives.
#[expect(
clippy::too_many_arguments,
reason = "instruction interface requires explicit pool, vault, user accounts, and bounds"
@ -315,16 +315,15 @@ mod amm {
vault_a: AccountWithMetadata,
#[account(mut)]
vault_b: AccountWithMetadata,
#[account(mut, signer)]
user_input_holding: AccountWithMetadata,
#[account(mut)]
user_holding_a: AccountWithMetadata,
#[account(mut)]
user_holding_b: AccountWithMetadata,
user_output_holding: AccountWithMetadata,
#[account(mut)]
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
swap_amount_in: u128,
min_amount_out: u128,
token_definition_id_in: AccountId,
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::swap::swap_exact_input(
@ -332,13 +331,12 @@ mod amm {
pool,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
user_input_holding,
user_output_holding,
current_tick_account,
clock,
swap_amount_in,
min_amount_out,
token_definition_id_in,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)
@ -347,8 +345,8 @@ mod amm {
/// 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.
/// The swap direction is the input holding's own token; `user_input_holding` must be signed so
/// the downstream token transfer can debit it. `user_output_holding` only receives.
#[expect(
clippy::too_many_arguments,
reason = "instruction interface requires explicit pool, vault, user accounts, and bounds"
@ -363,16 +361,15 @@ mod amm {
vault_a: AccountWithMetadata,
#[account(mut)]
vault_b: AccountWithMetadata,
#[account(mut, signer)]
user_input_holding: AccountWithMetadata,
#[account(mut)]
user_holding_a: AccountWithMetadata,
#[account(mut)]
user_holding_b: AccountWithMetadata,
user_output_holding: AccountWithMetadata,
#[account(mut)]
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
exact_amount_out: u128,
max_amount_in: u128,
token_definition_id_in: AccountId,
deadline: u64,
) -> SpelResult {
let (post_states, chained_calls) = amm_program::swap::swap_exact_output(
@ -380,13 +377,12 @@ mod amm {
pool,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
user_input_holding,
user_output_holding,
current_tick_account,
clock,
exact_amount_out,
max_amount_in,
token_definition_id_in,
ctx.self_program_id,
);
Ok(spel_framework::SpelOutput::execute(post_states, chained_calls)

View File

@ -65,8 +65,10 @@ fn finalize_swap(
pool_def_data: PoolDefinition,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
// Echoed back at the input/output slot positions the guest declared, so the framework matches
// each post-state to the correct account regardless of swap direction.
user_holding_input: AccountWithMetadata,
user_holding_output: AccountWithMetadata,
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
deposit_a: u128,
@ -124,8 +126,8 @@ fn finalize_swap(
AccountPostState::new(pool_post),
AccountPostState::new(vault_a.account),
AccountPostState::new(vault_b.account),
AccountPostState::new(user_holding_a.account),
AccountPostState::new(user_holding_b.account),
AccountPostState::new(user_holding_input.account),
AccountPostState::new(user_holding_output.account),
AccountPostState::new(current_tick_account.account),
AccountPostState::new(clock.account),
];
@ -143,13 +145,12 @@ pub fn swap_exact_input(
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
user_input_holding: AccountWithMetadata,
user_output_holding: AccountWithMetadata,
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
swap_amount_in: u128,
min_amount_out: u128,
token_in_id: AccountId,
amm_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
@ -173,6 +174,20 @@ pub fn swap_exact_input(
vault_b.account.program_owner, token_program_id,
"Vault B must be owned by the configured Token Program"
);
// Swap direction is taken from the (signed) input holding's own token definition, then the
// role-based holdings are mapped back to the pool's stored A/B order so the rest of the
// routine — reserve bookkeeping and finalize — stays keyed to token A/B.
let token_in_id = token_core::TokenHolding::try_from(&user_input_holding.account.data)
.expect("Swap exact input: input holding must be a valid token holding")
.definition_id();
let (user_holding_a, user_holding_b) = if token_in_id == pool_def_data.definition_token_a_id {
(user_input_holding, user_output_holding)
} else if token_in_id == pool_def_data.definition_token_b_id {
(user_output_holding, user_input_holding)
} else {
panic!("Swap exact input: input holding token is not part of the pool");
};
assert_eq!(
user_holding_a.account.program_owner, token_program_id,
"User Token A holding must be owned by the configured Token Program"
@ -228,14 +243,23 @@ pub fn swap_exact_input(
panic!("AccountId is not a token type for the pool");
};
// Echo the two user holdings in the guest's declared slot order (input, then output) so the
// framework matches each post-state to the right account. The a/b mapping above only drives the
// reserve/vault bookkeeping; post-states are matched to accounts positionally.
let (user_holding_input, user_holding_output) =
if token_in_id == pool_def_data.definition_token_a_id {
(user_holding_a, user_holding_b)
} else {
(user_holding_b, user_holding_a)
};
let (post_states, update_tick_call) = finalize_swap(
config,
pool,
pool_def_data,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
user_holding_input,
user_holding_output,
current_tick_account,
clock,
deposit_a,
@ -343,13 +367,12 @@ pub fn swap_exact_output(
pool: AccountWithMetadata,
vault_a: AccountWithMetadata,
vault_b: AccountWithMetadata,
user_holding_a: AccountWithMetadata,
user_holding_b: AccountWithMetadata,
user_input_holding: AccountWithMetadata,
user_output_holding: AccountWithMetadata,
current_tick_account: AccountWithMetadata,
clock: AccountWithMetadata,
exact_amount_out: u128,
max_amount_in: u128,
token_in_id: AccountId,
amm_program_id: ProgramId,
) -> (Vec<AccountPostState>, Vec<ChainedCall>) {
let pool_def_data = validate_swap_setup(&pool, &vault_a, &vault_b);
@ -373,6 +396,20 @@ pub fn swap_exact_output(
vault_b.account.program_owner, token_program_id,
"Vault B must be owned by the configured Token Program"
);
// Swap direction is taken from the (signed) input holding's own token definition, then the
// role-based holdings are mapped back to the pool's stored A/B order so the rest of the
// routine — reserve bookkeeping and finalize — stays keyed to token A/B.
let token_in_id = token_core::TokenHolding::try_from(&user_input_holding.account.data)
.expect("Swap exact output: input holding must be a valid token holding")
.definition_id();
let (user_holding_a, user_holding_b) = if token_in_id == pool_def_data.definition_token_a_id {
(user_input_holding, user_output_holding)
} else if token_in_id == pool_def_data.definition_token_b_id {
(user_output_holding, user_input_holding)
} else {
panic!("Swap exact output: input holding token is not part of the pool");
};
assert_eq!(
user_holding_a.account.program_owner, token_program_id,
"User Token A holding must be owned by the configured Token Program"
@ -428,14 +465,23 @@ pub fn swap_exact_output(
panic!("AccountId is not a token type for the pool");
};
// Echo the two user holdings in the guest's declared slot order (input, then output) so the
// framework matches each post-state to the right account. The a/b mapping above only drives the
// reserve/vault bookkeeping; post-states are matched to accounts positionally.
let (user_holding_input, user_holding_output) =
if token_in_id == pool_def_data.definition_token_a_id {
(user_holding_a, user_holding_b)
} else {
(user_holding_b, user_holding_a)
};
let (post_states, update_tick_call) = finalize_swap(
config,
pool,
pool_def_data,
vault_a,
vault_b,
user_holding_a,
user_holding_b,
user_holding_input,
user_holding_output,
current_tick_account,
clock,
deposit_a,

View File

@ -2424,7 +2424,7 @@ fn test_call_new_definition_chained_call_successful() {
assert_eq!(post_states.len(), 11);
}
#[should_panic(expected = "AccountId is not a token type for the pool")]
#[should_panic(expected = "Swap exact input: input holding token is not part of the pool")]
#[test]
fn test_call_swap_incorrect_token_type() {
let _post_states = swap_exact_input(
@ -2432,13 +2432,12 @@ fn test_call_swap_incorrect_token_type() {
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_lp_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2457,7 +2456,6 @@ fn test_call_swap_vault_a_omitted() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2476,7 +2474,6 @@ fn test_call_swap_vault_b_omitted() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2495,7 +2492,6 @@ fn test_call_swap_reserves_vault_mismatch_1() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2514,7 +2510,6 @@ fn test_call_swap_reserves_vault_mismatch_2() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2533,7 +2528,6 @@ fn test_call_swap_below_minimum_liquidity() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2557,7 +2551,6 @@ fn test_call_swap_rejects_unsupported_fee_tier() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_a_low(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2576,7 +2569,6 @@ fn test_call_swap_below_min_out() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out_too_high(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2595,7 +2587,6 @@ fn test_call_swap_effective_amount_zero() {
AccountWithMetadataForTests::clock(),
1,
0,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2614,7 +2605,6 @@ fn test_call_swap_output_rounds_to_zero() {
AccountWithMetadataForTests::clock(),
2,
0,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2633,7 +2623,6 @@ fn test_call_swap_exact_input_rejects_amount_that_rounds_down_below_target_outpu
AccountWithMetadataForTests::clock(),
2,
1,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2651,7 +2640,6 @@ fn test_call_swap_exact_input_accepts_smallest_amount_for_rounded_boundary() {
AccountWithMetadataForTests::clock(),
3,
1,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
@ -2721,7 +2709,6 @@ fn test_call_swap_chained_call_successful_1() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::add_max_amount_a_low(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
@ -2754,13 +2741,12 @@ fn test_call_swap_chained_call_successful_2() {
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_b(),
BalanceForTests::min_amount_out(),
IdForTests::token_b_definition_id(),
AMM_PROGRAM_ID,
);
@ -2786,7 +2772,7 @@ fn test_call_swap_chained_call_successful_2() {
assert_update_tick_call(&chained_calls, pool_post.account());
}
#[should_panic(expected = "AccountId is not a token type for the pool")]
#[should_panic(expected = "Swap exact output: input holding token is not part of the pool")]
#[test]
fn call_swap_exact_output_incorrect_token_type() {
let _post_states = swap_exact_output(
@ -2794,13 +2780,12 @@ fn call_swap_exact_output_incorrect_token_type() {
AccountWithMetadataForTests::pool_definition_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_lp_uninit(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_lp_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2819,7 +2804,6 @@ fn call_swap_exact_output_vault_a_omitted() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2838,7 +2822,6 @@ fn call_swap_exact_output_vault_b_omitted() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2857,7 +2840,6 @@ fn call_swap_exact_output_reserves_vault_mismatch_1() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2876,7 +2858,6 @@ fn call_swap_exact_output_reserves_vault_mismatch_2() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2895,7 +2876,6 @@ fn call_swap_exact_output_below_minimum_liquidity() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2914,7 +2894,6 @@ fn call_swap_exact_output_exceeds_max_in() {
AccountWithMetadataForTests::clock(),
166_u128,
100_u128,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2933,7 +2912,6 @@ fn call_swap_exact_output_zero() {
AccountWithMetadataForTests::clock(),
0_u128,
500_u128,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2952,7 +2930,6 @@ fn call_swap_exact_output_exceeds_reserve() {
AccountWithMetadataForTests::clock(),
BalanceForTests::vault_b_reserve_init(),
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -2970,7 +2947,6 @@ fn call_swap_exact_output_chained_call_successful() {
AccountWithMetadataForTests::clock(),
BalanceForTests::max_amount_in(),
BalanceForTests::vault_b_reserve_init(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
@ -3001,13 +2977,12 @@ fn call_swap_exact_output_chained_call_successful_2() {
AccountWithMetadataForTests::pool_definition_swap_exact_output_init(),
AccountWithMetadataForTests::vault_a_init(),
AccountWithMetadataForTests::vault_b_init(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::user_holding_b(),
AccountWithMetadataForTests::user_holding_a(),
AccountWithMetadataForTests::current_tick_account_uninit(),
AccountWithMetadataForTests::clock(),
285,
300,
IdForTests::token_b_definition_id(),
AMM_PROGRAM_ID,
);
@ -3047,7 +3022,6 @@ fn call_swap_exact_output_fee_enforced() {
AccountWithMetadataForTests::clock(),
166_u128, // exact_amount_out: token_b
499_u128, // max_amount_in: still one short after fee rounding
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -3069,7 +3043,6 @@ fn call_swap_exact_output_rejects_max_in_that_rounds_down_below_target_output()
AccountWithMetadataForTests::clock(),
1,
2,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -3087,7 +3060,6 @@ fn call_swap_exact_output_accepts_smallest_max_in_for_rounded_boundary() {
AccountWithMetadataForTests::clock(),
1,
3,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
@ -3185,7 +3157,6 @@ fn swap_exact_output_overflow_protection() {
2, // exact_amount_out: small, valid (< reserve_b)
1, // max_amount_in: tiny — real deposit would be enormous, but
// overflow wraps it to 0, making 0 <= 1 pass silently
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -3878,7 +3849,6 @@ fn swap_exact_input_overflow_protection() {
AccountWithMetadataForTests::clock(),
3,
1,
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
@ -4076,7 +4046,6 @@ fn test_swap_exact_input_rejects_user_holding_a_wrong_program() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -4095,7 +4064,6 @@ fn test_swap_exact_input_rejects_user_holding_b_wrong_program() {
AccountWithMetadataForTests::clock(),
BalanceForTests::add_max_amount_a(),
BalanceForTests::min_amount_out(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -4114,7 +4082,6 @@ fn test_swap_exact_output_rejects_user_holding_a_wrong_program() {
AccountWithMetadataForTests::clock(),
166,
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}
@ -4133,7 +4100,6 @@ fn test_swap_exact_output_rejects_user_holding_b_wrong_program() {
AccountWithMetadataForTests::clock(),
166,
BalanceForTests::max_amount_in(),
IdForTests::token_a_definition_id(),
AMM_PROGRAM_ID,
);
}

View File

@ -1286,7 +1286,6 @@ fn execute_swap_a_to_b(state: &mut V03State, swap_amount_in: u128, min_amount_ou
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in: Ids::token_a_definition(),
deadline: u64::MAX,
};
@ -1318,10 +1317,10 @@ fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_ou
let instruction = amm_core::Instruction::SwapExactInput {
swap_amount_in,
min_amount_out,
token_definition_id_in: Ids::token_b_definition(),
deadline: u64::MAX,
};
// Token B is the input, so the B holding occupies the signed input slot.
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
@ -1329,8 +1328,8 @@ fn execute_swap_b_to_a(state: &mut V03State, swap_amount_in: u128, min_amount_ou
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::user_a(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
@ -2725,10 +2724,10 @@ fn amm_swap_b_to_a() {
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,
};
// Token B is the input, so the B holding occupies the signed input slot.
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
@ -2736,8 +2735,8 @@ fn amm_swap_b_to_a() {
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::user_a(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
@ -2780,7 +2779,6 @@ fn amm_swap_a_to_b() {
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,
};
@ -2852,7 +2850,6 @@ fn amm_swap_exact_output_refreshes_current_tick() {
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,
};
@ -2923,10 +2920,10 @@ fn amm_swap_exact_output_b_to_a_signs_only_input() {
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,
};
// Token B is the input, so the B holding occupies the signed input slot; only it is signed.
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
@ -2934,8 +2931,8 @@ fn amm_swap_exact_output_b_to_a_signs_only_input() {
Ids::pool_definition(),
Ids::vault_a(),
Ids::vault_b(),
Ids::user_a(),
Ids::user_b(),
Ids::user_a(),
Ids::current_tick_account(),
CLOCK_01_PROGRAM_ACCOUNT_ID,
],
@ -2995,61 +2992,6 @@ fn amm_swap_exact_output_b_to_a_signs_only_input() {
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();
@ -3057,7 +2999,6 @@ fn amm_swap_exact_input_requires_input_signature() {
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,
};
@ -3089,16 +3030,17 @@ fn amm_swap_exact_input_requires_input_signature() {
}
#[test]
fn amm_swap_exact_input_rejects_token_definition_id_in_mismatch() {
fn amm_swap_exact_input_rejects_when_only_output_holding_signed() {
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,
};
// Input slot holds token A but only the output (B) holding is signed: the input is unsigned, so
// the swap must be rejected.
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
@ -3126,137 +3068,6 @@ fn amm_swap_exact_input_rejects_token_definition_id_in_mismatch() {
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();
@ -3264,7 +3075,6 @@ fn amm_swap_exact_output_requires_input_signature() {
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,
};
@ -3296,16 +3106,17 @@ fn amm_swap_exact_output_requires_input_signature() {
}
#[test]
fn amm_swap_exact_output_rejects_token_definition_id_in_mismatch() {
fn amm_swap_exact_output_rejects_when_only_output_holding_signed() {
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,
};
// Input slot holds token A but only the output (B) holding is signed: the input is unsigned, so
// the swap must be rejected.
let message = public_transaction::Message::try_new(
Ids::amm_program(),
vec![
@ -3333,83 +3144,6 @@ fn amm_swap_exact_output_rejects_token_definition_id_in_mismatch() {
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]
fn amm_sync_reserves_updates_pool_and_current_tick() {
let mut state = state_for_amm_tests();
@ -3503,7 +3237,6 @@ fn amm_swap_rejects_expired_deadline() {
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: deadline_ms,
};
@ -3542,7 +3275,6 @@ fn amm_swap_exact_output_rejects_expired_deadline() {
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: deadline_ms,
};