foundry_evm_traces/identifier/
external.rs1use super::{IdentifiedAddress, TraceIdentifier};
2use crate::debug::ContractSources;
3use alloy_primitives::{
4 Address,
5 map::{Entry, HashMap},
6};
7use eyre::WrapErr;
8use foundry_block_explorers::{contract::Metadata, errors::EtherscanError};
9use foundry_common::compile::etherscan_project;
10use foundry_config::{Chain, Config};
11use futures::{
12 future::join_all,
13 stream::{FuturesUnordered, Stream, StreamExt},
14 task::{Context, Poll},
15};
16use revm_inspectors::tracing::types::CallTraceNode;
17use serde::Deserialize;
18use std::{
19 borrow::Cow,
20 pin::Pin,
21 sync::{
22 Arc,
23 atomic::{AtomicBool, Ordering},
24 },
25};
26use tokio::time::{Duration, Interval};
27
28pub struct ExternalIdentifier {
30 fetchers: Vec<Arc<dyn ExternalFetcherT>>,
31 contracts: HashMap<Address, (FetcherKind, Option<Metadata>)>,
33}
34
35impl ExternalIdentifier {
36 pub fn new(config: &Config, mut chain: Option<Chain>) -> eyre::Result<Option<Self>> {
38 if config.offline {
39 return Ok(None);
40 }
41
42 let config = match config.get_etherscan_config_with_chain(chain) {
43 Ok(Some(config)) => {
44 chain = config.chain;
45 Some(config)
46 }
47 Ok(None) => {
48 warn!(target: "evm::traces::external", "etherscan config not found");
49 None
50 }
51 Err(err) => {
52 warn!(target: "evm::traces::external", ?err, "failed to get etherscan config");
53 None
54 }
55 };
56
57 let mut fetchers = Vec::<Arc<dyn ExternalFetcherT>>::new();
58 if let Some(chain) = chain {
59 debug!(target: "evm::traces::external", ?chain, "using sourcify identifier");
60 fetchers.push(Arc::new(SourcifyFetcher::new(chain)));
61 }
62 if let Some(config) = config {
63 debug!(target: "evm::traces::external", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
64 fetchers.push(Arc::new(EtherscanFetcher::new(config.into_client()?)));
65 }
66 if fetchers.is_empty() {
67 debug!(target: "evm::traces::external", "no fetchers enabled");
68 return Ok(None);
69 }
70
71 Ok(Some(Self { fetchers, contracts: Default::default() }))
72 }
73
74 pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
77 let contracts_info: Vec<_> = self
79 .contracts
80 .iter()
81 .filter_map(|(addr, (_, metadata))| {
83 if let Some(metadata) = metadata.as_ref()
84 && !metadata.is_vyper()
85 {
86 Some((*addr, metadata))
87 } else {
88 None
89 }
90 })
91 .collect();
92
93 let outputs_fut = contracts_info
94 .iter()
95 .map(|(addr, metadata)| async move {
96 sh_println!("Compiling: {} {addr}", metadata.contract_name)?;
97 let root = tempfile::tempdir()?;
98 let root_path = root.path();
99 let project = etherscan_project(metadata, root_path)?;
100 let output = project.compile()?;
101 if output.has_compiler_errors() {
102 eyre::bail!("{output}")
103 }
104
105 Ok((project, output, root))
106 })
107 .collect::<Vec<_>>();
108
109 let outputs = join_all(outputs_fut).await;
111
112 let mut sources: ContractSources = Default::default();
113
114 for (idx, res) in outputs.into_iter().enumerate() {
116 let (addr, metadata) = &contracts_info[idx];
117 let name = &metadata.contract_name;
118 let (project, output, _) =
119 res.wrap_err_with(|| format!("Failed to compile contract {name} at {addr}"))?;
120 sources
121 .insert(&output, project.root(), None)
122 .wrap_err_with(|| format!("Failed to insert contract {name} at {addr}"))?;
123 }
124
125 Ok(sources)
126 }
127
128 fn identify_from_metadata(
129 &self,
130 address: Address,
131 metadata: &Metadata,
132 ) -> IdentifiedAddress<'static> {
133 let label = metadata.contract_name.clone();
134 let abi = metadata.abi().ok().map(Cow::Owned);
135 IdentifiedAddress {
136 address,
137 label: Some(label.clone()),
138 contract: Some(label),
139 abi,
140 artifact_id: None,
141 }
142 }
143}
144
145impl TraceIdentifier for ExternalIdentifier {
146 fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
147 if nodes.is_empty() {
148 return Vec::new();
149 }
150
151 trace!(target: "evm::traces::external", "identify {} addresses", nodes.len());
152
153 let mut identities = Vec::new();
154 let mut to_fetch = Vec::new();
155
156 for &node in nodes {
158 let address = node.trace.address;
159 if let Some((_, metadata)) = self.contracts.get(&address) {
160 if let Some(metadata) = metadata {
161 identities.push(self.identify_from_metadata(address, metadata));
162 } else {
163 }
165 } else {
166 to_fetch.push(address);
167 }
168 }
169
170 if to_fetch.is_empty() {
171 return identities;
172 }
173 trace!(target: "evm::traces::external", "fetching {} addresses", to_fetch.len());
174
175 let fetchers =
176 self.fetchers.iter().map(|fetcher| ExternalFetcher::new(fetcher.clone(), &to_fetch));
177 let fetched_identities = foundry_common::block_on(
178 futures::stream::select_all(fetchers)
179 .filter_map(|(address, value)| {
180 let addr = value
181 .1
182 .as_ref()
183 .map(|metadata| self.identify_from_metadata(address, metadata));
184 match self.contracts.entry(address) {
185 Entry::Occupied(mut occupied_entry) => {
186 if !matches!(occupied_entry.get().0, FetcherKind::Etherscan)
190 || value.1.is_none()
191 {
192 occupied_entry.insert(value);
193 }
194 }
195 Entry::Vacant(vacant_entry) => {
196 vacant_entry.insert(value);
197 }
198 }
199 async move { addr }
200 })
201 .collect::<Vec<IdentifiedAddress<'_>>>(),
202 );
203 trace!(target: "evm::traces::external", "fetched {} addresses: {fetched_identities:#?}", fetched_identities.len());
204
205 identities.extend(fetched_identities);
206 identities
207 }
208}
209
210type FetchFuture =
211 Pin<Box<dyn Future<Output = (Address, Result<Option<Metadata>, EtherscanError>)>>>;
212
213struct ExternalFetcher {
217 fetcher: Arc<dyn ExternalFetcherT>,
219 timeout: Duration,
221 backoff: Option<Interval>,
223 concurrency: usize,
225 queue: Vec<Address>,
227 in_progress: FuturesUnordered<FetchFuture>,
229}
230
231impl ExternalFetcher {
232 fn new(fetcher: Arc<dyn ExternalFetcherT>, to_fetch: &[Address]) -> Self {
233 Self {
234 timeout: fetcher.timeout(),
235 backoff: None,
236 concurrency: fetcher.concurrency(),
237 fetcher,
238 queue: to_fetch.to_vec(),
239 in_progress: FuturesUnordered::new(),
240 }
241 }
242
243 fn queue_next_reqs(&mut self) {
244 while self.in_progress.len() < self.concurrency {
245 let Some(addr) = self.queue.pop() else { break };
246 let fetcher = Arc::clone(&self.fetcher);
247 self.in_progress.push(Box::pin(async move {
248 trace!(target: "evm::traces::external", ?addr, "fetching info");
249 let res = fetcher.fetch(addr).await;
250 (addr, res)
251 }));
252 }
253 }
254}
255
256impl Stream for ExternalFetcher {
257 type Item = (Address, (FetcherKind, Option<Metadata>));
258
259 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
260 let pin = self.get_mut();
261
262 let _guard =
263 info_span!("evm::traces::external", kind=?pin.fetcher.kind(), "ExternalFetcher")
264 .entered();
265
266 if pin.fetcher.invalid_api_key().load(Ordering::Relaxed) {
267 return Poll::Ready(None);
268 }
269
270 loop {
271 if let Some(mut backoff) = pin.backoff.take()
272 && backoff.poll_tick(cx).is_pending()
273 {
274 pin.backoff = Some(backoff);
275 return Poll::Pending;
276 }
277
278 pin.queue_next_reqs();
279
280 let mut made_progress_this_iter = false;
281 match pin.in_progress.poll_next_unpin(cx) {
282 Poll::Pending => {}
283 Poll::Ready(None) => return Poll::Ready(None),
284 Poll::Ready(Some((addr, res))) => {
285 made_progress_this_iter = true;
286 match res {
287 Ok(metadata) => {
288 return Poll::Ready(Some((addr, (pin.fetcher.kind(), metadata))));
289 }
290 Err(EtherscanError::ContractCodeNotVerified(_)) => {
291 return Poll::Ready(Some((addr, (pin.fetcher.kind(), None))));
292 }
293 Err(EtherscanError::RateLimitExceeded) => {
294 warn!(target: "evm::traces::external", "rate limit exceeded on attempt");
295 pin.backoff = Some(tokio::time::interval(pin.timeout));
296 pin.queue.push(addr);
297 }
298 Err(EtherscanError::InvalidApiKey) => {
299 warn!(target: "evm::traces::external", "invalid api key");
300 pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
302 return Poll::Ready(None);
303 }
304 Err(EtherscanError::BlockedByCloudflare) => {
305 warn!(target: "evm::traces::external", "blocked by cloudflare");
306 pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
308 return Poll::Ready(None);
309 }
310 Err(err) => {
311 warn!(target: "evm::traces::external", ?err, "could not get info");
312 }
313 }
314 }
315 }
316
317 if !made_progress_this_iter {
318 return Poll::Pending;
319 }
320 }
321 }
322}
323
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325enum FetcherKind {
326 Etherscan,
327 Sourcify,
328}
329
330#[async_trait::async_trait]
331trait ExternalFetcherT: Send + Sync {
332 fn kind(&self) -> FetcherKind;
333 fn timeout(&self) -> Duration;
334 fn concurrency(&self) -> usize;
335 fn invalid_api_key(&self) -> &AtomicBool;
336 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError>;
337}
338
339struct EtherscanFetcher {
340 client: foundry_block_explorers::Client,
341 invalid_api_key: AtomicBool,
342}
343
344impl EtherscanFetcher {
345 fn new(client: foundry_block_explorers::Client) -> Self {
346 Self { client, invalid_api_key: AtomicBool::new(false) }
347 }
348}
349
350#[async_trait::async_trait]
351impl ExternalFetcherT for EtherscanFetcher {
352 fn kind(&self) -> FetcherKind {
353 FetcherKind::Etherscan
354 }
355
356 fn timeout(&self) -> Duration {
357 Duration::from_secs(1)
358 }
359
360 fn concurrency(&self) -> usize {
361 5
362 }
363
364 fn invalid_api_key(&self) -> &AtomicBool {
365 &self.invalid_api_key
366 }
367
368 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
369 self.client.contract_source_code(address).await.map(|mut metadata| metadata.items.pop())
370 }
371}
372
373struct SourcifyFetcher {
374 client: reqwest::Client,
375 url: String,
376 invalid_api_key: AtomicBool,
377}
378
379impl SourcifyFetcher {
380 fn new(chain: Chain) -> Self {
381 Self {
382 client: reqwest::Client::new(),
383 url: format!("https://sourcify.dev/server/v2/contract/{}", chain.id()),
384 invalid_api_key: AtomicBool::new(false),
385 }
386 }
387}
388
389#[async_trait::async_trait]
390impl ExternalFetcherT for SourcifyFetcher {
391 fn kind(&self) -> FetcherKind {
392 FetcherKind::Sourcify
393 }
394
395 fn timeout(&self) -> Duration {
396 Duration::from_secs(1)
397 }
398
399 fn concurrency(&self) -> usize {
400 5
401 }
402
403 fn invalid_api_key(&self) -> &AtomicBool {
404 &self.invalid_api_key
405 }
406
407 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
408 let url = format!("{url}/{address}?fields=abi,compilation", url = self.url);
409 let response = self.client.get(url).send().await?;
410 let code = response.status();
411 let response: SourcifyResponse = response.json().await?;
412 trace!(target: "evm::traces::external", "Sourcify response for {address}: {response:#?}");
413 match code.as_u16() {
414 404 => return Err(EtherscanError::ContractCodeNotVerified(address)),
416 429 => return Err(EtherscanError::RateLimitExceeded),
418 _ => {}
419 }
420 match response {
421 SourcifyResponse::Success(metadata) => Ok(Some(metadata.into())),
422 SourcifyResponse::Error(error) => Err(EtherscanError::Unknown(format!("{error:#?}"))),
423 }
424 }
425}
426
427#[derive(Debug, Clone, Deserialize)]
429#[serde(untagged)]
430enum SourcifyResponse {
431 Success(SourcifyMetadata),
432 Error(SourcifyError),
433}
434
435#[derive(Debug, Clone, Deserialize)]
436#[serde(rename_all = "camelCase")]
437#[expect(dead_code)] struct SourcifyError {
439 custom_code: String,
440 message: String,
441 error_id: String,
442}
443
444#[derive(Debug, Clone, Deserialize)]
445#[serde(rename_all = "camelCase")]
446struct SourcifyMetadata {
447 #[serde(default)]
448 abi: Option<Box<serde_json::value::RawValue>>,
449 #[serde(default)]
450 compilation: Option<Compilation>,
451}
452
453#[derive(Debug, Clone, Deserialize)]
454#[serde(rename_all = "camelCase")]
455struct Compilation {
456 #[serde(default)]
457 compiler_version: String,
458 #[serde(default)]
459 name: String,
460}
461
462impl From<SourcifyMetadata> for Metadata {
463 fn from(metadata: SourcifyMetadata) -> Self {
464 let SourcifyMetadata { abi, compilation } = metadata;
465 let (contract_name, compiler_version) = compilation
466 .map(|c| (c.name, c.compiler_version))
467 .unwrap_or_else(|| (String::new(), String::new()));
468 Self {
470 source_code: foundry_block_explorers::contract::SourceCodeMetadata::Sources(
471 Default::default(),
472 ),
473 abi: Box::<str>::from(abi.unwrap_or_default()).into(),
474 contract_name,
475 compiler_version,
476 optimization_used: 0,
477 runs: 0,
478 constructor_arguments: Default::default(),
479 evm_version: String::new(),
480 library: String::new(),
481 license_type: String::new(),
482 proxy: 0,
483 implementation: None,
484 swarm_source: String::new(),
485 }
486 }
487}