tycho_common/
traits.rs

1use core::fmt::Debug;
2use std::{collections::HashMap, sync::Arc};
3
4use async_trait::async_trait;
5
6use crate::{
7    models::{
8        blockchain::{Block, BlockTag, EntryPointWithTracingParams, TracedEntryPoint},
9        contract::AccountDelta,
10        token::{Token, TokenQuality, TransferCost, TransferTax},
11        Address, Balance, BlockHash, StoreKey,
12    },
13    Bytes,
14};
15
16/// A struct representing a request to get an account state.
17#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18pub struct StorageSnapshotRequest {
19    // The address of the account to get the state of.
20    pub address: Address,
21    // The specific slots to get the state of. If `None`, the entire account state will be
22    // returned.
23    pub slots: Option<Vec<StoreKey>>,
24}
25
26impl std::fmt::Display for StorageSnapshotRequest {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        let address_str = self.address.to_string();
29        let truncated_address = if address_str.len() >= 10 {
30            format!("{}...{}", &address_str[0..8], &address_str[address_str.len() - 4..])
31        } else {
32            address_str
33        };
34
35        match &self.slots {
36            Some(slots) => write!(f, "{truncated_address}[{} slots]", slots.len()),
37            None => write!(f, "{truncated_address}[all slots]"),
38        }
39    }
40}
41
42/// Trait for getting multiple account states from chain data.
43#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
44#[async_trait]
45pub trait AccountExtractor {
46    type Error: Debug + Send + Sync;
47
48    /// Get the account states at the end of the given block (after all transactions in the block
49    /// have been applied).
50    ///
51    /// # Arguments
52    ///
53    /// * `block`: The block at which to retrieve the account states.
54    /// * `requests`: A slice of `StorageSnapshotRequest` objects, each containing an address and
55    ///   optional slots.
56    /// Note: If the `slots` field is `None`, the function will return the entire account state.
57    /// That could be a lot of data, so use with caution.
58    ///
59    /// returns: Result<HashMap<Bytes, AccountDelta, RandomState>, Self::Error>
60    /// A result containing a HashMap where the keys are `Bytes` (addresses) and the values are
61    /// `AccountDelta` objects.
62    async fn get_accounts_at_block(
63        &self,
64        block: &Block,
65        requests: &[StorageSnapshotRequest],
66    ) -> Result<HashMap<Bytes, AccountDelta>, Self::Error>; //TODO: do not return `AccountUpdate` but `Account`
67}
68
69/// Trait for analyzing a token, including its quality, transfer cost, and transfer tax.
70#[async_trait]
71pub trait TokenAnalyzer: Send + Sync {
72    type Error;
73
74    /// Analyzes the quality of a token given its address and a block tag.
75    ///
76    /// # Parameters
77    /// * `token` - The address of the token to analyze.
78    /// * `block` - The block tag at which the analysis should be performed.
79    ///
80    /// # Returns
81    /// A result containing:
82    /// * `TokenQuality` - The quality assessment of the token (either `Good` or `Bad`).
83    /// * `Option<TransferCost>` - The average cost per transfer, if available.
84    /// * `Option<TransferTax>` - The transfer tax, if applicable.
85    ///
86    /// On failure, returns `Self::Error`.
87    async fn analyze(
88        &self,
89        token: Bytes,
90        block: BlockTag,
91    ) -> Result<(TokenQuality, Option<TransferCost>, Option<TransferTax>), Self::Error>;
92}
93
94/// Trait for finding an address that owns a specific token. This is useful for detecting
95/// bad tokens by identifying addresses with enough balance to simulate transactions.
96#[async_trait]
97pub trait TokenOwnerFinding: Send + Sync + Debug {
98    /// Finds an address that holds at least `min_balance` of the specified token.
99    ///
100    /// # Parameters
101    /// * `token` - The address of the token to search for.
102    /// * `min_balance` - The minimum balance required for the address to be considered.
103    ///
104    /// # Returns
105    /// A result containing:
106    /// * `Option<(Address, Balance)>` - The address and its actual balance if an owner is found.
107    /// If no address meets the criteria, returns `None`.
108    /// On failure, returns a string representing an error message.
109    async fn find_owner(
110        &self,
111        token: Address,
112        min_balance: Balance,
113    ) -> Result<Option<(Address, Balance)>, String>; // TODO: introduce custom error type
114}
115
116/// Trait for retrieving additional information about tokens, such as the number of decimals
117/// and the token symbol, to help construct `CurrencyToken` objects.
118#[async_trait]
119pub trait TokenPreProcessor: Send + Sync {
120    /// Given a list of token addresses, this function retrieves additional metadata for each token.
121    ///
122    /// # Parameters
123    /// * `addresses` - A vector of token addresses to process.
124    /// * `token_finder` - A reference to a `TokenOwnerFinding` implementation to help find token
125    ///   owners.
126    /// * `block` - The block tag at which the information should be retrieved.
127    ///
128    /// # Returns
129    /// A vector of `CurrencyToken` objects, each containing the processed information for the
130    /// token.
131    async fn get_tokens(
132        &self,
133        addresses: Vec<Bytes>,
134        token_finder: Arc<dyn TokenOwnerFinding>,
135        block: BlockTag,
136    ) -> Vec<Token>;
137}
138
139/// Trait for tracing blockchain transaction execution.
140#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
141#[async_trait]
142pub trait EntryPointTracer: Sync {
143    type Error: Debug;
144
145    /// Traces the execution of a list of entry points at a specific block.
146    ///
147    /// # Parameters
148    /// * `block_hash` - The hash of the block at which to perform the trace. The trace will use the
149    ///   state of the blockchain at this block.
150    /// * `entry_points` - A list of entry points to trace with their data.
151    ///
152    /// # Returns
153    /// Returns a vector of `TracedEntryPoint`, where each element contains:
154    /// * `retriggers` - A set of (address, storage slot) pairs representing storage locations that
155    ///   could alter tracing results. If any of these storage slots change, the set of called
156    ///   contract might be outdated.
157    /// * `accessed_slots` - A map of all contract addresses that were called during the trace with
158    ///   a list of storage slots that were accessed (read or written).
159    async fn trace(
160        &self,
161        block_hash: BlockHash,
162        entry_points: Vec<EntryPointWithTracingParams>,
163    ) -> Vec<Result<TracedEntryPoint, Self::Error>>;
164}
165
166/// Trait for detecting storage slots that contain ERC20 token balances
167#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
168#[async_trait]
169pub trait BalanceSlotDetector: Send + Sync {
170    type Error: Debug;
171
172    /// Detect balance storage slots for multiple tokens from a single holder.
173    /// Useful to allow overriding balances.
174    ///
175    /// # Arguments
176    /// * `tokens` - Slice of ERC20 token addresses.
177    /// * `holder` - Address that holds the tokens (e.g., pool manager)
178    /// * `block_hash` - Block at which to detect slots
179    ///
180    /// # Returns
181    /// HashMap mapping Token -> Result containing (contract_address -> storage_slot) or error.
182    /// The storage slot is the one that controls the token's holder balance.
183    async fn detect_balance_slots(
184        &self,
185        tokens: &[Address],
186        holder: Address,
187        block_hash: BlockHash,
188    ) -> HashMap<Address, Result<(Address, Bytes), Self::Error>>;
189}
190
191/// Trait for detecting storage slots that contain ERC20 token allowances
192#[cfg_attr(feature = "test-utils", mockall::automock(type Error = String;))]
193#[async_trait]
194pub trait AllowanceSlotDetector: Send + Sync {
195    type Error: Debug;
196
197    /// Detect allowance storage slots for multiple tokens for owner-spender pairs.
198    /// Useful to allow overriding allowances in simulations.
199    ///
200    /// # Arguments
201    /// * `tokens` - Slice of ERC20 token addresses.
202    /// * `owner` - Address that owns the tokens
203    /// * `spender` - Address that is allowed to spend the tokens
204    /// * `block_hash` - Block at which to detect slots
205    ///
206    /// # Returns
207    /// HashMap mapping Token -> Result containing (contract_address -> storage_slot) or error.
208    /// The storage slot is the one that controls the allowance from owner to spender.
209    async fn detect_allowance_slots(
210        &self,
211        tokens: &[Address],
212        owner: Address,
213        spender: Address,
214        block_hash: BlockHash,
215    ) -> HashMap<Address, Result<(Address, Bytes), Self::Error>>;
216}
217
218#[cfg(test)]
219mod tests {
220    use std::str::FromStr;
221
222    use super::*;
223
224    #[test]
225    fn test_storage_snapshot_request_display() {
226        // Test with specific slots
227        let request_with_slots = StorageSnapshotRequest {
228            address: Address::from_str("0x1234567890123456789012345678901234567890").unwrap(),
229            slots: Some(vec![
230                StoreKey::from(vec![1, 2, 3, 4]),
231                StoreKey::from(vec![5, 6, 7, 8]),
232                StoreKey::from(vec![9, 10, 11, 12]),
233            ]),
234        };
235
236        let display_output = request_with_slots.to_string();
237        assert_eq!(display_output, "0x123456...7890[3 slots]");
238
239        // Test with all slots
240        let request_all_slots = StorageSnapshotRequest {
241            address: Address::from_str("0x9876543210987654321098765432109876543210").unwrap(),
242            slots: None,
243        };
244
245        let display_output = request_all_slots.to_string();
246        assert_eq!(display_output, "0x987654...3210[all slots]");
247
248        // Test with empty slots vector
249        let request_empty_slots = StorageSnapshotRequest {
250            address: Address::from_str("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd").unwrap(),
251            slots: Some(vec![]),
252        };
253
254        let display_output = request_empty_slots.to_string();
255        assert_eq!(display_output, "0xabcdef...abcd[0 slots]");
256    }
257}