Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 6 additions & 14 deletions artifacts/amm-idl.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,15 +423,15 @@
"init": false
},
{
"name": "user_holding_a",
"name": "user_input_holding",
"writable": true,
"signer": true,
"init": false
},
{
"name": "user_holding_b",
"name": "user_output_holding",
"writable": true,
"signer": true,
"signer": false,
"init": false
},
{
Expand All @@ -456,10 +456,6 @@
"name": "min_amount_out",
"type": "u128"
},
{
"name": "token_definition_id_in",
"type": "account_id"
},
{
"name": "deadline",
"type": "u64"
Expand Down Expand Up @@ -494,15 +490,15 @@
"init": false
},
{
"name": "user_holding_a",
"name": "user_input_holding",
"writable": true,
"signer": true,
"init": false
},
{
"name": "user_holding_b",
"name": "user_output_holding",
"writable": true,
"signer": true,
"signer": false,
"init": false
},
{
Expand All @@ -527,10 +523,6 @@
"name": "max_amount_in",
"type": "u128"
},
{
"name": "token_definition_id_in",
"type": "account_id"
},
{
"name": "deadline",
"type": "u64"
Expand Down
25 changes: 15 additions & 10 deletions programs/amm/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,44 +172,49 @@ pub enum Instruction {
deadline: u64,
},

/// Swap some quantity of Tokens (either Token A or Token B)
/// while maintaining the Pool constant product.
/// Swap some quantity of tokens while maintaining the Pool constant product.
///
/// 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
/// - User Holding Account for Token B; either 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
/// - Clock Account (the canonical 1-block LEZ clock)
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,
},

/// Swap tokens specifying the exact desired output amount,
/// while maintaining the Pool constant product.
/// Swap tokens specifying the exact desired output amount while maintaining the Pool constant
/// product.
///
/// 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
/// - User Holding Account for Token B; either 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
/// - Clock Account (the canonical 1-block LEZ clock)
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,
},
Expand Down
30 changes: 16 additions & 14 deletions programs/amm/methods/guest/src/bin/amm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,9 @@ mod amm {
}

/// Swap some quantity of tokens while maintaining the pool constant product.
///
/// 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"
Expand All @@ -313,36 +316,37 @@ mod amm {
#[account(mut)]
vault_b: AccountWithMetadata,
#[account(mut, signer)]
user_holding_a: AccountWithMetadata,
#[account(mut, signer)]
user_holding_b: AccountWithMetadata,
user_input_holding: AccountWithMetadata,
#[account(mut)]
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(
config,
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)
.with_timestamp_validity_window(..deadline))
}

/// Swap tokens specifying the exact desired output amount.
///
/// 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"
Expand All @@ -358,29 +362,27 @@ mod amm {
#[account(mut)]
vault_b: AccountWithMetadata,
#[account(mut, signer)]
user_holding_a: AccountWithMetadata,
#[account(mut, signer)]
user_holding_b: AccountWithMetadata,
user_input_holding: AccountWithMetadata,
#[account(mut)]
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(
config,
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)
Expand Down
74 changes: 60 additions & 14 deletions programs/amm/src/swap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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),
];
Expand All @@ -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);
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand All @@ -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"
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading