For the complete documentation index, see llms.txt. This page is also available as Markdown.

Encoding

The first step to executing a trade on-chain is encoding.

Our Rust crate converts your trades into calldata that the Tycho contracts can execute.

See this Quickstart section for an example of how to encode your trade.

Models

These are the models used as input and output of the encoding crate.

The Solution struct defines your order and how it should be filled. This is the input to the encoding module.

Attribute
Type
Description

sender

Bytes

Address of the sender of the token in

receiver

Bytes

Address that receives the output token. If set to the TychoRouterV3 address, the output is credited to the sender's vault balance instead of being transferred out.

token_in

Bytes

The input token

amount_in

BigUint

Amount of the input token

token_out

Bytes

The output token

expected_amount_out

BigUint

The output amount your simulation quoted. The router receives it as expectedAmountOut. Encoding fails if it is zero

min_amount_out

BigUint

The smallest output you accept. The router receives it as minAmountOut and reverts below it. Compute it off-chain from your slippage tolerance, e.g. expected_amount_out * 0.9975 for 0.25%

swaps

Vec<Swap>

List of swaps to fulfil the solution

user_transfer_type

UserTransferType

How the input token enters the router — see the UserTransferType tab

Output amounts

The router takes two output guardrails, and the solution supplies both:

Solution
Router argument

expected_amount_out()

expectedAmountOut

min_amount_out()

minAmountOut

Both are absolute amounts, so refreshing a quote means updating both — apply your slippage tolerance to the new quote and set min_amount_out to the result. The router caps minAmountOut at expectedAmountOut (see Slippage bounds), so it rejects a floor above the quote.

Specifies how user funds (the input token) enter the router:

Variant
Description

TransferFromPermit2

Use Permit2 for token transfer. You must approve the Permit2 contract and sign the permit externally.

TransferFrom (default)

Use standard ERC-20 approve + transferFrom. You must approve the TychoRouterV3 to spend your tokens.

UseVaultsFunds

No transfer is performed. Uses tokens already deposited in the TychoRouterV3 vault.

A solution consists of one or more swaps. Each swap represents an operation on a single pool.

The Swap struct has the following attributes:

Attribute
Type
Description

component

ProtocolComponent

Protocol component from tycho-common

token_in

Token

The token you provide to the pool

token_out

Token

The token you expect from the pool

split

f64

Fraction of the input amount to route through this swap, as a decimal between 0 and 1 (e.g. 0.5 = 50%)

user_data

Option<Bytes>

Optional user data to be passed to encoding

protocol_state

Option<Arc<dyn ProtocolSim>>

Optional protocol state used to perform the swap

estimated_amount_in

Option<BigUint>

Optional estimated amount in for this swap. Necessary for RFQ protocols — used to request the quote.

estimated_gas

BigUint

Per-swap gas estimate from simulation

Split Swaps

Solutions can split one or more token hops across multiple pools. The output of one swap is divided into parts, each used as input for subsequent swaps:

Diagram representing examples of split swaps

By combining splits, you can build complex trade paths.

We validate split swaps. A split swap is valid if:

  1. The output token is reachable from the input token through the swap path

  2. No tokens are unconnected

  3. Each split amount is smaller than 1 (100%) and at least 0 (0%)

  4. For each set of splits, set the split for the last swap to 0. This tells the router to send all tokens not assigned to the previous splits in the set (i.e., the remainder) to this pool.

  5. The sum of all non-remainder splits for each token is smaller than 1 (100%)

Split fractions are applied to the balance seen so far. The router processes swaps sequentially. A non-zero split takes a fraction of the total amount produced for that token up to that point in the swap array — not the final total. A split of 0 consumes whatever remains. If more of the same token is produced by a later swap, it will not be included in any earlier split's calculation.

Example Solution

The following diagram shows a swap from ETH to DAI through USDC. The first swap wraps ETH to WETH on the native_wrapper component. The solution then splits between three (WETH, USDC) pools and finally swaps from USDC to DAI on one pool.

Diagram of an example solution

The Solution object for the given scenario would look as follows:

The 4th argument to Swap::new is the per-swap estimated_gas (a BigUint). Splits are configured via .with_split(...) on the builder.

Swap Group

Protocols like Uniswap V4 eliminate token transfers between consecutive swaps through flash accounting. If your solution contains sequential (non-split) swaps on such protocols, the encoder compresses them into a single swap group, requiring only one call to the executor.

Diagram representing swap groups

In the example above, the encoder will compress three consecutive swaps into the following swap group to call the Executor:

A solution contains multiple swap groups when it uses different protocols.

Encoding produces an EncodedSolution with these attributes:

Attribute
Type
Description

swaps

Vec<u8>

The encoded calldata for the swaps

interacting_with

Bytes

The address of the contract to be called (e.g. the Tycho Router or an Executor)

function_signature

String

Full signature of the router method to call, e.g. splitSwapUsingVault(uint256,address,...). It reflects both the swap strategy and the funding mode

n_tokens

usize

The number of tokens in the trade (relevant for split swaps only)

estimated_gas

BigUint

Estimated gas usage for the encoded solution

Encoder

TychoRouterEncoder prepares calldata for execution via the Tycho Router contract. It supports multi-hop and split swaps.

Builder

Builder options:

  • swap_encoder_registry — Registry of protocol-specific SwapEncoders used during encoding. Use new_with_defaults for built-in support, or add custom encoders for protocols you've implemented locally.

  • router_address — Router address for execution. Defaults to the deployed address for the given chain ( see Tycho addresses).

Builder Example Usage

Swap Encoders

Each protocol needs its own SwapEncoder to define how the protocol encodes swaps into calldata.

The SwapEncoderRegistry manages these encoders. Use SwapEncoderRegistry::new_with_defaults(chain) to get a registry pre-populated with all built-in encoders. If you need to supply custom executor addresses, use SwapEncoderRegistry::new(chain).add_default_encoders(Some(addresses_json)) instead.

If you need to add custom protocol support, register your own encoder implementation:

Encode

Convert solutions into calldata:

This returns a Vec<EncodedSolution> containing only the encoded swaps. It does not build the full calldata. You must encode the full method call yourself. If you use Permit2, you must handle permit creation and signing yourself using the public Permit2 utility (see Token transfers).

The full method call includes the following parameters, which act as execution guardrails:

  • amountIn and tokenIn — the amount and token you transfer into the TychoRouterV3. For native ETH, use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE. The router reverts if the token is address(0) or the amount is zero.

  • expectedAmountOut — your quoted output amount, taken from solution.expected_amount_out(). The router measures positive slippage against it and caps minAmountOut at it (see Slippage bounds). It must be greater than zero.

  • minAmountOut and tokenOut — the smallest output you are willing to accept once fees are deducted, taken from solution.min_amount_out(). The same native ETH address rule applies. For maximum security, derive the underlying quote from a third-party source.

  • receiver — who receives the final output. Set this to the TychoRouterV3 address to credit output tokens to the vault.

  • nTokens(split swaps only) the number of distinct tokens in the split routing graph.

  • clientFeeParams — controls fee-taking and client contribution (see Client Fee Signature). Pass all-zero values if you don't need fees.

The ClientFeeParams struct is defined as:

Field
Description

clientFeeBps

Client fee as a uint32 in fee units, where 100_000_000 = 100% (see Fee units). Set to 0 to take no fee

clientFeeReceiver

Address that identifies the client and receives the client fee (credited to their vault balance). The router resolves negotiated fee rates and positive-slippage exemptions against it, so a signed receiver is worth passing even with clientFeeBps set to 0

maxClientContribution

Maximum amount the client is willing to pay out of pocket if slippage causes the output to fall below minAmountOut. If the shortfall exceeds this value, the transaction reverts. Set to 0 if the client should not subsidize

deadline

Unix timestamp after which the signature is no longer valid

clientSignature

EIP-712 signature over the fee fields and the full swap intent, signed by clientFeeReceiver: a 65-byte ECDSA signature when that address is an EOA, or an ERC-1271 signature of any length when it is a contract

Fee units

clientFeeBps and the router's own fee rates use an 8-decimal fee unit rather than plain basis points, which lets the router charge sub-BPS rates:

Rate
Fee units

100%

100_000_000

1%

1_000_000

1 BPS (0.01%)

10_000

0.1 BPS (0.001%)

1_000

The FeeCalculator exposes both values: MAX_BPS (100_000_000) and MAX_BPS_SQUARED (MAX_BPS², the combined denominator when the router charges a fee on another fee).

The tycho-execution crate provides a ClientFeeParams Rust struct that mirrors this. Callers are responsible for constructing and signing it — the encoder does not use it internally. Call .into_abi_params() to convert it to the ABI-encodable tuple for calldata construction.

These execution guardrails protect against MEV exploits. Setting them correctly gives you full control over swap security.

Refer to the quickstart for an example of converting an EncodedSolution into full calldata. Tailor the example to your use case. See the TychoRouterV3 contract functions for reference.

Slippage bounds

minAmountOut must be non-zero and no greater than expectedAmountOut:

The router reverts with TychoRouter__InvalidMinAmountOut for a zero minAmountOut or one above expectedAmountOut. There is no lower cap on how far below the quote you may set it, so compute a real floor from your slippage tolerance — a minAmountOut set too low exposes the swap to MEV attacks. Pass the amount your simulation returned as expectedAmountOut.

The router may capture output above expectedAmountOut as positive slippage, so it does not guarantee that surplus beyond your quote reaches the receiver. Amounts between minAmountOut and expectedAmountOut always do.

Native Tokens

ETH and WETH are separate tokens in a solution, and the encoder does not convert between them for you. Wherever your route goes from one to the other, add a swap on the native_wrapper protocol. The Tycho stream injects a native_wrapper component on every chain, so you route through it like through any other pool, and a dedicated WETH executor runs the swap.

Your swaps must connect token_in to token_out, so a missing wrap swap is rejected at validation.

Client Fee Signature

Only required when charging a fee or allowing a client contribution. The clientFeeReceiver must sign using EIP-712 — this prevents third parties from spoofing fee configurations. The signature covers the fee parameters and the full swap intent:

swaps is the encoded swap graph — the same bytes you pass to the router. EIP-712 requires dynamic types to be hashed, so pass keccak256(swaps) when you build the struct hash.

The signature covers every field above, which means it only validates for a swap with identical input parameters. Re-encoding the route or changing any amount invalidates it, so sign after you encode rather than before.

To confirm you are signing the right struct, compare your typehash against the router's public CLIENT_FEE_TYPEHASH constant.

The EIP-712 domain is:

with name = "TychoRouter", version = "1", and verifyingContract set to the TychoRouterV3 contract address.

The clientFeeReceiver can be an EOA or a contract. The router first recovers the signature with ECDSA and accepts it when it recovers to clientFeeReceiver. Otherwise it staticcalls isValidSignature(digest, clientSignature) on the receiver and accepts the fee when that call returns the ERC-1271 magic value. A contract receiver therefore decides for itself what counts as a valid signature — a Safe, for example, checks owner signatures — and clientSignature carries no length constraint in that case. ECDSA runs first, so an EOA that carries delegated code (EIP-7702) keeps signing with its own key.

Contract signatures are revocable. A clientSignature that a contract accepts in one block may fail in the next if the contract's validation state changes, for example after an owner rotation.

Sign fee parameters example

Example of signing the fee parameters in Rust using alloy:

Pass the returned 65-byte signature as the clientSignature field in ClientFeeParams. A contract receiver supplies its own ERC-1271 signature instead, of any length.

Run as a Binary

The encoding crate ships a tycho-encode CLI that lets you encode swaps without writing Rust. Install it with:

Pass a JSON-serialised Solution via stdin and specify the encoder as a subcommand:

  • tycho-router — encodes using TychoRouterEncoder

The CLI accepts the same options as the builder.

Example

Encodes a swap from DAI to WETH using Uniswap V2 on Ethereum:

Last updated

Was this helpful?