//! 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::{ abi::erc20, 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 { 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::>(); if !components.is_empty() { Some(TransactionProtocolComponents { tx: Some(tx.into()), components }) } else { None } }) .collect::>(), }) } /// 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 { let res = block .logs() .filter_map(|log| { 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> = 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> = 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::>(); 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 { // 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::>(), }) }