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