arrow-left

Only this pageAll pages
gitbookPowered by GitBook
1 of 51

Tycho

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

For Solvers

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

For DEXs

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Motivation

Tycho indexes on-chain liquidity, with a current focus on token swaps. Future development can include other liquidity provisioning, lending, and derivatives.

hashtag
The DeFi Fragmentation Challenge

The rapid innovation in DeFi protocols has created a fragmented ecosystem without standardized interfaces for fundamental operations like swaps, liquidity provisioning, etc.

Tycho aims to provide a standardized interface across those operations.

With a focus on fast local simulations on top of the latest known state of the chain and settlements through tycho-execution.

hashtag
Key Challenges in Liquidity Indexing

Before Tycho, you might face the following issues if you want to settle on onchain protocols:

hashtag
Technical Complexity

  • Rewrite protocol-specific mathematics in your application programming language to simulate fast locally.

  • Develop protocol-specific indexing to supply data for local simulations.

  • Watch and filter out user-created token pairs with unusual or malicious behavior.

hashtag
Blockchain-Specific Issues

  • Chain reorganizations ("reorgs") that alter transaction history must be handled with care.

  • Block propagation delays caused by peer-to-peer network topology and geographic distribution.

  • Continuous maintenance of node infrastructure, such as updating client versions (especially during hard forks), updating storage space, etc.

hashtag
Push-Based Architecture

hashtag
Problems with Traditional RPC Polling

Traditional indexers rely on node client RPC interfaces, which have significant limitations:

  • Data must be requested from nodes, introducing latency and potential for error.

  • Multiple requests are often needed to assemble a complete view of the data.

  • Complex query contracts may be required for comprehensive data extraction (e.g., to get all Uniswap V3 ticks) whose execution adds additional latency to data retrieval.

hashtag
The Streaming Solution

Tycho adopts a fundamentally different approach:

  • Data is pushed/streamed as a block is processed by the node client.

  • Current implementation leverages Substreams as the primary data source.

  • Alternative data sources can be integrated if they provide comparable richness.

hashtag
User Experience Philosophy

hashtag
Abstraction by Default

  • Non-blockchain-native users shouldn't need to understand chain-specific concepts.

  • Reorgs and optimistic state changes remain invisible to users by default.

  • Users perceive only that state has changed, regardless of underlying mechanism.

hashtag
Optional Transparency

  • Advanced users can access detailed information about state changes when needed.

  • Granular visibility allows inspection of upcoming state changes.

  • Applications can track specific liquidity pair changes for specialized use cases.

About

Overview of Tycho, its components and how to get started.

hashtag
What is Tycho?

Tycho is an open-source interface to on-chain liquidity. Tycho

  • Indexes DEX protocol state for you with low latency,

Transparency

Tycho funding and governance.

hashtag
Tycho is free and open source

Tycho is, and always will be, free software, open source, and MIT licensed.

Free hosted indexer: Today, we host a free indexer for each chain, and we aim to maintain this indefinitely. If this ever changes, you will know at least 3 months ahead of time and have enough time to host your own indexer.

Navigate an enormous search space of liquidity sources with effective filtering heuristics.
Load-balanced RPC endpoints can expose inconsistent state views during reorgs, making it hard to scale across many node clients.
  • May involve maintaining and running multiple instances of modified node clients.

  • State changes are communicated to clients through streaming interfaces.
    hashtag
    Indexer Redundancy

    We are working with independent third parties to host additional indexers, both free and paid – to add redundancy to our indexer.

    hashtag
    Tycho's Financial Model

    Tycho finances development and maintenance through:

    • Grants: Tycho receives grants from chains, DEXs, and others that benefit from the flow that Tycho brings.

    • Tools built on Tycho: Useful tooling built on Tycho with a fee model (Coming soon).

  • Simulates swaps extremely fast with one interface for all DEXs, and

  • Executes swaps on-chain

  • hashtag
    Ending the integration nightmare

    Integrations are the largest point of friction for both DEXs and Solvers:

    • Solvers can't scale integrations. So, Solvers spend much of their time on integrations, and new solvers can't catch up and compete.

    • DEXs need to convince solvers to integrate them to get orderflow and win LPs. But Solvers prioritize DEXs with liquidity. This makes it hard for new DEXs to get the flow their design deserves.

    In the end, every solver separately integrates every DEX – leading to massive wasted effort from which no one benefits.

    Tycho fixes this:

    • DEXs can integrate themselves and don't need to wait for solvers, and

    • Solvers can use new DEXs without any additional effort.

    Tycho lowers barriers to entry so that both innovative DEXs and Solvers have a chance to win flow.

    hashtag
    Get started

    hashtag
    Solvers – Access more liquidity

    Tycho makes it easy to simulate and execute over on-chain liquidity sources – without needing to understand protocol internals, run nodes, or do RPC calls.

    To set up, go to the Tycho Indexer quickstart and start your liquidity stream.

    hashtag
    DEXs – Get more flow

    To integrate your DEX, submit a PR to Tycho Protocol Integrations on GitHubarrow-up-right.

    To get started, check the Protocol SDK docs.

    Or contact our teamarrow-up-right so we can help you integrate.

    hashtag
    Components of Tycho

    Tycho has three components for solvers:

    • Tycho Indexer: Infrastructure to parse, store and stream protocol state deltas. It also comes with clients in Python and Rust and a hosted webstream if you don't want to run your version of the Indexer. -> Indexer docs.

    • Tycho Protocol Simulation: A simulation library with a unified interface to query swaps and prices. Optimized for speed, running on compiled contracts in REVM with in-memory storage. -> Protocol Simulation docs.

    • Tycho Execution: Audited and gas-efficient router and DEX executor contracts for safe, simple, and competitive execution of swaps.

    And one integration SDK for DEXs:

    • Tycho Protocol Integration: An SDK for any DEX (or Stable Coin, LRT, etc.) to integrate their liquidity and receive flow from solvers.

    hashtag
    FAQ

    chevron-rightHow does Tycho compare to just parsing logs myself?hashtag

    While you can parse logs directly, Tycho provides parsed, and structured data, offers a unified interface across protocols, manages reorgs automatically, can handle protocols that don't emit logs and saves you the infrastructure cost of running a node.

    chevron-rightDoes this add gas to my swaps?hashtag

    No it does not. Tycho contracts make it easy to simulate a DEX correctly for your swaps. But you can still execute the swaps directly with the DEX – as gas efficient as possible.

    chevron-rightHow do you handle reorgs?hashtag

    Reorgs are handled automatically through our delta system. The client maintains the correct state without you having to worry about block reorganizations.

    chevron-rightHow does latency compare to other solutions?hashtag

    Tycho processes updates in under 100ms (plus network latency). While an additional hop compared to running your own nodes, geographically distributed nodes race to provide data, which can be faster than relying on a single node.

    chevron-rightCan I still use my own UniV2/V3 implementations?hashtag

    Yes! Many teams use Tycho VM for newer/complex protocols while keeping their analytical implementations for simpler pools.

    chevron-rightWhat about UniV4 hooks?hashtag

    We aim to support as many hooks variants as possible through our VM implementation.

    chevron-rightWhat's the difference between Native and VM implementations?hashtag

    Native implementations provide protocol-specific state variables directly, letting you implement your own math and optimizations. VM implementations provide a unified interface through REVM, making integration easier but with less low-level control. Choose based on your needs. Native for maximum control, VM for easier integration.

    chevron-rightHow reliable is the state data?hashtag

    The system handles reorgs automatically, keeps track of TVL changes, and maintains consistency across state updates. The data is synced against on-chain state and continuously validated.

    How to Contribute

    Tycho is a community project that helps DEXs and Solvers coordinate.

    hashtag
    What you can contribute

    A quick overview, there are three ways to contribute to Tycho:

    • Pick an issue: Find issues to contribute to in the – or propose an issue for a new feature.

    • Build an app: Build an app using Tycho. Use the as an inspiration.

    hashtag
    How to get started

    Whichever way you choose to contribute – Tycho maintainers and the community are here to help you. Before you get started:

    1. Join – our telegram group for Tycho builders.

    2. Reach out to - so that he can support you and ensure someone else isn't already working on the same project.

    Tycho issue trackerarrow-up-right
    specifications in Tycho Xarrow-up-right
    tycho.buildarrow-up-right
    Tanayarrow-up-right

    Tycho Fellows

    Tycho Fellows are Solvers, Searchers, and Market Makers who work with the Propeller Foundation to bring all DeFi liquidity (across chains, pools, and tokens) to all traders.

    hashtag
    For Solvers & Searchers

    hashtag
    Benefits as a Tycho Fellow

    • Early Access to DEXs: Be first to access new DEXs on our development endpoint before they goes live in production.

    • Shape the future of Tycho: Give input and prioritize new features.

    hashtag
    Requirements

    As a Tycho Fellow, you need to:

    • Integrate new DEXs: Test and integrate new DEXs as soon as they are live.

    • Settle Volume on Tycho: Settle more than > 1mio / week on Tycho Router.

    If you want to become a Tycho Fellow please contact @tanay_j on Telegram.

    hashtag
    For DEXs, Auctions and Chains

    Tycho Fellows can connect your DEX to most orderflow in defi.

    For DEXs: Tycho Fellows integrate with all orderflow auctions and can quickly connect your DEX to most defi orderflow.

    For Auctions: Tycho Fellows can serve as a competitive and diverse set of solvers in your auction. Get top quotes, competitive solutions, and optimize revenue from day 0.

    For Chains: Help bring orderflow auctions and their users to your chain with the help of the solvers that power them.

    Write us (@tanay_j on tg) before you launch and we'll help you start ahead.

    Native Token Handling (Wrapping & Unwrapping)

    Wrapping and unwrapping use a dedicated WETH executor, the same mechanism as any other protocol swap. The encoding library automatically inserts WETH wrap/unwrap swaps wherever an ETH/WETH bridge is needed:

    • At the start: if token_in is ETH but the first swap expects WETH, a wrapping swap is prepended.

    • Between swaps: if one swap outputs ETH and the next expects WETH (or vice versa), a bridging swap is inserted.

    • At the end: if the last swap outputs ETH but token_out is WETH (or vice versa), an unwrapping swap is appended.

    Set token_in and token_out to the actual tokens the user holds and expects to receive (native ETH or WETH), and the encoder handles the rest.

    This approach supports wrapping/unwrapping at any position in the swap path (not just first and last) and works with protocols like Uniswap V4 that support native ETH swaps directly.

    Native Token Handling

    Tycho uses a specific address convention for native tokens that differs from some protocols.

    hashtag
    Address Convention

    Tycho reserves the zero address (0x0000000000000000000000000000000000000000) to represent the native token across all chains.

    Some protocols use 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE or other sentinel addresses for native tokens. If your protocol follows this pattern, you must normalize these addresses to the zero address in your Substreams package.

    Concepts

    Commonly used entities and concepts within Tycho.

    This outlines the core entities and components that form the foundation of the Tycho system. Understanding these concepts is essential for working with or on the application effectively.

    hashtag
    Entities

    hashtag
    ProtocolSystem

    With ProtocolSystems we usually refer to a DeFi protocol. A group of smart contracts that work collectively provide financial services to users. Each protocol typically contains:

    • A single Extractor (see below)

    • One or more ProtocolComponents

    We model major versions of protocols as distinct entities. For example, Uniswap V2 and Uniswap V3 are separate ProtocolSystems.

    Attributes:

    • name: The protocols' identifier

    • protocol_type: The category of protocol being indexed, currently pure organisational use.

      • name: The identifier of the protocol type

    hashtag
    Token

    Tokens represent fungible tradeable assets on a blockchain. Users interact with protocols primarily to buy, sell, or provide liquidity for tokens. While ERC20 is the most common standard, Tycho supports other token types as well.

    Tycho automatically detects and ingests new tokens when a ProtocolComponent using that token is ingested in the DB. Upon detection, we run test transactions to determine the token's behavior.

    Attributes:

    • Address: The blockchain address that uniquely identifies the token

    • Decimals: Number of decimal places used to represent token values

    • Symbol: Short human-readable identifier (e.g., ETH, USDC)

    hashtag
    ProtocolComponent

    ProtocolComponents represent specific operations that can be executed on token sets within a ProtocolSystem. Examples include liquidity pools in DEXes or lending markets in lending protocols.

    A new ProtocolComponent is created whenever a new operation becomes available for a set of tokens such as when a new trading pair is deployed on a DEX.

    Attributes:

    • id: A unique identifier for the component

    • protocol_system: The parent protocol system

    • protocol_type_name: Subtype classification for filtering components

    Each component also has dynamic attributes that change over time and contain state required to simulate operations.

    hashtag
    Indexer

    The indexer subsystem processes blockchain data, maintains an up-to-date representation of entities and provides RPC and Websocket endpoints exposing those entities to clients.

    hashtag
    Extractor

    An Extractor processes incoming blockchain data, either at the block level or at shorter intervals (e.g. mempool data or partial blocks from builders).

    The Extractor:

    1. Pushes finalized state changes to permanent storage

    2. Stores unfinalized data in system buffers (see ReorgBuffers below)

    3. Performs basic validation, such as checking for the existence of related entities and verifying the connectedness of incoming data

    hashtag
    Versioning

    Tycho's persistence layer tracks state changes at the transaction level. This granular versioning enables future use cases such as:

    • Replay changes transaction by transaction for backtesting

    • Historical analysis of protocol behavior

    The default storage backend (PostgreSQL) maintains versioned data up to a configurable time horizon. Older changes are pruned to conserve storage space and maintain query performance.

    circle-info

    While the system supports versioning, alternative persistence implementations aren't required to implement this feature.

    hashtag
    Reorg Buffer

    ReorgBuffers store unfinalized blockchain state changes that haven't yet reached sufficient confirmation depth.

    This approach allows Tycho to:

    1. Respond to queries with the latest state by merging buffer data with permanent storage

    2. Handle chain reorganizations by rolling back unconfirmed changes

    3. Send precise correction messages to clients when previously reported states are invalidated

    When a reorganization occurs, the system uses these buffers to calculate exactly what data needs correction, minimizing disruption to connected applications.

    hashtag
    Dynamic Contract Indexing (DCI)

    The DCI is an extractor extension designed to dynamically identify and index dependency contracts based on supplied tracing information. The DCI relies on an integrated protocol to provide the information with which it can analyse and detect contracts that require indexing.

    On a successful trace, the DCI identifies all external contracts that were called, which storage slots were accessed for those contracts, and potential retriggers for the entry point. A retrigger is any contract storage slot that is flagged for its potential to influence a trace result. If a retrigger slot is updated, the trace is repeated. For all identified contracts, the code and relevant storage is fetched at the current block. Thereafter, updates for those contracts are extracted from the block messages themselves.

    hashtag
    Simulation

    The simulation library allows clients to locally compute the outcome of potential operations without executing them on-chain, enabling efficient price discovery and impact analysis.

    hashtag
    Virtual Machine (VM) vs Native (Custom)

    Tycho offers two approaches for simulating protocol operations:

    Virtual Machine (VM) Integration

    • Uses the blockchain's VM to execute operations

    • Requires a contract that adapts the protocol's interface to Tycho's interface

    • Creates a minimal local blockchain view with only the necessary contract state

    Native Implementation

    • Reimplements protocol operations directly in Rust code

    • Compiles to optimized machine code for the target architecture

    • May still access the VM if required, e.g. to simulate Uniswap V4 hooks

    hashtag
    Execution

    hashtag
    Solution

    The Solution represents a complete pathway for moving tokens through one or more protocols to fulfil a trade. It bridges the gap between finding the best trade route and actually executing it on-chain.

    Solutions can represent single-hop swaps, sequential multi-hop trades, or split routes where a token amount is distributed across multiple pools. See more about Solutions .

    hashtag
    Strategy

    Strategies define how Solutions are translated into Transactions, offering different tradeoffs between complexity, gas efficiency, and security. They encapsulate the logic for how trades should be executed on-chain.

    Tycho currently supports three distinct strategies for executing trades: Single, Sequential, and Split.

    Before diving into these, it is useful to clarify a few terms:

    • Solution / Trade: A complete plan to exchange token A for token B. This may involve routing through intermediate tokens, but it is conceptually treated as a single trade.

    • Swap/Hop: An individual exchange between two tokens. A trade may consist of one or more swaps.

    hashtag
    Single

    The encoder uses the single strategy when a Solution has exactly one swap on one pool.

    hashtag
    Sequential

    The encoder uses the sequential strategy when your Solution has multiple sequential swaps, and no splits (e.g. A β†’ B β†’ C). Outputs from one are equal to the input to the next swap.

    hashtag
    Split

    With the Split strategy, you can encode the most advanced solutions: Trades that involve multiple swaps, where you split amounts either in parallel paths or within stages of a multi-hop route.

    For more about split swaps, see .

    Tycho RPC

    Tycho exposes data through two mechanisms, the RPC and the stream. The RPC provides you access to static data, like the state of a component at a given block or extended information about the tokens it has found. For streaming data, we recommend using the Tycho Client. This guide documents the RPC interfaces.

    hashtag
    Token Information

    Tycho stream provides only the token addresses that Protocol Components use. If you require more token information, you can request using Tycho RPC's Tycho RPCendpoint. This service allows filtering by both quality and activity.

    hashtag
    Quality Token Quality Ratings

    The quality rating system helps you quickly assess token's specific properties:

    • 100: Normal ERC-20 Token behavior

    • 75: Rebasing token

    • 50: Fee-on-transfer token

    circle-info

    The Token Quality Analysis was developed to aid Tycho Simulation in filtering out tokens that behave differently from standard ERC-20 Tokens. The analysis is under constant improvement and can provide wrong information.

    hashtag
    API Documentation

    This section documents Tycho's RPC API. Full swagger docs are available at:

    Tracking Components

    circle-info

    Note: this implementation pattern is, by default, used in the template.

    If protocols use factories to deploy components, a common pattern used during indexing is to detect the creation of these new components and store their contract addresses to track them downstream. Later, you might need to emit balance and state changes based on the current set of tracked components.

    hashtag

    Protocol Integration

    is a library to help you integrate liquidity layer protocols (DEXs, Staking, Lending, etc.) into Tycho.

    hashtag
    Integration Process

    To integrate with Tycho, you need three components:

    1. Setup

    hashtag
    Install Rust

    Install . You can do so with the following command:

    hashtag
    Install Substreams

    Indexer

    Stream real-time onchain liquidity data

    Tycho Indexer gives you a low-latency, reorg-aware stream of all attributes you need to simulate swaps over DEX and other onchain liquidity.

    hashtag
    Native and VM indexing

    Tycho can track protocols in two ways:

    Executing

    Once you have calldata from , you can execute your trade via the Tycho Router.

    hashtag
    Tycho Router

    Send the encoded calldata to the TychoRouter (see contract addresses ). Setup depends on the user_transfer_type in your Solution:

    Code Architecture

    Tycho Execution offers an encoding tool (a Rust crate for generating swap calldata) and execution components (Solidity contracts). This is how everything works together.

    The following diagram summarizes the code architecture:

    hashtag
    Encoding

    The TychoRouterEncoder validates solutions and produces a list of transactions to execute against the TychoRouter

    Implementation Steps
    1. Implement logic to identify newly created components. A recommended approach is to create a factory.rs module to facilitate the detection of newly deployed components.

    2. Use the logic/helper module from step 1 in a map handler that consumes substreams_ethereum::pb::eth::v2::Block models and outputs a message containing all available information about the component at the time of creation, along with the transaction that deployed it. The recommended output model for this initial handler is BlockTransactionProtocolComponentsarrow-up-right. Note that a single transaction may create multiple components. In such cases, TransactionProtocolComponents.components should list all newly created ProtocolComponents.

    3. After emitting, store the protocol components in a Store. This you will use later in the module to detect relevant balance changes and to determine whether a contract is relevant for tracking.

    triangle-exclamation

    Emitting state or balance changes for components not previously registered/stored is considered an error.

    ethereum-template-factoryarrow-up-right
    Indexing: You must provide the protocol state/data needed for simulation and execution.
  • Simulation: You have to implement the protocol's logic for simulations.

  • Execution: You have to define how to encode and execute swaps against your protocol.

  • We provide a comprehensive testing suite to ensure you can integrate indexing, simulation, and execution correctly. A passing test suite is essential for an integration to be considered complete.

    hashtag
    Indexing

    You will need a substreamsarrow-up-right package that emits a specified set of messages. If your protocol already has a substreams packagearrow-up-right, you can adjust it to emit the required messages.

    It's important to note that simulation happens entirely off-chain. This means everything you need during simulation must be explicitly indexed.

    hashtag
    Simulation

    Tycho offers two integration modes:

    • VM Integration: You need to implement an adapter interface in a language that compiles to VM bytecode. This SDK provides a Solidity interface (read more here). Simulations run in an empty VM loaded only with the indexed contracts, storage and token balances.

    • Native Rust Integration: You need to implement a Rust trait that defines the protocol logic. You must index values used in this logic as state attributes.

    hashtag
    Execution

    To enable swap execution, implement:

    1. SwapEncoder: This is a Rust struct that formats input/output tokens, pool addresses, and other parameters correctly for the Executor contract.

    2. Executor: This is a Solidity contract that handles the execution of swaps over your protocol's liquidity pools.

    hashtag
    Integration Criteria

    Tycho supports many protocol designs. However, certain architectures present indexing challenges.

    Before you integrate, consider these unsupported designs:

    • Protocols where any operation that Tycho should support requires off-chain data, such as signed prices.

    Tycho Protocol SDK arrow-up-right

    TransferFrom: Approve the TychoRouter to spend your input token via approve() before submitting the transaction.

  • TransferFromPermit2: Approve the Permit2 contract, then create and sign the permit yourself. Use the public Permit2 utility from the encoding crate to build the PermitSingle. The encoder does not produce the permit β€” you handle this externally.

  • UseVaultsFunds: No approval or transfer needed. The router draws from your pre-deposited vault balance. Ensure you have deposited sufficient funds.

  • For an example of how to execute trades using the Tycho Router, refer to the Quickstart.

    hashtag
    Fee Taking

    The TychoRouter V3 supports a dual fee system:

    • Client fees: Set client_fee_bps and client_fee_receiver in the Solution to charge a percentage of the output amount. Fees are credited to the receiver's vault balance.

    • Router fees: Configured on-chain by Propeller Heads. These are mandatory and cannot be bypassed through encoding. The router can charge a fee on the output amount and/or a percentage of the client fee.

    hashtag
    Client Contribution (Slippage Subsidy)

    If the swap output falls below min_amount_out, the router can draw from the client's vault balance (up to max_client_contribution) to cover the difference. If the shortfall exceeds max_client_contribution, the transaction reverts. This lets solvers subsidize slippage-affected trades without a separate transaction. Be careful when setting max_client_contribution; a value exceeding the cost of a separate on-chain transaction may expose you to sandwich attacks.

    Encoding
    contractarrow-up-right
    here

    financial_type: The specific financial service provided:

    • Swap

    • PSM

    • Debt

    • Leverage

  • attribute_schema: Currently unused; initially intended to validate static and hybrid attributes.

  • implementation_type: Either VM or Custom (native - see below)

  • Tax: Token transfer tax in basis points, averaged across simulated transfers
  • Gas: Cost to transfer the token in the blockchain's native compute units

  • Chain: The blockchain where the token is deployed

  • Quality: Score from 0-100 indicating token reliability:

    • 100: Standard token with normal behavior

    • 75: Rebase token (supply adjusts automatically)

    • 50: Fee token (charges fees on transfers)

    • 10: Failed initial token analysis

    • 9-5: Failed subsequent analysis after creation

    • 0: Could not extract decimals from on-chain data

  • chain: Blockchain where the component operates
  • tokens: Addresses of tokens this component works with

  • contract_addresses: Smart contracts involved in executing operations (may be empty for native implementations)

  • static_attributes: Constant properties known at creation time, including:

    • Attributes used to filter components (e.g. RPC and/or DB queries)

    • Parameters needed to execute operations (fees, factory addresses, pool keys)

  • creation_tx: Transaction hash that created this component

  • created_at: Timestamp of component creation

  • Aggregates processed changes and broadcasts them to connected clients
  • Handles chain reorganizations by reverting changes in buffers and sending correction messages to clients

  • Advantages:
    • Faster integration of new protocols

    • No need to reimplement complex protocol math

  • Disadvantages:

    • Significantly slower simulation compared to native implementations

  • Advantages:
    • Much faster simulation performance

    • More efficient for high-volume protocols

  • Disadvantages:

    • Longer integration time

    • Requires comprehensive understanding of protocol mathematics

    • Must identify and index all relevant state variables

  • here
    here
    Diagram representing examples of the multiple types of solutions

    For Native Simulation: Tycho gives structured data that mirrors on-chain states, so you can simulate protocol logic outside the VM (e.g. in your own Rust rewrite of Uni v2 swap function). Useful for example if you solve analytically over the trading curves.

  • Virtual Machine (VM) Compatibility: Tycho tracks the state of all protocol contracts so you can simulate calls over it with no network overhead (locally on revm). Used by Protocol Simulation to simulate key protocol functions (swap, price, derivatives etc.).

  • Native integrations are more effort, but run faster (~1-5 microseconds or less per simulation), VM integrations are easier to do but run slower (~100–1000 microseconds per simulation).

    hashtag
    What Makes Tycho Unique?

    • Complete Protocol Systems: Tycho doesn’t just track standalone data; it indexes whole systems, like Uniswap pools or Balancer components, even detecting new elements as they’re created.

    • Detailed Component Data: For each tracked protocol component, Tycho records not just static values (like fees or token pairs) but also dynamic state changes, ensuring you have all you need to replicate the onchain state.

    hashtag
    Leveraging Substreams

    Tycho Indexer leverages Substreams, a robust and scalable indexing framework by StreamingFast.

    While Tycho currently uses Substreams to deliver high-performance indexing, our architecture is designed to be flexible, supporting future integrations with other data sources.

    hashtag
    A Simple Setup

    Setting up using Tycho is simple with the tycho client.

    Available as a CLI binary, rust crate, or python package.

    .

    The TychoRouterEncoder uses a StrategyEncoder that it chooses automatically based on the solution (see more about strategies here).

    Internally, all encoders choose the appropriate SwapEncoder(s) to encode the individual swaps, which depend on the protocols used in the solution.

    hashtag
    Execution

    The TychoRouter calls one or more Executors (corresponding with the output of the SwapEncoders) to interact with the correct protocol and perform each swap of the solution. The TychoRouter verifies that the user receives a minimum amount of the output token.

    Custom protobuf models

    In some cases, you may need to create custom intermediate protobuf messages, especially when facilitating communication between Substreams handler modules or storing additional data in stores.

    Place these protobuf files within your Substreams package, such as ./substreams/ethereum-template/proto/custom-messages.proto. Be sure to link them in the substreams.yaml file. For more details, refer to the substreams manifest documentationarrow-up-right or review the official Substreams UniswapV2arrow-up-right example integration.

    You can do so with any of the following:

    hashtag
    Using Homebrew:

    hashtag
    Using precompiled binaries

    hashtag
    Compiling from source:

    hashtag
    Install Buf

    hashtag
    Using Homebrew:

    For other installation methods, see the official buf websitearrow-up-right\

    hashtag
    Fork the SDK repo

    1. Start by making a fork of the Tycho Protocol SDKarrow-up-right repository

    2. Clone the fork you just created

    3. Make sure everything compiles fine

    Rustarrow-up-right
    cd substreams
    cargo check --all
    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
    brew install streamingfast/tap/substreams
    # Use correct binary for your platform
    LINK=$(curl -s https://api.github.com/repos/streamingfast/substreams/releases/latest | awk '/download.url.*linux/ {print $2}' | sed 's/"//g')
    curl -L  $LINK  | tar zxf -
    git clone https://github.com/streamingfast/substreams
    cd substreams
    go install -v ./cmd/substreams
    brew install bufbuild/buf/buf
    10: Token analysis failed at first detection
  • 5: Token analysis failed multiple times (after creation)

  • 0: Failed to extract attributes, like Decimal or Symbol

  • https://tycho-beta.propellerheads.xyz/docs/arrow-up-right

    Quickstart

    How to swap on-chain with Tycho. This quickstart will help you:

    • Fetch real-time market data from Tycho Indexer.

    • Simulate swaps between token pairs. This lets you calculate spot prices and output amounts using Tycho Simulation.

    • Encode the best trade for given token pairs.

    • Simulate or execute the best trade using Tycho Execution.

    circle-check

    Want to chat with our docs? Download an LLM-friendly .

    hashtag
    Run the Quickstart

    Clone the ; here's code.

    Run the quickstart with execution using the following commands:

    If you don't have an RPC URL, here are some public ones for , , and .

    The PRIVATE_KEY environment variable is unnecessary if you want to run the quickstart without simulation or execution.

    hashtag
    What it does

    The quickstart fetches all protocol states. Then it returns the best amount out (best price) for a given token pair (by default, 10 USDC to WETH).

    Additionally, it returns calldata to execute the swap on this pool with the Tycho Router.

    You should see an output like this:

    If you want to see results for a different token, amount, or chain, or minimum TVL, you can set additional flags:

    This example would seek the best swap for 10 USDC -> WETH on Base.

    The TVL filter means we will only look for snapshot data for pools with TVL greater than the specified threshold (in ETH). Its default is 1000 ETH to limit the data you pull.

    hashtag
    Logs

    If you want to see all the Tycho Indexer and Simulation logs, run with RUST_LOG=info:

    hashtag
    How the quickstart works

    The quickstart shows you how to:

    1. Set up and load necessary data, like available tokens.

    2. Connect to the Tycho Indexer to fetch on-chain protocol data (e.g., Uniswap V2, Balancer V2) and build a Protocol Stream that streams updates, like new pools and states, in real-time.

    3. Simulate swaps on all available pools for a specified pair (e.g., USDC, WETH), and print out the most WETH available for 10 USDC.

    hashtag
    1. Set up

    Run Tycho Indexer by setting up the following environment variables:

    • TYCHO_URL (by default "tycho-beta.propellerheads.xyz")

    • TYCHO_API_KEY key

    • PRIVATE_KEY if you wish to execute the swap against the Tycho Router

    The Indexer stream or the Simulation does not manage tokens; you manage them yourself.

    To simplify this, gets all current token information from Tycho Indexer RPC for you.

    hashtag
    2. Connect to Tycho Indexer

    The protocol stream connects to Tycho Indexer to fetch the real-time state of protocols.

    Here, you only subscribe to Uniswap V2 and Balancer V2. To include additional protocols like Uniswap V3, simply add:

    For a full list of supported protocols and which simulation state (like UniswapV3State) they use, see .

    Note: The protocol stream supplies all protocol states in the first BlockUpdate object. All subsequent BlockUpdates contain only new and changed protocol states (i.e., deltas).

    hashtag
    3. Simulate swap

    get_best_swap uses Tycho Simulation to simulate swaps and calculate buy amounts. We inspect all protocols updated in the current block (i.e., protocols with balance changes).

    hashtag
    a. Simulating token swaps

    result is a GetAmountOutResult containing information on amount out, gas cost, and the protocol's new state. So you could follow your current swap with another.

    By inspecting each of the amount outs, you can then choose the protocol component with the highest amount out.

    hashtag
    4. Encode a swap

    After choosing the best swap, you can use Tycho Execution to encode it.

    hashtag
    a. Create a solution object

    Now you know the best protocol component (i.e., pool), you can compute a minimum amount out. And you can put the swap into the expected input format for your encoder.

    The minimum amount out is a very important parameter to set in Tycho Execution. The value acts as a guardrail and protects your funds during execution against MEV. This quickstart accepts a slippage of 0.25% over the simulated amount out.

    circle-exclamation

    For maximum security, you should determine the minimum amount from a third-party source.

    After this, you can create the Swap and Solution objects. For more info about the Swap and Solution models, see .

    hashtag
    b. Encode solution

    hashtag
    5. Encode full method calldata

    You need to build the full calldata for the router. Tycho handles the swap encoding, but you control the full input to the router method. This quickstart provides helper functions (encode_tycho_router_call and sign_permit)

    Use it as follows:

    triangle-exclamation

    ⚠️ These functions are only examples intended for use within the quickstart. Do not use them in production. You must write your own logic to:

    • Control parameters like minAmountOut and receiver .

    hashtag
    6. Simulate or execute the best swap

    This step allows you to test or perform real transactions based on the best available swap options. For this step, you need to pass your wallet's private key in the run command. Handle it securely and never expose it publicly.

    When you provide your private key, the quickstart will check your token balances and display them before showing you options:

    If you don't have enough tokens for the swap, you'll see a warning:

    You'll then encounter the following prompt:

    You have three options:

    1. Simulate the swap: Tests the swap without executing it on-chain. It simulates an approval (for permit2) and a swap transaction on the node. You'll see something like this:

    If status is false, the simulation has failed. You can print the full simulation output for detailed failure information.

    1. Execute the swap: Performs the swap on-chain using your real funds. The process performs an approval (for permit2) and a swap transaction. You'll receive transaction hashes and statuses like this:

    After a successful execution, the program will exit. If the transaction fails, the program continues to stream new blocks.

    1. Skip this swap: Ignores this swap. Then the program resumes listening for blocks.

    circle-exclamation

    Important Note

    Market conditions can change rapidly. Delays in your decision-making can lead to transaction reverts, especially if you've set parameters like minimum amount out or slippage. Always ensure you're comfortable with the potential risks before executing swaps.

    hashtag
    Recap

    In this quickstart, you explored how to use Tycho to:

    1. Connect to the Tycho Indexer: Retrieve real-time protocol data filtered by TVL.

    2. Fetch Token and Pool Data: Load all token details and process protocol updates.

    3. Simulate Token Swaps: Compute the output amount, gas cost, and updated protocol state for a swap.

    hashtag
    What's next?

    • Integrate with your Solver: Add Tycho pool liquidity to your solver, using this .

    • Learn more about and the datatypes necessary to encode an execution against a Tycho router or executor.

    • Learn more about : Explore custom filters, protocol-specific simulations, and state transitions.

    Common Patterns & Problems

    Some protocol design choices follow a common pattern. Instructions on how to handle these cases are provided. Such cases include:

    • Factory contracts

    • Tracking contract storage [VM implementations]

    hashtag
    Factory contracts

    A common protocol design is to use factories to deploy components. In this case it is recommended to detect the creation of these components and store their contract addresses (an potentially other metadata) to track them for use later in the module. See

    hashtag
    Tracking contract storage

    For VM implementations it is essential that the contract code and storage of all involved contracts are tracked. If these contracts are known, static, and their creation event is observable by the substreams package (occurs after the start block of the package), they can be indexed by the substream package with some helpful utils: see .

    If these contracts need to be dynamically determined or their creation event is not observable, instead see below:

    hashtag
    Using the Dynamic Contract Indexer (DCI)

    For contracts that cannot be statically determined at time of integration or their creation events are not observable by the substreams package, Dynamic Contract Indexer (DCI) support is provided. Keep in mind using this feature adds indexing latency and should be avoided if possible.

    The DCI allows you to specify external contract call information, which it will use to trace and identify all contract dependencies. It then automates the indexing of those identified contracts and their relevant storage slots. See.

    hashtag
    Using relative component balances

    For some protocols, absolute component balances are not easily obtainable. Instead, balance deltas/changes are observed. Since absolute balances are expected by Tycho, it is recommended to use a balance store to track current balances and apply deltas as the occur. See .

    hashtag
    Vaults/Singleton contracts

    For protocols that store balances in an a-typical way (not on dedicated pool contracts), a special approach to balance tracking must be used. See.

    When a contract change is indexed, consumers of the indexed data typically trigger recalculating prices on all pools marked as associated with that contract (the contract is listed in the ProtocolComponent's contracts field). In the case where multiple components are linked to a single contract, such as a vault, this may cause excessive and unnecessary simulations on components that are unaffected by a specific change on the linked contract. In this case it is recommended to use 'manual update' triggers. See for more details.

    hashtag
    Persisting data between modules

    It is often the case where data needs to be persisted between modules in your substream package. This may be because components and their metadata (such as their tokens, or pool type) are needed when handling state changes downstream, or could be because the protocol reports relative changes instead of absolute values and the relative changes must be compounded to reach an absolute value. For this, substream and are recommended.

    Normalizing relative ERC20 Balances

    Tracking balances is complex if only relative values are available. If the protocol provides absolute balances (e.g., through logs), you can skip this section and simply emit the absolute balances.

    To derive absolute balances from relative values, you’ll need to aggregate by component and token, ensuring that balance changes are tracked at the transaction level within each block.

    hashtag
    Implementation Steps:

    hashtag
    1. Index relative balance changes

    To accurately process each block and report balance changes, implement a handler that returns the BlockBalanceDeltas struct. Each BalanceDelta for a component-token pair must be assigned a strictly increasing ordinal to preserve transaction-level integrity. Incorrect ordinal sequencing can lead to inaccurate balance aggregation.

    Example interface for a handler that uses an integer, loaded from a store to indicate if a specific address is a component:

    Use the tycho_substream::balances::extract_balance_deltas_from_tx function from our Substreams SDK to extract BalanceDelta data from ERC20 Transfer events for a given transaction, as in the .

    hashtag
    2. Aggregate balances with an additive store

    To efficiently convert BlockBalanceDeltas messages into absolute values while preserving transaction granularity, use the StoreAddBigInt type with a store module. The tycho_substream::balances::store_balance_changes helper function simplifies this task.

    Typical usage of this function:

    hashtag
    3. Combine absolute values with component and address

    Finally, associate absolute balances with their corresponding transaction, component, and token. Use the tycho_substream::balances::aggregate_balances_changes helper function for the final aggregation step. This function outputs BalanceChange structs for each transaction, which can then be integrated into map_protocol_changes to retrieve absolute balance changes per transaction.

    Example usage:

    Each step ensures accurate tracking of balance changes, making it possible to reflect absolute values for components and tokens reliably.

    Execution

    Execute swaps through any protocol.

    triangle-exclamation

    Router V3 is still undergoing an audit. Use at your own discretion. Funds stored in the router (including vault deposits) might be lost.

    Tycho Execution provides tools for encoding and executing swaps against Tycho Router and protocol executors. It is divided into two main components:

    • Encoding: A Rust crate that encodes swaps and generates calldata for execution.

    • Executing: Solidity contracts for executing trades on-chain.

    The source code for Tycho Execution is available . For a practical example of its usage, please refer to our .

    hashtag
    Token transfers

    You can transfer tokens in one of three ways with Tycho Execution:

    • Permit2

    • Standard ERC20 Approvals

    • Using Vault funds

    See how to change between these options when encoding .

    hashtag
    Permit2

    Tycho Execution supports Permit2 for token approvals. Before executing a swap via our router, you must approve the Permit2 contract for the specified token and amount. This ensures the router has the necessary permissions to execute trades on your behalf.

    Permit2 handling is not part of the encoding step. You are responsible for creating and signing the permit yourself. The Permit2 utility struct is publicly exported from the encoding crate, so you can use it to build the PermitSingle and obtain the data needed for signing.

    For more details on Permit2 and how to use it, see the .

    hashtag
    Standard ERC20 Approvals

    Tycho also supports traditional ERC20 approvals. In this model, you explicitly call approve on the token contract to grant the router permission to transfer tokens on your behalf. This is widely supported and may be preferred in environments where Permit2 is not yet available.

    hashtag
    Using the Vault

    The TychoRouter includes a built-in vault () that lets you deposit, hold, and withdraw tokens directly in the router contract. The vault tracks per-user balances, so your tokens are only accessible by you.

    The router draws from your deposited balance instead of performing a transferFrom on your wallet. This saves gas (no approval or external transfer needed) and lets you use fees, proceeds from previous trades, or pre-positioned liquidity directly.

    Fees earned through the fee-taking system are automatically credited to the fee receiver's vault balance, making them immediately available for future swaps or withdrawals.

    More on the Vault .

    hashtag
    Security and Audits

    The Tycho Router V2 has been audited by . Past audits are .

    triangle-exclamation

    Router V3 is still undergoing an audit. Use at your own discretion. Funds stored in the router (including vault deposits) might be lost.

    If you discover potential security issues or have suggestions for improvements, please reach out through our official channels.

    Vault

    The TychoRouter V3 includes an integrated vaultarrow-up-right built on the ERC6909arrow-up-right multi-token standard. This replaces the "direct transfer" pattern from V2, where tokens sent to the router risked being lost.

    hashtag
    How It Works

    The vault uses dual storage:

    • Transient storage tracks balance changes ("deltas") during a swap. Credits are recorded when tokens arrive at the router; debits when they leave. This is cheap (~100 gas per operation) and automatically clears at the end of each transaction.

    • Persistent storage (ERC6909 balances) holds final user balances across transactions. These are updated only after a swap is fully validated.

    At the end of every swap, _finalizeBalances validates the transient state before committing:

    • For wallet-funded swaps: all deltas must net to zero.

    • For vault-funded swaps: at most one negative delta is allowed (the input token), which is burned from the user's vault balance.

    This catches encoding errors and balance mismatches before any persistent state changes.

    hashtag
    Depositing and Withdrawing

    Tokens in the vault can be used for swaps by setting user_transfer_type: UseVaultsFunds in the . They can be withdrawn at any time.

    hashtag
    Crediting Output to the Vault

    By default, output tokens are sent to the receiver address after a swap. If you set the receiver to the TychoRouter address, the output tokens are credited to the caller's vault balance instead of being transferred out.

    This works with all swap types β€” single, sequential, and split β€” and with both wallet-funded and vault-funded swaps.

    This enables vault rebalancing: converting one token to another without tokens leaving the contract. For example, a solver holding WETH in the vault can convert it to USDC in a single transaction, with both the debit and credit happening within the vault.

    It also supports cyclical arbitrage, where you route through multiple pools and end up with more of the starting token, all settled within the vault.

    hashtag
    Why a Vault?

    The vault serves three purposes:

    1. Gas savings for repeat users. Solvers and market makers can keep tokens in the contract, avoiding repeated approval and transfer costs.

    2. In-contract rebalancing. Convert between tokens in the vault without additional ERC-20 transfers or approvals, since both input and output stay in the router.

    3. Fee accounting. Client fees and router fees are credited directly to the receiver's vault balance. No ERC-20 transfers needed at fee-taking time, just a persistent storage write.

    hashtag
    Security Guarantees

    • Vault balances are scoped per user. Only the owner can use their funds (via msg.sender or signature checks). This is enforced at the contract level and cannot be overridden through encoding.

    • Output tokens credited to the vault always go to msg.sender, not to the receiver parameter. This prevents a malicious encoder from redirecting output into another user's vault.

    • Tokens sent to the router without calling

    Best Practices

    Some best practices we encourage on all integrations are:

    • Clear Documentation: Write clear, thorough comments. Good documentation:

      • Helps reviewers understand your logic and provide better feedback

      • Serves as a guide for future developers who may adapt your solutions

      • Explains why you made certain decisions, not just what they do

    • Module Organisation: For complex implementations it is recommended to:

      • Break large module.rs files into smaller, focused files

      • Place these files in a modules directory

    • Substream Initial Block: Your package will work just fine setting the initial block in your manifest file to 1, however it means anyone indexing your protocol has to wait for it to process an excessive number of unnecessary blocks before it reaches the first relevant block. This increases substream costs and causes long wait times for the protocol to reach the current block.

      A good rule of thumb is to identify the earliest deployed contract that you index and set this config to that block.

    • Performance Considerations:

      • Minimize use of .clone(), especially in loops or on complex/nested data structures. Instead use references (&) when possible.

    Tracking Contract Balances

    Sometimes the balances a component uses is stored on a contract that is not a dedicated single pool contract. During Tycho VM simulations, token contracts are mocked and any balances checked or used during a swap need to be overwritten for a simulation to succeed. Default behavior is for the component balances reported to be used to overwrite the pool contract balances. This assumes 2 things: there is a one-to-one relationship between contracts and components, and the hex-encoded contract address serves as the component ID.

    If a protocol deviates from this assumption, the balances for each appropriate contract needs to be tracked for that contract. All contracts that have their balances checked/accessed during a simulation need to be tracked in this way.

    hashtag
    Implementation Steps:

    1. Implement logic/a helper function to extract the absolute balances of the contract. This is protocol specific and might be obtained from an event, or extracted from a storage slot if an appropriate one is identified.

    2. Create an InterimContractChange for the contract and add the contract balances using upsert_token_balance.

    3. Add these contract changes to the appropriate TransactionChangesBuilder using add_contract_changes.

    An example for a protocol that uses a single vault contract is as follows:

    Request for Quote Protocols

    To add support for a new RFQ provider in Tycho, you’ll need to implement a client, a state, and the logic to encode and execute trades.

    The state, encoding, and execution logic for RFQs follow the same structure as on-chain protocol integrations. See our simulation and execution guides for details.

    We recommend using the existing Bebop integration as a reference.

    hashtag
    RFQClient

    Each RFQ protocol must implement the RFQClient trait:

    Responsibilities:

    • stream: Connects to the RFQ provider and emits real-time indicative price updates.

    • request_binding_quote: Sends an HTTP request to fetch a binding quote for a specific swap.

    You’ll also need to provide a builder to configure and construct your client, similar to BebopClientBuilder.

    hashtag
    State

    Each provider must define a state object that represents a full snapshot of their indicative prices.

    This state must implement:

    • ProtocolSim for simulation

    • TryFromWithBlock to decode incoming messages into a usable state

    Details on how to implement these can be found .

    hashtag
    Encoder + Executor

    To support execution, implement:

    • Encoder: Encodes the calldata to execute a swap on the RFQ via the Tycho Router. Be sure to request the binding quote here.

    • Executor: Executes the swap

    For more see .

    This allows the RFQ to be used in hybrid routes and benefit from Tycho’s execution optimizations.

    Binary / CLI

    Tycho Client CLI installation documentation

    hashtag
    When to use the binary client

    The binary client is recommended for 2 situations:

    • For a quick setup, to consume data from Tycho Indexer direct on a terminal

    Python Client

    A python package is available to ease integration into python-based projects. To install locally:

    hashtag
    Setup Guide

    hashtag
    Prerequisites

    Execution Venues

    How to integrate Tycho in different execution venues.

    hashtag
    Cow Protocol

    To solve orders on , you'll need to prepare your solution following specific formatting requirements.

    First, initialize the encoder:

    When solving for CoW Protocol, you need to return a that contains a list of interactions to be executed in sequence.

    Tracking Contract Storage

    circle-info

    This implementation pattern is, by default, used in both the and the templates.

    In VM implementations, accurately identifying and extracting relevant contract changes is essential.

    The tycho_substreams::contract::extract_contract_changes helper function simplifies this process significantly.

    circle-exclamation

    Name files clearly with numerical prefixes indicating execution order (e.g., 01_parse_events.rs, 02_process_data.rs)

  • Use the same number for parallel modules that depend on the same previous module

  • A good example of this done well is in the uniswap-v4 implementationarrow-up-right.

    Using the Dynamic Contract Indexer (DCI)
    Using relative component balances
    Vaults/singleton contracts
    Persisting data between modules
    Tracking Components.
    Tracking Contract Storage
    Using the Dynamic Contract Indexer
    Dynamic Contract Indexer
    Normalizing relative ERC20 Balances
    Tracking Contract Balances
    Reserved Attributes
    Storesarrow-up-right
    Custom Protobuf Models
    herearrow-up-right
    Quickstart
    here
    Permit2 official documentationarrow-up-right
    ERC6909arrow-up-right
    here
    Maximilian KrΓΌgerarrow-up-right
    herearrow-up-right

    Encode a swap of 10 USDC against the best pool.

  • Execute the swap against the Tycho Router.

  • Sign the permit2 object safely and correctly.

    This gives you full control over execution. And it protects you from MEV and slippage risks.

    Encode a Swap: Create a solution from the best pool state and retrieve calldata to execute against a Tycho router.
  • Execute a Swap: Execute the best trade using the Tycho Router.

  • Explore Tycho Indexer: Add or modify the data that Tycho indexes.

    text file of the full Tycho docsarrow-up-right
    Tycho Simulation repositoryarrow-up-right
    the quickstartarrow-up-right
    Ethereum Mainnetarrow-up-right
    Unichainarrow-up-right
    Basearrow-up-right
    load_all_tokens
    Supported Protocols
    here
    guide
    Tycho Execution
    Tycho Simulation
    unset HISTFILE # to not save your private key to your bash history
    export RPC_URL=https://ethereum.publicnode.com
    export PRIVATE_KEY=<your-private-key>
    cargo run --release --example quickstart --
    unset HISTFILE # to not save your private key to your bash history
    export RPC_URL=https://base-rpc.publicnode.com
    export PRIVATE_KEY=<your-private-key>
    cargo run --release --example quickstart -- --chain base
    unset HISTFILE # to not save your private key to your bash history
    export RPC_URL=https://unichain-rpc.publicnode.com
    export PRIVATE_KEY=<your-private-key>
    cargo run --release --example quickstart -- --chain unichain
    Curve implementationarrow-up-right
    deposit()
    are considered lost. The vault does not credit balances from raw transfers.
    Solution
    here
    here
    To solve with the Tycho Router you only need one custom interaction where:
    1. callData is the full encoded method calldata using the encoded solution returned from encoder.encode_solutions(...)

    2. allowances is a list with one entry where the allowance for the token in and amount in is set for spender to be the Tycho Router. This is necessary for the transferFrom to work.

    hashtag
    Uniswap X

    To help you fill Uniswap X orders using Tycho, we provide an example UniswapXFillerarrow-up-right contract. This contract is a starting pointβ€”you should adapt it to fit your use case.

    The example contract:

    • Inherits from IReactorCallback and implements execute and reactorCallback

    • Calls the TychoRouter from reactorCallback to execute swaps

    • Uses standard token approvals to allow TychoRouter to pull funds; you can replace this with Permit2 easily (you need to change the encoding accordingly though).

    • Approves the UniswapX Reactor contract to transfer tokens out after execution

    • Only supports solving one order at a time; you can extend it to support batching by implementing executeBatch and updating reactorCallback

    • Can safely hold tokens. The Uniswap X Reactor only transfers out the required amount. If your solution is more efficient, any surplus stays in the filler contract

    • Is not auditedβ€”use at your own risk

    See how to encode the callbackData for TychoRouter herearrow-up-right.

    chevron-rightHow to deploy the Uniswap X Fillerhashtag

    The current scriptarrow-up-right deploys an Uniswap X filler and verifies it in the corresponding blockchain explorer.

    Make sure to run unset HISTFILE in your terminal before setting the private key. This will prevent the private key from being stored in the shell history.

    1. Set the following environment variables:

    1. Confirm that the variables tychoRouter, uniswapXReactor and nativeToken are correctly set in the script. Make sure that the Uniswap X Reactor address matches the reactor you are targeting.

    2. Run npx hardhat run scripts/deploy-uniswap-x-filler.js --network NETWORK.

    For more on filling Uniswap X orders, see their docsarrow-up-right and examplesarrow-up-right.

    hashtag
    Other competition venues

    For other venues, like 1inch Fusion, please contact us.

    CoW Protocolarrow-up-right
    Solution objectarrow-up-right
  • Git

  • Rust 1.84.0 or later

  • Python 3.9 or above

  • hashtag
    Install the package

    hashtag
    Understanding and using the Python Client

    The Python client is a Python wrapper around our Rust Client that enables interaction with the Tycho Indexer. It provides two main functionalities:

    • Streaming Client: Python wrapper around Rust Client for real-time data streaming

    • RPC Client: Pure Python implementation for querying Tycho RPC data

    hashtag
    Streaming Implementation

    The TychoStream class:

    1. Locates the Rust binary (tycho-client-cli)

    2. Spawns the binary as a subprocess

    3. Configures it with parameters like URL, authentication, exchanges, and filters

    4. Implements an async iterator pattern that:

      • Reads JSON output from the binary's stdout

      • Parses messages into Pydantic models

      • Handles errors and process termination

    Here's one example on how to use it:

    hashtag
    RPC Client Implementation

    The TychoRPCClient class:

    • Makes HTTP requests to the Tycho RPC server

    • Serializes Python objects to JSON

    • Deserializes JSON responses to typed Pydantic models

    • Handles blockchain-specific data types like HexBytes

    Here's one example on how to use it to fetch tokens information (available at Tycho RPC endpoint):

    Note: These contract helper functions require the extended block model from substreams for your target chain.

    hashtag
    Factory protocols

    In factory-based protocols, each contract typically corresponds to a unique component, allowing its hex-encoded address to serve as the component ID, provided there is a one-to-one relationship between contracts and components.

    The example below shows how to use a component store to define a predicate. This predicate filters for contract addresses of interest:

    hashtag
    Other protocols

    For protocols where contracts aren't necessarily pools themselves, you'll need to identify specific contracts to track. These addresses can be:

    1. Hard-coded (for single-chain implementations)

    2. Configured via parameters in your substreams.yamlarrow-up-right file (for chain-agnostic implementations)

    3. Read from the storage of a known contract (hardcoded or configured)

    Here's how to extract changes for specific addresses using configuration parameters:

    ethereum-template-factoryarrow-up-right
    ethereum-template-singleton arrow-up-right
    Looking for pool with best price for 10 USDC -> WETH
    
    ==================== Received block 14222319 ====================
    
    The best swap (out of 6 possible pools) is:
    Protocol: "uniswap_v3"
    Pool address: "0x65081cb48d74a32e9ccfed75164b8c09972dbcf1"
    Swap: 10.000000 USDC -> 0.006293 WETH 
    Price: 0.000629 WETH per USDC, 1589.052587 USDC per WETH
    
    Signer private key was not provided. Skipping simulation/execution. Set PRIVATE_KEY env variable to perform simulation/execution.
    export TYCHO_URL=<tycho-api-url-for-chain>
    export TYCHO_API_KEY=<tycho-api-key-for-chain>
    cargo run --release --example quickstart -- --sell-token "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" --buy-token "0x4200000000000000000000000000000000000006" --tvl-threshold 100 --sell-amount 10 --chain "base"
    RUST_LOG=info cargo run --release --example quickstart
    let mut protocol_stream = ProtocolStreamBuilder::new(&tycho_url, Chain::Ethereum)
        .exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None)
        .exchange::<EVMPoolState<PreCachedDB>>(
            "vm:balancer_v2",
            tvl_filter.clone(),
            Some(balancer_pool_filter),
        )
        .auth_key(Some(tycho_api_key.clone()))
        .set_tokens(all_tokens.clone())
        .await
        .build()
        .await
        .expect("Failed building protocol stream");
    .exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None)
    let result = state.get_amount_out(amount_in, &tokens[0], &tokens[1])
    let other_result = result.new_state.get_amount_out(other_amount_in, &tokens[0], &tokens[1])
    let slippage = 0.0025; // 0.25% slippage
    let bps = BigUint::from(10_000u32);
    let slippage_percent = BigUint::from((slippage * 10000.0) as u32);
    let multiplier = &bps - slippage_percent;
    let min_amount_out = (expected_amount * &multiplier) / &bps;
    let simple_swap =
        Swap::new(component, sell_token.clone(), buy_token.clone());
    
    // Then we create a solution object with the previous swap
    let solution = Solution::new(
        user_address.clone(),
        user_address,
        sell_token.address,
        buy_token.address,
        sell_amount,
        min_amount_out,
        vec![simple_swap],
        )
        .with_user_transfer_type(UserTransferType::TransferFromPermit2);
    let swap_encoder_registry = SwapEncoderRegistry::new(Chain::Ethereum)
        .add_default_encoders(None)
        .expect("Failed to get default SwapEncoderRegistry");
        
    let encoder = TychoRouterEncoderBuilder::new()
        .chain(chain)
        .swap_encoder_registry(swap_encoder_registry)
        .build()
        .expect("Failed to build encoder");
    
    let encoded_solution = encoder
        .encode_solutions(vec![solution.clone()])
        .expect("Failed to encode router calldata")[0]
    let tx = encode_tycho_router_call(
        named_chain.into(),
        encoded_solution.clone(),
        &solution,
        chain.native_token().address,
        signer.clone(),
    )
    .expect("Failed to encode router call");
    cargo run --release --example quickstart -- --swapper-pk $PK
    Your balance: 100.000000 USDC
    Your WETH balance: 1.500000 WETH
    Your balance: 5.000000 USDC
    ⚠️ Warning: Insufficient balance for swap. You have 5.000000 USDC but need 10.000000 USDC
    Your WETH balance: 1.500000 WETH
    Would you like to simulate or execute this swap?
    Please be aware that the market might move while you make your decision, which might lead to a revert if you've set a min amount out or slippage.
    Warning: slippage is set to 0.25% during execution by default.
    
    ? What would you like to do? β€Ί
    ❯ Simulate the swap
      Execute the swap
      Skip this swap
    Simulating by performing an approval (for permit2) and a swap transaction...
    
    Simulated Block 21944458:
      Transaction 1: Status: true, Gas Used: 46098
      Transaction 2: Status: true, Gas Used: 182743
    Executing by performing an approval (for permit2) and a swap transaction...
    
    Approval transaction sent with hash: 0xf2a9217016397b09f5274e225754029ebda31743b4da7dd1441e13971e1f43b0 and status: true
    
    Swap transaction sent with hash: 0x0b26c9965b4ee39b5646ab93070f018c027ac3d0c9d56548a6db4412be7abbc8 and status: true
    
    βœ… Swap executed successfully! Exiting the session...
    
    Summary: Swapped 10.000000 USDC β†’ 0.006293 WETH at a price of 0.000629 WETH per USDC
    #[substreams::handlers::map]
    pub fn map_relative_balances(
        block: eth::v2::Block,
        components_store: StoreGetInt64,
    ) -> Result<BlockBalanceDeltas, anyhow::Error> {
        todo!()
    }
    #[substreams::handlers::store]
    pub fn store_balances(deltas: BlockBalanceDeltas, store: StoreAddBigInt) {
        tycho_substreams::balances::store_balance_changes(deltas, store);
    }
    #[substreams::handlers::map]
    pub fn map_protocol_changes(
        block: eth::v2::Block,
        grouped_components: BlockTransactionProtocolComponents,
        deltas: BlockBalanceDeltas,
        components_store: StoreGetInt64,
        balance_store: StoreDeltas,
    ) -> Result<BlockChanges> {
        let mut transaction_contract_changes: HashMap<_, TransactionChanges> = HashMap::new();
    
        aggregate_balances_changes(balance_store, deltas)
            .into_iter()
            .for_each(|(_, (tx, balances))| {
                transaction_contract_changes
                    .entry(tx.index)
                    .or_insert_with(|| TransactionChanges::new(&tx))
                    .balance_changes
                    .extend(balances.into_values());
            });
    }
    // Deposit ERC-20 tokens 
    router.deposit(tokenAddress, amount);
    
    // Deposit native ETH 
    router.deposit{value: amount}(address(0), amount);
    
    // Withdraw 
    router.withdraw(tokenAddress, amount);
    use tycho_substreams::models::{InterimContractChange, TransactionChangesBuilder};
    
    // all changes on this block, aggregated by transaction
    let mut transaction_changes: HashMap<_, TransactionChanges> = HashMap::new();
    
    // Extract token balances for vault contract
    block
        .transaction_traces
        .iter()
        .for_each(|tx| {
            // use helper function to get absolute balances at this transaction
            let vault_balance_change = get_vault_reserves(tx, &components_store, &tokens_store);
    
            if !vault_balance_change.is_empty() {
                let tycho_tx = Transaction::from(tx);
                let builder = transaction_changes
                    .entry(tx.index.into())
                    .or_insert_with(|| TransactionChangesBuilder::new(&tycho_tx));
    
                let mut vault_contract_changes = InterimContractChange::new(VAULT_ADDRESS, false);
                for (token_addr, reserve_value) in vault_balance_change {
                    vault_contract_changes.upsert_token_balance(
                        token_addr.as_slice(),
                        reserve_value.value.as_slice(),
                    );
                }
                builder.add_contract_changes(&vault_contract_changes);
            }
        });
    #[async_trait]
    pub trait RFQClient: Send + Sync {
        fn stream(
            &self,
        ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>>;
    
        async fn request_binding_quote(
            &self,
            params: &GetAmountOutParams,
        ) -> Result<SignedQuote, RFQError>;
    }
    export RPC_URL=<chain-rpc-url>
    export PRIVATE_KEY=<deploy-wallet-private-key>
    export BLOCKCHAIN_EXPLORER_API_KEY=<blockchain-explorer-api-key>
    let swap_encoder_registry = SwapEncoderRegistry::new(Chain::Ethereum)
        .add_default_encoders(None)
        .expect("Failed to get default SwapEncoderRegistry");
        
    let encoder = TychoRouterEncoderBuilder::new()
        .chain(Chain::Ethereum)
        .swap_encoder_registry(swap_encoder_registry)
        .build()
        .expect("Failed to build encoder");
    pip install git+https://github.com/propeller-heads/tycho-indexer.git#subdirectory=tycho-client-py
    import asyncio
    from tycho_indexer_client import Chain, TychoStream
    from decimal import Decimal
    
    async def main():
        stream = TychoStream(
            tycho_url="localhost:8888",
            auth_token="secret_token",
            exchanges=["uniswap_v2"],
            min_tvl=Decimal(100),
            blockchain=Chain.ethereum,
        )
    
        await stream.start()
    
        async for message in stream:
            print(message)
    
    asyncio.run(main())
    from tycho_indexer_client import (
        TychoRPCClient,
        TokensParams,
        Chain,
        PaginationParams
    )
    
    client = TychoRPCClient("http://0.0.0.0:4242", chain=Chain.ethereum)
    
    all_tokens = []
    page = 0
    
    while True:
        tokens = client.get_tokens(
            TokensParams(
                min_quality=51,
                traded_n_days_ago=30,
                pagination=PaginationParams(page=page, page_size=1000),
            )
        )
        
        if not tokens:
            break
        
        all_tokens.extend(tokens)
        page += 1
    use tycho_substreams::contract::extract_contract_changes;
    
    // all changes on this block, aggregated by transaction
    let mut transaction_changes: HashMap<_, TransactionChanges> = HashMap::new();
    
    extract_contract_changes(
        &block,
        |addr| {
            components_store
                .get_last(format!("pool:{0}", hex::encode(addr)))
                .is_some()
        },
        &mut transaction_changes,
    );
    // substreams.yaml
    ...
    
    networks:
      mainnet:
        params:
          map_protocol_changes: "vault_address=0000,swap_helper_address=0000"
    
    ...
    // map_protocol_changes
    use tycho_substreams::contract::extract_contract_changes;
    
    // all changes on this block, aggregated by transaction
    let mut transaction_changes: HashMap<_, TransactionChanges> = HashMap::new();
    
    // *params* is a module input var
    let config: DeploymentConfig = serde_qs::from_str(params.as_str())?;
    extract_contract_changes_builder(
        &block,
        |addr| {
            addr == config.vault_address
            || addr == config.swap_helper_address
        },
        &mut transaction_changes,
    );

    To consume data from Tycho Indexer on apps developed in languages where there isn't a native tycho client available (e.g: any languages apart from Rust and Python). For the supported languages, please check the Rust Clientor Python Clientdocs.

    hashtag
    Installing Tycho-client

    This guide provides two methods to install Tycho Client:

    1. Install with Cargo (recommended for most users)

    2. Download pre-built binaries from GitHub Releases

    hashtag
    Method 1: Install with Cargo

    hashtag
    Prerequisites

    • Cargo

    • Rust 1.84.0 or later

    hashtag
    Method 2: Download from GitHub Releases

    Step 1: Download the pre-built binary

    For a simple, setup-free start, download the latest tycho-client binary release that matches your OS/architecture on GitHubarrow-up-right.

    circle-info

    πŸ’‘ Tip: Choose the latest release unless you need a specific version.

    Step 2: Extract the binary from the tar.gz

    Open a terminal and navigate to the directory where the file was downloaded. Run the following command to extract the contents:

    Step 3: Link the binary to a directory in your system's PATH (recommended):

    chevron-rightAdditional info on adding to PATHhashtag

    NOTE: This command requires /usr/local/bin to be included in the system's PATH. While this is typically the case, there may be exceptions.

    If /usr/local/bin is not in your PATH, you can either:

    1. Add it to your PATH by exporting it:

    1. Or create a symlink in any of the following directories (if they are in your PATH):

    Step 4: Verify Installation

    You should see the Tycho Client version displayed. If you need more guidance, contact us via Telegramarrow-up-right


    hashtag
    Using Tycho Client

    hashtag
    Running the client

    hashtag
    Step 1: Setting up API Key

    If you're connecting to our hosted service, please follow our to get an API Key. Once you have a key, export it using an environment variable

    or use the command line flag

    hashtag
    Step 2: Consume data from Tycho Indexer

    Now, you're all set up!

    Before consuming the data, you first need to choose which protocols you want to track. You can find a list ofHosted Endpoints here. For example, to track the Uniswap V2 and V3 pools on Mainnet, with a minimum value locked of 100 ETH, run:

    Or skip secure connections entirely with --no-tls for local setups [coming soon].

    hashtag
    Debugging

    Since all messages are sent directly to stdout in a single line, logs are saved to a file: ./logs/dev_logs.log. You can configure the directory with the --log-dir option.

    hashtag
    Configuring the client

    For more details on using the CLI and its parameters, run:

    For extended explanation on how each parameter works, check our guide.

    Rust Client

    The rust crate provides a flexible library for developers to integrate Tycho’s real-time data into any Rust application.

    circle-info

    Tycho offers another packaged called Tycho Simulation, which uses Tycho Client to handle data streams and also implements simulations, allowing you to leverage the full power of Tycho. If your goal is to simulate the protocol's behavior, please check our Simulation guide.

    hashtag
    Setup Guide

    To use Tycho Client in Rust, add the following crates to your Cargo.toml:

    Step 2: Use Tycho-client

    From there it is easy to add a Tycho stream to your rust program like so:

    You can also use the client to interact with Tycho RPC for fetching static information. For example, you can fetch tokens (available at endpoint) with the following:

    Dynamic Contract Indexing (DCI)

    Substreams relies on witnessing contract creations to provide a contract's entire storage. Unless the system witnesses the creation and identifies at that point that the contract is relevant to the protocol, it cannot be indexed or used in simulations.

    The Dynamic Contract Indexing (DCI) system is a Tycho feature that addresses this limitation by dynamically identifying and indexing dependency contracts - such as oracles and price feeds - whose creation events are not observable. This may be because:

    • the contracts were created long before the protocol's first indexed block (startBlock on the substreams configuration file)

    • the dependency is updatable and which contracts are called may change during the protocol's lifetime. For example: a protocol switches oracle provider.

    Using predefined tracing information (known as entry points), Tycho's DCI assumes responsibility for these edge cases, with Substreams supplying only the core subset of the data for simulation.

    hashtag
    Understanding Entry Points

    DCI relies on the substreams package to supply tracing information for it to analyse and detect dependency contracts. It is important to understand the protocol being integrated and know where it might make external calls during simulations (swaps, price etc). These external calls need to be able to be defined fully by the combination of 'Entry Points' and 'Tracing Parameters'. See below for more information on what is not covered by the current DCI implementation.

    When an entry point is traced, all subsequent calls to other external contracts are automatically traced. Only the initial entry point needs to be supplied.

    hashtag
    Entry Point

    An entry point defines an external call in very simple terms:

    • address of the contract called

    • signature of the function called on that contract

    hashtag
    Tracing Parameters

    This defines how the entry point should be analysed and provides extra data needed for that analysis. Currently only one approach is supported:

    • RPC Trace

      This uses an RPC to simulate the defined external call (entry point) using sample call data. The sample data/parameters that can be defined for this trace include: caller and call data. Any new contracts detected by these traces are fetched at the current blockβ€”both code and relevant storageβ€”using an RPC as well. Once the contract is known, further updates are extracted by the DCI from the substream message's block storage_changes (see implementation step 2 below). Note: This approach may cause a temporary indexing delay whenever a new trace is conducted: ie. when new entry points or new tracing parameters are added. The delay depends on the complexity/depth of the trace.

    hashtag
    Retracing

    A retrace of an entry point occurs in one of two situations:

    1. New trace parameters are added to the entry point.

    2. A retrigger is triggered. Retriggers are storage slots automatically flagged by the DCI for their potential to influence a trace result. Every time one those identified storage slots are updated, the trace is redone.

    hashtag
    Implementation Steps

    To use the DCI system, you will need to extend your substream package to emit the following:

    hashtag
    1. Data to perform a trace

    For successful tracing we need to define: - An 'Entry Point' for each call made to an external contract during a simulation action (swap, price calculation, etc.). - Tracing parameters for the entry point. For every entry point defined, at least 1 set of tracing parameters must be supplied. It is vital that every component that uses an entry point is explicitly linked to that entry point. Some useful helper functions are provided to facilitate building the entry point messages:

    • To create a new entry point, use: tycho_substreams::entrypoint::create_entrypoint. Add the returned entry point and entry point parameter messages to the TransactionChangesBuilder using add_entrypoint and add_entrypoint_params respectively. They should be added to the transaction builder for the transaction the linked component was created.

    hashtag
    2. All contract changes that occurred on the current block

    The tycho_substreams::block_storage::get_block_storage_changes helper function simplifies this process by collecting all relevant changes for you. These changes need to be added to the storage_changes field of the final BlockChanges message emitted by the substream package.

    This will be used by the DCI to extract and index contract storage updates for all contracts it identifies.

    hashtag
    Limitations

    DCI is currently limited to only support cases that can be covered by explicitly defined example trace parameters (i,e callers and call data). This means it cannot cover:

    • Arbitrary call data: the automatic generation of call data, or fuzzing, is not supported. For example, external calls that take swap amounts as input - example amounts will not be auto generated and must be explicitly supplied as a Tracing Parameter.

    • External signatures: calls that require externally created signatures (like Permit2 signatures). DCI cannot automatically generate valid cryptographic signatures and therefore can only support cases where a valid signature can be defined as a Tracing Parameter.

    • Call data from external sources: input parameters that need to be fetched or derived from a separate trace are not supported. Only call data available within the Substreams package context can be processed.

    hashtag
    Frequently Asked Questions

    Q: Is it okay to redefine the same entry point multiple times? A: Yes. Tycho will deduplicate entry points, allowing you to add the same entry point for every new component without needing to track which ones already exist. Using storage on a substreams module affects the performance of the module so should be avoided where possible.

    Indexing

    Our indexing integrations require a Substreams SPKG to transform raw blockchain data into structured data streams. These packages enable our indexing integrations to track protocol state changes with low latency.

    hashtag
    What is Substreams?

    Substreams is a new indexing technology that uses Rust modules to process blockchain data. An SPKG file contains the Rust modules, protobuf definitions, and a manifest, and runs on the Substreams server.

    Learn more:

    hashtag
    Integration Modes

    hashtag
    VM Integration

    VM integrations primarily track contract storage associated with the protocol’s behavior. Most integrations will likely use the VM method due to its relative simplicity, so this guide focuses on VM-based integrations.

    It's important to know that simulations run in an empty VM, which is only loaded with the indexed contracts and storage. If your protocol calls external contracts during any simulation (swaps, price calculations, etc.), those contracts also have to be indexed. There are 2 approaches that can be used to index external contracts:

    • Direct indexing on the substream package. This is where you index the external contract the same way you would index your own protocol's contract. A key limitation in Substreams to keep in mind is that you must witness a contract’s creation to access its full storage and index it.

    • Using the DCI (Dynamic Contract Indexer). To be used if your protocol calls external contracts whose creation event cannot be witnessed within the Substreams package - for example: oracles deployed long before the protocol's initial block, or when which contract is called can be changed during the protocol's lifetime. Use of the DCI introduces indexing latency and should only be used if necessary.

    hashtag
    Native Integration

    Native integrations follow a similar approach, with one main difference: Instead of emitting changes in contract storage slots, they should emit values for all created and updated attributes relevant to the protocol’s behavior.

    hashtag
    Understanding the Data Model

    The Tycho Indexer ingests all data versioned by block and transaction. This approach maintains a low-latency feed. And it correctly handles chains that undergo reorgs. Here are the key requirements for the data emitted:

    1. Each state change must include the transaction that caused it.

    2. Each transaction must be paired with its corresponding block.

    3. All changes must be absolute values (final state), not deltas.

    Details of the data model that encodes these changes, transactions, and blocks in messages are available . These models facilitate communication between Substreams and the Tycho Indexer, and within Substreams modules. Tycho Indexer expects to receive a BlockChanges output from your Substreams package.

    You must aggregate changes at the transaction level. Emitting BlockChanges with duplicate transactions in the changes attributes is an error.

    hashtag
    Data Encoding

    To ensure compatibility across blockchains, many data types are encoded as variable-length bytes. This flexible approach requires an informal interface so that consuming applications can interpret these bytes consistently:

    • Integers: When encoding integers, particularly those representing balances, always use unsigned big-endian format. Multiple points within the system reference balances, so they must be consistently decoded along their entire journey.

    • Strings: Use UTF-8 encoding for any string data stored as bytes.

    • Attributes: Attribute encoding is variable and depends on specific use cases. But whenever possible, follow the encoding standards above for integers and strings.

    hashtag
    Reserved Attributes

    We reserve some attribute names for specific functions in our simulation process. Use these names only for their intended purposes. .

    hashtag
    Changes of interest

    Tycho Protocol Integrations should communicate the following changes:

    1. New Protocol Components: Signify any newly added protocol components. For example, pools, pairs, or markets – anything that indicates you can execute a new operation using the protocol.

    2. ERC20 Balances: For any contracts involved with the protocol, you should report balance changes in terms of absolute balances.

    3. Protocol State Changes: For VM integrations, this typically involves reporting contract storage changes for all contracts whose state is accessible during a swap operation (except token contracts).

    For a hands-on integration guide, see the following pages:

    Contributing guidelines

    hashtag
    Local Development

    hashtag
    Changing Rust Code

    Please make sure that the following commands pass if you have changed the code:

    We are using the stable toolchain for building and testing, but the nightly toolchain for formatting and linting, as it allows us to use the latest features of rustfmt and clippy.

    If you are working in VSCode, we recommend you install the extension, and use the following VSCode user settings:

    hashtag
    Changing Solidity code

    hashtag
    Setup

    Install foudryup and foundry

    hashtag
    Executors

    For security purposes, new Executors must have:

    • No ERC20 token transfers

    • No delegatecalls

    • Only perform native ETH transfers if this behaviour is safely reflected in getTransferData or getCallbackTransferData

    hashtag
    Running tests

    hashtag
    Code formatting

    hashtag
    Assembly

    Please minimize use of assembly for security reasons.

    hashtag
    Contract Analysis

    We use to detect any potential vulnerabilities in our contracts.

    To run locally, simply install Slither in your conda env and run it inside the foundry directory.

    hashtag
    Creating a Pull Request

    We use as our convention for formatting commit messages and PR titles.

    Reserved Attributes

    Certain attribute names are reserved exclusively for specific purposes. Please use them only for their intended applications. Attribute names are unique: if the same attribute is set twice, the value will be overwritten.

    hashtag
    Static Attributes

    The following attributes names are reserved and must be given using ProtocolComponent.static_att. These attributes MUST be immutable.

    Ethereum: Solidity

    hashtag
    Swap/Exchange Protocol Guide

    hashtag
    Implementing the Protocol

    To integrate an EVM exchange protocol:

    Hosted Endpoints

    Tycho Indexer's hosted endpoints

    hashtag
    Tycho Indexer

    Chain
    URL

    2. Implementation

    hashtag
    1. Understanding the protocol

    Before integrating, ensure you understand the protocol’s structure and behavior. Here are the key areas:

    1. Contracts and their roles: Identify the protocol's contracts and their specific roles. Understand how the contracts impact the behavior of the component you want to integrate.

    Simulation

    To enable simulations for a newly added protocol, it must first be integrated into the Tycho Simulation repository. Please submit a pull request to the to include it.

    hashtag
    Native Integration

    In order to add a new native protocol, you will need to complete the following high-level steps:

    cargo install tycho-client
    tar -xvzf tycho-client-aarch64-apple-darwin-{version}.tar.gz
    // Ensure the binary is executable:
    sudo chmod +x tycho-client
    // Create symlink
    sudo ln -s $(pwd)/tycho-client /usr/local/bin/tycho-client
    tycho-client --version
    tycho-client 0.54.0 # should match the latest version published on GitHub
    export TYCHO_AUTH_TOKEN={your_token}
    tycho-client --auth-key {your_token}
    tycho-client --exchange uniswap_v2 --exchange uniswap_v3 --min-tvl 100 --tycho-url 
    tycho-beta.propellerheads.xyz
    tycho client --help
    cargo check --all
    cargo test --all --all-features
    cargo +nightly fmt -- --check
    cargo +nightly clippy --workspace --all-features --all-targets -- -D warnings
    Quick explanationarrow-up-right
    SPKGsarrow-up-right
    Full documentationarrow-up-right
    herearrow-up-right
    See list of reserved attributes
    1. Setupchevron-right
    2. Implementationchevron-right
    Testingchevron-right
    // Cargo.toml
    
    [dependencies]
    tycho-client = "0.66.2"
    tycho-common = "0.66.2"
    Tycho RPC
    limitations
    rust-analyzerarrow-up-right
    Slitherarrow-up-right
    conventional commitsarrow-up-right
    hashtag
    1. manual_updates

    hashtag
    Description

    Determines whether the component updates should be manually triggered using the update_marker state attribute. By default, updates occur automatically whenever there is a change indexed for any of the required contracts. For contracts with frequent changes, automatic updates may not be desirable. For instance, a change in Balancer Vault storage should only trigger updates for the specific pools affected by the change, rather than for all pools indiscriminately. The manual_updates field helps to control and prevent unnecessary updates in such cases.

    If it's enable, updates on this component are only triggered by emitting an update_marker state attribute (described below).

    hashtag
    Type

    Set to [1u8]to enable manual updates.

    hashtag
    Example Usage

    hashtag
    2. pool_id

    hashtag
    Description

    The pool_id static attribute is used to specify the identifier of the pool when it differs from the ProtocolComponent.id. For example, Balancer pools have a component ID that corresponds to their contract address, and a separate pool ID used for registration on the Balancer Vault contract (needed for swaps and simulations).

    Notice: In most of the cases, using ProtocolComponent.id is preferred over pool_id and pool_id should only be used if a special identifier is strictly necessary.

    hashtag
    Type

    This attribute value must be provided as a UTF-8 encoded string in bytes.

    hashtag
    Example Usage

    hashtag

    hashtag
    State Attributes

    The following attributes names are reserved and must be given using EntityChanges. Unlike static attributes, state attributes are updatable.

    hashtag
    1. stateless_contract_addr

    hashtag
    Description

    The stateless_contract_addr_{index} field specifies the address of a stateless contract required by the component. Stateless contracts are those where storage is not accessed for the calls made to it during swaps or simulations.

    This is particularly useful in scenarios involving DELEGATECALL. If the contract's bytecode can be retrieved in Substreams, provide it using the stateless_contract_code attribute (see below).

    Note: If no contract code is given, the consumer of the indexed protocol has to access a chain node to fetch the code. This is considered non-ideal and should be avoided where possible.

    An index is used if multiple stateless contracts are needed. This index should start at 0 and increment by 1 for each additional stateless_contract_addr.

    The value for stateless_contract_addr_{index} can be provided in two ways:

    1. Direct Contract Address: A static contract address can be specified directly.

    2. Dynamic Address Resolution: Alternatively, you can define a function or method that dynamically resolves and retrieves the stateless contract address at runtime. This can be particularly useful in complex contract architectures, such as those using a dynamic proxy pattern. It is important to note that the called contract must be indexed by the Substreams module.

    hashtag
    Type

    This attribute value must be provided as a UTF-8 encoded string in bytes.

    hashtag
    Example Usage

    1. Direct Contract Address

    To specify a direct contract address:

    2. Dynamic Address Resolution

    To specify a function that dynamically resolves the address:

    hashtag
    2. stateless_contract_code

    hashtag
    Description

    The stateless_contract_code_{index} field is used to specify the bytecode for a given stateless_contract_addr. The index used here must match with the index of the related address.

    hashtag
    Type

    This attribute value must be provided as bytes.

    hashtag
    Example Usage

    hashtag
    3. update_marker

    hashtag
    Description

    The update_marker field is used to indicate that a pool has changed, thereby triggering an update on the protocol component. This is particularly useful for when manual_updates is enabled.

    hashtag
    Type

    Set to [1u8]to trigger an update.

    hashtag
    Example Usage

    hashtag
    4. balance_owner[deprecated]

    hashtag
    Description

    The balance_owner field specifies the address of the account that owns the protocol component tokens, when tokens are not owned by the protocol component itself or the multiple contracts are involved. This is particularly useful for protocols that use a vault, for example Balancer.

    circle-info

    The use of the balance_owner reserved attribute has been deprecated in favour of tracking contract balances directly. See Tracking Contract Balances.

    hashtag
    Type

    This attribute value must be provided as bytes.

    hashtag
    Example Usage

  • Implement the ISwapAdapter.solarrow-up-right interface.

  • Create a manifest file summarizing the protocol's metadata.

  • hashtag
    The Manifest File

    The manifest file contains author information and additional static details about the protocol and its testing. Here's a list of all valid keys:

    hashtag
    Key Functions

    hashtag
    Price (optional)

    Calculates marginal prices for specified amounts.

    circle-info

    The marginal price which is distinct from the executed price: swap(amount_in) / amount_in! The marginal price is defined as the price to trade an arbitrarily small (almost zero) amount after the trade of (amount). E.g. the marginal price of a uniswapv2 pool at zero is: price(0) = reserve0/reserve1

    • Return marginal prices in buyToken/sellToken units.

    • Include all protocol fees (use minimum fee for dynamic fees).

    • If you don't implement this function, flag it accordingly in capabilities and make it revert using the NotImplemented error.

    • While optional, we highly recommend implementing this function. If unavailable, we'll numerically estimate the price function from the swap function.

    hashtag
    Swap

    Simulates token swapping on a given pool.

    • Execute the swap and change the VM state accordingly.

    • Include a gas usage estimate for each amount (use gasleft() function).

    • Return a Trade struct with a price attribute containing price(specifiedAmount).

    • If the price function isn't supported, return Fraction(0, 1) for the price (we'll estimate it numerically).

    hashtag
    GetLimits

    Retrieves token trading limits.

    • Return the maximum tradeable amount for each token.

    • The limit is reached when the change in received amounts is zero or close to zero.

    • Overestimate the limit if in doubt.

    • Ensure the swap function doesn't error with LimitExceeded for amounts below the limit.

    hashtag
    getCapabilities

    Retrieves pool capabilities.

    hashtag
    getTokens (optional)

    Retrieves tokens for a given pool.

    • We mainly use this for testing, as it's redundant with the required substreams implementation.

    hashtag
    getPoolIds (optional)

    Retrieves a range of pool IDs.

    • We mainly use this for testing. It's okay not to return all available pools here.

    • This function helps us test against the substreams implementation.

    • If you implement it, it saves us time writing custom tests.

    // Import required dependencies
    use tracing_subscriber::EnvFilter;
    use tycho_client::{feed::component_tracker::ComponentFilter, stream::TychoStreamBuilder};
    use tycho_common::dto::Chain;
    
    /// Example of using the Tycho client to subscribe to exchange data streams
    ///
    /// This example demonstrates how to:
    /// 1. Initialize a connection to the Tycho service
    /// 2. Set up filters for specific exchanges and pools
    /// 3. Receive and process real-time updates
    #[tokio::main]
    async fn main() {
        // Initialize the tracing subscriber with environment-based filter configuration
        // Set RUST_LOG environment variable (e.g., RUST_LOG=info) to control logging level
        tracing_subscriber::fmt()
            .with_env_filter(EnvFilter::from_default_env())
            .init();
    
        // Create a new Tycho stream for Ethereum blockchain
        // The first returned value is a JoinHandle that we're ignoring here (_)
        let (_, mut receiver) =
            TychoStreamBuilder::new("tycho-beta.propellerheads.xyz", Chain::Ethereum)
                // Set authentication key
                // In production, use environment variable: std::env::var("TYCHO_AUTH_KEY").expect("...")
                .auth_key(Some("your-api-key".into()))
                // Subscribe to Uniswap V2 pools with TVL above 1000 ETH and remove the ones below 900 ETH
                .exchange("uniswap_v2", ComponentFilter::with_tvl_range(900.0, 1000.0))
                // Subscribe to specific Uniswap V3 pools by their pool IDs (contract addresses)
                .exchange(
                    "uniswap_v3",
                    ComponentFilter::Ids(vec![
                        // Include only these 2 UniswapV3 pools.
                        "0xCBCdF9626bC03E24f779434178A73a0B4bad62eD".to_string(), // USDC/WETH 0.3% pool
                        "0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640".to_string(), // USDC/WETH 0.05% pool
                    ]),
                )
                // Build the stream client
                .build()
                .await
                .expect("Failed to build tycho stream");
    
        // Process incoming messages in an infinite loop
        // NOTE: This will continue until the channel is closed or the program is terminated
        while let Some(msg) = receiver.recv().await {
            // Print each received message to stdout
            println!("{:?}", msg);
        }
    }
    use tycho_client::rpc::HttpRPCClient;
    use tycho_common::dto::Chain;
    
    let client = HttpRPCClient::new("insert_tycho_url", Some("my_auth_token"));
    
    let tokens = client
        .get_all_tokens(
            Chain::Ethereum,
            Some(51_i32), // min token quality to filter for certain token types
            Some(30_u64), // number of days since last traded
            1000,         // pagination chunk size
        )
        .await
        .unwrap();
        
    /// Token quality is between 0-100, where:
    ///  - 100: Normal token
    ///  - 75: Rebase token
    ///  - 50: Fee token
    ///  - 10: Token analysis failed at creation
    ///  - 5: Token analysis failed on cronjob (after creation).
    ///  - 0: Failed to extract decimals onchain
    use tycho_substreams::entrypoint::create_entrypoint;
    
    // defined example trace data
    let trace_data = TraceData::Rpc(RpcTraceData{
        caller: None, // None means a default caller will be used
        calldata: "0xabcd123400000000000012345678901234567890", // 0xabcd1234 - function selector, 00000000000012345678901234567890 - input address
    });
    
    let entrypoint, entrypoint_params = create_entrypoint(
        target: target_address,
        signature: "getFees(fromAddress)",
        component_id: "pool_id",
        trace_data,
    )
    
    // use the TransactionChangesBuilder for the tx where component [pool_id] was created
    builder.add_entrypoint(&entrypoint);
    builder.add_entrypoint_params(&entrypoint_params);
    use tycho_substreams::block_storage::get_block_storage_changes;
    
    let block_storage_changes = get_block_storage_changes(&block);
    
    ...
    
    Ok(BlockChanges {
        block: Some((&block).into()),
        ...
        storage_changes: block_storage_changes,
    })
    "editor.formatOnSave": true,
    "rust-analyzer.rustfmt.extraArgs": ["+nightly"],
    "rust-analyzer.check.overrideCommand": [
    "cargo",
    "+nightly",
    "clippy",
    "--workspace",
    "--all-features",
    "--all-targets",
    "--message-format=json"
    ],
    "[rust]": {
    "editor.defaultFormatter": "rust-lang.rust-analyzer"
    }
    curl -L https://foundry.paradigm.xyz | bash
    foundryup
    export ETH_RPC_URL=<url>
    forge test
    forge fmt
    conda create --name tycho-execution python=3.10
    conda activate tycho-execution
    
    pip install slither-analyzer
    cd foundry
    slither .
    Attribute {
        name: "manual_updates".to_string(),
        value: [1u8],
        change: ChangeType::Creation.into(),
    }
    Attribute {
        name: "pool_id".to_string(),
        value: format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(),
        change: ChangeType::Creation.into(),
    }
    Attribute {
        name: "stateless_contract_addr_0".into(),
        value: format!("0x{}", hex::encode(address)).into_bytes(),
        change: ChangeType::Creation.into(),
    }
    Attribute {
        name: "stateless_contract_addr_1".into(),
        value: format!("0x{}", hex::encode(other_address)).into_bytes(),
        change: ChangeType::Creation.into(),
    }
    Attribute {
        name: "stateless_contract_addr_0".into(),
        // Call views_implementation() on TRICRYPTO_FACTORY
        value: format!("call:0x{}:views_implementation()", hex::encode(TRICRYPTO_FACTORY)).into_bytes(),
        change: ChangeType::Creation.into(),
    }
    Attribute {
        name: "stateless_contract_code_0".to_string(),
        value: code.to_vec(),
        change: ChangeType::Creation.into(),
    }
    Attribute {
        name: "update_marker".to_string(),
        value: vec![1u8],
        change: ChangeType::Update.into(),
    };
    Attribute {
        name: "balance_owner".to_string(),
        value: VAULT_ADDRESS.to_vec(),
        change: ChangeType::Creation.into(),
    }
    yamlCopy# Author information helps us reach out in case of issues
    author:
      name: Propellerheads.xyz
      email: [email protected]
    
    # Protocol Constants
    constants:
      # Minimum gas usage for a swap, excluding token transfers
      protocol_gas: 30000
      # Minimum expected capabilities (individual pools may extend these)
      # To learn about Capabilities, see ISwapAdapter.sol)
      capabilities:
        - SellSide
        - BuySide
        - PriceFunction
    
    # Adapter contract (byte)code files
    contract: 
      # Contract runtime (deployed) bytecode (required if no source is provided)
      runtime: UniswapV2SwapAdapter.bin
      # Source code (our CI can generate bytecode if you submit this)
      source: UniswapV2SwapAdapter.sol
    
    # Deployment instances for chain-specific bytecode
    # Used by the runtime bytecode build script
    instances:
      - chain:
          name: mainnet
          id: 1
        # Constructor arguments for building the contract
        arguments:
          - "0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f"
    
    # Automatic test cases (useful if getPoolIds and getTokens aren't implemented)
    tests:
      instances:
        - pool_id: "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc"
          sell_token: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
          buy_token: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
          block: 17000000
          chain:
            name: mainnet
            id: 1
    function price(
        bytes32 poolId,
        IERC20 sellToken,
        IERC20 buyToken,
        uint256[] memory sellAmounts
    ) external returns (Fraction[] memory prices);
    function swap(
        bytes32 poolId,
        IERC20 sellToken,
        IERC20 buyToken,
        OrderSide side,
        uint256 specifiedAmount
    ) external returns (Trade memory trade);
    function getLimits(bytes32 poolId, address sellToken, address buyToken)
        external
        returns (uint256[] memory limits);
    function getCapabilities(bytes32 poolId, IERC20 sellToken, IERC20 buyToken)
        external
        returns (Capability[] memory);
    function getTokens(bytes32 poolId)
        external
        returns (IERC20[] memory tokens);
    function getPoolIds(uint256 offset, uint256 limit)
        external
        returns (bytes32[] memory ids);

    Base Mainnet

    tycho-base-beta.propellerheads.xyz

    Unichain Mainnet

    tycho-unichain-beta.propellerheads.xyz

    hashtag
    Tycho Fynd

    Tycho Fynd endpoints are dedicated instances for Fynd users. They enforce stricter data filtering restrictions to support higher load.

    Chain
    URL

    Ethereum (Mainnet)

    tycho-fynd-ethereum.propellerheads.xyz

    Base Mainnet

    tycho-fynd-base.propellerheads.xyz

    Unichain Mainnet

    tycho-fynd-unichain.propellerheads.xyz

    circle-info

    For API Documentation, Tycho Indexer includes Swagger docs, available at /docs/ path.

    Example, for Mainnet: https://tycho-beta.propellerheads.xyz/docs/arrow-up-right

    hashtag
    Plans

    Each API key is assigned a plan that determines rate limits and endpoint access.

    hashtag
    Rate Limits

    Plan
    Requests/sec
    Burst
    Max WebSocket Connections
    Allowed Endpoints

    basic

    50

    300/s (6x)

    2

    Tycho Indexer, Tycho Fynd

    circle-info

    Need higher limits? Contact @tanay_j on Telegram.

    hashtag
    Data Restrictions

    Endpoints enforce data filtering restrictions on API queries. When a restriction is active, requests that do not include the required parameter values are rejected.

    hashtag
    Tycho Indexer

    Restriction
    Value

    Max version age

    10 minutes

    Protocol systems

    All available

    tvl_gt

    No restriction

    min_quality

    hashtag
    Tycho Fynd

    All Fynd endpoints share the same filtering restrictions:

    Restriction
    Value

    Max version age

    10 minutes

    tvl_gt

    10.0

    min_quality

    100

    traded_n_days_ago

    The available protocol systems vary by chain:

    Chain
    Protocol Systems

    Ethereum (Mainnet)

    uniswap_v2, uniswap_v3, uniswap_v4, sushiswap_v2, pancakeswap_v2, pancakeswap_v3, ekubo_v2, ekubo_v3, fluid_v1

    Base Mainnet

    uniswap_v2, uniswap_v3, uniswap_v4, pancakeswap_v3, aerodrome_slipstreams

    Unichain Mainnet

    uniswap_v2, uniswap_v3, uniswap_v4, velodrome_slipstreams

    circle-info

    We're constantly adding new protocols. To see the current protocol systems for a specific endpoint, use the Retrieve protocol systems endpoint.

    hashtag
    Metrics

    Ethereum (Mainnet)

    tycho-beta.propellerheads.xyz

    Conditions for state changes: Determine which conditions trigger state changes – like price updates – in the protocol. For example, oracle updates or particular method calls.

  • Component addition and removal: Check how the protocol adds and removes components. Many protocols use a factory contract to deploy new components. Or they provide new components directly through specific method calls.

  • Once you understand the protocol's mechanics, you can proceed with implementation.

    hashtag
    2. Choosing a template

    These two templates outline all necessary steps for implementation:

    • Useethereum-template-factoryarrow-up-right when the protocol deploys one contract per pool (e.g., UniswapV2, UniswapV3).

    • Useethereum-template-singletonarrow-up-rightwhen the protocol uses a fixed set of contracts (e.g., UniswapV4).

    Find support in the tycho.buildarrow-up-right group if you don't know which template to choose.

    After choosing a template:

    1. Create a new directory for your integration: copy the template and rename all the references to ethereum-template-[factory|singleton] to [CHAIN]-[PROTOCOL_SYSTEM](use lowercase letters):

    2. Generate the required protobuf code by running:

    3. Register the new package within the workspace by adding it to the members list in substreams/Cargo.toml.

    4. Add any protocol-specific ABIs under [CHAIN]-[PROTOCOL-SYSTEM]/abi/

    5. Your project should compile and it should run with substreams:

    hashtag
    3. Implementation

    If you use a template, you must implement at least three key sections to ensure proper functionality:

    circle-info

    The templates include TO-DO comments at lines that probably require your attention. Each function is also documented with explanations and hints for when modifications may be necessary.

    1. Identify new ProtocolComponents and metadata

      Extract relevant protocol components and attach all metadata (attributes) necessary to encode swaps (or other actions) or to filter components. For example: pool identifier, pool keys, swap fees, pool_type, or other relevant static properties. Some attribute names are reserved. They are not always needed but must be respected for compatibility.

    2. Emit balances forProtocolComponents Tycho tracks TVL per component. So you must emit a BalanceChange whenever an event impacts a component's associated balances. Absolute balances are expected here. Protocols often only identify balance deltas - to handle these effectively please see ''.

    3. Track relevant storage slot changes (VM implementations only) For factory-like protocols: the template covers this automatically if the ProtocolComponent.id matches the contract address. For singleton contracts: you must collect the changes for contracts that you need tracked. To do this effectively, please see ''.

    Some protocols may require additional customisation based on their specific architecture. See Common Patterns & Problems for how to handle these cases.

    hashtag
    4. Testing

    To test indexing only, follow the instructions for the full test suite, but set skip_simulations to true. This will limit the test run to evaluating only the substreams package's indexing behavior, without running simulations.

    Create a protocol state struct that contains the state of the protocol, and implements the ProtocolSim trait (see herearrow-up-right).
  • Create a tycho decoder for the protocol state: i.e. implement TryFromWithBlock for ComponentWithState to your new protocol state.

  • Each native protocol should have its own module under tycho-simulation/src/evm/protocol.

    hashtag
    VM Integration

    To create a VM integration, provide a manifest file and an implementation of the corresponding adapter interface. Tycho Protocol SDK arrow-up-rightis a library to integrate DEXs and other onchain liquidity protocols into Tycho.

    hashtag
    Example Implementations

    The following exchanges are integrated with the VM approach:

    • Balancer V2 (see code herearrow-up-right)

    hashtag
    Install prerequisites

    1. Install Foundryarrow-up-right, start by downloading and installing the Foundry installer:

      then start a new terminal session and run

    2. Clone the Tycho Protocol SDK:

    3. Install dependencies:

    hashtag
    Understanding the ISwapAdapter

    Read the documentation of the Ethereum Solidity interface. It describes the functions that need to be implemented and the manifest file.

    Additionally, read through the docstring of the ISwapAdapter.solarrow-up-right interface and the ISwapAdapterTypes.solarrow-up-right interface, which defines the data types and errors the adapter interface uses. You can also generate the documentation locally and look at the generated documentation in the ./docs folder:

    hashtag
    Implementing the ISwapAdapter interface

    Your integration should be in a separate directory in the evm/src folder. Start by cloning the template directory:

    Implement the ISwapAdapter interface in the ./evm/src/<your-adapter-name>.sol file. See Balancer V2 implementation for reference.

    hashtag
    Testing your implementation

    1. Set up test files:

      • Copy evm/test/TemplateSwapAdapter.t.sol

      • Rename to <your-adapter-name>.t.sol

    2. Write comprehensive tests:

      • Test all implemented functions.

      • Use fuzz testing (see , especially the chapter for )

      • Reference existing test files: BalancerV2SwapAdapter.t.sol

    3. Configure fork testing (run a local mainnet fork against actual contracts and data):

      • Set ETH_RPC_URL environment variable

      • Use your own Ethereum node or services like

    4. Run the tests with

    hashtag
    Add implementation to Tycho simulation

    Once you have the swap adapter implemented for the new protocol, you will need to:

    1. Generate the adapter runtime file by running the evm/scripts/buildRuntime.sharrow-up-right script in our SDK repository with the proper input parameters. For example, in order to build the Balancer V2 runtime, the following command can be run:\

    2. Add the associated adapter runtime file to tycho-simulations/src/protocol/vm/assets. Make sure to name the file according to the protocol name used by Tycho Indexer in the following format: <Protocol><Version>Adapter.evm.runtime. For example: vm:balancer_v2 will be BalancerV2Adapter.evm.runtime. Following this naming format is important as we use an automated name resolution for these files.

    hashtag
    Filtering

    If your implementation does not support all pools indexed for a protocol, you can create a filter function to handle this. This filter can then be used when registering an exchange in the ProtocolStreamBuilder. See herearrow-up-right for example implementations.

    repositoryarrow-up-right
    cp -r ./substreams/ethereum-template-factory ./substreams/[CHAIN]-[PROTOCOL_SYSTEM]
    substreams protogen substreams.yaml --exclude-paths="google"
    curl -L https://foundry.paradigm.xyz | bash
    foundryup
    git clone https://github.com/propeller-heads/tycho-protocol-lib
    cd ./tycho-protocol-lib/evm/
    forge install
    >>> cd evm
    >>> ./scripts/buildRuntime.sh -c β€œBalancerV2SwapAdapter” -s β€œconstructor(address)” -a β€œ0xBA12222222228d8Ba445958a75a0704d566BF2C8”
    cd ./evm/
    forge doc
    cp ./evm/src/template ./evm/src/<your-adapter-name>
    export PATH"/usr/local/bin:$PATH"
    /bin
    /sbin
    /usr/bin
    /usr/sbin
    /usr/local/bin
    /usr/local/sbin
    handling relative balances
    tracking of contract storage
    Foundry test guidearrow-up-right
    Fuzz testingarrow-up-right
    Infuraarrow-up-right
    cd [CHAIN]-[PROTOCOL-SYSTEM]
    cargo build --release --target wasm32-unknown-unknown
    substreams gui substreams.yaml map_protocol_changes
    cd ./evm
    forge test

    fynd-basic

    50

    300/s (6x)

    2

    Tycho Fynd only

    No restriction

    traded_n_days_ago

    No restriction

    3

    Request for Quote Protocols

    Request for Quote (RFQ) protocols work differently from on-chain protocols. Instead of reading pool data from the chain, they fetch prices from off-chain market makers via WebSocket or API.

    You ask for a quote for a specific trade size, and they return a price. Quotes can be:

    • Indicative β€” estimated prices used for simulation.

    • Binding β€” firm prices, valid for a short time, used at execution.

    Tycho supports streaming, simulating, and executing RFQ quotes as part of multi-protocol swaps.

    hashtag
    Quickstart

    The RFQ quickstart is similar to the other protocols .

    See the code . As of now, and are the only supported providers.

    You need to set up the API credentials of the desired RFQs to access live pricing data and quoting, as well as your private key if you wish to execute against the Tycho Router:

    Then run the example:

    circle-info

    You’ll need to request credentials directly from RFQ providers.

    hashtag
    What it does

    The quickstart:

    • Connects to the RFQ stream and fetches live price updates.

    • Simulates the best available amount out for a given pair (default: 10 USDC β†’ WETH on mainnet).

    • Encodes the swap and prepares calldata to execute it via the Tycho Router.

    If you want to see results for a different token, amount, minimum TVL, or chain, you can set additional flags:

    This example would seek the best swap for 10 USDC -> WETH on Base.

    hashtag
    Set up

    You’ll need to configure:

    • Tycho URL (by default "tycho-beta.propellerheads.xyz")

    • Tycho API key

    • RFQ API keys (Have a look at src/rfq/constants.rs to see the authentication variables that are expected)

    To get token information from Tycho Indexer RPC please use .

    hashtag
    RFQClient

    Each RFQ protocol will have its own client. The client can stream live prices updates and request binding quotes.

    Example setup for Bebop:

    TVL threshold is specified in USD, as most RFQ quotes are USD-denominated. This setting filters out token pairs with low liquidity on the RFQ side, helping avoid thin or illiquid quotes.

    Quote tokens: You can optionally specify quote tokens when configuring the RFQ client to define which tokens the client should consider β€œapproved” for TVL normalization purposes. The client uses this approved quote token list exclusively for TVL filtering and does not use it for quote requests or trade execution.

    You should specify USD-priced stablecoins (e.g., USDC, USDT, DAI) as quote tokens, since currently-supported RFQ providers quote most of their currently supported liquidity in USD stablecoins. This ensures the client calculates TVL accurately when comparing pairs with different quote tokens. For instance, if you receive price levels for an ETH/WBTC pair where WBTC is the quote token, the client will look up the WBTC price in one of your approved quote tokens (USD stablecoins) to properly calculate the TVL in dollar terms. If you don’t explicitly set quote tokens, the client uses chain-specific defaults.

    Note: Some RFQ providers may support tokens that Tycho does not. Because execution happens through the Tycho Router, it’s important to ensure that all tokens used in RFQ quotes are also supported by Tycho.

    hashtag
    Stream: Real-Time Price Updates

    The RFQStreamBuilder handles registration of multiple RFQ clients and merges their message streams. It merges updates from one or more RFQ clients and decodes them into Update messages:

    • Use add_client() for each RFQ provider.

    • Streams that return errors are removed automatically.

    RFQ streams are timestamped, not block-based. Each update provides the full known state from the provider at that moment (not just deltas). The removed_pairs field indicates any pairs that disappeared since the last update. The new_pairs field contains all the currently available pairs.

    hashtag
    Simulation

    You can simulate a swap against an RFQ state using:

    This returns an indicative output amount, which you can use to decide if this swap is worth including.

    hashtag
    Encoding

    After choosing the best swap, you can use Tycho Execution to encode it. This is very similar to the encoding done in the general .

    hashtag
    Create a solution object

    The key parameter is minimum amount out, which protects against slippage and MEV. The quickstart applies 0.25% slippage tolerance.

    circle-exclamation

    For maximum security, you should determine the minimum amount from a third-party source.

    Build the Swap and Solution:

    When working with RFQs, two fields are required in Swap:

    • protocol_state: This is needed to enable the runtime generation of a binding quote at encoding timeβ€”for example:

    • estimated_amount_in : This represents the estimaed input amount for the quote request. It’s especially important when the swap path is complex (e.g., involving multiple hops), where the actual input amount may differ slightly because of slippage. We recommend setting estimated_amount_in a bit higher than your expected value. Many RFQs enforce that execution can only occur for amounts less than or equal to the quoted base amountβ€”so setting it conservatively helps avoid dropping funds. If the actual required input exceeds your estimate, any leftover tokens will remain in the Tycho Router.

    This mechanism also makes RFQs composable with other on-chain swaps. That enables hybrid routing strategies, such as a path like Uniswap β†’ RFQ β†’ Curve, seamlessly combining RFQ-based and traditional on-chain routes.

    circle-exclamation

    After encoding, quotes are valid for only 1–3 seconds. Execution must follow immediately, otherwise the transaction will revert.

    hashtag
    Encode solution

    hashtag
    Encode full method calldata

    You need to build the full calldata for the router. Tycho handles the swap encoding, but you control the full input to the router method. This quickstart provides helper functions (encode_tycho_router_call and sign_permit)

    Use it as follows:

    triangle-exclamation

    These functions are only examples intended for use within the quickstart. Do not use them in production. You must write your own logic to:

    • Control parameters like minAmountOut and receiver

    hashtag
    Execution

    This step allows you to test or perform real transactions based on the best available swap options. For this step, you need to pass your wallet's private key in the run command. Handle it securely and never expose it publicly.

    Once the best swap is found you can:

    1. Simulate the swap: Tests the swap without executing it on-chain. It simulates an approval (for permit2) and a swap transaction on the node. If the status is false, the simulation has failed. You can print the full simulation output for detailed failure information.

    2. Execute the swap: Performs the swap on-chain using your real funds. The process performs an approval (for permit2) and a swap transaction. You'll receive transaction hashes and statuses. After a successful execution, the program will exit. If the transaction fails, the program continues to stream new price updates.

    circle-exclamation

    Important Note

    Market conditions can change rapidly. Delays in your decision-making can lead to transaction reverts, especially if you've set parameters like minimum amount out or slippage. Always ensure you're comfortable with the potential risks before executing swaps.

    circle-info

    Because the RFQ will only let you swap up to the amount of tokens specified in the quote, when the RFQ swap happens after another protocol in a sequential swap, if positive slippage occurs during the preceding swap, any additional input tokens beyond the permitted quote amount will remain in the Tycho Router and not be sent to the RFQ protocol.

    Supported Protocols

    Currently, Tycho supports the following protocols:

    Protocol
    Integration Type
    Simulation Time
    Chains
    Partial Support Notes

    Private key if you wish to execute the swap against the Tycho Router

    Sign the permit2 object safely and correctly.

    This gives you full control over execution. And it protects you from MEV and slippage risks.

    Skip this swap: Ignores this swap. Then the program resumes listening for price updates.
    quickstart
    herearrow-up-right
    Beboparrow-up-right
    Hashflowarrow-up-right
    load_all_tokens
    quickstart
    export BEBOP_USER=<your-bebop-ws-username>
    export BEBOP_KEY=<your-bebop-ws-key>
    export HASHFLOW_USER=<your-hashflow-api-username>
    export HASHFLOW_KEY=<your-hashflow-api-key>
    export PRIVATE_KEY=<your-wallet-private-key>
    cargo run --release --example rfq_quickstart
    cargo run --release --example rfq_quickstart -- --sell-token "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" --buy-token "0x4200000000000000000000000000000000000006" --sell-amount 10 --tvl-threshold 1000 --chain "base"
    let bebop_client = BebopClientBuilder::new(chain, bebop_ws_user, bebop_ws_key)
        .tokens(rfq_tokens)
        .quote_tokens(quote_tokens)
        .tvl_threshold(cli.tvl_threshold)
        .build()
        .expect("Failed to create RFQ clients");
    let rfq_stream_builder = RFQStreamBuilder::new()
        .add_client::<BebopState>("bebop", Box::new(bebop_client))
        .set_tokens(all_tokens.clone())
        .await;
    state.get_amount_out(amount_in, &sell_token, &buy_token)
    let swap =
        Swap::new(component, sell_token.address.clone(), buy_token.address.clone())
            .protocol_state(state)
            .estimated_amount_in(sell_amount.clone());
    
    let solution = Solution::new(
        user_address.clone(),
        user_address,
        sell_token.address,
        buy_token.address,
        sell_amount,
        min_amount_out,
        vec![simple_swap],
        )
        .with_user_transfer_type(UserTransferType::TransferFromPermit2);
    state.request_binding_quote(&GetAmountOutParams { ... }).await
    let swap_encoder_registry = SwapEncoderRegistry::new(Chain::Ethereum)
        .add_default_encoders(None)
        .expect("Failed to get default SwapEncoderRegistry");
        
    let encoder = TychoRouterEncoderBuilder::new()
        .chain(chain)
        .swap_encoder_registry(swap_encoder_registry)
        .build()
        .expect("Failed to build encoder");
    
    let encoded_solution = encoder
        .encode_solutions(vec![solution.clone()])
        .expect("Failed to encode router calldata")[0]
    let tx = encode_tycho_router_call(
        named_chain.into(),
        encoded_solution.clone(),
        &solution,
        chain.native_token().address,
        signer.clone(),
    )
    .expect("Failed to encode router call");
    cargo run --release --example quickstart -- --swapper-pk $PK

    uniswap_v3

    Native (UniswapV3State)

    20 ΞΌs (0.02 ms)

    Ethereum, Base, Unichain

    uniswap_v4

    Native (UniswapV4State)

    3 ΞΌs (0.003 ms)

    Ethereum, Base, Unichain

    Only core uniswap V4 pools are supported on this native implementation.

    uniswap_v4_hooks

    Hybrid (UniswapV4State) [DCI indexed]

    1 ms

    Ethereum, Unichain

    All composable hooks are supported. Angstrom: see more details . recommended: set a high startup timeout on the stream builder: .startup_timeout(Duration::from_secs(120))

    vm:balancer_v2

    VM (EVMPoolState) [DCI indexed]

    0.5 ms

    Ethereum

    A few pools are currently unsupported. Use balancer_v2_pool_filter

    vm:curve

    VM (EVMPoolState) [DCI indexed]

    1 ms

    Ethereum

    NOTE: curve requires a node RPC to fetch some code at startup. Please set the RPC_URL env var.

    sushiswap_v2

    Native (UniswapV2State)

    1 ΞΌs (0.001 ms)

    Ethereum

    pancakeswap_v2

    Native (PancakeswapV2State)

    1 ΞΌs (0.001 ms)

    Ethereum

    pancakeswap_v3

    Native (UniswapV3State)

    20 ΞΌs (0.02 ms)

    Ethereum, Base

    ekubo_v2

    Native (EkuboState)

    1.5 ΞΌs (0.0015 ms)

    Ethereum

    vm:maverick_v2

    VM (EVMPoolState)

    -

    Ethereum

    aerodrome_slipstreams

    Native

    (AerodromeSlipstreamsState)

    -

    Base

    rocketpool

    Native (RocketpoolState)

    -

    Ethereum

    Note: the DepositPool was recently updated to v1.4. This new version is supported by tycho_simulation and above.

    fluid_v1

    Native (FluidV1)

    -

    Ethereum

    Note: paused pools are still indexed. To filter them out use fluid_v1_paused_pools_filter.

    cowamm

    Native (CowAMMState)

    -

    Ethereum

    CoWAMM doesn't have a Tycho Execution component. This is because of CoWAMM's unique design where only cowswap solvers can unlock the liquidity pools after sharing a quote.

    You will have to integrate execution yourself (see and ).

    ekubo_v3

    Native (EkuboV3State)

    9ΞΌs

    Ethereum

    circle-info

    Live tracker & Upcoming protocols

    • Currently supported protocols and Tycho status: http://tycho.live/arrow-up-right

    • List of upcoming protocolsarrow-up-right

    circle-info

    Register code snippet

    hashtag
    Integration Types

    There are three types of protocol integrations:

    • Native protocols have been implemented using an analytical approach and are ported to Rust - faster simulation.

    • VM protocols execute the VM bytecode locally - this is easier to integrate the more complex protocols, however has slower simulation times than a native implementation.

      • Some VM protocols are DCI indexed. DCI is our Dynamic Contract Indexer and provides more flexibility on indexing restraints. Note - these protocols tend to serve a lot of data and experience occasional streaming delays.

    • Hybrid uses a combination of the two - native for general protocol logic portable to Rust, and VM for the more complex or pool-specific logic.

    Interested in adding a protocol? Refer to the Tycho Simulation for DEXs documentation for implementation guidelines.

    hashtag
    Protocol-Specific Details

    While most protocols work out of the box, some require additional configuration or have specific considerations you should be aware of.

    hashtag
    Angstrom (Uniswap V4 Hook)

    Angstrom requires querying their API for attestationsarrow-up-right per block to unlock their contract. If execution comes too late, the contract can no longer be unlocked for that block.

    Required configuration:

    • Set the ANGSTROM_API_KEY environment variable (request one from the Angstrom team directly)

    • Set ANGSTROM_BLOCKS_IN_FUTURE environment variable (if you want to override the default valuearrow-up-right of 5 blocks). Important trade-off: The more blocks you fetch, the more calldata will be sent to the Tycho Router, making execution more gas expensive.

    uniswap_v2

    Native (UniswapV2State)

    1 ΞΌs (0.001 ms)

    Ethereum, Base, Unichain

    Simulation

    Simulate interactions with any protocol.

    Tycho Simulation is a Rust crate that provides powerful tools for interacting with protocol states, calculating spot prices, and simulating token swaps.

    The repository is available herearrow-up-right.

    circle-info

    ✨ New: Sub-Second Latency with Partial Blocks

    Tycho now provides early support for partial blocks on Base, enabling sub-second latency by streaming pre-confirmation updates. Enable this feature with .enable_partial_blocks() on your ProtocolStreamBuilder.

    hashtag
    Installation

    The tycho-simulation package is available on .

    To use the simulation tools with Ethereum Virtual Machine (EVM) chains, add the optional evm feature flag to your dependency configuration:

    Add this to your project's Cargo.toml file.

    circle-info

    Note: Replace x.y.z with the latest version number from our . Using the latest release ensures you have the most up-to-date features and bug fixes.

    hashtag
    Main Interface

    All protocols implement the ProtocolSim trait (see definition ). It has the main methods:

    hashtag
    Spot price

    spot_price returns the pool's current marginal price.

    hashtag
    Get amount out

    get_amount_out simulates token swaps.

    You receive a GetAmountOutResult , which is defined as follows:

    new state allows you to, for example, simulate consecutive swaps in the same protocol.

    Please refer to the of the ProtocolSim trait and its methods for more in-depth information.

    hashtag
    Fee

    fee returns the fee of the protocol as a ratio. For example if the fee is 1%, the value returned would be 0.01.

    circle-info

    If the fee is dynamic, it returns the minimal fee.

    hashtag
    Get limits

    get_limits returns a tuple containing the maximum amount in and out that can be traded between two tokens.

    circle-info

    If there are no hard limits to the swap (for example for Uniswap V2), the returned amount will be a "soft" limit, meaning that the actual amount traded could be higher but it's advised to not exceed it.

    hashtag
    Swap to price

    swap_to_price returns the amount of token_in required to move the pool's marginal price down to a target price, and the amount of token_out received. The target_price is denoted as token_out (numerator) per token_in (denominator) net of all fees.

    Price represents a price as a rational fraction (numerator / denominator).

    Trade represents a trade between two tokens at a given price on a pool.

    hashtag
    Query supply

    query_supply returns the maximum amount of token_out a pool can supply, and corresponding token_in demand, while respecting a minimum trade price. The target_price is denoted as token_out (numerator) per token_in (denominator) net of all fees.

    Please refer to the of the ProtocolSim trait and its methods for more in-depth information.

    hashtag
    Streaming Protocol States

    To maintain up-to-date states of the protocols you wish to simulate over, you can use a Tycho Indexer stream. Such a stream can be set up in 2 easy steps:

    hashtag
    Step 1: Fetch tokens

    It is necessary to collect all tokens you are willing to support/swap over as this must be set on the stream builder in step 2. You can either set up custom logic to define this, or use the Tycho Indexer RPC to fetch and filter for tokens of interest. To simplify this, a util function called load_all_tokensis supplied and can be used as follows:

    hashtag
    Step 2: Create a stream

    You can use the to easily set up and manage multiple protocols within one stream. An example of creating such a stream with Uniswap V2 and Balancer V2 protocols is as follows:

    Some protocols, such as Balancer V2 and Curve, require a pool filter to be defined to filter out unsupported pools. If a protocol needs a pool filter and the user does not provide one, a warning will be raised during the stream setup process.

    The stream created emits Update messages which consist of:

    • block number_or_timestamp- the block this update message refers to

    • new_pairs- new components witnessed (either recently created or newly meeting filter criteria)

    • removed_pairs- components no longer tracked (either deleted due to a reorg or no longer meeting filter criteria)

    The first message received will contain states for all protocol components registered to. Thereafter, further block updates will only contain data for updated or new components.

    Note: For efficiency, ProtocolSim states contain simulation-critical data only. Reference data such as protocol names and token information is provided in the ProtocolComponent objects within the new_pairs field. Consider maintaining a store of these components if you need this metadata.

    For a full list of supported protocols and the simulation state implementations they use, see .

    circle-info

    ProtocolStreamBuilder supports the same as the Tycho Client, with one difference: TVL estimates are always included in the simulation package and cannot be disabled.

    chevron-rightExample: Consuming the Stream and Simulatinghashtag

    This simplified example shows how to process the stream created above and run simulations on the updated pools. Since the first message of the stream contains all pools, this means the first iteration of the loop will simulate on everything.

    In this example we choose 2 tokens: a buy and a sell token, and simulate only on pools that contain those tokens.

    hashtag
    Example Use Case: Token Price Printer

    You can find an example of a price printer .

    Clone the repo, then run:

    circle-info

    You'll need an RPC to fetch some static protocol info. You can use any RPC provider – e.g. set one up with .

    You will see a UI where you can select any pool, press enter, and simulate different trade amounts on the pool.

    The program prints logs automatically to a file in the logs directory in the repo.

    Migration Guide: V2 to V3

    This guide covers the breaking changes between V2 and V3 from the perspective of users who consume the Rust encoding library or interact with the TychoRouter contracts.

    hashtag
    Encoding Changes

    hashtag
    Solution Struct

    Renamed fields:

    V2
    V3
    Notes

    Removed fields:

    Field
    Replacement

    New fields:

    Field
    Type
    Description

    Private fields with getters/setters:

    In V2, Solution fields were pub. In V3, all fields are private. Use the constructor and builder methods:

    hashtag
    UserTransferType Moved to Solution

    In V2, UserTransferType was set on the encoder builder. In V3, it is a field on each Solution, allowing different solutions in the same batch to use different funding methods.

    The UserTransferType::None variant has been renamed to UserTransferType::UseVaultsFunds, reflecting the new vault-based architecture.

    hashtag
    Swap Struct

    Builder methods renamed (added with_ prefix for consistency):

    V2
    V3

    Getter methods renamed (dropped get_ prefix):

    V2
    V3

    hashtag
    EncodedSolution Struct

    Fields are now private with getter methods, matching the pattern used elsewhere:

    The function_signature field now reflects both the swap strategy and the funding mode. For example, splitSwapUsingVault(...) for a split swap using vault funds.

    Removed permit field:

    The permit: Option<PermitSingle> field has been removed from EncodedSolution. The encoder no longer creates or returns Permit2 data. If you use TransferFromPermit2, you must handle permit creation and signing yourself.

    The Permit2 utility struct has been made public, so you can use it directly.

    hashtag
    Wrapping and Unwrapping

    V2 used a NativeAction enum on the Solution with Wrap and Unwrap variants. The router had dedicated wrap/unwrap flags.

    V3 removes this entirely. Instead, a WETH executor handles wrapping and unwrapping as regular swap steps. The encoder automatically inserts these swaps when it detects ETH↔WETH gaps in the swap path.

    This also works for mid-path bridging (e.g., if one swap outputs ETH and the next expects WETH) and at the end of a path. See more in .

    hashtag
    Encoder Builder

    Removed options:

    V2 option
    Notes

    The V3 builder only requires chain and swap_encoder_registry:

    hashtag
    Transaction and encode_full_calldata Removed

    The Transaction struct and encode_full_calldata method have been removed entirely. In V2, encode_full_calldata was already deprecated. V3 only supports encode_solutions, which returns EncodedSolution objects.

    You are responsible for constructing the full method call, including execution-critical parameters like min_amount_out, receiver, and fee configuration.

    hashtag
    SwapEncoderRegistry

    SwapEncoderRegistry::new now requires a Chain parameter:

    hashtag
    Execution Changes

    hashtag
    Router Function Signatures

    The TychoRouter V3 methods now include a ClientFeeParams struct in their signatures:

    When constructing calldata yourself (recommended), encode this struct as part of the function arguments. Even if you are not charging fees, you must pass this parameter with zero values.

    hashtag
    Vault Integration

    The TychoRouter now includes an ERC6909 vault. Key changes:

    • UseVaultsFunds replaces the old None transfer type. Tokens deposited in the vault are tracked per-user and can be used for swaps or withdrawn.

    • Deposit tokens via router.deposit(token, amount) before swapping with vault funds.

    For more see .

    hashtag
    No More Wrap/Unwrap Flags

    The router no longer accepts wrap or unwrap boolean flags. If your calldata construction includes these parameters, remove them. The WETH executor handles wrapping and unwrapping as part of the swap path. See .

    hashtag
    Method Variants

    Each swap strategy (single, sequential, split) now has three variants instead of two, with a new UsingVault variant:

    V2
    V3

    The same pattern applies for sequentialSwap and splitSwap. The EncodedSolution.function_signature tells you which variant to call.

    Contract Addresses

    hashtag
    Ethereum

    Contract
    Address

    hashtag
    Base

    Contract
    Address

    hashtag
    Unichain

    Contract
    Address
    fn register_exchanges(
        mut builder: ProtocolStreamBuilder,
        chain: &Chain,
        tvl_filter: ComponentFilter,
    ) -> ProtocolStreamBuilder {
        match chain {
            Chain::Ethereum => {
                builder = builder
                    .exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None)
                    .exchange::<UniswapV2State>("sushiswap_v2", tvl_filter.clone(), None)
                    .exchange::<PancakeswapV2State>("pancakeswap_v2", tvl_filter.clone(), None)
                    .exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None)
                    .exchange::<UniswapV3State>("pancakeswap_v3", tvl_filter.clone(), None)
                    .exchange::<EVMPoolState<PreCachedDB>>("vm:balancer_v2", tvl_filter.clone(), Some(balancer_v2_pool_filter))
                    .exchange::<UniswapV4State>("uniswap_v4", tvl_filter.clone(), None)
                    .exchange::<EkuboState>("ekubo_v2", tvl_filter.clone(), None)
                    .exchange::<EVMPoolState<PreCachedDB>>("vm:curve", tvl_filter.clone(), None)
                    .exchange::<UniswapV4State>("uniswap_v4_hooks", tvl_filter.clone(), None)
                    .exchange::<EVMPoolState<PreCachedDB>>("vm:maverick_v2", tvl_filter.clone(), None)
                    .exchange::<EkuboV3State>("ekubo_v3", tvl_filter.clone(), None)
            }
            Chain::Base => {
                builder = builder
                    .exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None)
                    .exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None)
                    .exchange::<UniswapV4State>("uniswap_v4", tvl_filter.clone(), None)
                    .exchange::<UniswapV3State>("pancakeswap_v3", tvl_filter.clone(), None)
                    .exchange::<AerodromeSlipstreamsState>("aerodrome_slipstreams", tvl_filter.clone(), None)
            }
            Chain::Unichain => {
                builder = builder
                    .exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None)
                    .exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None)
                    .exchange::<UniswapV4State>("uniswap_v4", tvl_filter.clone(), None)
            }
            _ => {}
        }
        builder
    }
    below
    v0.248.0arrow-up-right
    cowamm docsarrow-up-right
    examplearrow-up-right
    Authentication
    Usage
  • states- the updated ProtocolSim states for all components modified in this block

  • Githubarrow-up-right
    GitHub Releases pagearrow-up-right
    herearrow-up-right
    in-code documentationarrow-up-right
    in-code documentationarrow-up-right
    ProtocolStreamBuilderarrow-up-right
    Supported Protocols
    streaming optionsarrow-up-right
    herearrow-up-right
    Infuraarrow-up-right

    token_out

    The token being bought

    checked_amount

    min_amount_out

    Minimum acceptable output amount

    Bytes

    Address to receive the client fee.

    max_client_contribution

    BigUint

    Maximum amount the client will subsidize from their vault if slippage reduces the output below min_amount_out.

    Fees (both client and router fees) are credited to the receiver's vault balance rather than transferred immediately.

    given_token

    token_in

    The token being sold

    given_amount

    amount_in

    Amount of the input token

    checked_token

    native_action: Option<NativeAction>

    No longer needed. The encoder automatically inserts WETH wrap/unwrap swaps (see Wrapping and Unwrapping).

    exact_out: bool

    Only exact-in was ever supported. Removed for simplicity.

    user_transfer_type

    UserTransferType

    How user funds enter the router. Moved here from the encoder builder.

    client_fee_bps

    u16

    Fee in basis points charged by the client (0–10000).

    .split(0.5)

    .with_split(0.5)

    .user_data(data)

    .with_user_data(data)

    .protocol_state(state)

    .with_protocol_state(state)

    .estimated_amount_in(amount)

    .get_split()

    .split()

    .get_user_data()

    .user_data()

    .get_protocol_state()

    .protocol_state()

    .get_estimated_amount_in()

    .user_transfer_type(...)

    Moved to Solution.

    .swapper_pk(...)

    Removed. Sign Permit2 externally.

    .historical_trade()

    Removed. No longer needed.

    singleSwap(...)

    singleSwap(...)

    singleSwapPermit2(...)

    singleSwapPermit2(...)

    β€”

    singleSwapUsingVault(...)

    Wrapping & Unwrapping
    Vault
    Native Token Handling (Wrapping & Unwrapping)

    client_fee_receiver

    .with_estimated_amount_in(amount)

    .estimated_amount_in()

    tycho-simulation = { 
         git = "https://github.com/propeller-heads/tycho-simulation.git",
         package = "tycho-simulation",
         tag = "x.y.z", # Replace with latest version
         features = ["evm"]
    }
    
    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
    fn get_amount_out(
        &self,
        amount_in: BigUint,
        token_in: &Token,
        token_out: &Token,
    ) -> Result<GetAmountOutResult, SimulationError>;
    pub struct GetAmountOutResult {
        pub amount: BigUint, // token_out amount you receive
        pub gas: BigUint, // gas cost
        pub new_state: Box<dyn ProtocolSim>, // state of the protocol after the swap
    }
    fn fee(&self) -> f64;
    fn get_limits(
            &self,
            sell_token: Address,
            buy_token: Address,
        ) -> Result<(BigUint, BigUint), SimulationError>;
    fn swap_to_price(
            &self,
            token_in: Address,
            token_out: Address,
            target_price: Price,
        ) -> Result<Trade, SimulationError>
    pub struct Price {
        pub numerator: BigUint,
        pub denominator: BigUint,
    }
    pub struct Trade {
        pub amount_in: BigUint,
        pub amount_out: BigUint,
    }
    fn query_supply(
            &self,
            token_in: Address,
            token_out: Address,
            target_price: Price,
        ) -> Result<Trade, SimulationError>
    use tycho_simulation::utils::load_all_tokens;
    use tycho_core::models::Chain;
    
    let all_tokens = load_all_tokens(
                "tycho-beta.propellerheads.xyz",  // tycho url
                false,                            // use tsl (this flag disables tsl)
                Some("your-api-token"),           // auth key
                Chain::Ethereum,                  // chain
                None,                             // min quality (defaults to 100: ERC20-like tokens only) 
                None,                             // days since last trade (has chain specific defaults)
            ).await;
    use tycho_simulation::evm::{
        engine_db::tycho_db::PreCachedDB,
        protocol::{uniswap_v2::state::UniswapV2State, vm::state::EVMPoolState},
        stream::ProtocolStreamBuilder,
    };
    use tycho_core::models::Chain;
    use tycho_client::feed::component_tracker::ComponentFilter;
    
    let tvl_filter = ComponentFilter::with_tvl_range(9, 10); // filter buffer of 9-10ETH
    let mut protocol_stream = ProtocolStreamBuilder::new("tycho-beta.propellerheads.xyz", Chain::Ethereum)
        .exchange::<UniswapV2State>("uniswap_v2", tvl_filter.clone(), None)
        // add other protocols here
        .auth_key(Some("your-api-token"))
        .skip_state_decode_failures(true) // skips the pool instead of panicking if it errors on decode
        .set_tokens(all_tokens.clone())
        .await
        .build()
        .await
        .expect("Failed building protocol stream");
     // SIMULATION PARAMS
     // Set sell and buy tokens to USDC and USDT respectively
     let sell_token = Token::new("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", 6, "USDC", BigUint::from(10000u64));
     let buy_token = Token::new("0xdac17f958d2ee523a2206206994597c13d831ec7", 6, "USDT", BigUint::from(10000u64));
     let sell_amount = BigUint::from(1000000000u64); // 1000 USDC
    
    // PERSIST DATA BETWEEN BLOCKS
    // track all witnessed ProtocolComponents from the stream
    let mut all_pools = HashMap::new();
    // track all amount_outs for each pool simulated
    let mut amount_out = HashMap::new()
    
    // loop through stream messages
    while let Some(stream_message) = protocol_stream.next().await {
         let message = match stream_message {
            Ok(msg) => msg,
            Err(err) => {
                eprintln!("Error receiving message: {err}");
                break; // Exit loop on stream error
            }
        };
        
        // Store any new protocol components we haven't seen before
        for (id, comp) in message.new_pairs.iter() {
            all_pools
                .entry(id.clone())
                .or_insert_with(|| comp.clone());
        }
        
        // Simulate swaps on any updated pools that contain our token pair
        for (id, state) in message.states.iter() {
            if let Some(component) = all_pools.get(id) {
                // Skip if this pool doesn't contain both of our tokens
                let tokens = &component.tokens;
                if !tokens.contains(&sell_token) || !tokens.contains(&buy_token) {
                    continue;
                }
                
                // Calculate the amount out for our swap
                match state.get_amount_out(sell_amount.clone(), &sell_token, &buy_token) {
                    Ok(result) => {
                        amounts_out.insert(id.clone(), result.amount);
                    },
                    Err(err) => {
                        eprintln!("Error calculating amount out for pool {id}: {err}");
                    }
                }
            }
        }
    }
    export RPC_URL=<your-eth-rpc-url>
    cargo run --release --example price_printer -- --tvl-threshold 1000
    // V2
    let solution = Solution {
    sender: addr,
    receiver: addr,
    given_token: token_a,
    given_amount: amount,
    checked_token: token_b,
    checked_amount: min_out,
    swaps: vec![swap],
    exact_out: false,
    native_action: Some(NativeAction::Wrap),
    };
    
    // V3
    let solution = Solution::new(
    addr,        // sender
    addr,        // receiver
    token_a,     // token_in
    token_b,     // token_out
    amount,      // amount_in
    min_out,     // min_amount_out
    vec![swap],  // swaps
    )
    .with_user_transfer_type(UserTransferType::TransferFrom)
    .with_client_fee_bps(50)
    .with_client_fee_receiver(fee_addr)
    .with_max_client_contribution(BigUint::from(0u64));
    // V2
    let encoder = TychoRouterEncoderBuilder::new()
    .chain(chain)
    .user_transfer_type(UserTransferType::TransferFrom)  // set here
    .swap_encoder_registry(registry)
    .build() ?;
    
    // V3
    let encoder = TychoRouterEncoderBuilder::new()
    .chain(chain)
    .swap_encoder_registry(registry)
    .build() ?;
    
    let solution = Solution::new(/* ... */)
    .with_user_transfer_type(UserTransferType::TransferFrom);  // set here
    // V2
    let swaps = encoded_solution.swaps;
    let sig = encoded_solution.function_signature;
    
    // V3
    let swaps = encoded_solution.swaps();
    let sig = encoded_solution.function_signature();
    // V2
    let solution = Solution {
    given_token: eth_address,
    checked_token: dai_address,
    native_action: Some(NativeAction::Wrap),
    swaps: vec![weth_to_dai_swap],
    ..
    };
    
    // V3 β€” just set token_in to ETH; the encoder adds a WETH wrap swap automatically
    let solution = Solution::new(
    sender,
    receiver,
    eth_address,   // token_in is ETH
    dai_address,   // token_out is DAI
    amount,
    min_out,
    vec![weth_to_dai_swap],  // first swap expects WETH β€” encoder bridges the gap
    );
    // V3
    let encoder = TychoRouterEncoderBuilder::new()
    .chain(Chain::Ethereum)
    .swap_encoder_registry(registry)
    .build() ?;
    // V2
    let registry = SwapEncoderRegistry::new()
    .add_default_encoders(executors_addresses) ?;
    
    // V3
    let registry = SwapEncoderRegistry::new(Chain::Ethereum)
    .add_default_encoders(executors_addresses) ?;
    struct ClientFeeParams {
        uint16 clientFeeBps;
        address clientFeeReceiver;
        uint256 maxClientContribution;
        uint256 deadline;
        bytes clientSignature;
    }

    FeeCalculatorarrow-up-right

    0x24AD1d4a2666a99Ef46adA68999a89E324CD914Carrow-up-right

    UniswapV2Executorarrow-up-right

    0x79087ADB525a6D4e20799AE68Ac06dE0a15c278Earrow-up-right

    SushiswapV2Executorarrow-up-right

    0x79087ADB525a6D4e20799AE68Ac06dE0a15c278Earrow-up-right

    PancakeswapV2Executorarrow-up-right

    0x3201ea6B93731F30e55Cb87660eEd70B369fadC7arrow-up-right

    UniswapV3Executorarrow-up-right

    0xc7d47F3C3f755ed977f3C19F4C1f007CbEd109b0arrow-up-right

    PancakeswapV3Executorarrow-up-right

    0xc7d47F3C3f755ed977f3C19F4C1f007CbEd109b0arrow-up-right

    UniswapV4Executorarrow-up-right

    0xA942c54f2E58153eCdD4DD24B9bF98F57c9D7d55arrow-up-right

    BalancerV2Executorarrow-up-right

    0x9D517d5a3A3266fbD75b1Ad4fe6CfC40087Cfdc0arrow-up-right

    BalancerV3Executorarrow-up-right

    0x8594ac3486B6c68DF5Bf5F9aDd25FdcaC69F2588arrow-up-right

    EkuboV2Executorarrow-up-right

    0x25b670A94a376254Bac2B5f16B0Dc040Df44d1ECarrow-up-right

    EkuboV3Executorarrow-up-right

    0x128BA676f1426D0260f7f1EEDD799777dbe12fdbarrow-up-right

    CurveExecutorarrow-up-right

    0xAF0E1ac9EA1A81120bf4f285340ac70e41c9D65farrow-up-right

    MaverickV2Executorarrow-up-right

    0x95E8D6E3997D98170ab7243DFeCF93B5f5e25BEDarrow-up-right

    BebopExecutorarrow-up-right

    0xD74644F4ed013DC5f63fe2a576e5fBF6070AEC00arrow-up-right

    HashflowExecutorarrow-up-right

    0x93Fc40cD88B54f2CBCbF182fa1c78522805B213Aarrow-up-right

    FluidV1Executorarrow-up-right

    0xe7B267d06Df83c8fEcAd18af8be0CeF54068F138arrow-up-right

    RocketpoolExecutorarrow-up-right

    0x6Ad86dec4c9b897640730eaEdF8ff4659A3be8BEarrow-up-right

    ERC4626Executorarrow-up-right

    0x11C1951e404e1a2A18a046F22501019a02C23D0Barrow-up-right

    TychoRouterarrow-up-right

    0x06dd6A2e78c757BE63BC47DaA841Cf1a8B50ee5Earrow-up-right

    FeeCalculatorarrow-up-right

    0x8cfea5cc589155C0128267e8D2b027362b661b14arrow-up-right

    UniswapV2Executorarrow-up-right

    0xD689b184C250E543eB3938D524733Ff6B4cfC296arrow-up-right

    UniswapV3Executorarrow-up-right

    TychoRouterarrow-up-right

    0x1d8dfe01731fcd0ac9b2fe4d0fb238c42d71cf74arrow-up-right

    FeeCalculatorarrow-up-right

    0x957ea7a45a29f8d02485f3e08e47b540d3be93abarrow-up-right

    UniswapV2Executorarrow-up-right

    0x6794ce07fddc3bb5f747b4510d0ebb0d11a03f2carrow-up-right

    UniswapV3Executorarrow-up-right

    TychoRouterarrow-up-right
    0x1f8dB310f32D48B6180fF902EC60C586128cEf47arrow-up-right

    hashtag
    Health check endpoint

    get
    /v1/health

    This endpoint is used to check the health of the service.

    Authorizations
    authorizationstringRequired

    Use 'sampletoken' as value for testing

    Responses
    chevron-right
    200

    OK

    application/json
    or
    or
    get
    /v1/health
    200

    OK

    hashtag
    Retrieve protocol components

    post
    /v1/protocol_components

    This endpoint retrieves components within a specific execution environment, filtered by various criteria.

    Authorizations
    authorizationstringRequired

    Use 'sampletoken' as value for testing

    Body
    chainstring Β· enumOptional

    Currently supported Blockchains

    Possible values:
    component_idsstring[] Β· nullableOptional

    Filter by component ids

    protocol_systemstringRequired

    Filters by protocol, required to correctly apply unconfirmed state from ReorgBuffers

    tvl_gtnumber Β· double Β· nullableOptional

    The minimum TVL of the protocol components to return, denoted in the chain's native token.

    hashtag
    Retrieve protocol states

    post
    /v1/protocol_state

    This endpoint retrieves the state of protocols within a specific execution environment.

    Authorizations
    authorizationstringRequired

    Use 'sampletoken' as value for testing

    Body

    Max page size supported is 100

    chainstring Β· enumOptional

    Currently supported Blockchains

    Possible values:
    include_balancesbooleanOptional

    Whether to include account balances in the response. Defaults to true.

    protocol_idsstring[] Β· nullableOptional

    Filters response by protocol components ids

    protocol_systemstringRequired

    Filters by protocol, required to correctly apply unconfirmed state from ReorgBuffers

    hashtag
    Retrieve protocol systems

    post
    /v1/protocol_systems

    This endpoint retrieves the protocol systems available in the indexer.

    Authorizations
    authorizationstringRequired

    Use 'sampletoken' as value for testing

    Body
    chainstring Β· enumOptional

    Currently supported Blockchains

    Possible values:
    Responses
    chevron-right
    200

    OK

    application/json
    protocol_systemsstring[]Required

    List of currently supported protocol systems

    post
    /v1/protocol_systems
    200

    OK

    hashtag
    Retrieve tokens

    post
    /v1/tokens

    This endpoint retrieves tokens for a specific execution environment, filtered by various criteria. The tokens are returned in a paginated format.

    Authorizations
    authorizationstringRequired

    Use 'sampletoken' as value for testing

    Body
    chainstring Β· enumOptional

    Currently supported Blockchains

    Possible values:
    min_qualityinteger Β· int32 Β· nullableOptional

    Quality is between 0-100, where:

    • 100: Normal ERC-20 Token behavior
    • 75: Rebasing token
    • 50: Fee-on-transfer token
    • 10: Token analysis failed at first detection
    • 5: Token analysis failed multiple times (after creation)
    • 0: Failed to extract attributes, like Decimal or Symbol
    token_addressesstring[] Β· nullableOptional

    Filters tokens by addresses

    traded_n_days_agointeger Β· int64 Β· nullableOptional

    Filters tokens by recent trade activity

    hashtag
    Retrieve contract states

    post
    /v1/contract_state

    This endpoint retrieves the state of contracts within a specific execution environment. If no contract ids are given, all contracts are returned. Note that protocol_system is not a filter; it's a way to specify the protocol system associated with the contracts requested and is used to ensure that the correct extractor's block status is used when querying the database. If omitted, the block status will be determined by a random extractor, which could be risky if the extractor is out of sync. Filtering by protocol system is not currently supported on this endpoint and should be done client side.

    Authorizations
    authorizationstringRequired

    Use 'sampletoken' as value for testing

    Body

    Maximum page size for this endpoint is 100

    chainstring Β· enumOptional

    Currently supported Blockchains

    Possible values:
    contract_idsstring[] Β· nullableOptional

    Filters response by contract addresses

    protocol_systemstringOptional

    Does not filter response, only required to correctly apply unconfirmed state from ReorgBuffers

    Tycho Client

    Tycho Client helps you consume data from Tycho Indexer. It's the recommended way to connect to the Indexer data stream, whether you're using our or running your own instance.

    In this guide, you'll learn more about the Tycho Client and the streamed data models.

    circle-check

    If you are developing in Rust and is using Tycho to simulate DeFi Protocol's behavior, we recommend checking out our package - this tool extends Tycho Client's data streaming functionality with powerful simulation capabilities.

    Execution

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

    1. SwapEncoder (Rust struct) – Handles swap encoding.

    2. Executor (Solidity contract) – Executes the swap on-chain.

    See more about our code architecture .

    Grafanatycho.livechevron-right
    0x6e644B877c1247c8ab8613fE0787a7B444768d23arrow-up-right
    PancakeswapV3Executorarrow-up-right
    0x6e644B877c1247c8ab8613fE0787a7B444768d23arrow-up-right
    UniswapV4Executorarrow-up-right
    0x711EA5D03541AB0ee84711D1ED92B5De0bB178c1arrow-up-right
    BebopExecutorarrow-up-right
    0xbfb5fBfD4C4182D3A1df91CEd45335C543BED3f0arrow-up-right
    AerodromeSlipstreamsExecutorarrow-up-right
    0x8fccDb466b71B715355919b3Db0f6B14bfba9dD7arrow-up-right
    0x7270991eb529c7e45a3ec98de542b01e4da32d82arrow-up-right
    UniswapV4Executorarrow-up-right
    0x45356073646354f162982bed83c71ef0f310e201arrow-up-right
    VelodromeSlipstreamsExecutorarrow-up-right
    0xb208092276fde05cff20341049f1e384b1b31112arrow-up-right
    CurveExecutorarrow-up-right
    0xbc4d9e944ad40480a34ebaf38cd2acf6e1dc0defarrow-up-right
    Responses
    chevron-right
    200

    OK

    application/json

    Response from Tycho server for a protocol components request.

    post
    /v1/protocol_components
    200

    OK

    Responses
    chevron-right
    200

    OK

    application/json
    post
    /v1/protocol_state
    200

    OK

    Responses
    chevron-right
    200

    OK

    application/json

    Response from Tycho server for a tokens request.

    post
    /v1/tokens
    200

    OK

    Responses
    chevron-right
    200

    OK

    application/json

    Response from Tycho server for a contract state request.

    post
    /v1/contract_state
    200

    OK

    POST /v1/protocol_components HTTP/1.1
    Host: tycho-beta.propellerheads.xyz
    authorization: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 119
    
    {
      "chain": "ethereum",
      "component_ids": [
        "text"
      ],
      "pagination": {
        "page": 1,
        "page_size": 1
      },
      "protocol_system": "text",
      "tvl_gt": 1
    }
    {
      "pagination": {
        "page": 1,
        "page_size": 1,
        "total": 1
      },
      "protocol_components": [
        {
          "chain": "ethereum",
          "change": "Update",
          "contract_ids": [
            "text"
          ],
          "created_at": "2026-03-27T00:26:43.001Z",
          "creation_tx": "text",
          "id": "text",
          "protocol_system": "text",
          "protocol_type_name": "text",
          "static_attributes": {
            "ANY_ADDITIONAL_PROPERTY": "text"
          },
          "tokens": [
            "text"
          ]
        }
      ]
    }
    POST /v1/protocol_state HTTP/1.1
    Host: tycho-beta.propellerheads.xyz
    authorization: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 236
    
    {
      "chain": "ethereum",
      "include_balances": true,
      "pagination": {
        "page": 1,
        "page_size": 1
      },
      "protocol_ids": [
        "text"
      ],
      "protocol_system": "text",
      "version": {
        "block": {
          "chain": "ethereum",
          "hash": "text",
          "number": 1
        },
        "timestamp": "2026-03-27T00:26:43.001Z"
      }
    }
    {
      "pagination": {
        "page": 1,
        "page_size": 1,
        "total": 1
      },
      "states": [
        {
          "attributes": {
            "ANY_ADDITIONAL_PROPERTY": "text"
          },
          "balances": {
            "ANY_ADDITIONAL_PROPERTY": "text"
          },
          "component_id": "text"
        }
      ]
    }
    POST /v1/tokens HTTP/1.1
    Host: tycho-beta.propellerheads.xyz
    authorization: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 123
    
    {
      "chain": "ethereum",
      "min_quality": 1,
      "pagination": {
        "page": 1,
        "page_size": 1
      },
      "token_addresses": [
        "text"
      ],
      "traded_n_days_ago": 1
    }
    {
      "pagination": {
        "page": 1,
        "page_size": 1,
        "total": 1
      },
      "tokens": [
        {
          "address": "0xc9f2e6ea1637E499406986ac50ddC92401ce1f58",
          "chain": "ethereum",
          "decimals": 1,
          "gas": [
            1
          ],
          "quality": 1,
          "symbol": "WETH",
          "tax": 1
        }
      ]
    }
    POST /v1/contract_state HTTP/1.1
    Host: tycho-beta.propellerheads.xyz
    authorization: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 212
    
    {
      "chain": "ethereum",
      "contract_ids": [
        "text"
      ],
      "pagination": {
        "page": 1,
        "page_size": 1
      },
      "protocol_system": "text",
      "version": {
        "block": {
          "chain": "ethereum",
          "hash": "text",
          "number": 1
        },
        "timestamp": "2026-03-27T00:26:43.001Z"
      }
    }
    {
      "accounts": [
        {
          "address": "0xc9f2e6ea1637E499406986ac50ddC92401ce1f58",
          "balance_modify_tx": "0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4",
          "chain": "ethereum",
          "code": "0xBADBABE",
          "code_hash": "0x123456789",
          "code_modify_tx": "0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4",
          "creation_tx": "0x8f1133bfb054a23aedfe5d25b1d81b96195396d8b88bd5d4bcf865fc1ae2c3f4",
          "native_balance": "0x00",
          "slots": {
            "0x....": "0x...."
          },
          "title": "Protocol Vault",
          "token_balances": {
            "0x....": "0x...."
          }
        }
      ],
      "pagination": {
        "page": 1,
        "page_size": 1,
        "total": 1
      }
    }
    GET /v1/health HTTP/1.1
    Host: tycho-beta.propellerheads.xyz
    authorization: YOUR_API_KEY
    Accept: */*
    
    POST /v1/protocol_systems HTTP/1.1
    Host: tycho-beta.propellerheads.xyz
    authorization: YOUR_API_KEY
    Content-Type: application/json
    Accept: */*
    Content-Length: 58
    
    {
      "chain": "ethereum",
      "pagination": {
        "page": 1,
        "page_size": 1
      }
    }
    {
      "pagination": {
        "page": 1,
        "page_size": 1,
        "total": 1
      },
      "protocol_systems": [
        "text"
      ]
    }
    {
      "message": "No db connection",
      "status": "NotReady"
    }
    circle-info

    ✨ New: Sub-Second Latency with Partial Blocks

    Tycho now provides early support for partial blocks on Base, enabling sub-second latency by streaming pre-confirmation updates. Enable via --partial-blocks (CLI), partial_blocks=True (Python), or .enable_partial_blocks() (Rust). See Streaming Options below for details.

    hashtag
    Key Features

    • Real-Time Streaming: Get low-latency updates to stay in sync with the latest protocol changes. Discover new pools as they’re created.

    • TVL Filtering: Receive updates only for pools exceeding a specified TVL threshold (denominated in the Chain's Native Token).

    • Support for multiple protocols and chains

    hashtag
    Available Clients

    The client is written in Rust and available as:

    • Rust Client

    • Binary / CLI

    • Python Client

    Follow one of the guides above to learn how to set up the client appropriate for you.

    We welcome community contributions to expand language support. See our contribution guidelines How to Contribute.


    hashtag
    Authentication

    Currently, interacting with the hosted Tycho Indexer requires a personalized API Key. Please contact @tanay_j on Telegram to get your API key.


    hashtag
    Usage

    Tycho Client provides a stream of protocol components, snapshots, their state changes, and associated tokens. For simplicity, we will use Tycho Client Binary as a reference, but the parameters described below are also available for our Rust and Python versions.

    circle-info

    Note: While Tycho takes chain as a parameter, it is designed to support streaming from a single chain. If you want to consume data from multiple chains you will need to use more than one client connection.

    hashtag
    Component Filtering

    You can request individual pools or use a minimum TVL threshold to filter the components. If you choose minimum TVL tracking, Tycho-client will automatically add snapshots for any components that exceed the TVL threshold, e.g., because more liquidity was provided. It will also notify you and remove any components that fall below the TVL threshold. Note that the TVL values are estimates intended solely for filtering the most relevant components.

    TVL Filtering:

    circle-info

    Tycho indexes all the components in a Protocol. TVL filtering is highly encouraged to speed up data transfer and processing times by reducing the number of returned components.

    TVL is measured in the chain's native currency (e.g., 1 00 ETH on Ethereum Mainnet).

    You can filter by TVL in 2 ways:

    1. Set an exact TVL boundary:

    This will stream updates for all components whose TVL exceeds the minimum threshold set. Note: if a pool fluctuates in TVL close to this boundary, the client will emit a message to add/remove that pool every time it crosses that boundary. To mitigate this, please use the ranged tv boundary described below.

    1. Set a ranged TVL boundary (recommended):

    This will stream state updates for all components whose TVL exceeds the add-tvl-threshold. It will continue to track already added components if they drop below the add-tvl-threshold, only emitting a message to remove them if they drop below remove-tvl-threshold.

    hashtag
    Streaming Options

    Tycho Client supports several options to customize the data stream. These are available as CLI flags, Rust builder methods, and Python parameters. Refer to each client's documentation for usage details.

    Option
    Description
    When to use

    Partial blocks

    Stream pre-confirmation blocks

    For lower stream latency

    No state

    Stream only component metadata and tokens

    For lower stream latency

    circle-info

    Note: disable compression and no tls are not intended to be used with the hosted Tycho Indexer endpoints.

    chevron-rightDetails: Partial Blockshashtag

    Some chains, such as Basearrow-up-right, support flash blocks - pre-confirmation updates that contain parts of a future block before its construction is finished. When partial blocks is enabled, Tycho streams these incremental updates as they arrive, giving you sub-block latency. On chains without flash block support, enabling this flag is unsupported.

    circle-exclamation

    Block hashes in partial block messages are unstable β€” they change between partial updates and will differ from the final block hash. Do not use them as persistent identifiers or cache keys. See the for details.

    chevron-rightDetails: No Statehashtag

    By default, the first sync message includes full component snapshots, and every subsequent block includes state deltas (reserves, balances, contract storage). If you only need to discover which components exist and which tokens they involve, such as to build a pool registry or monitor new deployments, you can disable state monitoring with no state. This significantly reduces message sizes, startup time, and processing overhead, as both snapshots and per-block state updates are omitted entirely.

    chevron-rightDetails: Include TVLhashtag

    When enabled, each message includes an approximate TVL estimate for every tracked component. This is useful for building dashboards or for ranking pools by liquidity. Note that enabling this option increases startup latency: for each snapshot request, the client makes additional RPC calls to fetch token prices and compute TVL for all tracked components. This overhead scales with the number of components you're tracking.


    hashtag
    Understanding Tycho Client Messages

    Tycho emits data in an easy-to-read JSON format. Get granular updates on each block:

    • Snapshots for complete component (or pool) states,

    • Deltas for specific updates, and

    • Removal notices for components that no longer match your filtration criteria.

    • Extractor status for keeping track of the sync status of each extractor.

    chevron-rightBlock message examplehashtag

    Each message includes block details to help you stay on track with the latest block data.

    FeedMessage

    The main outer message type. It contains both the individual SynchronizerState (one per extractor) and the StateSyncMessage (also one per extractor). Each extractor is supposed to emit one message per block (even if no changes happened in that block) and metadata about the extractor's block synchronization state. The latter allows consumers to handle delayed extractors gracefully.

    SynchronizerState (sync_states)

    This struct contains metadata about the extractor's block synchronization state. It allows consumers to handle delayed extractors gracefully. Extractors can have any of the following states:

    • Ready: the extractor is in sync with the expected block

    • Advanced: the extractor is ahead of the expected block

    • Delayed: the extractor has fallen behind on recent blocks but is still active and trying to catch up

    • Stale: the extractor has made no progress for a significant amount of time and is flagged to be deactivated

    • Ended: the synchronizer has ended, usually due to a termination or an error

    StateSyncMessage (state_msgs )

    This struct, as the name states, serves to synchronize the state of any consumer to be up-to-date with the blockchain.

    The attributes of this struct include the header (block information), snapshots, deltas, and removed components.

    • Snapshots are provided for any components that have NOT been observed yet by the client. A snapshot contains the entire state at the header.

    • Deltas contain state updates observed after or at the snapshot. Any components mentioned in the snapshots and deltas within the same StateSynchronization message must have the deltas applied to their snapshot to arrive at a correct state for the current header.

    • Removed components is a map of components that should be removed by consumers. Any components mentioned here will not appear in any further messages/updates.

    Snapshots

    Snapshots are simple messages that contain the complete state of a component (ComponentWithState) along with the related contract data (ResponseAccount). Contract data is only emitted for protocols that require vm simulations, it is omitted for protocols implemented natively (like UniswapV2 - see the list of Supported Protocolsand how they're implemented).

    Snapshots are only emitted once per protocol, upon the client's startup. All the state is updated later via deltas from the next block onwards.

    circle-info

    Note: for related tokens, only their addresses are emitted with the component snapshots. If you require more token information, you can request using Tycho RPC's Tycho RPCendpoint

    ComponentWithState

    Tycho differentiates between component and component state.

    The component itself is static: it describes, for example, which tokens are involved or how much fees are charged (if this value is static).

    The component state is dynamic: it contains attributes that can change at any block, such as reserves, balances, etc.

    ResponseAccount

    This contains all contract data needed to perform simulations. This includes the contract address, code, storage slots, native balance, account balances, etc.

    Deltas

    Deltas contain only targeted changes to the component state. They are designed to be lightweight and always contain absolute new values. They will never contain delta values so that clients have an easy time updating their internal state.

    Deltas include the following few special attributes:

    • state_updates: Includes attribute changes, given as a component to state key-value mapping, with keys being strings and values being bytes. The attributes provided are protocol-specific. Tycho occasionally makes use of reserved attributes, see herearrow-up-right for more details.

    • account_updates: Includes contract storage changes given as a contract storage key-value mapping for each involved contract address. Here, both keys and values are bytes.

    • new_protocol_components: Components that were created on this block. Must not necessarily pass the tvl filter to appear here.

    • deleted_protocol_components: Any components mentioned here have been removed from the protocol and are not available anymore.

    • new_tokens: Token metadata of all newly created components.

    • component_balances: Balances changes are emitted for every tracked protocol component.

    • component_tvl: If there was a balance change in a tracked component, the new tvl for the component is emitted.

    • account_balances: For protocols that need the balance (both native and ERC-20) of accounts tracked for the simulation package (like BalancerV3 which needs the Vault balances), the updated balances are emitted.

    Note: exact byte encoding might differ depending on the protocol, but as a general guideline integers are big-endian encoded.

    hosted endpoint
    Simulation
    hashtag
    Encoder Interface

    Each new protocol requires a dedicated SwapEncoder that implements the SwapEncoder trait. This trait defines how swaps for the protocol are encoded into calldata.

    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 herearrow-up-right.

    If your protocol needs some specific constant addresses please add them in config/protocol_specific_addresses.jsonarrow-up-right.

    After implementing your SwapEncoder , you need to:

    • Add your protocol with a placeholder address in: config/executor_addresses.jsonarrow-up-right and config/test_executor_addresses.jsonarrow-up-right

    • Add your protocol in the SwapEncoderRegisterarrow-up-right (if you want it to be one of the default protocols)

    chevron-rightProtocols Supporting Consecutive Swap Optimizationshashtag

    As described in the Swap Group section, our encoding supports protocols which save token transfers between consecutive swaps using systems such as flash accounting. In such cases, as shown in the diagram below using Uniswap V4 as an example, the SwapEncoder is still only in charge of encoding a single swap. These swaps will then be concatenated at the StrategyEncoder level as a single executor call.

    Depending on the index of the swap in the swap group, the encoder may be responsible for adding additional information which is not necessary in other swaps of the sequence (see the first swap in the diagram below).

    Diagram representing swap groups

    hashtag
    Swap Interface

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

    The IExecutor interface requires three methods:

    hashtag
    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.

    hashtag
    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.

    • tokenOut: The output token address.

    • outputToRouter: Whether the protocol automatically sends the output token back to the TychoRouter. 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.

    hashtag
    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).

    hashtag
    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 ICallbackarrow-up-right.

    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.

    Callback Flow

    When a protocol initiates a callback during swap execution, it flows through the TychoRouter'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.

    hashtag
    Token Transfers

    Executors do not handle any token transfers. All ERC20 token transfers are orchestrated by the Dispatcher via the TransferManagerarrow-up-right. 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.

    hashtag
    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

    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.

    hashtag
    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.

    hashtag
    Native Token Address Handling

    When encoding swaps, you may need to handle address conversions for native tokens.

    hashtag
    Converting Zero Address to Protocol-Specific Address

    Tycho uses the zero address (0x0000000000000000000000000000000000000000) to represent native tokens across all chains during indexing and simulation. However, if your protocol's contracts expect a different address conventionβ€”such as 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeEβ€”you must convert the address when encoding.

    In your SwapEncoder implementation:

    1. Check if the input or output token is the zero address

    2. If your protocol requires a different sentinel address for native tokens, convert it in the encoding step

    3. Ensure the conversion happens only in the calldata generation, not in the protocol state

    This ensures compatibility with your protocol's on-chain contracts while maintaining Tycho's standardized native token representation throughout indexing and simulation.

    hashtag
    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 input swap amount when calling swap but only transfer the input token in the callback.

    hashtag
    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 TychoRouter integration test

    hashtag
    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.

    hashtag
    2. Full TychoRouter 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 .

    • 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.

    hashtag
    Deploying and Whitelisting

    Once your implementation is approved:

    1. Deploy the executor contract on the appropriate network (more herearrow-up-right).

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

    3. 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.

    here
    {
      "state_msgs": {
        "uniswap_v2": {
          "header": {
            "hash": "0x063a4837d7689df84c3b106be6ee1a31a65afb7122f9847bf566a3f97fdd6dd7",
            "number": 21926578,
            "parent_hash": "0xef792af9f9cc6036a4b7d8fb66879162e5b6edd30a6d4f1eec817be91bc950b1",
            "revert": false
          },
          "snapshots": {
            "states": {
              "0x21b8065d10f73ee2e260e5b47d3344d3ced7596e": {
                "state": {
                  "component_id": "0x21b8065d10f73ee2e260e5b47d3344d3ced7596e",
                  "attributes": {
                    "reserve0": "0x019cd10cabe7a7916b2963a5",
                    "reserve1": "0x064e2eb1ad62df7d3620"
                  },
                  "balances": {
                    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "0x064e2eb1ad62df7d3620",
                    "0x66a0f676479cee1d7373f3dc2e2952778bff5bd6": "0x019cd10cabe7a7916b2963a5"
                  }
                },
                "component": {
                  "id": "0x21b8065d10f73ee2e260e5b47d3344d3ced7596e",
                  "protocol_system": "uniswap_v2",
                  "protocol_type_name": "uniswap_v2_pool",
                  "chain": "ethereum",
                  "tokens": [
                    "0x66a0f676479cee1d7373f3dc2e2952778bff5bd6",
                    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
                  ],
                  "contract_ids": [],
                  "static_attributes": {
                    "pool_address": "0x21b8065d10f73ee2e260e5b47d3344d3ced7596e",
                    "fee": "0x1e"
                  },
                  "change": "Creation",
                  "creation_tx": "0xdd4b8bb7d2965ff7aa72e1c588fa0b57a69c83cad511fff0ae8356617c5e6fa3",
                  "created_at": "2020-12-22T17:13:12"
                }
              },
              "0xa43fe16908251ee70ef74718545e4fe6c5ccec9f": {
                "state": {
                  "component_id": "0xa43fe16908251ee70ef74718545e4fe6c5ccec9f",
                  "attributes": {
                    "reserve1": "0x01a43a590836b94fa2ba",
                    "reserve0": "0x1d9b4fe1831a31d214d18686b4"
                  },
                  "balances": {
                    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "0x01a43a590836b94fa2ba",
                    "0x6982508145454ce325ddbe47a25d4ec3d2311933": "0x1d9b4fe1831a31d214d18686b4"
                  }
                },
                "component": {
                  "id": "0xa43fe16908251ee70ef74718545e4fe6c5ccec9f",
                  "protocol_system": "uniswap_v2",
                  "protocol_type_name": "uniswap_v2_pool",
                  "chain": "ethereum",
                  "tokens": [
                    "0x6982508145454ce325ddbe47a25d4ec3d2311933",
                    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
                  ],
                  "contract_ids": [],
                  "static_attributes": {
                    "pool_address": "0xa43fe16908251ee70ef74718545e4fe6c5ccec9f",
                    "fee": "0x1e"
                  },
                  "change": "Creation",
                  "creation_tx": "0x273894b35d8c30d32e1ffa22ee6aa320cc9f55f2adbba0583594ed47c031f6f6",
                  "created_at": "2023-04-14T17:21:11"
                }
              }
            },
            "vm_storage": {}
          },
          "deltas": {
            "extractor": "uniswap_v2",
            "chain": "ethereum",
            "block": {
              "number": 21926578,
              "hash": "0x063a4837d7689df84c3b106be6ee1a31a65afb7122f9847bf566a3f97fdd6dd7",
              "parent_hash": "0xef792af9f9cc6036a4b7d8fb66879162e5b6edd30a6d4f1eec817be91bc950b1",
              "chain": "ethereum",
              "ts": "2025-02-25T23:18:59"
            },
            "finalized_block_height": 21926513,
            "revert": false,
            "new_tokens": {},
            "account_updates": {},
            "state_updates": {},
            "new_protocol_components": {},
            "deleted_protocol_components": {},
            "component_balances": {},
            "component_tvl": {}
          },
          "removed_components": {}
        }
      },
      "sync_states": {
        "uniswap_v2": {
          "status": "ready",
          "hash": "0x063a4837d7689df84c3b106be6ee1a31a65afb7122f9847bf566a3f97fdd6dd7",
          "number": 21926578,
          "parent_hash": "0xef792af9f9cc6036a4b7d8fb66879162e5b6edd30a6d4f1eec817be91bc950b1",
          "revert": false
        }
      }
    }
    tycho-client --min-tvl 100 --exchange uniswap_v2
    tycho-client --remove-tvl-threshold 95 --add-tvl-threshold 100 --exchange uniswap_v3
    fn encode_swap(
        &self,
        swap: Swap,
        encoding_context: EncodingContext,
    ) -> Result<Vec<u8>, EncodingError>;
    function swap(uint256 amountIn, bytes calldata data, address receiver)
        external
        payable;
    function getTransferData(bytes calldata data)
        external
        payable
        returns (
            TransferManager.TransferType transferType,
            address receiver,
            address tokenIn,
            address tokenOut,
            bool outputToRouter
        );
    function fundsExpectedAddress(bytes calldata data)
        external
        returns (address receiver);
    function handleCallback(
        bytes calldata data
    ) external returns (bytes memory result);
    
    function verifyCallback(bytes calldata data) external view;
    
    function getCallbackTransferData(bytes calldata data)
        external
        payable
        returns (
            TransferManager.TransferType transferType,
            address receiver,
            address tokenIn,
            uint256 amountIn
        );
     contract TychoRouterForYouProtocolTest is TychoRouterTestSetup {
        function getForkBlock() public pure override returns (uint256) {
            return 22644371; // Use a block that fits your test scenario
        }
    
        function testSingleYourProtocolIntegration() public {
            ...
        }
    }

    Include TVL

    Attach approximate TVL estimates to each component

    For getting components TVL

    No TLS

    Use unencrypted transports (http/ws)

    For local self-hosted indexers

    Disable compression

    Turn off stream message compression

    Debugging Only

    Substreams documentationarrow-up-right

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

    config/test_executor_addresses.jsonarrow-up-right
    Output of a SwapEncoder for a group swap

    Complete Case Study: Euler Hooks (External Liquidity Example)

    circle-info

    ⚠️ Important Context: Euler represents a hook with EXTERNAL LIQUIDITY. This case study demonstrates implementing custom metadata generators and parsers. If your hook is Composable and uses internal PoolManager liquidity, your hook should be already indexed by Tycho.

    This section provides a comprehensive walkthrough of the Euler hook integration as a real-world example of handling external liquidity.

    hashtag
    Euler Vault Architecture

    What is Euler? Euler is a lending protocol that allows users to deposit tokens into vaults to earn yield. Each vault is an ERC-4626 compliant contract that manages deposits and withdrawals.

    Euler is a standalone protocol, that designed an interface to be Hook-compliant, allowing it to be accessible by UniswapV4 Pools. This is a common pattern with current hooks, and are considered by Tycho Hooks with External Liquidity.

    You can learn more about the protocol

    Euler Hook Pattern (External Liquidity):

    Contrast with Internal Liquidity:

    Why Euler Requires Custom Implementation:

    1. External Balances: Tokens are in Euler vaults, not PoolManager β†’ Need MetadataRequestGenerator

    2. Withdrawal Limits: Vaults have maximum withdrawal amounts β†’ Need limits fetching logic

    3. Yield Accrual: Balances increase over time from lending yield β†’ Need periodic balance updates

    What Euler Does NOT Need:

    • ❌ Custom Hook Orchestrator (default works fine)

    • ❌ Special entrypoint encoding (standard Uniswap V4 swaps)

    • ❌ Custom state transformations

    hashtag
    Implementation Walkthrough

    1. Balance Collection

    Objective: Query the current token reserves in the Euler vaults.

    Approach: Euler hooks implement a getReserves() function that returns the current balances of both tokens.

    Code:

    Response Format:

    Parsing:

    2. Limits Collection Using Lens Contract

    Objective: Determine the maximum swap amounts for each direction (token0β†’token1, token1β†’token0).

    Challenge: Euler vaults have withdrawal limits that depend on available liquidity, which requires complex calculations involving multiple contract calls.

    Solution: Deploy a "lens" contract via state overrides that performs the calculation in a single eth_call.

    Lens Contract Pattern:

    Request Generation:

    Response Format:

    Parsing:

    3. Entrypoint Generation with Detected Slots

    Objective: Generate entrypoints that simulate swaps with correct balance overwrites for both PoolManager and external vault tokens.

    Process:

    1. Estimate Swap Amounts (using limits):

    1. Detect Balance Slots (for wstETH, WETH, etc.):

    1. Build State Overrides:

    1. Create Entrypoint:

    4. Full Processing Flow

    Initialization (one-time):

    Block Processing (per block):

    hashtag
    Key Takeaways from Euler

    1. Balance Slot Detection: Essential for hooks with external token holdings

    2. Lens Contract Pattern: Powerful technique for complex multi-call queries using state overrides

    3. Limits-Based Estimation: Provides more accurate swap amount samples than balance-based

    The Euler implementation demonstrates that with proper metadata collection and entrypoint generation, the Hooks DCI can handle even complex external liquidity scenarios.

    Multiple Vaults: Each token pair might use different vault addresses β†’ Need parser logic

    Default Orchestrator: Often sufficient even for complex hooks like Euler
  • State Override Composition: Combine router deployment, ERC6909 overwrites, and ERC20 overwrites in a single call

  • herearrow-up-right
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Uniswap V4 Euler Hook   β”‚
    β”‚  (Liquidity Coordinator) β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 β”‚ Manages deposits/withdrawals
                 ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Euler Vault Contract    β”‚  ← EXTERNAL liquidity storage
    β”‚  - Token0 deposited      β”‚
    β”‚  - Token1 deposited      β”‚
    β”‚  - Earns lending yield   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    Internal Liquidity Hook (No Custom Code Needed):
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Uniswap V4 Hook         β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                 β”‚
                 ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  PoolManager (ERC6909)   β”‚  ← INTERNAL liquidity storage
    β”‚  - Automatic extraction  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    // From: euler/metadata_generator.rs
    
    fn create_balance_request(
        &self,
        component: &ProtocolComponent,
        block: &Block,
        hook_address: &Address,
    ) -> Result<MetadataRequest, MetadataError> {
        Ok(MetadataRequest {
            request_type: MetadataRequestType::ComponentBalance {
                token_addresses: component.tokens.clone(),
            },
            routing_key: "rpc_default".to_string(),
            generator_name: "euler".to_string(),
            transport: RpcTransport::new(
                self.rpc_url.clone(),
                "eth_call".to_string(),
                vec![
                    json!({
                        "to": hook_address,
                        "data": "0x0902f1ac" // getReserves() selector
                    }),
                    json!(format!("0x{:x}", block.number)),
                ],
            ),
        })
    }
    0x
      0000000000000000000000000000000000000000000000000de0b6b3a7640000  // reserve0 (1e18)
      0000000000000000000000000000000000000000000000000de0b6b3a7640000  // reserve1 (1e18)
    // From: euler/metadata_generator.rs
    
    fn parse_balance_response(&self, res_str: &str) -> Result<MetadataValue, MetadataError> {
        // Extract two 32-byte values
        let balance_0 = Bytes::from(&res_str[0..64]);
        let balance_1 = Bytes::from(&res_str[64..128]);
    
        // Map to sorted tokens
        let mut tokens = component.tokens.clone();
        tokens.sort();
    
        let mut balances = HashMap::new();
        balances.insert(tokens[0].clone(), balance_0);
        balances.insert(tokens[1].clone(), balance_1);
    
        Ok(MetadataValue::Balances(balances))
    }
    // Simplified EulerLensContract
    contract EulerLensContract {
        address public hook; // Stored in slot 0
        
        function getLimits(address tokenIn, address tokenOut)
            external
            view
            returns (uint256 realInLimit, uint256 realOutLimit)
        {
            IEulerSwap pool = IEulerSwap(hookAddress);
    
            // Step 1: Get the protocol limits
            (uint256 inLimit, uint256 outLimit) = pool.getLimits(tokenIn, tokenOut);
    
            // If no limits returned (e.g., not authorized), return zeros
            if (inLimit == 0 && outLimit == 0) {
                return (0, 0);
            }
    
            // Step 2: Compute quotes in both directions
            uint256 quotedOutFromIn;
            uint256 requiredInFromOut;
            bool exactInSucceeded = false;
            bool exactOutSucceeded = false;
    
            // Try exactIn = inLimit
            try pool.computeQuote(tokenIn, tokenOut, inLimit * 99 / 100, true) returns (uint256 quotedOut) {
                quotedOutFromIn = quotedOut;
                exactInSucceeded = true;
            } catch {}
    
            // Try exactOut = outLimit
            try pool.computeQuote(tokenIn, tokenOut, outLimit * 99 / 100, false) returns (uint256 requiredIn) {
                requiredInFromOut = requiredIn;
                exactOutSucceeded = true;
            } catch {}
    
            // Step 3: If both failed, revert
            if (!exactInSucceeded && !exactOutSucceeded) {
                revert QuoteComputationFailed();
            }
    
            // Step 4: Keep the smallest valid limits
            if (exactInSucceeded && exactOutSucceeded) {
                // Both succeeded - take the minimum of both approaches
                uint256 outLimitFromIn = quotedOutFromIn < outLimit ? quotedOutFromIn : outLimit;
                uint256 inLimitFromOut = requiredInFromOut < inLimit ? requiredInFromOut : inLimit;
    
                // Choose the approach that gives the smallest limits
                realInLimit = inLimitFromOut < inLimit ? inLimitFromOut : inLimit;
                realOutLimit = outLimitFromIn < outLimit ? outLimitFromIn : outLimit;
            } else if (exactInSucceeded) {
                // Only exactIn succeeded
                realInLimit = inLimit;
                realOutLimit = quotedOutFromIn < outLimit ? quotedOutFromIn : outLimit;
            } else {
                // Only exactOut succeeded
                realInLimit = requiredInFromOut < inLimit ? requiredInFromOut : inLimit;
                realOutLimit = outLimit;
            }
        }
    }
    // From: euler/metadata_generator.rs
    
    fn create_limits_request(
        &self,
        component: &ProtocolComponent,
        block: &Block,
        hook_address: &Address,
        token_pair: &[Address],
    ) -> Result<MetadataRequest, MetadataError> {
        let lens_address = "0x0000000000000000000000000000000000001337";
        let lens_bytecode_hex = hex::encode(EULER_LENS_BYTECODE_BYTES);
    
        // Encode getLimits(address,address) call
        let token0_hex = &token_pair[0].to_string()[2..];  // Remove 0x prefix
        let token1_hex = &token_pair[1].to_string()[2..];
        let calldata = format!("0xaaed87a3{token0_hex}{token1_hex}");
    
        Ok(MetadataRequest {
            request_type: MetadataRequestType::Limits {
                token_pair: token_pair.to_vec(),
            },
            routing_key: "rpc_default".to_string(),
            generator_name: "euler".to_string(),
            transport: RpcTransport::new(
                self.rpc_url.clone(),
                "eth_call".to_string(),
                vec![
                    json!({
                        "to": lens_address,
                        "data": calldata
                    }),
                    json!(format!("0x{:x}", block.number)),
                    json!({
                        lens_address: {
                            // Deploy lens bytecode at deterministic address
                            "code": format!("0x{}", lens_bytecode_hex),
                            "state": {
                                // Store hook address in slot 0
                                "0x0000000000000000000000000000000000000000000000000000000000000000":
                                    format!("0x{:0>64}", &hook_address.to_string()[2..])
                            }
                        }
                    }),
                ],
            ),
        })
    }
    0x
      0000000000000000000000000000000000000000000000056bc75e2d63100000  // limit0 (100e18)
      0000000000000000000000000000000000000000000000056bc75e2d63100000  // limit1 (100e18)
    // From: euler/metadata_generator.rs
    
    fn parse_limits_response(
        &self,
        component: &ProtocolComponent,
        request: &MetadataRequest,
        res_str: &str,
        token_pair: &[Address],
    ) -> Result<MetadataValue, MetadataError> {
        // Extract limits
        let limit_0 = Bytes::from(&res_str[0..64]);
        let limit_1 = Bytes::from(&res_str[64..128]);
    
        // Create entrypoint for the limits call (for reference)
        let limits_entrypoint = create_euler_limits_entrypoint(
            component,
            hook_address,
            token_pair,
        )?;
    
        Ok(MetadataValue::Limits(vec![
            (token_pair[0].clone(), (limit_0, limit_1, Some(limits_entrypoint)))
        ]))
    }
    // From: entrypoint_generator.rs
    
    let estimator = DefaultSwapAmountEstimator::with_limits();
    let swap_amounts = estimator.estimate_swap_amounts(&metadata, &component.tokens)?;
    
    // For Euler with limits = [100e18, 100e18]:
    // swap_amounts = [
    //     (token0, token1, 1e18),   // 1% of limit
    //     (token0, token1, 10e18),  // 10% of limit
    //     (token0, token1, 50e18),  // 50% of limit
    //     (token0, token1, 95e18),  // 95% of limit
    // ]
    // From: entrypoint_generator.rs
    
    let detected_slots = balance_slot_detector
        .detect_balance_slots(
            &component.tokens,
            pool_manager,
            &block.hash,
        )
        .await?;
    
    // Returns mapping: token_address β†’ storage_slot
    // Example: wstETH β†’ 0x0000...0001 (slot 1 for standard ERC20)
    let mut state_overrides = HashMap::new();
    
    // A. Deploy V4MiniRouter
    state_overrides.insert(
        router_address,
        AccountOverrides {
            code: Some(V4_MINI_ROUTER_BYTECODE),
            balance: None,
            nonce: None,
            slots: None,
        },
    );
    
    // B. Set ERC6909 balances in PoolManager
    let erc6909_slot = calculate_erc6909_balance_slot(&sender, &token_in);
    state_overrides.insert(
        pool_manager,
        AccountOverrides {
            slots: Some(StorageOverride::Diff(
                vec![(erc6909_slot, amount_in * 2)].into_iter().collect()
            )),
            ..Default::default()
        },
    );
    
    // C. Set detected ERC20 balance slots
    if let Some(token_in_slot) = detected_slots.get(&token_in) {
        state_overrides.insert(
            token_in.clone(),
            AccountOverrides {
                slots: Some(StorageOverride::Diff(
                    vec![(token_in_slot.clone(), amount_in * 2)].into_iter().collect()
                )),
                ..Default::default()
            },
        );
    }
    // Build V4Router execute() call
    let pool_key = build_pool_key_from_component(component)?;
    let params = ExactInputSingleParams {
        pool_key,
        zero_for_one: true,
        amount_in,
        amount_out_minimum: Bytes::from([0u8]),
        hook_data: Bytes::from([0u8]),
    };
    
    let actions = vec![
        V4RouterAction::SWAP_EXACT_IN_SINGLE,
        V4RouterAction::SETTLE_ALL,
        V4RouterAction::TAKE_ALL,
    ];
    
    let calldata = encode_execute_call(actions, vec![params])?;
    
    let entrypoint = EntryPointWithTracingParams {
        entry_point: EntryPoint {
            external_id: format!("swap_{}_{}_{}_{}",
                component.id, token_in, token_out, amount_in),
            target: router_address,
            signature: "execute(bytes,bytes[])".to_string(),
        },
        params: TracingParams::RPCTracer(RPCTracerParams {
            caller: Some(sender),
            calldata,
            state_overrides: Some(state_overrides),
            prune_addresses: None,
        }),
    };
    1. Load all uniswap_v4_hooks components from database
    2. Filter for components with swap hook permissions
    3. Check if entrypoints already exist
       - Has entrypoints β†’ State = TracingComplete
       - No entrypoints β†’ State = Unprocessed
    4. Cache all components and states
    1. Extract components with balance/state changes
    2. Filter for swap hook permissions
    3. Categorize:
       - Unprocessed β†’ Full processing list
       - TracingComplete β†’ Balance-only list
       - Failed (retryable) β†’ Full processing list
       - Failed (paused) β†’ Skip
    
    4. Collect Metadata:
       - Full processing: getLimits() + getReserves()
       - Balance-only: getReserves()
    
    5. Check for metadata errors:
       - Errors β†’ Mark as Failed, increment retry_count
       - retry_count >= pause_after_retries β†’ Set "paused" attribute
    
    6. Process each component via orchestrator:
       - Generate entrypoints (if full processing)
       - Inject balances into component
       - Inject limits for optimization
       - Update block_changes
    
    7. Delegate to inner DCI:
       - Trace entrypoints
       - Store results in database
       - Prune old data
    
    8. Handle finality:
       - Prune cache layers below finalized height

    Testing

    We provide a comprehensive testing suite for the whole Tycho stack. The suite facilitates end-to-end testing and ensures your protocol integration behaves as expected. For unit tests, please use standard Rust unit testing practicesarrow-up-right.

    circle-info

    Find the suite in /protocol_testingarrow-up-right.

    hashtag
    What does the suite test?

    There are two test modes:

    • range β€” indexes and validates test cases defined in integration_test.tycho.yaml for specific block ranges.

    • full β€” indexes and validates the entire protocol history from creation to the latest block, without comparing specific component information.

    Here's what the testing suite does:

    1. Runs with your Substreams implementation for a specific block range. If running on the range test mode, it also verifies that the components' state matches the expected states specified by the testing YAML file. This confirms that your Substreams package is indexable and that it outputs what you expect.

    2. Retrieves swap quotes using . This verifies that all necessary data for simulation is indexed and, for , that the provided SwapAdapter contract works. It is important to know that the simulation engine runs entirely off-chain and only accesses the data and contracts you index (token contracts are mocked and don't need to be indexed)

    hashtag
    How to run

    hashtag
    Prerequisites

    hashtag
    Archive node

    You need an EVM Archive node to fetch the state from a previous block. If you index only with Substreams, as in Tycho's production mode, you must sync blocks since the protocol's deployment date, which can take a long time. The archive node skips this requirement by fetching all the required account's storage slots on the block you specify in the testing yaml file.

    The node also needs to support the method, which is required for our Token Quality Analysis.

    As of February 2026, Erigon is the only major client supporting debug_storageRangeAt. The following API providers support archive nodes with debug_storageRangeAt:

    hashtag
    Test Configuration

    hashtag
    Range Mode

    Range mode tests specific block intervals to verify that your Substreams implementation correctly indexes and outputs expected component states. Please make sure that this block range is as small as possible so that the test runs quickly. The purpose of this test is to validate the logic on a few blocks only; for longer tests please see test.

    hashtag
    Configuration

    Create an file in your Substreams directory with the following:

    • Target Substreams config file

    • SwapAdapter and construction arguments (for VM integrations)

    • identifier

    hashtag
    How It Works

    Each test validates your integration across the specified block range:

    1. Index blocks: Indexes all blocks between start-block and stop-block

    2. Verify state: Confirms the indexed state matches expected component creation

    3. Simulate swap: Runs get_amount_out

    This ensures your indexing captures the correct protocol state and that simulation and execution remain consistent.

    hashtag
    Full Mode

    Full mode validates the complete protocol lifecycleβ€”from indexing to live streamingβ€”ensuring your integration works end-to-end in a production-like environment.

    hashtag
    How It Works

    1. Initial Indexing

    The test indexes all blocks from start-block to the current block. Depending on the block range, this may take significant time.

    Tip: Use the --reuse-last-sync flag to skip re-indexing on subsequent runs. This reuses the existing database state and syncs only new blocks, rather than starting from scratch.

    2. Live Streaming

    Once syncing catches up, the test begins streaming blocks live, simulating a production environment. For each block and each component, it:

    1. Simulates get_amount_out using the current block state

    2. Encodes a single swap and executes it in the current block

    3. Verifies that the execution output matches the simulation output

    This validates that your indexing, simulation, and execution implementations are consistent and correct.

    ⚠️ If you encounter issues running the full test, please contact us for support.

    hashtag
    Test Parameters

    Here are the test parameters that you need to set:

    hashtag
    1. initialized_accounts

    This is a list of contract addresses that simulation requires, although their creation is not indexed within the test block range. Leave empty if not required.

    Importantly, you use this config during testing only. Your Substreams package should still properly initialise the accounts listed here. This configuration only eliminates the need to include historical blocks that contain the initialisation events in your test data. This ensures tests are targeted and quick to run.

    You can use the initialized_accounts config at two levels in the test configuration file:

    • : accounts listed here are used for all tests in this suite;

    • : accounts listed here are scoped to that test only.

    hashtag
    2. expected_components (for range mode)

    This is a list of components whose creation you are testing. It includes all component data (tokens, static attributes, etc.). You don't need to include all components created within your test block range – only those on which the test should focus.

    hashtag
    3. skip_balance_check

    By default, this should be false. Testing verifies the balances reported for the component by comparing them to the on-chain balances of the Component.id . This should be false if:

    1. the Component.id does not correlate to a contract address;

    2. balances are not stored on the component's contract (i.e. they're stored on a vault).

    If this skip is set to true, you must comment on why.

    hashtag
    4. skip_simulation

    By default this should be false . It should only be true temporarily if you want to isolate testing the indexing phase only. If set to true, you must comment on why.

    triangle-exclamation

    Code changes

    If the protocol you are integrating is not a vm integration, to be able to test simulation, you need to register it in register_decoder_for_protocol (). This is to match your protocol system name with the State that is used in Tycho Simulation.

    hashtag
    5. skip_execution

    By default this should be false . It should only be true temporarily if you want to isolate testing the indexing and simulation phases only. If set to true, you must comment on why.

    To be able to test execution, you need to provide the executor's runtime bytecode file.

    1. Export it using the helper script in (see the for instructions on how).

    2. Copy YourExecutor.runtime.json file to the SDK repository in .

    3. Import the file in and add the corresponding entry to the EXECUTOR_MAPPING .

    triangle-exclamation

    Block Compatibility Requirements

    The TychoRouter requires post-Cancun blocks for execution. Testing must use block numbers after the Cancun upgrade.

    circle-info

    Testing during development

    To test your protocol integration during development, update the tycho-simulation and tycho-execution dependencies in protocol-testing/Cargo.toml to point to your working branch/commit or to your local repository.

    Example:

    hashtag
    Running Tests

    We offer two approaches for running tests: local run and Docker run.

    Local run works best when you're actively developing your integration. You can test individual phases (indexing, simulation, execution) in isolation and get faster iteration cycles for debugging. However, you'll need to handle additional setup and prerequisites yourself.

    Docker run suits CI environments and final validation. You run the complete end-to-end test suite in an encapsulated environment, which eliminates the setup complexity you'd face otherwise. The actual test execution is fast once you have the images built, but every time you change something in your package, you'll need to rebuild the imagesβ€”and that's the slow part. This approach makes most sense once your package is stable.

    Here is how you can run the tests with each approach:

    Prerequisites:

    Before continuing, ensure the following tools and libraries are installed on your system:

    • : Containerization platform for running applications in isolated environments.

    • : Version control tool

    hashtag
    Installing or updating the Tycho Indexer version (Optional)

    If you're running on a MacOS (either Apple Silicon or Intel) - or any architecture that is not supported by pre-built releases, you need to compile the Tycho Indexer:

    Step 1: Clone Tycho-Indexer repo

    Step 2: Build the binary in release mode

    Step 3: Link the binary to a directory in your system's PATH:

    NOTE: This command requires /usr/local/bin to be included in the system's PATH. While this is typically the case, there may be exceptions.

    hashtag
    Troubleshooting

    hashtag
    Slow tests

    An integration test should take a maximum 5–10 minutes. If the tests take longer, here are key things you can explore:

    1. Ensure you have no infinite loops within your code.

    2. Ensure you're using a small block range for your test, ideally below 1,000 blocks. The blocks in your test only need to cover the creation of the component you are testing. Optionally, they can extend to blocks with changes for the component you want the test to cover. To help limit the test block range, you could explore the config.

    3. Ensure you are not indexing tokens. Token contracts use a lot of storage, so fetching their historical data is slow. Instead, they are mocked on the simulation engine and don't have to be explicitly indexed. Make an exception if they have unique behavior, like acting as both a token and a pool, or if they are rebasing tokens that provide a

    Note: Substreams uses cache to improve the speed of subsequent runs of the same module. A test's first run is always slower than subsequent runs, unless you change the Substreams module's code.

    hashtag
    Account not initialised

    There are two main causes for this error:

    1. Your Substreams package is not indexing a contract that is necessary for simulations.

    2. Your test begins at a block that is later than the block on which the contract was created. To fix this, add the missing contract to the test config.

    hashtag
    Dev Cluster Tests

    After your protocol is moved to our dev environment, it will be subject to constant indexing, simulation, and execution testing via a constant running pod in our cluster. If we find problems in any of these areas, we may reach out to you for help debugging.

    Encodes and simulates transactions using Tycho Executionarrow-up-right against an RPC on an historical block. This ensures that your protocol swaps can be executed on chain and that the indexed data and quotes match onchain state and logic.
    Expected protocol types
  • Test cases to execute

  • simulation (uses the provided
    SwapAdapter
    for VM integrations)
  • Execute swap: Encodes a single swap and simulates its execution

  • Validate consistency: Verifies that execution output matches simulation output

  • This allows you to iterate on your protocol implementation and run tests against your changes before submitting pull requests.
    Rustarrow-up-right: Programming language and toolchain
  • GCCarrow-up-right: GNU Compiler Collection

  • libpqarrow-up-right: PostgreSQL client library

  • OpenSSL (libssl)arrow-up-right: OpenSSL development library

  • pkg-configarrow-up-right: Helper tool for managing compiler flags

  • Substreams CLIarrow-up-right: Indexing tool that uses Rust modules to process blockchain data.

  • Tycho Indexerarrow-up-right: The testing module runs a minified version of Tycho Indexer. You need to ensure that the latest version is correctly setup in your PATH and if it isn't you need to (re)install Tycho. Run the following command on your terminal to check the version:\

  • Step 1: Export Environment Variables

    • RPC_URL: The URL for the Ethereum RPC endpoint. This fetches the storage data.

    • SUBSTREAMS_API_TOKEN: The JWT token for accessing Substreams services. This token is necessary for authentication. Please refer to the Substreams Authenticationarrow-up-right guide to set up and validate your token.

    • RUST_LOG: Defines the log level for test output. For enhanced debugging:

      • Indexer: Run the testing module with Tycho indexer logs enabled: RUST_LOG=tycho_client=info,tycho_indexer=info,error

      • Simulation: Set the Tycho simulation module to debug level: RUST_LOG=tycho_simulation=debug,info

      • Execution traces: Set the Tycho testing module to debug level: RUST_LOG=tycho_test=debug,info

    Step 2: Build the substreams wasm file

    If you do not have one already, you must build the wasm file of the substreams package you wish to test. This can be done by navigating to the substreams package directory and running:

    Step 3: Run a local Postgres test database using docker-compose.

    In /protocol-testing , run:

    Step 4: Run tests

    In /protocol-testing , run:

    Select range or full depending on your test mode.

    These are the optional arguments:

    Complete example

    If you want to run the range tests for ethereum-balancer-v2, use the following:

    Prerequisites:

    Before continuing, ensure the following tools and libraries are installed on your system:

    • Dockerarrow-up-right: Containerization platform for running applications in isolated environments.

    Step 1: Export Environment Variables

    • RPC_URL: The URL for the Ethereum RPC endpoint. This fetches the storage data.

    • SUBSTREAMS_API_TOKEN: The JWT token for accessing Substreams services. This token is necessary for authentication. Please refer to the guide to set up and validate your token.

    • PROTOCOLS to test, separated by space and with optional filter.

    Step 2: Build images

    Build the image at the repository root path with:

    Step 3: Run tests

    In /protocol-testing, run:

    Complete example

    If you want to run tests for ethereum-balancer-v2, use the following:

    Note that only range tests are supported with Docker.

    If /usr/local/bin is not in your PATH, you can either:
    1. Add it to your PATH by exporting it:

    2. Or create a symlink in any of the following directories (if they are in your PATH):

    Step 4: Verify Installation

    We provide a binary compiled for Linux x86/x64 architecture on our GitHub releasesarrow-up-right page.

    circle-exclamation

    This method will only work if you are running on a Linux with an x86/x64 architecture

    Step 1: Download the pre-built binary

    Navigate to the Tycho Indexer Releasesarrow-up-right page, locate the latest version (e.g.: 0.88.0) and download the tycho-indexer-x86_64-unknown-linux-gnu-{version}.tar.gz file.

    Step 2: Extract the binary from the tar.gz

    Open a terminal and navigate to the directory where the file was downloaded. Run the following command to extract the contents:

    Step 3: Link the binary to a directory in your system's PATH:

    NOTE: This command requires /usr/local/bin to be included in the system's PATH. While this is typically the case, there may be exceptions.

    If /usr/local/bin is not in your PATH, you can either:

    1. Add it to your PATH by exporting it:

    2. Or create a symlink in any of the following directories (if they are in your PATH):

    Step 4: Verify Installation

    getRate
    method.
    Tycho Indexerarrow-up-right
    Tycho Simulationarrow-up-right
    VM implementations
    debug_storageRangeAtarrow-up-right
    Chainnodesarrow-up-right
    Chainstackarrow-up-right
    full mode
    integration_test.tycho.yamlarrow-up-right
    Protocol system
    globalarrow-up-right
    test levelarrow-up-right
    herearrow-up-right
    tycho-execution/foundry/scripts/export-runtime-bytecode.js arrow-up-right
    READMEarrow-up-right
    tycho-protocol-sdk/evm/test/executorsarrow-up-right
    tycho-protocol-sdk/protocol-testing/src/execution.rsarrow-up-right
    Dockerarrow-up-right
    Gitarrow-up-right
    initialized_accounts
    initialized_accounts

    Encoding

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

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

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

    hashtag
    Models

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

    > tycho-indexer --version
    tycho-indexer 0.88.0 # should match the latest version published on GitHub
    export PATH="/usr/local/bin:$PATH"
    [dependencies]
    tycho-simulation = { git = "https://github.com/propeller-heads/tycho-simulation", rev = "your-commit-hash" }
    git clone [email protected]:propeller-heads/tycho-indexer.git
    cd tycho-indexer
    cargo build --release --bin tycho-indexer
    sudo ln -s $(pwd)/target/release/tycho-indexer /usr/local/bin/tycho-indexer
    export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"
    export SUBSTREAMS_API_TOKEN=eyJhbGci...
    export RUST_LOG=protocol_testing=info,tycho_client=info,tycho_indexer=error,error
    cargo build --target wasm32-unknown-unknown --release
    docker compose -f ./docker-compose.yaml up -d db
    cargo run -- range/full --package <package-name>
    Options:
    # Range tests
    --package <PACKAGE>         # Name of the package to test
    --match-test <MATCH_TEST>   # Run only tests matching name
    --db-url <DB_URL>           # Database URL (default: postgres://postgres:mypassword@localhost:5431/tycho_indexer_0)
    --rpc-url                   # RPC endpoint with trace support (required)
    --chain                     # Chain to run the tests on (defaults to Ethereum)
    
    # Full tests
    --initial-block    # Start block (default: protocol creation block)
    --stop-block       # End block (default: latest)
    
    # Debugging
    --vm-simulation-traces    # Enable VM simulation traces
    # Setup Environment Variables
    export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"
    export SUBSTREAMS_API_TOKEN=eyJhbGci...
    export RUST_LOG=info,protocol_testing=info,tycho_client=error
    
    # Build Substreams wasm for BalancerV2
    cd substreams
    cargo build --release --package ethereum-balancer-v2 --target wasm32-unknown-unknown
    cd ../protocol-testing
    
    # Run Postgres DB using Docker compose
    docker compose -f ./docker-compose.yaml up -d db
    
    # Run test
    cargo run -- range --package ethereum-balancer-v2 
    /bin
    /sbin
    /usr/bin
    /usr/sbin
    /usr/local/bin
    /usr/local/sbin
    > tycho-indexer --version
    tycho-indexer 0.88.0 # should match the latest version published on GitHub
    Substreams Authenticationarrow-up-right
    export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"
    export SUBSTREAMS_API_TOKEN=eyJhbGci...
    export PROTOCOLS="ethereum-balancer-v2=weighted_legacy_creation ethereum-ekubo-v2"
    docker buildx build -f protocol-testing/run.Dockerfile -t protocol-testing-test-runner:latest --load .
    docker compose up -d && docker compose logs test-runner --follow
    # Setup Environment Variables
    export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"
    export SUBSTREAMS_API_TOKEN=eyJhbGci...
    export PROTOCOLS="ethereum-balancer-v2"
    
    # Build image
    docker buildx build -f protocol-testing/run.Dockerfile -t protocol-testing-test-runner:latest --load .
    
    # Run test
    cd protocol-testing/
    docker compose up -d && docker compose logs test-runner --follow
    tar -xvzf tycho-indexer-x86_64-unknown-linux-gnu-{version}.tar.gz
    // Ensure the binary is executable:
    sudo chmod +x tycho-indexer
    // Create symlink
    sudo ln -s $(pwd)/tycho-indexer /usr/local/bin/tycho-indexer
    export PATH="/usr/local/bin:$PATH"
    /bin
    /sbin
    /usr/bin
    /usr/sbin
    /usr/local/bin
    /usr/local/sbin
    > tycho-indexer --version
    tycho-indexer 0.88.0 # should match the latest version published on GitHub

    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

    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 TychoRouter to spend your tokens.

    UseVaultsFunds

    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

    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:

    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%)

    chevron-rightExample Solutionhashtag

    The following diagram shows a swap from ETH to DAI through USDC. ETH arrives in the router and is wrapped to WETH. The solution then splits between three (WETH, USDC) pools and finally swaps from USDC to DAI on one pool.

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

    hashtag
    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.

    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

    hashtag
    Encoder

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

    hashtag
    Builder

    Builder options:

    • swap_encoder_registry β€” Registry of protocol-specific SwapEncoder s used during encoding. Use add_default_encoders 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).

    hashtag
    Builder Example Usage

    hashtag
    Swap Encoders

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

    The SwapEncoderRegistry manages these encoders. Call add_default_encoders() to use the built-in implementations. This method accepts an optional executors_addresses JSON string with executor addresses for encoding. Pass None to default to config/executor_addresses.json.

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

    hashtag
    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 to be transferred into the TychoRouter from you.

    • minAmountOut and tokenOut β€” the minimum amount you want to receive. For maximum security, determine this from a * third-party source*.

    • receiver β€” who receives the final output. Set this to the TychoRouter 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 below). Pass all-zero values if you don't need fees.

    The ClientFeeParams struct is defined as:

    Field
    Description

    clientFeeBps

    Fee percentage in basis points. 100 = 1%. Set to 0 to disable.

    clientFeeReceiver

    Address that receives the client's portion of the fee (credited to their vault balance).

    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

    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 TychoRouter contract functions for reference.

    Client Fee Signature

    If you don't want fees, pass all-zero values: clientFeeBps: 0, clientFeeReceiver: address(0), maxClientContribution: 0, deadline: 0, and an empty clientSignature.

    If you do want fees, the clientFeeReceiver must sign the fee parameters using EIP-712. This prevents third parties from spoofing fee configurations. The signature covers the following typed struct:

    The EIP-712 domain is:

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

    chevron-rightSign fee parameters examplehashtag

    Example of signing the fee parameters in Rust using alloy:

    The returned 65-byte signature is passed as the clientSignature field in ClientFeeParams.

    hashtag
    Run as a Binary

    hashtag
    Installation

    Build and install the binary:

    After installation, you can use the tycho-encode command from any directory in your terminal.

    hashtag
    Commands

    The command lets you choose the encoder:

    • tycho-router: Encodes a transaction using the TychoRouterEncoder.

    The commands accept the same options as the builder (more here).

    chevron-rightExamplehashtag

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

    cratearrow-up-right
    Quickstart
    use alloy::primitives::{keccak256, Address, B256, U256};
    use alloy::signers::{local::PrivateKeySigner, SignerSync};
    use alloy::sol_types::SolValue;
    
    fn sign_client_fee(
        chain_id: u64,
        router_address: Address,
        client_fee_bps: u16,
        client_fee_receiver: Address,
        max_client_contribution: U256,
        deadline: U256,
        signer: &PrivateKeySigner,
    ) -> Vec<u8> {
        // Must match CLIENT_FEE_TYPEHASH in TychoRouter.sol
        let type_hash: B256 = keccak256(
            b"ClientFee(uint16 clientFeeBps,address clientFeeReceiver,\
              uint256 maxClientContribution,uint256 deadline)",
        );
    
        // EIP-712 domain separator
        let domain_type_hash: B256 = keccak256(
            b"EIP712Domain(string name,string version,uint256 chainId,\
              address verifyingContract)",
        );
        let domain_separator: B256 = keccak256(
            (
                domain_type_hash,
                keccak256(b"TychoRouter"),
                keccak256(b"1"),
                U256::from(chain_id),
                router_address,
            )
                .abi_encode(),
        );
    
        // Struct hash
        let struct_hash: B256 = keccak256(
            (
                type_hash,
                U256::from(client_fee_bps),
                client_fee_receiver,
                max_client_contribution,
                deadline,
            )
                .abi_encode(),
        );
    
        // EIP-712 digest: keccak256("\x19\x01" ++ domainSeparator ++ structHash)
        let mut data = [0u8; 66];
        data[0] = 0x19;
        data[1] = 0x01;
        data[2..34].copy_from_slice(domain_separator.as_ref());
        data[34..66].copy_from_slice(struct_hash.as_ref());
        let digest: B256 = keccak256(data);
    
        signer
            .sign_hash_sync(&digest)
            .expect("signing failed")
            .as_bytes()
            .to_vec()
    }
    echo '{
      "sender": "0x1234567890123456789012345678901234567890",
      "receiver": "0x1234567890123456789012345678901234567890",
      "token_in": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
      "amount_in": "1000000000000000000",
      "token_out": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
      "exact_out": false,
      "min_amount_out": "1",
      "max_client_contribution": "0",
      "user_transfer_type": "TransferFrom",
      "swaps": [
        {
          "component": {
            "id": "0xA478c2975Ab1Ea89e8196811F51A7B7Ade33eB11",
            "protocol_system": "uniswap_v2",
            "protocol_type_name": "uniswap_v2_pool",
            "chain": "ethereum",
            "tokens": [
              "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
              "0x6B175474E89094C44Da98b954EedeAC495271d0F"
            ],
            "contract_addresses": [],
            "static_attributes": {
              "factory": "0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"
            },
            "change": "Update",
            "creation_tx": "0x0000000000000000000000000000000000000000000000000000000000000000",
            "created_at": "2024-01-01T00:00:00"
          },
          "token_in": "0x6B175474E89094C44Da98b954EedeAC495271d0F",
          "token_out": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
          "split": 0.0
        }
      ]
    }' | tycho-encode --chain ethereum tycho-router
    let swap_encoder_registry = SwapEncoderRegistry::new(Chain::Ethereum)
    .add_default_encoders(None)
    .expect("Failed to get default SwapEncoderRegistry");
    
    let encoder = TychoRouterEncoderBuilder::new()
    .chain(Chain::Ethereum)
    .swap_encoder_registry(swap_encoder_registry)
    .build()
    .expect("Failed to build encoder");
    registry.register_encoder("my_protocol", Box::new(MyCustomEncoder));
    let encoded_solutions = encoder.encode_solutions(solutions);
    ClientFee(uint16 clientFeeBps,address clientFeeReceiver, uint256 maxClientContribution, uint256 deadline)
    EIP712Domain(string name,string version, uint256 chainId, address verifyingContract)
    # Build the project
    cargo build --release
    
    # Install the binary to your system
    cargo install --path .
    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.
  • The sum of all non-remainder splits for each token is smaller than 1 (100%)

  • Bytes

    Address of the receiver of the token out. If set to the TychoRouter address, these funds will be assigned to the user's .

    token_in

    Bytes

    The token being sold

    amount_in

    BigUint

    Amount of the input token

    token_out

    Bytes

    The token being bought.

    min_amount_out

    BigUint

    Minimum amount the receiver must receive at the end of the transaction

    swaps

    Vec<Swap>

    List of swaps to fulfil the solution.

    user_transfer_type

    UserTransferType

    How user funds are transferred into the router (see below)

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

    Protocol component from Tycho core

    token_in

    Bytes

    Address of the token you provide to the pool

    token_out

    Bytes

    Address of the token you expect from the pool

    split

    f64

    Percentage of the amount in to be swapped in this operation (for example, 0.5 means 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. This is necessary for RFQ protocols. This value is used to request the quote.

    Bytes

    The address of the contract to be called (it can be the Tycho Router or an Executor)

    selector

    String

    The selector of the function to be called.

    n_tokens

    usize

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

    Unix timestamp after which the signature is no longer valid.

    clientSignature

    EIP-712 signature over all other fields, signed by clientFeeReceiver.

    Diagram representing examples of split swaps
    Diagram of an example solution
    Diagram representing swap groups
    swap_a = Swap::new(
        pool_a,
        weth_address,
        usdc_address,
        0.3, // 30% of WETH amount
    );
    swap_b = Swap::new(
        pool_b,
        weth_address,
        usdc_address,
        0.3, // 30% of WETH amount
    );
    swap_c = Swap::new(
        pool_c,
        weth_address,
        usdc_address,
        0f64, // Rest of remaining WETH amount (40%)
    );
    swap_d = Swap::new(
        pool_d,
        usdc,
        dai,
        0f64, // All of USDC amount
    );
    
    let solution = Solution::new(
        user_address.clone(),
        user_address,
        eth_address,       // token_in (ETH β€” encoder auto-wraps to WETH)
        dai_address,       // token_out
        sell_amount,       // amount_in
        min_amount_out,    // min_amount_out
        vec![swap_a, swap_b, swap_c, swap_d],
    );
    SwapGroup {
    input_token: weth_address,
    output_token: dai_address,
    protocol_system: "uniswap_v4",
    swaps: vec![weth_wbtc_swap, wbtc_usdc_swap, usdc_dai_swap],
    split: 0,
    }
    Vault

    Hook Integration Guide

    hashtag
    Integration Guide

    This page provides step-by-step instructions for integrating any Uniswap V4 hook with the Hooks DCI.

    hashtag
    Determine Your Requirements

    Before implementing anything, determine what (if anything) you need to customize:

    Decision Tree

    Quick Reference Table

    Hook Type
    What to Implement

    hashtag
    Prerequisites

    Understand the Hook's Architecture:

    • Where tokens are stored (which external contracts?)

    • How balances are queried (what functions?)

    • How limits are determined (withdrawal limits, caps, etc.)

    hashtag
    1. Minimal Setup (Internal Liquidity Hooks)

    If your hook stores all liquidity in the PoolManager and is Composable, your hook should be auto-indexed by Tycho.

    If you have external liquidity, continue to the next Section

    hashtag
    2. Custom Setup (External Liquidity Hooks)

    hashtag
    2.1 Implementation Steps

    Step 1: Implement Metadata Request Generator

    The generator creates requests to fetch external data for your hook.

    Trait to Implement:

    Template:

    Euler Reference Implementation:

    Key Decisions:

    1. Balance Request: How do you query balances? Direct call, lens contract, or multiple calls?

    2. Limits Request: Do you have withdrawal limits, liquidity caps, or other constraints?

    3. State Overrides: Do you need to deploy helper contracts or modify state for queries?

    Step 2: Implement Response Parser

    The parser converts raw RPC responses into structured metadata.

    Trait to Implement:

    Template:

    Euler Reference Implementation:

    Key Considerations:

    1. Response Format: Understand the ABI encoding of your response

    2. Error Handling: Handle malformed responses gracefully

    3. Token Ordering: Ensure consistent token ordering between request and response

    Step 3: (Optional) Implement Custom Hook Orchestrator

    Most hooks can use the default orchestrator. Implement a custom one only if you need:

    • Special entrypoint encoding logic

    • Custom balance/limit transformations

    • Hook-specific state updates

    When Default is Sufficient:

    • Balances come directly from metadata

    • Limits are straightforward max amounts

    • Standard Uniswap V4 swap encoding works

    Euler Example: Uses the default orchestrator because it meets all standard requirements.

    Custom Orchestrator Template (if needed):

    For most use cases, proceed with the default orchestrator and skip this step.

    Step 4: Register Components

    Set up all registries to wire your implementation into the Hooks DCI.

    Registration Code:

    In your integration folder add a register function with your protocol specifics

    Then add it in the global registration function with other hooks

    Key Configuration Points:

    1. Generator Registration: Use register_hook_identifier() if your components have a "hook_identifier" static attribute, or register_hook_generator() for specific addresses

    2. Parser Name: Must match the generator_name in your MetadataRequests

    Step 5: Initialize Hooks DCI

    Create and initialize the UniswapV4HookDCI instance.

    Initialization Code:

    Configuration Parameters:

    • max_retries: Maximum total retry attempts before permanently failing a component

    • pause_after_retries: Number of retries before pausing (setting "paused" attribute)

    Typical Values:

    • max_retries: 5, pause_after_retries: 3

    Step 6: Testing Your Integration

    Test your implementation at multiple levels.

    Unit Tests:

    Integration Tests with Real RPC:

    hashtag
    Final Step: Submitting a PR

    After your integration is tested, please submit a PR on Github so we can add it to our codebase and start indexing the hook on our hosted service.

    What state needs to be simulated
    Token Pairs: Do limits apply per token or per token pair?
    Entrypoint Creation: Optional but useful for tracing the limits call itself
    Non-standard token accounting
    No special state transformations needed
    Routing Key: Must match the routing_key in your MetadataRequests
  • Estimation Method: Choose with_limits() if you provide limits, with_balances() otherwise

  • Sample Size: Number of entrypoints to generate per token pair (typically 4)

  • Internal Liquidity

    Nothing (auto-handled)

    External Liquidity (Standard)

    Generator + Parser

    External Liquidity (Custom)

    Generator + Parser + Orchestrator

    Non-Composable

    Not supported yet

    START: I want to index my Uniswap V4 hook
    
    Q1: Is my hook composable (works with empty hookData)?
        β”œβ”€ NO  β†’ ⚠️ STOP: Non-composable hooks not yet supported
        β”‚         Wait for future release with hookData source support
        └─ YES β†’ Continue to Q2
    
    Q2: Where does my hook store liquidity?
        β”œβ”€ In PoolManager (ERC6909 claims)
        β”‚   └─→ INTERNAL LIQUIDITY
        β”‚       βœ“ No custom code needed - your hook will be automatically indexed
        β”‚       
        β”‚
        └─ In external contracts (vaults, protocols, etc.)
            └─→ EXTERNAL LIQUIDITY
                βš™οΈ Requires metadata generator + parser
                β†’ Continue to the next step 
    
    Q3: (External liquidity only) Does my hook need custom entrypoint encoding?
        β”œβ”€ NO  β†’ Implement Generator + Parser only
        β”‚         Skip custom orchestrator (use default)
        β”‚
        └─ YES β†’ Implement Generator + Parser + Custom Orchestrator
    
    pub trait MetadataRequestGenerator: Send + Sync {
        fn generate_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError>;
    
        fn generate_balance_only_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError>;
    
        fn supported_metadata_types(&self) -> Vec<MetadataRequestType>;
    }
    use tycho_common::models::{Block, Address};
    use crate::extractor::dynamic_contract_indexer::component_metadata::{
        MetadataRequestGenerator, MetadataRequest, MetadataRequestType, MetadataError,
    };
    
    pub struct MyHookGenerator {
        rpc_url: String,
    }
    
    impl MyHookGenerator {
        pub fn new(rpc_url: String) -> Self {
            Self { rpc_url }
        }
    
        // Helper to extract hook address from component
        fn get_hook_address(
            &self,
            component: &ProtocolComponent,
        ) -> Result<Address, MetadataError> {
            component
                .static_attributes
                .get("hooks")
                .and_then(|v| v.as_address())
                .ok_or_else(|| MetadataError::InvalidComponent(
                    "Missing 'hooks' attribute".to_string()
                ))
        }
    }
    
    impl MetadataRequestGenerator for MyHookGenerator {
        fn generate_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError> {
            let hook_address = self.get_hook_address(component)?;
            let mut requests = Vec::new();
    
            // 1. Generate balance request
            requests.push(self.create_balance_request(component, block, &hook_address)?);
    
            // 2. Generate limits requests (if applicable)
            requests.extend(self.create_limits_requests(component, block, &hook_address)?);
    
            // 3. Generate TVL request (if applicable)
            // requests.push(self.create_tvl_request(component, block, &hook_address)?);
    
            Ok(requests)
        }
    
        fn generate_balance_only_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError> {
            let hook_address = self.get_hook_address(component)?;
    
            // Only generate balance request for balance-only updates
            Ok(vec![self.create_balance_request(component, block, &hook_address)?])
        }
    
        fn supported_metadata_types(&self) -> Vec<MetadataRequestType> {
            vec![
                MetadataRequestType::ComponentBalance {
                    token_addresses: vec![],
                },
                MetadataRequestType::Limits {
                    token_pair: vec![],
                },
            ]
        }
    }
    
    impl MyHookGenerator {
        fn create_balance_request(
            &self,
            component: &ProtocolComponent,
            block: &Block,
            hook_address: &Address,
        ) -> Result<MetadataRequest, MetadataError> {
            // TODO: Implement your balance request logic
            // Example: Call a function like getBalances() or getReserves()
    
            let calldata = format!(
                "0x{}", // Function selector + encoded parameters
                "YOUR_FUNCTION_SELECTOR_HERE"
            );
    
            Ok(MetadataRequest {
                request_type: MetadataRequestType::ComponentBalance {
                    token_addresses: component.tokens.clone(),
                },
                routing_key: "rpc_default".to_string(),
                generator_name: "my_hook".to_string(), // Must match parser registration
                transport: RpcTransport::new(
                    self.rpc_url.clone(),
                    "eth_call".to_string(),
                    vec![
                        json!({
                            "to": hook_address,
                            "data": calldata,
                        }),
                        json!(format!("0x{:x}", block.number)),
                    ],
                ),
            })
        }
    
        fn create_limits_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
            hook_address: &Address,
        ) -> Result<Vec<MetadataRequest>, MetadataError> {
            let mut requests = Vec::new();
            let tokens = &component.tokens;
    
            // Generate limits request for each token pair
            for i in 0..tokens.len() {
                for j in (i + 1)..tokens.len() {
                    let token_pair = vec![tokens[i].clone(), tokens[j].clone()];
    
                    // TODO: Implement your limits request logic
                    // This might involve:
                    // - Calling a function on the hook
                    // - Using a lens contract pattern (like Euler)
                    // - Querying external protocol limits
    
                    requests.push(MetadataRequest {
                        request_type: MetadataRequestType::Limits {
                            token_pair: token_pair.clone(),
                        },
                        routing_key: "rpc_default".to_string(),
                        generator_name: "my_hook".to_string(),
                        transport: RpcTransport::new(
                            self.rpc_url.clone(),
                            "eth_call".to_string(),
                            vec![
                                json!({
                                    "to": "YOUR_CONTRACT_ADDRESS",
                                    "data": "YOUR_CALLDATA",
                                }),
                                json!(format!("0x{:x}", block.number)),
                                // Optional: state overrides
                                // json!({ "address": { "code": "0x...", "state": {...} } }),
                            ],
                        ),
                    });
                }
            }
    
            Ok(requests)
        }
    }
    // From: tycho-indexer/src/extractor/dynamic_contract_indexer/hooks/integrations/euler/metadata_generator.rs
    
    impl MetadataRequestGenerator for EulerMetadataGenerator {
        fn generate_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError> {
            let hook_address = self.get_hook_address(component)?;
            let mut requests = Vec::new();
    
            // 1. Balance request: Call getReserves() on hook
            requests.push(MetadataRequest {
                request_type: MetadataRequestType::ComponentBalance {
                    token_addresses: component.tokens.clone(),
                },
                routing_key: "rpc_default".to_string(),
                generator_name: "euler".to_string(),
                transport: RpcTransport::new(
                    self.rpc_url.clone(),
                    "eth_call".to_string(),
                    vec![
                        json!({
                            "to": hook_address,
                            "data": "0x0902f1ac" // getReserves() selector
                        }),
                        json!(format!("0x{:x}", block.number)),
                    ],
                ),
            });
    
            // 2. Limits requests: Use lens contract with state overrides
            let lens_address = "0x0000000000000000000000000000000000001337";
            let lens_bytecode_hex = hex::encode(EULER_LENS_BYTECODE_BYTES);
    
            for token_pair in get_token_pairs(&component.tokens) {
                requests.push(MetadataRequest {
                    request_type: MetadataRequestType::Limits {
                        token_pair: token_pair.clone(),
                    },
                    routing_key: "rpc_default".to_string(),
                    generator_name: "euler".to_string(),
                    transport: RpcTransport::new(
                        self.rpc_url.clone(),
                        "eth_call".to_string(),
                        vec![
                            json!({
                                "to": lens_address,
                                "data": format!(
                                    "0xaaed87a3{}{}",  // getLimits(address,address)
                                    &token_pair[0].to_string()[2..],
                                    &token_pair[1].to_string()[2..]
                                )
                            }),
                            json!(format!("0x{:x}", block.number)),
                            json!({  // Deploy lens contract at deterministic address
                                lens_address: {
                                    "code": format!("0x{}", lens_bytecode_hex),
                                    "state": {
                                        // Store hook address in slot 0
                                        "0x0000000000000000000000000000000000000000000000000000000000000000":
                                            format!("0x{:0>64}", &hook_address.to_string()[2..])
                                    }
                                }
                            }),
                        ],
                    ),
                });
            }
    
            Ok(requests)
        }
    
        fn generate_balance_only_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError> {
            // Only balance request needed for balance-only updates
            let hook_address = self.get_hook_address(component)?;
    
            Ok(vec![MetadataRequest {
                request_type: MetadataRequestType::ComponentBalance {
                    token_addresses: component.tokens.clone(),
                },
                routing_key: "rpc_default".to_string(),
                generator_name: "euler".to_string(),
                transport: RpcTransport::new(
                    self.rpc_url.clone(),
                    "eth_call".to_string(),
                    vec![
                        json!({"to": hook_address, "data": "0x0902f1ac"}),
                        json!(format!("0x{:x}", block.number)),
                    ],
                ),
            }])
        }
    }
    pub trait MetadataResponseParser: Send + Sync {
        fn parse_response(
            &self,
            component: &ProtocolComponent,
            request: &MetadataRequest,
            response: &Value,
        ) -> Result<MetadataValue, MetadataError>;
    }
    use serde_json::Value;
    use tycho_common::models::{ProtocolComponent, Address};
    use crate::extractor::dynamic_contract_indexer::component_metadata::{
        MetadataResponseParser, MetadataRequest, MetadataRequestType,
        MetadataValue, MetadataError,
    };
    
    pub struct MyHookParser;
    
    impl MetadataResponseParser for MyHookParser {
        fn parse_response(
            &self,
            component: &ProtocolComponent,
            request: &MetadataRequest,
            response: &Value,
        ) -> Result<MetadataValue, MetadataError> {
            // Extract hex string from response
            let hex_str = response
                .as_str()
                .ok_or_else(|| MetadataError::InvalidResponse(
                    "Response is not a string".to_string()
                ))?
                .trim_start_matches("0x");
    
            match &request.request_type {
                MetadataRequestType::ComponentBalance { token_addresses } => {
                    self.parse_balances(component, hex_str, token_addresses)
                }
                MetadataRequestType::Limits { token_pair } => {
                    self.parse_limits(component, request, hex_str, token_pair)
                }
                MetadataRequestType::Tvl => {
                    self.parse_tvl(component, hex_str)
                }
                _ => Err(MetadataError::UnsupportedRequestType),
            }
        }
    }
    
    impl MyHookParser {
        fn parse_balances(
            &self,
            component: &ProtocolComponent,
            hex_str: &str,
            token_addresses: &[Address],
        ) -> Result<MetadataValue, MetadataError> {
            // TODO: Parse your balance response format
            // Example: Two 32-byte values (64 hex chars each)
    
            if hex_str.len() < 128 {
                return Err(MetadataError::InvalidResponse(
                    format!("Balance response too short: {} chars", hex_str.len())
                ));
            }
    
            // Ensure tokens are sorted (for consistent mapping)
            let mut tokens = component.tokens.clone();
            tokens.sort();
    
            // Extract balances
            let balance_0 = Bytes::from(&hex_str[0..64]);
            let balance_1 = Bytes::from(&hex_str[64..128]);
    
            let mut balances = HashMap::new();
            balances.insert(tokens[0].clone(), balance_0);
            balances.insert(tokens[1].clone(), balance_1);
    
            Ok(MetadataValue::Balances(balances))
        }
    
        fn parse_limits(
            &self,
            component: &ProtocolComponent,
            request: &MetadataRequest,
            hex_str: &str,
            token_pair: &[Address],
        ) -> Result<MetadataValue, MetadataError> {
            // TODO: Parse your limits response format
    
            if hex_str.len() < 128 {
                return Err(MetadataError::InvalidResponse(
                    format!("Limits response too short: {} chars", hex_str.len())
                ));
            }
    
            // Extract limits
            let limit_0 = Bytes::from(&hex_str[0..64]);
            let limit_1 = Bytes::from(&hex_str[64..128]);
    
            // Optional: Create entrypoint for the limits call itself
            // This can be used for tracing/reference
            let limits_entrypoint = self.create_limits_entrypoint(
                component,
                token_pair,
                request,
            ).ok(); // Make optional
    
            Ok(MetadataValue::Limits(vec![
                (token_pair[0].clone(), (limit_0, limit_1, limits_entrypoint))
            ]))
        }
    
        fn parse_tvl(
            &self,
            component: &ProtocolComponent,
            hex_str: &str,
        ) -> Result<MetadataValue, MetadataError> {
            // TODO: Parse TVL if applicable
            // This might involve converting token amounts to USD values
    
            Err(MetadataError::UnsupportedRequestType)
        }
    
        fn create_limits_entrypoint(
            &self,
            component: &ProtocolComponent,
            token_pair: &[Address],
            request: &MetadataRequest,
        ) -> Result<EntryPointWithTracingParams, MetadataError> {
            // TODO: Create entrypoint for limits call
            // This is optional but useful for tracing
    
            Ok(EntryPointWithTracingParams {
                entry_point: EntryPoint {
                    external_id: format!(
                        "limits_{}_{}_{}",
                        component.id,
                        token_pair[0],
                        token_pair[1]
                    ),
                    target: /* your target address */,
                    signature: "getLimits(address,address)".to_string(),
                },
                params: TracingParams::RPCTracer(RPCTracerParams {
                    caller: None,
                    calldata: /* your calldata */,
                    state_overrides: /* your overrides */,
                    prune_addresses: None,
                }),
            })
        }
    }
    // From: tycho-indexer/src/extractor/dynamic_contract_indexer/hooks/integrations/euler/metadata_generator.rs
    
    impl MetadataResponseParser for EulerMetadataResponseParser {
        fn parse_response(
            &self,
            component: &ProtocolComponent,
            request: &MetadataRequest,
            response: &Value,
        ) -> Result<MetadataValue, MetadataError> {
            let res_str = response
                .as_str()
                .ok_or_else(|| MetadataError::InvalidResponse(
                    "Expected string response".to_string()
                ))?
                .trim_start_matches("0x");
    
            match &request.request_type {
                MetadataRequestType::ComponentBalance { .. } => {
                    // Parse getReserves() response: two uint112 values
                    if res_str.len() < 128 {
                        return Err(MetadataError::InvalidResponse(
                            format!("Balance response too short: {}", res_str.len())
                        ));
                    }
    
                    let balance_0 = Bytes::from(&res_str[0..64]);
                    let balance_1 = Bytes::from(&res_str[64..128]);
    
                    let mut tokens = component.tokens.clone();
                    tokens.sort();
    
                    let mut balances = HashMap::new();
                    balances.insert(tokens[0].clone(), balance_0);
                    balances.insert(tokens[1].clone(), balance_1);
    
                    Ok(MetadataValue::Balances(balances))
                }
    
                MetadataRequestType::Limits { token_pair } => {
                    // Parse getLimits() response from lens contract
                    if res_str.len() < 128 {
                        return Err(MetadataError::InvalidResponse(
                            format!("Limits response too short: {}", res_str.len())
                        ));
                    }
    
                    let limit_0 = Bytes::from(&res_str[0..64]);
                    let limit_1 = Bytes::from(&res_str[64..128]);
    
                    // Create entrypoint for limits call
                    let hook_address = component
                        .static_attributes
                        .get("hooks")
                        .and_then(|v| v.as_address())
                        .ok_or_else(|| MetadataError::InvalidComponent(
                            "Missing hooks attribute".to_string()
                        ))?;
    
                    let limits_entrypoint = create_euler_limits_entrypoint(
                        component,
                        &hook_address,
                        token_pair,
                    )?;
    
                    Ok(MetadataValue::Limits(vec![
                        (token_pair[0].clone(), (limit_0, limit_1, Some(limits_entrypoint)))
                    ]))
                }
    
                _ => Err(MetadataError::UnsupportedRequestType),
            }
        }
    }
    use async_trait::async_trait;
    use crate::extractor::{
        dynamic_contract_indexer::{
            hook_orchestrator::{HookOrchestrator, HookOrchestratorError},
            component_metadata::ComponentTracingMetadata,
        },
        models::BlockChanges,
    };
    
    pub struct MyHookOrchestrator {
        entrypoint_generator: Box<dyn HookEntrypointGenerator>,
    }
    
    #[async_trait]
    impl HookOrchestrator for MyHookOrchestrator {
        async fn update_components(
            &self,
            block_changes: &mut BlockChanges,
            components: &[ProtocolComponent],
            metadata: &HashMap<String, ComponentTracingMetadata>,
            generate_entrypoints: bool,
        ) -> Result<(), HookOrchestratorError> {
            // TODO: Implement custom orchestration logic
    
            // 1. Extract metadata for components
            // 2. Generate entrypoints (if generate_entrypoints == true)
            // 3. Inject balances into components
            // 4. Inject limits for RPC optimization
            // 5. Update block_changes with new data
    
            Ok(())
        }
    }
    pub(super) fn register_my_hook_integrations(
        generator_registry: &mut MetadataGeneratorRegistry,
        parser_registry: &mut MetadataResponseParserRegistry,
        _provider_registry: &mut ProviderRegistry,
        rpc_url: String,
    ) {
        generator_registry.register_hook_identifier(
            "my_hook".to_string(),
            Box::new(MyHookMetadataGenerator::new(rpc_url)),
        );
        parser_registry.register_parser("my_hook".to_string(), Box::new(MyHookMetadataResponseParser));
    }
    // From: tycho-indexer/src/extractor/dynamic_contract_indexer/hooks/integrations/mod.rs
    
    pub(super) fn register_integrations(
        generator_registry: &mut MetadataGeneratorRegistry,
        parser_registry: &mut MetadataResponseParserRegistry,
        provider_registry: &mut ProviderRegistry,
        rpc_url: String,
    ) {
        euler::register_euler_integrations(
            generator_registry,
            parser_registry,
            provider_registry,
            rpc_url,
        );
        
        // Add your hook registration here
    }
    use tycho_common::models::{Chain, Address};
    use crate::extractor::dynamic_contract_indexer::{
        dci::DynamicContractIndexer,
        hook_dci::UniswapV4HookDCI,
    };
    
    pub async fn create_hooks_dci_indexer(
        chain: Chain,
        extractor_name: String,
        rpc_url: String,
        router_address: Address,
        pool_manager: Address,
        db_gateway: impl EntryPointGateway + ProtocolGateway + Send + Sync + 'static,
        account_extractor: impl AccountExtractor + Send + Sync + 'static,
        entrypoint_tracer: impl EntryPointTracer + Send + Sync + 'static,
    ) -> Result<UniswapV4HookDCI<...>, ExtractionError> {
        // 1. Create inner DCI (standard indexer)
        let inner_dci = DynamicContractIndexer::new(
            chain.clone(),
            extractor_name.clone(),
            db_gateway.clone(),
            account_extractor,
            entrypoint_tracer,
        );
    
        // 2. Setup metadata and hook orchestrators (from Step 4)
        let (metadata_orchestrator, hook_orchestrator_registry) =
            setup_my_hook_indexing(rpc_url, router_address, pool_manager, chain.clone());
    
        // 3. Create Hooks DCI
        let mut hook_dci = UniswapV4HookDCI::new(
            inner_dci,
            metadata_orchestrator,
            hook_orchestrator_registry,
            db_gateway,
            chain,
            max_retries: 3,        // Retry up to 3 times before giving up
            pause_after_retries: 2 // Pause after 2 retries (before hitting max)
        );
    
        // 4. Initialize (loads existing components from database)
        hook_dci.initialize().await?;
    
        Ok(hook_dci)
    }
    #[cfg(test)]
    mod tests {
        use super::*;
    
        #[test]
        fn test_generator_creates_balance_request() {
            let generator = MyHookGenerator::new("http://localhost:8545".to_string());
            let component = create_test_component();
            let block = create_test_block();
    
            let requests = generator.generate_balance_only_requests(&component, &block)
                .expect("Should generate requests");
    
            assert_eq!(requests.len(), 1);
            assert!(matches!(
                requests[0].request_type,
                MetadataRequestType::ComponentBalance { .. }
            ));
        }
    
        #[test]
        fn test_parser_handles_balance_response() {
            let parser = MyHookParser;
            let component = create_test_component();
            let request = create_test_balance_request();
            let response = json!("0x000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000027100");
    
            let result = parser.parse_response(&component, &request, &response)
                .expect("Should parse response");
    
            match result {
                MetadataValue::Balances(balances) => {
                    assert_eq!(balances.len(), 2);
                }
                _ => panic!("Expected Balances variant"),
            }
        }
    }
    #[tokio::test]
    #[ignore] // Requires RPC access
    async fn test_metadata_collection_integration() {
        let rpc_url = std::env::var("RPC_URL")
            .expect("RPC_URL environment variable must be set");
    
        // Setup registries
        let (metadata_orchestrator, _) = setup_my_hook_indexing(
            rpc_url,
            router_address,
            pool_manager,
            Chain::Ethereum,
        );
    
        // Create test component
        let component = create_real_hook_component();
        let block = Block::new(/* real block data */);
    
        // Collect metadata
        let metadata = metadata_orchestrator
            .collect_metadata_for_block(
                &[],  // No balance-only components
                &[(TxHash::default(), component.clone())],  // Full processing
                &block,
            )
            .await
            .expect("Should collect metadata");
    
        // Verify metadata
        assert_eq!(metadata.len(), 1);
        let (comp, meta) = &metadata[0];
        assert!(meta.balances.is_some());
        assert!(meta.limits.is_some());
    }
    Logo

    Uniswap V4 Hooks DCI

    Complete Indexing Solution for All Uniswap V4 Hooks


    hashtag
    Introduction

    hashtag
    What is the Hooks DCI?

    The Hooks DCI (Dynamic Contract Indexer) is Tycho's specialized indexing plugin for all Uniswap V4 hooks. It extends the standard DCI with capabilities designed specifically for hooks, including automatic balance tracking, sophisticated entrypoint generation, and optional external metadata collection.

    The Hooks DCI is required for indexing all Uniswap V4 hooks. It provides a complete solution with sensible defaults that work out-of-the-box for most hooks, and optional extension points for hooks with advanced requirements.

    In this document, we break down UniswapV4 pools into different categories, describe the challenges to index each one, and provide a guide on how to index pools that need custom integration to be indexed by Tycho.

    hashtag
    Hook Types:

    Before diving into the solution, we need to understand the different categories that differentiate hooks Indexing:

    1. Composable vs Non-Composable

    • Composable Hooks: Work with empty hookData in swaps

    • Non-Composable Hooks: Require custom hookData for before or after swap hooks

    2. Internal vs External Liquidity

    • Internal Liquidity: Tokens accounting in PoolManager as ERC6909 claims

    • External Liquidity: Tokens in external contracts, outside UniswapV4's Pool Manager.

    We define deeper these categories further on the section

    circle-exclamation

    Currently, Tycho only supports Composable Hooks. Non-composable support is coming soon.

    Why Hooks DCI Exists

    The standard DCI works well for self-contained protocols, but Uniswap V4 hooks require some extra steps for correct indexing. For Tycho to index all the state necessary for simulating each hook, it needs to have well-defined Entrypoints that cover all the possible Hook execution paths. This was achieved by adding:

    • V4-specific entrypoint generation with custom swap encoding and state overrides, aiming to cover all the paths that a hook might take

    • Flexible metadata collection supporting both internal (automatic) and external (custom) liquidity sources

    • Registry-based extension system for hooks with specialized requirements

    On section below, we provide detailed explanations of hook types and architecture.

    hashtag
    Background & Concepts

    hashtag
    Uniswap V4 Hooks Primer

    Uniswap V4 introduces hooks - smart contracts that can execute custom logic at specific points in the pool lifecycle. Hooks enable powerful features like:

    • Dynamic fees based on market conditions

    • Custom oracle integrations

    • Liquidity management strategies

    Each hook address encodes permissions in its bytes, indicating which lifecycle events it handles:

    The Hooks DCI only processes hooks with swap permissions (beforeSwap and/or afterSwap), as these are the ones that manage liquidity and affect swap behavior.

    hashtag
    Hook Classification

    Understanding hook types helps determine what (if anything) you need to implement for your hook.

    1. Composable vs Non-Composable Hooks

    1.1 - Composable Hooks (Currently Supported)

    Composable hooks do NOT require custom calldata (hookData) to be passed during swaps. They work with empty or default hookData.

    Examples:

    • Dynamic fee hooks (calculate fees from pool state)

    • Oracle integration hooks (read from external oracles, no user input needed)

    • Internal liquidity management hooks

    1.2 - Non-Composable Hooks (Future Support)

    circle-exclamation

    ⚠️ Not Currently Supported: Non-composable hooks REQUIRE specific calldata to be passed in hookData for each swap. Support for these hooks is planned for a future release.

    Examples (not yet supported):

    • Hooks requiring user signatures per swap

    • Intent-based routing hooks

    • Hooks with swap-specific configuration

    2. Internal vs External Liquidity (Primary Classification)

    This is the key distinction that determines what you, as a hook integrator, need to implement.

    2.1 - Internal Liquidity Hooks

    βœ… No Custom Implementation Required - Composable Internal liquidity hooks are automatically indexed by Tycho.

    Characteristics:

    • All liquidity tracked in PoolManager as

    • Balances automatically extracted from blockchain state

    • No external calls needed for Metadata (Pool balances and Limits)

    How It Works:

    1. Hooks DCI extracts pool balances from BlockChanges.balance_changes

    2. Default orchestrator's enrich_metadata_from_block_balances() builds metadata from the pool internal balance

    3. Entrypoint generator creates state overrides for PoolManager ERC6909 only

    Examples:

    • Dynamic fee hooks using PoolManager liquidity

    • Hooks with custom AMM curves but standard storage

    • Time-weighted average price (TWAP) hooks

    2.2 - External Liquidity Hooks

    βš™οΈ Requires Custom Metadata Implementation

    Characteristics:

    • Liquidity stored in external contracts (lending vaults, yield protocols, etc.)

    • Requires custom RPC or API calls to fetch current balances and withdrawal limits

    • Needs custom MetadataRequestGenerator and MetadataResponseParser

    What You Need to Implement:

    1. MetadataRequestGenerator - Creates RPC requests for balances/limits

    2. MetadataResponseParser - Parses RPC responses into structured data

    3. (Optional) Custom HookOrchestrator - Only if entrypoint encoding is non-standard

    On we go deeper on the Metadata collection and how you can implement to track any hook with External Liquidity. We also provide an to guide you through the implementation steps.

    Examples:

    • Euler Hooks: Tokens in Euler lending vaults

    • Yearn Integration: Tokens in Yearn vaults earning yield

    • Staking Hooks: Tokens locked in staking contracts

    hashtag
    What You Need to Implement (Decision Tree)

    hashtag
    Architecture Overview

    High-Level System Diagram

    hashtag
    Core Components

    1. UniswapV4HookDCI

    The main orchestrator that coordinates all hook indexing operations. It:

    • Filters components with swap hook permissions

    • Categorizes components by processing state

    • Coordinates metadata collection

    2. Metadata Orchestrator System

    Purpose: Collects external metadata for hooks with external liquidity. Optional - only used when a metadata generator is registered for a hook.

    For increased performance, the external data collection is split into a three-layer architecture:

    Layer 1: Request Generation (Protocol-Specific - Optional)

    • Creates MetadataRequest objects specifying what data to fetch

    • Supports different request types: Balances, Limits, TVL

    • Not needed for internal liquidity hooks - system uses block balances instead

    Layer 2: Request Execution (Transport-Specific)

    • Implemented by providers (e.g., RPCMetadataProvider)

    • Handles batching, deduplication, retries

    • Routes requests to appropriate backends (RPC, HTTP APIs)

    Layer 3: Response Parsing (Protocol-Specific - Optional)

    • Converts raw responses into structured metadata

    • Handles errors and validation

    Fallback for Internal Liquidity: When no metadata generator is registered, the default orchestrator automatically enriches metadata from BlockChanges.balance_changes - no RPC calls needed.

    3. Hook Orchestrator Registry

    Maps hook addresses/identifiers to orchestrators that handle component processing. The default orchestrator (DefaultUniswapV4HookOrchestrator) handles both internal and external liquidity hooks automatically.

    Lookup Priority:

    1. By Hook Address: Direct mapping for specific hook deployments

    2. By Identifier: String-based lookup (e.g., "euler_v1")

    3. Default Orchestrator: Fallback for all hooks

    Orchestrator Responsibilities:

    • Generating entrypoints with appropriate tracing parameters

    • Injecting balances and limits into components

    • Updating component state attributes

    Key Feature: The default orchestrator's enrich_metadata_from_block_balances() method automatically extracts balances from blockchain state for hooks without custom metadata generators. This means internal liquidity hooks work with zero custom code.

    Internal vs External Liquidity Paths

    The system automatically chooses the appropriate path based on whether a metadata generator is registered:

    Path A: Internal Liquidity (Automatic)

    Path B: External Liquidity (Custom Metadata)

    Key Takeaway: The only difference is Step 3 (Metadata Collection). The rest of the flow is identical. This is why internal liquidity hooks require no custom implementation - they automatically use Path A.

    hashtag
    Metadata Collection System

    circle-info

    πŸ’‘ For Internal Liquidity Hooks: You can skip this entire section! The default orchestrator automatically extracts balances from blockchain state using enrich_metadata_from_block_balances(). This section is only relevant for hooks with external liquidity.

    The metadata collection system uses a three-layer architecture that separates protocol-specific logic from transport concerns. This system is optional and only activated when you register a custom metadata generator for your hook.

    Two Paths for Metadata Collection

    Path A: Internal Liquidity (Automatic - No Implementation Needed)

    • System checks: generator_registry.get_generator(component) β†’ None

    • Default orchestrator calls enrich_metadata_from_block_balances()

    • Balances extracted from BlockChanges.balance_changes

    Path B: External Liquidity (Requires Implementation)

    • System checks: generator_registry.get_generator(component) β†’ Some(generator)

    • Generator creates RPC requests for external data

    • Provider executes requests

    Layer 1: Request Generation (Protocol-Specific - External Liquidity Only)

    Purpose: Create metadata requests specific to your hook's data needs.

    Interface:

    Metadata Request Types:

    • ComponentBalance: Fetch token balances for the component

    • Limits: Fetch maximum swap amounts (withdrawal limits, liquidity caps)

    • Tvl: Total value locked calculation

    Euler Example - Balance Request:

    Euler Example - Limits Request with State Overrides:

    The lens contract pattern allows querying multiple values in a single RPC call using a custom contract deployed via state overrides.

    Layer 2: Request Execution (Transport-Specific)

    Purpose: Execute metadata requests efficiently, handling batching and retries.

    Interface:

    RPCMetadataProvider Features:

    • Batching: Groups multiple eth_call requests into JSON-RPC batches

    • Deduplication: Avoids duplicate requests in the same batch

    • Retry Logic: Exponential backoff for transient RPC failures

    Configuration:

    Request Flow:

    Layer 3: Response Parsing (Protocol-Specific)

    Purpose: Convert raw RPC responses into structured metadata.

    Interface:

    Metadata Value Types:

    Euler Example - Balance Parsing:

    Euler Example - Limits Parsing:

    Assembled Metadata

    All parsed metadata for a component is assembled into:

    Note that each field is Option<Result<...>>:

    • None: Metadata type not requested

    • Some(Ok(...)): Successfully collected

    • Some(Err(...)): Collection failed (triggers component failure)

    4.3 Hook Orchestrators

    Hook orchestrators coordinate the processing of components, including entrypoint generation and metadata injection.

    Orchestrator Responsibilities

    1. Entrypoint Generation: Create EntryPointWithTracingParams for tracing

    2. Balance Injection: Add balances to ProtocolComponent for storage

    3. Limits Injection: Provide limits for RPC query optimization

    Interface

    Parameters:

    • block_changes: Mutable reference to modify transactions and components

    • components: Components to process in this call

    • metadata: Collected external metadata (balances, limits, TVL)

    Registry Lookup Mechanisms

    The HookOrchestratorRegistry provides multiple lookup strategies:

    1. By Hook Address (Highest Priority)

    2. By Hook Identifier (Medium Priority)

    3. Default Orchestrator (Lowest Priority)

    Lookup Order:

    1. Try hook address lookup

    2. Try hook identifier lookup (from component static attributes)

    3. Fall back to default orchestrator

    4. Return error if no orchestrator found

    Default Orchestrator

    The DefaultUniswapV4HookOrchestrator handles most hook types:

    Features:

    • Extracts balances from block changes for components without external metadata

    • Delegates entrypoint generation to UniswapV4DefaultHookEntrypointGenerator

    • Injects balances and limits into BlockChanges

    When to Use Custom Orchestrator:

    • Hook requires special entrypoint encoding

    • Balance/limit data needs transformation before injection

    • Component state updates follow custom logic

    Euler Example - When Default is Sufficient:

    For Euler hooks, the default orchestrator works well because:

    • Balances come directly from metadata (no transformation needed)

    • Limits are standard max withdrawal amounts

    • Entrypoints follow standard Uniswap V4 swap encoding

    Therefore, Euler only requires custom metadata generator/parser, not a custom orchestrator.

    4.4 Entrypoint Generation

    Entrypoints define the calls that will be traced to understand how a component behaves under different conditions.

    Entrypoints allow Tycho to:

    • Simulate swaps at various amounts to understand pricing curves

    • Test edge cases (e.g., swaps at 1%, 50%, 95% of liquidity)

    • Understand touched contracts and state that are necessary for reproducing a hook's behavior

    For hooks with external liquidity, accurate entrypoints require:

    • Correct balance overwrites (both in PoolManager and external contracts)

    • Appropriate swap amounts based on limits

    • State overrides to simulate external contract states

    Swap Amount Estimation

    The system supports two estimation strategies:

    1. Limits-Based Estimation (Preferred)

    When limits are available, generate samples at:

    • 1% of limit (test small swaps)

    • 10% of limit (test medium swaps)

    • 50% of limit (test large swaps)

    • 95% of limit (test near-maximum swaps)

    2. Balance-Based Estimation (Fallback)

    When limits are unavailable, generate samples at:

    • 1% of balance

    • 2% of balance

    • 5% of balance

    • 10% of balance

    Euler Example - Limits-Based Amounts:

    V4MiniRouter Pattern

    For Uniswap V4, entrypoints use a custom router deployed via state overrides:

    Purpose: Execute swap operations against the PoolManager with proper token settlements

    Pattern:

    ERC6909 Overwrites

    Uniswap V4 uses ERC6909 for internal PoolManager accounting. To simulate swaps, we must set balances:

    Balance Slot Detection

    For hooks with external liquidity, tokens may need balances set in external contracts:

    Optional Feature: EVMBalanceSlotDetector

    Euler Example - Balance Overwrites:

    For Euler hooks, tokens are held in external vaults. The entrypoint generator:

    1. Detects balance slots for vault tokens (wstETH, WETH, etc.)

    2. Overwrites those slots with swap amounts

    3. Ensures PoolManager has ERC6909 balances

    This allows accurate tracing even though liquidity is external to PoolManager.

    \

    State-aware processing to optimize performance and handle failures gracefully

    Integration with external DeFi protocols
    external liquidity hooks
    Works with default orchestrator out-of-the-box

    Everything works automatically - no custom code needed

    Most hooks that don't integrate with external DeFi

    May need balance slot detection for accurate simulations

    Manages component lifecycle (success, failure, retry, pause)
  • Delegates to inner DCI for tracing operations

  • Zero RPC calls, zero custom code required

  • Parser converts responses to structured metadata

  • Requires implementing Generator + Parser traits

  • Custom: Extensible for hook-specific needs

    Concurrency Limiting: Prevents overwhelming RPC endpoints

    State Updates: Modify component state attributes as needed

    generate_entrypoints: true for full processing, false for balance-only

    Handles both full processing and balance-only updates

    Hook uses non-standard token accounting
    No special state updates required
    Simulates full swap flow including vault withdrawals
    ERC6909 claimsarrow-up-right
    Hook Integration Guide
    Hook Classification
    Background & Concepts
    Metadata Collection System
    Bit 7: beforeSwap
    Bit 6: afterSwap
    Bit 5: beforeAddLiquidity
    Bit 4: afterAddLiquidity
    ...
    // Composable hook - works with empty hookData
    function beforeSwap(
        address sender,
        PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        bytes calldata hookData  // Can be empty: 0x
    ) external returns (bytes4, BeforeSwapDelta, uint24);
    // Non-composable hook - requires meaningful hookData
    function beforeSwap(
        address sender,
        PoolKey calldata key,
        IPoolManager.SwapParams calldata params,
        bytes calldata hookData  // MUST contain routing info, signatures, etc.
    ) external returns (bytes4, BeforeSwapDelta, uint24) {
        // Decode hookData for routing decisions, user signatures, etc.
        (address router, bytes memory signature) = abi.decode(hookData, (address, bytes));
        // ...
    }
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Uniswap V4 Hook        β”‚
    β”‚  (Logic & Coordination) β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
               β”‚ Uses internal accounting
               ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  PoolManager            β”‚
    β”‚  - ERC6909 claims       β”‚
    β”‚  - token0 balance: 1000 β”‚
    β”‚  - token1 balance: 2000 β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  Uniswap V4 Hook β”‚
    β”‚  (Coordination)  β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
             β”‚ Deposits/withdraws
             ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚  External Vault  β”‚
    β”‚  - token0: 1000  β”‚
    β”‚  - token1: 2000  β”‚
    β”‚  - Earning yield β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    START: I have a Uniswap V4 hook to index
    
    β”œβ”€ Q1: Does my hook require custom calldata (hookData) in swaps?
    β”‚   β”œβ”€ YES β†’ ⚠️ NOT CURRENTLY SUPPORTED
    β”‚   β”‚         Non-composable hooks will be supported in future release
    β”‚   └─ NO  β†’ Continue to Q2 (Composable hook βœ“)
    β”‚
    β”œβ”€ Q2: Does my hook store liquidity in external contracts?
    β”‚   β”œβ”€ YES β†’ Implement MetadataRequestGenerator + Parser
    β”‚   β”‚         (See Integration Guide)
    β”‚   └─ NO  β†’ Skip to Q3 (Internal liquidity - auto-handled βœ“)
    β”‚
    β”œβ”€ Q3: Does my hook need non-standard entrypoint encoding?
    β”‚   β”œβ”€ YES β†’ Implement custom HookOrchestrator
    β”‚   β”‚         (Rare - See Integration Guide)
    β”‚   └─ NO  β†’ Use default orchestrator βœ“
    β”‚
    └─ RESULT: Register and initialize Hooks DCI
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚                    UniswapV4HookDCI                           β”‚
    β”‚                                                               β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
    β”‚  β”‚  Inner DCI (Standard Dynamic Contract Indexer)           β”‚ β”‚
    β”‚  β”‚  - Component tracing                                     β”‚ β”‚
    β”‚  β”‚  - Storage operations                                    β”‚ β”‚
    β”‚  β”‚  - Pruning logic                                         β”‚ β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
    β”‚                                                               β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
    β”‚  β”‚  Metadata Orchestrator                                   β”‚ β”‚
    β”‚  β”‚  - Request generation (Generator Registry)               β”‚ β”‚
    β”‚  β”‚  - Request execution (Provider Registry)                 β”‚ β”‚
    β”‚  β”‚  - Response parsing (Parser Registry)                    β”‚ β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
    β”‚                                                               β”‚
    β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
    β”‚  β”‚  Hook Orchestrator Registry                              β”‚ β”‚
    β”‚  β”‚  - Entrypoint generation                                 β”‚ β”‚
    β”‚  β”‚  - Balance/limit injection                               β”‚ β”‚
    β”‚  β”‚  - Component updates                                     β”‚ β”‚
    β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
    Block Arrives
         ↓
    Extract Swap Hook Components
         ↓
    Categorize: Full processing or balance-only
         ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Metadata Collection (Step 3)                        β”‚
    β”‚                                                      β”‚
    β”‚ generator_registry.get_generator() β†’ None           β”‚
    β”‚                                                      β”‚
    β”‚ βœ“ Skip external RPC calls                           β”‚
    β”‚ βœ“ Balances will be enriched from block changes      β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         ↓
    Process Each Component via Orchestrator
         ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Default Orchestrator Processing                     β”‚
    β”‚                                                      β”‚
    β”‚ 1. enrich_metadata_from_block_balances()            β”‚
    β”‚    - Extracts balances from BlockChanges            β”‚
    β”‚    - No RPC calls needed                            β”‚
    β”‚                                                      β”‚
    β”‚ 2. generate_entrypoints()                           β”‚
    β”‚    - Uses enriched balances                         β”‚
    β”‚    - ERC6909 state overrides for PoolManager        β”‚
    β”‚                                                      β”‚
    β”‚ 3. Inject balances/limits into block_changes        β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         ↓
    Delegate to Inner DCI β†’ Store Results
    Block Arrives
         ↓
    Extract Swap Hook Components
         ↓
    Categorize: Full processing or balance-only
         ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Metadata Collection (Step 3)                        β”‚
    β”‚                                                     β”‚
    β”‚ generator_registry.get_generator() β†’ Some(generator)β”‚
    β”‚                                                     β”‚
    β”‚ 1. Custom Generator creates metadata requests       β”‚
    β”‚    - getReserves(), getLimits(), etc.               β”‚
    β”‚                                                     β”‚
    β”‚ 2. Provider executes batched requests               β”‚
    β”‚    - Handles retries, rate limits                   β”‚
    β”‚                                                     β”‚
    β”‚ 3. Custom Parser converts responses                 β”‚
    β”‚    - Extracts balances, limits, TVL                 β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         ↓
    Process Each Component via Orchestrator
         ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Default Orchestrator Processing                     β”‚
    β”‚                                                     β”‚
    β”‚ 1. Uses external metadata (already collected)       β”‚
    β”‚    - Balances from RPC responses                    β”‚
    β”‚    - Limits from external protocol                  β”‚
    β”‚                                                     β”‚
    β”‚ 2. generate_entrypoints()                           β”‚
    β”‚    - Uses external balances + limits                β”‚
    β”‚    - ERC6909 overrides for PoolManager              β”‚
    β”‚    - Optional: ERC20 overrides for external tokens  β”‚
    β”‚                                                     β”‚
    β”‚ 3. Inject balances/limits into block_changes        β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
         ↓
    Delegate to Inner DCI β†’ Store Results
    pub trait MetadataRequestGenerator: Send + Sync {
        fn generate_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError>;
    
        fn generate_balance_only_requests(
            &self,
            component: &ProtocolComponent,
            block: &Block,
        ) -> Result<Vec<MetadataRequest>, MetadataError>;
    
        fn supported_metadata_types(&self) -> Vec<MetadataRequestType>;
    }
    // Generate request to call getReserves() on the hook contract
    let balance_request = MetadataRequest {
        request_type: MetadataRequestType::ComponentBalance {
            token_addresses: component.tokens.clone(),
        },
        routing_key: "rpc_default".to_string(),
        generator_name: "euler".to_string(),
        transport: RpcTransport::new(
            rpc_url.clone(),
            "eth_call".to_string(),
            vec![
                json!({
                    "to": hook_address,
                    "data": "0x0902f1ac" // getReserves() selector
                }),
                json!(format!("0x{:x}", block.number)),
            ],
        ),
    };
    // Deploy lens contract at deterministic address to query limits
    let lens_address = "0x0000000000000000000000000000000000001337";
    let limits_request = MetadataRequest {
        request_type: MetadataRequestType::Limits {
            token_pair: vec![token0, token1],
        },
        routing_key: "rpc_default".to_string(),
        generator_name: "euler".to_string(),
        transport: RpcTransport::new(
            rpc_url.clone(),
            "eth_call".to_string(),
            vec![
                json!({
                    "to": lens_address,
                    "data": format!("0xaaed87a3{token0}{token1}") // getLimits(token0, token1)
                }),
                json!(format!("0x{:x}", block.number)),
                json!({  // State overrides
                    lens_address: {
                        "code": "0x608060...",  // Lens contract bytecode
                        "state": {
                            "0x00...00": format!("0x{hook_address}") // Hook addr in slot 0
                        }
                    }
                }),
            ],
        ),
    };
    #[async_trait]
    pub trait RequestProvider: Send + Sync {
        async fn execute(
            &self,
            requests: Vec<MetadataRequest>,
        ) -> Vec<MetadataResponse>;
    }
    let retry_config = RPCRetryConfig {
        max_retries: 5,
        initial_backoff_ms: 150,
        max_backoff_ms: 5000,
    };
    
    let provider = RPCMetadataProvider::new_with_retry_config(
        50,  // Max batch size
        retry_config,
    );
    Multiple MetadataRequests
            ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Group by routing_key    β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Batch RPC calls         β”‚
    β”‚ (up to batch_size)      β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                ↓
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Execute with retries    β”‚
    β”‚ (exponential backoff)   β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                ↓
    Multiple MetadataResponses
    pub trait MetadataResponseParser: Send + Sync {
        fn parse_response(
            &self,
            component: &ProtocolComponent,
            request: &MetadataRequest,
            response: &Value,
        ) -> Result<MetadataValue, MetadataError>;
    }
    pub enum MetadataValue {
        Balances(HashMap<Address, Bytes>),
        Limits(Vec<((Address, Address), (Bytes, Bytes, Option<EntryPointWithTracingParams>))>),
        Tvl(f64),
        Custom(serde_json::Value),
    }
    // Parse getReserves() response: two 32-byte balance values
    fn parse_balance_response(
        &self,
        component: &ProtocolComponent,
        response: &Value,
    ) -> Result<MetadataValue, MetadataError> {
        let hex_str = response.as_str()
            .ok_or(MetadataError::InvalidResponse)?
            .trim_start_matches("0x");
    
        // Ensure we have tokens sorted
        let mut tokens = component.tokens.clone();
        tokens.sort();
    
        // Extract balances (64 hex chars = 32 bytes each)
        let balance_0 = Bytes::from(&hex_str[0..64]);
        let balance_1 = Bytes::from(&hex_str[64..128]);
    
        let mut balances = HashMap::new();
        balances.insert(tokens[0].clone(), balance_0);
        balances.insert(tokens[1].clone(), balance_1);
    
        Ok(MetadataValue::Balances(balances))
    }
    // Parse getLimits() response: two 32-byte limit values
    fn parse_limits_response(
        &self,
        component: &ProtocolComponent,
        request: &MetadataRequest,
        response: &Value,
    ) -> Result<MetadataValue, MetadataError> {
        let hex_str = response.as_str()
            .ok_or(MetadataError::InvalidResponse)?
            .trim_start_matches("0x");
    
        let limit_0 = Bytes::from(&hex_str[0..64]);
        let limit_1 = Bytes::from(&hex_str[64..128]);
    
        // Extract token pair from request
        let token_pair = match &request.request_type {
            MetadataRequestType::Limits { token_pair } => token_pair,
            _ => return Err(MetadataError::InvalidRequest),
        };
    
        // Create entrypoint for the limits call (for reference/tracing)
        let limits_entrypoint = create_limits_entrypoint(component, token_pair)?;
    
        Ok(MetadataValue::Limits(vec![
            (token_pair[0].clone(), (limit_0, limit_1, Some(limits_entrypoint)))
        ]))
    }
    pub struct ComponentTracingMetadata {
        pub tx_hash: TxHash,
        pub balances: Option<Result<Balances, MetadataError>>,
        pub limits: Option<Result<Limits, MetadataError>>,
        pub tvl: Option<Result<Tvl, MetadataError>>,
    }
    #[async_trait]
    pub trait HookOrchestrator: Send + Sync {
        async fn update_components(
            &self,
            block_changes: &mut BlockChanges,
            components: &[ProtocolComponent],
            metadata: &HashMap<String, ComponentTracingMetadata>,
            generate_entrypoints: bool,
        ) -> Result<(), HookOrchestratorError>;
    }
    registry.register_hook_orchestrator(
        Address::from("0x55dcf9455eee8fd3f5eed17606291272cde428a8"),
        Box::new(MyOrchestrator::new()),
    );
    registry.register_hook_identifier(
        "euler_v1".to_string(),
        Box::new(EulerOrchestrator::new()),
    );
    registry.set_default_orchestrator(
        Box::new(DefaultUniswapV4HookOrchestrator::new(entrypoint_generator)),
    );
    EstimationMethod::Limits
    EstimationMethod::Balances
    // Euler provides withdrawal limits from getLimits()
    // For token0 β†’ token1 swap with limit = 1000000000000000000 (1e18):
    let amounts = [
        10000000000000000,    // 1% = 0.01e18
        100000000000000000,   // 10% = 0.1e18
        500000000000000000,   // 50% = 0.5e18
        950000000000000000,   // 95% = 0.95e18
    ];
    // 1. Define router address (deterministic)
    let router_address = Address::from("0x2626664c2603336E57B271c5C0b26F421741e481");
    
    // 2. Build swap parameters
    let pool_key = build_pool_key(component); // Extract from component attributes
    let params = ExactInputSingleParams {
        pool_key,
        zero_for_one: true,  // token0 β†’ token1
        amount_in,
        amount_out_minimum: Bytes::from([0u8]),
        hook_data: Bytes::from([0u8]),
    };
    
    // 3. Encode V4Router actions
    let actions = vec![
        V4RouterAction::SWAP_EXACT_IN_SINGLE,  // Execute swap
        V4RouterAction::SETTLE_ALL,             // Settle input token
        V4RouterAction::TAKE_ALL,               // Take output token
    ];
    
    let calldata = encode_execute_call(actions, params);
    
    // 4. Set state overrides
    let state_overrides = {
        // Deploy router bytecode
        router_address => AccountOverrides {
            code: Some(V4_MINI_ROUTER_BYTECODE),
            ...
        },
        // Set ERC6909 balances in PoolManager
        pool_manager => AccountOverrides {
            slots: erc6909_overwrites(token_in, sender, amount_in),
            ...
        },
        // (Optional) Set ERC20 balances for external tokens
        token_in => AccountOverrides {
            slots: erc20_balance_overwrite(sender, amount_in),
            ...
        },
    };
    
    // 5. Create entrypoint
    let entrypoint = EntryPointWithTracingParams {
        entry_point: EntryPoint {
            external_id: format!("swap_{token0}_{token1}_{amount_in}"),
            target: router_address,
            signature: "execute(bytes,bytes[])".to_string(),
        },
        params: TracingParams::RPCTracer(RPCTracerParams {
            caller: Some(sender),
            calldata,
            state_overrides: Some(state_overrides),
            prune_addresses: None,
        }),
    };
    // Slot calculation: keccak256(abi.encode(owner, id)) + 1
    // Where id = uint256(uint160(currency))
    fn calculate_erc6909_balance_slot(owner: &Address, currency: &Address) -> Bytes {
        let id = U256::from_be_bytes(currency.as_bytes());
        let key = encode_packed(&[
            Token::Address(owner.clone()),
            Token::Uint(id),
        ]);
        let base_slot = keccak256(&key);
        base_slot + U256::from(1)
    }
    
    // Overwrite with amount * 2 (to account for settlements)
    state_overrides.insert(
        pool_manager,
        AccountOverrides {
            slots: Some(StorageOverride::Diff(
                vec![(balance_slot, amount_in * 2)].into_iter().collect()
            )),
            ...
        },
    );
    // Detect ERC20 balance slots for tokens
    let detected_slots = balance_slot_detector
        .detect_balance_slots(&[token_in], pool_manager, block_hash)
        .await?;
    
    // Overwrite detected slots
    if let Some(slot) = detected_slots.get(&token_in) {
        state_overrides.insert(
            token_in.clone(),
            AccountOverrides {
                slots: Some(StorageOverride::Diff(
                    vec![(slot.clone(), amount_in * 2)].into_iter().collect()
                )),
                ...
            },
        );
    }
    Eulerswap'sarrow-up-right