Skip to main content

tycho_ethereum/services/
token_pre_processor.rs

1use std::sync::Arc;
2
3use alloy::{primitives::Address, rpc::types::BlockNumberOrTag, sol_types::SolCall};
4use async_trait::async_trait;
5use tracing::{instrument, warn};
6use tycho_common::{
7    models::{
8        blockchain::BlockTag,
9        token::{Token, TokenQuality},
10        Chain,
11    },
12    traits::{TokenAnalyzer, TokenOwnerFinding, TokenPreProcessor},
13    Bytes,
14};
15use unicode_segmentation::UnicodeSegmentation;
16
17use crate::{
18    erc20::{decimalsCall, symbolCall},
19    rpc::EthereumRpcClient,
20    services::token_analyzer::{call_request, EthCallDetector},
21    BytesCodec,
22};
23
24#[derive(Debug, Clone)]
25pub struct EthereumTokenPreProcessor {
26    rpc: EthereumRpcClient,
27    chain: Chain,
28    settlement_contract: Address,
29}
30
31impl EthereumTokenPreProcessor {
32    pub fn new(rpc: &EthereumRpcClient, chain: Chain, settlement_contract: Address) -> Self {
33        EthereumTokenPreProcessor { rpc: rpc.clone(), chain, settlement_contract }
34    }
35
36    async fn call_symbol(&self, token: Address) -> String {
37        let calldata = symbolCall {}.abi_encode();
38
39        let result = match self
40            .rpc
41            .eth_call(call_request(None, token, calldata), BlockNumberOrTag::Latest)
42            .await
43        {
44            Ok(result) => result,
45            Err(e) => {
46                warn!(?e, ?token, "Failed to call symbol function, using address as fallback");
47                return format!("0x{:x}", token);
48            }
49        };
50
51        match symbolCall::abi_decode_returns_validate(&result) {
52            Ok(symbol) => symbol,
53            Err(e) => {
54                warn!(
55                    ?e,
56                    ?token,
57                    "Failed to decode symbol function result, using address as fallback"
58                );
59                format!("0x{:x}", token)
60            }
61        }
62    }
63
64    async fn call_decimals(&self, token: Address) -> u8 {
65        let calldata = decimalsCall {}.abi_encode();
66
67        let result = match self
68            .rpc
69            .eth_call(call_request(None, token, calldata), BlockNumberOrTag::Latest)
70            .await
71        {
72            Ok(result) => result,
73            Err(e) => {
74                warn!(?e, ?token, "Failed to call decimals function, using default decimals 18");
75                return 18;
76            }
77        };
78
79        match decimalsCall::abi_decode_returns_validate(&result) {
80            Ok(decimals) => decimals,
81            Err(e) => {
82                warn!(
83                    ?e,
84                    ?token,
85                    "Failed to decode decimals function result, using default decimals 18"
86                );
87                18
88            }
89        }
90    }
91}
92
93#[async_trait]
94impl TokenPreProcessor for EthereumTokenPreProcessor {
95    #[instrument(skip_all, fields(n_addresses=addresses.len(), block = ?block))]
96    async fn get_tokens(
97        &self,
98        addresses: Vec<Bytes>,
99        token_finder: Arc<dyn TokenOwnerFinding>,
100        block: BlockTag,
101    ) -> Vec<Token> {
102        let mut tokens_info = Vec::new();
103
104        for address in addresses {
105            let token_address = Address::from_bytes(&address);
106
107            // Make RPC calls directly for symbol and decimals
108            let symbol = self.call_symbol(token_address).await;
109            let decimals = self.call_decimals(token_address).await;
110
111            let detector =
112                EthCallDetector::new(&self.rpc, token_finder.clone(), self.settlement_contract);
113
114            let (token_quality, gas, tax) = detector
115                .analyze(address.clone(), block)
116                .await
117                .unwrap_or_else(|e| {
118                    warn!(error=?e, "TokenDetectionFailure");
119                    (TokenQuality::bad("Detection failed"), None, None)
120                });
121
122            let mut quality = 100;
123
124            if let TokenQuality::Bad { reason } = token_quality {
125                warn!(address=?address, ?reason, "BadToken");
126                // Flag this token as bad using quality, an external script is responsible for
127                // analyzing these tokens again.
128                quality = 10;
129            };
130
131            // If quality is 100 but it's a fee token, set quality to 50
132            if quality == 100 && tax.is_some_and(|tax_value| tax_value > 0) {
133                quality = 50;
134            }
135
136            tokens_info.push(Token {
137                address,
138                symbol: symbol
139                    .replace('\0', "")
140                    .graphemes(true)
141                    .take(255)
142                    .collect::<String>(),
143                decimals: decimals.into(),
144                tax: tax.unwrap_or(0),
145                gas: gas
146                    .map(|g| vec![Some(g)])
147                    .unwrap_or_else(Vec::new),
148                chain: self.chain,
149                quality,
150            });
151        }
152
153        tokens_info
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use std::str::FromStr;
160
161    use alloy::primitives::address;
162    use tycho_common::models::token::TokenOwnerStore;
163
164    use super::*;
165    use crate::test_fixtures::{TestFixture, TEST_BLOCK_NUMBER, TOKEN_HOLDERS, USDC_STR, WETH_STR};
166
167    const COWSWAP_SETTLEMENT: Address = address!("c9f2e6ea1637E499406986ac50ddC92401ce1f58");
168
169    impl TestFixture {
170        fn create_token_preprocessor(&self) -> EthereumTokenPreProcessor {
171            // We do not enable batching as the token pre-processor does not leverage it currently
172            let rpc = self.create_rpc_client(false);
173
174            EthereumTokenPreProcessor::new(&rpc, Chain::Ethereum, COWSWAP_SETTLEMENT)
175        }
176    }
177
178    #[tokio::test]
179    #[ignore = "require RPC connection"]
180    async fn test_call_symbol() {
181        let fixture = TestFixture::new();
182        let processor = fixture.create_token_preprocessor();
183
184        // Test WETH symbol
185        let weth_address = Address::from_str(WETH_STR).expect("Failed to parse WETH address");
186        let symbol = processor
187            .call_symbol(weth_address)
188            .await;
189        assert_eq!(symbol, "WETH", "Expected WETH symbol");
190
191        // Test USDC symbol
192        let usdc_address = Address::from_str(USDC_STR).expect("Failed to parse USDC address");
193        let symbol = processor
194            .call_symbol(usdc_address)
195            .await;
196        assert_eq!(symbol, "USDC", "Expected USDC symbol");
197    }
198
199    #[tokio::test]
200    #[ignore = "require RPC connection"]
201    async fn test_call_decimals() {
202        let fixture = TestFixture::new();
203        let processor = fixture.create_token_preprocessor();
204
205        // Test WETH decimals (18)
206        let weth_address = Address::from_str(WETH_STR).expect("Failed to parse WETH address");
207        let decimals = processor
208            .call_decimals(weth_address)
209            .await;
210        assert_eq!(decimals, 18, "Expected WETH to have 18 decimals");
211
212        // Test USDC decimals (6)
213        let usdc_address = Address::from_str(USDC_STR).expect("Failed to parse USDC address");
214        let decimals = processor
215            .call_decimals(usdc_address)
216            .await;
217        assert_eq!(decimals, 6, "Expected USDC to have 6 decimals");
218    }
219
220    #[tokio::test]
221    #[ignore = "require archive RPC connection"]
222    async fn test_get_tokens() {
223        let fixture = TestFixture::new();
224        let processor = fixture.create_token_preprocessor();
225
226        let tf = TokenOwnerStore::new(TOKEN_HOLDERS.clone());
227
228        let fake_address: &str = "0xA0b86991c7456b36c1d19D4a2e9Eb0cE3606eB48";
229        let addresses = vec![
230            Bytes::from_str(WETH_STR).unwrap(),
231            Bytes::from_str(USDC_STR).unwrap(),
232            Bytes::from_str(fake_address).unwrap(),
233        ];
234
235        let results = processor
236            .get_tokens(addresses, Arc::new(tf), BlockTag::Number(TEST_BLOCK_NUMBER))
237            .await;
238        assert_eq!(results.len(), 3);
239        let relevant_attrs: Vec<(String, u32, u32)> = results
240            .iter()
241            .map(|t| (t.symbol.clone(), t.decimals, t.quality))
242            .collect();
243        assert_eq!(
244            relevant_attrs,
245            vec![
246                ("WETH".to_string(), 18, 100),
247                ("USDC".to_string(), 6, 100),
248                (fake_address.to_lowercase(), 18, 10)
249            ]
250        );
251    }
252}