Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Overview of Tycho, its components and how to get started.
Tycho is an open-source interface to on-chain liquidity. Tycho
Indexes DEX protocol state for you with low latency,
Simulates swaps extremely fast with one interface for all DEXs, and
Executes swaps on-chain
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.
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.
To integrate your DEX, submit a PR to Tycho Protocol Integrations on GitHub.
To get started, check the Protocol SDK docs.
Or contact our team so we can help you integrate.
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.
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.
Want to chat with our docs? Download an LLM-friendly text file of the full Tycho docs.
Clone the Tycho Simulation repository; here's the quickstart code.
Run the quickstart with execution using the following commands:
If you don't have an RPC URL, here are some public ones for Ethereum Mainnet, Unichain, and Base.
The --swapper-pk
flag is unnecessary if you want to run the quickstart without simulation or execution.
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:
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...
If you want to see results for a different token, amount, or chain, you can set additional flags:
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" --sell-amount 10 --chain "base" --swapper-pk $PK
This example would seek the best swap for 10 USDC -> WETH on Base.
If you want to see all the Tycho Indexer and Simulation logs, run with RUST_LOG=info
:
RUST_LOG=info cargo run --release --example quickstart --swapper-pk $PK
The quickstart shows you how to:
Set up and load necessary data, like available tokens.
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.
Simulate swaps on all available pools for a specified pair (e.g., USDC, WETH), and print out the most USDC available for 1 WETH.
Encode a swap to swap 1 WETH against the best pool.
Execute the swap against the Tycho Router.
Run Tycho Indexer by setting up the following environment variables:
URL (by default "tycho-beta.propellerheads.xyz"
)
API key (by default, the test key is sampletoken
)
TVL threshold: This is a filter for snapshot data for pools with TVL greater than the specified threshold (in ETH). Its default is 10,000 ETH to limit the data you pull.
The Indexer stream or the Simulation does not manage tokens; you manage them yourself.
To simplify this, load_all_tokens gets all current token information from Tycho Indexer RPC for you.
The protocol stream connects to Tycho Indexer to fetch the real-time state of protocols.
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");
Here, you only subscribe to Uniswap V2 and Balancer V2. To include additional protocols like Uniswap V3, simply add:
.exchange::<UniswapV3State>("uniswap_v3", tvl_filter.clone(), None)
For a full list of supported protocols and which simulation state (like UniswapV3State
) they use, see Supported Protocols.
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).
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).
let result = state.get_amount_out(amount_in, &tokens[0], &tokens[1])
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.
let other_result = result.new_state.get_amount_out(other_amount_in, &tokens[0], &tokens[1])
By inspecting each of the amount outs, you can then choose the protocol component with the highest amount out.
After choosing the best swap, you can use Tycho Execution to encode it.
a. Create encoder
let encoder = TychoRouterEncoderBuilder::new()
.chain(chain)
.user_transfer_type(UserTransferType::TransferFromPermit2)
.build()
.expect("Failed to build encoder");
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.
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;
⚠️ For maximum security, you should determine the minimum amount should 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 here.
let simple_swap = Swap::new(
protocol_component,
sell_token.address.clone(),
buy_token.address.clone(),
// Split defines the fraction of the amount to be swapped.
0f64,
);
// Then we create a solution object with the previous swap
let solution = Solution {
sender: user_address.clone(),
receiver: user_address,
given_token: sell_token.address,
given_amount: sell_amount,
checked_token: buy_token.address,
exact_out: false, // it's an exact in solution
checked_amount: min_amount_out,
swaps: vec![simple_swap],
..Default::default()
};
let encoded_solution = encoder
.encode_solutions(vec![solution.clone()])
.expect("Failed to encode router calldata")[0]
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:
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");
⚠️ 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
, receiver
, and transfer type.
Sign the permit2 object safely and correctly.
This gives you full control over execution. And it protects you from MEV and slippage risks.
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.
cargo run --release --example quickstart -- --swapper-pk $PK
When you provide your private key, the quickstart will check your token balances and display them before showing you options:
Your balance: 100.000000 USDC
Your WETH balance: 1.500000 WETH
If you don't have enough tokens for the swap, you'll see a warning:
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
You'll then encounter the following prompt:
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
You have three options:
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:
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
If status is false
, the simulation has failed. You can print the full simulation output for detailed failure information.
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:
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
After a successful execution, the program will exit. If the transaction fails, the program continues to stream new blocks.
Skip this swap: Ignores this swap. Then the program resumes listening for blocks.
⚠️ 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.
In this quickstart, you explored how to use Tycho to:
Connect to the Tycho Indexer: Retrieve real-time protocol data filtered by TVL.
Fetch Token and Pool Data: Load all token details and process protocol updates.
Simulate Token Swaps: Compute the output amount, gas cost, and updated protocol state for a swap.
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.
Integrate with your Solver: Add Tycho pool liquidity to your solver, using this guide.
Learn more about Tycho Execution and the datatypes necessary to encode an execution against a Tycho router or executor.
Learn more about Tycho Simulation: Explore custom filters, protocol-specific simulations, and state transitions.
Explore Tycho Indexer: Add or modify the data that Tycho indexes.
Tycho is a community project that helps DEXs and Solvers coordinate.
A quick overview, there are three ways to contribute to Tycho:
Pick an issue: Find issues to contribute to in the Tycho issue tracker – or propose an issue for a new feature.
Build an app: Build an app using Tycho. Use the specifications in Tycho X as an inspiration.
Win a bounty: Some issues, integrations, or Tycho X projects have bounties sponsored by the community; you can find all bounties here: Bounty Tracker.
Whichever way you choose to contribute – Tycho maintainers and the community are here to help you. Before you get started:
Join tycho.build – our telegram group for Tycho builders.
Reach out to Tanay - so that he can support you and ensure someone else isn't already working on the same project.
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 documentation or review the official Substreams UniswapV2 example integration.
In some cases, the community sponsors a bounty.
Specifically for:
DEX Integrations: Some DEXs can't integrate themselves - and instead sponsor a bounty for the community.
Orderflow integrations: Tools that want to use Tycho in their router.
Tycho X Projects: Teams can also sponsor bounties for Tycho X projects.
Cumulative Bounties: Several parties can sponsor and cumulate a bounty for the same issue.
Single winner: Bounties are, unless specified otherwise, awarded in full to the first team that satisfies the requirements.
Core Maintainer Support: Tycho maintainers will support every team working on a bounty. Incl. guidance, PR reviews, and final assessment.
Award of a Bounty: Each bounty has a board of three assessors, usually the one who specified the bounty, the sponsor of the bounty, and one dev from the Tycho team.
Discover a bounty: Find current open bounties in the – .
Reach out: Reach out to Tycho maintainers at or dm if you plan to work on a bounty.
Submit: Submit your work in a PR and notify maintainers.
Review & Award: After a successful review by the three assessors, maintainers will merge the PR and payout the bounty. (any merged PR automatically qualifies for the bounty.)
Find the list of open bounties here:
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.
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.
Create an InterimContractChange
for the contract and add the contract balances using upsert_token_balance
.
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:
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.
Tycho can track protocols in two ways:
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 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).
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.
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.
Setting up using Tycho is simple with the .
Available as a CLI binary, rust crate, or python package.
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:
The TychoRouterEncoder
and TychoExecutorEncoder
are responsible for validating the solutions of orders and providing you with a list of transactions that you must execute against the TychoRouter
or Executor
s.
The TychoRouterEncoder
uses a StrategyEncoder
that it choses automatically depending on the solution (see more about strategies ).
Internally, both encoders choose the appropriate SwapEncoder
(s) to encode the individual swaps, which depend on the protocols used in the solution.
The TychoRouter
calls one or more Executor
s (corresponding with the output of the SwapEncoder
s) to interact with the correct protocol and perform each swap of the solution. The TychoRouter
optionally verifies that the user receives a minimum amount of the output token.
If you select the ExecutorStrategyEncoder
during setup, you must execute the outputted calldata directly against the Executor
which corresponds to the solution’s swap’s protocol. Beware that you are responsible for performing any necessary output amount checks. This strategy is useful if you want to call Tycho executors through your own router. For more information direct execution, see .
Install . You can do so with the following command:
You can do so with any of the following:
For other installation methods, see the
Start by making a fork of the repository
Clone the fork you just created
Make sure everything compiles fine
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.
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.
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 .
Note that a single transaction may create multiple components. In such cases, TransactionProtocolComponents.components
should list all newly created ProtocolComponents
.
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.
Emitting state or balance changes for components not previously registered/stored is considered an error.
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);
}
});
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
cd substreams
cargo check --all
Once you have the calldata from Encoding, you can execute your trade in one of two ways:
Via the Tycho Router – Execute trades through our audited router for seamless execution.
Directly to the Executor – Bypass the Tycho Router and execute the trade using your own router.
The source code for the Tycho Router is here (see contract addresses here). To execute a trade, simply send the calldata generated by the TychoRouterEncoder
to the router. The setup for token transfers will vary depending on the token allowance method you chose: if you're using Permit2, ensure the Permit2 contract is approved; for standard ERC-20 approvals, approve the router to spend the token; and if you're using direct transfers, make sure you send tokens to the router before execution.
For an example of how to execute trades using the Tycho Router, refer to the Quickstart.
If you use the TychoExecutorEncoder
(see here how to select encoders), you will receive only the calldata for a single swap without any Tycho Router-specific data.
This provides greater control on the token transfers and approvals. But also gives you greater responsibility to make sure that the swap was executed correctly. You are responsible for token approvals, token transfers and error handling in your execution flow.
You need to integrate Tycho Executors into your own router contract. Implement a mechanism similar to our Dispatcher
, which uses delegate calls to interact with the Executor
contracts.
Steps to integrate Tycho Executors into your own router:
Implement something similar to Dispatcher that routes calldata to the correct Executor
contract for swap and in case of callbacks.
Ensure that your router contract correctly manages token approvals and transfers.
Append the calldata for the swap to your overall execution flow.
⚠️ Security Considerations
Tycho's Router has been audited, and its entire execution flow has been verified. However, when using direct execution, Tycho is not responsible for security checks, validation, or execution guarantees. You assume full responsibility for managing token approvals, transfers, and error handling. Ensure that your router contract implements the necessary security measures to prevent reentrancy, slippage manipulation, or loss of funds.
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.
Note: These contract helper functions require the extended block model from substreams for your target chain.
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:
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,
);
For protocols where contracts aren't necessarily pools themselves, you'll need to identify specific contracts to track. These addresses can be:
Hard-coded (for single-chain implementations)
Configured via parameters in your substreams.yaml file (for chain-agnostic implementations)
Read from the storage of a known contract (hardcoded or configured)
Here's how to extract changes for specific addresses using configuration parameters:
// 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,
);
Tycho Protocol SDK is a library to integrate liquidity layer protocols (DEXs, Staking, Lending etc.) into Tycho.
Integrating with Tycho requires three components:
Indexing: Provide the protocol state/data needed for simulation and execution
Simulation: Implement the protocol's logic for simulations
Execution: Define how to encode and execute swaps against your protocol
A comprehensive testing suite is provided to ensure the above are correctly integrated. A passing test suite is essential for an integration to be considered complete.
Provide a substreams package that emits a specified set of messages. If your protocol already has a substreams package, you can adjust it to emit the required messages.
Important: Simulation happens entirely off-chain. This means everything needed during simulation must be explicitly indexed.
Tycho offers two integration modes:
VM Integration: 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: Implement a Rust trait that defines the protocol logic. Values used in this logic must be indexed as state attributes.
To enable swap execution, implement:
SwapEncoder: A Rust struct that formats input/output tokens, pool addresses, and other parameters correctly for the Executor
contract.
Executor: A Solidity contract that handles the execution of swaps over your protocol's liquidity pools.
Tycho supports many protocol designs, however certain architectures present indexing challenges.
Before integrating, consider these unsupported designs:
Protocols where any operation that Tycho should support requires off-chain data, such as signed prices.
export RPC_URL=https://ethereum.publicnode.com
export PK=<your-private-key>
cargo run --release --example quickstart -- --swapper-pk $PK
export RPC_URL=https://base-rpc.publicnode.com
export PK=<your-private-key>
cargo run --release --example quickstart -- --chain base --swapper-pk $PK
export RPC_URL=https://unichain-rpc.publicnode.com
export PK=<your-private-key>
cargo run --release --example quickstart -- --chain unichain --swapper-pk $PK
Tycho indexes on-chain liquidity, with a current focus on token swaps. Future development can include other liquidity provisioning, lending, and derivatives.
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.
Before Tycho, you might face the following issues if you want to settle on onchain protocols:
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.
Navigate an enormous search space of liquidity sources with effective filtering heuristics.
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.
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.
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.
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.
State changes are communicated to clients through streaming interfaces.
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.
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.
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.
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.
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
10: Token analysis failed at first detection
5: Token analysis failed multiple times (after creation)
0: Failed to extract attributes, like Decimal or Symbol
This section documents Tycho's RPC API. Full swagger docs are available at: https://tycho-beta.propellerheads.xyz/docs/
The rust crate provides a flexible library for developers to integrate Tycho’s real-time data into any Rust application.
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:
A python package is available to ease integration into python-based projects. To install locally:
Git
Rust 1.84.0 or later
Python 3.9 or above
The Python client is a Python wrapper around our that enables interaction with the Tycho Indexer. It provides two main functionalities:
Streaming Client: Python wrapper around for real-time data streaming
RPC Client: Pure Python implementation for querying data
The TychoStream
class:
Locates the Rust binary (tycho-client-cli
)
Spawns the binary as a subprocess
Configures it with parameters like URL, authentication, exchanges, and filters
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:
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 endpoint):
Currently, Tycho supports the following protocols:
There are two types of implementations:
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.
Interested in adding a protocol? Refer to the documentation for implementation guidelines.
Execute swaps through any protocol.
Tycho Execution provides tools for encoding and executing swaps against Tycho routers 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 .
You can authorize token transfers in one of three ways with Tycho Execution:
Permit2
Standard ERC20 Approvals
Direct Transfers
Tycho Execution leverages 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.
When encoding a transaction, we provide functionality to build the Permit
struct. However, you are responsible for signing the permit.
For more details on Permit2 and how to use it, see the .
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.
It is possible to bypass approvals altogether by directly transferring the input token to the router within the same transaction. When using this option, the router must be funded during execution.
⚠️ Warning: This feature is intended for advanced users only. The Tycho Router is not designed to securely hold funds — any tokens left in the router are considered lost. Ensure you have appropriate security measures in place to guarantee that funds pass through the router safely and cannot be intercepted or lost.
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
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 .
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.
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:
Install foudryup and foundry
Please minimize use of assembly for security reasons.
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.
We use as our convention for formatting commit messages and PR titles.
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.
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 .
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:
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.
Some protocol design choices follow a common pattern. Instructions on how to handle these cases are provided. Such cases include:
[VM implementations]
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
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:
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.
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 .
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.
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.
uniswap_v2
Native (UniswapV2State
)
1 μs (0.001 ms)
Ethereum, Base, Unichain
uniswap_v3
Native (UniswapV3State
)
20 μs (0.02 ms)
Ethereum, Base, Unichain
uniswap_v4
Native (UniswapV4State
)
3 μs (0.003 ms)
Ethereum, Base, Unichain
vm:balancer_v2
VM (EVMPoolState
)
0.5 ms
Ethereum
vm:curve
VM (EVMPoolState
)
1 ms
Ethereum
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
ekubo_v2
Native (EkuboState
)
1.5 μs (0.0015 ms)
Ethereum
cargo check --all
cargo test --all --all-features
cargo +nightly fmt -- --check
cargo +nightly clippy --workspace --all-features --all-targets -- -D warnings
"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 .
#[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());
});
}
// Cargo.toml
[dependencies]
tycho-client = "0.66.2"
tycho-common = "0.66.2"
// 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("sampletoken".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
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
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.
The following attributes names are reserved and must be given using ProtocolComponent.static_att
. These attributes MUST be immutable.
manual_updates
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).
Set to [1u8]
to enable manual updates.
Attribute {
name: "manual_updates".to_string(),
value: [1u8],
change: ChangeType::Creation.into(),
}
pool_id
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.
This attribute value must be provided as a UTF-8 encoded string in bytes.
Attribute {
name: "pool_id".to_string(),
value: format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(),
change: ChangeType::Creation.into(),
}
The following attributes names are reserved and must be given using EntityChanges
. Unlike static attributes, state attributes are updatable.
stateless_contract_addr
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:
Direct Contract Address: A static contract address can be specified directly.
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.
This attribute value must be provided as a UTF-8 encoded string in bytes.
1. Direct Contract Address
To specify a direct contract address:
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(),
}
2. Dynamic Address Resolution
To specify a function that dynamically resolves the address:
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(),
}
stateless_contract_code
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.
This attribute value must be provided as bytes.
Attribute {
name: "stateless_contract_code_0".to_string(),
value: code.to_vec(),
change: ChangeType::Creation.into(),
}
update_marker
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.
Set to [1u8]
to trigger an update.
Attribute {
name: "update_marker".to_string(),
value: vec![1u8],
change: ChangeType::Update.into(),
};
balance_owner
[deprecated]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.
This attribute value must be provided as bytes.
Attribute {
name: "balance_owner".to_string(),
value: VAULT_ADDRESS.to_vec(),
change: ChangeType::Creation.into(),
}
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.
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:
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.
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.
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:
Each state change must include the transaction that caused it.
Each transaction must be paired with its corresponding block.
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 here. 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.
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.
We reserve some attribute names for specific functions in our simulation process. Use these names only for their intended purposes. See list of reserved attributes.
Tycho Protocol Integrations should communicate the following changes:
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.
ERC20 Balances: For any contracts involved with the protocol, you should report balance changes in terms of absolute balances.
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:
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.
In order to add a new native protocol, you will need to complete the following high-level steps:
Create a protocol state struct that contains the state of the protocol, and implements the ProtocolSim
trait (see ).
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
.
To create a VM integration, provide a manifest file and an implementation of the corresponding adapter interface. is a library to integrate DEXs and other onchain liquidity protocols into Tycho.
The following exchanges are integrated with the VM approach:
Balancer V2 (see code )
Install , start by downloading and installing the Foundry installer:
then start a new terminal session and run
Clone the Tycho Protocol SDK:
Install dependencies:
Read the documentation of the interface. It describes the functions that need to be implemented and the manifest file.
Additionally, read through the docstring of the interface and the 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:
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.
Set up test files:
Copy evm/test/TemplateSwapAdapter.t.sol
Rename to <your-adapter-name>.t.sol
Write comprehensive tests:
Test all implemented functions.
Use fuzz testing (see , especially the chapter for )
Reference existing test files: BalancerV2SwapAdapter.t.sol
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
Run the tests with
Once you have the swap adapter implemented for the new protocol, you will need to:
Generate the adapter runtime file by running the 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:
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.
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 for example implementations.
Before integrating, ensure you understand the protocol’s structure and behavior. Here are the key areas:
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.
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.
These two templates outline all necessary steps for implementation:
Use when the protocol deploys one contract per pool (e.g., UniswapV2, UniswapV3).
Usewhen the protocol uses a fixed set of contracts (e.g., UniswapV4).
Find support in the group if you don't know which template to choose.
After choosing a template:
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):
Generate the required protobuf code by running:
Register the new package within the workspace by adding it to the members list in substreams/Cargo.toml
.
Add any protocol-specific ABIs under [CHAIN]-[PROTOCOL-SYSTEM]/abi/
Your project should compile and it should run with substreams:
If you use a template, you must implement at least three key sections to ensure proper functionality:
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. They are not always needed but must be respected for compatibility.
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 ''.
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 for how to handle these cases.
To test indexing only, follow the instructions for the, but set to true. This will limit the test run to evaluating only the substreams package's indexing behavior, without running simulations.
We provide a comprehensive testing suite for Substreams modules. The suite facilitates end-to-end testing and ensures your Substreams modules function as expected. For unit tests, please use standard .
The testing suite runs with your Substreams implementation for a specific block range. It verifies that the end state matches the expected state specified by the testing YAML file. This confirms that your Substreams package is indexable and that it outputs what you expect.
Next the suite simulates transactions using engine. This will verify that all necessary data is indexed and that the provided SwapAdapter
contract works as intended.
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).
Inside your Substreams directory, you need an file. This test template file already outlines everything you need. But for clarity, we expand on some test configs here:
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 set to false
if:
the Component.id
does not correlate to a contract address;
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.
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, this config is used 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 is useful to ensure 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.
expected_components
This is a list of components whose creation you are testing. It includes all component data (tokens, static attributes, etc.). You do not need to include all components created within your test block range; only those on which the test should focus.
skip_simulation
By default this should be set to false
. It should only be set to true
temporarily if you want to isolate testing the indexing phase only; or for extenuating circumstances (like testing indexing a pool type that simulation doesn't yet support). If set to true
, you must comment on why.
An integration test should take a maximum of 5–10 minutes. If the tests take longer, here are key things you can explore:
Ensure you have no infinite loops within your code.
Ensure you are 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.
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 rebasing tokens that provide a getRate
method.
Note: Substreams uses cache to improve speed up subsequent runs of the same module. A test's first run is always slower than subsequent runs, unless you adjust the Substreams module.
There are two main causes for this error:
Your Substreams package is not indexing a contract that is necessary for simulations.
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.
For enhanced debugging, we recommend running the testing module with the --tycho-logs flag. This will enable Tycho-indexer logs.
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/
forge doc
cp ./evm/src/template ./evm/src/<your-adapter-name>
cd ./evm
forge test
>>> cd evm
>>> ./scripts/buildRuntime.sh -c “BalancerV2SwapAdapter” -s “constructor(address)” -a “0xBA12222222228d8Ba445958a75a0704d566BF2C8”
cp -r ./substreams/ethereum-template-factory ./substreams/[CHAIN]-[PROTOCOL_SYSTEM]
substreams protogen substreams.yaml --exclude-paths="google"
cd [CHAIN]-[PROTOCOL-SYSTEM]
cargo build --release --target wasm32-unknown-unknown
substreams gui substreams.yaml map_protocol_changes
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.
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 limitations 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.
An entry point defines an external call in very simple terms:
address of the contract called
signature of the function called on that contract
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.
A retrace of an entry point occurs in one of two situations:
New trace parameters are added to the entry point.
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.
To use the DCI system, you will need to extend your substream package to emit the following:
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.
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);
To add new tracing params to an existing entry point, use: tycho_substreams::entrypoint::add_entrypoint_params
. Add the created entry point parameter message to the TransactionChangesBuilder
using add_entrypoint_params
:
use tycho_substreams::entrypoint::add_entrypoint_params;
// 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_params = add_entrypoint_params(
target: target_address,
signature: "getFees(fromAddress)",
trace_data,
component_id: Some("pool_id"), // optional to link to a component
)
// use the TransactionChangesBuilder linked to an appropriate tx (up to your discretion)
builder.add_entrypoint_params(&entrypoint_params);
To link a new component to an existing entry point, use: tycho_substreams::entrypoint::add_component_to_entrypoint
. Add the created entry point message to the TransactionChangesBuilder
using add_entrypoint
:
use tycho_substreams::entrypoint::add_component_to_entrypoint;
let entrypoint = add_component_to_entrypoint(
target: target_address,
signature: "getFees(fromAddress)",
component_id: "pool_id",
)
// use the TransactionChangesBuilder for the tx where component [pool_id] was created
builder.add_entrypoint(&entrypoint);
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.
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,
})
This will be used by the DCI to extract and index contract storage updates for all contracts it identifies.
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.
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.
To integrate an EVM exchange protocol:
Implement the interface.
Create a manifest file summarizing the protocol's metadata.
The manifest file contains author information and additional static details about the protocol and its testing. Here's a list of all valid keys:
Calculates marginal prices for specified amounts.
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.
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).
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.
Retrieves pool capabilities.
Retrieves tokens for a given pool.
We mainly use this for testing, as it's redundant with the required substreams implementation.
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.
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);
How to integrate Tycho in different execution venues.
To solve orders on CoW Protocol, you'll need to prepare your solution following specific formatting requirements.
First, initialize the encoder:
let encoder = TychoRouterEncoderBuilder::new()
.chain(Chain::Ethereum)
.build()
.expect("Failed to build encoder");
Since you are not passing the swapper_pk
, the TychoRouter
will use a transferFrom
to transfer the token in as opposed to using permit2.
When solving for CoW Protocol, you need to return a Solution object that contains a list of interactions to be executed in sequence.
To solve with the Tycho Router you only need one custom interaction where:
callData
is the full encoded method calldata using the encoded solution returned from encoder.encode_solutions(...)
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.
For other venues, like UniswapX or 1inch Fusion, please contact us.
To integrate a new protocol into Tycho, you need to implement two key components:
SwapEncoder (Rust struct) – Handles swap encoding.
Executor (Solidity contract) – Executes the swap on-chain.
See more about our code architecture here.
Each new protocol requires a dedicated SwapEncoder
that implements the SwapEncoder
trait. This trait defines how swaps for the protocol are encoded into calldata.
fn encode_swap(
&self,
swap: Swap,
encoding_context: EncodingContext,
) -> Result<Vec<u8>, EncodingError>;
This function encodes a swap and its relevant context information into calldata that is compatible with the Executor
contract. The output of the SwapEncoder
is the input of the Executor
(see next section). See current implementations here.
If your protocol needs some specific constant addresses please add them in config/protocol_specific_addresses.json.
After implementing your SwapEncoder
, you need to:
Add your protocol with a placeholder address in: config/executor_addresses.json and config/test_executor_addresses.json
Add your protocol in the SwapEncoderBuilder
.
Every integrated protocol requires its own swap executor contract. This contract must conform to the IExecutor
interface, allowing it to interact with the protocol and perform swaps be leveraging the RestrictTransferFrom
contract. See currently implemented executors here.
The IExecutor
interface has the main method:
function swap(uint256 givenAmount, bytes calldata data)
external
payable
returns (uint256 calculatedAmount)
{
This function:
Accepts the input amount (givenAmount
). Note that the input amount is calculated at execution time and not during encoding. This is to account for possible slippage.
Processes the swap using the provided calldata (data
) which is the output of the SwapEncoder
.
Returns the final output amount (calculatedAmount
).
Ensure that the implementation supports transferring received tokens to a designated receiver address, either within the swap function or through an additional transfer step.
If the protocol requires token approvals (allowances) before swaps can occur, manage these approvals within the implementation to ensure smooth execution of the swap.
Some protocols require a callback during swap execution. In these cases, the executor contract must inherit from ICallback
and implement the necessary callback functions.
Required Methods
function handleCallback(
bytes calldata data
) external returns (bytes memory result);
function verifyCallback(bytes calldata data) external view;
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.
The Executor contracts manage token transfers between the user, protocols, and the Tycho Router. The only exception is when unwrapping WETH to ETH after a swap—in this case, the router performs the final transfer to the receiver.
The TychoRouter
architecture optimizes token transfers and reduces gas costs during both single and sequential swaps. Whenever possible:
The executor transfers input tokens directly from the user to the target protocol.
The executor instructs the protocol to send output tokens directly to the next protocol in the swap sequence.
For the final swap in a sequence, the protocol sends output tokens directly to the user.
Each executor must inherit from the RestrictTransferFrom
contract, which enables flexible and safe transfer logic. During encoding, the executor receives instructions specifying one of the following transfer types:
TRANSFER_FROM
Transfers tokens from the user's wallet into the TychoRouter
or into the pool. It can use permit2 or normal token transfers.
TRANSFER
Assumes funds are already in the TychoRouter
and transfers tokens into the pool
NONE
Assumes tokens are already in place for the swap; no transfer action is taken.
Two key constants are used in encoding to configure protocol-specific behavior:
IN_TRANSFER_REQUIRED_PROTOCOLS
A list of protocols that require tokens to be transferred into the pool prior to swapping. These protocols do not perform a transferFrom
during the swap
themselves, and therefore require tokens to be transferred beforehand or during a callback.
CALLBACK_CONSTRAINED_PROTOCOLS
Protocols that require owed tokens to be transferred during a callback. In these cases, tokens cannot be transferred directly from the previous pool before the current swap begins.
Include your protocol in these constants if necessary.
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
SwapEncoder
↔ Executor
integration testVerify 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
.
In tests/protocol_integration_tests.rs
, write a Rust test that encodes a single swap and saves the calldata using write_calldata_to_file()
.
In TychoRouterTestSetup
, deploy your new executor and add it to executors list in deployExecutors
.
Run the setup to retrieve your executor’s deployed address and add it to config/test_executor_addresses.json.
Create a new Solidity test contract that inherits from TychoRouterTestSetup
. For example:
contract TychoRouterForYouProtocolTest is TychoRouterTestSetup {
function getForkBlock() public pure override returns (uint256) {
return 22644371; // Use a block that fits your test scenario
}
function testSingleYourProtocolIntegration() public {
...
}
}
These tests ensure your integration works end-to-end within Tycho’s architecture.
Once your implementation is approved:
Deploy the executor contract on the appropriate network.
Contact us to whitelist the new executor address on our main router contract.
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, enabling it to execute swaps seamlessly.
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.
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
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)
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)
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
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
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
Each component also has dynamic attributes that change over time and contain state required to simulate operations.
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.
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:
Pushes finalized state changes to permanent storage
Stores unfinalized data in system buffers (see ReorgBuffers below)
Performs basic validation, such as checking for the existence of related entities and verifying the connectedness of incoming data
Aggregates processed changes and broadcasts them to connected clients
Handles chain reorganizations by reverting changes in buffers and sending correction messages to clients
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.
ReorgBuffers store unfinalized blockchain state changes that haven't yet reached sufficient confirmation depth.
This approach allows Tycho to:
Respond to queries with the latest state by merging buffer data with permanent storage
Handle chain reorganizations by rolling back unconfirmed changes
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.
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.
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.
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
Advantages:
Faster integration of new protocols
No need to reimplement complex protocol math
Disadvantages:
Significantly slower simulation compared to native implementations
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
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
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.
The flexible nature of Solutions allows them to represent simple single-hop swaps, sequential multi-hop trades, or split routes where a token amount is distributed across multiple pools simultaneously. You can see more about Solutions .
A Transaction turns a Solution into actual blockchain instructions. It contains the specific data needed to execute your trade: which contract to call, what function to use, what parameters to pass, and how much native token to send.
This is the final product that you submit to the blockchain. It handles approvals, native token wrapping/unwrapping, and proper contract interactions so you don't have to. For more about Transactions, see .
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.
The encoder uses the single strategy when a Solution has exactly one swap on one pool.
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.
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 .
Before continuing, ensure the following tools and libraries are installed on your system:
Docker: Containerization platform for running applications in isolated environments.
Conda: Package and environment manager for Python and other languages.
AWS CLI: Tool to manage AWS services from the command line.
Git: Version control tool
Rust: Programming language and toolchain
GCC: GNU Compiler Collection
libpq: PostgreSQL client library
OpenSSL (libssl): OpenSSL development library
pkg-config: Helper tool for managing compiler flags
pip: Python package installer
The testing system relies on an EVM Archive node to fetch the state from a previous block. Indexing only with Substreams, as done in Tycho's production mode, requires syncing blocks since the protocol's deployment date, which can take a long time. The node skips this requirement by fetching all the required account's storage slots on the block specified in the testing yaml
file.
The node also needs to support the debug_storageRangeAt method, as it's a requirement for our Token Quality Analysis.
The testing module runs a minified version of Tycho Indexer. You can ensure that the latest version is correctly setup in your PATH by running the following command on your terminal:
> tycho-indexer --version
tycho-indexer 0.62.0 # should match the latest version published on GitHub
If the command above does not provide the expected output, you need to (re)install Tycho.
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
git clone [email protected]:propeller-heads/tycho-indexer.git
cd tycho-indexer
Step 2: Build the binary in release mode
cargo build --release --bin tycho-indexer
Step 3: Link the binary to a directory in your system's PATH:
sudo ln -s $(pwd)/target/release/tycho-indexer /usr/local/bin/tycho-indexer
NOTE: This command requires
/usr/local/bin
to be included in the system'sPATH.
While this is typically the case, there may be exceptions.If
/usr/local/bin
is not in yourPATH
, you can either:
Add it to your
PATH
by exporting it:export PATH="/usr/local/bin:$PATH"
Or create a symlink in any of the following directories (if they are in your
PATH
):/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin
Step 4: Verify Installation
> tycho-indexer --version
tycho-indexer 0.54.0 # should match the latest version published on GitHub
We provide a binary compiled for Linux x86/x64 architecture on our GitHub releases page.
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 Releases page, locate the latest version (e.g.: 0.54.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:
tar -xvzf tycho-indexer-x86_64-unknown-linux-gnu-{version}.tar.gz
Step 3: Link the binary to a directory in your system's PATH:
// Ensure the binary is executable:
sudo chmod +x tycho-indexer
// Create symlink
sudo ln -s $(pwd)/tycho-indexer /usr/local/bin/tycho-indexer
NOTE: This command requires
/usr/local/bin
to be included in the system'sPATH.
While this is typically the case, there may be exceptions.If
/usr/local/bin
is not in yourPATH
, you can either:
Add it to your
PATH
by exporting it:export PATH="/usr/local/bin:$PATH"
Or create a symlink in any of the following directories (if they are in your
PATH
):/bin /sbin /usr/bin /usr/sbin /usr/local/bin /usr/local/sbin
Step 4: Verify Installation
> tycho-indexer --version
tycho-indexer 0.54.0 # should match the latest version published on GitHub
Tests are defined in a yaml
file. A documented template can be found at substreams/ethereum-template/integration_test.tycho.yaml
. The configuration file should include:
The target Substreams config file.
The corresponding SwapAdapter and args to build it.
The expected protocol types.
The tests to be run.
Each test will index all blocks between start-block
and stop-block
, verify that the indexed state matches the expected state, and optionally simulate transactions using the provided SwapAdapter
. For more details on the individual test-level configs, see here.
You will also need the VM Runtime file for the adapter contract. Our testing script should be able to build it using your test config. The script to generate this file manually is available under evm/scripts/buildRuntime.sh
.
To set up your test environment, run the setup environment script. It will create a Conda virtual env and install all the required dependencies.
./setup_env.sh
This script must be run from within the tycho-protocol-sdk/testing
directory.
Lastly, you need to activate the conda env:
conda activate tycho-protocol-sdk-testing
Export the required environment variables for the execution. You can find the available environment variables in the .env.default
file. Please create a .env
file in the testing
directory and set the required environment variables.
RPC_URL
Description: The URL for the Ethereum RPC endpoint. This is used to fetch the storage data.
The node needs to be an archive node and support debug_storageRangeAt method.
Example: export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"
SUBSTREAMS_API_TOKEN
Description: The JWT token for accessing Substreams services. This token is required for authentication. Please refer to Substreams Authentication guide to setup and validate your token.
Example: export SUBSTREAMS_API_TOKEN=eyJhbGci...
If you do not have one already, you must build the wasm file of the package you wish to test. This can be done by navigating to the package directory and running:
cargo build --target wasm32-unknown-unknown --release
Then, run a local Postgres test database using docker-compose.
docker compose -f ./testing/docker-compose.yaml up -d db
Run tests for your package. This must be done from the main project directory.
python ./testing/src/runner/cli.py --package "your-package-name"
Example
If you want to run tests for ethereum-balancer-v2
, use:
// Activate conda environment
conda activate tycho-protocol-sdk-testing
// Setup Environment Variables
export RPC_URL="https://ethereum-mainnet.core.chainstack.com/123123123123"
export SUBSTREAMS_API_TOKEN=eyJhbGci...
// Build BalancerV2's Substreams wasm
cd substreams
cargo build --release --package ethereum-balancer-v2 --target wasm32-unknown-unknown
cd ..
// Run Postgres DB using Docker compose
docker compose -f ./testing/docker-compose.yaml up -d db
// Run the testing file
python ./testing/src/runner/cli.py --package "ethereum-balancer-v2"
Testing CLI args
A list and description of all available CLI args can be found using:
python ./testing/src/runner/cli.py --help
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 hosted endpoint or running your own instance.
In this guide, you'll learn more about the Tycho Client and the streamed data models.
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
The client is written in Rust and available as:
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.
Currently, interacting with the hosted Tycho Indexer doesn't require a personalized API Key; you can use the key sampletoken
. For broader rate-limiting, priority support, and access to new products, please contact @tanay_j
on Telegram.
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.
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:
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:
Set an exact TVL boundary:
tycho-client --min-tvl 100 --exchange uniswap_v2
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.
Set a ranged TVL boundary (recommended):
tycho-client --remove-tvl-threshold 95 --add-tvl-threshold 100 --exchange uniswap_v3
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
.
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.
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.
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 here 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.
Tycho Client CLI installation documentation
The binary client is recommended for 2 situations:
For a quick setup, to consume data from Tycho Indexer direct on a terminal
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.
This guide provides two methods to install Tycho Client:
Install with Cargo (recommended for most users)
Download pre-built binaries from GitHub Releases
Cargo
Rust 1.84.0 or later
cargo install tycho-client
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 GitHub.
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:
tar -xvzf tycho-client-aarch64-apple-darwin-{version}.tar.gz
Step 3: Link the binary to a directory in your system's PATH (recommended):
// Ensure the binary is executable:
sudo chmod +x tycho-client
// Create symlink
sudo ln -s $(pwd)/tycho-client /usr/local/bin/tycho-client
Step 4: Verify Installation
tycho-client --version
tycho-client 0.54.0 # should match the latest version published on GitHub
You should see the Tycho Client version displayed. If you need more guidance, contact us via Telegram
If you're connecting to our hosted service, please follow our Authentication to get an API Key. Once you have a key, export it using an environment variable
export TYCHO_AUTH_TOKEN={your_token}
or use the command line flag
tycho-client --auth-key {your_token}
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:
tycho-client --exchange uniswap_v2 --exchange uniswap_v3 --min-tvl 100 --tycho-url
tycho-beta.propellerheads.xyz
Or skip secure connections entirely with --no-tls
for local setups [coming soon].
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.
For more details on using the CLI and its parameters, run:
tycho client --help
For extended explanation on how each parameter works, check our Usageguide.
first step to execute a trade on chain is encoding.
Our Rust crate offers functionality to convert your trades into calldata, which the Tycho contracts can execute.
See this Quickstart section for an example of how to encode your trade.
These are the models used as input and output of the encoding crate.
The Solution
struct specifies the details of your order and how it should be filled. This is the input of the encoding module.
The Solution
struct consists of the following attributes:
given_token
Bytes
The token being sold (exact in) or bought (exact out)
given_amount
BigUint
Amount of the given token
checked_token
Bytes
The token being bought. This token's final balance will be checked by the router using checked_amount
.
sender
Bytes
Address of the sender of the given token
receiver
Bytes
Address of the receiver of the checked token
exact_out
bool
False if the solution is an exact input solution (i.e. solves a sell order). Currently only exact input solutions are supported.
router_address
Bytes
Address of the router contract to be used. See Tycho addresses .
swaps
Vec<Swap>
List of swaps to fulfil the solution.
checked_amount
BigUint
Minimum amount out to be checked for the solution to be valid if passing through the TychoRouter
.
native_action
Option<NativeAction>
If set, the native token will be wrapped before the swap or unwrapped after the swap (more ).
user_data
Option<Bytes>
Additional user data that can be passed to encoding.
Our router accepts wrapping native tokens to wrapped token before performing the first swap, and unwrapping wrapped tokens to native tokens after the final swap, before sending the funds to the receiver.
In order to perform this, the native_action
parameter of the solution must be set to either Some(NativeAction.WRAP)
or Some(NativeAction.UNWRAP)
.
When wrapping:
The given_token
of the solution should be ETH
The token_in
of the first swap should be WETH
When unwrapping:
The checked_token
of the solution should be ETH
The token_out
of the final swap should be WETH
A solution consists of one or more swaps. A swap represents a swap operation to be performed on a pool.
The Swap
struct has the following attributes:
component
ProtocolComponent
Protocol component from Tycho core
token_in
Bytes
Token you provide to the pool
token_out
Bytes
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%)
To create a Swap
, use the new
function where you can pass any struct that implements Into<ProtocolComponent>
.
Solutions can have splits where one or more token hops are split between two or more pools. This means that the output of one swap can be split into several parts, each used as the input for subsequent swaps. The following are examples of different split configurations:
By combining splits creatively, you can build highly customized and complex trade paths.
We perform internal validation on split swaps. A split swap is considered valid if:
The checked token is reachable from the given token through the swap path
There are no tokens that are unconnected
Each split amount is small than 1 (100%) and larger or equal to 0 (0%)
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%)
Certain protocols, such as Uniswap V4, allow you to save token transfers between consecutive swaps thanks to their flash accounting. In case your solution contains sequential (non-split) swaps of such protocols, our encoders compress these consecutive swaps into a single swap group, meaning that a single call to our executor is sufficient for performing these multiple swaps.
In the example above, the encoder will compress three consecutive swaps into the following swap group to call the Executor:
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,
}
One solution will contain multiple swap groups if different protocols are used.
The output of encoding is EncodedSolution
. It has the following attributes.
swaps
Vec<u8>
The encoded calldata for the swaps.
interacting_with
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.
permit
Option<PermitSingle>
Optional permit object for the trade (if permit2 is enabled).
Tycho Execution provides two main encoder types:
TychoRouterEncoder: This encoder prepares calldata for execution via the Tycho Router contract. It supports complex swap strategies, including multi-hop and split swaps. Use this when you want Tycho to handle routing and execution within its own router contract.
TychoExecutorEncoder: This encoder prepares calldata for direct execution of individual swaps using the Executor contracts, bypassing the router entirely. It encodes one swap at a time and is ideal when integrating Tycho Executors into your own router contract. See more details here.
Choose the encoder that aligns with how you plan to route and execute trades.
For each encoder, there is a corresponding builder:
TychoRouterEncoderBuilder
TychoExecutorEncoderBuilder
Both builders require the target chain to be set.
let encoder = TychoRouterEncoderBuilder::new()
.chain(Chain::Ethereum)
.user_transfer_type(UserTransferType::TransferFromPermit2)
.build()
.expect("Failed to build encoder");
let encoder = TychoExecutorEncoderBuilder::new()
.chain(Chain::Ethereum)
.build()
.expect("Failed to build encoder");
You can convert solutions into calldata using:
let encoded_solutions = encoder.encode_solutions(solutions);
This method returns a Vec<
EncodedSolution
>
, which contains only the encoded swaps of the solutions. It does not build the full calldata. You must encode the full method call yourself. If you are using Permit2 for token transfers, you need to sign the permit object as well.
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/Executor from you
minAmountOut
and tokenOut
– the minimum amount you want to receive of token out. For maximum security, this min amount should be determined from a third party source.
receiver
– who receives the final output
wrap/unwrap
flags – if native token wrapping is needed
isTransferFromAllowed
– if this should perform a transferFrom
to retrieve the input funds. This will be false if you send tokens to the router in the same transaction before the swap.
These execution guardrails protect against exploits such as MEV. Correctly setting these guardrails yourself gives you full control over your swap security and ensures that the transaction cannot be exploited in any way.
Refer to the quickstart code for an example of how to convert an EncodedSolution
into full calldata. You must tailor this example to your use case to ensure that arguments are safe and correct. See the functions defined in the TychoRouter
contract for reference.
First, build and install the binary:
# Build the project
cargo build --release
# Install the binary to your system
cargo install --path .
After installation, you can use the tycho-encode
command from any directory in your terminal.
The command lets you choose the encoder:
tycho-router
: Encodes a transaction using the TychoRouterEncoder
.
tycho-execution
: Encodes a transaction using the TychoExecutorEncoder
.
The commands accept the same options as the builders (more here).
Example
Here's a complete example that encodes a swap from WETH to DAI using Uniswap V2 and the TychoRouterEncoder
with Permit2 on Ethereum:
echo '{"sender":"0x1234567890123456789012345678901234567890","receiver":"0x1234567890123456789012345678901234567890","given_token":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","given_amount":"1000000000000000000","checked_token":"0x6B175474E89094C44Da98b954EedeAC495271d0F","exact_out":false,"checked_amount":"990000000000000000","swaps":[{"component":{"id":"0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640","protocol_system":"uniswap_v2","protocol_type_name":"UniswapV2Pool","contract_addresses":[], "chain":"ethereum","tokens":["0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"],"contract_ids":["0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D"],"static_attributes":{"factory":"0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f"},"change":"Update","creation_tx":"0x0000000000000000000000000000000000000000000000000000000000000000","created_at":"2024-02-28T12:00:00"},"token_in":"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2","token_out":"0x6B175474E89094C44Da98b954EedeAC495271d0F","split":0.0}],"direct_execution":true}' | tycho-encode --chain ethereum --user-transfer-type transfer-from-permit2 tycho-router
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 here.
The tycho-simulation
package is available on crates.io.
To use the simulation tools with Ethereum Virtual Machine (EVM) chains, add the optional evm
feature flag to your dependency configuration:
tycho-simulation = {
version = "x.y.z", # Replace with latest version
features = ["evm"]
}
Add this to your project's Cargo.toml
file.
All protocols implement the ProtocolSim
trait (see definition here). It has the main methods:
spot_price
returns the pool's current marginal price.
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
get_amount_out
simulates token swaps.
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError>;
You receive a GetAmountOutResult
, which is defined as follows:
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
}
new state
allows you to, for example, simulate consecutive swaps in the same protocol.
Please refer to the in-code documentation of the ProtocolSim
trait and its methods for more in-depth information.
fee
returns the fee of the protocol as a ratio.
For example if the fee is 1%, the value returned would be 0.01.
fn fee(&self) -> f64;
get_limits
returns a tuple containing the maximum amount in and out that can be traded between two tokens.
fn get_limits(
&self,
sell_token: Address,
buy_token: Address,
) -> Result<(BigUint, BigUint), SimulationError>;
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:
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_tokens
is supplied and can be used as follows:
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("sampletoken"), // 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;
You can use the ProtocolStreamBuilder 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:
use tycho_simulation::evm::{
engine_db::tycho_db::PreCachedDB,
protocol::{
filters::balancer_pool_filter, 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)
.exchange::<EVMPoolState<PreCachedDB>>(
"vm:balancer_v2",
tvl_filter.clone(),
Some(balancer_pool_filter),
)
.auth_key(Some("sampletoken"))
.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");
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 BlockUpdate
messages which consist of:
block number
- 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)
states
- the updated ProtocolSim
states for all components modified in this block
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 theProtocolComponent
objects within thenew_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 Supported Protocols.
You can find an example of a price printer here.
Clone the repo, then run:
export RPC_URL=<your-eth-rpc-url>
cargo run --release --example price_printer -- --tvl-threshold 1000
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.
This endpoint is used to check the health of the service.
{"message":"No db connection","status":"NotReady"}
GET /v1/health HTTP/1.1
Host: tycho-beta.propellerheads.xyz
authorization: YOUR_API_KEY
Accept: */*
OK
{
"message": "No db connection",
"status": "NotReady"
}
This endpoint retrieves components within a specific execution environment, filtered by various criteria.
Currently supported Blockchains
Filter by component ids
Filters by protocol, required to correctly apply unconfirmed state from ReorgBuffers
The minimum TVL of the protocol components to return, denoted in the chain's native token.
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
}
OK
{
"pagination": {
"page": 1,
"page_size": 1,
"total": 1
},
"protocol_components": [
{
"chain": "ethereum",
"change": "Update",
"contract_ids": [
"text"
],
"created_at": "2025-07-08T06:31:58.877Z",
"creation_tx": "text",
"id": "text",
"protocol_system": "text",
"protocol_type_name": "text",
"static_attributes": {
"ANY_ADDITIONAL_PROPERTY": "text"
},
"tokens": [
"text"
]
}
]
}
This endpoint retrieves the state of protocols within a specific execution environment.
Max page size supported is 100
Currently supported Blockchains
Whether to include account balances in the response. Defaults to true.
Filters response by protocol components ids
Filters by protocol, required to correctly apply unconfirmed state from ReorgBuffers
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": "2025-07-08T06:31:58.877Z"
}
}
OK
{
"pagination": {
"page": 1,
"page_size": 1,
"total": 1
},
"states": [
{
"attributes": {
"ANY_ADDITIONAL_PROPERTY": "text"
},
"balances": {
"ANY_ADDITIONAL_PROPERTY": "text"
},
"component_id": "text"
}
]
}
This endpoint retrieves the protocol systems available in the indexer.
Currently supported Blockchains
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
}
}
OK
{
"pagination": {
"page": 1,
"page_size": 1,
"total": 1
},
"protocol_systems": [
"text"
]
}
This endpoint retrieves tokens for a specific execution environment, filtered by various criteria. The tokens are returned in a paginated format.
Currently supported Blockchains
Quality is between 0-100, where:
Filters tokens by addresses
Filters tokens by recent trade activity
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
}
OK
{
"pagination": {
"page": 1,
"page_size": 1,
"total": 1
},
"tokens": [
{
"address": "0xc9f2e6ea1637E499406986ac50ddC92401ce1f58",
"chain": "ethereum",
"decimals": 1,
"gas": [
1
],
"quality": 1,
"symbol": "WETH",
"tax": 1
}
]
}
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.
Maximum page size for this endpoint is 100
Currently supported Blockchains
Filters response by contract addresses
Does not filter response, only required to correctly apply unconfirmed state from ReorgBuffers
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": "2025-07-08T06:31:58.877Z"
}
}
OK
{
"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
}
}