foundry_evm_traces/identifier/
etherscan.rs

1use super::{IdentifiedAddress, TraceIdentifier};
2use crate::debug::ContractSources;
3use alloy_primitives::Address;
4use foundry_block_explorers::{
5    contract::{ContractMetadata, Metadata},
6    errors::EtherscanError,
7};
8use foundry_common::compile::etherscan_project;
9use foundry_config::{Chain, Config};
10use futures::{
11    future::join_all,
12    stream::{FuturesUnordered, Stream, StreamExt},
13    task::{Context, Poll},
14};
15use revm_inspectors::tracing::types::CallTraceNode;
16use std::{
17    borrow::Cow,
18    collections::BTreeMap,
19    pin::Pin,
20    sync::{
21        Arc,
22        atomic::{AtomicBool, Ordering},
23    },
24};
25use tokio::time::{Duration, Interval};
26
27/// A trace identifier that tries to identify addresses using Etherscan.
28pub struct EtherscanIdentifier {
29    /// The Etherscan client
30    client: Arc<foundry_block_explorers::Client>,
31    /// Tracks whether the API key provides was marked as invalid
32    ///
33    /// After the first [EtherscanError::InvalidApiKey] this will get set to true, so we can
34    /// prevent any further attempts
35    invalid_api_key: Arc<AtomicBool>,
36    pub contracts: BTreeMap<Address, Metadata>,
37    pub sources: BTreeMap<u32, String>,
38}
39
40impl EtherscanIdentifier {
41    /// Creates a new Etherscan identifier with the given client
42    pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Option<Self>> {
43        // In offline mode, don't use Etherscan.
44        if config.offline {
45            return Ok(None);
46        }
47
48        let config = match config.get_etherscan_config_with_chain(chain) {
49            Ok(Some(config)) => config,
50            Ok(None) => {
51                warn!(target: "traces::etherscan", "etherscan config not found");
52                return Ok(None);
53            }
54            Err(err) => {
55                warn!(?err, "failed to get etherscan config");
56                return Ok(None);
57            }
58        };
59
60        trace!(target: "traces::etherscan", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
61        Ok(Some(Self {
62            client: Arc::new(config.into_client()?),
63            invalid_api_key: Arc::new(AtomicBool::new(false)),
64            contracts: BTreeMap::new(),
65            sources: BTreeMap::new(),
66        }))
67    }
68
69    /// Goes over the list of contracts we have pulled from the traces, clones their source from
70    /// Etherscan and compiles them locally, for usage in the debugger.
71    pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
72        let outputs_fut = self
73            .contracts
74            .iter()
75            // filter out vyper files
76            .filter(|(_, metadata)| !metadata.is_vyper())
77            .map(|(address, metadata)| async move {
78                sh_println!("Compiling: {} {address}", metadata.contract_name)?;
79                let root = tempfile::tempdir()?;
80                let root_path = root.path();
81                let project = etherscan_project(metadata, root_path)?;
82                let output = project.compile()?;
83
84                if output.has_compiler_errors() {
85                    eyre::bail!("{output}")
86                }
87
88                Ok((project, output, root))
89            })
90            .collect::<Vec<_>>();
91
92        // poll all the futures concurrently
93        let outputs = join_all(outputs_fut).await;
94
95        let mut sources: ContractSources = Default::default();
96
97        // construct the map
98        for res in outputs {
99            let (project, output, _root) = res?;
100            sources.insert(&output, project.root(), None)?;
101        }
102
103        Ok(sources)
104    }
105
106    fn identify_from_metadata(
107        &self,
108        address: Address,
109        metadata: &Metadata,
110    ) -> IdentifiedAddress<'static> {
111        let label = metadata.contract_name.clone();
112        let abi = metadata.abi().ok().map(Cow::Owned);
113        IdentifiedAddress {
114            address,
115            label: Some(label.clone()),
116            contract: Some(label),
117            abi,
118            artifact_id: None,
119        }
120    }
121}
122
123impl TraceIdentifier for EtherscanIdentifier {
124    fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
125        if self.invalid_api_key.load(Ordering::Relaxed) || nodes.is_empty() {
126            return Vec::new();
127        }
128
129        trace!(target: "evm::traces::etherscan", "identify {} addresses", nodes.len());
130
131        let mut identities = Vec::new();
132        let mut fetcher = EtherscanFetcher::new(
133            self.client.clone(),
134            Duration::from_secs(1),
135            5,
136            Arc::clone(&self.invalid_api_key),
137        );
138
139        for &node in nodes {
140            let address = node.trace.address;
141            if let Some(metadata) = self.contracts.get(&address) {
142                identities.push(self.identify_from_metadata(address, metadata));
143            } else {
144                fetcher.push(address);
145            }
146        }
147
148        let fetched_identities = foundry_common::block_on(
149            fetcher
150                .map(|(address, metadata)| {
151                    let addr = self.identify_from_metadata(address, &metadata);
152                    self.contracts.insert(address, metadata);
153                    addr
154                })
155                .collect::<Vec<IdentifiedAddress<'_>>>(),
156        );
157
158        identities.extend(fetched_identities);
159        identities
160    }
161}
162
163type EtherscanFuture =
164    Pin<Box<dyn Future<Output = (Address, Result<ContractMetadata, EtherscanError>)>>>;
165
166/// A rate limit aware Etherscan client.
167///
168/// Fetches information about multiple addresses concurrently, while respecting rate limits.
169struct EtherscanFetcher {
170    /// The Etherscan client
171    client: Arc<foundry_block_explorers::Client>,
172    /// The time we wait if we hit the rate limit
173    timeout: Duration,
174    /// The interval we are currently waiting for before making a new request
175    backoff: Option<Interval>,
176    /// The maximum amount of requests to send concurrently
177    concurrency: usize,
178    /// The addresses we have yet to make requests for
179    queue: Vec<Address>,
180    /// The in progress requests
181    in_progress: FuturesUnordered<EtherscanFuture>,
182    /// tracks whether the API key provides was marked as invalid
183    invalid_api_key: Arc<AtomicBool>,
184}
185
186impl EtherscanFetcher {
187    fn new(
188        client: Arc<foundry_block_explorers::Client>,
189        timeout: Duration,
190        concurrency: usize,
191        invalid_api_key: Arc<AtomicBool>,
192    ) -> Self {
193        Self {
194            client,
195            timeout,
196            backoff: None,
197            concurrency,
198            queue: Vec::new(),
199            in_progress: FuturesUnordered::new(),
200            invalid_api_key,
201        }
202    }
203
204    fn push(&mut self, address: Address) {
205        self.queue.push(address);
206    }
207
208    fn queue_next_reqs(&mut self) {
209        while self.in_progress.len() < self.concurrency {
210            let Some(addr) = self.queue.pop() else { break };
211            let client = Arc::clone(&self.client);
212            self.in_progress.push(Box::pin(async move {
213                trace!(target: "traces::etherscan", ?addr, "fetching info");
214                let res = client.contract_source_code(addr).await;
215                (addr, res)
216            }));
217        }
218    }
219}
220
221impl Stream for EtherscanFetcher {
222    type Item = (Address, Metadata);
223
224    fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
225        let pin = self.get_mut();
226
227        loop {
228            if let Some(mut backoff) = pin.backoff.take()
229                && backoff.poll_tick(cx).is_pending()
230            {
231                pin.backoff = Some(backoff);
232                return Poll::Pending;
233            }
234
235            pin.queue_next_reqs();
236
237            let mut made_progress_this_iter = false;
238            match pin.in_progress.poll_next_unpin(cx) {
239                Poll::Pending => {}
240                Poll::Ready(None) => return Poll::Ready(None),
241                Poll::Ready(Some((addr, res))) => {
242                    made_progress_this_iter = true;
243                    match res {
244                        Ok(mut metadata) => {
245                            if let Some(item) = metadata.items.pop() {
246                                return Poll::Ready(Some((addr, item)));
247                            }
248                        }
249                        Err(EtherscanError::RateLimitExceeded) => {
250                            warn!(target: "traces::etherscan", "rate limit exceeded on attempt");
251                            pin.backoff = Some(tokio::time::interval(pin.timeout));
252                            pin.queue.push(addr);
253                        }
254                        Err(EtherscanError::InvalidApiKey) => {
255                            warn!(target: "traces::etherscan", "invalid api key");
256                            // mark key as invalid
257                            pin.invalid_api_key.store(true, Ordering::Relaxed);
258                            return Poll::Ready(None);
259                        }
260                        Err(EtherscanError::BlockedByCloudflare) => {
261                            warn!(target: "traces::etherscan", "blocked by cloudflare");
262                            // mark key as invalid
263                            pin.invalid_api_key.store(true, Ordering::Relaxed);
264                            return Poll::Ready(None);
265                        }
266                        Err(err) => {
267                            warn!(target: "traces::etherscan", "could not get etherscan info: {:?}", err);
268                        }
269                    }
270                }
271            }
272
273            if !made_progress_this_iter {
274                return Poll::Pending;
275            }
276        }
277    }
278}