1use super::{IdentifiedAddress, TraceIdentifier};
2use crate::debug::ContractSources;
3use alloy_json_abi::JsonAbi;
4use alloy_primitives::{
5 Address,
6 map::{AddressMap, AddressSet, Entry, HashMap, HashSet},
7};
8use eyre::WrapErr;
9use foundry_block_explorers::{contract::Metadata, errors::EtherscanError};
10use foundry_common::compile::etherscan_project;
11use foundry_config::{Chain, Config, EtherscanConfigs, NamedChain, ResolvedEtherscanConfig};
12use foundry_evm_core::constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS};
13use futures::{
14 future::join_all,
15 stream::{FuturesUnordered, Stream, StreamExt},
16 task::{Context, Poll},
17};
18use revm_inspectors::tracing::types::CallTraceNode;
19use serde::Deserialize;
20use std::{
21 borrow::Cow,
22 pin::Pin,
23 sync::{
24 Arc,
25 atomic::{AtomicBool, Ordering},
26 },
27};
28use tokio::time::{Duration, Interval};
29
30pub struct ExternalIdentifier {
32 fetchers: Vec<Arc<dyn ExternalFetcherT>>,
33 contracts: HashMap<Address, (FetcherKind, Option<Metadata>)>,
35 remaining_budget: Duration,
37}
38
39#[derive(Clone, Debug, Default)]
45pub struct ExternalIdentifierConfig {
46 offline: bool,
48 timeout: u64,
50 no_proxy: bool,
52 etherscan: EtherscanConfigs,
54 etherscan_alias: Option<String>,
55 etherscan_api_key: Option<String>,
56 chain: Option<Chain>,
58}
59
60impl ExternalIdentifierConfig {
61 pub fn new(config: &Config) -> Self {
63 Self {
64 offline: config.offline,
65 timeout: config.tracing.external_identification_timeout,
66 no_proxy: config.eth_rpc_no_proxy,
67 etherscan: config.etherscan.clone(),
68 etherscan_alias: config.etherscan_alias().map(str::to_string),
69 etherscan_api_key: config.etherscan_api_key.clone(),
70 chain: config.chain,
71 }
72 }
73
74 pub fn identifier(&self, chain: Option<Chain>) -> Option<ExternalIdentifier> {
79 self.identifier_with(
80 chain,
81 self.etherscan_alias.as_deref(),
82 self.etherscan_api_key.as_deref(),
83 true,
84 )
85 }
86
87 pub fn storage_identifier(&self, chain: Chain) -> Option<ExternalIdentifier> {
93 self.identifier_with(Some(chain), None, self.literal_api_key(), false)
94 }
95
96 fn identifier_with(
97 &self,
98 chain: Option<Chain>,
99 etherscan_alias: Option<&str>,
100 etherscan_api_key: Option<&str>,
101 sourcify: bool,
102 ) -> Option<ExternalIdentifier> {
103 if self.offline || self.timeout == 0 {
104 return None;
105 }
106
107 let (chain, etherscan) = self.resolve_explorer(chain, etherscan_alias, etherscan_api_key);
108
109 let mut fetchers = Vec::<Arc<dyn ExternalFetcherT>>::new();
110 if sourcify
112 && let Some(chain) = chain
113 && !matches!(chain.named(), Some(NamedChain::AnvilHardhat | NamedChain::Dev))
114 {
115 debug!(target: "evm::traces::external", ?chain, "using sourcify identifier");
116 fetchers.push(Arc::new(SourcifyFetcher::new(chain)));
117 }
118 if let Some(config) = etherscan {
119 debug!(target: "evm::traces::external", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
120 match config.into_client_with_no_proxy(self.no_proxy) {
121 Ok(client) => {
122 fetchers.push(Arc::new(EtherscanFetcher::new(client)));
123 }
124 Err(err) => {
125 warn!(target: "evm::traces::external", ?err, "failed to create etherscan client");
126 }
127 }
128 }
129 if fetchers.is_empty() {
130 debug!(target: "evm::traces::external", "no fetchers enabled");
131 return None;
132 }
133
134 Some(ExternalIdentifier {
135 fetchers,
136 contracts: Default::default(),
137 remaining_budget: Duration::from_secs(self.timeout),
138 })
139 }
140
141 fn resolve_explorer(
143 &self,
144 chain: Option<Chain>,
145 etherscan_alias: Option<&str>,
146 etherscan_api_key: Option<&str>,
147 ) -> (Option<Chain>, Option<ResolvedEtherscanConfig>) {
148 let mut resolved =
149 self.etherscan.resolve_for(etherscan_alias, etherscan_api_key, chain.or(self.chain));
150 if let Some(chain) = chain
153 && let Ok(Some(config)) = &resolved
154 && config.chain.is_some_and(|c| c != chain)
155 {
156 resolved = self.etherscan.resolve_for(None, self.literal_api_key(), Some(chain));
157 }
158 match resolved {
159 Ok(Some(config)) => (chain.or(config.chain), Some(config)),
160 Ok(None) => {
161 warn!(target: "evm::traces::external", "etherscan config not found");
162 (chain, None)
163 }
164 Err(err) => {
165 warn!(target: "evm::traces::external", ?err, "failed to get etherscan config");
166 (chain, None)
167 }
168 }
169 }
170
171 pub const fn storage_timeout(&self) -> Duration {
173 Duration::from_secs(self.timeout)
174 }
175
176 fn literal_api_key(&self) -> Option<&str> {
181 self.etherscan_api_key.as_deref().filter(|key| !self.etherscan.contains_key(*key))
182 }
183}
184
185impl ExternalIdentifier {
186 pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Option<Self>> {
188 Ok(ExternalIdentifierConfig::new(config).identifier(chain))
189 }
190
191 pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
194 let contracts_info: Vec<_> = self
196 .contracts
197 .iter()
198 .filter_map(|(addr, (_, metadata))| {
200 if let Some(metadata) = metadata.as_ref()
201 && !metadata.is_vyper()
202 {
203 Some((*addr, metadata))
204 } else {
205 None
206 }
207 })
208 .collect();
209
210 let outputs_fut = contracts_info
211 .iter()
212 .map(|(addr, metadata)| async move {
213 sh_println!("Compiling: {} {addr}", metadata.contract_name)?;
214 let root = tempfile::tempdir()?;
215 let root_path = root.path();
216 let project = etherscan_project(metadata, root_path)?;
217 let output = project.compile()?;
218 if output.has_compiler_errors() {
219 eyre::bail!("{output}");
220 }
221
222 Ok((project, output, root))
223 })
224 .collect::<Vec<_>>();
225
226 let outputs = join_all(outputs_fut).await;
228
229 let mut sources: ContractSources = Default::default();
230
231 for (idx, res) in outputs.into_iter().enumerate() {
233 let (addr, metadata) = &contracts_info[idx];
234 let name = &metadata.contract_name;
235 let (project, output, _) =
236 res.wrap_err_with(|| format!("Failed to compile contract {name} at {addr}"))?;
237 sources
238 .insert(&output, project.root(), None)
239 .wrap_err_with(|| format!("Failed to insert contract {name} at {addr}"))?;
240 }
241
242 Ok(sources)
243 }
244
245 fn identify_from_metadata(
246 &self,
247 address: Address,
248 metadata: &Metadata,
249 ) -> IdentifiedAddress<'static> {
250 let label = metadata.contract_name.clone();
251 let abi = metadata.abi().ok().map(Cow::Owned);
252 IdentifiedAddress {
253 address,
254 label: Some(label.clone()),
255 contract: Some(label),
256 abi,
257 constructor_args_offset: None,
258 artifact_id: None,
259 }
260 }
261
262 fn cache_fetched(&mut self, address: Address, value: (FetcherKind, Option<Metadata>)) {
263 match self.contracts.entry(address) {
264 Entry::Occupied(mut occupied_entry) => {
265 let old = occupied_entry.get();
266 let should_replace = match (&old.1, &value.1) {
271 (None, Some(_)) => true,
272 (Some(_), None) => false,
273 _ => {
274 matches!(value.0, FetcherKind::Etherscan)
275 && !matches!(old.0, FetcherKind::Etherscan)
276 }
277 };
278 if should_replace {
279 occupied_entry.insert(value);
280 }
281 }
282 Entry::Vacant(vacant_entry) => {
283 vacant_entry.insert(value);
284 }
285 }
286 }
287
288 async fn fetch_addresses_async(&mut self, addresses: &[Address]) {
289 self.fetch_addresses_with_timeout(addresses, self.remaining_budget).await;
290 }
291
292 async fn fetch_addresses_with_timeout(&mut self, addresses: &[Address], timeout: Duration) {
293 let timeout = timeout.min(self.remaining_budget);
294 if addresses.is_empty() || timeout.is_zero() {
295 return;
296 }
297
298 let fetchers = self
299 .fetchers
300 .clone()
301 .into_iter()
302 .map(|fetcher| ExternalFetcher::new(fetcher, addresses));
303 let started = tokio::time::Instant::now();
304 let timed_out = tokio::time::timeout(timeout, async {
305 let mut fetched = futures::stream::select_all(fetchers);
306 while let Some((address, value)) = fetched.next().await {
307 self.cache_fetched(address, value);
308 }
309 })
310 .await
311 .is_err();
312 self.remaining_budget = self.remaining_budget.saturating_sub(started.elapsed());
313 if timed_out && self.remaining_budget.is_zero() {
314 warn!(target: "evm::traces::external", "external identification timed out; disabling it for the remainder of this session");
315 }
316 }
317
318 pub async fn get_abis(
320 &mut self,
321 addresses: &[Address],
322 ) -> Vec<(Address, eyre::Result<(Vec<JsonAbi>, bool)>)> {
323 const MAX_PROXY_DEPTH: usize = 16;
324
325 struct Chain {
326 current: Option<Address>,
327 visited: HashSet<Address>,
328 abis: Vec<JsonAbi>,
329 complete: bool,
330 }
331
332 let mut chains = addresses
333 .iter()
334 .map(|&address| Chain {
335 current: Some(address),
336 visited: HashSet::default(),
337 abis: Vec::new(),
338 complete: true,
339 })
340 .collect::<Vec<_>>();
341
342 for _ in 0..MAX_PROXY_DEPTH {
343 let to_fetch = chains
344 .iter()
345 .filter_map(|chain| chain.current)
346 .filter(|address| !self.contracts.contains_key(address))
347 .collect::<HashSet<_>>()
348 .into_iter()
349 .collect::<Vec<_>>();
350 self.fetch_addresses_async(&to_fetch).await;
351
352 let mut has_next = false;
353 for chain in &mut chains {
354 let Some(current) = chain.current else { continue };
355 if !chain.visited.insert(current) {
356 chain.current = None;
357 chain.complete = false;
358 continue;
359 }
360 let Some((_, Some(metadata))) = self.contracts.get(¤t) else {
361 chain.current = None;
362 chain.complete = false;
363 continue;
364 };
365 if let Ok(abi) = metadata.abi() {
366 chain.abis.push(abi);
367 } else {
368 chain.complete = false;
369 }
370 chain.current = (metadata.proxy != 0).then_some(metadata.implementation).flatten();
371 if metadata.proxy != 0 && chain.current.is_none() {
372 chain.complete = false;
373 }
374 has_next |= chain.current.is_some();
375 }
376 if !has_next {
377 break;
378 }
379 }
380
381 chains
382 .into_iter()
383 .zip(addresses.iter().copied())
384 .map(|(mut chain, address)| {
385 chain.complete &= chain.current.is_none();
386 let result = if chain.abis.is_empty() {
387 Err(eyre::eyre!("external ABI lookup failed"))
388 } else {
389 Ok((chain.abis.into_iter().rev().collect(), chain.complete))
390 };
391 (address, result)
392 })
393 .collect()
394 }
395
396 pub async fn get_metadata(
401 &mut self,
402 addresses: &[Address],
403 timeout: Duration,
404 ) -> AddressMap<Option<Metadata>> {
405 let to_fetch = addresses
406 .iter()
407 .copied()
408 .filter(|address| !self.contracts.contains_key(address))
409 .collect::<Vec<_>>();
410 self.fetch_addresses_with_timeout(&to_fetch, timeout).await;
411 addresses
412 .iter()
413 .filter_map(|address| {
414 self.contracts.get(address).map(|(_, metadata)| (*address, metadata.clone()))
415 })
416 .collect()
417 }
418}
419
420impl TraceIdentifier for ExternalIdentifier {
421 fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
422 if nodes.is_empty() {
423 return Vec::new();
424 }
425
426 trace!(target: "evm::traces::external", "identify {} addresses", nodes.len());
427
428 let mut identities = Vec::new();
429 let mut to_fetch = AddressSet::default();
430
431 for &node in nodes {
433 let address = node.trace.address;
434 if matches!(address, CHEATCODE_ADDRESS | HARDHAT_CONSOLE_ADDRESS) {
436 continue;
437 }
438 if let Some((_, metadata)) = self.contracts.get(&address) {
439 if let Some(metadata) = metadata {
440 identities.push(self.identify_from_metadata(address, metadata));
441 } else {
442 }
444 } else {
445 to_fetch.insert(address);
446 }
447 }
448
449 if to_fetch.is_empty() {
450 return identities;
451 }
452 if self.remaining_budget.is_zero() {
453 return identities;
454 }
455 trace!(target: "evm::traces::external", "fetching {} addresses", to_fetch.len());
456
457 let to_fetch = to_fetch.into_iter().collect::<Vec<_>>();
458 foundry_common::block_on(self.fetch_addresses_async(&to_fetch));
459
460 for address in to_fetch {
461 if let Some((_, Some(metadata))) = self.contracts.get(&address) {
462 identities.push(self.identify_from_metadata(address, metadata));
463 }
464 }
465 trace!(target: "evm::traces::external", "identified {} addresses", identities.len());
466 identities
467 }
468}
469
470type FetchFuture =
471 Pin<Box<dyn Future<Output = (Address, Result<Option<Metadata>, EtherscanError>)>>>;
472
473const MAX_TRANSIENT_RETRIES: u32 = 5;
476
477fn backoff_interval(period: Duration) -> Interval {
478 tokio::time::interval_at(tokio::time::Instant::now() + period, period)
479}
480
481struct ExternalFetcher {
485 fetcher: Arc<dyn ExternalFetcherT>,
487 timeout: Duration,
489 backoff: Option<Interval>,
491 concurrency: usize,
493 queue: Vec<Address>,
495 in_progress: FuturesUnordered<FetchFuture>,
497 attempts: HashMap<Address, u32>,
499}
500
501impl ExternalFetcher {
502 fn new(fetcher: Arc<dyn ExternalFetcherT>, to_fetch: &[Address]) -> Self {
503 Self {
504 timeout: fetcher.timeout(),
505 backoff: None,
506 concurrency: fetcher.concurrency(),
507 fetcher,
508 queue: to_fetch.to_vec(),
509 in_progress: FuturesUnordered::new(),
510 attempts: HashMap::default(),
511 }
512 }
513
514 fn queue_next_reqs(&mut self) {
515 while self.in_progress.len() < self.concurrency {
516 let Some(addr) = self.queue.pop() else { break };
517 let fetcher = Arc::clone(&self.fetcher);
518 self.in_progress.push(Box::pin(async move {
519 trace!(target: "evm::traces::external", ?addr, "fetching info");
520 let res = fetcher.fetch(addr).await;
521 (addr, res)
522 }));
523 }
524 }
525
526 fn retry(&mut self, addr: Address) -> bool {
531 let attempts = self.attempts.entry(addr).or_default();
532 *attempts += 1;
533 if *attempts > MAX_TRANSIENT_RETRIES {
534 return false;
535 }
536 self.backoff = Some(backoff_interval(self.timeout));
537 self.queue.push(addr);
538 true
539 }
540}
541
542impl Stream for ExternalFetcher {
543 type Item = (Address, (FetcherKind, Option<Metadata>));
544
545 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
546 let pin = self.get_mut();
547
548 let _guard =
549 info_span!("evm::traces::external", kind=?pin.fetcher.kind(), "ExternalFetcher")
550 .entered();
551
552 if pin.fetcher.invalid_api_key().load(Ordering::Relaxed) {
553 return Poll::Ready(None);
554 }
555
556 loop {
557 if let Some(mut backoff) = pin.backoff.take()
558 && backoff.poll_tick(cx).is_pending()
559 {
560 pin.backoff = Some(backoff);
561 return Poll::Pending;
562 }
563
564 pin.queue_next_reqs();
565
566 let mut made_progress_this_iter = false;
567 match pin.in_progress.poll_next_unpin(cx) {
568 Poll::Pending => {}
569 Poll::Ready(None) => return Poll::Ready(None),
570 Poll::Ready(Some((addr, res))) => {
571 made_progress_this_iter = true;
572 match res {
573 Ok(metadata) => {
574 return Poll::Ready(Some((addr, (pin.fetcher.kind(), metadata))));
575 }
576 Err(EtherscanError::ContractCodeNotVerified(_)) => {
577 return Poll::Ready(Some((addr, (pin.fetcher.kind(), None))));
578 }
579 Err(EtherscanError::RateLimitExceeded) => {
580 if pin.retry(addr) {
581 warn!(target: "evm::traces::external", "rate limit exceeded, backing off");
582 } else {
583 warn!(target: "evm::traces::external", "rate limit exceeded, giving up on address");
584 }
585 }
586 Err(EtherscanError::InvalidApiKey) => {
587 warn!(target: "evm::traces::external", "invalid api key");
588 pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
590 return Poll::Ready(None);
591 }
592 Err(EtherscanError::ChainNotSupported(chain)) => {
593 warn!(target: "evm::traces::external", %chain, "chain not supported");
594 pin.fetcher.invalid_api_key().store(true, Ordering::Relaxed);
596 return Poll::Ready(None);
597 }
598 Err(EtherscanError::BlockedByCloudflare) => {
599 if pin.retry(addr) {
605 warn!(target: "evm::traces::external", "blocked by cloudflare, backing off");
606 } else {
607 warn!(target: "evm::traces::external", "blocked by cloudflare, giving up on address");
608 }
611 }
612 Err(err) => {
613 warn!(target: "evm::traces::external", ?err, "could not get info");
614 }
616 }
617 }
618 }
619
620 if !made_progress_this_iter {
621 return Poll::Pending;
622 }
623 }
624 }
625}
626
627#[derive(Debug, Clone, Copy, PartialEq, Eq)]
628enum FetcherKind {
629 Etherscan,
630 Sourcify,
631}
632
633#[async_trait::async_trait]
634trait ExternalFetcherT: Send + Sync {
635 fn kind(&self) -> FetcherKind;
636 fn timeout(&self) -> Duration;
637 fn concurrency(&self) -> usize;
638 fn invalid_api_key(&self) -> &AtomicBool;
639 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError>;
640}
641
642struct EtherscanFetcher {
643 client: foundry_block_explorers::Client,
644 invalid_api_key: AtomicBool,
645}
646
647impl EtherscanFetcher {
648 const fn new(client: foundry_block_explorers::Client) -> Self {
649 Self { client, invalid_api_key: AtomicBool::new(false) }
650 }
651}
652
653#[async_trait::async_trait]
654impl ExternalFetcherT for EtherscanFetcher {
655 fn kind(&self) -> FetcherKind {
656 FetcherKind::Etherscan
657 }
658
659 fn timeout(&self) -> Duration {
660 Duration::from_secs(1)
661 }
662
663 fn concurrency(&self) -> usize {
664 5
665 }
666
667 fn invalid_api_key(&self) -> &AtomicBool {
668 &self.invalid_api_key
669 }
670
671 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
672 self.client.contract_source_code(address).await.map(|mut metadata| metadata.items.pop())
673 }
674}
675
676struct SourcifyFetcher {
677 client: reqwest::Client,
678 chain: Chain,
679 url: String,
680 invalid_api_key: AtomicBool,
681}
682
683impl SourcifyFetcher {
684 fn new(chain: Chain) -> Self {
685 Self {
686 client: reqwest::Client::builder()
687 .user_agent(foundry_common::DEFAULT_USER_AGENT)
688 .build()
689 .expect("Client::builder() with static config cannot fail"),
690 chain,
691 url: format!("https://sourcify.dev/server/v2/contract/{}", chain.id()),
692 invalid_api_key: AtomicBool::new(false),
693 }
694 }
695}
696
697#[async_trait::async_trait]
698impl ExternalFetcherT for SourcifyFetcher {
699 fn kind(&self) -> FetcherKind {
700 FetcherKind::Sourcify
701 }
702
703 fn timeout(&self) -> Duration {
704 Duration::from_secs(1)
705 }
706
707 fn concurrency(&self) -> usize {
708 5
709 }
710
711 fn invalid_api_key(&self) -> &AtomicBool {
712 &self.invalid_api_key
713 }
714
715 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
716 let url = format!("{url}/{address}?fields=abi,compilation", url = self.url);
717 let response = self
718 .client
719 .get(url)
720 .send()
721 .await
722 .map_err(|e| EtherscanError::Unknown(e.to_string()))?;
723 let code = response.status();
724 match code.as_u16() {
725 404 => return Err(EtherscanError::ContractCodeNotVerified(address)),
727 429 => return Err(EtherscanError::RateLimitExceeded),
729 400 => {
731 let error = response
732 .json::<SourcifyError>()
733 .await
734 .map_err(|e| EtherscanError::Unknown(e.to_string()))?;
735 return Err(match error.custom_code.as_str() {
736 "unsupported_chain" => EtherscanError::ChainNotSupported(self.chain),
737 "invalid_parameter" => EtherscanError::ContractCodeNotVerified(address),
738 _ => EtherscanError::Unknown(format!("{error:#?}")),
739 });
740 }
741 _ => {}
742 }
743 let response: SourcifyResponse =
744 response.json().await.map_err(|e| EtherscanError::Unknown(e.to_string()))?;
745 trace!(target: "evm::traces::external", "Sourcify response for {address}: {response:#?}");
746 match response {
747 SourcifyResponse::Success(metadata) => Ok(Some(metadata.into())),
748 SourcifyResponse::Error(error) => Err(EtherscanError::Unknown(format!("{error:#?}"))),
749 }
750 }
751}
752
753#[derive(Debug, Clone, Deserialize)]
755#[serde(untagged)]
756enum SourcifyResponse {
757 Success(SourcifyMetadata),
758 Error(SourcifyError),
759}
760
761#[derive(Debug, Clone, Deserialize)]
762#[serde(rename_all = "camelCase")]
763#[expect(dead_code)] struct SourcifyError {
765 custom_code: String,
766 message: String,
767 error_id: String,
768}
769
770#[derive(Debug, Clone, Deserialize)]
771#[serde(rename_all = "camelCase")]
772struct SourcifyMetadata {
773 #[serde(default)]
774 abi: Option<Box<serde_json::value::RawValue>>,
775 #[serde(default)]
776 compilation: Option<Compilation>,
777}
778
779#[derive(Debug, Clone, Deserialize)]
780#[serde(rename_all = "camelCase")]
781struct Compilation {
782 #[serde(default)]
783 compiler_version: String,
784 #[serde(default)]
785 name: String,
786}
787
788impl From<SourcifyMetadata> for Metadata {
789 fn from(metadata: SourcifyMetadata) -> Self {
790 let SourcifyMetadata { abi, compilation } = metadata;
791 let (contract_name, compiler_version) = compilation
792 .map(|c| (c.name, c.compiler_version))
793 .unwrap_or_else(|| (String::new(), String::new()));
794 Self {
796 source_code: foundry_block_explorers::contract::SourceCodeMetadata::Sources(
797 Default::default(),
798 ),
799 abi: Box::<str>::from(abi.unwrap_or_default()).into(),
800 contract_name,
801 compiler_version,
802 optimization_used: 0,
803 runs: 0,
804 constructor_arguments: Default::default(),
805 evm_version: String::new(),
806 library: String::new(),
807 license_type: String::new(),
808 proxy: 0,
809 implementation: None,
810 swarm_source: String::new(),
811 }
812 }
813}
814
815#[cfg(test)]
816mod tests {
817 use super::*;
818 use std::{
819 collections::HashSet as StdHashSet,
820 future::pending,
821 io::{Read, Write},
822 net::TcpListener,
823 sync::{
824 Mutex,
825 atomic::{AtomicUsize, Ordering as AtomicOrdering},
826 },
827 };
828
829 struct TestFetcher {
830 kind: FetcherKind,
831 delay: Option<Duration>,
832 contract_name: Option<&'static str>,
833 calls: Arc<AtomicUsize>,
834 invalid: AtomicBool,
835 }
836
837 #[async_trait::async_trait]
838 impl ExternalFetcherT for TestFetcher {
839 fn kind(&self) -> FetcherKind {
840 self.kind
841 }
842
843 fn timeout(&self) -> Duration {
844 Duration::from_millis(1)
845 }
846
847 fn concurrency(&self) -> usize {
848 1
849 }
850
851 fn invalid_api_key(&self) -> &AtomicBool {
852 &self.invalid
853 }
854
855 async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
856 self.calls.fetch_add(1, AtomicOrdering::Relaxed);
857 let Some(delay) = self.delay else { return pending().await };
858 if !delay.is_zero() {
859 tokio::time::sleep(delay).await;
860 }
861 Ok(self.contract_name.map(metadata))
862 }
863 }
864
865 struct RateLimitedFetcher {
866 calls: Arc<AtomicUsize>,
867 invalid: AtomicBool,
868 }
869
870 #[async_trait::async_trait]
871 impl ExternalFetcherT for RateLimitedFetcher {
872 fn kind(&self) -> FetcherKind {
873 FetcherKind::Sourcify
874 }
875
876 fn timeout(&self) -> Duration {
877 Duration::from_millis(5)
878 }
879
880 fn concurrency(&self) -> usize {
881 1
882 }
883
884 fn invalid_api_key(&self) -> &AtomicBool {
885 &self.invalid
886 }
887
888 async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
889 self.calls.fetch_add(1, AtomicOrdering::Relaxed);
890 Err(EtherscanError::RateLimitExceeded)
891 }
892 }
893
894 struct ErrorFetcher {
895 calls: Arc<AtomicUsize>,
896 invalid: AtomicBool,
897 }
898
899 #[async_trait::async_trait]
900 impl ExternalFetcherT for ErrorFetcher {
901 fn kind(&self) -> FetcherKind {
902 FetcherKind::Etherscan
903 }
904
905 fn timeout(&self) -> Duration {
906 Duration::ZERO
907 }
908
909 fn concurrency(&self) -> usize {
910 1
911 }
912
913 fn invalid_api_key(&self) -> &AtomicBool {
914 &self.invalid
915 }
916
917 async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
918 self.calls.fetch_add(1, AtomicOrdering::Relaxed);
919 Err(EtherscanError::Unknown("temporary explorer failure".to_string()))
920 }
921 }
922
923 fn metadata(contract_name: &str) -> Metadata {
924 SourcifyMetadata {
925 abi: None,
926 compilation: Some(Compilation {
927 compiler_version: String::new(),
928 name: contract_name.to_string(),
929 }),
930 }
931 .into()
932 }
933
934 fn test_identifier(
935 fetchers: Vec<Arc<dyn ExternalFetcherT>>,
936 remaining_budget: Duration,
937 ) -> ExternalIdentifier {
938 ExternalIdentifier { fetchers, contracts: Default::default(), remaining_budget }
939 }
940
941 #[test]
942 fn zero_timeout_disables_external_identification() {
943 let mut config = Config::default();
944 config.tracing.external_identification_timeout = 0;
945
946 assert!(ExternalIdentifier::new(&config, Some(Chain::mainnet())).unwrap().is_none());
947 }
948
949 #[test]
950 fn alias_for_another_chain_does_not_redirect_lookups() {
951 let config = ExternalIdentifierConfig {
952 timeout: 1,
953 etherscan: serde_json::from_value(serde_json::json!({
954 "mainnet": { "key": "key" }
955 }))
956 .unwrap(),
957 etherscan_alias: Some("mainnet".to_string()),
958 etherscan_api_key: Some("mainnet".to_string()),
959 ..Default::default()
960 };
961
962 assert!(config.storage_identifier(Chain::from(8453)).is_none());
963
964 let kinds = |identifier: ExternalIdentifier| {
965 identifier.fetchers.iter().map(|fetcher| fetcher.kind()).collect::<Vec<_>>()
966 };
967 assert_eq!(
968 kinds(config.identifier(Some(Chain::from(8453))).unwrap()),
969 [FetcherKind::Sourcify]
970 );
971 assert_eq!(
972 kinds(config.identifier(Some(Chain::mainnet())).unwrap()),
973 [FetcherKind::Sourcify, FetcherKind::Etherscan]
974 );
975
976 let config = ExternalIdentifierConfig {
978 etherscan: serde_json::from_value(serde_json::json!({
979 "mainnet": { "key": "key" },
980 "base": { "key": "key" }
981 }))
982 .unwrap(),
983 ..config
984 };
985 let (chain, etherscan) =
986 config.resolve_explorer(Some(Chain::from(8453)), Some("mainnet"), Some("mainnet"));
987 assert_eq!(chain, Some(Chain::from(8453)));
988 assert_eq!(etherscan.unwrap().chain, Some(Chain::from(8453)));
989
990 let config = ExternalIdentifierConfig {
992 etherscan: serde_json::from_value(serde_json::json!({
993 "custom": { "key": "key", "url": "https://explorer.invalid/api" }
994 }))
995 .unwrap(),
996 etherscan_alias: Some("custom".to_string()),
997 etherscan_api_key: Some("custom".to_string()),
998 ..config
999 };
1000 assert_eq!(
1001 kinds(config.identifier(Some(Chain::from(8453))).unwrap()),
1002 [FetcherKind::Sourcify, FetcherKind::Etherscan]
1003 );
1004 }
1005
1006 struct FlakyCloudflareFetcher {
1009 seen: Mutex<StdHashSet<Address>>,
1010 invalid: AtomicBool,
1011 }
1012
1013 #[async_trait::async_trait]
1014 impl ExternalFetcherT for FlakyCloudflareFetcher {
1015 fn kind(&self) -> FetcherKind {
1016 FetcherKind::Etherscan
1017 }
1018 fn timeout(&self) -> Duration {
1019 Duration::from_millis(1)
1020 }
1021 fn concurrency(&self) -> usize {
1022 1
1023 }
1024 fn invalid_api_key(&self) -> &AtomicBool {
1025 &self.invalid
1026 }
1027 async fn fetch(&self, address: Address) -> Result<Option<Metadata>, EtherscanError> {
1028 let first_time = self.seen.lock().unwrap().insert(address);
1029 if first_time { Err(EtherscanError::BlockedByCloudflare) } else { Ok(None) }
1030 }
1031 }
1032
1033 #[tokio::test]
1037 async fn cloudflare_block_retries_instead_of_abandoning_queue() {
1038 let addrs: Vec<Address> = (1u8..=4).map(Address::with_last_byte).collect();
1039 let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(FlakyCloudflareFetcher {
1040 seen: Mutex::new(StdHashSet::new()),
1041 invalid: AtomicBool::new(false),
1042 });
1043
1044 let collected: Vec<_> = ExternalFetcher::new(fetcher, &addrs).collect().await;
1045
1046 let got: StdHashSet<Address> = collected.into_iter().map(|(addr, _)| addr).collect();
1047 let want: StdHashSet<Address> = addrs.into_iter().collect();
1048 assert_eq!(got, want, "every address must be yielded despite a transient cloudflare block");
1049 }
1050
1051 #[tokio::test(start_paused = true)]
1052 async fn timeout_keeps_partial_results_and_opens_circuit() {
1053 let successful_calls = Arc::new(AtomicUsize::new(0));
1054 let stalled_calls = Arc::new(AtomicUsize::new(0));
1055 let fetchers: Vec<Arc<dyn ExternalFetcherT>> = vec![
1056 Arc::new(TestFetcher {
1057 kind: FetcherKind::Sourcify,
1058 delay: Some(Duration::ZERO),
1059 contract_name: Some("PartialResult"),
1060 calls: Arc::clone(&successful_calls),
1061 invalid: AtomicBool::new(false),
1062 }),
1063 Arc::new(TestFetcher {
1064 kind: FetcherKind::Etherscan,
1065 delay: None,
1066 contract_name: None,
1067 calls: Arc::clone(&stalled_calls),
1068 invalid: AtomicBool::new(false),
1069 }),
1070 ];
1071 let mut identifier = test_identifier(fetchers, Duration::from_millis(20));
1072 let address = Address::with_last_byte(1);
1073
1074 identifier.fetch_addresses_async(&[address]).await;
1075
1076 assert!(identifier.remaining_budget.is_zero());
1077 assert_eq!(
1078 identifier.contracts[&address].1.as_ref().unwrap().contract_name,
1079 "PartialResult"
1080 );
1081 assert_eq!(successful_calls.load(AtomicOrdering::Relaxed), 1);
1082 assert_eq!(stalled_calls.load(AtomicOrdering::Relaxed), 1);
1083
1084 identifier.fetch_addresses_async(&[Address::with_last_byte(2)]).await;
1085 assert_eq!(successful_calls.load(AtomicOrdering::Relaxed), 1);
1086 assert_eq!(stalled_calls.load(AtomicOrdering::Relaxed), 1);
1087 }
1088
1089 #[tokio::test(flavor = "multi_thread")]
1090 async fn timeout_returns_partial_identity() {
1091 let fetchers: Vec<Arc<dyn ExternalFetcherT>> = vec![
1092 Arc::new(TestFetcher {
1093 kind: FetcherKind::Sourcify,
1094 delay: Some(Duration::ZERO),
1095 contract_name: Some("PartialResult"),
1096 calls: Arc::new(AtomicUsize::new(0)),
1097 invalid: AtomicBool::new(false),
1098 }),
1099 Arc::new(TestFetcher {
1100 kind: FetcherKind::Etherscan,
1101 delay: None,
1102 contract_name: None,
1103 calls: Arc::new(AtomicUsize::new(0)),
1104 invalid: AtomicBool::new(false),
1105 }),
1106 ];
1107 let mut identifier = test_identifier(fetchers, Duration::from_millis(20));
1108 let mut node = CallTraceNode::default();
1109 node.trace.address = Address::with_last_byte(1);
1110
1111 let identities = identifier.identify_addresses(&[&node]);
1112
1113 assert_eq!(identities.len(), 1);
1114 assert_eq!(identities[0].label.as_deref(), Some("PartialResult"));
1115 }
1116
1117 #[tokio::test(start_paused = true)]
1118 async fn timeout_budget_is_cumulative_across_fetches() {
1119 let calls = Arc::new(AtomicUsize::new(0));
1120 let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(TestFetcher {
1121 kind: FetcherKind::Sourcify,
1122 delay: Some(Duration::from_millis(20)),
1123 contract_name: Some("FirstResult"),
1124 calls: Arc::clone(&calls),
1125 invalid: AtomicBool::new(false),
1126 });
1127 let mut identifier = test_identifier(vec![fetcher], Duration::from_millis(30));
1128 let first = Address::with_last_byte(1);
1129 let second = Address::with_last_byte(2);
1130
1131 identifier.fetch_addresses_async(&[first]).await;
1132 assert!(identifier.contracts[&first].1.is_some());
1133 assert!(identifier.remaining_budget < Duration::from_millis(15));
1134
1135 identifier.fetch_addresses_async(&[second]).await;
1136 assert!(identifier.remaining_budget.is_zero());
1137 assert!(!identifier.contracts.contains_key(&second));
1138 assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
1139 }
1140
1141 #[tokio::test(start_paused = true)]
1142 async fn metadata_requests_preserve_cumulative_budget() {
1143 let calls = Arc::new(AtomicUsize::new(0));
1144 let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(TestFetcher {
1145 kind: FetcherKind::Etherscan,
1146 delay: None,
1147 contract_name: None,
1148 calls: Arc::clone(&calls),
1149 invalid: AtomicBool::new(false),
1150 });
1151 let mut identifier = test_identifier(vec![fetcher], Duration::from_millis(30));
1152 let address = Address::with_last_byte(1);
1153
1154 assert!(identifier.get_metadata(&[address], Duration::from_millis(10)).await.is_empty());
1155 assert!(!identifier.remaining_budget.is_zero());
1156 assert!(identifier.remaining_budget <= Duration::from_millis(20));
1157 assert!(identifier.get_metadata(&[address], Duration::from_secs(1)).await.is_empty());
1158 assert!(identifier.remaining_budget.is_zero());
1159 assert!(identifier.get_metadata(&[address], Duration::from_secs(1)).await.is_empty());
1160 assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
1161 }
1162
1163 #[tokio::test(start_paused = true)]
1164 async fn rate_limit_retries_cannot_escape_timeout_budget() {
1165 let calls = Arc::new(AtomicUsize::new(0));
1166 let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(RateLimitedFetcher {
1167 calls: Arc::clone(&calls),
1168 invalid: AtomicBool::new(false),
1169 });
1170 let mut identifier = test_identifier(vec![fetcher], Duration::from_millis(20));
1171
1172 identifier.fetch_addresses_async(&[Address::with_last_byte(1)]).await;
1173
1174 assert!(identifier.remaining_budget.is_zero());
1175 assert!(calls.load(AtomicOrdering::Relaxed) > 1);
1176 }
1177
1178 #[tokio::test(start_paused = true)]
1179 async fn rate_limit_retries_are_bounded() {
1180 let calls = Arc::new(AtomicUsize::new(0));
1181 let fetcher: Arc<dyn ExternalFetcherT> = Arc::new(RateLimitedFetcher {
1182 calls: Arc::clone(&calls),
1183 invalid: AtomicBool::new(false),
1184 });
1185
1186 let fetched =
1187 ExternalFetcher::new(fetcher, &[Address::with_last_byte(1)]).collect::<Vec<_>>().await;
1188
1189 assert!(fetched.is_empty());
1190 assert_eq!(calls.load(AtomicOrdering::Relaxed), MAX_TRANSIENT_RETRIES as usize + 1);
1191 }
1192
1193 struct UnsupportedChainFetcher {
1194 calls: Arc<AtomicUsize>,
1195 invalid: AtomicBool,
1196 }
1197
1198 #[async_trait::async_trait]
1199 impl ExternalFetcherT for UnsupportedChainFetcher {
1200 fn kind(&self) -> FetcherKind {
1201 FetcherKind::Sourcify
1202 }
1203
1204 fn timeout(&self) -> Duration {
1205 Duration::ZERO
1206 }
1207
1208 fn concurrency(&self) -> usize {
1209 1
1210 }
1211
1212 fn invalid_api_key(&self) -> &AtomicBool {
1213 &self.invalid
1214 }
1215
1216 async fn fetch(&self, _address: Address) -> Result<Option<Metadata>, EtherscanError> {
1217 self.calls.fetch_add(1, AtomicOrdering::Relaxed);
1218 Err(EtherscanError::ChainNotSupported(Chain::from_id(12345)))
1219 }
1220 }
1221
1222 #[tokio::test]
1223 async fn unsupported_chain_disables_fetcher() {
1224 let calls = Arc::new(AtomicUsize::new(0));
1225 let mut identifier = test_identifier(
1226 vec![Arc::new(UnsupportedChainFetcher {
1227 calls: Arc::clone(&calls),
1228 invalid: AtomicBool::new(false),
1229 })],
1230 Duration::from_secs(1),
1231 );
1232
1233 identifier
1234 .fetch_addresses_async(&[Address::with_last_byte(1), Address::with_last_byte(2)])
1235 .await;
1236 identifier.fetch_addresses_async(&[Address::with_last_byte(3)]).await;
1237
1238 assert_eq!(calls.load(AtomicOrdering::Relaxed), 1);
1239 }
1240
1241 #[test]
1242 fn sourcify_skips_local_chains() {
1243 let config = ExternalIdentifierConfig { timeout: 1, ..Default::default() };
1244
1245 assert!(config.identifier(Some(Chain::from_id(31337))).is_none());
1246 assert!(config.identifier(Some(Chain::dev())).is_none());
1247 assert!(config.identifier(Some(Chain::mainnet())).is_some());
1248 }
1249
1250 #[tokio::test(flavor = "multi_thread")]
1251 async fn foundry_addresses_are_not_fetched() {
1252 let calls = Arc::new(AtomicUsize::new(0));
1253 let mut identifier = test_identifier(
1254 vec![Arc::new(TestFetcher {
1255 kind: FetcherKind::Sourcify,
1256 delay: Some(Duration::ZERO),
1257 contract_name: None,
1258 calls: Arc::clone(&calls),
1259 invalid: AtomicBool::new(false),
1260 })],
1261 Duration::from_secs(1),
1262 );
1263 let nodes = [CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS].map(|address| {
1264 let mut node = CallTraceNode::default();
1265 node.trace.address = address;
1266 node
1267 });
1268
1269 assert!(identifier.identify_addresses(&[&nodes[0], &nodes[1]]).is_empty());
1270 assert_eq!(calls.load(AtomicOrdering::Relaxed), 0);
1271 }
1272
1273 async fn sourcify_bad_request(body: &'static str) -> EtherscanError {
1275 let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1276 let addr = listener.local_addr().unwrap();
1277 std::thread::spawn(move || {
1278 let (mut stream, _) = listener.accept().unwrap();
1279 let _ = stream.read(&mut [0; 4096]);
1280 let response = format!(
1281 "HTTP/1.1 400 Bad Request\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
1282 body.len()
1283 );
1284 stream.write_all(response.as_bytes()).unwrap();
1285 });
1286 let mut fetcher = SourcifyFetcher::new(Chain::mainnet());
1287 fetcher.url = format!("http://{addr}");
1288 fetcher.fetch(Address::with_last_byte(1)).await.unwrap_err()
1289 }
1290
1291 #[tokio::test]
1292 async fn sourcify_bad_request_is_final_only_when_recognized() {
1293 let error =
1294 sourcify_bad_request(r#"{"customCode":"unsupported_chain","message":"","errorId":""}"#)
1295 .await;
1296 assert!(matches!(error, EtherscanError::ChainNotSupported(_)));
1297
1298 let error =
1299 sourcify_bad_request(r#"{"customCode":"invalid_parameter","message":"","errorId":""}"#)
1300 .await;
1301 assert!(matches!(error, EtherscanError::ContractCodeNotVerified(_)));
1302
1303 let error =
1304 sourcify_bad_request(r#"{"customCode":"other","message":"","errorId":""}"#).await;
1305 assert!(matches!(error, EtherscanError::Unknown(_)));
1306
1307 let error = sourcify_bad_request("<html>bad request</html>").await;
1308 assert!(matches!(error, EtherscanError::Unknown(_)));
1309 }
1310
1311 #[tokio::test]
1312 async fn transient_errors_are_not_cached_as_unverified() {
1313 let calls = Arc::new(AtomicUsize::new(0));
1314 let mut identifier = test_identifier(
1315 vec![Arc::new(ErrorFetcher {
1316 calls: Arc::clone(&calls),
1317 invalid: AtomicBool::new(false),
1318 })],
1319 Duration::from_secs(1),
1320 );
1321 let address = Address::with_last_byte(1);
1322
1323 assert!(identifier.get_metadata(&[address], Duration::from_secs(1)).await.is_empty());
1324 assert!(identifier.get_metadata(&[address], Duration::from_secs(1)).await.is_empty());
1325 assert_eq!(calls.load(AtomicOrdering::Relaxed), 2);
1326 }
1327
1328 #[test]
1329 fn etherscan_metadata_takes_precedence() {
1330 let address = Address::with_last_byte(1);
1331 let mut identifier = test_identifier(Vec::new(), Duration::ZERO);
1332
1333 identifier
1334 .cache_fetched(address, (FetcherKind::Sourcify, Some(metadata("SourcifyResult"))));
1335 identifier.cache_fetched(address, (FetcherKind::Etherscan, None));
1336 assert_eq!(
1337 identifier.contracts[&address].1.as_ref().unwrap().contract_name,
1338 "SourcifyResult"
1339 );
1340
1341 identifier
1342 .cache_fetched(address, (FetcherKind::Etherscan, Some(metadata("EtherscanResult"))));
1343 assert_eq!(
1344 identifier.contracts[&address].1.as_ref().unwrap().contract_name,
1345 "EtherscanResult"
1346 );
1347 }
1348
1349 #[tokio::test]
1350 async fn proxy_metadata_preserves_address_identity_and_all_abis() {
1351 let proxy = Address::with_last_byte(1);
1352 let implementation_address = Address::with_last_byte(2);
1353 let mut proxy_metadata = metadata("Proxy");
1354 proxy_metadata.abi =
1355 r#"[{"anonymous":false,"inputs":[],"name":"ProxyEvent","type":"event"}]"#.to_string();
1356 proxy_metadata.proxy = 1;
1357 proxy_metadata.implementation = Some(implementation_address);
1358 let mut implementation = metadata("Implementation");
1359 implementation.abi =
1360 r#"[{"anonymous":false,"inputs":[],"name":"ImplementationEvent","type":"event"}]"#
1361 .to_string();
1362 let mut identifier = test_identifier(Vec::new(), Duration::from_secs(1));
1363 let identity = identifier.identify_from_metadata(proxy, &proxy_metadata);
1364 assert_eq!(identity.contract.as_deref(), Some("Proxy"));
1365 identifier.cache_fetched(proxy, (FetcherKind::Etherscan, Some(proxy_metadata)));
1366 identifier
1367 .cache_fetched(implementation_address, (FetcherKind::Etherscan, Some(implementation)));
1368
1369 let mut results = identifier.get_abis(&[proxy]).await;
1370 let (result_address, result) = results.pop().unwrap();
1371 let (abis, complete) = result.unwrap();
1372 let event_names =
1373 abis.into_iter().map(|abi| abi.events.into_keys().next().unwrap()).collect::<Vec<_>>();
1374
1375 assert_eq!(result_address, proxy);
1376 assert!(complete);
1377 assert_eq!(event_names, ["ImplementationEvent", "ProxyEvent"]);
1378
1379 identifier.contracts.remove(&implementation_address);
1380 let (_, result) = identifier.get_abis(&[proxy]).await.pop().unwrap();
1381 let (abis, complete) = result.unwrap();
1382 assert_eq!(abis.len(), 1);
1383 assert!(!complete);
1384 }
1385
1386 #[tokio::test]
1387 async fn storage_metadata_does_not_follow_explorer_proxy_hints() {
1388 let proxy = Address::with_last_byte(1);
1389 let implementation_address = Address::with_last_byte(2);
1390 let mut proxy_metadata = metadata("Proxy");
1391 proxy_metadata.proxy = 1;
1392 proxy_metadata.implementation = Some(implementation_address);
1393 let mut identifier = test_identifier(Vec::new(), Duration::from_secs(1));
1394 identifier.cache_fetched(proxy, (FetcherKind::Etherscan, Some(proxy_metadata)));
1395 identifier.cache_fetched(
1396 implementation_address,
1397 (FetcherKind::Etherscan, Some(metadata("CurrentImplementation"))),
1398 );
1399
1400 let result = identifier.get_metadata(&[proxy], Duration::from_secs(1)).await;
1401
1402 assert_eq!(result[&proxy].as_ref().unwrap().contract_name, "Proxy");
1403 assert_eq!(result.len(), 1);
1404 }
1405}