Hook Integration Guide
Integration Guide
This page provides step-by-step instructions for integrating any Uniswap V4 hook with the Hooks DCI.
Determine Your Requirements
Before implementing anything, determine what (if anything) you need to customize:
Decision Tree
START: I want to index my Uniswap V4 hook
Q1: Is my hook composable (works with empty hookData)?
├─ NO → ⚠️ STOP: Non-composable hooks not yet supported
│ Wait for future release with hookData source support
└─ YES → Continue to Q2
Q2: Where does my hook store liquidity?
├─ In PoolManager (ERC6909 claims)
│ └─→ INTERNAL LIQUIDITY
│ ✓ No custom code needed - your hook will be automatically indexed
│
│
└─ In external contracts (vaults, protocols, etc.)
└─→ EXTERNAL LIQUIDITY
⚙️ Requires metadata generator + parser
→ Continue to the next step
Q3: (External liquidity only) Does my hook need custom entrypoint encoding?
├─ NO → Implement Generator + Parser only
│ Skip custom orchestrator (use default)
│
└─ YES → Implement Generator + Parser + Custom Orchestrator
Quick Reference Table
Internal Liquidity
Nothing (auto-handled)
External Liquidity (Standard)
Generator + Parser
External Liquidity (Custom)
Generator + Parser + Orchestrator
Non-Composable
Not supported yet
Prerequisites
Understand the Hook's Architecture:
Where tokens are stored (which external contracts?)
How balances are queried (what functions?)
How limits are determined (withdrawal limits, caps, etc.)
What state needs to be simulated
1. Minimal Setup (Internal Liquidity Hooks)
If your hook stores all liquidity in the PoolManager and is Composable, your hook should be auto-indexed by Tycho.
If you have external liquidity, continue to the next Section
2. Custom Setup (External Liquidity Hooks)
2.1 Implementation Steps
Step 1: Implement Metadata Request Generator
The generator creates requests to fetch external data for your hook.
Trait to Implement:
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>;
}Template:
use tycho_common::models::{Block, Address};
use crate::extractor::dynamic_contract_indexer::component_metadata::{
MetadataRequestGenerator, MetadataRequest, MetadataRequestType, MetadataError,
};
pub struct MyHookGenerator {
rpc_url: String,
}
impl MyHookGenerator {
pub fn new(rpc_url: String) -> Self {
Self { rpc_url }
}
// Helper to extract hook address from component
fn get_hook_address(
&self,
component: &ProtocolComponent,
) -> Result<Address, MetadataError> {
component
.static_attributes
.get("hooks")
.and_then(|v| v.as_address())
.ok_or_else(|| MetadataError::InvalidComponent(
"Missing 'hooks' attribute".to_string()
))
}
}
impl MetadataRequestGenerator for MyHookGenerator {
fn generate_requests(
&self,
component: &ProtocolComponent,
block: &Block,
) -> Result<Vec<MetadataRequest>, MetadataError> {
let hook_address = self.get_hook_address(component)?;
let mut requests = Vec::new();
// 1. Generate balance request
requests.push(self.create_balance_request(component, block, &hook_address)?);
// 2. Generate limits requests (if applicable)
requests.extend(self.create_limits_requests(component, block, &hook_address)?);
// 3. Generate TVL request (if applicable)
// requests.push(self.create_tvl_request(component, block, &hook_address)?);
Ok(requests)
}
fn generate_balance_only_requests(
&self,
component: &ProtocolComponent,
block: &Block,
) -> Result<Vec<MetadataRequest>, MetadataError> {
let hook_address = self.get_hook_address(component)?;
// Only generate balance request for balance-only updates
Ok(vec![self.create_balance_request(component, block, &hook_address)?])
}
fn supported_metadata_types(&self) -> Vec<MetadataRequestType> {
vec![
MetadataRequestType::ComponentBalance {
token_addresses: vec![],
},
MetadataRequestType::Limits {
token_pair: vec![],
},
]
}
}
impl MyHookGenerator {
fn create_balance_request(
&self,
component: &ProtocolComponent,
block: &Block,
hook_address: &Address,
) -> Result<MetadataRequest, MetadataError> {
// TODO: Implement your balance request logic
// Example: Call a function like getBalances() or getReserves()
let calldata = format!(
"0x{}", // Function selector + encoded parameters
"YOUR_FUNCTION_SELECTOR_HERE"
);
Ok(MetadataRequest {
request_type: MetadataRequestType::ComponentBalance {
token_addresses: component.tokens.clone(),
},
routing_key: "rpc_default".to_string(),
generator_name: "my_hook".to_string(), // Must match parser registration
transport: RpcTransport::new(
self.rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({
"to": hook_address,
"data": calldata,
}),
json!(format!("0x{:x}", block.number)),
],
),
})
}
fn create_limits_requests(
&self,
component: &ProtocolComponent,
block: &Block,
hook_address: &Address,
) -> Result<Vec<MetadataRequest>, MetadataError> {
let mut requests = Vec::new();
let tokens = &component.tokens;
// Generate limits request for each token pair
for i in 0..tokens.len() {
for j in (i + 1)..tokens.len() {
let token_pair = vec![tokens[i].clone(), tokens[j].clone()];
// TODO: Implement your limits request logic
// This might involve:
// - Calling a function on the hook
// - Using a lens contract pattern (like Euler)
// - Querying external protocol limits
requests.push(MetadataRequest {
request_type: MetadataRequestType::Limits {
token_pair: token_pair.clone(),
},
routing_key: "rpc_default".to_string(),
generator_name: "my_hook".to_string(),
transport: RpcTransport::new(
self.rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({
"to": "YOUR_CONTRACT_ADDRESS",
"data": "YOUR_CALLDATA",
}),
json!(format!("0x{:x}", block.number)),
// Optional: state overrides
// json!({ "address": { "code": "0x...", "state": {...} } }),
],
),
});
}
}
Ok(requests)
}
}Euler Reference Implementation:
// From: tycho-indexer/src/extractor/dynamic_contract_indexer/hooks/integrations/euler/metadata_generator.rs
impl MetadataRequestGenerator for EulerMetadataGenerator {
fn generate_requests(
&self,
component: &ProtocolComponent,
block: &Block,
) -> Result<Vec<MetadataRequest>, MetadataError> {
let hook_address = self.get_hook_address(component)?;
let mut requests = Vec::new();
// 1. Balance request: Call getReserves() on hook
requests.push(MetadataRequest {
request_type: MetadataRequestType::ComponentBalance {
token_addresses: component.tokens.clone(),
},
routing_key: "rpc_default".to_string(),
generator_name: "euler".to_string(),
transport: RpcTransport::new(
self.rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({
"to": hook_address,
"data": "0x0902f1ac" // getReserves() selector
}),
json!(format!("0x{:x}", block.number)),
],
),
});
// 2. Limits requests: Use lens contract with state overrides
let lens_address = "0x0000000000000000000000000000000000001337";
let lens_bytecode_hex = hex::encode(EULER_LENS_BYTECODE_BYTES);
for token_pair in get_token_pairs(&component.tokens) {
requests.push(MetadataRequest {
request_type: MetadataRequestType::Limits {
token_pair: token_pair.clone(),
},
routing_key: "rpc_default".to_string(),
generator_name: "euler".to_string(),
transport: RpcTransport::new(
self.rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({
"to": lens_address,
"data": format!(
"0xaaed87a3{}{}", // getLimits(address,address)
&token_pair[0].to_string()[2..],
&token_pair[1].to_string()[2..]
)
}),
json!(format!("0x{:x}", block.number)),
json!({ // Deploy lens contract at deterministic address
lens_address: {
"code": format!("0x{}", lens_bytecode_hex),
"state": {
// Store hook address in slot 0
"0x0000000000000000000000000000000000000000000000000000000000000000":
format!("0x{:0>64}", &hook_address.to_string()[2..])
}
}
}),
],
),
});
}
Ok(requests)
}
fn generate_balance_only_requests(
&self,
component: &ProtocolComponent,
block: &Block,
) -> Result<Vec<MetadataRequest>, MetadataError> {
// Only balance request needed for balance-only updates
let hook_address = self.get_hook_address(component)?;
Ok(vec![MetadataRequest {
request_type: MetadataRequestType::ComponentBalance {
token_addresses: component.tokens.clone(),
},
routing_key: "rpc_default".to_string(),
generator_name: "euler".to_string(),
transport: RpcTransport::new(
self.rpc_url.clone(),
"eth_call".to_string(),
vec![
json!({"to": hook_address, "data": "0x0902f1ac"}),
json!(format!("0x{:x}", block.number)),
],
),
}])
}
}Key Decisions:
Balance Request: How do you query balances? Direct call, lens contract, or multiple calls?
Limits Request: Do you have withdrawal limits, liquidity caps, or other constraints?
State Overrides: Do you need to deploy helper contracts or modify state for queries?
Token Pairs: Do limits apply per token or per token pair?
Step 2: Implement Response Parser
The parser converts raw RPC responses into structured metadata.
Trait to Implement:
pub trait MetadataResponseParser: Send + Sync {
fn parse_response(
&self,
component: &ProtocolComponent,
request: &MetadataRequest,
response: &Value,
) -> Result<MetadataValue, MetadataError>;
}Template:
use serde_json::Value;
use tycho_common::models::{ProtocolComponent, Address};
use crate::extractor::dynamic_contract_indexer::component_metadata::{
MetadataResponseParser, MetadataRequest, MetadataRequestType,
MetadataValue, MetadataError,
};
pub struct MyHookParser;
impl MetadataResponseParser for MyHookParser {
fn parse_response(
&self,
component: &ProtocolComponent,
request: &MetadataRequest,
response: &Value,
) -> Result<MetadataValue, MetadataError> {
// Extract hex string from response
let hex_str = response
.as_str()
.ok_or_else(|| MetadataError::InvalidResponse(
"Response is not a string".to_string()
))?
.trim_start_matches("0x");
match &request.request_type {
MetadataRequestType::ComponentBalance { token_addresses } => {
self.parse_balances(component, hex_str, token_addresses)
}
MetadataRequestType::Limits { token_pair } => {
self.parse_limits(component, request, hex_str, token_pair)
}
MetadataRequestType::Tvl => {
self.parse_tvl(component, hex_str)
}
_ => Err(MetadataError::UnsupportedRequestType),
}
}
}
impl MyHookParser {
fn parse_balances(
&self,
component: &ProtocolComponent,
hex_str: &str,
token_addresses: &[Address],
) -> Result<MetadataValue, MetadataError> {
// TODO: Parse your balance response format
// Example: Two 32-byte values (64 hex chars each)
if hex_str.len() < 128 {
return Err(MetadataError::InvalidResponse(
format!("Balance response too short: {} chars", hex_str.len())
));
}
// Ensure tokens are sorted (for consistent mapping)
let mut tokens = component.tokens.clone();
tokens.sort();
// Extract balances
let balance_0 = Bytes::from(&hex_str[0..64]);
let balance_1 = Bytes::from(&hex_str[64..128]);
let mut balances = HashMap::new();
balances.insert(tokens[0].clone(), balance_0);
balances.insert(tokens[1].clone(), balance_1);
Ok(MetadataValue::Balances(balances))
}
fn parse_limits(
&self,
component: &ProtocolComponent,
request: &MetadataRequest,
hex_str: &str,
token_pair: &[Address],
) -> Result<MetadataValue, MetadataError> {
// TODO: Parse your limits response format
if hex_str.len() < 128 {
return Err(MetadataError::InvalidResponse(
format!("Limits response too short: {} chars", hex_str.len())
));
}
// Extract limits
let limit_0 = Bytes::from(&hex_str[0..64]);
let limit_1 = Bytes::from(&hex_str[64..128]);
// Optional: Create entrypoint for the limits call itself
// This can be used for tracing/reference
let limits_entrypoint = self.create_limits_entrypoint(
component,
token_pair,
request,
).ok(); // Make optional
Ok(MetadataValue::Limits(vec![
(token_pair[0].clone(), (limit_0, limit_1, limits_entrypoint))
]))
}
fn parse_tvl(
&self,
component: &ProtocolComponent,
hex_str: &str,
) -> Result<MetadataValue, MetadataError> {
// TODO: Parse TVL if applicable
// This might involve converting token amounts to USD values
Err(MetadataError::UnsupportedRequestType)
}
fn create_limits_entrypoint(
&self,
component: &ProtocolComponent,
token_pair: &[Address],
request: &MetadataRequest,
) -> Result<EntryPointWithTracingParams, MetadataError> {
// TODO: Create entrypoint for limits call
// This is optional but useful for tracing
Ok(EntryPointWithTracingParams {
entry_point: EntryPoint {
external_id: format!(
"limits_{}_{}_{}",
component.id,
token_pair[0],
token_pair[1]
),
target: /* your target address */,
signature: "getLimits(address,address)".to_string(),
},
params: TracingParams::RPCTracer(RPCTracerParams {
caller: None,
calldata: /* your calldata */,
state_overrides: /* your overrides */,
prune_addresses: None,
}),
})
}
}Euler Reference Implementation:
// From: tycho-indexer/src/extractor/dynamic_contract_indexer/hooks/integrations/euler/metadata_generator.rs
impl MetadataResponseParser for EulerMetadataResponseParser {
fn parse_response(
&self,
component: &ProtocolComponent,
request: &MetadataRequest,
response: &Value,
) -> Result<MetadataValue, MetadataError> {
let res_str = response
.as_str()
.ok_or_else(|| MetadataError::InvalidResponse(
"Expected string response".to_string()
))?
.trim_start_matches("0x");
match &request.request_type {
MetadataRequestType::ComponentBalance { .. } => {
// Parse getReserves() response: two uint112 values
if res_str.len() < 128 {
return Err(MetadataError::InvalidResponse(
format!("Balance response too short: {}", res_str.len())
));
}
let balance_0 = Bytes::from(&res_str[0..64]);
let balance_1 = Bytes::from(&res_str[64..128]);
let mut tokens = component.tokens.clone();
tokens.sort();
let mut balances = HashMap::new();
balances.insert(tokens[0].clone(), balance_0);
balances.insert(tokens[1].clone(), balance_1);
Ok(MetadataValue::Balances(balances))
}
MetadataRequestType::Limits { token_pair } => {
// Parse getLimits() response from lens contract
if res_str.len() < 128 {
return Err(MetadataError::InvalidResponse(
format!("Limits response too short: {}", res_str.len())
));
}
let limit_0 = Bytes::from(&res_str[0..64]);
let limit_1 = Bytes::from(&res_str[64..128]);
// Create entrypoint for limits call
let hook_address = component
.static_attributes
.get("hooks")
.and_then(|v| v.as_address())
.ok_or_else(|| MetadataError::InvalidComponent(
"Missing hooks attribute".to_string()
))?;
let limits_entrypoint = create_euler_limits_entrypoint(
component,
&hook_address,
token_pair,
)?;
Ok(MetadataValue::Limits(vec![
(token_pair[0].clone(), (limit_0, limit_1, Some(limits_entrypoint)))
]))
}
_ => Err(MetadataError::UnsupportedRequestType),
}
}
}Key Considerations:
Response Format: Understand the ABI encoding of your response
Error Handling: Handle malformed responses gracefully
Token Ordering: Ensure consistent token ordering between request and response
Entrypoint Creation: Optional but useful for tracing the limits call itself
Step 3: (Optional) Implement Custom Hook Orchestrator
Most hooks can use the default orchestrator. Implement a custom one only if you need:
Special entrypoint encoding logic
Custom balance/limit transformations
Hook-specific state updates
Non-standard token accounting
When Default is Sufficient:
Balances come directly from metadata
Limits are straightforward max amounts
Standard Uniswap V4 swap encoding works
No special state transformations needed
Euler Example: Uses the default orchestrator because it meets all standard requirements.
Custom Orchestrator Template (if needed):
use async_trait::async_trait;
use crate::extractor::{
dynamic_contract_indexer::{
hook_orchestrator::{HookOrchestrator, HookOrchestratorError},
component_metadata::ComponentTracingMetadata,
},
models::BlockChanges,
};
pub struct MyHookOrchestrator {
entrypoint_generator: Box<dyn HookEntrypointGenerator>,
}
#[async_trait]
impl HookOrchestrator for MyHookOrchestrator {
async fn update_components(
&self,
block_changes: &mut BlockChanges,
components: &[ProtocolComponent],
metadata: &HashMap<String, ComponentTracingMetadata>,
generate_entrypoints: bool,
) -> Result<(), HookOrchestratorError> {
// TODO: Implement custom orchestration logic
// 1. Extract metadata for components
// 2. Generate entrypoints (if generate_entrypoints == true)
// 3. Inject balances into components
// 4. Inject limits for RPC optimization
// 5. Update block_changes with new data
Ok(())
}
}For most use cases, proceed with the default orchestrator and skip this step.
Step 4: Register Components
Set up all registries to wire your implementation into the Hooks DCI.
Registration Code:
In your integration folder add a register function with your protocol specifics
pub(super) fn register_my_hook_integrations(
generator_registry: &mut MetadataGeneratorRegistry,
parser_registry: &mut MetadataResponseParserRegistry,
_provider_registry: &mut ProviderRegistry,
rpc_url: String,
) {
generator_registry.register_hook_identifier(
"my_hook".to_string(),
Box::new(MyHookMetadataGenerator::new(rpc_url)),
);
parser_registry.register_parser("my_hook".to_string(), Box::new(MyHookMetadataResponseParser));
}Then add it in the global registration function with other hooks
// From: tycho-indexer/src/extractor/dynamic_contract_indexer/hooks/integrations/mod.rs
pub(super) fn register_integrations(
generator_registry: &mut MetadataGeneratorRegistry,
parser_registry: &mut MetadataResponseParserRegistry,
provider_registry: &mut ProviderRegistry,
rpc_url: String,
) {
euler::register_euler_integrations(
generator_registry,
parser_registry,
provider_registry,
rpc_url,
);
// Add your hook registration here
}Key Configuration Points:
Generator Registration: Use
register_hook_identifier()if your components have a "hook_identifier" static attribute, orregister_hook_generator()for specific addressesParser Name: Must match the
generator_namein your MetadataRequestsRouting Key: Must match the
routing_keyin your MetadataRequestsEstimation Method: Choose
with_limits()if you provide limits,with_balances()otherwiseSample Size: Number of entrypoints to generate per token pair (typically 4)
Step 5: Initialize Hooks DCI
Create and initialize the UniswapV4HookDCI instance.
Initialization Code:
use tycho_common::models::{Chain, Address};
use crate::extractor::dynamic_contract_indexer::{
dci::DynamicContractIndexer,
hook_dci::UniswapV4HookDCI,
};
pub async fn create_hooks_dci_indexer(
chain: Chain,
extractor_name: String,
rpc_url: String,
router_address: Address,
pool_manager: Address,
db_gateway: impl EntryPointGateway + ProtocolGateway + Send + Sync + 'static,
account_extractor: impl AccountExtractor + Send + Sync + 'static,
entrypoint_tracer: impl EntryPointTracer + Send + Sync + 'static,
) -> Result<UniswapV4HookDCI<...>, ExtractionError> {
// 1. Create inner DCI (standard indexer)
let inner_dci = DynamicContractIndexer::new(
chain.clone(),
extractor_name.clone(),
db_gateway.clone(),
account_extractor,
entrypoint_tracer,
);
// 2. Setup metadata and hook orchestrators (from Step 4)
let (metadata_orchestrator, hook_orchestrator_registry) =
setup_my_hook_indexing(rpc_url, router_address, pool_manager, chain.clone());
// 3. Create Hooks DCI
let mut hook_dci = UniswapV4HookDCI::new(
inner_dci,
metadata_orchestrator,
hook_orchestrator_registry,
db_gateway,
chain,
max_retries: 3, // Retry up to 3 times before giving up
pause_after_retries: 2 // Pause after 2 retries (before hitting max)
);
// 4. Initialize (loads existing components from database)
hook_dci.initialize().await?;
Ok(hook_dci)
}Configuration Parameters:
max_retries: Maximum total retry attempts before permanently failing a componentpause_after_retries: Number of retries before pausing (setting "paused" attribute)
Typical Values:
max_retries: 5,pause_after_retries: 3
Step 6: Testing Your Integration
Test your implementation at multiple levels.
Unit Tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_generator_creates_balance_request() {
let generator = MyHookGenerator::new("http://localhost:8545".to_string());
let component = create_test_component();
let block = create_test_block();
let requests = generator.generate_balance_only_requests(&component, &block)
.expect("Should generate requests");
assert_eq!(requests.len(), 1);
assert!(matches!(
requests[0].request_type,
MetadataRequestType::ComponentBalance { .. }
));
}
#[test]
fn test_parser_handles_balance_response() {
let parser = MyHookParser;
let component = create_test_component();
let request = create_test_balance_request();
let response = json!("0x000000000000000000000000000000000000000000000000000000000000271000000000000000000000000000000000000000000000000000000000000027100");
let result = parser.parse_response(&component, &request, &response)
.expect("Should parse response");
match result {
MetadataValue::Balances(balances) => {
assert_eq!(balances.len(), 2);
}
_ => panic!("Expected Balances variant"),
}
}
}Integration Tests with Real RPC:
#[tokio::test]
#[ignore] // Requires RPC access
async fn test_metadata_collection_integration() {
let rpc_url = std::env::var("RPC_URL")
.expect("RPC_URL environment variable must be set");
// Setup registries
let (metadata_orchestrator, _) = setup_my_hook_indexing(
rpc_url,
router_address,
pool_manager,
Chain::Ethereum,
);
// Create test component
let component = create_real_hook_component();
let block = Block::new(/* real block data */);
// Collect metadata
let metadata = metadata_orchestrator
.collect_metadata_for_block(
&[], // No balance-only components
&[(TxHash::default(), component.clone())], // Full processing
&block,
)
.await
.expect("Should collect metadata");
// Verify metadata
assert_eq!(metadata.len(), 1);
let (comp, meta) = &metadata[0];
assert!(meta.balances.is_some());
assert!(meta.limits.is_some());
}Final Step: Submitting a PR
After your integration is tested, please submit a PR on Github so we can add it to our codebase and start indexing the hook on our hosted service.
Last updated