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