Skip to main content

tycho_simulation/evm/
decoder.rs

1use std::{
2    collections::{hash_map::Entry, HashMap, HashSet},
3    future::Future,
4    pin::Pin,
5    sync::Arc,
6};
7
8use alloy::primitives::{Address, U256};
9use thiserror::Error;
10use tokio::sync::{watch, RwLock, RwLockReadGuard};
11use tracing::{debug, error, info, warn};
12use tycho_client::feed::{synchronizer::ComponentWithState, BlockHeader, FeedMessage, HeaderLike};
13use tycho_common::{
14    dto::{ChangeType, ProtocolStateDelta},
15    models::{blockchain::BlockAggregatedChanges, token::Token, Chain},
16    simulation::protocol_sim::{Balances, ProtocolSim},
17    Bytes,
18};
19#[cfg(test)]
20use {
21    mockall::mock,
22    num_bigint::BigUint,
23    std::any::Any,
24    tycho_common::simulation::{
25        errors::{SimulationError, TransitionError},
26        protocol_sim::GetAmountOutResult,
27    },
28};
29
30use crate::{
31    evm::{
32        engine_db::{update_engine, SHARED_TYCHO_DB},
33        override_stream::{OverrideSnapshot, StateOverrideProvider},
34        protocol::{
35            utils::bytes_to_address,
36            vm::{constants::ERC20_PROXY_BYTECODE, erc20_token::IMPLEMENTATION_SLOT},
37        },
38        tycho_models::{AccountUpdate, ResponseAccount},
39    },
40    protocol::{
41        errors::InvalidSnapshotError,
42        models::{DecoderContext, ProtocolComponent, TryFromWithBlock, Update},
43    },
44};
45
46#[derive(Error, Debug)]
47pub enum StreamDecodeError {
48    #[error("{0}")]
49    Fatal(String),
50}
51
52#[derive(Default)]
53struct DecoderState {
54    tokens: HashMap<Bytes, Token>,
55    states: HashMap<String, Box<dyn ProtocolSim>>,
56    components: HashMap<String, ProtocolComponent>,
57    // maps contract address to the pools they affect
58    contracts_map: HashMap<Bytes, HashSet<String>>,
59    // Maps original token address to their new proxy token address
60    proxy_token_addresses: HashMap<Address, Address>,
61    // Set of failed components, these are components that failed to decode and will not be emitted
62    // again TODO: handle more gracefully inside tycho-client. We could fetch the snapshot and
63    // try to decode it again.
64    failed_components: HashSet<String>,
65    // The block number of the last confirmed block decoded via `decode()`.
66    current_block_number: u64,
67}
68
69type DecodeFut =
70    Pin<Box<dyn Future<Output = Result<Box<dyn ProtocolSim>, InvalidSnapshotError>> + Send + Sync>>;
71type AccountBalances = HashMap<Bytes, HashMap<Bytes, Bytes>>;
72type RegistryFn<H> = dyn Fn(
73        ComponentWithState,
74        H,
75        AccountBalances,
76        Arc<RwLock<DecoderState>>,
77        Option<watch::Receiver<OverrideSnapshot>>,
78    ) -> DecodeFut
79    + Send
80    + Sync;
81type FilterFn = fn(&ComponentWithState) -> bool;
82
83/// A decoder to process raw messages.
84///
85/// This struct decodes incoming messages of type `FeedMessage` and converts it into the
86/// `BlockUpdate` struct.
87///
88/// # Important:
89/// - Supports registering exchanges and their associated filters for specific protocol components.
90/// - Allows the addition of client-side filters for custom conditions.
91///
92/// **Note:** Tokens provided via [`set_tokens`](Self::set_tokens) are used to decode startup
93/// snapshots and initialize protocol states. This is not an ongoing filter — components arriving
94/// after startup include their own token metadata.
95pub struct TychoStreamDecoder<H>
96where
97    H: HeaderLike,
98{
99    state: Arc<RwLock<DecoderState>>,
100    skip_state_decode_failures: bool,
101    min_token_quality: u32,
102    registry: HashMap<String, Box<RegistryFn<H>>>,
103    inclusion_filters: HashMap<String, FilterFn>,
104    /// Live override providers keyed by `protocol_system`. A pool of that protocol subscribes to
105    /// its provider at creation time and reads fresh overrides on every simulation.
106    override_providers: HashMap<String, Arc<dyn StateOverrideProvider>>,
107}
108
109impl<H> Default for TychoStreamDecoder<H>
110where
111    H: HeaderLike + Clone + Sync + Send + 'static + std::fmt::Debug,
112{
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118/// Curve migrated from the generic VM adapter (`EVMPoolState`) to the native [`CurveState`]
119/// decoder. Returns true when `vm:curve` is registered with any other type — i.e. the deprecated
120/// VM-adapter path, still supported for a few releases before removal.
121fn is_deprecated_curve_registration<T: 'static>(exchange: &str) -> bool {
122    exchange == "vm:curve" &&
123        std::any::type_name::<T>() !=
124            std::any::type_name::<crate::evm::protocol::curve::CurveState>()
125}
126
127impl<H> TychoStreamDecoder<H>
128where
129    H: HeaderLike + Clone + Sync + Send + 'static + std::fmt::Debug,
130{
131    pub fn new() -> Self {
132        Self {
133            state: Arc::new(RwLock::new(DecoderState::default())),
134            skip_state_decode_failures: false,
135            min_token_quality: 100,
136            registry: HashMap::new(),
137            inclusion_filters: HashMap::new(),
138            override_providers: HashMap::new(),
139        }
140    }
141
142    /// Registers `provider` as the live override source for `protocol_system`.
143    ///
144    /// Pools of that protocol subscribe to it at creation time, so overrides apply from the first
145    /// simulation onward. A later call for the same `protocol_system` replaces the previous
146    /// provider.
147    pub fn set_override_provider(
148        &mut self,
149        protocol_system: String,
150        provider: Arc<dyn StateOverrideProvider>,
151    ) {
152        self.override_providers
153            .insert(protocol_system, provider);
154    }
155
156    /// Provides token metadata used to decode startup snapshots and initialize protocol states.
157    ///
158    /// This is not an ongoing stream filter. Components arriving after startup include their
159    /// own token metadata for decoding.
160    pub async fn set_tokens(&self, tokens: HashMap<Bytes, Token>) {
161        let mut guard = self.state.write().await;
162        guard.tokens = tokens;
163    }
164
165    pub fn skip_state_decode_failures(&mut self, skip: bool) {
166        self.skip_state_decode_failures = skip;
167    }
168
169    /// Sets the minimum token quality for decoding.
170    ///
171    /// Tokens arriving in stream deltas below this threshold are ignored. Defaults to 100.
172    /// Set this to the same value used in [`load_all_tokens()`](crate::utils::load_all_tokens) to
173    /// apply consistent filtering.
174    pub fn min_token_quality(&mut self, quality: u32) {
175        self.min_token_quality = quality;
176    }
177
178    /// Registers a decoder for a given exchange with a decoder context.
179    ///
180    /// This method maps an exchange identifier to a specific protocol simulation type.
181    /// The associated type must implement the `TryFromWithBlock` trait to enable decoding
182    /// of state updates from `ComponentWithState` objects. This allows the decoder to transform
183    /// the component data into the appropriate protocol simulation type based on the current
184    /// blockchain state and the provided block header.
185    /// For example, to register a decoder for the `uniswap_v2` exchange with an additional decoder
186    /// context, you must call this function with
187    /// `register_decoder_with_context::<UniswapV2State>("uniswap_v2", context)`.
188    /// This ensures that the exchange ID `uniswap_v2` is properly associated with the
189    /// `UniswapV2State` decoder for use in the protocol stream.
190    pub fn register_decoder_with_context<T>(&mut self, exchange: &str, context: DecoderContext)
191    where
192        T: ProtocolSim
193            + TryFromWithBlock<ComponentWithState, H, Error = InvalidSnapshotError>
194            + Send
195            + 'static,
196    {
197        if is_deprecated_curve_registration::<T>(exchange) {
198            warn!(
199                registered_type = std::any::type_name::<T>(),
200                "Registering \"vm:curve\" with the generic VM adapter is deprecated; register the \
201                 native `CurveState` decoder instead (`exchange::<CurveState>(\"vm:curve\", ...)`). \
202                 The VM-adapter path still works but will be removed in a future release."
203            );
204        }
205        let decoder = Box::new(
206            move |component: ComponentWithState,
207                  header: H,
208                  account_balances: AccountBalances,
209                  state: Arc<RwLock<DecoderState>>,
210                  live_override: Option<watch::Receiver<OverrideSnapshot>>| {
211                let mut context = context.clone();
212                context.live_override = live_override;
213                Box::pin(async move {
214                    let guard = state.read().await;
215                    T::try_from_with_header(
216                        component,
217                        header,
218                        &account_balances,
219                        &guard.tokens,
220                        &context,
221                    )
222                    .await
223                    .map(|c| Box::new(c) as Box<dyn ProtocolSim>)
224                }) as DecodeFut
225            },
226        );
227        self.registry
228            .insert(exchange.to_string(), decoder);
229    }
230
231    /// Registers a decoder for a given exchange.
232    ///
233    /// This method maps an exchange identifier to a specific protocol simulation type.
234    /// The associated type must implement the `TryFromWithBlock` trait to enable decoding
235    /// of state updates from `ComponentWithState` objects. This allows the decoder to transform
236    /// the component data into the appropriate protocol simulation type based on the current
237    /// blockchain state and the provided block header.
238    /// For example, to register a decoder for the `uniswap_v2` exchange, you must call
239    /// this function with `register_decoder::<UniswapV2State>("uniswap_v2", vm_attributes)`.
240    /// This ensures that the exchange ID `uniswap_v2` is properly associated with the
241    /// `UniswapV2State` decoder for use in the protocol stream.
242    pub fn register_decoder<T>(&mut self, exchange: &str)
243    where
244        T: ProtocolSim
245            + TryFromWithBlock<ComponentWithState, H, Error = InvalidSnapshotError>
246            + Send
247            + 'static,
248    {
249        let context = DecoderContext::new();
250        self.register_decoder_with_context::<T>(exchange, context);
251    }
252
253    /// Registers a client-side filter function for a given exchange.
254    ///
255    /// Associates a filter function with an exchange ID, enabling custom filtering of protocol
256    /// components. The filter function is applied client-side to refine the data received from the
257    /// stream. It can be used to exclude certain components based on attributes or conditions that
258    /// are not supported by the server-side filtering logic. This is particularly useful for
259    /// implementing custom behaviors, such as:
260    /// - Filtering out pools with specific attributes (e.g., unsupported features).
261    /// - Blacklisting pools based on custom criteria.
262    /// - Excluding pools that do not meet certain requirements (e.g., token pairs or liquidity
263    ///   constraints).
264    ///
265    /// For example, you might use a filter to exclude pools that are not fully supported in the
266    /// protocol, or to ignore pools with certain attributes that are irrelevant to your
267    /// application.
268    pub fn register_filter(&mut self, exchange: &str, predicate: FilterFn) {
269        self.inclusion_filters
270            .insert(exchange.to_string(), predicate);
271    }
272
273    /// Decodes a `FeedMessage` into a `BlockUpdate` containing the updated states of protocol
274    /// components
275    pub async fn decode(&self, msg: &FeedMessage<H>) -> Result<Update, StreamDecodeError> {
276        // stores all states updated in this tick/msg
277        let mut updated_states = HashMap::new();
278        let mut new_pairs = HashMap::new();
279        let mut removed_pairs = HashMap::new();
280        let mut contracts_map = HashMap::new();
281        let mut msg_failed_components = HashSet::new();
282
283        let header = msg
284            .state_msgs
285            .values()
286            .next()
287            .ok_or_else(|| StreamDecodeError::Fatal("Missing block!".into()))?
288            .header
289            .clone();
290
291        let block_number_or_timestamp = header
292            .clone()
293            .block_number_or_timestamp();
294        let current_block = header.clone().block();
295        let is_partial = current_block
296            .as_ref()
297            .map(|h| h.partial_block_index.is_some())
298            .unwrap_or(false);
299
300        for (protocol, protocol_msg) in msg.state_msgs.iter() {
301            // Add any new tokens
302            if let Some(deltas) = protocol_msg.deltas.as_ref() {
303                let mut state_guard = self.state.write().await;
304
305                let new_tokens = deltas
306                    .new_tokens
307                    .iter()
308                    .filter(|(addr, t)| {
309                        t.quality >= self.min_token_quality &&
310                            !state_guard.tokens.contains_key(*addr)
311                    })
312                    .map(|(addr, t)| (addr.clone(), t.clone()))
313                    .collect::<HashMap<Bytes, Token>>();
314
315                if !new_tokens.is_empty() {
316                    debug!(n = new_tokens.len(), "NewTokens");
317                    state_guard.tokens.extend(new_tokens);
318                }
319            }
320
321            // Remove untracked components
322            {
323                let mut state_guard = self.state.write().await;
324                let removed_components: Vec<(String, ProtocolComponent)> = protocol_msg
325                    .removed_components
326                    .iter()
327                    .map(|(id, comp)| {
328                        if *id != comp.id {
329                            error!(
330                                "Component id mismatch in removed components {id} != {}",
331                                comp.id
332                            );
333                            return Err(StreamDecodeError::Fatal("Component id mismatch".into()));
334                        }
335
336                        let tokens = comp
337                            .tokens
338                            .iter()
339                            .flat_map(|addr| state_guard.tokens.get(addr).cloned())
340                            .collect::<Vec<_>>();
341
342                        if tokens.len() == comp.tokens.len() {
343                            Ok(Some((
344                                id.clone(),
345                                ProtocolComponent::from_with_tokens(comp.clone(), tokens),
346                            )))
347                        } else {
348                            Ok(None)
349                        }
350                    })
351                    .collect::<Result<Vec<Option<(String, ProtocolComponent)>>, StreamDecodeError>>(
352                    )?
353                    .into_iter()
354                    .flatten()
355                    .collect();
356
357                // Remove components from state and add to removed_pairs
358                for (id, component) in removed_components {
359                    state_guard.components.remove(&id);
360                    state_guard.states.remove(&id);
361                    removed_pairs.insert(id, component);
362                }
363
364                // UPDATE VM STORAGE
365                info!(
366                    "Processing {} contracts from snapshots",
367                    protocol_msg
368                        .snapshots
369                        .get_vm_storage()
370                        .len()
371                );
372
373                let mut proxy_token_accounts: HashMap<Address, AccountUpdate> = HashMap::new();
374                let mut storage_by_address: HashMap<Address, ResponseAccount> = HashMap::new();
375                for (key, value) in protocol_msg
376                    .snapshots
377                    .get_vm_storage()
378                    .iter()
379                {
380                    let account: ResponseAccount = value.clone().into();
381
382                    if state_guard.tokens.contains_key(key) {
383                        let original_address = account.address;
384                        // To work with Tycho's token overwrites system, if we get account
385                        // snapshots for a token we must handle them with a proxy/wrapper
386                        // contract.
387                        // Note: storage for the original contract must be set at the proxy
388                        // contract address. This is because the proxy contract uses
389                        // delegatecall to the original (implementation) contract.
390
391                        // Handle proxy token accounts
392                        let (impl_addr, proxy_state) = match state_guard
393                            .proxy_token_addresses
394                            .get(&original_address)
395                        {
396                            Some(impl_addr) => {
397                                // Token already has a proxy contract, simply update it.
398
399                                // Note: we apply the snapshot as an update. This is to cover the
400                                // case where a contract may be stale as it stopped being tracked
401                                // for some reason (e.g. due to a drop in tvl) and is now being
402                                // tracked again.
403                                let proxy_state = AccountUpdate::new(
404                                    original_address,
405                                    value.chain,
406                                    account.slots.clone(),
407                                    Some(account.native_balance),
408                                    None,
409                                    ChangeType::Update,
410                                );
411                                (*impl_addr, proxy_state)
412                            }
413                            None => {
414                                // Token does not have a proxy contract yet, create one
415
416                                // Assign original token contract to new address
417                                let impl_addr = generate_proxy_token_address(
418                                    state_guard.proxy_token_addresses.len() as u32,
419                                )?;
420                                state_guard
421                                    .proxy_token_addresses
422                                    .insert(original_address, impl_addr);
423
424                                // Add proxy token contract at original token address
425                                let proxy_state = create_proxy_token_account(
426                                    original_address,
427                                    Some(impl_addr),
428                                    &account.slots,
429                                    value.chain,
430                                    Some(account.native_balance),
431                                );
432
433                                (impl_addr, proxy_state)
434                            }
435                        };
436
437                        proxy_token_accounts.insert(original_address, proxy_state);
438
439                        // Assign original token contract to the implementation address
440                        let impl_update = ResponseAccount {
441                            address: impl_addr,
442                            slots: HashMap::new(),
443                            ..account.clone()
444                        };
445                        storage_by_address.insert(impl_addr, impl_update);
446                    } else {
447                        // Not a token, apply snapshot to the account at its original address
448                        storage_by_address.insert(account.address, account);
449                    }
450                }
451
452                // Split proxy accounts by change type:
453                // - Creation: new proxies that must overwrite any existing placeholder
454                // - Update: existing proxies whose storage is being refreshed (handled normally)
455                let mut proxy_creates: Vec<AccountUpdate> = Vec::new();
456                let mut proxy_updates: HashMap<Address, AccountUpdate> = HashMap::new();
457                for (addr, update) in proxy_token_accounts {
458                    if matches!(update.change, ChangeType::Creation) {
459                        proxy_creates.push(update);
460                    } else {
461                        proxy_updates.insert(addr, update);
462                    }
463                }
464
465                info!("Updating engine with {} contracts from snapshots", storage_by_address.len());
466                update_engine(
467                    SHARED_TYCHO_DB.clone(),
468                    header.clone().block(),
469                    Some(storage_by_address),
470                    proxy_updates,
471                )
472                .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
473
474                // Force-overwrite new proxy token accounts so that authoritative vm_storage data
475                // always wins over any empty placeholder previously inserted by another
476                // decoder's snapshot loop (which uses init_account / init-if-not-exists).
477                if !proxy_creates.is_empty() {
478                    SHARED_TYCHO_DB
479                        .force_update_accounts(proxy_creates)
480                        .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
481                }
482                info!("Engine updated");
483                drop(state_guard);
484            }
485
486            // Construct a contract to token balances map: HashMap<ContractAddress,
487            // HashMap<TokenAddress, Balance>>
488            let account_balances = protocol_msg
489                .clone()
490                .snapshots
491                .get_vm_storage()
492                .iter()
493                .filter_map(|(addr, acc)| {
494                    if acc.token_balances.is_empty() {
495                        return None;
496                    }
497                    let balances = acc
498                        .token_balances
499                        .iter()
500                        .map(|(token_addr, ab)| (token_addr.clone(), ab.balance.clone()))
501                        .collect::<HashMap<Bytes, Bytes>>();
502                    Some((addr.clone(), balances))
503                })
504                .collect::<AccountBalances>();
505
506            let mut new_components = HashMap::new();
507            let mut count_token_skips = 0;
508            let mut components_to_store = HashMap::new();
509            {
510                let state_guard = self.state.read().await;
511
512                // PROCESS SNAPSHOTS
513                'snapshot_loop: for (id, snapshot) in protocol_msg
514                    .snapshots
515                    .get_states()
516                    .clone()
517                {
518                    // Skip any unsupported pools
519                    if self
520                        .inclusion_filters
521                        .get(protocol.as_str())
522                        .is_some_and(|predicate| !predicate(&snapshot))
523                    {
524                        continue;
525                    }
526
527                    // Construct component from snapshot
528                    let mut component_tokens = Vec::new();
529                    let mut new_tokens_accounts = HashMap::new();
530                    for token in snapshot.component.tokens.clone() {
531                        match state_guard.tokens.get(&token) {
532                            Some(token) => {
533                                component_tokens.push(token.clone());
534
535                                // If the token is not an existing proxy token, we need to add it to
536                                // the simulation engine
537                                let token_address = match bytes_to_address(&token.address) {
538                                    Ok(addr) => addr,
539                                    Err(_) => {
540                                        count_token_skips += 1;
541                                        msg_failed_components.insert(id.clone());
542                                        warn!(
543                                            "Token address could not be decoded {}, ignoring pool {:x?}",
544                                            token.address, id
545                                        );
546                                        continue 'snapshot_loop;
547                                    }
548                                };
549                                // Deploy a proxy account without an implementation set
550                                if !state_guard
551                                    .proxy_token_addresses
552                                    .contains_key(&token_address)
553                                {
554                                    new_tokens_accounts.insert(
555                                        token_address,
556                                        create_proxy_token_account(
557                                            token_address,
558                                            None,
559                                            &HashMap::new(),
560                                            snapshot.component.chain,
561                                            None,
562                                        ),
563                                    );
564                                }
565                            }
566                            None => {
567                                count_token_skips += 1;
568                                msg_failed_components.insert(id.clone());
569                                debug!("Token not found {}, ignoring pool {:x?}", token, id);
570                                continue 'snapshot_loop;
571                            }
572                        }
573                    }
574                    let component = ProtocolComponent::from_with_tokens(
575                        snapshot.component.clone(),
576                        component_tokens,
577                    );
578
579                    // Add new tokens to the simulation engine
580                    if !new_tokens_accounts.is_empty() {
581                        update_engine(
582                            SHARED_TYCHO_DB.clone(),
583                            header.clone().block(),
584                            None,
585                            new_tokens_accounts,
586                        )
587                        .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
588                    }
589
590                    // collect contracts:ids mapping for states that should update on contract
591                    // changes (non-manual updates)
592                    if !component
593                        .static_attributes
594                        .contains_key("manual_updates")
595                    {
596                        for contract in &component.contract_ids {
597                            contracts_map
598                                .entry(contract.clone())
599                                .or_insert_with(HashSet::new)
600                                .insert(id.clone());
601                        }
602                        // Add DCI contracts so changes to these contracts trigger
603                        // an update
604                        for (_, tracing) in snapshot.entrypoints.iter() {
605                            for contract in tracing.accessed_slots.keys().cloned() {
606                                contracts_map
607                                    .entry(contract)
608                                    .or_insert_with(HashSet::new)
609                                    .insert(id.clone());
610                            }
611                        }
612                    }
613
614                    // Collect new pairs (components)
615                    new_pairs.insert(id.clone(), component.clone());
616
617                    // Store component for later batch insertion
618                    components_to_store.insert(id.clone(), component);
619
620                    // Construct state from snapshot
621                    if let Some(state_decode_f) = self.registry.get(protocol.as_str()) {
622                        let live_override = self
623                            .override_providers
624                            .get(protocol.as_str())
625                            .and_then(|provider| provider.subscribe(protocol.as_str()));
626                        match state_decode_f(
627                            snapshot,
628                            header.clone(),
629                            account_balances.clone(),
630                            self.state.clone(),
631                            live_override,
632                        )
633                        .await
634                        {
635                            Ok(state) => {
636                                new_components.insert(id.clone(), state);
637                            }
638                            Err(e) => {
639                                if self.skip_state_decode_failures {
640                                    warn!(pool = id, error = %e, "StateDecodingFailure");
641                                    msg_failed_components.insert(id.clone());
642                                    continue 'snapshot_loop;
643                                } else {
644                                    error!(pool = id, error = %e, "StateDecodingFailure");
645                                    return Err(StreamDecodeError::Fatal(format!("{e}")));
646                                }
647                            }
648                        }
649                    } else if self.skip_state_decode_failures {
650                        warn!(pool = id, "MissingDecoderRegistration");
651                        msg_failed_components.insert(id.clone());
652                        continue 'snapshot_loop;
653                    } else {
654                        error!(pool = id, "MissingDecoderRegistration");
655                        return Err(StreamDecodeError::Fatal(format!(
656                            "Missing decoder registration for: {id}"
657                        )));
658                    }
659                }
660            }
661
662            // Batch insert components into state
663            if !components_to_store.is_empty() {
664                let mut state_guard = self.state.write().await;
665                for (id, component) in components_to_store {
666                    state_guard
667                        .components
668                        .insert(id, component);
669                }
670            }
671
672            if !protocol_msg.snapshots.states.is_empty() {
673                info!("Decoded {} snapshots for protocol {protocol}", new_components.len());
674            }
675            if count_token_skips > 0 {
676                info!("Skipped {count_token_skips} pools due to missing tokens");
677            }
678
679            //TODO: should we remove failed components for new_components?
680            updated_states.extend(new_components);
681
682            // PROCESS DELTAS
683            if let Some(deltas) = protocol_msg.deltas.clone() {
684                // Update engine with account changes
685                let mut state_guard = self.state.write().await;
686
687                let mut account_update_by_address: HashMap<Address, AccountUpdate> = HashMap::new();
688                // New proxy token accounts that must overwrite any existing placeholder.
689                let mut new_proxy_accounts: Vec<AccountUpdate> = Vec::new();
690                for (key, value) in deltas.account_deltas.iter() {
691                    let mut update: AccountUpdate = value.clone().into();
692
693                    // TEMP PATCH (ENG-4993)
694                    //
695                    // The indexer may emit Creation deltas with no code for EOA addresses.
696                    // Treat them as EOAs (empty code) rather than downgrading to Update, which
697                    // would skip init_account and cause "uninitialized account" warnings.
698                    if update.code.is_none() && matches!(update.change, ChangeType::Creation) {
699                        error!(
700                            update = ?update,
701                            "FaultyCreationDelta"
702                        );
703                        update.code = Some(vec![]);
704                    }
705
706                    if state_guard.tokens.contains_key(key) {
707                        let original_address = update.address;
708                        // If the account is a token, we need to handle it with a proxy contract.
709                        // Storage updates apply to the proxy contract (at original address).
710                        // Code updates (if any) apply to the token implementation contract (at
711                        // impl_addr).
712
713                        // Handle proxy contract updates
714                        let impl_addr = match state_guard
715                            .proxy_token_addresses
716                            .get(&original_address)
717                        {
718                            Some(impl_addr) => {
719                                // Token already has a proxy contract.
720
721                                // Apply the storage update to proxy contract
722                                let proxy_update = AccountUpdate { code: None, ..update.clone() };
723                                account_update_by_address.insert(original_address, proxy_update);
724
725                                *impl_addr
726                            }
727                            None => {
728                                // Token does not have a proxy contract yet, create one
729
730                                // Assign original token (implementation) contract to new proxy
731                                // address
732                                let impl_addr = generate_proxy_token_address(
733                                    state_guard.proxy_token_addresses.len() as u32,
734                                )?;
735                                state_guard
736                                    .proxy_token_addresses
737                                    .insert(original_address, impl_addr);
738
739                                // Create proxy token account with original account's storage (at
740                                // original address). Track it separately so it can be
741                                // force-overwritten and win over any placeholder that another
742                                // decoder's snapshot loop may have written earlier.
743                                let proxy_state = create_proxy_token_account(
744                                    original_address,
745                                    Some(impl_addr),
746                                    &update.slots,
747                                    update.chain,
748                                    update.balance,
749                                );
750                                new_proxy_accounts.push(proxy_state);
751
752                                impl_addr
753                            }
754                        };
755
756                        // Apply code update to token implementation contract
757                        if update.code.is_some() {
758                            let impl_update = AccountUpdate {
759                                address: impl_addr,
760                                slots: HashMap::new(),
761                                ..update.clone()
762                            };
763                            account_update_by_address.insert(impl_addr, impl_update);
764                        }
765                    } else {
766                        // Not a token, apply update to the account at its original address
767                        account_update_by_address.insert(update.address, update);
768                    }
769                }
770                drop(state_guard);
771
772                let state_guard = self.state.read().await;
773                info!("Updating engine with {} contract deltas", deltas.account_deltas.len());
774                update_engine(
775                    SHARED_TYCHO_DB.clone(),
776                    header.clone().block(),
777                    None,
778                    account_update_by_address,
779                )
780                .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
781
782                // Force-overwrite any newly-created proxy token accounts so they always win
783                // over placeholder entries inserted by other decoders' snapshot loops.
784                if !new_proxy_accounts.is_empty() {
785                    SHARED_TYCHO_DB
786                        .force_update_accounts(new_proxy_accounts)
787                        .map_err(|e| StreamDecodeError::Fatal(e.to_string()))?;
788                }
789                info!("Engine updated");
790
791                // Collect all pools related to the updated accounts
792                let mut pools_to_update = HashSet::new();
793                for (account, _update) in deltas.account_deltas {
794                    // get new pools related to the account updated
795                    pools_to_update.extend(
796                        contracts_map
797                            .get(&account)
798                            .cloned()
799                            .unwrap_or_default(),
800                    );
801                    // get existing pools related to the account updated
802                    pools_to_update.extend(
803                        state_guard
804                            .contracts_map
805                            .get(&account)
806                            .cloned()
807                            .unwrap_or_default(),
808                    );
809                }
810
811                // Collect all balance changes this block
812                let all_balances = Balances {
813                    component_balances: deltas
814                        .component_balances
815                        .iter()
816                        .map(|(pool_id, bals)| {
817                            let mut balances = HashMap::new();
818                            for (t, b) in bals {
819                                balances.insert(t.clone(), b.balance.clone());
820                            }
821                            pools_to_update.insert(pool_id.clone());
822                            (pool_id.clone(), balances)
823                        })
824                        .collect(),
825                    account_balances: deltas
826                        .account_balances
827                        .iter()
828                        .map(|(account, bals)| {
829                            let mut balances = HashMap::new();
830                            for (t, b) in bals {
831                                balances.insert(t.clone(), b.balance.clone());
832                            }
833                            pools_to_update.extend(
834                                contracts_map
835                                    .get(account)
836                                    .cloned()
837                                    .unwrap_or_default(),
838                            );
839                            (account.clone(), balances)
840                        })
841                        .collect(),
842                };
843
844                // update states with protocol state deltas (attribute changes etc.)
845                for (id, update) in deltas.state_deltas {
846                    // TODO: is this needed?
847                    let update_with_block = Self::add_block_info_to_delta(
848                        ProtocolStateDelta::from(update),
849                        current_block.clone(),
850                    );
851                    match Self::apply_update(
852                        &id,
853                        update_with_block,
854                        &mut updated_states,
855                        &state_guard,
856                        &all_balances,
857                    ) {
858                        Ok(_) => {
859                            pools_to_update.remove(&id);
860                        }
861                        Err(e) => {
862                            if self.skip_state_decode_failures {
863                                warn!(pool = id, error = %e, "Failed to apply state update, marking component as removed");
864                                // Remove from updated_states if it was there
865                                updated_states.remove(&id);
866                                // Try to get component from new_pairs first, then from state
867                                if let Some(component) = new_pairs.remove(&id) {
868                                    removed_pairs.insert(id.clone(), component);
869                                } else if let Some(component) = state_guard.components.get(&id) {
870                                    removed_pairs.insert(id.clone(), component.clone());
871                                } else {
872                                    // Component not found in new_pairs or state, this shouldn't
873                                    // happen
874                                    warn!(pool = id, "Component not found in new_pairs or state, cannot add to removed_pairs");
875                                }
876                                pools_to_update.remove(&id);
877
878                                // Add to failed components
879                                msg_failed_components.insert(id.clone());
880                            } else {
881                                return Err(e);
882                            }
883                        }
884                    }
885                }
886
887                // update remaining pools linked to updated contracts/updated balances
888                for pool in pools_to_update {
889                    // TODO: is this needed?
890                    let default_delta_with_block = Self::add_block_info_to_delta(
891                        ProtocolStateDelta::default(),
892                        current_block.clone(),
893                    );
894                    match Self::apply_update(
895                        &pool,
896                        default_delta_with_block,
897                        &mut updated_states,
898                        &state_guard,
899                        &all_balances,
900                    ) {
901                        Ok(_) => {}
902                        Err(e) => {
903                            if self.skip_state_decode_failures {
904                                warn!(pool = pool, error = %e, "Failed to apply contract/balance update, marking component as removed");
905                                // Remove from updated_states if it was there
906                                updated_states.remove(&pool);
907                                // Try to get component from new_pairs first, then from state
908                                if let Some(component) = new_pairs.remove(&pool) {
909                                    removed_pairs.insert(pool.clone(), component);
910                                } else if let Some(component) = state_guard.components.get(&pool) {
911                                    removed_pairs.insert(pool.clone(), component.clone());
912                                } else {
913                                    // Component not found in new_pairs or state, this shouldn't
914                                    // happen
915                                    warn!(pool = pool, "Component not found in new_pairs or state, cannot add to removed_pairs");
916                                }
917
918                                // Add to failed components
919                                msg_failed_components.insert(pool.clone());
920                            } else {
921                                return Err(e);
922                            }
923                        }
924                    }
925                }
926            };
927        }
928
929        // Persist the newly added/updated states
930        let mut state_guard = self.state.write().await;
931
932        // Update failed components with any new ones
933        state_guard
934            .failed_components
935            .extend(msg_failed_components);
936
937        // Remove any failed components from Updates
938        // Perf: we could do it directly in the decoder logic to avoid some steps, but this logic is
939        // complex and this is more robust.
940        updated_states.retain(|id, _| {
941            !state_guard
942                .failed_components
943                .contains(id)
944        });
945        new_pairs.retain(|id, _| {
946            !state_guard
947                .failed_components
948                .contains(id)
949        });
950
951        state_guard
952            .states
953            .extend(updated_states.clone());
954
955        state_guard.current_block_number = block_number_or_timestamp;
956
957        // Add new components to persistent state
958        for (id, component) in new_pairs.iter() {
959            state_guard
960                .components
961                .insert(id.clone(), component.clone());
962        }
963
964        // Remove components from persistent state
965        for id in removed_pairs.keys() {
966            state_guard.components.remove(id);
967        }
968
969        for (key, values) in contracts_map {
970            state_guard
971                .contracts_map
972                .entry(key)
973                .or_insert_with(HashSet::new)
974                .extend(values);
975        }
976
977        // Send the tick with all updated states
978        Ok(Update::new(block_number_or_timestamp, updated_states, new_pairs)
979            .set_is_partial(is_partial)
980            .set_removed_pairs(removed_pairs)
981            .set_sync_states(msg.sync_states.clone()))
982    }
983
984    /// Applies pending deltas from one or more `TxDeltaIndexer`s against the current confirmed
985    /// state and returns an ephemeral `Update`.
986    ///
987    /// This is the read-only counterpart of `decode()`. It clones pool states, applies the
988    /// supplied `pending_deltas`, and returns the result — **without writing back** to
989    /// `DecoderState`. Calling this method twice with the same input produces identical results.
990    ///
991    /// Only native protocols are supported. VM protocols (extractor prefix `"vm:"`) are rejected
992    /// at registration time in
993    /// [`with_pending_indexer`](crate::evm::stream::ProtocolStreamBuilder::with_pending_indexer).
994    ///
995    /// # Parameters
996    /// * `pending_deltas` — map from extractor name to the `BlockAggregatedChanges` produced by the
997    ///   corresponding `TxDeltaIndexer::generate_deltas()` call.
998    /// * `header` — the target block header. Its `block_number_or_timestamp()` is stamped on the
999    ///   returned [`Update`]; its `block_number` and `block_timestamp` are injected into each state
1000    ///   delta so that protocols relying on block context (e.g. aerodrome slipstreams, etherfi)
1001    ///   receive correct values.
1002    pub async fn apply_deltas_ephemeral(
1003        &self,
1004        pending_deltas: &HashMap<String, BlockAggregatedChanges>,
1005        header: H,
1006    ) -> Result<Update, StreamDecodeError> {
1007        let block_number_or_timestamp = header
1008            .clone()
1009            .block_number_or_timestamp();
1010        let current_block = header.block();
1011        let state_guard = self.state.read().await;
1012
1013        let mut updated_states: HashMap<String, Box<dyn ProtocolSim>> = HashMap::new();
1014
1015        for deltas in pending_deltas.values() {
1016            let all_balances = Balances {
1017                component_balances: deltas
1018                    .component_balances
1019                    .iter()
1020                    .map(|(pool_id, bals)| {
1021                        let balances = bals
1022                            .iter()
1023                            .map(|(t, b)| (t.clone(), b.balance.clone()))
1024                            .collect();
1025                        (pool_id.clone(), balances)
1026                    })
1027                    .collect(),
1028                account_balances: HashMap::new(),
1029            };
1030
1031            for (id, state_delta) in &deltas.state_deltas {
1032                let dto_delta = Self::add_block_info_to_delta(
1033                    ProtocolStateDelta::from(state_delta.clone()),
1034                    current_block.clone(),
1035                );
1036                if let Err(e) = Self::apply_update(
1037                    id,
1038                    dto_delta,
1039                    &mut updated_states,
1040                    &state_guard,
1041                    &all_balances,
1042                ) {
1043                    warn!(pool = id, error = %e, "EphemeralDeltaTransitionError");
1044                }
1045            }
1046        }
1047
1048        Ok(Update::new(block_number_or_timestamp, updated_states, HashMap::new()))
1049    }
1050
1051    /// Add current block information (number and timestamp) to a ProtocolStateDelta.
1052    fn add_block_info_to_delta(
1053        mut delta: ProtocolStateDelta,
1054        block_header_opt: Option<BlockHeader>,
1055    ) -> ProtocolStateDelta {
1056        if let Some(header) = block_header_opt {
1057            // Add block_number and block_timestamp attributes to ensure pool states
1058            // receive current block information during delta_transition
1059            delta.updated_attributes.insert(
1060                "block_number".to_string(),
1061                Bytes::from(header.number.to_be_bytes().to_vec()),
1062            );
1063            delta.updated_attributes.insert(
1064                "block_timestamp".to_string(),
1065                Bytes::from(header.timestamp.to_be_bytes().to_vec()),
1066            );
1067        }
1068        delta
1069    }
1070
1071    fn apply_update(
1072        id: &String,
1073        update: ProtocolStateDelta,
1074        updated_states: &mut HashMap<String, Box<dyn ProtocolSim>>,
1075        state_guard: &RwLockReadGuard<'_, DecoderState>,
1076        all_balances: &Balances,
1077    ) -> Result<(), StreamDecodeError> {
1078        match updated_states.entry(id.clone()) {
1079            Entry::Occupied(mut entry) => {
1080                // If state exists in updated_states, apply the delta to it
1081                let state: &mut Box<dyn ProtocolSim> = entry.get_mut();
1082                state
1083                    .delta_transition(update, &state_guard.tokens, all_balances)
1084                    .map_err(|e| {
1085                        error!(pool = id, error = ?e, "DeltaTransitionError");
1086                        StreamDecodeError::Fatal(format!("TransitionFailure: {e:?}"))
1087                    })?;
1088            }
1089            Entry::Vacant(_) => {
1090                match state_guard.states.get(id) {
1091                    // If state does not exist in updated_states, apply the delta to the stored
1092                    // state
1093                    Some(stored_state) => {
1094                        let mut state = stored_state.clone();
1095                        state
1096                            .delta_transition(update, &state_guard.tokens, all_balances)
1097                            .map_err(|e| {
1098                                error!(pool = id, error = ?e, "DeltaTransitionError");
1099                                StreamDecodeError::Fatal(format!("TransitionFailure: {e:?}"))
1100                            })?;
1101                        updated_states.insert(id.clone(), state);
1102                    }
1103                    None => debug!(pool = id, reason = "MissingState", "DeltaTransitionError"),
1104                }
1105            }
1106        }
1107        Ok(())
1108    }
1109}
1110
1111/// Generate a proxy token address for a given token index
1112fn generate_proxy_token_address(idx: u32) -> Result<Address, StreamDecodeError> {
1113    let padded_idx = format!("{idx:x}");
1114    let padded_zeroes = "0".repeat(33 - padded_idx.len());
1115    let proxy_token_address = format!("{padded_zeroes}{padded_idx}BAdbaBe");
1116    let decoded = hex::decode(proxy_token_address).map_err(|e| {
1117        StreamDecodeError::Fatal(format!("Invalid proxy token address encoding: {e}"))
1118    })?;
1119
1120    const ADDRESS_LENGTH: usize = 20;
1121    if decoded.len() != ADDRESS_LENGTH {
1122        return Err(StreamDecodeError::Fatal(format!(
1123            "Invalid proxy token address length: expected {}, got {}",
1124            ADDRESS_LENGTH,
1125            decoded.len(),
1126        )));
1127    }
1128
1129    Ok(Address::from_slice(&decoded))
1130}
1131
1132/// Create a proxy token account for a token at a given address
1133///
1134/// The proxy token account is created at the original token address and points to the new token
1135/// address.
1136fn create_proxy_token_account(
1137    addr: Address,
1138    new_address: Option<Address>,
1139    storage: &HashMap<U256, U256>,
1140    chain: Chain,
1141    balance: Option<U256>,
1142) -> AccountUpdate {
1143    let mut slots = storage.clone();
1144    if let Some(new_address) = new_address {
1145        slots.insert(*IMPLEMENTATION_SLOT, U256::from_be_slice(new_address.as_slice()));
1146    }
1147
1148    AccountUpdate {
1149        address: addr,
1150        chain,
1151        slots,
1152        balance,
1153        code: Some(ERC20_PROXY_BYTECODE.to_vec()),
1154        change: ChangeType::Creation,
1155    }
1156}
1157
1158#[cfg(test)]
1159mock! {
1160    #[derive(Debug)]
1161    pub ProtocolSim {
1162        pub fn fee(&self) -> f64;
1163        pub fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError>;
1164        pub fn get_amount_out(
1165            &self,
1166            amount_in: BigUint,
1167            token_in: &Token,
1168            token_out: &Token,
1169        ) -> Result<GetAmountOutResult, SimulationError>;
1170        pub fn get_limits(
1171            &self,
1172            sell_token: Bytes,
1173            buy_token: Bytes,
1174        ) -> Result<(BigUint, BigUint), SimulationError>;
1175        pub fn delta_transition(
1176            &mut self,
1177            delta: ProtocolStateDelta,
1178            tokens: &HashMap<Bytes, Token>,
1179            balances: &Balances,
1180        ) -> Result<(), TransitionError>;
1181        pub fn clone_box(&self) -> Box<dyn ProtocolSim>;
1182        pub fn eq(&self, other: &dyn ProtocolSim) -> bool;
1183    }
1184}
1185
1186#[cfg(test)]
1187crate::impl_non_serializable_protocol!(MockProtocolSim, "test protocol");
1188
1189#[cfg(test)]
1190impl ProtocolSim for MockProtocolSim {
1191    fn fee(&self) -> f64 {
1192        self.fee()
1193    }
1194
1195    fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
1196        self.spot_price(base, quote)
1197    }
1198
1199    fn get_amount_out(
1200        &self,
1201        amount_in: BigUint,
1202        token_in: &Token,
1203        token_out: &Token,
1204    ) -> Result<GetAmountOutResult, SimulationError> {
1205        self.get_amount_out(amount_in, token_in, token_out)
1206    }
1207
1208    fn get_limits(
1209        &self,
1210        sell_token: Bytes,
1211        buy_token: Bytes,
1212    ) -> Result<(BigUint, BigUint), SimulationError> {
1213        self.get_limits(sell_token, buy_token)
1214    }
1215
1216    fn delta_transition(
1217        &mut self,
1218        delta: ProtocolStateDelta,
1219        tokens: &HashMap<Bytes, Token>,
1220        balances: &Balances,
1221    ) -> Result<(), TransitionError> {
1222        self.delta_transition(delta, tokens, balances)
1223    }
1224
1225    fn clone_box(&self) -> Box<dyn ProtocolSim> {
1226        self.clone_box()
1227    }
1228
1229    fn as_any(&self) -> &dyn Any {
1230        panic!("MockProtocolSim does not support as_any")
1231    }
1232
1233    fn as_any_mut(&mut self) -> &mut dyn Any {
1234        panic!("MockProtocolSim does not support as_any_mut")
1235    }
1236
1237    fn eq(&self, other: &dyn ProtocolSim) -> bool {
1238        self.eq(other)
1239    }
1240
1241    fn typetag_name(&self) -> &'static str {
1242        unreachable!()
1243    }
1244
1245    fn typetag_deserialize(&self) {
1246        unreachable!()
1247    }
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252    use std::str::FromStr;
1253
1254    use alloy::primitives::address;
1255    use mockall::predicate::*;
1256    use rstest::*;
1257    use tycho_client::feed::BlockHeader;
1258    use tycho_common::{models::Chain, Bytes};
1259
1260    use super::*;
1261    use crate::evm::protocol::{curve::CurveState, uniswap_v2::state::UniswapV2State};
1262
1263    #[test]
1264    fn curve_vm_adapter_registration_flagged_deprecated() {
1265        // The native decoder is the supported path — not flagged.
1266        assert!(!is_deprecated_curve_registration::<CurveState>("vm:curve"));
1267        // Any other type for vm:curve is the deprecated VM-adapter path.
1268        assert!(is_deprecated_curve_registration::<UniswapV2State>("vm:curve"));
1269        // Other exchanges are unaffected.
1270        assert!(!is_deprecated_curve_registration::<UniswapV2State>("uniswap_v2"));
1271    }
1272
1273    async fn setup_decoder(set_tokens: bool) -> TychoStreamDecoder<BlockHeader> {
1274        let mut decoder = TychoStreamDecoder::new();
1275        decoder.register_decoder::<UniswapV2State>("uniswap_v2");
1276        if set_tokens {
1277            let tokens = [
1278                Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").lpad(20, 0),
1279                Bytes::from("0xdac17f958d2ee523a2206206994597c13d831ec7").lpad(20, 0),
1280            ]
1281            .iter()
1282            .map(|addr| {
1283                let addr_str = format!("{addr:x}");
1284                (
1285                    addr.clone(),
1286                    Token::new(addr, &addr_str, 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1287                )
1288            })
1289            .collect();
1290            decoder.set_tokens(tokens).await;
1291        }
1292        decoder
1293    }
1294
1295    fn load_test_msg(name: &str) -> FeedMessage<BlockHeader> {
1296        use std::{fs, path::Path};
1297
1298        use tycho_client::feed::dto;
1299        let project_root = env!("CARGO_MANIFEST_DIR");
1300        let asset_path = Path::new(project_root).join(format!("tests/assets/decoder/{name}.json"));
1301        let json_data = fs::read_to_string(asset_path).expect("Failed to read test asset");
1302        let feed_msg: dto::FeedMessage<BlockHeader> =
1303            serde_json::from_str(&json_data).expect("Failed to deserialize FeedMsg json!");
1304        FeedMessage::from(feed_msg)
1305    }
1306
1307    #[tokio::test]
1308    async fn test_decode() {
1309        let decoder = setup_decoder(true).await;
1310
1311        let msg = load_test_msg("uniswap_v2_snapshot");
1312        let res1 = decoder
1313            .decode(&msg)
1314            .await
1315            .expect("decode failure");
1316        let msg = load_test_msg("uniswap_v2_delta");
1317        let res2 = decoder
1318            .decode(&msg)
1319            .await
1320            .expect("decode failure");
1321
1322        assert_eq!(res1.states.len(), 1);
1323        assert_eq!(res2.states.len(), 1);
1324        assert_eq!(res1.sync_states.len(), 1);
1325        assert_eq!(res2.sync_states.len(), 1);
1326    }
1327
1328    #[tokio::test]
1329    async fn test_decode_component_missing_token() {
1330        let decoder = setup_decoder(false).await;
1331        let tokens = [Bytes::from("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").lpad(20, 0)]
1332            .iter()
1333            .map(|addr| {
1334                let addr_str = format!("{addr:x}");
1335                (
1336                    addr.clone(),
1337                    Token::new(addr, &addr_str, 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1338                )
1339            })
1340            .collect();
1341        decoder.set_tokens(tokens).await;
1342
1343        let msg = load_test_msg("uniswap_v2_snapshot");
1344        let res1 = decoder
1345            .decode(&msg)
1346            .await
1347            .expect("decode failure");
1348
1349        assert_eq!(res1.states.len(), 0);
1350    }
1351
1352    #[tokio::test]
1353    async fn test_decode_component_bad_id() {
1354        let decoder = setup_decoder(true).await;
1355        let msg = load_test_msg("uniswap_v2_snapshot_broken_id");
1356
1357        match decoder.decode(&msg).await {
1358            Err(StreamDecodeError::Fatal(msg)) => {
1359                assert_eq!(msg, "Component id mismatch");
1360            }
1361            Ok(_) => {
1362                panic!("Expected failures to be raised")
1363            }
1364        }
1365    }
1366
1367    #[rstest]
1368    #[case(true)]
1369    #[case(false)]
1370    #[tokio::test]
1371    async fn test_decode_component_bad_state(#[case] skip_failures: bool) {
1372        let mut decoder = setup_decoder(true).await;
1373        decoder.skip_state_decode_failures = skip_failures;
1374
1375        let msg = load_test_msg("uniswap_v2_snapshot_broken_state");
1376        match decoder.decode(&msg).await {
1377            Err(StreamDecodeError::Fatal(msg)) => {
1378                if !skip_failures {
1379                    assert_eq!(msg, "Missing attributes reserve0");
1380                } else {
1381                    panic!("Expected failures to be ignored. Err: {msg}")
1382                }
1383            }
1384            Ok(res) => {
1385                if !skip_failures {
1386                    panic!("Expected failures to be raised")
1387                } else {
1388                    assert_eq!(res.states.len(), 0);
1389                }
1390            }
1391        }
1392    }
1393
1394    #[tokio::test]
1395    async fn test_decode_updates_state_on_contract_change() {
1396        let decoder = setup_decoder(true).await;
1397
1398        // Create the mock instances
1399        let mut mock_state = MockProtocolSim::new();
1400
1401        mock_state
1402            .expect_clone_box()
1403            .times(1)
1404            .returning(|| {
1405                let mut cloned_mock_state = MockProtocolSim::new();
1406                // Expect `delta_transition` to be called once with any parameters
1407                cloned_mock_state
1408                    .expect_delta_transition()
1409                    .times(1)
1410                    .returning(|_, _, _| Ok(()));
1411                cloned_mock_state
1412                    .expect_clone_box()
1413                    .times(1)
1414                    .returning(|| Box::new(MockProtocolSim::new()));
1415                Box::new(cloned_mock_state)
1416            });
1417
1418        // Insert mock state into `updated_states`
1419        let pool_id =
1420            "0x93d199263632a4ef4bb438f1feb99e57b4b5f0bd0000000000000000000005c2".to_string();
1421        decoder
1422            .state
1423            .write()
1424            .await
1425            .states
1426            .insert(pool_id.clone(), Box::new(mock_state) as Box<dyn ProtocolSim>);
1427        decoder
1428            .state
1429            .write()
1430            .await
1431            .contracts_map
1432            .insert(
1433                Bytes::from("0xba12222222228d8ba445958a75a0704d566bf2c8").lpad(20, 0),
1434                HashSet::from([pool_id.clone()]),
1435            );
1436
1437        // Load a test message containing a contract update
1438        let msg = load_test_msg("balancer_v2_delta");
1439
1440        // Decode the message
1441        let _ = decoder
1442            .decode(&msg)
1443            .await
1444            .expect("decode failure");
1445
1446        // The mock framework will assert that `delta_transition` was called exactly once
1447    }
1448
1449    #[test]
1450    fn test_generate_proxy_token_address() {
1451        let idx = 1;
1452        let generated_address =
1453            generate_proxy_token_address(idx).expect("proxy token address should be valid");
1454        assert_eq!(generated_address, address!("000000000000000000000000000000001badbabe"));
1455
1456        let idx = 123456;
1457        let generated_address =
1458            generate_proxy_token_address(idx).expect("proxy token address should be valid");
1459        assert_eq!(generated_address, address!("00000000000000000000000000001e240badbabe"));
1460    }
1461
1462    #[tokio::test(flavor = "multi_thread")]
1463    async fn test_euler_hook_low_pool_manager_balance() {
1464        let mut decoder = TychoStreamDecoder::new();
1465
1466        decoder.register_decoder_with_context::<crate::evm::protocol::uniswap_v4::state::UniswapV4State>(
1467            "uniswap_v4_hooks", DecoderContext::new().vm_traces(true)
1468        );
1469
1470        let weth = Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap();
1471        let teth = Bytes::from_str("0xd11c452fc99cf405034ee446803b6f6c1f6d5ed8").unwrap();
1472        let tokens = HashMap::from([
1473            (
1474                weth.clone(),
1475                Token::new(&weth, "WETH", 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1476            ),
1477            (
1478                teth.clone(),
1479                Token::new(&teth, "tETH", 18, 100, &[Some(100_000)], Chain::Ethereum, 100),
1480            ),
1481        ]);
1482
1483        decoder.set_tokens(tokens.clone()).await;
1484
1485        let msg = load_test_msg("euler_hook_snapshot");
1486        let res = decoder
1487            .decode(&msg)
1488            .await
1489            .expect("decode failure");
1490
1491        let pool_state = res
1492            .states
1493            .get("0xc70d7fbd7fcccdf726e02fed78548b40dc52502b097c7a1ee7d995f4d4396134")
1494            .expect("Couldn't find target pool");
1495        let amount_out = pool_state
1496            .get_amount_out(
1497                BigUint::from_str("1000000000000000000").unwrap(),
1498                tokens.get(&teth).unwrap(),
1499                tokens.get(&weth).unwrap(),
1500            )
1501            .expect("Get amount out failed");
1502
1503        assert_eq!(amount_out.amount, BigUint::from_str("1216190190361759119").unwrap());
1504    }
1505}