Uniswap V4 Hooks DCI
Complete Indexing Solution for All Uniswap V4 Hooks
Introduction
What is the Hooks DCI?
The Hooks DCI (Dynamic Contract Indexer) is Tycho's specialized indexing plugin for all Uniswap V4 hooks. It extends the standard DCI with capabilities designed specifically for hooks, including automatic balance tracking, sophisticated entrypoint generation, and optional external metadata collection.
The Hooks DCI is required for indexing all Uniswap V4 hooks. It provides a complete solution with sensible defaults that work out-of-the-box for most hooks, and optional extension points for hooks with advanced requirements.
In this document, we break down UniswapV4 pools into different categories, describe the challenges to index each one, and provide a guide on how to index pools that need custom integration to be indexed by Tycho.
Hook Types:
Before diving into the solution, we need to understand the different categories that differentiate hooks Indexing:
1. Composable vs Non-Composable
Composable Hooks: Work with empty
hookDatain swapsNon-Composable Hooks: Require custom
hookDatafor before or after swap hooks
2. Internal vs External Liquidity
Internal Liquidity: Tokens accounting in PoolManager as ERC6909 claims
External Liquidity: Tokens in external contracts, outside UniswapV4's Pool Manager.
We define deeper these categories further on the Hook Classification section
Currently, Tycho only supports Composable Hooks. Non-composable support is coming soon.
Why Hooks DCI Exists
The standard DCI works well for self-contained protocols, but Uniswap V4 hooks require some extra steps for correct indexing. For Tycho to index all the state necessary for simulating each hook, it needs to have well-defined Entrypoints that cover all the possible Hook execution paths. This was achieved by adding:
V4-specific entrypoint generation with custom swap encoding and state overrides, aiming to cover all the paths that a hook might take
Flexible metadata collection supporting both internal (automatic) and external (custom) liquidity sources
Registry-based extension system for hooks with specialized requirements
State-aware processing to optimize performance and handle failures gracefully
On Background & Concepts section below, we provide detailed explanations of hook types and architecture.
Background & Concepts
Uniswap V4 Hooks Primer
Uniswap V4 introduces hooks - smart contracts that can execute custom logic at specific points in the pool lifecycle. Hooks enable powerful features like:
Dynamic fees based on market conditions
Custom oracle integrations
Liquidity management strategies
Integration with external DeFi protocols
Each hook address encodes permissions in its bytes, indicating which lifecycle events it handles:
Bit 7: beforeSwap
Bit 6: afterSwap
Bit 5: beforeAddLiquidity
Bit 4: afterAddLiquidity
...The Hooks DCI only processes hooks with swap permissions (beforeSwap and/or afterSwap), as these are the ones that manage liquidity and affect swap behavior.
Hook Classification
Understanding hook types helps determine what (if anything) you need to implement for your hook.
1. Composable vs Non-Composable Hooks
1.1 - Composable Hooks (Currently Supported)
Composable hooks do NOT require custom calldata (hookData) to be passed during swaps. They work with empty or default hookData.
// Composable hook - works with empty hookData
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData // Can be empty: 0x
) external returns (bytes4, BeforeSwapDelta, uint24);Examples:
Dynamic fee hooks (calculate fees from pool state)
Oracle integration hooks (read from external oracles, no user input needed)
Internal liquidity management hooks
Eulerswap's external liquidity hooks
1.2 - Non-Composable Hooks (Future Support)
⚠️ Not Currently Supported: Non-composable hooks REQUIRE specific calldata to be passed in hookData for each swap. Support for these hooks is planned for a future release.
// Non-composable hook - requires meaningful hookData
function beforeSwap(
address sender,
PoolKey calldata key,
IPoolManager.SwapParams calldata params,
bytes calldata hookData // MUST contain routing info, signatures, etc.
) external returns (bytes4, BeforeSwapDelta, uint24) {
// Decode hookData for routing decisions, user signatures, etc.
(address router, bytes memory signature) = abi.decode(hookData, (address, bytes));
// ...
}Examples (not yet supported):
Hooks requiring user signatures per swap
Intent-based routing hooks
Hooks with swap-specific configuration
2. Internal vs External Liquidity (Primary Classification)
This is the key distinction that determines what you, as a hook integrator, need to implement.
2.1 - Internal Liquidity Hooks
✅ No Custom Implementation Required - Composable Internal liquidity hooks are automatically indexed by Tycho.
┌─────────────────────────┐
│ Uniswap V4 Hook │
│ (Logic & Coordination) │
└──────────┬──────────────┘
│ Uses internal accounting
↓
┌─────────────────────────┐
│ PoolManager │
│ - ERC6909 claims │
│ - token0 balance: 1000 │
│ - token1 balance: 2000 │
└─────────────────────────┘Characteristics:
All liquidity tracked in PoolManager as ERC6909 claims
Balances automatically extracted from blockchain state
No external calls needed for Metadata (Pool balances and Limits)
Works with default orchestrator out-of-the-box
How It Works:
Hooks DCI extracts pool balances from
BlockChanges.balance_changesDefault orchestrator's
enrich_metadata_from_block_balances()builds metadata from the pool internal balanceEntrypoint generator creates state overrides for PoolManager ERC6909 only
Everything works automatically - no custom code needed
Examples:
Dynamic fee hooks using PoolManager liquidity
Hooks with custom AMM curves but standard storage
Time-weighted average price (TWAP) hooks
Most hooks that don't integrate with external DeFi
2.2 - External Liquidity Hooks
⚙️ Requires Custom Metadata Implementation
┌──────────────────┐
│ Uniswap V4 Hook │
│ (Coordination) │
└────────┬─────────┘
│ Deposits/withdraws
↓
┌──────────────────┐
│ External Vault │
│ - token0: 1000 │
│ - token1: 2000 │
│ - Earning yield │
└──────────────────┘Characteristics:
Liquidity stored in external contracts (lending vaults, yield protocols, etc.)
Requires custom RPC or API calls to fetch current balances and withdrawal limits
Needs custom
MetadataRequestGeneratorandMetadataResponseParserMay need balance slot detection for accurate simulations
What You Need to Implement:
MetadataRequestGenerator- Creates RPC requests for balances/limitsMetadataResponseParser- Parses RPC responses into structured data(Optional) Custom
HookOrchestrator- Only if entrypoint encoding is non-standard
On Metadata Collection Systemwe go deeper on the Metadata collection and how you can implement to track any hook with External Liquidity. We also provide an Hook Integration Guideto guide you through the implementation steps.
Examples:
Euler Hooks: Tokens in Euler lending vaults
Yearn Integration: Tokens in Yearn vaults earning yield
Staking Hooks: Tokens locked in staking contracts
What You Need to Implement (Decision Tree)
START: I have a Uniswap V4 hook to index
├─ Q1: Does my hook require custom calldata (hookData) in swaps?
│ ├─ YES → ⚠️ NOT CURRENTLY SUPPORTED
│ │ Non-composable hooks will be supported in future release
│ └─ NO → Continue to Q2 (Composable hook ✓)
│
├─ Q2: Does my hook store liquidity in external contracts?
│ ├─ YES → Implement MetadataRequestGenerator + Parser
│ │ (See Integration Guide)
│ └─ NO → Skip to Q3 (Internal liquidity - auto-handled ✓)
│
├─ Q3: Does my hook need non-standard entrypoint encoding?
│ ├─ YES → Implement custom HookOrchestrator
│ │ (Rare - See Integration Guide)
│ └─ NO → Use default orchestrator ✓
│
└─ RESULT: Register and initialize Hooks DCIArchitecture Overview
High-Level System Diagram
┌───────────────────────────────────────────────────────────────┐
│ UniswapV4HookDCI │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Inner DCI (Standard Dynamic Contract Indexer) │ │
│ │ - Component tracing │ │
│ │ - Storage operations │ │
│ │ - Pruning logic │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Metadata Orchestrator │ │
│ │ - Request generation (Generator Registry) │ │
│ │ - Request execution (Provider Registry) │ │
│ │ - Response parsing (Parser Registry) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Hook Orchestrator Registry │ │
│ │ - Entrypoint generation │ │
│ │ - Balance/limit injection │ │
│ │ - Component updates │ │
│ └──────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────┘Core Components
1. UniswapV4HookDCI
The main orchestrator that coordinates all hook indexing operations. It:
Filters components with swap hook permissions
Categorizes components by processing state
Coordinates metadata collection
Manages component lifecycle (success, failure, retry, pause)
Delegates to inner DCI for tracing operations
2. Metadata Orchestrator System
Purpose: Collects external metadata for hooks with external liquidity. Optional - only used when a metadata generator is registered for a hook.
For increased performance, the external data collection is split into a three-layer architecture:
Layer 1: Request Generation (Protocol-Specific - Optional)
Creates
MetadataRequestobjects specifying what data to fetchSupports different request types: Balances, Limits, TVL
Not needed for internal liquidity hooks - system uses block balances instead
Layer 2: Request Execution (Transport-Specific)
Implemented by providers (e.g.,
RPCMetadataProvider)Handles batching, deduplication, retries
Routes requests to appropriate backends (RPC, HTTP APIs)
Layer 3: Response Parsing (Protocol-Specific - Optional)
Converts raw responses into structured metadata
Handles errors and validation
Fallback for Internal Liquidity: When no metadata generator is registered, the default orchestrator automatically enriches metadata from BlockChanges.balance_changes - no RPC calls needed.
3. Hook Orchestrator Registry
Maps hook addresses/identifiers to orchestrators that handle component processing. The default orchestrator (DefaultUniswapV4HookOrchestrator) handles both internal and external liquidity hooks automatically.
Lookup Priority:
By Hook Address: Direct mapping for specific hook deployments
By Identifier: String-based lookup (e.g., "euler_v1")
Default Orchestrator: Fallback for all hooks
Orchestrator Responsibilities:
Generating entrypoints with appropriate tracing parameters
Injecting balances and limits into components
Updating component state attributes
Key Feature: The default orchestrator's enrich_metadata_from_block_balances() method automatically extracts balances from blockchain state for hooks without custom metadata generators. This means internal liquidity hooks work with zero custom code.
Internal vs External Liquidity Paths
The system automatically chooses the appropriate path based on whether a metadata generator is registered:
Path A: Internal Liquidity (Automatic)
Block Arrives
↓
Extract Swap Hook Components
↓
Categorize: Full processing or balance-only
↓
┌─────────────────────────────────────────────────────┐
│ Metadata Collection (Step 3) │
│ │
│ generator_registry.get_generator() → None │
│ │
│ ✓ Skip external RPC calls │
│ ✓ Balances will be enriched from block changes │
└─────────────────────────────────────────────────────┘
↓
Process Each Component via Orchestrator
↓
┌─────────────────────────────────────────────────────┐
│ Default Orchestrator Processing │
│ │
│ 1. enrich_metadata_from_block_balances() │
│ - Extracts balances from BlockChanges │
│ - No RPC calls needed │
│ │
│ 2. generate_entrypoints() │
│ - Uses enriched balances │
│ - ERC6909 state overrides for PoolManager │
│ │
│ 3. Inject balances/limits into block_changes │
└─────────────────────────────────────────────────────┘
↓
Delegate to Inner DCI → Store ResultsPath B: External Liquidity (Custom Metadata)
Block Arrives
↓
Extract Swap Hook Components
↓
Categorize: Full processing or balance-only
↓
┌─────────────────────────────────────────────────────┐
│ Metadata Collection (Step 3) │
│ │
│ generator_registry.get_generator() → Some(generator)│
│ │
│ 1. Custom Generator creates metadata requests │
│ - getReserves(), getLimits(), etc. │
│ │
│ 2. Provider executes batched requests │
│ - Handles retries, rate limits │
│ │
│ 3. Custom Parser converts responses │
│ - Extracts balances, limits, TVL │
└─────────────────────────────────────────────────────┘
↓
Process Each Component via Orchestrator
↓
┌─────────────────────────────────────────────────────┐
│ Default Orchestrator Processing │
│ │
│ 1. Uses external metadata (already collected) │
│ - Balances from RPC responses │
│ - Limits from external protocol │
│ │
│ 2. generate_entrypoints() │
│ - Uses external balances + limits │
│ - ERC6909 overrides for PoolManager │
│ - Optional: ERC20 overrides for external tokens │
│ │
│ 3. Inject balances/limits into block_changes │
└─────────────────────────────────────────────────────┘
↓
Delegate to Inner DCI → Store ResultsKey Takeaway: The only difference is Step 3 (Metadata Collection). The rest of the flow is identical. This is why internal liquidity hooks require no custom implementation - they automatically use Path A.
Metadata Collection System
The metadata collection system uses a three-layer architecture that separates protocol-specific logic from transport concerns. This system is optional and only activated when you register a custom metadata generator for your hook.
Two Paths for Metadata Collection
Path A: Internal Liquidity (Automatic - No Implementation Needed)
System checks:
generator_registry.get_generator(component)→NoneDefault orchestrator calls
enrich_metadata_from_block_balances()Balances extracted from
BlockChanges.balance_changesZero RPC calls, zero custom code required
Path B: External Liquidity (Requires Implementation)
System checks:
generator_registry.get_generator(component)→Some(generator)Generator creates RPC requests for external data
Provider executes requests
Parser converts responses to structured metadata
Requires implementing Generator + Parser traits
Layer 1: Request Generation (Protocol-Specific - External Liquidity Only)
Purpose: Create metadata requests specific to your hook's data needs.
Interface:
pub trait MetadataRequestGenerator: Send + Sync {
fn generate_requests(
&self,
component: &ProtocolComponent,
block: &Block,
) -> Result<Vec<MetadataRequest>, MetadataError>;
fn generate_balance_only_requests(
&self,
component: &ProtocolComponent,
block: &Block,
) -> Result<Vec<MetadataRequest>, MetadataError>;
fn supported_metadata_types(&self) -> Vec<MetadataRequestType>;
}Metadata Request Types:
ComponentBalance: Fetch token balances for the componentLimits: Fetch maximum swap amounts (withdrawal limits, liquidity caps)Tvl: Total value locked calculationCustom: Extensible for hook-specific needs
Euler Example - Balance Request:
// Generate request to call getReserves() on the hook contract
let balance_request = MetadataRequest {
request_type: MetadataRequestType::ComponentBalance {
token_addresses: component.tokens.clone(),
},
routing_key: "rpc_default".to_string(),
generator_name: "euler".to_string(),
transport: RpcTransport::new(
rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({
"to": hook_address,
"data": "0x0902f1ac" // getReserves() selector
}),
json!(format!("0x{:x}", block.number)),
],
),
};Euler Example - Limits Request with State Overrides:
// Deploy lens contract at deterministic address to query limits
let lens_address = "0x0000000000000000000000000000000000001337";
let limits_request = MetadataRequest {
request_type: MetadataRequestType::Limits {
token_pair: vec![token0, token1],
},
routing_key: "rpc_default".to_string(),
generator_name: "euler".to_string(),
transport: RpcTransport::new(
rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({
"to": lens_address,
"data": format!("0xaaed87a3{token0}{token1}") // getLimits(token0, token1)
}),
json!(format!("0x{:x}", block.number)),
json!({ // State overrides
lens_address: {
"code": "0x608060...", // Lens contract bytecode
"state": {
"0x00...00": format!("0x{hook_address}") // Hook addr in slot 0
}
}
}),
],
),
};The lens contract pattern allows querying multiple values in a single RPC call using a custom contract deployed via state overrides.
Layer 2: Request Execution (Transport-Specific)
Purpose: Execute metadata requests efficiently, handling batching and retries.
Interface:
#[async_trait]
pub trait RequestProvider: Send + Sync {
async fn execute(
&self,
requests: Vec<MetadataRequest>,
) -> Vec<MetadataResponse>;
}RPCMetadataProvider Features:
Batching: Groups multiple
eth_callrequests into JSON-RPC batchesDeduplication: Avoids duplicate requests in the same batch
Retry Logic: Exponential backoff for transient RPC failures
Concurrency Limiting: Prevents overwhelming RPC endpoints
Configuration:
let retry_config = RPCRetryConfig {
max_retries: 5,
initial_backoff_ms: 150,
max_backoff_ms: 5000,
};
let provider = RPCMetadataProvider::new_with_retry_config(
50, // Max batch size
retry_config,
);Request Flow:
Multiple MetadataRequests
↓
┌─────────────────────────┐
│ Group by routing_key │
└───────────┬─────────────┘
↓
┌─────────────────────────┐
│ Batch RPC calls │
│ (up to batch_size) │
└───────────┬─────────────┘
↓
┌─────────────────────────┐
│ Execute with retries │
│ (exponential backoff) │
└───────────┬─────────────┘
↓
Multiple MetadataResponsesLayer 3: Response Parsing (Protocol-Specific)
Purpose: Convert raw RPC responses into structured metadata.
Interface:
pub trait MetadataResponseParser: Send + Sync {
fn parse_response(
&self,
component: &ProtocolComponent,
request: &MetadataRequest,
response: &Value,
) -> Result<MetadataValue, MetadataError>;
}Metadata Value Types:
pub enum MetadataValue {
Balances(HashMap<Address, Bytes>),
Limits(Vec<((Address, Address), (Bytes, Bytes, Option<EntryPointWithTracingParams>))>),
Tvl(f64),
Custom(serde_json::Value),
}Euler Example - Balance Parsing:
// Parse getReserves() response: two 32-byte balance values
fn parse_balance_response(
&self,
component: &ProtocolComponent,
response: &Value,
) -> Result<MetadataValue, MetadataError> {
let hex_str = response.as_str()
.ok_or(MetadataError::InvalidResponse)?
.trim_start_matches("0x");
// Ensure we have tokens sorted
let mut tokens = component.tokens.clone();
tokens.sort();
// Extract balances (64 hex chars = 32 bytes each)
let balance_0 = Bytes::from(&hex_str[0..64]);
let balance_1 = Bytes::from(&hex_str[64..128]);
let mut balances = HashMap::new();
balances.insert(tokens[0].clone(), balance_0);
balances.insert(tokens[1].clone(), balance_1);
Ok(MetadataValue::Balances(balances))
}Euler Example - Limits Parsing:
// Parse getLimits() response: two 32-byte limit values
fn parse_limits_response(
&self,
component: &ProtocolComponent,
request: &MetadataRequest,
response: &Value,
) -> Result<MetadataValue, MetadataError> {
let hex_str = response.as_str()
.ok_or(MetadataError::InvalidResponse)?
.trim_start_matches("0x");
let limit_0 = Bytes::from(&hex_str[0..64]);
let limit_1 = Bytes::from(&hex_str[64..128]);
// Extract token pair from request
let token_pair = match &request.request_type {
MetadataRequestType::Limits { token_pair } => token_pair,
_ => return Err(MetadataError::InvalidRequest),
};
// Create entrypoint for the limits call (for reference/tracing)
let limits_entrypoint = create_limits_entrypoint(component, token_pair)?;
Ok(MetadataValue::Limits(vec![
(token_pair[0].clone(), (limit_0, limit_1, Some(limits_entrypoint)))
]))
}Assembled Metadata
All parsed metadata for a component is assembled into:
pub struct ComponentTracingMetadata {
pub tx_hash: TxHash,
pub balances: Option<Result<Balances, MetadataError>>,
pub limits: Option<Result<Limits, MetadataError>>,
pub tvl: Option<Result<Tvl, MetadataError>>,
}Note that each field is Option<Result<...>>:
None: Metadata type not requestedSome(Ok(...)): Successfully collectedSome(Err(...)): Collection failed (triggers component failure)
4.3 Hook Orchestrators
Hook orchestrators coordinate the processing of components, including entrypoint generation and metadata injection.
Orchestrator Responsibilities
Entrypoint Generation: Create
EntryPointWithTracingParamsfor tracingBalance Injection: Add balances to
ProtocolComponentfor storageLimits Injection: Provide limits for RPC query optimization
State Updates: Modify component state attributes as needed
Interface
#[async_trait]
pub trait HookOrchestrator: Send + Sync {
async fn update_components(
&self,
block_changes: &mut BlockChanges,
components: &[ProtocolComponent],
metadata: &HashMap<String, ComponentTracingMetadata>,
generate_entrypoints: bool,
) -> Result<(), HookOrchestratorError>;
}Parameters:
block_changes: Mutable reference to modify transactions and componentscomponents: Components to process in this callmetadata: Collected external metadata (balances, limits, TVL)generate_entrypoints:truefor full processing,falsefor balance-only
Registry Lookup Mechanisms
The HookOrchestratorRegistry provides multiple lookup strategies:
1. By Hook Address (Highest Priority)
registry.register_hook_orchestrator(
Address::from("0x55dcf9455eee8fd3f5eed17606291272cde428a8"),
Box::new(MyOrchestrator::new()),
);2. By Hook Identifier (Medium Priority)
registry.register_hook_identifier(
"euler_v1".to_string(),
Box::new(EulerOrchestrator::new()),
);3. Default Orchestrator (Lowest Priority)
registry.set_default_orchestrator(
Box::new(DefaultUniswapV4HookOrchestrator::new(entrypoint_generator)),
);Lookup Order:
Try hook address lookup
Try hook identifier lookup (from component static attributes)
Fall back to default orchestrator
Return error if no orchestrator found
Default Orchestrator
The DefaultUniswapV4HookOrchestrator handles most hook types:
Features:
Extracts balances from block changes for components without external metadata
Delegates entrypoint generation to
UniswapV4DefaultHookEntrypointGeneratorInjects balances and limits into
BlockChangesHandles both full processing and balance-only updates
When to Use Custom Orchestrator:
Hook requires special entrypoint encoding
Balance/limit data needs transformation before injection
Component state updates follow custom logic
Hook uses non-standard token accounting
Euler Example - When Default is Sufficient:
For Euler hooks, the default orchestrator works well because:
Balances come directly from metadata (no transformation needed)
Limits are standard max withdrawal amounts
Entrypoints follow standard Uniswap V4 swap encoding
No special state updates required
Therefore, Euler only requires custom metadata generator/parser, not a custom orchestrator.
4.4 Entrypoint Generation
Entrypoints define the calls that will be traced to understand how a component behaves under different conditions.
Entrypoints allow Tycho to:
Simulate swaps at various amounts to understand pricing curves
Test edge cases (e.g., swaps at 1%, 50%, 95% of liquidity)
Understand touched contracts and state that are necessary for reproducing a hook's behavior
For hooks with external liquidity, accurate entrypoints require:
Correct balance overwrites (both in PoolManager and external contracts)
Appropriate swap amounts based on limits
State overrides to simulate external contract states
Swap Amount Estimation
The system supports two estimation strategies:
1. Limits-Based Estimation (Preferred)
EstimationMethod::LimitsWhen limits are available, generate samples at:
1% of limit (test small swaps)
10% of limit (test medium swaps)
50% of limit (test large swaps)
95% of limit (test near-maximum swaps)
2. Balance-Based Estimation (Fallback)
EstimationMethod::BalancesWhen limits are unavailable, generate samples at:
1% of balance
2% of balance
5% of balance
10% of balance
Euler Example - Limits-Based Amounts:
// Euler provides withdrawal limits from getLimits()
// For token0 → token1 swap with limit = 1000000000000000000 (1e18):
let amounts = [
10000000000000000, // 1% = 0.01e18
100000000000000000, // 10% = 0.1e18
500000000000000000, // 50% = 0.5e18
950000000000000000, // 95% = 0.95e18
];V4MiniRouter Pattern
For Uniswap V4, entrypoints use a custom router deployed via state overrides:
Purpose: Execute swap operations against the PoolManager with proper token settlements
Pattern:
// 1. Define router address (deterministic)
let router_address = Address::from("0x2626664c2603336E57B271c5C0b26F421741e481");
// 2. Build swap parameters
let pool_key = build_pool_key(component); // Extract from component attributes
let params = ExactInputSingleParams {
pool_key,
zero_for_one: true, // token0 → token1
amount_in,
amount_out_minimum: Bytes::from([0u8]),
hook_data: Bytes::from([0u8]),
};
// 3. Encode V4Router actions
let actions = vec![
V4RouterAction::SWAP_EXACT_IN_SINGLE, // Execute swap
V4RouterAction::SETTLE_ALL, // Settle input token
V4RouterAction::TAKE_ALL, // Take output token
];
let calldata = encode_execute_call(actions, params);
// 4. Set state overrides
let state_overrides = {
// Deploy router bytecode
router_address => AccountOverrides {
code: Some(V4_MINI_ROUTER_BYTECODE),
...
},
// Set ERC6909 balances in PoolManager
pool_manager => AccountOverrides {
slots: erc6909_overwrites(token_in, sender, amount_in),
...
},
// (Optional) Set ERC20 balances for external tokens
token_in => AccountOverrides {
slots: erc20_balance_overwrite(sender, amount_in),
...
},
};
// 5. Create entrypoint
let entrypoint = EntryPointWithTracingParams {
entry_point: EntryPoint {
external_id: format!("swap_{token0}_{token1}_{amount_in}"),
target: router_address,
signature: "execute(bytes,bytes[])".to_string(),
},
params: TracingParams::RPCTracer(RPCTracerParams {
caller: Some(sender),
calldata,
state_overrides: Some(state_overrides),
prune_addresses: None,
}),
};ERC6909 Overwrites
Uniswap V4 uses ERC6909 for internal PoolManager accounting. To simulate swaps, we must set balances:
// Slot calculation: keccak256(abi.encode(owner, id)) + 1
// Where id = uint256(uint160(currency))
fn calculate_erc6909_balance_slot(owner: &Address, currency: &Address) -> Bytes {
let id = U256::from_be_bytes(currency.as_bytes());
let key = encode_packed(&[
Token::Address(owner.clone()),
Token::Uint(id),
]);
let base_slot = keccak256(&key);
base_slot + U256::from(1)
}
// Overwrite with amount * 2 (to account for settlements)
state_overrides.insert(
pool_manager,
AccountOverrides {
slots: Some(StorageOverride::Diff(
vec![(balance_slot, amount_in * 2)].into_iter().collect()
)),
...
},
);Balance Slot Detection
For hooks with external liquidity, tokens may need balances set in external contracts:
Optional Feature: EVMBalanceSlotDetector
// Detect ERC20 balance slots for tokens
let detected_slots = balance_slot_detector
.detect_balance_slots(&[token_in], pool_manager, block_hash)
.await?;
// Overwrite detected slots
if let Some(slot) = detected_slots.get(&token_in) {
state_overrides.insert(
token_in.clone(),
AccountOverrides {
slots: Some(StorageOverride::Diff(
vec![(slot.clone(), amount_in * 2)].into_iter().collect()
)),
...
},
);
}Euler Example - Balance Overwrites:
For Euler hooks, tokens are held in external vaults. The entrypoint generator:
Detects balance slots for vault tokens (wstETH, WETH, etc.)
Overwrites those slots with swap amounts
Ensures PoolManager has ERC6909 balances
Simulates full swap flow including vault withdrawals
This allows accurate tracing even though liquidity is external to PoolManager.
\
Last updated