Skip to main content

tycho_simulation/evm/protocol/balancer_v3/
decoder.rs

1//! Decodes a `vm:balancer_v3` snapshot into a native [`BalancerV3State`].
2//!
3//! Mirrors the Curve hybrid decoder: the VM engine is used to resolve the pool family and read
4//! state through the pool's own getters, after which quoting is pure Rust. Pool families the maths
5//! library cannot price are rejected here so they never reach the router with wrong numbers.
6use std::{collections::HashMap, str::FromStr};
7
8use alloy::primitives::Address as AlloyAddress;
9use tycho_client::feed::synchronizer::ComponentWithState;
10use tycho_common::{models::token::Token, Bytes};
11
12use crate::{
13    evm::{
14        engine_db::{create_engine, SHARED_TYCHO_DB},
15        protocol::{
16            balancer_v3::{state::BalancerV3State, vm},
17            vm::utils::load_stateless_contracts,
18        },
19    },
20    protocol::{
21        errors::InvalidSnapshotError,
22        models::{DecoderContext, TryFromWithBlock},
23    },
24};
25
26impl TryFromWithBlock<ComponentWithState, tycho_client::feed::BlockHeader> for BalancerV3State {
27    type Error = InvalidSnapshotError;
28
29    /// Decodes a `vm:balancer_v3` snapshot.
30    async fn try_from_with_header(
31        value: ComponentWithState,
32        block: tycho_client::feed::BlockHeader,
33        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
34        _all_tokens: &HashMap<Bytes, Token>,
35        decoder_context: &DecoderContext,
36    ) -> Result<Self, Self::Error> {
37        let pool_address = Bytes::from_str(value.component.id.as_str()).map_err(|e| {
38            InvalidSnapshotError::ValueError(format!(
39                "expected balancer_v3 component id to be the pool address: {e}"
40            ))
41        })?;
42
43        let pool = AlloyAddress::from_slice(pool_address.as_ref());
44        // Resolved before any engine work: a component whose family this module cannot quote can
45        // only fail, so it must not cost stateless-contract fetches first.
46        let pool_type = vm::resolve_pool_type(&value.component.static_attributes, &pool)
47            .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))?;
48
49        let engine = create_engine(
50            SHARED_TYCHO_DB.clone(),
51            decoder_context
52                .vm_traces
53                .unwrap_or_default(),
54        )
55        .expect("Infallible");
56
57        // The pool's data getters read through the Vault, which delegatecalls into VaultExtension.
58        // That implementation is published as a stateless contract on the component, so its code
59        // has to be in the engine before any getter runs.
60        load_stateless_contracts(&engine, &value.state.attributes)
61            .await
62            .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))?;
63
64        // The component's token list is the pool's registration order, which its balances, rates
65        // and weights are all indexed by.
66        let tokens = value.component.tokens.clone();
67        let state = vm::read_pool_state(
68            &engine,
69            &pool,
70            pool_type,
71            &tokens,
72            &value.component.static_attributes,
73            block.timestamp,
74        )
75        .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))?;
76        // Only the weighted family registers per-token minimum balances. QuantAMM shares
77        // `WeightedMath`'s curve but not that check — it bounds swaps by its own trade-size ratio.
78        let min_token_balances = match pool_type {
79            vm::BalancerPoolType::Weighted => vm::read_weighted_min_token_balances(&engine, &pool),
80            vm::BalancerPoolType::Stable |
81            vm::BalancerPoolType::Reclamm |
82            vm::BalancerPoolType::QuantAmm => Vec::new(),
83        };
84
85        Ok(BalancerV3State::new(pool_address, tokens, min_token_balances, block.timestamp, state))
86    }
87}