foundry_evm_traces/identifier/
etherscan.rs

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