Version 1.
TernSwap is deployed on every supported chain, and TernPrivateSwap on Starknet, with no owner and no upgrade path. Their addresses are in Contract addresses.TernSwap runs a same-chain swap in one transaction. It takes the user’s tokens, trades them through a DEX aggregator’s route, and checks the result: the user gets at least the quoted minimum, or the whole transaction reverts. This page is the full technical specification, for auditors and for teams calling the contract directly. To integrate through the API, see Same-chain swaps instead.
1. Summary
A swap whose two tokens are on the same chain needs no escrows, no execution condition, and no solver. It settles in the one transaction the user sends, through that chain’sTernSwap contract.
TernSwap takes the user’s input, has a separate executor run a DEX aggregator’s route, and measures what comes back. It takes Tern’s fee from the output, pays the recipient at least the order’s minimum or reverts, refunds unspent input to the sender, and emits one Swapped event. The event has the same shape whichever aggregator built the route, so an indexer follows a swap the way it follows escrows: from chain events alone, with no client hints and no per-aggregator decoding.
To an API client a same-chain swap is still a quote, with the same legs, status, and execution data as a cross-chain one.
The key words MUST, MUST NOT, SHOULD, and MAY are used as in RFC 2119.
2. Terminology
3. Design
- The executor runs the route; nobody approves the executor. A route is arbitrary calldata from an aggregator. If
TernSwapran it, a route could spend approvals users gaveTernSwap, by calling a token’stransferFrom. The executor holds only the current swap’s input, and only during the swap. - Settlement by balance change.
TernSwapmeasures its own balance of both tokens before and after the route, so a route can neither fake its output nor spend another swap’s tokens. Donations to either contract only add to the next swap of that token. - The minimum protects the user, whatever the route does. A route that delivers too little, delivers elsewhere, or fails makes the whole swap revert.
- No admin. No owner, pause, allowlist, or upgrade path. The service that builds routes chooses which router contracts it uses.
- One event for every route.
Swappedcarries the quote ID and the actual amounts.
4. EVM interface
TernSwap’s CREATE address for nonce 1. TernSwap deploys through CreateX, so one deployer gets the same address on every EVM chain.
5. Starknet interface
Swapped event carries the same fields in snake case, with quote_id, sender, and recipient as keys. Starknet has no native coin: STRK and ETH are ERC-20 tokens.
A route is a list of calls, because aggregators take their input in different ways:
- By approval (AVNU):
spenderis AVNU’s exchange, and the route is itsmulti_route_swapcall. - By transfer first (Ekubo): there is no spender. The calls
transferthe input to Ekubo’s router,multi_multihop_swap,clear_minimumthe bought token, andclearthe sold token back to the executor.
TernSwap deploys the executor in its constructor (deploy_syscall, salt 0, no calldata), so the executor’s address follows from TernSwap’s address and the executor class hash.
6. Behavior of swap
swap MUST revert when:
- the block time is after
deadline; sellTokenequalsbuyToken;sellAmountorminBuyAmountis zero;recipientis zero;feeBpsis over 1000;- on EVM, the call’s value is not exactly
sellAmountwhen selling the native coin, or is not zero otherwise.
- Record
TernSwap’s balances of both tokens (on EVM, without the value just received). - Move
sellAmountfrom the caller to the executor:transferFromfor a token, or forward the value for the native coin. - Call the executor. It approves
route.spenderfor exactlysellAmount(if set and not selling the native coin), runs the route, resets the approval to zero, and sends its whole balance of both tokens back toTernSwap. A failing call reverts the swap. - Compute what came back:
outputis the increase inTernSwap’s bought-token balance;unspentis the increase in its sold-token balance;fee = floor(output × feeBps / 10000);buyAmount = output − fee.
- If
buyAmount < minBuyAmount, revert. - Pay
feetofeeRecipientif it is not zero, paybuyAmounttorecipient, and refundunspentto the caller if it is not zero. - Emit
SwappedwithsellAmount − unspentas the amount sold.
swap MUST be non-reentrant. TernSwap MUST accept the native coin only from its executor. Neither contract holds anything between swaps.
7. Rules for the service that builds swaps
- The quote ID MUST be text of 1 to 31 printable ASCII characters. On EVM it is written left-aligned and zero-padded into the
bytes32(as Solidity’sbytes32("q_…")); on Starknet it is a Cairo short string. Explorers then show the ID as text. - Routes MUST be built with the executor as the aggregator’s taker and recipient.
- The service SHOULD only accept routes whose calls and spender are known router contracts. On Starknet, it should also accept a
transferof the sold token to one of those routers. - The route’s own minimum MUST be high enough that the recipient still receives
minBuyAmountafter the fee. deadlineSHOULD be short, around a minute, because routes go stale.- An indexer MUST NOT treat a
Swappedevent as settling a quote just because the event names the quote’s ID: anyone can callswapwith any ID. The event settles the quote only when all of these match the quote:- the chain,
sender,recipient,sellTokenandbuyToken; - the amount sold is at most the quoted amount;
buyAmountis at least the quoted minimum;- the fee is the quoted rate, paid to the quoted recipient.
- the chain,
8. Security considerations
9. API usage (informative)
POST /v1/quote with from and to on one chain returns a quote with route: "swap". Without sender and recipient it is indicative; with both, it is firm, and its execute holds the transactions to send: on EVM, an approve of TernSwap when the allowance is short, then swap; on Starknet, the [approve, swap] multicall. to.amount is the expected output after Tern’s fee, and to.minAmount is the order’s minBuyAmount.
GET /v1/quote/{id} is awaiting_deposit until the swap’s transaction is final, then completed, or expired if it wasn’t sent before expiresAt. Both legs report that one transaction, with the amounts spent and received. See Same-chain swaps for the full flow.
10. Private swaps on Starknet
TernPrivateSwap lets a user swap from a shielded balance in Starknet’s privacy pool (STRK20) and get the output back as a new shielded note. It is a helper (“anonymizer”) that the pool invokes inside the user’s private transaction. It wraps the deployed TernSwap, so the order checks, Tern’s fee, the minimum, and the Swapped event are all the ones above. It is deployed on Starknet mainnet: see Contract addresses. See Private swaps for the API.
- withdraw
order.sell_amountof the sold token to the helper; - withdraw the relayer’s fee, in STRK, to the relayer;
- open a note of the bought token for the user;
- invoke the helper with the order, the route, and that note’s ID.
privacy_invoke MUST revert unless order.recipient is the helper. Otherwise:
- Record the helper’s balances of both tokens.
- Approve
TernSwapforsell_amountand callswap(order, route). The helper is both the swap’s sender and its recipient. - Revert unless the route spent the whole
sell_amount(PrivateSwap: partial fill). - Approve the caller (the pool) for the output and return it as one deposit into
note_id. The pool pulls it.
TernSwap’s address.
The pool takes its own fee, in STRK, from whoever sends the private transaction, before it applies the actions. Tern’s relayer therefore sends [STRK.approve(pool, fee), pool.apply_actions(…)] and fronts the fee. Action 2 repays it, plus the relayer’s gas.