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

Execution

To integrate a new protocol into Tycho, you need to implement two key components:

  1. SwapEncoder (Rust struct) – Translates swap parameters into protocol-specific calldata.

  2. Executor (Solidity contract) – Processes that calldata to perform the swap on-chain.

See more about our code architecture here.

Encoder Interface

Each protocol needs a dedicated encoder that implements the SwapEncoder trait:

fn encode_swap(
    &self,
    swap: Swap,
    encoding_context: EncodingContext,
) -> Result<Vec<u8>, EncodingError>;

This function encodes a swap and its relevant context information into calldata that is compatible with the Executor contract. The output of the SwapEncoder is the input of the Executor (see next section). We recommend using packed encoding to save gas. See current implementations here.

For protocol-specific constant addresses, add them to config/protocol_specific_addresses.json.

Once your SwapEncoder is implemented:

Protocols Supporting Consecutive Swap Optimizations

Some protocols — like Uniswap V4 — use flash accounting to batch consecutive swaps without intermediate token transfers. As described in the Swap Group section, each SwapEncoder still encodes only a single swap; the strategy encoder concatenates them into a single executor call.

The first swap in a group may carry extra data that subsequent swaps omit (see the diagram below).

Diagram representing swap groups
Output of a SwapEncoder for a group swap

Swap Interface

Every integrated protocol requires its own swap executor contract. This contract must implement the IExecutor interface. See currently implemented executors here. Please also look through our Contributing Guidelines for Solidity.

The IExecutor interface requires three methods:

swap

Called by the Dispatcher via delegatecall. This function:

  • Accepts the input amount (amountIn). The input amount is calculated at execution time, not during encoding, to account for possible slippage.

  • Processes the swap using the provided calldata (data), which is the output of the SwapEncoder.

  • Sends output tokens to receiver.

  • Does not return any amountOut - for security purposes, this information is automatically detected using balance checks in the Dispatcher

Important: Executors must not transfer any ERC20 tokens. All input and output token transfers are handled by the Dispatcher (via the TransferManager). The only exception is native ETH — executors that interact with protocols requiring ETH as msg.value (e.g., Fluid, Rocketpool) handle this themselves and declare TransferNativeInExecutor as their transfer type.

getTransferData

Called by the Dispatcher via staticcall before each swap to determine how input tokens should be transferred. The executor returns:

  • transferType: How the protocol expects to receive tokens (see Token Transfers).

  • receiver: Where tokens should be sent (typically the pool address or the router).

  • tokenIn: The input token address. For native ETH, this must be 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (the ETH_ADDRESS constant), not address(0).

  • tokenOut: The output token address. Same rule applies for native ETH.

  • outputToRouter: Whether the protocol automatically sends the output token back to the TychoRouterV3. The Dispatcher uses this to decide whether it needs to transfer the token to the intended receiver.

transferType, receiver and outputToRouter must be hardcoded per-executor based on the protocol's requirements — they are not encodable in calldata.

fundsExpectedAddress

Used during sequential swaps to determine where the previous swap should send its output tokens. For example, in a route WBTC → USDC → DAI, before executing the first swap, the Dispatcher calls fundsExpectedAddress on the second executor to decide where to send USDC.

  • Return the pool address if the protocol accepts direct transfers (e.g., Uniswap V2 pools).

  • Return msg.sender (the router) if the protocol expects tokens in the router (e.g., callback-based protocols).

Callbacks

Some protocols require a callback during swap execution (e.g., Uniswap V3, Uniswap V4, Balancer V3). In these cases, the executor contract must also implement ICallback.

Required Methods

  • handleCallback: The main entry point for handling callbacks.

  • verifyCallback: Should be called within handleCallback to ensure that the msg.sender is a valid pool from the expected protocol.

  • getCallbackTransferData: Called by the Dispatcher during the callback to determine how tokens should be transferred. Like getTransferData, the transfer type must be hardcoded — the Dispatcher handles the actual transfer based on the returned values. The Dispatcher supplies tokenIn from its own transient storage (recorded during getTransferData) rather than trusting the callback data, so a protocol cannot inject a different token. caller is the msg.sender seen in the router's fallback(), which lets the executor validate the calling pool.

Callback Flow

When a protocol initiates a callback during swap execution, it flows through the TychoRouterV3's fallback() method, which acts as the entry point for all callback requests. The router's fallback function delegates the call to the Dispatcher, which:

  1. Calls getCallbackTransferData on the executor to determine transfer requirements.

  2. Performs the token transfer via the TransferManager (the executor does not transfer tokens itself).

  3. Calls handleCallback on the executor to complete the swap interaction.

The callback data passed through this flow should include the function selector and all necessary information for the executor to complete the swap operation, such as token addresses, amounts, and any protocol-specific parameters required by the pool contract.

Token Transfers

Executors do not handle any token transfers. All ERC20 token transfers are orchestrated by the Dispatcher via the TransferManager. The Dispatcher calls getTransferData (or getCallbackTransferData during callbacks) on the executor to learn how the protocol expects to receive tokens, and then performs the transfer itself.

This design reduces the attack surface — a malicious or buggy executor cannot misroute user funds because it never touches the input token directly.

TransferType

Each executor must return a hardcoded TransferManager.TransferType from getTransferData (and getCallbackTransferData for callback executors). The available types are:

Transfer Type
Description

Transfer

The Dispatcher transfers tokens to the pool (or router) before calling swap. Used by protocols that expect tokens to be present in the pool before the swap call (e.g., Uniswap V2).

ProtocolWillDebit

The protocol pulls tokens from the router via an approval. The Dispatcher approves the protocol to spend the required amount. Used by protocols like Curve and Balancer V2.

TransferNativeInExecutor

The executor sends native ETH as msg.value during the swap. The Dispatcher only performs accounting — no ERC-20 transfer occurs. Used by protocols like Fluid and Rocketpool.

None

No transfer is needed at this point. Typically returned by getTransferData for callback-based protocols where the transfer happens inside the callback instead.

The only case where an executor handles a token transfer is native ETH (TransferNativeInExecutor). For all ERC-20 tokens, the Dispatcher is solely responsible for transfers.

How the Dispatcher Resolves Transfers

Before each swap, the Dispatcher:

  1. Calls getTransferData on the executor to get the TransferType, receiver, token addresses, and whether the protocol sends out tokens back to the router automatically.

  2. Determines the transfer strategy based on the swap context (first swap vs. subsequent, split swap, vault-funded, etc.).

  3. Performs the input token transfer via the TransferManager.

After each swap, the Dispatcher:

  1. Performs a balance check to determine the token output amount of the swap

  2. If outputToRouter is true, forwards output tokens to swap receiver

For sequential swaps, the Dispatcher also calls fundsExpectedAddress on the next executor to decide where the current swap should send its output tokens — either directly to the next pool or back to the router.

The transfer behavior is fully determined by the values your executor returns from getTransferData and fundsExpectedAddress.

Native Token Address Handling

Tycho uses address(0) to represent native tokens during indexing and simulation, but uses 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE (ETH_ADDRESS) consistently within the router and executor calldata. The boundary between these two representations lives in the swap encoder.

SwapEncoder: translate address(0) → ETH_ADDRESS

In your SwapEncoder, if you encode token addresses into the calldata, convert address(0) to ETH_ADDRESS (0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE). A helper is available:

Executor swap: translate ETH_ADDRESS → protocol address

If your protocol uses address(0) (e.g., UniswapV4, Ekubo) or another sentinel for native ETH in its pool keys or function calls, translate ETH_ADDRESS back to the protocol's expected address inside swap(), right before interacting with the protocol.

Fee Tokens

Balance checks before and after token transfers mean fee-on-transfer tokens and rebasing tokens work on most protocols. The exception is Uniswap V3-like protocols, which require declaring the exact input amount upfront but only transfer it inside a callback.

Security Requirements

TychoRouterV3 calls executors via delegatecall, so executor code runs within TychoRouterV3's context — it can freely transfer the router's assets and write to the router's storage, including users' vault balances. Follow this checklist when building or reviewing an executor.

  • Never call ERC20.transfer, ERC20.transferFrom, or Permit2.transferFrom directly. Communicate transfer intent through getTransferData and getCallbackTransferData instead — TychoRouterV3 performs the actual transfers with its own safeguards. The only exception is for native ETH transfers: this transfers must be handled inside the Executor and have a transferType of TransferNativeInExecutor.

  • Never do ERC20 token approvals. These are all handled by the TychoRouterV3.

  • Never assign to a state variable or perform any operation that writes to TychoRouterV3's storage.

  • Do not execute delegatecall. If a protocol makes it unavoidable, ensure the caller cannot control the delegatecall target — attacker-controlled targets enable arbitrary code execution within TychoRouterV3's context.

  • Avoid trusting data sent via callback. If necessary, call verifyCallback within handleCallback to confirm msg.sender is a valid pool from the expected protocol.

  • handleCallback's data argument contains raw ABI-encoded calldata that the executor must decode manually.

  • handleCallback's return value must be raw ABI-encoded return data that the executor encodes manually.

Security Model

Tycho maintains a Rust security model of TychoRouterV3 that simulates many swap-parameter combinations to find inputs that let a caller drain the router's assets. The model mirrors the router's Solidity logic and reports suspicious outcomes. During the V3 security assessment it surfaced several critical vulnerabilities, all of which the team fixed before launch. Adding new executors keeps this coverage up to date.

The model only covers executors that give the caller control over the called pool contract — for example, executors where the caller supplies the pool address. These executors present the highest risk and are the easiest to model. If your executor falls into this category, add it to the model so the simulation covers it.

To add your executor, edit model/src/model/executors.rs:

  1. Add a variant to the Executor enum and to the Executor::VARIANTS array.

  2. Implement the executor's behavior in the get_transfer_data, swap, and funds_expected_address match arms, mirroring your Solidity executor. For callback-based protocols, also implement get_callback_transfer_data and handle_callback.

Run the model with cargo run --release from the model crate (release mode is required — it is very slow otherwise). Run cargo doc --open for the full model documentation.

Testing

Each new integration must be thoroughly tested in both Rust and Solidity. This includes:

  • Unit tests for the SwapEncoder in Rust

  • Unit tests for the Executor in Solidity

  • Two key integration tests to verify the full swap flow: SwapEncoder to Executor integration test and a full TychoRouterV3 integration test

1. SwapEncoder ↔ Executor Integration Test

Verify that the calldata generated by the SwapEncoder is accepted by the corresponding Executor.

Use the helper functions:

  • write_calldata_to_file() in the encoding module (Rust)

  • loadCallDataFromFile() in the execution module (Solidity)

These helpers save and load the calldata to/from calldata.txt.

2. Full TychoRouterV3 Integration Test

  • In tests/protocol_integration_tests.rs, write a Rust test that encodes a single swap and saves the calldata using write_calldata_to_file().

  • In TychoRouterTestSetup, deploy your new executor and add it to executors list in deployExecutors.

  • Run the setup to retrieve your executor’s deployed address and add it to config/test_executor_addresses.json.

  • Create a new Solidity test contract that inherits from TychoRouterTestSetup. For example:

These tests ensure your integration works end-to-end within Tycho’s architecture.

Deploying and Whitelisting

Once your implementation is approved:

  1. Declare the executor's deployment by adding its contract name and constructor arguments to config/executor_deployments.json (keyed by chain and protocol) and listing its protocol in the deploy_protocols map in deploy-executors.js. The deploy script and the SDK test-fixture generation both read the constructor arguments from this config.

  2. Deploy the executor contract on the appropriate network (more here).

  3. Contact us to whitelist the new executor address on our main router contract.

  4. Update the configuration by adding the new executor address to executor_addresses.json and register the SwapEncoder within the SwapEncoderBuilder .

By following these steps, your protocol will be fully integrated with Tycho.

Last updated

Was this helpful?