feat(template): Add template for singleton protocols.

Separate templates and add one for protocols with only a single contract.
This commit is contained in:
kayibal
2025-01-30 15:54:25 -03:00
committed by Alan Höng
parent d0e73a20ea
commit 08d2c7a82e
26 changed files with 2141 additions and 43 deletions

View File

@@ -0,0 +1,27 @@
[package]
name = "ethereum-template-singleton"
version = "0.1.0"
edition = "2021"
[lib]
name = "ethereum_template_singleton"
crate-type = ["cdylib"]
[dependencies]
substreams = "0.5.22"
substreams-ethereum = "0.9.9"
prost = "0.11"
tycho-substreams = { git = "https://github.com/propeller-heads/tycho-protocol-sdk.git", rev = "3c08359" }
anyhow = "1.0.95"
ethabi = "18.0.0"
num-bigint = "0.4.6"
hex = { version = "0.4", features = ["serde"] }
itertools = "0.10.5"
serde = "1.0.217"
serde-sibor = "0.1.0"
serde_qs = "0.13.0"
[build-dependencies]
anyhow = "1"
substreams-ethereum = "0.9.9"

View File

@@ -0,0 +1,222 @@
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_spender",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_from",
"type": "address"
},
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "balance",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "_to",
"type": "address"
},
{
"name": "_value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": true,
"inputs": [
{
"name": "_owner",
"type": "address"
},
{
"name": "_spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function"
},
{
"payable": true,
"stateMutability": "payable",
"type": "fallback"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event"
}
]

View File

@@ -0,0 +1,12 @@
version: v1
plugins:
- plugin: buf.build/community/neoeinstein-prost:v0.2.2
out: src/pb
opt:
- file_descriptor_set=false
- plugin: buf.build/community/neoeinstein-prost-crate:v0.3.1
out: src/pb
opt:
- no_features

View File

@@ -0,0 +1,49 @@
use anyhow::Result;
use std::{fs, io::Write};
use substreams_ethereum::Abigen;
fn main() -> Result<()> {
let abi_folder = "abi";
let output_folder = "src/abi";
let abis = fs::read_dir(abi_folder)?;
let mut files = abis.collect::<Result<Vec<_>, _>>()?;
// Sort the files by their name
files.sort_by_key(|a| a.file_name());
let mut mod_rs_content = String::new();
mod_rs_content.push_str("#![allow(clippy::all)]\n");
for file in files {
let file_name = file.file_name();
let file_name = file_name.to_string_lossy();
if !file_name.ends_with(".json") {
continue;
}
let contract_name = file_name.split('.').next().unwrap();
let input_path = format!("{}/{}", abi_folder, file_name);
let output_path = format!("{}/{}.rs", output_folder, contract_name);
mod_rs_content.push_str(&format!("pub mod {};\n", contract_name));
if std::path::Path::new(&output_path).exists() {
continue;
}
Abigen::new(contract_name, &input_path)?
.generate()?
.write_to_file(&output_path)?;
}
let mod_rs_path = format!("{}/mod.rs", output_folder);
let mut mod_rs_file = fs::File::create(mod_rs_path)?;
mod_rs_file.write_all(mod_rs_content.as_bytes())?;
Ok(())
}

View File

@@ -0,0 +1,57 @@
# Name of the substreams config file in your substreams module. Usually "./substreams.yaml"
substreams_yaml_path: ./substreams.yaml
# Name of the adapter contract, usually: ProtocolSwapAdapter"
adapter_contract: "SwapAdapter"
# Constructor signature of the Adapter contract"
adapter_build_signature: "constructor(address)"
# A comma separated list of args to be passed to the contructor of the Adapter contract"
adapter_build_args: "0x0000000000000000000000000000000000000000"
# Whether or not the testing script should skip checking balances of the protocol components.
# If set to `true` please always add a reason why it's skipped.
skip_balance_check: false
# A list of accounts that need to be indexed to run the tests properly.
# Usually used when there is a global component required by all pools and created before the tested range of blocks. For example a factory or a vault.
# Please note that this component needs to be indexed by your substreams module, this feature is only for testing purpose.
# Also please always add a reason why this account is needed for your tests.
# This will be applied to each test.
initialized_accounts:
- "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" # Needed for ....
# A list of protocol types names created by your Substreams module.
protocol_type_names:
- "type_name_1"
- "type_name_2"
# A list of tests.
tests:
# Name of the test
- name: test_pool_creation
# Indexed block range
start_block: 123
stop_block: 456
# Same as global `initialized_accounts` but only scoped to this test.
initialized_accounts:
- "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963" # Needed for ....
# A list of expected component indexed in the block range. Each component must match perfectly the `ProtocolComponent` indexed by your subtreams module.
expected_components:
- id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
tokens:
- "0xdac17f958d2ee523a2206206994597c13d831ec7"
- "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"
- "0x6b175474e89094c44da98b954eedeac495271d0f"
static_attributes:
attr_1: "value"
attr_2: "value"
creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6"
# Whether or not the script should skip trying to simulate a swap on this component.
# If set to `true` please always add a reason why it's skipped.
skip_simulation: false
- name: test_something_else
start_block: 123
stop_block: 456
expected_components:
- id: "0xdc24316b9ae028f1497c275eb9192a3ea0f67022"
tokens:
- "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE"
- "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"
static_attributes: null
creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa"
skip_simulation: true # If true, always add a reason

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,2 @@
#![allow(clippy::all)]
pub mod erc20;

View File

@@ -0,0 +1,4 @@
mod abi;
mod pool_factories;
mod modules;

View File

@@ -0,0 +1,246 @@
//! Template for Protocols with singleton contract
//!
//!
use std::collections::HashMap;
use anyhow::Result;
use substreams::pb::substreams::StoreDeltas;
use substreams::prelude::*;
use substreams_ethereum::Event;
use substreams_ethereum::pb::eth;
use tycho_substreams::balances::aggregate_balances_changes;
use tycho_substreams::contract::extract_contract_changes_builder;
use tycho_substreams::prelude::*;
use itertools::Itertools;
use prost::Message;
use substreams_ethereum::block_view::CallView;
use crate::pool_factories;
use crate::pool_factories::DeploymentConfig;
/// 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(
params: String,
block: eth::v2::Block,
) -> Result<BlockTransactionProtocolComponents> {
let config = serde_qs::from_str(params.as_str())?;
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,
&config,
)
})
.collect::<Vec<_>>();
if !components.is_empty() {
Some(TransactionProtocolComponents { tx: Some(tx.into()), components })
} else {
None
}
})
.collect::<Vec<_>>(),
})
}
#[substreams::handlers::store]
fn store_protocol_tokens(map_protocol_components: BlockTransactionProtocolComponents, store: StoreSetInt64) {
map_protocol_components.tx_components
.into_iter()
.for_each(|tx_pc| {
tx_pc
.components
.into_iter()
.for_each(|pc| {
pc.tokens.iter().for_each(|token| {
let token_addr_hex = hex::encode(token);
store.set(0, &token_addr_hex, &1);
})
})
});
}
/// Extracts balance changes per component
///
/// This template function inspects ERC20 transfer events to/from the singleton contract
/// to extract balance changes. 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 or your component burns or
/// mints 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(params: String, block: eth::v2::Block, store: StoreGetInt64) -> Result<BlockBalanceDeltas> {
let config: DeploymentConfig = serde_qs::from_str(params.as_str())?;
let res = block
.transactions()
.flat_map(|tx| {
tx
.logs_with_calls()
.filter_map(|(log, call)| {
let token_addr_hex = hex::encode(&log.address);
if !store.has_last(&token_addr_hex) {
return None;
}
crate::abi::erc20::events::Transfer::match_and_decode(log).map(|transfer| {
let to_addr = transfer.to.as_slice();
let from_addr = transfer.from.as_slice();
if let Some(component_id) = extract_component_id_from_call(call) {
if to_addr == config.vault_address {
return Some(BalanceDelta {
ord: log.ordinal,
tx: Some(tx.into()),
token: log.address.to_vec(),
delta: transfer.value.to_signed_bytes_be(),
component_id: component_id.encode_to_vec(),
});
} else if from_addr == config.vault_address {
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: component_id.encode_to_vec(),
});
}
}
None
})
})
.flatten()
})
.collect::<Vec<_>>();
Ok(BlockBalanceDeltas { balance_deltas: res })
}
// TODO: given a relevant balance changing call associate it with the respective
// component
fn extract_component_id_from_call(_call: CallView) -> Option<String> {
todo!()
}
/// 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_component_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(
params: String,
block: eth::v2::Block,
new_components: BlockTransactionProtocolComponents,
balance_store: StoreDeltas,
deltas: BlockBalanceDeltas,
) -> Result<BlockChanges, substreams::errors::Error> {
let config: DeploymentConfig = serde_qs::from_str(params.as_str())?;
// 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));
let mut contract_changes = InterimContractChange::new(&config.vault_address, false);
balances
.values()
.for_each(|token_bc_map| {
token_bc_map
.values()
.for_each(|bc| {
// track component balance
builder.add_balance_change(bc);
// track vault contract balance
contract_changes.upsert_token_balance(bc.token.as_slice(), bc.balance.as_slice())
})
});
builder.add_contract_changes(&contract_changes);
});
// 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.
addr == config.vault_address
},
&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<_>>(),
})
}

View File

@@ -0,0 +1,61 @@
use serde::Deserialize;
use substreams_ethereum::pb::eth::v2::{Call, Log, TransactionTrace};
use tycho_substreams::models::{ChangeType, FinancialType, ImplementationType, ProtocolComponent, ProtocolType};
#[derive(Deserialize)]
pub struct DeploymentConfig {
#[serde(with = "hex::serde")]
pub vault_address: Vec<u8>,
}
/// Potentially constructs a new ProtocolComponent given a call
///
/// This method is given each individual call within a transaction, the corresponding
/// logs emitted during that call as well as the full transaction trace.
///
/// If this call creates a component in your protocol please contstruct and return it
/// here. Otherwise, simply return None.
pub fn maybe_create_component(
call: &Call,
_log: &Log,
_tx: &TransactionTrace,
config: &DeploymentConfig,
) -> Option<ProtocolComponent> {
if call.address == config.vault_address {
// TODO: replace with your logic
Some(ProtocolComponent {
id: "".to_string(),
tokens: vec![
// TODO: add the components tokens
],
contracts: vec![
// TODO: any contracts required during swapping
],
static_att: vec![
// TODO: any additional metadata required, e.g. for swap encoding
],
change: ChangeType::Creation.into(),
protocol_type: Some(ProtocolType {
name: "template".to_string(),
financial_type: FinancialType::Swap.into(),
attribute_schema: vec![],
implementation_type: ImplementationType::Vm.into(),
}),
})
} else {
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_decode_config() {
let config: DeploymentConfig = serde_qs::from_str("vault_address=0001").unwrap();
assert_eq!(config.vault_address, [0u8, 1u8]);
}
}

View File

@@ -0,0 +1,80 @@
specVersion: v0.1.0
package:
name: "ethereum_template"
version: v0.1.0
protobuf:
files:
- tycho/evm/v1/vm.proto
- tycho/evm/v1/common.proto
- tycho/evm/v1/utils.proto
importPaths:
- ../../proto
binaries:
default:
type: wasm/rust-v1
file: ../target/wasm32-unknown-unknown/release/ethereum_template_singleton.wasm
network: mainnet
networks:
mainnet:
initialBlock:
map_protocol_components: 1
store_protocol_components: 1
map_relative_component_balance: 1
store_balances: 1
map_protocol_changes: 1
params:
map_protocol_components: "vault_address=0000"
map_relative_component_balance: "vault_address=0000"
map_protocol_changes: "vault_address=0000"
params:
map_protocol_components: "vault_address=0000"
map_relative_component_balance: "vault_address=0000"
map_protocol_changes: "vault_address=0000"
modules:
- name: map_protocol_components
kind: map
inputs:
- params: string
- source: sf.ethereum.type.v2.Block
output:
type: proto:tycho.evm.v1.BlockTransactionProtocolComponents
- name: store_protocol_tokens
kind: store
updatePolicy: set
valueType: string
inputs:
- map: map_protocol_components
- name: map_relative_component_balance
kind: map
inputs:
- params: string
- source: sf.ethereum.type.v2.Block
- store: store_protocol_tokens
output:
type: proto:tycho.evm.v1.BlockBalanceDeltas
- name: store_component_balances
kind: store
updatePolicy: add
valueType: bigint
inputs:
- map: map_relative_component_balance
- name: map_protocol_changes
kind: map
inputs:
- params: string
- source: sf.ethereum.type.v2.Block
- map: map_protocol_components
- map: map_relative_component_balance
- store: store_component_balances
mode: deltas
output:
type: proto:tycho.evm.v1.BlockChanges