Skip to content

Refactor AMM swaps to use one signed input holding #207

Description

@3esmit

Problem

Current swap instructions take both user token-side holdings:

  • user_holding_a
  • user_holding_b

Only one is debited. token_definition_id_in selects which side pays.

Current SPEL signer checks are unconditional. Marking both holdings as
#[account(signer)] forces the output holding to sign even though it only
receives tokens. Marking neither gives IDL-driven clients no signer guidance.

Proposal

Refactor the existing swap instructions to use input/output roles:

  • user_holding_in: signed holding debited by the token transfer.
  • user_holding_out: unsigned recipient holding.

Derive swap direction from user_holding_in.definition_id(). Remove
token_definition_id_in.

#[instruction]
pub fn swap_exact_input(
    ctx: ProgramContext,
    config: AccountWithMetadata,
    #[account(mut)]
    pool: AccountWithMetadata,
    #[account(mut)]
    vault_a: AccountWithMetadata,
    #[account(mut)]
    vault_b: AccountWithMetadata,
    #[account(mut, signer)]
    user_holding_in: AccountWithMetadata,
    #[account(mut)]
    user_holding_out: AccountWithMetadata,
    #[account(mut)]
    current_tick_account: AccountWithMetadata,
    clock: AccountWithMetadata,
    swap_amount_in: u128,
    min_amount_out: u128,
    deadline: u64,
) -> SpelResult

Apply the same account shape to swap_exact_output, with
exact_amount_out, max_amount_in, and deadline as arguments.

Side resolution

Decode user_holding_in and compare its token definition to the pool:

  • match definition_token_a_id: run A-in/B-out;
  • match definition_token_b_id: run B-in/A-out;
  • otherwise reject.

For A-in/B-out:

  • deposit source: user_holding_in
  • deposit vault: vault_a
  • withdraw vault: vault_b
  • recipient: user_holding_out

For B-in/A-out:

  • deposit source: user_holding_in
  • deposit vault: vault_b
  • withdraw vault: vault_a
  • recipient: user_holding_out

Validation

Keep current config, pool, vault, oracle, clock, reserve, fee, and slippage
checks.

Add role checks:

  • user_holding_in is owned by the configured token program.
  • user_holding_in decodes as a token holding.
  • user_holding_in.definition_id() matches exactly one pool token.
  • initialized user_holding_out is owned by the configured token program.
  • initialized user_holding_out matches the opposite token definition.
  • user_holding_out != user_holding_in.

The downstream token transfer still enforces debit authorization because its
sender is always user_holding_in, and SPEL requires that account to sign.

Chained calls

First transfer:

vec![user_holding_in, vault_deposit]

Second transfer:

vec![vault_withdraw_authorized, user_holding_out]

This keeps current security properties:

  • user debit requires user signature;
  • vault debit requires existing vault PDA seed;
  • output holding does not sign only to receive tokens.

Migration impact

This is an IDL-breaking refactor of current swap instructions. Update callers,
tests, examples, and generated IDL in the same change.

The old token_definition_id_in argument should disappear because the signed
input holding already identifies the input token definition.

Tests

Add unit and integration tests for:

  • A-in/B-out exact input signs only user_holding_in;
  • B-in/A-out exact input signs only user_holding_in;
  • A-in/B-out exact output signs only user_holding_in;
  • B-in/A-out exact output signs only user_holding_in;
  • unsigned user_holding_in is rejected;
  • non-pool-token user_holding_in is rejected;
  • wrong initialized user_holding_out definition is rejected;
  • user_holding_out == user_holding_in is rejected;
  • TWAP update and vault PDA chained calls remain unchanged;
  • regenerated AMM IDL shows one user signer for each swap.

Rejected alternatives

  • Require both user holdings to sign.
    • Works with current SPEL, but over-constrains swaps.
  • Keep A/B user accounts and remove signer metadata.
    • Leaves clients without signer guidance.
  • Split into A-to-B and B-to-A instructions.
    • Also works, but duplicates the public API. Input/output role accounts keep
      one instruction per swap mode.
  • Keep token_definition_id_in.
    • It duplicates information already carried by the signed input holding.

Acceptance criteria

  • Current swap_exact_input and swap_exact_output use user_holding_in as
    the only user signer.
  • Swap direction is derived from user_holding_in.definition_id().
  • token_definition_id_in is removed.
  • Output holding is never required to sign only because it receives tokens.
  • Reserve math, fees, slippage, vault PDA auth, and TWAP update stay unchanged.
  • artifacts/amm-idl.json is regenerated.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions