Merge pull request #54 from propeller-heads/ah/balancer-misc-updates

feat(balancer): Add update markers & rate providers
This commit is contained in:
Alan Höng
2024-08-02 11:01:02 +02:00
committed by GitHub
45 changed files with 50290 additions and 57300 deletions

View File

@@ -169,12 +169,26 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
returns (uint256[] memory limits) returns (uint256[] memory limits)
{ {
limits = new uint256[](2); limits = new uint256[](2);
address pool;
(pool,) = vault.getPool(poolId);
uint256 bptIndex = maybeGetBptTokenIndex(pool);
uint256 circulatingSupply = getBptCirculatingSupply(pool);
(address[] memory tokens, uint256[] memory balances,) = (address[] memory tokens, uint256[] memory balances,) =
vault.getPoolTokens(poolId); vault.getPoolTokens(poolId);
for (uint256 i = 0; i < tokens.length; i++) { for (uint256 i = 0; i < tokens.length; i++) {
if (tokens[i] == sellToken) { if (tokens[i] == sellToken) {
limits[0] = balances[i] * RESERVE_LIMIT_FACTOR / 10; if (i == bptIndex) {
// Some pools pre-mint the bpt tokens and keep the balance
// on the
// pool we can't sell more than the circulating supply
// though,
// else we get an underflow error.
limits[0] = circulatingSupply;
} else {
limits[0] = balances[i] * RESERVE_LIMIT_FACTOR / 10;
}
} }
if (tokens[i] == buyToken) { if (tokens[i] == buyToken) {
limits[1] = balances[i] * RESERVE_LIMIT_FACTOR / 10; limits[1] = balances[i] * RESERVE_LIMIT_FACTOR / 10;
@@ -182,6 +196,35 @@ contract BalancerV2SwapAdapter is ISwapAdapter {
} }
} }
function maybeGetBptTokenIndex(address poolAddress)
internal
view
returns (uint256)
{
IPool pool = IPool(poolAddress);
try pool.getBptIndex() returns (uint256 index) {
return index;
} catch {
return type(uint256).max;
}
}
function getBptCirculatingSupply(address poolAddress)
internal
view
returns (uint256)
{
IPool pool = IPool(poolAddress);
try pool.getActualSupply() returns (uint256 supply) {
return supply;
} catch {}
try pool.getVirtualSupply() returns (uint256 supply) {
return supply;
} catch {}
return type(uint256).max;
}
function getCapabilities(bytes32, address, address) function getCapabilities(bytes32, address, address)
external external
pure pure
@@ -486,3 +529,11 @@ interface IVault {
GIVEN_OUT GIVEN_OUT
} }
} }
interface IPool {
function getBptIndex() external view returns (uint256);
function getActualSupply() external view returns (uint256);
function getVirtualSupply() external view returns (uint256);
}

14
substreams/Cargo.lock generated
View File

@@ -857,18 +857,18 @@ checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.197" version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.197" version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -877,9 +877,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.114" version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@@ -1104,6 +1104,8 @@ dependencies = [
"itertools 0.12.1", "itertools 0.12.1",
"num-bigint", "num-bigint",
"prost 0.11.9", "prost 0.11.9",
"serde",
"serde_json",
"substreams", "substreams",
"substreams-ethereum", "substreams-ethereum",
] ]

View File

@@ -12,6 +12,8 @@ hex-literal = "0.4.1"
hex = "0.4.3" hex = "0.4.3"
ethabi = "18.0.0" ethabi = "18.0.0"
tycho-substreams = { path = "crates/tycho-substreams" } tycho-substreams = { path = "crates/tycho-substreams" }
serde = "1.0.204"
serde_json = "1.0.120"
[profile.release] [profile.release]
lto = true lto = true

View File

@@ -11,3 +11,5 @@ hex.workspace = true
itertools = "0.12.0" itertools = "0.12.0"
ethabi.workspace = true ethabi.workspace = true
num-bigint = "0.4.4" num-bigint = "0.4.4"
serde.workspace = true
serde_json.workspace = true

View File

@@ -0,0 +1,46 @@
use std::fmt::Debug;
use substreams::prelude::BigInt;
/// Encodes a value to bytes using json.
///
/// ## Panics
/// In case the serialisation to json fails.
pub fn json_serialize_value<T: serde::Serialize + Debug>(v: T) -> Vec<u8> {
serde_json::to_value(v)
.unwrap_or_else(|e| panic!("Failed to encode value as json {e}"))
.to_string()
.as_bytes()
.to_vec()
}
/// Encodes a list of addresses (in byte representation) into json.
///
/// Converts each address to a 0x prefixed hex string and then serializes
/// the list of strings as a json.
///
/// ## Panics
/// In case the serialisation to json fails.
pub fn json_serialize_address_list(addresses: &[Vec<u8>]) -> Vec<u8> {
json_serialize_value(
addresses
.iter()
.map(|a| format!("0x{}", hex::encode(a)))
.collect::<Vec<_>>(),
)
}
/// Encodes a list of BigInt values into json.
///
/// Converts each integer to a 0x prefixed hex string and then serializes
/// the list of strings as a json.
///
/// ## Panics
/// In case the serialisation to json fails.
pub fn json_serialize_bigint_list(values: &[BigInt]) -> Vec<u8> {
json_serialize_value(
values
.iter()
.map(|v| format!("0x{}", hex::encode(v.to_signed_bytes_be())))
.collect::<Vec<_>>(),
)
}

View File

@@ -10,67 +10,14 @@
/// more [here](https://streamingfastio.medium.com/new-block-model-to-accelerate-chain-integration-9f65126e5425) /// more [here](https://streamingfastio.medium.com/new-block-model-to-accelerate-chain-integration-9f65126e5425)
use std::collections::HashMap; use std::collections::HashMap;
use substreams_ethereum::pb::eth::{ use crate::{
self, models::{InterimContractChange, TransactionChanges},
v2::{block::DetailLevel, CallType, StorageChange}, prelude::TransactionChangesBuilder,
};
use substreams_ethereum::pb::{
eth,
eth::v2::{block::DetailLevel, CallType, TransactionTrace},
}; };
use crate::pb::tycho::evm::v1::{self as tycho};
struct SlotValue {
new_value: Vec<u8>,
start_value: Vec<u8>,
}
impl From<&StorageChange> for SlotValue {
fn from(change: &StorageChange) -> Self {
Self { new_value: change.new_value.clone(), start_value: change.old_value.clone() }
}
}
impl SlotValue {
fn has_changed(&self) -> bool {
self.start_value != self.new_value
}
}
// Uses a map for slots, protobuf does not allow bytes in hashmap keys
struct InterimContractChange {
address: Vec<u8>,
balance: Vec<u8>,
code: Vec<u8>,
slots: HashMap<Vec<u8>, SlotValue>,
change: tycho::ChangeType,
}
impl InterimContractChange {
fn new(address: &[u8], creation: bool) -> Self {
Self {
address: address.to_vec(),
balance: vec![],
code: vec![],
slots: Default::default(),
change: if creation { tycho::ChangeType::Creation } else { tycho::ChangeType::Update },
}
}
}
impl From<InterimContractChange> for tycho::ContractChange {
fn from(value: InterimContractChange) -> Self {
tycho::ContractChange {
address: value.address,
balance: value.balance,
code: value.code,
slots: value
.slots
.into_iter()
.filter(|(_, value)| value.has_changed())
.map(|(slot, value)| tycho::ContractSlot { slot, value: value.new_value })
.collect(),
change: value.change.into(),
}
}
}
/// Extracts and aggregates contract changes from a block. /// Extracts and aggregates contract changes from a block.
/// ///
@@ -101,7 +48,45 @@ impl From<InterimContractChange> for tycho::ContractChange {
pub fn extract_contract_changes<F: Fn(&[u8]) -> bool>( pub fn extract_contract_changes<F: Fn(&[u8]) -> bool>(
block: &eth::v2::Block, block: &eth::v2::Block,
inclusion_predicate: F, inclusion_predicate: F,
transaction_changes: &mut HashMap<u64, tycho::TransactionChanges>, transaction_changes: &mut HashMap<u64, TransactionChanges>,
) {
extract_contract_changes_generic(block, inclusion_predicate, |tx, changed_contracts| {
transaction_changes
.entry(tx.index.into())
.or_insert_with(|| TransactionChanges::new(&(tx.into())))
.contract_changes
.extend(
changed_contracts
.clone()
.into_values()
.map(|change| change.into()),
);
})
}
pub fn extract_contract_changes_builder<F: Fn(&[u8]) -> bool>(
block: &eth::v2::Block,
inclusion_predicate: F,
transaction_changes: &mut HashMap<u64, TransactionChangesBuilder>,
) {
extract_contract_changes_generic(block, inclusion_predicate, |tx, changed_contracts| {
let builder = transaction_changes
.entry(tx.index.into())
.or_insert_with(|| TransactionChangesBuilder::new(&(tx.into())));
changed_contracts
.clone()
.into_iter()
.for_each(|(_, change)| builder.add_contract_changes(&change));
})
}
fn extract_contract_changes_generic<
F: Fn(&[u8]) -> bool,
G: FnMut(&TransactionTrace, &HashMap<Vec<u8>, InterimContractChange>),
>(
block: &eth::v2::Block,
inclusion_predicate: F,
mut store_changes: G,
) { ) {
if block.detail_level != Into::<i32>::into(DetailLevel::DetaillevelExtended) { if block.detail_level != Into::<i32>::into(DetailLevel::DetaillevelExtended) {
panic!("Only extended blocks are supported"); panic!("Only extended blocks are supported");
@@ -160,14 +145,7 @@ pub fn extract_contract_changes<F: Fn(&[u8]) -> bool>(
) )
}); });
let slot_value = contract_change contract_change.upsert_slot(storage_change);
.slots
.entry(storage_change.key.clone())
.or_insert_with(|| storage_change.into());
slot_value
.new_value
.copy_from_slice(&storage_change.new_value);
}); });
balance_changes balance_changes
@@ -184,10 +162,7 @@ pub fn extract_contract_changes<F: Fn(&[u8]) -> bool>(
}); });
if let Some(new_balance) = &balance_change.new_value { if let Some(new_balance) = &balance_change.new_value {
contract_change.balance.clear(); contract_change.set_balance(&new_balance.bytes);
contract_change
.balance
.extend_from_slice(&new_balance.bytes);
} }
}); });
@@ -204,25 +179,15 @@ pub fn extract_contract_changes<F: Fn(&[u8]) -> bool>(
) )
}); });
contract_change.code.clear(); contract_change.set_code(&code_change.new_code);
contract_change
.code
.extend_from_slice(&code_change.new_code);
}); });
if !storage_changes.is_empty() || if !storage_changes.is_empty() ||
!balance_changes.is_empty() || !balance_changes.is_empty() ||
!code_changes.is_empty() !code_changes.is_empty()
{ {
transaction_changes store_changes(block_tx, &changed_contracts)
.entry(block_tx.index.into())
.or_insert_with(|| tycho::TransactionChanges::new(&(block_tx.into())))
.contract_changes
.extend(
changed_contracts
.drain()
.map(|(_, change)| change.into()),
);
} }
changed_contracts.clear()
}); });
} }

View File

@@ -1,4 +1,5 @@
mod abi; mod abi;
pub mod attributes;
pub mod balances; pub mod balances;
pub mod contract; pub mod contract;
mod mock_store; mod mock_store;

View File

@@ -1,4 +1,5 @@
use substreams_ethereum::pb::eth::v2::{self as sf}; use std::collections::HashMap;
use substreams_ethereum::pb::eth::v2::{self as sf, StorageChange};
// re-export the protobuf types here. // re-export the protobuf types here.
pub use crate::pb::tycho::evm::v1::*; pub use crate::pb::tycho::evm::v1::*;
@@ -17,6 +18,161 @@ impl TransactionChanges {
} }
} }
/// Builds `TransactionChanges` struct
///
/// Ensures uniqueness for contract addresses and component ids.
#[derive(Default)]
pub struct TransactionChangesBuilder {
tx: Option<Transaction>,
contract_changes: HashMap<Vec<u8>, InterimContractChange>,
entity_changes: HashMap<String, InterimEntityChanges>,
component_changes: HashMap<String, ProtocolComponent>,
balance_changes: HashMap<(Vec<u8>, Vec<u8>), BalanceChange>,
}
impl TransactionChangesBuilder {
/// Initialize a new builder for a transaction.
pub fn new(tx: &Transaction) -> Self {
Self { tx: Some(tx.clone()), ..Default::default() }
}
/// Register a new contract change.
///
/// Will prioritize the new change over any already present one.
pub fn add_contract_changes(&mut self, change: &InterimContractChange) {
self.contract_changes
.entry(change.address.clone())
.and_modify(|c| {
if !change.balance.is_empty() {
c.set_balance(&change.balance)
}
if !change.slots.is_empty() {
c.upsert_slots(&change.slots)
}
if !change.code.is_empty() {
c.set_code(&change.code)
}
})
.or_insert_with(|| {
let mut c = InterimContractChange::new(
&change.address,
change.change == ChangeType::Creation,
);
c.upsert_slots(&change.slots);
c.set_code(&change.code);
c.set_balance(&change.balance);
c
});
}
/// Unique contract/account addresses that have been changed so far.
pub fn changed_contracts(&self) -> impl Iterator<Item = &[u8]> {
self.contract_changes
.keys()
.map(|k| k.as_slice())
}
/// Marks a component as updated.
///
/// If the protocol does not follow a 1:1 logic between components and contracts.
/// Components can be manually marked as updated using this method.
pub fn mark_component_as_updated(&mut self, component_id: &str) {
let attr = Attribute {
name: "update_marker".to_string(),
value: vec![1u8],
change: ChangeType::Update.into(),
};
if let Some(entry) = self
.entity_changes
.get_mut(component_id)
{
entry.set_attribute(&attr);
} else {
let mut change = InterimEntityChanges::new(component_id);
change.set_attribute(&attr);
self.entity_changes
.insert(component_id.to_string(), change);
}
}
/// Registers a new entity change.
///
/// Will prioritize the new change over any already present one.
pub fn add_entity_change(&mut self, change: &EntityChanges) {
self.entity_changes
.entry(change.component_id.clone())
.and_modify(|ec| {
for attr in change.attributes.iter() {
ec.set_attribute(attr);
}
})
.or_insert_with(|| InterimEntityChanges {
component_id: change.component_id.clone(),
attributes: change
.attributes
.clone()
.into_iter()
.map(|a| (a.name.clone(), a))
.collect(),
});
}
/// Adds a new protocol component.
///
/// ## Note
/// This method is a noop, in case the component is already present. Since
/// components are assumed to be immutable.
pub fn add_protocol_component(&mut self, component: &ProtocolComponent) {
if !self
.component_changes
.contains_key(&component.id)
{
self.component_changes
.insert(component.id.clone(), component.clone());
}
}
/// Updates a components balances
///
/// Overwrites any previous balance changes of the component if present.
pub fn add_balance_change(&mut self, change: &BalanceChange) {
self.balance_changes
.insert((change.component_id.clone(), change.token.clone()), change.clone());
}
pub fn build(self) -> Option<TransactionChanges> {
if self.contract_changes.is_empty() &&
self.component_changes.is_empty() &&
self.balance_changes.is_empty() &&
self.entity_changes.is_empty()
{
None
} else {
Some(TransactionChanges {
tx: self.tx,
contract_changes: self
.contract_changes
.into_values()
.map(|interim| interim.into())
.collect::<Vec<_>>(),
entity_changes: self
.entity_changes
.into_values()
.map(|interim| interim.into())
.collect::<Vec<_>>(),
component_changes: self
.component_changes
.into_values()
.collect::<Vec<_>>(),
balance_changes: self
.balance_changes
.into_values()
.collect::<Vec<_>>(),
})
}
}
}
impl From<&sf::TransactionTrace> for Transaction { impl From<&sf::TransactionTrace> for Transaction {
fn from(tx: &sf::TransactionTrace) -> Self { fn from(tx: &sf::TransactionTrace) -> Self {
Self { Self {
@@ -145,3 +301,128 @@ impl ProtocolComponent {
self self
} }
} }
/// Same as `EntityChanges` but ensures attributes are unique by name.
#[derive(Default)]
pub struct InterimEntityChanges {
component_id: String,
attributes: HashMap<String, Attribute>,
}
impl InterimEntityChanges {
pub fn new(id: &str) -> Self {
Self { component_id: id.to_string(), ..Default::default() }
}
pub fn set_attribute(&mut self, attr: &Attribute) {
self.attributes
.entry(attr.name.clone())
.and_modify(|existing| *existing = attr.clone())
.or_insert(attr.clone());
}
}
impl From<InterimEntityChanges> for EntityChanges {
fn from(value: InterimEntityChanges) -> Self {
EntityChanges {
component_id: value.component_id.clone(),
attributes: value
.attributes
.into_values()
.collect::<Vec<_>>(),
}
}
}
#[derive(Clone)]
struct SlotValue {
new_value: Vec<u8>,
start_value: Vec<u8>,
}
impl SlotValue {
fn has_changed(&self) -> bool {
self.start_value != self.new_value
}
}
impl From<&StorageChange> for SlotValue {
fn from(change: &StorageChange) -> Self {
Self { new_value: change.new_value.clone(), start_value: change.old_value.clone() }
}
}
// Uses a map for slots, protobuf does not allow bytes in hashmap keys
#[derive(Clone)]
pub struct InterimContractChange {
address: Vec<u8>,
balance: Vec<u8>,
code: Vec<u8>,
slots: HashMap<Vec<u8>, SlotValue>,
change: ChangeType,
}
impl InterimContractChange {
pub fn new(address: &[u8], creation: bool) -> Self {
Self {
address: address.to_vec(),
balance: vec![],
code: vec![],
slots: Default::default(),
change: if creation { ChangeType::Creation } else { ChangeType::Update },
}
}
pub fn upsert_slot(&mut self, change: &StorageChange) {
if change.address != self.address {
panic!("Bad storage change");
}
self.slots
.entry(change.key.clone())
.and_modify(|sv| {
sv.new_value
.copy_from_slice(&change.new_value)
})
.or_insert_with(|| change.into());
}
fn upsert_slots(&mut self, changes: &HashMap<Vec<u8>, SlotValue>) {
for (slot, value) in changes.iter() {
self.slots
.entry(slot.clone())
.and_modify(|sv| {
sv.new_value
.copy_from_slice(&value.new_value)
})
.or_insert(value.clone());
}
}
pub fn set_balance(&mut self, new_balance: &[u8]) {
self.balance.clear();
self.balance
.extend_from_slice(new_balance);
}
pub fn set_code(&mut self, code: &[u8]) {
self.code.clear();
self.code.extend_from_slice(code);
}
}
impl From<InterimContractChange> for ContractChange {
fn from(value: InterimContractChange) -> Self {
ContractChange {
address: value.address,
balance: value.balance,
code: value.code,
slots: value
.slots
.into_iter()
.filter(|(_, value)| value.has_changed())
.map(|(slot, value)| ContractSlot { slot, value: value.new_value })
.collect(),
change: value.change.into(),
}
}
}

View File

@@ -0,0 +1,25 @@
# Balancer Substream
## Open tasks
### Missing rate provider state
Any pool that does use rate providers, is currently not supported by tycho since we do
not witness the contract creation of rate providers and thus can't provide the required
contract state.
This is planned to be resolved with the dynamic contract indexing module.
## Static Attributes
| name | type | description |
|--------------------|-------|---------------------------------------------------------------------------------------------------------|
| pool_type | str | A unique identifier per pool type. Set depending on the factory |
| normalized weights | json | The normalised weights of a weighted pool. |
| pool_id | bytes | The balancer pool id. |
| rate_providers | json | A list of rate provider addresses. |
| bpt | bytes | The balancer lp token, set if the pool support entering and exiting lp postions via the swap interface. |
| main_token | bytes | The main token address for a linear pool |
| wrapped_token | bytes | The wrapped token address for a linear pool |
| fee | int | The fee charged by the pool set at deployment time |
| upper_target | int | The upper target for a linear pool |

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -9,7 +9,7 @@ use substreams::{
}; };
use substreams_ethereum::{pb::eth, Event}; use substreams_ethereum::{pb::eth, Event};
use tycho_substreams::{ use tycho_substreams::{
balances::aggregate_balances_changes, contract::extract_contract_changes, prelude::*, balances::aggregate_balances_changes, contract::extract_contract_changes_builder, prelude::*,
}; };
pub const VAULT_ADDRESS: &[u8] = &hex!("BA12222222228d8Ba445958a75a0704d566BF2C8"); pub const VAULT_ADDRESS: &[u8] = &hex!("BA12222222228d8Ba445958a75a0704d566BF2C8");
@@ -146,39 +146,43 @@ pub fn map_protocol_changes(
) -> Result<BlockChanges> { ) -> Result<BlockChanges> {
// We merge contract changes by transaction (identified by transaction index) making it easy to // We merge contract changes by transaction (identified by transaction index) making it easy to
// sort them at the very end. // sort them at the very end.
let mut transaction_changes: HashMap<_, TransactionChanges> = HashMap::new(); let mut transaction_changes: HashMap<_, TransactionChangesBuilder> = HashMap::new();
// `ProtocolComponents` are gathered from `map_pools_created` which just need a bit of work to // `ProtocolComponents` are gathered from `map_pools_created` which just need a bit of work to
// convert into `TransactionChanges` // convert into `TransactionChanges`
let default_attributes = vec![
Attribute {
name: "balance_owner".to_string(),
value: VAULT_ADDRESS.to_vec(),
change: ChangeType::Creation.into(),
},
Attribute {
name: "update_marker".to_string(),
value: vec![1u8],
change: ChangeType::Creation.into(),
},
];
grouped_components grouped_components
.tx_components .tx_components
.iter() .iter()
.for_each(|tx_component| { .for_each(|tx_component| {
// initialise builder if not yet present for this tx
let tx = tx_component.tx.as_ref().unwrap(); let tx = tx_component.tx.as_ref().unwrap();
transaction_changes let builder = transaction_changes
.entry(tx.index) .entry(tx.index)
.or_insert_with(|| TransactionChanges::new(tx)) .or_insert_with(|| TransactionChangesBuilder::new(tx));
.component_changes
.extend_from_slice(&tx_component.components); // iterate over individual components created within this tx
tx_component tx_component
.components .components
.iter() .iter()
.for_each(|component| { .for_each(|component| {
transaction_changes builder.add_protocol_component(component);
.entry(tx.index) let entity_change = EntityChanges {
.or_insert_with(|| TransactionChanges::new(tx)) component_id: component.id.clone(),
.entity_changes attributes: default_attributes.clone(),
.push(EntityChanges { };
component_id: component.id.clone(), builder.add_entity_change(&entity_change)
attributes: vec![Attribute {
name: "balance_owner".to_string(),
value: "0xBA12222222228d8Ba445958a75a0704d566BF2C8"
.to_string()
.as_bytes()
.to_vec(),
change: ChangeType::Creation.into(),
}],
});
}); });
}); });
@@ -189,15 +193,16 @@ pub fn map_protocol_changes(
aggregate_balances_changes(balance_store, deltas) aggregate_balances_changes(balance_store, deltas)
.into_iter() .into_iter()
.for_each(|(_, (tx, balances))| { .for_each(|(_, (tx, balances))| {
transaction_changes let builder = transaction_changes
.entry(tx.index) .entry(tx.index)
.or_insert_with(|| TransactionChanges::new(&tx)) .or_insert_with(|| TransactionChangesBuilder::new(&tx));
.balance_changes balances
.extend(balances.into_values()); .values()
.for_each(|bc| builder.add_balance_change(bc));
}); });
// Extract and insert any storage changes that happened for any of the components. // Extract and insert any storage changes that happened for any of the components.
extract_contract_changes( extract_contract_changes_builder(
&block, &block,
|addr| { |addr| {
components_store components_store
@@ -208,6 +213,24 @@ pub fn map_protocol_changes(
&mut transaction_changes, &mut transaction_changes,
); );
transaction_changes
.iter_mut()
.for_each(|(_, change)| {
// this indirection is necessary due to borrowing rules.
let addresses = change
.changed_contracts()
.map(|e| e.to_vec())
.collect::<Vec<_>>();
addresses
.into_iter()
.for_each(|address| {
if address != VAULT_ADDRESS {
// We reconstruct the component_id from the address here
change.mark_component_as_updated(&format!("0x{}", hex::encode(address)))
}
})
});
// Process all `transaction_changes` for final output in the `BlockChanges`, // Process all `transaction_changes` for final output in the `BlockChanges`,
// sorted by transaction index (the key). // sorted by transaction index (the key).
Ok(BlockChanges { Ok(BlockChanges {
@@ -215,17 +238,7 @@ pub fn map_protocol_changes(
changes: transaction_changes changes: transaction_changes
.drain() .drain()
.sorted_unstable_by_key(|(index, _)| *index) .sorted_unstable_by_key(|(index, _)| *index)
.filter_map(|(_, change)| { .filter_map(|(_, builder)| builder.build())
if change.contract_changes.is_empty() &&
change.component_changes.is_empty() &&
change.balance_changes.is_empty() &&
change.entity_changes.is_empty()
{
None
} else {
Some(change)
}
})
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
}) })
} }

View File

@@ -1,33 +1,13 @@
use crate::{abi, modules::VAULT_ADDRESS}; use crate::{abi, modules::VAULT_ADDRESS};
use substreams::{hex, scalar::BigInt}; use substreams::hex;
use substreams_ethereum::{ use substreams_ethereum::{
pb::eth::v2::{Call, Log, TransactionTrace}, pb::eth::v2::{Call, Log, TransactionTrace},
Event, Function, Event, Function,
}; };
use tycho_substreams::prelude::*; use tycho_substreams::{
attributes::{json_serialize_address_list, json_serialize_bigint_list},
/// This trait defines some helpers for serializing and deserializing `Vec<BigInt` which is needed prelude::*,
/// to be able to encode the `normalized_weights` and `weights` `Attribute`s. This should also be };
/// handled by any downstream application.
trait SerializableVecBigInt {
fn serialize_bytes(&self) -> Vec<u8>;
#[allow(dead_code)]
fn deserialize_bytes(bytes: &[u8]) -> Vec<BigInt>;
}
impl SerializableVecBigInt for Vec<BigInt> {
fn serialize_bytes(&self) -> Vec<u8> {
self.iter()
.flat_map(|big_int| big_int.to_signed_bytes_be())
.collect()
}
fn deserialize_bytes(bytes: &[u8]) -> Vec<BigInt> {
bytes
.chunks_exact(32)
.map(BigInt::from_signed_bytes_be)
.collect::<Vec<BigInt>>()
}
}
/// Helper function to get pool_registered event /// Helper function to get pool_registered event
fn get_pool_registered( fn get_pool_registered(
@@ -42,6 +22,18 @@ fn get_pool_registered(
.clone() .clone()
} }
fn get_token_registered(
tx: &TransactionTrace,
pool_id: &[u8],
) -> abi::vault::events::TokensRegistered {
tx.logs_with_calls()
.filter(|(log, _)| log.address == VAULT_ADDRESS)
.filter_map(|(log, _)| abi::vault::events::TokensRegistered::match_and_decode(log))
.find(|ev| ev.pool_id == pool_id)
.unwrap()
.clone()
}
/// This is the main function that handles the creation of `ProtocolComponent`s with `Attribute`s /// This is the main function that handles the creation of `ProtocolComponent`s with `Attribute`s
/// based on the specific factory address. There's 3 factory groups that are represented here: /// based on the specific factory address. There's 3 factory groups that are represented here:
/// - Weighted Pool Factories /// - Weighted Pool Factories
@@ -76,14 +68,20 @@ pub fn address_map(
("pool_type", "WeightedPoolFactory".as_bytes()), ("pool_type", "WeightedPoolFactory".as_bytes()),
( (
"normalized_weights", "normalized_weights",
&create_call &json_serialize_bigint_list(&create_call.normalized_weights),
.normalized_weights ),
.serialize_bytes(), ("pool_id", &pool_registered.pool_id),
(
"rate_providers",
&json_serialize_address_list(&create_call.rate_providers),
), ),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
("manual_updates", &[1u8]),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
) )
@@ -94,17 +92,27 @@ pub fn address_map(
let pool_created = let pool_created =
abi::composable_stable_pool_factory::events::PoolCreated::match_and_decode(log)?; abi::composable_stable_pool_factory::events::PoolCreated::match_and_decode(log)?;
let pool_registered = get_pool_registered(tx, &pool_created.pool); let pool_registered = get_pool_registered(tx, &pool_created.pool);
let tokens_registered = get_token_registered(tx, &pool_registered.pool_id);
Some( Some(
ProtocolComponent::at_contract(&pool_created.pool, &(tx.into())) ProtocolComponent::at_contract(&pool_created.pool, &(tx.into()))
.with_contracts(&[pool_created.pool, VAULT_ADDRESS.to_vec()]) .with_contracts(&[pool_created.pool.clone(), VAULT_ADDRESS.to_vec()])
.with_tokens(&create_call.tokens) .with_tokens(&tokens_registered.tokens)
.with_attributes(&[ .with_attributes(&[
("pool_type", "ComposableStablePoolFactory".as_bytes()), ("pool_type", "ComposableStablePoolFactory".as_bytes()),
("pool_id", &pool_registered.pool_id),
("bpt", &pool_created.pool),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
(
"rate_providers",
&json_serialize_address_list(&create_call.rate_providers),
),
("manual_updates", &[1u8]),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
) )
@@ -115,11 +123,12 @@ pub fn address_map(
let pool_created = let pool_created =
abi::erc_linear_pool_factory::events::PoolCreated::match_and_decode(log)?; abi::erc_linear_pool_factory::events::PoolCreated::match_and_decode(log)?;
let pool_registered = get_pool_registered(tx, &pool_created.pool); let pool_registered = get_pool_registered(tx, &pool_created.pool);
let tokens_registered = get_token_registered(tx, &pool_registered.pool_id);
Some( Some(
ProtocolComponent::at_contract(&pool_created.pool, &(tx.into())) ProtocolComponent::at_contract(&pool_created.pool, &(tx.into()))
.with_contracts(&[pool_created.pool, VAULT_ADDRESS.to_vec()]) .with_contracts(&[pool_created.pool.clone(), VAULT_ADDRESS.to_vec()])
.with_tokens(&[create_call.main_token, create_call.wrapped_token]) .with_tokens(&tokens_registered.tokens)
.with_attributes(&[ .with_attributes(&[
("pool_type", "ERC4626LinearPoolFactory".as_bytes()), ("pool_type", "ERC4626LinearPoolFactory".as_bytes()),
( (
@@ -128,9 +137,16 @@ pub fn address_map(
.upper_target .upper_target
.to_signed_bytes_be(), .to_signed_bytes_be(),
), ),
("pool_id", &pool_registered.pool_id),
("manual_updates", &[1u8]),
("bpt", &pool_created.pool),
("main_token", &create_call.main_token),
("wrapped_token", &create_call.wrapped_token),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
@@ -142,11 +158,12 @@ pub fn address_map(
let pool_created = let pool_created =
abi::euler_linear_pool_factory::events::PoolCreated::match_and_decode(log)?; abi::euler_linear_pool_factory::events::PoolCreated::match_and_decode(log)?;
let pool_registered = get_pool_registered(tx, &pool_created.pool); let pool_registered = get_pool_registered(tx, &pool_created.pool);
let tokens_registered = get_token_registered(tx, &pool_registered.pool_id);
Some( Some(
ProtocolComponent::at_contract(&pool_created.pool, &(tx.into())) ProtocolComponent::at_contract(&pool_created.pool, &(tx.into()))
.with_contracts(&[pool_created.pool, VAULT_ADDRESS.to_vec()]) .with_contracts(&[pool_created.pool.clone(), VAULT_ADDRESS.to_vec()])
.with_tokens(&[create_call.main_token, create_call.wrapped_token]) .with_tokens(&tokens_registered.tokens)
.with_attributes(&[ .with_attributes(&[
("pool_type", "EulerLinearPoolFactory".as_bytes()), ("pool_type", "EulerLinearPoolFactory".as_bytes()),
( (
@@ -155,9 +172,16 @@ pub fn address_map(
.upper_target .upper_target
.to_signed_bytes_be(), .to_signed_bytes_be(),
), ),
("pool_id", &pool_registered.pool_id),
("manual_updates", &[1u8]),
("bpt", &pool_created.pool),
("main_token", &create_call.main_token),
("wrapped_token", &create_call.wrapped_token),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
@@ -217,11 +241,12 @@ pub fn address_map(
let pool_created = let pool_created =
abi::silo_linear_pool_factory::events::PoolCreated::match_and_decode(log)?; abi::silo_linear_pool_factory::events::PoolCreated::match_and_decode(log)?;
let pool_registered = get_pool_registered(tx, &pool_created.pool); let pool_registered = get_pool_registered(tx, &pool_created.pool);
let tokens_registered = get_token_registered(tx, &pool_registered.pool_id);
Some( Some(
ProtocolComponent::at_contract(&pool_created.pool, &(tx.into())) ProtocolComponent::at_contract(&pool_created.pool, &(tx.into()))
.with_contracts(&[pool_created.pool, VAULT_ADDRESS.to_vec()]) .with_contracts(&[pool_created.pool.clone(), VAULT_ADDRESS.to_vec()])
.with_tokens(&[create_call.main_token, create_call.wrapped_token]) .with_tokens(&tokens_registered.tokens)
.with_attributes(&[ .with_attributes(&[
("pool_type", "SiloLinearPoolFactory".as_bytes()), ("pool_type", "SiloLinearPoolFactory".as_bytes()),
( (
@@ -230,9 +255,16 @@ pub fn address_map(
.upper_target .upper_target
.to_signed_bytes_be(), .to_signed_bytes_be(),
), ),
("pool_id", &pool_registered.pool_id),
("manual_updates", &[1u8]),
("bpt", &pool_created.pool),
("main_token", &create_call.main_token),
("wrapped_token", &create_call.wrapped_token),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
@@ -244,11 +276,12 @@ pub fn address_map(
let pool_created = let pool_created =
abi::yearn_linear_pool_factory::events::PoolCreated::match_and_decode(log)?; abi::yearn_linear_pool_factory::events::PoolCreated::match_and_decode(log)?;
let pool_registered = get_pool_registered(tx, &pool_created.pool); let pool_registered = get_pool_registered(tx, &pool_created.pool);
let tokens_registered = get_token_registered(tx, &pool_registered.pool_id);
Some( Some(
ProtocolComponent::at_contract(&pool_created.pool, &(tx.into())) ProtocolComponent::at_contract(&pool_created.pool, &(tx.into()))
.with_contracts(&[pool_created.pool, VAULT_ADDRESS.to_vec()]) .with_contracts(&[pool_created.pool.clone(), VAULT_ADDRESS.to_vec()])
.with_tokens(&[create_call.main_token, create_call.wrapped_token]) .with_tokens(&tokens_registered.tokens)
.with_attributes(&[ .with_attributes(&[
("pool_type", "YearnLinearPoolFactory".as_bytes()), ("pool_type", "YearnLinearPoolFactory".as_bytes()),
( (
@@ -257,9 +290,16 @@ pub fn address_map(
.upper_target .upper_target
.to_signed_bytes_be(), .to_signed_bytes_be(),
), ),
("pool_id", &pool_registered.pool_id),
("manual_updates", &[1u8]),
("bpt", &pool_created.pool),
("main_token", &create_call.main_token),
("wrapped_token", &create_call.wrapped_token),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
@@ -280,11 +320,15 @@ pub fn address_map(
.with_tokens(&create_call.tokens) .with_tokens(&create_call.tokens)
.with_attributes(&[ .with_attributes(&[
("pool_type", "WeightedPool2TokensFactory".as_bytes()), ("pool_type", "WeightedPool2TokensFactory".as_bytes()),
("weights", &create_call.weights.serialize_bytes()), ("weights", &json_serialize_bigint_list(&create_call.weights)),
("pool_id", &pool_registered.pool_id),
( (
"pool_id", "fee",
format!("0x{}", hex::encode(pool_registered.pool_id)).as_bytes(), &create_call
.swap_fee_percentage
.to_signed_bytes_be(),
), ),
("manual_updates", &[1u8]),
]) ])
.as_swap_type("balancer_pool", ImplementationType::Vm), .as_swap_type("balancer_pool", ImplementationType::Vm),
) )

View File

@@ -1,121 +1,125 @@
substreams_yaml_path: ./substreams.yaml substreams_yaml_path: ./substreams.yaml
protocol_type_names: protocol_type_names:
- "balancer_pool" - "balancer_pool"
adapter_contract: "BalancerV2SwapAdapter.evm.runtime" adapter_contract: "BalancerV2SwapAdapter"
skip_balance_check: true skip_balance_check: true
initialized_accounts:
- "0xba12222222228d8ba445958a75a0704d566bf2c8"
# Uncomment entries below to include composable stable pool dependencies
# wstETH dependencies
# - "0x72D07D7DcA67b8A406aD1Ec34ce969c90bFEE768"
# - "0xb8ffc3cd6e7cf5a098a1c92f48009765b24088dc"
# - "0xae7ab96520de3a18e5e111b5eaab095312d7fe84"
# - "0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0"
# - "0x2b33cf282f867a7ff693a66e11b0fcc5552e4425"
# - "0x17144556fd3424edc8fc8a4c940b2d04936d17eb"
# sfrxETH dependencies
# - "0x302013E7936a39c358d07A3Df55dc94EC417E3a1"
# - "0xac3e018457b222d93114458476f3e3416abbe38f"
# rETH dependencies
# - "0x1a8F81c256aee9C640e14bB0453ce247ea0DFE6F"
# - "0x07fcabcbe4ff0d80c2b1eb42855c0131b6cba2f4"
# - "0x1d8f8f00cfa6758d7be78336684788fb0ee0fa46"
# - "0xae78736cd615f374d3085123a210448e74fc6393"
tests: tests:
# WeightedPoolFactory - 0x897888115Ada5773E02aA29F775430BFB5F34c51 # WeightedPoolFactory - 0x897888115Ada5773E02aA29F775430BFB5F34c51
- name: test_weighted_pool_creation - name: test_weighted_pool_creation
start_block: 16878320 start_block: 20128706
stop_block: 16971020 stop_block: 20128806
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0x8055b8C947De30130BC1Ec750C8F345a50006B23" - id: "0xe96a45f66bdDA121B24F0a861372A72E8889523d"
tokens: tokens:
- "0xba100000625a3754423978a60c9317c58a424e3D" - "0x38C2a4a7330b22788374B8Ff70BBa513C8D848cA"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - "0x514910771AF9Ca656af840dff83E8264EcF986CA"
static_attributes: static_attributes: null
creation_tx: "0x3ae08d6ff86737a64827855af810f7ee9ee208ff8e6d8c916495d09a83282c8a" creation_tx: "0xa63c671046ad2075ec8ea83ac21199cf3e3a5f433e72ec4c117cbabfb9b18de2"
- id: "0xDac7eF49161bdBf0e8f0B4c8e2D38DF19D972874"
tokens: # WeightedPool2TokensFactory - 0xA5bf2ddF098bb0Ef6d120C98217dD6B141c74EE0
- "0x9A62fB1CAFEa99f8f0441f80af7F7ccf0d46847D" - name: weighted_legacy_creation
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" start_block: 13148365
static_attributes: stop_block: 13148465
creation_tx: "0xbed0c745c3761cd54a7489181a5f1165c628c35ef69ecb0bfeec08f09f0ec407"
# ComposableStablePoolFactory - 0xDB8d758BCb971e482B2C45f7F8a7740283A1bd3A
- name: test_composable_stable_pool_creation
start_block: 17672478
stop_block: 17677310
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0x20356663C17D31549d1210379749E2aE36722D8f" - id: "0xBF96189Eee9357a95C7719f4F5047F76bdE804E5"
tokens: tokens:
- "0xba100000625a3754423978a60c9317c58a424e3D" - "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
static_attributes: static_attributes: null
creation_tx: "0x822653ae905ab40f51f46c7b8185ba9a4aa06e674789f87dd26d0d11b26dc7c9" creation_tx: "0xdced662e41b1608c386551bbc89894a10321fd8bd58782e22077d1044cf99cb5"
- id: "0x42ED016F826165C2e5976fe5bC3df540C5aD0Af7"
tokens: # ComposableStablePoolFactory - 0xDB8d758BCb971e482B2C45f7F8a7740283A1bd3A
- name: test_composable_stable_pool_creation
start_block: 17677300
stop_block: 17678400
expected_state:
protocol_components:
- id: "0x42ED016F826165C2e5976fe5bC3df540C5aD0Af7"
tokens:
- "0x42ed016f826165c2e5976fe5bc3df540c5ad0af7"
- "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0" - "0x7f39C581F595B53c5cb19bD0b3f8dA6c935E2Ca0"
- "0xac3E018457B222d93114458476f3E3416Abbe38F" - "0xac3E018457B222d93114458476f3E3416Abbe38F"
- "0xae78736Cd615f374D3085123A210448E74Fc6393" - "0xae78736Cd615f374D3085123A210448E74Fc6393"
static_attributes: static_attributes: null
creation_tx: "0x53ff6bab0d8a76a998e29e59da8068ad906ae85507a1c2fbf2505e2cb52fd754" skip_simulation: true
# ERC4626LinearPoolFactory - 0x813EE7a840CE909E7Fea2117A44a90b8063bd4fd creation_tx: "0x53ff6bab0d8a76a998e29e59da8068ad906ae85507a1c2fbf2505e2cb52fd754"
# ERC4626LinearPoolFactory - 0x813EE7a840CE909E7Fea2117A44a90b8063bd4fd
- name: test_erc4626_linear_pool_creation - name: test_erc4626_linear_pool_creation
start_block: 17045881 start_block: 17480142
stop_block: 17480143 stop_block: 17480242
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0x9516a2d25958EdB8da246a320f2c7d94A0DBe25d" - id: "0x3fCb7085B8F2F473F80bF6D879cAe99eA4DE9344"
tokens: tokens:
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - "0x39Dd7790e75C6F663731f7E1FdC0f35007D3879b"
- "0xB99eDc4b289B0F2284fCF3f66884191BdCe29624" - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
static_attributes: - "0x3fcb7085b8f2f473f80bf6d879cae99ea4de9344"
creation_tx: "0x756e81cea4cf725c738bcef3852ad57687156b561574cad3e2956e6cb48da5e6" static_attributes: null
- id: "0x3fCb7085B8F2F473F80bF6D879cAe99eA4DE9344" skip_simulation: true
tokens: creation_tx: "0x5ff97870685370bab3876a4335d28c42e24659064fe78b486d6fb1b37b992877"
- "0x39Dd7790e75C6F663731f7E1FdC0f35007D3879b"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" # EulerLinearPoolFactory - 0x5F43FBa61f63Fa6bFF101a0A0458cEA917f6B347
static_attributes:
creation_tx: "0x5ff97870685370bab3876a4335d28c42e24659064fe78b486d6fb1b37b992877"
# EulerLinearPoolFactory - 0x5F43FBa61f63Fa6bFF101a0A0458cEA917f6B347
- name: test_euler_linear_pool_creation - name: test_euler_linear_pool_creation
start_block: 16588078 start_block: 16588117
stop_block: 16588118 stop_block: 16588217
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0xDEC02e6642e2c999aF429F5cE944653CAd15e093" - id: "0xD4e7C1F3DA1144c9E2CfD1b015eDA7652b4a4399"
tokens: tokens:
- "0xC101dcA301a4011C1F925e9622e749e550a1B667" - "0xD4e7C1F3DA1144c9E2CfD1b015eDA7652b4a4399"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
static_attributes: - "0xEb91861f8A4e1C12333F42DCE8fB0Ecdc28dA716"
creation_tx: "0xd639c8a6c3a553d47fd7f3d384ec4bc50a2cd6dfb2c3135b7f5db49d73c15df2" static_attributes: null
- id: "0xD4e7C1F3DA1144c9E2CfD1b015eDA7652b4a4399" skip_simulation: true
tokens: creation_tx: "0x4a9ea683052afefdae3d189862868c3a7dc8f431d1d9828b6bfd9451a8816426"
- "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
- "0xEb91861f8A4e1C12333F42DCE8fB0Ecdc28dA716" # SiloLinearPoolFactory - 0x4E11AEec21baF1660b1a46472963cB3DA7811C89
static_attributes:
creation_tx: "0x4a9ea683052afefdae3d189862868c3a7dc8f431d1d9828b6bfd9451a8816426"
# SiloLinearPoolFactory - 0x4E11AEec21baF1660b1a46472963cB3DA7811C89
- name: test_silo_linear_pool_creation - name: test_silo_linear_pool_creation
start_block: 17173185 start_block: 17173185
stop_block: 17173187 stop_block: 17173187
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0x74CBfAF94A3577c539a9dCEE9870A6349a33b34f" - id: "0x74CBfAF94A3577c539a9dCEE9870A6349a33b34f"
tokens: tokens:
- "0x192E67544694a7bAA2DeA94f9B1Df58BB3395A12" - "0x192E67544694a7bAA2DeA94f9B1Df58BB3395A12"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
static_attributes: - "0x74cbfaf94a3577c539a9dcee9870a6349a33b34f"
creation_tx: "0xd639c8a6c3a553d47fd7f3d384ec4bc50a2cd6dfb2c3135b7f5db49d73c15df2" static_attributes: null
# YearnLinearPoolFactory - 0x5F5222Ffa40F2AEd6380D022184D6ea67C776eE0a skip_simulation: true
creation_tx: "0x215c9f4256ab450368132f4063611ae8cdd98e80bea7e44ecf0600ed1d757018"
# YearnLinearPoolFactory - 0x5F5222Ffa40F2AEd6380D022184D6ea67C776eE0a
- name: test_yearn_linear_pool_creation - name: test_yearn_linear_pool_creation
start_block: 17052601 start_block: 17052601
stop_block: 17052605 stop_block: 17052605
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0x5F5222Ffa40F2AEd6380D022184D6ea67C776eE0" - id: "0xac5b4ef7ede2f2843a704e96dcaa637f4ba3dc3f"
tokens: tokens:
- "0x806E02Dea8d4a0882caD9fA3Fa75B212328692dE" - "0x806E02Dea8d4a0882caD9fA3Fa75B212328692dE"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
static_attributes: - "0xac5b4ef7ede2f2843a704e96dcaa637f4ba3dc3f"
creation_tx: "0x497aa03ce84d236c183204ddfc6762c8e4158da1ebc5e7e18e7f6cceaa497a2a" static_attributes: null
# WeightedPool2TokensFactory - 0xA5bf2ddF098bb0Ef6d120C98217dD6B141c74EE0 skip_simulation: true
- name: test_yearn_linear_pool_creation creation_tx: "0x497aa03ce84d236c183204ddfc6762c8e4158da1ebc5e7e18e7f6cceaa497a2a"
start_block: 12349890
stop_block: 12363660
expected_state:
protocol_components:
- id: "0x021c343C6180f03cE9E48FaE3ff432309b9aF199"
tokens:
- "0xD291E7a03283640FDc51b121aC401383A46cC623"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
static_attributes:
creation_tx: "0xc6b9f8ee6c6f17edacc6df1b6287c57157a16f2aa5b315a03cf2c42d7ebb74e3"
- id: "0x571046EaE58C783f29f95ADBa17Dd561Af8a8712"
tokens:
- "0x6B175474E89094C44Da98b954EedeAC495271d0F"
- "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
static_attributes:
creation_tx: "0xe60d97670f902d58e65fb2d56f77a0cee19ffad47deae1fb8a126d534ee1ece5"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -929,8 +929,8 @@ pub mod events {
.topics .topics
.get(0) .get(0)
.expect("bounds already checked") .expect("bounds already checked")
.as_ref() == .as_ref()
Self::TOPIC_ID; == Self::TOPIC_ID;
} }
pub fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String> { pub fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String> {
let mut values = let mut values =
@@ -1009,8 +1009,8 @@ pub mod events {
.topics .topics
.get(0) .get(0)
.expect("bounds already checked") .expect("bounds already checked")
.as_ref() == .as_ref()
Self::TOPIC_ID; == Self::TOPIC_ID;
} }
pub fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String> { pub fn decode(log: &substreams_ethereum::pb::eth::v2::Log) -> Result<Self, String> {
let mut values = let mut values =

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
#![allow(clippy::all)] #![allow(clippy::all)]
pub mod crypto_pool_factory; pub mod crypto_pool_factory;
pub mod stableswap_factory;
pub mod crypto_swap_ng_factory; pub mod crypto_swap_ng_factory;
pub mod meta_registry;
pub mod tricrypto_factory;
pub mod twocrypto_factory;
pub mod erc20; pub mod erc20;
pub mod meta_pool_factory; pub mod meta_pool_factory;
pub mod meta_registry;
pub mod stableswap_factory;
pub mod tricrypto_factory;
pub mod twocrypto_factory;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -8,6 +8,8 @@ tests:
- name: test_pool_creation - name: test_pool_creation
start_block: 123 start_block: 123
stop_block: 456 stop_block: 456
initialized_accounts:
- "0x0c0e5f2fF0ff18a3be9b835635039256dC4B4963" # Needed for ....
expected_state: expected_state:
protocol_components: protocol_components:
- id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7" - id: "0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7"
@@ -17,6 +19,7 @@ tests:
- "0x6b175474e89094c44da98b954eedeac495271d0f" - "0x6b175474e89094c44da98b954eedeac495271d0f"
static_attributes: static_attributes:
creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6" creation_tx: "0x20793bbf260912aae189d5d261ff003c9b9166da8191d8f9d63ff1c7722f3ac6"
skip_simulation: false
- name: test_something_else - name: test_something_else
start_block: 123 start_block: 123
stop_block: 456 stop_block: 456
@@ -28,3 +31,4 @@ tests:
- "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84" - "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84"
static_attributes: static_attributes:
creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa" creation_tx: "0xfac67ecbd423a5b915deff06045ec9343568edaec34ae95c43d35f2c018afdaa"
skip_simulation: true # If true, always add a reason

View File

@@ -2,6 +2,7 @@ version: '3.1'
services: services:
db: db:
build: build:
context: .
dockerfile: postgres.Dockerfile dockerfile: postgres.Dockerfile
restart: "always" restart: "always"
environment: environment:
@@ -11,8 +12,7 @@ services:
POSTGRESQL_SHARED_PRELOAD_LIBRARIES: pg_cron POSTGRESQL_SHARED_PRELOAD_LIBRARIES: pg_cron
ports: ports:
- "5431:5432" - "5431:5432"
volumes: shm_size: '1gb'
- postgres_data:/var/lib/postgresql/data
app: app:
build: build:
context: . context: .
@@ -22,7 +22,7 @@ services:
- ../substreams:/app/substreams - ../substreams:/app/substreams
- ../proto:/app/proto - ../proto:/app/proto
- ./tycho-indexer:/app/testing/tycho-indexer - ./tycho-indexer:/app/testing/tycho-indexer
- ./runner.py:/app/testing/runner.py - ./src/runner/runner.py:/app/testing/src.py
ports: ports:
- "80:80" - "80:80"
depends_on: depends_on:

View File

@@ -2,4 +2,4 @@ psycopg2==2.9.9
PyYAML==6.0.1 PyYAML==6.0.1
Requests==2.32.2 Requests==2.32.2
web3==5.31.3 web3==5.31.3
./tycho-client -e ./tycho-client

View File

@@ -7,20 +7,28 @@ def main() -> None:
description="Run indexer within a specified range of blocks" description="Run indexer within a specified range of blocks"
) )
parser.add_argument( parser.add_argument(
"--test_yaml_path", type=str, help="Path to the test configuration YAML file." "--package", type=str, help="Name of the package to test."
) )
parser.add_argument( parser.add_argument(
"--with_binary_logs", "--tycho-logs",
action="store_true", action="store_true",
help="Flag to activate logs from Tycho.", help="Flag to activate logs from Tycho.",
) )
parser.add_argument( parser.add_argument(
"--db_url", type=str, help="Postgres database URL for the Tycho indexer." "--db-url", type=str, help="Postgres database URL for the Tycho indexer."
)
parser.add_argument(
"--vm-traces",
action="store_true",
help="Enable tracing during vm simulations.",
) )
args = parser.parse_args() args = parser.parse_args()
test_runner = TestRunner( test_runner = TestRunner(
args.test_yaml_path, args.with_binary_logs, db_url=args.db_url args.package,
args.tycho_logs,
db_url=args.db_url,
vm_traces=args.vm_traces,
) )
test_runner.run_tests() test_runner.run_tests()

View File

@@ -46,21 +46,26 @@ class SimulationFailure(BaseModel):
class TestRunner: class TestRunner:
def __init__(self, config_path: str, with_binary_logs: bool, db_url: str): def __init__(self, package: str, with_binary_logs: bool, db_url: str, vm_traces: bool):
self.repo_root = os.getcwd()
config_path = os.path.join(self.repo_root, "substreams", package, "test_assets.yaml")
self.config = load_config(config_path) self.config = load_config(config_path)
self.base_dir = os.path.dirname(config_path) self.spkg_src = os.path.join(self.repo_root, "substreams", package)
self.tycho_runner = TychoRunner(with_binary_logs) self.adapters_src = os.path.join(self.repo_root, "evm")
self.tycho_runner = TychoRunner(db_url, with_binary_logs, self.config["initialized_accounts"])
self.tycho_rpc_client = TychoRPCClient() self.tycho_rpc_client = TychoRPCClient()
self.db_url = db_url self.db_url = db_url
self._vm_traces = vm_traces
self._chain = Blockchain.ethereum self._chain = Blockchain.ethereum
def run_tests(self) -> None: def run_tests(self) -> None:
"""Run all tests specified in the configuration.""" """Run all tests specified in the configuration."""
print(f"Running tests ...") print(f"Running tests ...")
for test in self.config["tests"]: for test in self.config["tests"]:
self.tycho_runner.empty_database(self.db_url)
spkg_path = self.build_spkg( spkg_path = self.build_spkg(
os.path.join(self.base_dir, self.config["substreams_yaml_path"]), os.path.join(self.spkg_src, self.config["substreams_yaml_path"]),
lambda data: self.update_initial_block(data, test["start_block"]), lambda data: self.update_initial_block(data, test["start_block"]),
) )
self.tycho_runner.run_tycho( self.tycho_runner.run_tycho(
@@ -68,6 +73,7 @@ class TestRunner:
test["start_block"], test["start_block"],
test["stop_block"], test["stop_block"],
self.config["protocol_type_names"], self.config["protocol_type_names"],
test.get("initialized_accounts", []),
) )
result = self.tycho_runner.run_with_rpc_server( result = self.tycho_runner.run_with_rpc_server(
@@ -80,8 +86,6 @@ class TestRunner:
else: else:
print(f"❗️ {test['name']} failed: {result.message}") print(f"❗️ {test['name']} failed: {result.message}")
self.tycho_runner.empty_database(self.db_url)
def validate_state(self, expected_state: dict, stop_block: int) -> TestResult: def validate_state(self, expected_state: dict, stop_block: int) -> TestResult:
"""Validate the current protocol state against the expected state.""" """Validate the current protocol state against the expected state."""
protocol_components = self.tycho_rpc_client.get_protocol_components() protocol_components = self.tycho_rpc_client.get_protocol_components()
@@ -101,13 +105,15 @@ class TestRunner:
component = components[comp_id] component = components[comp_id]
for key, value in expected_component.items(): for key, value in expected_component.items():
if key not in ["tokens", "static_attributes", "creation_tx"]:
continue
if key not in component: if key not in component:
return TestResult.Failed( return TestResult.Failed(
f"Missing '{key}' in component '{comp_id}'." f"Missing '{key}' in component '{comp_id}'."
) )
if isinstance(value, list): if isinstance(value, list):
if set(map(str.lower, value)) != set( if set(map(str.lower, value)) != set(
map(str.lower, component[key]) map(str.lower, component[key])
): ):
return TestResult.Failed( return TestResult.Failed(
f"List mismatch for key '{key}': {value} != {component[key]}" f"List mismatch for key '{key}': {value} != {component[key]}"
@@ -145,11 +151,14 @@ class TestRunner:
f"from rpc call and {tycho_balance} from Substreams" f"from rpc call and {tycho_balance} from Substreams"
) )
contract_states = self.tycho_rpc_client.get_contract_state() contract_states = self.tycho_rpc_client.get_contract_state()
filtered_components = {'protocol_components': [pc for pc in protocol_components["protocol_components"] if
pc["id"] in [c["id"].lower() for c in
expected_state["protocol_components"] if
c.get("skip_simulation", False) is False]]}
simulation_failures = self.simulate_get_amount_out( simulation_failures = self.simulate_get_amount_out(
token_balances,
stop_block, stop_block,
protocol_states, protocol_states,
protocol_components, filtered_components,
contract_states, contract_states,
) )
if len(simulation_failures): if len(simulation_failures):
@@ -169,12 +178,11 @@ class TestRunner:
return TestResult.Failed(error_message) return TestResult.Failed(error_message)
def simulate_get_amount_out( def simulate_get_amount_out(
self, self,
token_balances: dict[str, dict[str, int]], block_number: int,
block_number: int, protocol_states: dict,
protocol_states: dict, protocol_components: dict,
protocol_components: dict, contract_state: dict,
contract_state: dict,
) -> dict[str, list[SimulationFailure]]: ) -> dict[str, list[SimulationFailure]]:
protocol_type_names = self.config["protocol_type_names"] protocol_type_names = self.config["protocol_type_names"]
@@ -188,9 +196,10 @@ class TestRunner:
failed_simulations: dict[str, list[SimulationFailure]] = dict() failed_simulations: dict[str, list[SimulationFailure]] = dict()
for protocol in protocol_type_names: for protocol in protocol_type_names:
adapter_contract = os.path.join( adapter_contract = os.path.join(
self.base_dir, "evm", self.config["adapter_contract"] self.adapters_src, "out", f"{self.config['adapter_contract']}.sol",
f"{self.config['adapter_contract']}.evm.runtime"
) )
decoder = ThirdPartyPoolTychoDecoder(adapter_contract, 0, False) decoder = ThirdPartyPoolTychoDecoder(adapter_contract, 0, trace=self._vm_traces)
stream_adapter = TychoPoolStateStreamAdapter( stream_adapter = TychoPoolStateStreamAdapter(
tycho_url="0.0.0.0:4242", tycho_url="0.0.0.0:4242",
protocol=protocol, protocol=protocol,
@@ -204,21 +213,17 @@ class TestRunner:
for pool_state in decoded.pool_states.values(): for pool_state in decoded.pool_states.values():
pool_id = pool_state.id_ pool_id = pool_state.id_
protocol_balances = token_balances.get(pool_id) if not pool_state.balances:
if not protocol_balances:
raise ValueError(f"Missing balances for pool {pool_id}") raise ValueError(f"Missing balances for pool {pool_id}")
for sell_token, buy_token in itertools.permutations( for sell_token, buy_token in itertools.permutations(
pool_state.tokens, 2 pool_state.tokens, 2
): ):
# Try to sell 0.1% of the protocol balance
sell_amount = Decimal("0.001") * pool_state.balances[sell_token.address]
try: try:
# Try to sell 0.1% of the protocol balance
sell_amount = Decimal("0.001") * sell_token.from_onchain_amount(
protocol_balances[sell_token.address]
)
amount_out, gas_used, _ = pool_state.get_amount_out( amount_out, gas_used, _ = pool_state.get_amount_out(
sell_token, sell_amount, buy_token sell_token, sell_amount, buy_token
) )
# TODO: Should we validate this with an archive node or RPC reader?
print( print(
f"Amount out for {pool_id}: {sell_amount} {sell_token} -> {amount_out} {buy_token} - " f"Amount out for {pool_id}: {sell_amount} {sell_token} -> {amount_out} {buy_token} - "
f"Gas used: {gas_used}" f"Gas used: {gas_used}"
@@ -233,8 +238,8 @@ class TestRunner:
failed_simulations[pool_id].append( failed_simulations[pool_id].append(
SimulationFailure( SimulationFailure(
pool_id=pool_id, pool_id=pool_id,
sell_token=sell_token, sell_token=str(sell_token),
buy_token=buy_token, buy_token=str(buy_token),
error=str(e), error=str(e),
) )
) )

View File

@@ -1,29 +1,42 @@
import os
import platform
import signal import signal
import subprocess import subprocess
import sys
import threading import threading
import time import time
from pathlib import Path
import psycopg2 import psycopg2
import requests import requests
from psycopg2 import sql from psycopg2 import sql
import os
def get_binary_path():
path = Path(__file__).parent
if sys.platform.startswith("darwin") and platform.machine() == "arm64":
return Path(__file__).parent / "tycho-indexer-mac-arm64"
elif sys.platform.startswith("linux") and platform.machine() == "x86_64":
return Path(__file__).parent / "tycho-indexer-linux-x64"
else:
raise RuntimeError("Unsupported platform or architecture")
binary_path = get_binary_path() def find_binary_file(file_name):
# Define usual locations for binary files in Unix-based systems
locations = [
"/bin",
"/sbin",
"/usr/bin",
"/usr/sbin",
"/usr/local/bin",
"/usr/local/sbin",
]
# Add user's local bin directory if it exists
home = os.path.expanduser("~")
if os.path.exists(home + "/.local/bin"):
locations.append(home + "/.local/bin")
# Check each location
for location in locations:
potential_path = location + "/" + file_name
if os.path.exists(potential_path):
return potential_path
# If binary is not found in the usual locations, return None
raise RuntimeError("Unable to locate tycho-indexer binary")
binary_path = find_binary_file("tycho-indexer")
class TychoRPCClient: class TychoRPCClient:
@@ -50,7 +63,7 @@ class TychoRPCClient:
def get_contract_state(self) -> dict: def get_contract_state(self) -> dict:
"""Retrieve contract state from the RPC server.""" """Retrieve contract state from the RPC server."""
url = self.rpc_url + "/v1/ethereum/contract_state" url = self.rpc_url + "/v1/ethereum/contract_state?include_balances=false"
headers = {"accept": "application/json", "Content-Type": "application/json"} headers = {"accept": "application/json", "Content-Type": "application/json"}
data = {} data = {}
@@ -59,25 +72,32 @@ class TychoRPCClient:
class TychoRunner: class TychoRunner:
def __init__(self, with_binary_logs: bool = False): def __init__(self, db_url: str, with_binary_logs: bool = False, initialized_accounts: list[str] = None):
self.with_binary_logs = with_binary_logs self.with_binary_logs = with_binary_logs
self._db_url = db_url
self._initialized_accounts = initialized_accounts or []
def run_tycho( def run_tycho(
self, self,
spkg_path: str, spkg_path: str,
start_block: int, start_block: int,
end_block: int, end_block: int,
protocol_type_names: list, protocol_type_names: list,
initialized_accounts: list,
) -> None: ) -> None:
"""Run the Tycho indexer with the specified SPKG and block range.""" """Run the Tycho indexer with the specified SPKG and block range."""
env = os.environ.copy() env = os.environ.copy()
env["RUST_LOG"] = "info" env["RUST_LOG"] = "tycho_indexer=info"
all_accounts = self._initialized_accounts + initialized_accounts
try: try:
process = subprocess.Popen( process = subprocess.Popen(
[ [
binary_path, binary_path,
"--database-url",
self._db_url,
"run", "run",
"--spkg", "--spkg",
spkg_path, spkg_path,
@@ -88,14 +108,15 @@ class TychoRunner:
"--start-block", "--start-block",
str(start_block), str(start_block),
"--stop-block", "--stop-block",
str(end_block + 2), # +2 is to make up for the cache in the index side.
], # +2 is to make up for the cache in the index side. str(end_block + 2)
] + (["--initialized-accounts", ",".join(all_accounts)] if all_accounts else []),
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
bufsize=1, bufsize=1,
env=env, env=env,
) )
with process.stdout: with process.stdout:
for line in iter(process.stdout.readline, ""): for line in iter(process.stdout.readline, ""):
@@ -128,7 +149,12 @@ class TychoRunner:
env["RUST_LOG"] = "info" env["RUST_LOG"] = "info"
process = subprocess.Popen( process = subprocess.Popen(
[binary_path, "rpc"], [
binary_path,
"--database-url",
self._db_url,
"rpc"
],
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
text=True, text=True,
@@ -178,7 +204,7 @@ class TychoRunner:
def empty_database(db_url: str) -> None: def empty_database(db_url: str) -> None:
"""Drop and recreate the Tycho indexer database.""" """Drop and recreate the Tycho indexer database."""
try: try:
conn = psycopg2.connect(db_url) conn = psycopg2.connect(db_url[:db_url.rfind('/')])
conn.autocommit = True conn.autocommit = True
cursor = conn.cursor() cursor = conn.cursor()

View File

@@ -1,7 +1,4 @@
from setuptools import setup, find_packages from setuptools import setup, find_packages
import sys
import platform
from pathlib import Path
def read_requirements(): def read_requirements():
@@ -11,25 +8,6 @@ def read_requirements():
return [req for req in requirements if req and not req.startswith("#")] return [req for req in requirements if req and not req.startswith("#")]
# Determine the correct wheel file based on the platform and Python version
def get_wheel_file():
path = Path(__file__).parent
if sys.platform.startswith("darwin") and platform.machine() == "arm64":
return str(
path / "wheels" / f"protosim_py-0.4.9-cp39-cp39-macosx_11_0_arm64.whl"
)
elif sys.platform.startswith("linux") and platform.machine() == "x86_64":
return str(
path
/ "wheels"
/ f"protosim_py-0.4.11-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl"
)
else:
raise RuntimeError("Unsupported platform or architecture")
wheel_file = get_wheel_file()
setup( setup(
name="tycho-client", name="tycho-client",
version="0.1.0", version="0.1.0",
@@ -51,7 +29,7 @@ setup(
"eth-utils==1.9.5", "eth-utils==1.9.5",
"hexbytes==0.3.1", "hexbytes==0.3.1",
"pydantic==2.8.2", "pydantic==2.8.2",
f"protosim_py @ file://{wheel_file}", "protosim_py==0.4.11",
], ],
package_data={"tycho-client": ["../wheels/*", "./assets/*", "./bins/*"]}, package_data={"tycho-client": ["../wheels/*", "./assets/*", "./bins/*"]},
include_package_data=True, include_package_data=True,

View File

@@ -1,11 +1,18 @@
import time
from decimal import Decimal from decimal import Decimal
from logging import getLogger from logging import getLogger
from typing import Any from typing import Any
import eth_abi
from eth_utils import keccak
from protosim_py import SimulationEngine, SimulationParameters, AccountInfo
from .constants import EXTERNAL_ACCOUNT
from .exceptions import TychoDecodeError from .exceptions import TychoDecodeError
from .models import EVMBlock, EthereumToken from .models import EVMBlock, EthereumToken
from .pool_state import ThirdPartyPool from .pool_state import ThirdPartyPool
from .utils import decode_tycho_exchange from .tycho_db import TychoDBSingleton
from .utils import decode_tycho_exchange, get_code_for_address
log = getLogger(__name__) log = getLogger(__name__)
@@ -13,16 +20,16 @@ log = getLogger(__name__)
class ThirdPartyPoolTychoDecoder: class ThirdPartyPoolTychoDecoder:
"""ThirdPartyPool decoder for protocol messages from the Tycho feed""" """ThirdPartyPool decoder for protocol messages from the Tycho feed"""
def __init__(self, adapter_contract: str, minimum_gas: int, hard_limit: bool): def __init__(self, adapter_contract: str, minimum_gas: int, trace: bool):
self.adapter_contract = adapter_contract self.adapter_contract = adapter_contract
self.minimum_gas = minimum_gas self.minimum_gas = minimum_gas
self.hard_limit = hard_limit self.trace = trace
def decode_snapshot( def decode_snapshot(
self, self,
snapshot: dict[str, Any], snapshot: dict[str, Any],
block: EVMBlock, block: EVMBlock,
tokens: dict[str, EthereumToken], tokens: dict[str, EthereumToken],
) -> tuple[dict[str, ThirdPartyPool], list[str]]: ) -> tuple[dict[str, ThirdPartyPool], list[str]]:
pools = {} pools = {}
failed_pools = [] failed_pools = []
@@ -38,7 +45,7 @@ class ThirdPartyPoolTychoDecoder:
return pools, failed_pools return pools, failed_pools
def decode_pool_state( def decode_pool_state(
self, snap: dict, block: EVMBlock, tokens: dict[str, EthereumToken] self, snap: dict, block: EVMBlock, tokens: dict[str, EthereumToken]
) -> ThirdPartyPool: ) -> ThirdPartyPool:
component = snap["component"] component = snap["component"]
exchange, _ = decode_tycho_exchange(component["protocol_system"]) exchange, _ = decode_tycho_exchange(component["protocol_system"])
@@ -49,7 +56,7 @@ class ThirdPartyPoolTychoDecoder:
raise TychoDecodeError("Unsupported token", pool_id=component["id"]) raise TychoDecodeError("Unsupported token", pool_id=component["id"])
balances = self.decode_balances(snap, tokens) balances = self.decode_balances(snap, tokens)
optional_attributes = self.decode_optional_attributes(component, snap) optional_attributes = self.decode_optional_attributes(component, snap, block.id)
return ThirdPartyPool( return ThirdPartyPool(
id_=optional_attributes.pop("pool_id", component["id"]), id_=optional_attributes.pop("pool_id", component["id"]),
@@ -61,41 +68,68 @@ class ThirdPartyPoolTychoDecoder:
exchange=exchange, exchange=exchange,
adapter_contract_name=self.adapter_contract, adapter_contract_name=self.adapter_contract,
minimum_gas=self.minimum_gas, minimum_gas=self.minimum_gas,
hard_sell_limit=self.hard_limit, trace=self.trace,
trace=False,
**optional_attributes, **optional_attributes,
) )
@staticmethod @staticmethod
def decode_optional_attributes(component, snap): def decode_optional_attributes(component, snap, block_number):
# Handle optional state attributes # Handle optional state attributes
attributes = snap["state"]["attributes"] attributes = snap["state"]["attributes"]
pool_id = attributes.get("pool_id") or component["id"]
balance_owner = attributes.get("balance_owner") balance_owner = attributes.get("balance_owner")
stateless_contracts = {} stateless_contracts = {}
static_attributes = snap["component"]["static_attributes"] static_attributes = snap["component"]["static_attributes"]
pool_id = static_attributes.get("pool_id") or component["id"]
index = 0 index = 0
while f"stateless_contract_addr_{index}" in static_attributes: while f"stateless_contract_addr_{index}" in static_attributes:
encoded_address = static_attributes[f"stateless_contract_addr_{index}"] encoded_address = static_attributes[f"stateless_contract_addr_{index}"]
address = bytes.fromhex(encoded_address[2:] if encoded_address.startswith('0x') else encoded_address).decode('utf-8') decoded = bytes.fromhex(
encoded_address[2:] if encoded_address.startswith('0x') else encoded_address).decode('utf-8')
if decoded.startswith("call"):
address = ThirdPartyPoolTychoDecoder.get_address_from_call(block_number, decoded)
else:
address = decoded
code = static_attributes.get(f"stateless_contract_code_{index}") or get_code_for_address(address) code = static_attributes.get(f"stateless_contract_code_{index}") or get_code_for_address(address)
stateless_contracts[address] = code stateless_contracts[address] = code
index += 1 index += 1
index = 0 index = 0
while f"stateless_contract_addr_{index}" in attributes: while f"stateless_contract_addr_{index}" in attributes:
address = attributes[f"stateless_contract_addr_{index}"] address = attributes[f"stateless_contract_addr_{index}"]
code = attributes.get(f"stateless_contract_code_{index}") or get_code_for_address(address) code = attributes.get(f"stateless_contract_code_{index}") or get_code_for_address(address)
stateless_contracts[address] = code stateless_contracts[address] = code
index += 1 index += 1
return { return {
"balance_owner": balance_owner, "balance_owner": balance_owner,
"pool_id": pool_id, "pool_id": pool_id,
"stateless_contracts": stateless_contracts, "stateless_contracts": stateless_contracts,
} }
@staticmethod
def get_address_from_call(block_number, decoded):
db = TychoDBSingleton.get_instance()
engine = SimulationEngine.new_with_tycho_db(db=db)
engine.init_account(
address="0x0000000000000000000000000000000000000000",
account=AccountInfo(balance=0, nonce=0),
mocked=False,
permanent_storage=None,
)
selector = keccak(text=decoded.split(":")[-1])[:4]
sim_result = engine.run_sim(SimulationParameters(
data=bytearray(selector),
to=decoded.split(':')[1],
block_number=block_number,
timestamp=int(time.time()),
overrides={},
caller=EXTERNAL_ACCOUNT,
value=0,
))
address = eth_abi.decode(["address"], bytearray(sim_result.result))
return address[0]
@staticmethod @staticmethod
def decode_balances(snap, tokens): def decode_balances(snap, tokens):
balances = {} balances = {}
@@ -109,10 +143,10 @@ class ThirdPartyPoolTychoDecoder:
@staticmethod @staticmethod
def apply_update( def apply_update(
pool: ThirdPartyPool, pool: ThirdPartyPool,
pool_update: dict[str, Any], pool_update: dict[str, Any],
balance_updates: dict[str, Any], balance_updates: dict[str, Any],
block: EVMBlock, block: EVMBlock,
) -> ThirdPartyPool: ) -> ThirdPartyPool:
# check for and apply optional state attributes # check for and apply optional state attributes
attributes = pool_update.get("updated_attributes") attributes = pool_update.get("updated_attributes")

View File

@@ -51,7 +51,7 @@ class EthereumToken(BaseModel):
return int(amount) return int(amount)
def from_onchain_amount( def from_onchain_amount(
self, onchain_amount: Union[int, Fraction], quantize: bool = True self, onchain_amount: Union[int, Fraction], quantize: bool = True
) -> Decimal: ) -> Decimal:
"""Converts an Integer to a quantized decimal, by shifting left by the token's """Converts an Integer to a quantized decimal, by shifting left by the token's
maximum amount of decimals (e.g.: 1000000 becomes 1.000000 for a 6-decimal token maximum amount of decimals (e.g.: 1000000 becomes 1.000000 for a 6-decimal token
@@ -66,14 +66,14 @@ class EthereumToken(BaseModel):
with localcontext(Context(rounding=ROUND_FLOOR, prec=256)): with localcontext(Context(rounding=ROUND_FLOOR, prec=256)):
if isinstance(onchain_amount, Fraction): if isinstance(onchain_amount, Fraction):
return ( return (
Decimal(onchain_amount.numerator) Decimal(onchain_amount.numerator)
/ Decimal(onchain_amount.denominator) / Decimal(onchain_amount.denominator)
/ Decimal(10 ** self.decimals) / Decimal(10 ** self.decimals)
).quantize(Decimal(f"{1 / 10 ** self.decimals}")) ).quantize(Decimal(f"{1 / 10 ** self.decimals}"))
if quantize is True: if quantize is True:
try: try:
amount = ( amount = (
Decimal(str(onchain_amount)) / 10 ** self.decimals Decimal(str(onchain_amount)) / 10 ** self.decimals
).quantize(Decimal(f"{1 / 10 ** self.decimals}")) ).quantize(Decimal(f"{1 / 10 ** self.decimals}"))
except InvalidOperation: except InvalidOperation:
amount = Decimal(str(onchain_amount)) / Decimal(10 ** self.decimals) amount = Decimal(str(onchain_amount)) / Decimal(10 ** self.decimals)
@@ -114,6 +114,8 @@ class Capability(IntEnum):
ConstantPrice = auto() ConstantPrice = auto()
TokenBalanceIndependent = auto() TokenBalanceIndependent = auto()
ScaledPrice = auto() ScaledPrice = auto()
HardLimits = auto()
MarginalPrice = auto()
class SynchronizerState(Enum): class SynchronizerState(Enum):

View File

@@ -64,13 +64,6 @@ class ThirdPartyPool(BaseModel):
trace: bool = False trace: bool = False
hard_sell_limit: bool = False
"""
Whether the pool will revert if you attempt to sell more than the limit. Defaults to
False where it is assumed that exceeding the limit will provide a bad price but will
still succeed.
"""
def __init__(self, **data): def __init__(self, **data):
super().__init__(**data) super().__init__(**data)
self._set_engine(data.get("engine", None)) self._set_engine(data.get("engine", None))
@@ -112,7 +105,7 @@ class ThirdPartyPool(BaseModel):
engine.init_account( engine.init_account(
address=ADAPTER_ADDRESS, address=ADAPTER_ADDRESS,
account=AccountInfo( account=AccountInfo(
balance=0, balance=MAX_BALANCE,
nonce=0, nonce=0,
code=get_contract_bytecode(self.adapter_contract_name), code=get_contract_bytecode(self.adapter_contract_name),
), ),
@@ -172,14 +165,14 @@ class ThirdPartyPool(BaseModel):
) )
def get_amount_out( def get_amount_out(
self: TPoolState, self: TPoolState,
sell_token: EthereumToken, sell_token: EthereumToken,
sell_amount: Decimal, sell_amount: Decimal,
buy_token: EthereumToken, buy_token: EthereumToken,
) -> tuple[Decimal, int, TPoolState]: ) -> tuple[Decimal, int, TPoolState]:
# if the pool has a hard limit and the sell amount exceeds that, simulate and # if the pool has a hard limit and the sell amount exceeds that, simulate and
# raise a partial trade # raise a partial trade
if self.hard_sell_limit: if Capability.HardLimits in self.capabilities:
sell_limit = self.get_sell_amount_limit(sell_token, buy_token) sell_limit = self.get_sell_amount_limit(sell_token, buy_token)
if sell_amount > sell_limit: if sell_amount > sell_limit:
partial_trade = self._get_amount_out(sell_token, sell_limit, buy_token) partial_trade = self._get_amount_out(sell_token, sell_limit, buy_token)
@@ -192,10 +185,10 @@ class ThirdPartyPool(BaseModel):
return self._get_amount_out(sell_token, sell_amount, buy_token) return self._get_amount_out(sell_token, sell_amount, buy_token)
def _get_amount_out( def _get_amount_out(
self: TPoolState, self: TPoolState,
sell_token: EthereumToken, sell_token: EthereumToken,
sell_amount: Decimal, sell_amount: Decimal,
buy_token: EthereumToken, buy_token: EthereumToken,
) -> tuple[Decimal, int, TPoolState]: ) -> tuple[Decimal, int, TPoolState]:
trade, state_changes = self._adapter_contract.swap( trade, state_changes = self._adapter_contract.swap(
cast(HexStr, self.id_), cast(HexStr, self.id_),
@@ -223,7 +216,7 @@ class ThirdPartyPool(BaseModel):
return buy_amount, trade.gas_used, new_state return buy_amount, trade.gas_used, new_state
def _get_overwrites( def _get_overwrites(
self, sell_token: EthereumToken, buy_token: EthereumToken, **kwargs self, sell_token: EthereumToken, buy_token: EthereumToken, **kwargs
) -> dict[Address, dict[int, int]]: ) -> dict[Address, dict[int, int]]:
"""Get an overwrites dictionary to use in a simulation. """Get an overwrites dictionary to use in a simulation.
@@ -234,7 +227,7 @@ class ThirdPartyPool(BaseModel):
return _merge(self.block_lasting_overwrites, token_overwrites) return _merge(self.block_lasting_overwrites, token_overwrites)
def _get_token_overwrites( def _get_token_overwrites(
self, sell_token: EthereumToken, buy_token: EthereumToken, max_amount=None self, sell_token: EthereumToken, buy_token: EthereumToken, max_amount=None
) -> dict[Address, dict[int, int]]: ) -> dict[Address, dict[int, int]]:
"""Creates overwrites for a token. """Creates overwrites for a token.
@@ -297,13 +290,12 @@ class ThirdPartyPool(BaseModel):
engine=self._engine, engine=self._engine,
balances=self.balances, balances=self.balances,
minimum_gas=self.minimum_gas, minimum_gas=self.minimum_gas,
hard_sell_limit=self.hard_sell_limit,
balance_owner=self.balance_owner, balance_owner=self.balance_owner,
stateless_contracts=self.stateless_contracts, stateless_contracts=self.stateless_contracts,
) )
def get_sell_amount_limit( def get_sell_amount_limit(
self, sell_token: EthereumToken, buy_token: EthereumToken self, sell_token: EthereumToken, buy_token: EthereumToken
) -> Decimal: ) -> Decimal:
""" """
Retrieves the sell amount of the given token. Retrieves the sell amount of the given token.

View File

@@ -26,10 +26,10 @@ log = getLogger(__name__)
class TokenLoader: class TokenLoader:
def __init__( def __init__(
self, self,
tycho_url: str, tycho_url: str,
blockchain: Blockchain, blockchain: Blockchain,
min_token_quality: Optional[int] = 0, min_token_quality: Optional[int] = 0,
): ):
self.tycho_url = tycho_url self.tycho_url = tycho_url
self.blockchain = blockchain self.blockchain = blockchain
@@ -45,10 +45,10 @@ class TokenLoader:
start = time.monotonic() start = time.monotonic()
all_tokens = [] all_tokens = []
while data := self._get_all_with_pagination( while data := self._get_all_with_pagination(
url=url, url=url,
page=page, page=page,
limit=self._token_limit, limit=self._token_limit,
params={"min_quality": self.min_token_quality}, params={"min_quality": self.min_token_quality},
): ):
all_tokens.extend(data) all_tokens.extend(data)
page += 1 page += 1
@@ -73,10 +73,10 @@ class TokenLoader:
start = time.monotonic() start = time.monotonic()
all_tokens = [] all_tokens = []
while data := self._get_all_with_pagination( while data := self._get_all_with_pagination(
url=url, url=url,
page=page, page=page,
limit=self._token_limit, limit=self._token_limit,
params={"min_quality": self.min_token_quality, "addresses": addresses}, params={"min_quality": self.min_token_quality, "addresses": addresses},
): ):
all_tokens.extend(data) all_tokens.extend(data)
page += 1 page += 1
@@ -95,7 +95,7 @@ class TokenLoader:
@staticmethod @staticmethod
def _get_all_with_pagination( def _get_all_with_pagination(
url: str, params: Optional[Dict] = None, page: int = 0, limit: int = 50 url: str, params: Optional[Dict] = None, page: int = 0, limit: int = 50
) -> Dict: ) -> Dict:
if params is None: if params is None:
params = {} params = {}
@@ -122,14 +122,14 @@ class BlockProtocolChanges:
class TychoPoolStateStreamAdapter: class TychoPoolStateStreamAdapter:
def __init__( def __init__(
self, self,
tycho_url: str, tycho_url: str,
protocol: str, protocol: str,
decoder: ThirdPartyPoolTychoDecoder, decoder: ThirdPartyPoolTychoDecoder,
blockchain: Blockchain, blockchain: Blockchain,
min_tvl: Optional[Decimal] = 10, min_tvl: Optional[Decimal] = 10,
min_token_quality: Optional[int] = 0, min_token_quality: Optional[int] = 0,
include_state=True, include_state=True,
): ):
""" """
:param tycho_url: URL to connect to Tycho DB :param tycho_url: URL to connect to Tycho DB
@@ -238,7 +238,7 @@ class TychoPoolStateStreamAdapter:
@staticmethod @staticmethod
def build_snapshot_message( def build_snapshot_message(
protocol_components: dict, protocol_states: dict, contract_states: dict protocol_components: dict, protocol_states: dict, contract_states: dict
) -> dict[str, ThirdPartyPool]: ) -> dict[str, ThirdPartyPool]:
vm_states = {state["address"]: state for state in contract_states["accounts"]} vm_states = {state["address"]: state for state in contract_states["accounts"]}
states = {} states = {}
@@ -248,7 +248,7 @@ class TychoPoolStateStreamAdapter:
for state in protocol_states["states"]: for state in protocol_states["states"]:
pool_id = state["component_id"] pool_id = state["component_id"]
if pool_id not in states: if pool_id not in states:
log.warning(f"State for pool {pool_id} not found in components") log.debug(f"{pool_id} was present in snapshot but not in components")
continue continue
states[pool_id]["state"] = state states[pool_id]["state"] = state
snapshot = {"vm_storage": vm_states, "states": states} snapshot = {"vm_storage": vm_states, "states": states}
@@ -269,7 +269,7 @@ class TychoPoolStateStreamAdapter:
return self.process_snapshot(block, state_msg["snapshot"]) return self.process_snapshot(block, state_msg["snapshot"])
def process_snapshot( def process_snapshot(
self, block: EVMBlock, state_msg: dict self, block: EVMBlock, state_msg: dict
) -> BlockProtocolChanges: ) -> BlockProtocolChanges:
start = time.monotonic() start = time.monotonic()
removed_pools = set() removed_pools = set()

View File

@@ -74,6 +74,7 @@ class ERC20OverwriteFactory:
self._overwrites = dict() self._overwrites = dict()
self._balance_slot: Final[int] = 0 self._balance_slot: Final[int] = 0
self._allowance_slot: Final[int] = 1 self._allowance_slot: Final[int] = 1
self._total_supply_slot: Final[int] = 2
def set_balance(self, balance: int, owner: Address): def set_balance(self, balance: int, owner: Address):
""" """
@@ -111,6 +112,19 @@ class ERC20OverwriteFactory:
f"spender={spender} value={allowance} slot={storage_index}", f"spender={spender} value={allowance} slot={storage_index}",
) )
def set_total_supply(self, supply: int):
"""
Set the total supply of the token.
Parameters:
supply: The total supply value.
"""
self._overwrites[self._total_supply_slot] = supply
log.log(
5,
f"Override total supply: token={self._token.address} supply={supply}"
)
def get_protosim_overwrites(self) -> dict[Address, dict[int, int]]: def get_protosim_overwrites(self) -> dict[Address, dict[int, int]]:
""" """
Get the overwrites dictionary of previously collected values. Get the overwrites dictionary of previously collected values.