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.
Problem
Current swap instructions take both user token-side holdings:
user_holding_auser_holding_bOnly one is debited.
token_definition_id_inselects which side pays.Current SPEL signer checks are unconditional. Marking both holdings as
#[account(signer)]forces the output holding to sign even though it onlyreceives 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(). Removetoken_definition_id_in.Apply the same account shape to
swap_exact_output, withexact_amount_out,max_amount_in, anddeadlineas arguments.Side resolution
Decode
user_holding_inand compare its token definition to the pool:definition_token_a_id: run A-in/B-out;definition_token_b_id: run B-in/A-out;For A-in/B-out:
user_holding_invault_avault_buser_holding_outFor B-in/A-out:
user_holding_invault_bvault_auser_holding_outValidation
Keep current config, pool, vault, oracle, clock, reserve, fee, and slippage
checks.
Add role checks:
user_holding_inis owned by the configured token program.user_holding_indecodes as a token holding.user_holding_in.definition_id()matches exactly one pool token.user_holding_outis owned by the configured token program.user_holding_outmatches 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:
Second transfer:
This keeps current security properties:
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_inargument should disappear because the signedinput holding already identifies the input token definition.
Tests
Add unit and integration tests for:
user_holding_in;user_holding_in;user_holding_in;user_holding_in;user_holding_inis rejected;user_holding_inis rejected;user_holding_outdefinition is rejected;user_holding_out == user_holding_inis rejected;Rejected alternatives
one instruction per swap mode.
token_definition_id_in.Acceptance criteria
swap_exact_inputandswap_exact_outputuseuser_holding_inasthe only user signer.
user_holding_in.definition_id().token_definition_id_inis removed.artifacts/amm-idl.jsonis regenerated.