Files
tycho-protocol-sdk/substreams/ethereum-template-factory/src/modules.rs
2025-02-06 11:04:39 -06:00

269 lines
11 KiB
Rust

//! Template for Protocols with contract factories
//!
//! This template provides foundational maps and store substream modules for indexing a
//! protocol where each component (e.g., pool) is deployed to a separate contract. Each
//! contract is expected to escrow its ERC-20 token balances.
//!
//! If your protocol supports native ETH, you may need to adjust the balance tracking
//! logic in `map_relative_component_balance` to account for native token handling.
//!
//! ## Assumptions
//! - Assumes each pool has a single newly deployed contract linked to it
//! - Assumes pool identifier equals the deployed contract address
//! - Assumes any price or liquidity updated correlates with a pools contract storage update.
//!
//! ## Alternative Module
//! If your protocol uses a vault-like contract to manage balances, or if pools are
//! registered within a singleton contract, refer to the `ethereum-template-singleton`
//! substream for an appropriate alternative.
//!
//! ## Warning
//! This template provides a general framework for indexing a protocol. However, it is
//! likely that you will need to adapt the steps to suit your specific use case. Use the
//! provided code with care and ensure you fully understand each step before proceeding
//! with your implementation.
//!
//! ## Example Use Case
//! For an Uniswap-like protocol where each liquidity pool is deployed as a separate
//! contract, you can use this template to:
//! - Track relative component balances (e.g., ERC-20 token balances in each pool).
//! - Index individual pool contracts as they are created by the factory contract.
//!
//! Adjustments to the template may include:
//! - Handling native ETH balances alongside token balances.
//! - Customizing indexing logic for specific factory contract behavior.
use crate::pool_factories;
use anyhow::Result;
use itertools::Itertools;
use std::collections::HashMap;
use substreams::{pb::substreams::StoreDeltas, prelude::*};
use substreams_ethereum::{pb::eth, Event};
use tycho_substreams::{
balances::aggregate_balances_changes, contract::extract_contract_changes_builder, prelude::*,
};
/// Find and create all relevant protocol components
///
/// This method maps over blocks and instantiates ProtocolComponents with a unique ids
/// as well as all necessary metadata for routing and encoding.
#[substreams::handlers::map]
fn map_protocol_components(block: eth::v2::Block) -> Result<BlockTransactionProtocolComponents> {
Ok(BlockTransactionProtocolComponents {
tx_components: block
.transactions()
.filter_map(|tx| {
let components = tx
.logs_with_calls()
.filter_map(|(log, call)| {
// TODO: ensure this method is implemented correctly
pool_factories::maybe_create_component(call.call, log, tx)
})
.collect::<Vec<_>>();
if !components.is_empty() {
Some(TransactionProtocolComponents { tx: Some(tx.into()), components })
} else {
None
}
})
.collect::<Vec<_>>(),
})
}
/// Stores all protocol components in a store.
///
/// Stores information about components in a key value store. This is only necessary if
/// you need to access the whole set of components within your indexing logic.
///
/// Popular use cases are:
/// - Checking if a contract belongs to a component. In this case suggest to use an address as the
/// store key so lookup operations are O(1).
/// - Tallying up relative balances changes to calcualte absolute erc20 token balances per
/// component.
///
/// Usually you can skip this step if:
/// - You are interested in a static set of components only
/// - Your protocol emits balance change events with absolute values
#[substreams::handlers::store]
fn store_protocol_components(
map_protocol_components: BlockTransactionProtocolComponents,
store: StoreSetRaw,
) {
map_protocol_components
.tx_components
.into_iter()
.for_each(|tx_pc| {
tx_pc
.components
.into_iter()
.for_each(|pc| {
// Assumes that the component id is a hex encoded contract address
let key = pc.id.clone();
// we store the components tokens
// TODO: proper error handling
let val = serde_sibor::to_bytes(&pc.tokens).unwrap();
store.set(0, key, &val);
})
});
}
/// Extracts balance changes per component
///
/// This template function uses ERC20 transfer events to extract balance changes. It
/// assumes that each component is deployed at a dedicated contract address. If a
/// transfer to the component is detected, it's balanced is increased and if a balance
/// from the component is detected its balance is decreased.
///
/// ## Note:
/// Changes are necessary if your protocol uses native ETH, uses a vault contract or if
/// your component burn or mint tokens without emitting transfer events.
///
/// You may want to ignore LP tokens if your protocol emits transfer events for these
/// here.
#[substreams::handlers::map]
fn map_relative_component_balance(
block: eth::v2::Block,
store: StoreGetRaw,
) -> Result<BlockBalanceDeltas> {
let res = block
.logs()
.filter_map(|log| {
crate::abi::erc20::events::Transfer::match_and_decode(log).map(|transfer| {
let to_addr = hex::encode(transfer.to.as_slice());
let from_addr = hex::encode(transfer.from.as_slice());
let tx = log.receipt.transaction;
if let Some(val) = store.get_last(&to_addr) {
let component_tokens: Vec<Vec<u8>> = serde_sibor::from_bytes(&val).unwrap();
if component_tokens.contains(&log.address().to_vec()) {
return Some(BalanceDelta {
ord: log.ordinal(),
tx: Some(tx.into()),
token: log.address().to_vec(),
delta: transfer.value.to_signed_bytes_be(),
component_id: to_addr.into_bytes(),
});
}
} else if let Some(val) = store.get_last(&from_addr) {
let component_tokens: Vec<Vec<u8>> = serde_sibor::from_bytes(&val).unwrap();
if component_tokens.contains(&log.address().to_vec()) {
return Some(BalanceDelta {
ord: log.ordinal(),
tx: Some(tx.into()),
token: log.address().to_vec(),
delta: (transfer.value.neg()).to_signed_bytes_be(),
component_id: to_addr.into_bytes(),
});
}
}
None
})
})
.flatten()
.collect::<Vec<_>>();
Ok(BlockBalanceDeltas { balance_deltas: res })
}
/// Aggregates relative balances values into absolute values
///
/// Aggregate the relative balances in an additive store since tycho-indexer expects
/// absolute balance inputs.
///
/// ## Note:
/// This method should usually not require any changes.
#[substreams::handlers::store]
pub fn store_balances(deltas: BlockBalanceDeltas, store: StoreAddBigInt) {
tycho_substreams::balances::store_balance_changes(deltas, store);
}
/// Aggregates protocol components and balance changes by transaction.
///
/// This is the main method that will aggregate all changes as well as extract all
/// relevant contract storage deltas.
///
/// ## Note:
/// You may have to change this method if your components have any default dynamic
/// attributes, or if you need any additional static contracts indexed.
#[substreams::handlers::map]
fn map_protocol_changes(
block: eth::v2::Block,
new_components: BlockTransactionProtocolComponents,
components_store: StoreGetRaw,
balance_store: StoreDeltas,
deltas: BlockBalanceDeltas,
) -> Result<BlockChanges, substreams::errors::Error> {
// We merge contract changes by transaction (identified by transaction index)
// making it easy to sort them at the very end.
let mut transaction_changes: HashMap<_, TransactionChangesBuilder> = HashMap::new();
// Aggregate newly created components per tx
new_components
.tx_components
.iter()
.for_each(|tx_component| {
// initialise builder if not yet present for this tx
let tx = tx_component.tx.as_ref().unwrap();
let builder = transaction_changes
.entry(tx.index)
.or_insert_with(|| TransactionChangesBuilder::new(tx));
// iterate over individual components created within this tx
tx_component
.components
.iter()
.for_each(|component| {
builder.add_protocol_component(component);
// TODO: In case you require to add any dynamic attributes to the
// component you can do so here:
/*
builder.add_entity_change(&EntityChanges {
component_id: component.id.clone(),
attributes: default_attributes.clone(),
});
*/
});
});
// Aggregate absolute balances per transaction.
aggregate_balances_changes(balance_store, deltas)
.into_iter()
.for_each(|(_, (tx, balances))| {
let builder = transaction_changes
.entry(tx.index)
.or_insert_with(|| TransactionChangesBuilder::new(&tx));
balances
.values()
.for_each(|token_bc_map| {
token_bc_map
.values()
.for_each(|bc| builder.add_balance_change(bc))
});
});
// Extract and insert any storage changes that happened for any of the components.
extract_contract_changes_builder(
&block,
|addr| {
// we assume that the store holds contract addresses as keys and if it
// contains a value, that contract is of relevance.
// TODO: if you have any additional static contracts that need to be indexed,
// please add them here.
components_store
.get_last(hex::encode(addr))
.is_some()
},
&mut transaction_changes,
);
// Process all `transaction_changes` for final output in the `BlockChanges`,
// sorted by transaction index (the key).
Ok(BlockChanges {
block: Some((&block).into()),
changes: transaction_changes
.drain()
.sorted_unstable_by_key(|(index, _)| *index)
.filter_map(|(_, builder)| builder.build())
.collect::<Vec<_>>(),
})
}