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, Future},
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 atomic::{AtomicBool, Ordering},
22 Arc,
23 },
24};
25use tokio::time::{Duration, Interval};
26
27pub struct EtherscanIdentifier {
29 client: Arc<foundry_block_explorers::Client>,
31 invalid_api_key: Arc<AtomicBool>,
36 pub contracts: BTreeMap<Address, Metadata>,
37 pub sources: BTreeMap<u32, String>,
38}
39
40impl EtherscanIdentifier {
41 pub fn new(config: &Config, chain: Option<Chain>) -> eyre::Result<Option<Self>> {
43 if config.offline {
45 return Ok(None);
46 }
47 let Some(config) = config.get_etherscan_config_with_chain(chain)? else {
48 return Ok(None);
49 };
50 trace!(target: "traces::etherscan", chain=?config.chain, url=?config.api_url, "using etherscan identifier");
51 Ok(Some(Self {
52 client: Arc::new(config.into_client()?),
53 invalid_api_key: Arc::new(AtomicBool::new(false)),
54 contracts: BTreeMap::new(),
55 sources: BTreeMap::new(),
56 }))
57 }
58
59 pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
62 let outputs_fut = self
64 .contracts
65 .iter()
66 .filter(|(_, metadata)| !metadata.is_vyper())
68 .map(|(address, metadata)| async move {
69 sh_println!("Compiling: {} {address}", metadata.contract_name)?;
70 let root = tempfile::tempdir()?;
71 let root_path = root.path();
72 let project = etherscan_project(metadata, root_path)?;
73 let output = project.compile()?;
74
75 if output.has_compiler_errors() {
76 eyre::bail!("{output}")
77 }
78
79 Ok((project, output, root))
80 })
81 .collect::<Vec<_>>();
82
83 let outputs = join_all(outputs_fut).await;
85
86 let mut sources: ContractSources = Default::default();
87
88 for res in outputs {
90 let (project, output, _root) = res?;
91 sources.insert(&output, project.root(), None)?;
92 }
93
94 Ok(sources)
95 }
96
97 fn identify_from_metadata(
98 &self,
99 address: Address,
100 metadata: &Metadata,
101 ) -> IdentifiedAddress<'static> {
102 let label = metadata.contract_name.clone();
103 let abi = metadata.abi().ok().map(Cow::Owned);
104 IdentifiedAddress {
105 address,
106 label: Some(label.clone()),
107 contract: Some(label),
108 abi,
109 artifact_id: None,
110 }
111 }
112}
113
114impl TraceIdentifier for EtherscanIdentifier {
115 fn identify_addresses(&mut self, nodes: &[&CallTraceNode]) -> Vec<IdentifiedAddress<'_>> {
116 if self.invalid_api_key.load(Ordering::Relaxed) || nodes.is_empty() {
117 return Vec::new()
118 }
119
120 trace!(target: "evm::traces::etherscan", "identify {} addresses", nodes.len());
121
122 let mut identities = Vec::new();
123 let mut fetcher = EtherscanFetcher::new(
124 self.client.clone(),
125 Duration::from_secs(1),
126 5,
127 Arc::clone(&self.invalid_api_key),
128 );
129
130 for &node in nodes {
131 let address = node.trace.address;
132 if let Some(metadata) = self.contracts.get(&address) {
133 identities.push(self.identify_from_metadata(address, metadata));
134 } else {
135 fetcher.push(address);
136 }
137 }
138
139 let fetched_identities = foundry_common::block_on(
140 fetcher
141 .map(|(address, metadata)| {
142 let addr = self.identify_from_metadata(address, &metadata);
143 self.contracts.insert(address, metadata);
144 addr
145 })
146 .collect::<Vec<IdentifiedAddress<'_>>>(),
147 );
148
149 identities.extend(fetched_identities);
150 identities
151 }
152}
153
154type EtherscanFuture =
155 Pin<Box<dyn Future<Output = (Address, Result<ContractMetadata, EtherscanError>)>>>;
156
157struct EtherscanFetcher {
161 client: Arc<foundry_block_explorers::Client>,
163 timeout: Duration,
165 backoff: Option<Interval>,
167 concurrency: usize,
169 queue: Vec<Address>,
171 in_progress: FuturesUnordered<EtherscanFuture>,
173 invalid_api_key: Arc<AtomicBool>,
175}
176
177impl EtherscanFetcher {
178 fn new(
179 client: Arc<foundry_block_explorers::Client>,
180 timeout: Duration,
181 concurrency: usize,
182 invalid_api_key: Arc<AtomicBool>,
183 ) -> Self {
184 Self {
185 client,
186 timeout,
187 backoff: None,
188 concurrency,
189 queue: Vec::new(),
190 in_progress: FuturesUnordered::new(),
191 invalid_api_key,
192 }
193 }
194
195 fn push(&mut self, address: Address) {
196 self.queue.push(address);
197 }
198
199 fn queue_next_reqs(&mut self) {
200 while self.in_progress.len() < self.concurrency {
201 let Some(addr) = self.queue.pop() else { break };
202 let client = Arc::clone(&self.client);
203 self.in_progress.push(Box::pin(async move {
204 trace!(target: "traces::etherscan", ?addr, "fetching info");
205 let res = client.contract_source_code(addr).await;
206 (addr, res)
207 }));
208 }
209 }
210}
211
212impl Stream for EtherscanFetcher {
213 type Item = (Address, Metadata);
214
215 fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
216 let pin = self.get_mut();
217
218 loop {
219 if let Some(mut backoff) = pin.backoff.take() {
220 if backoff.poll_tick(cx).is_pending() {
221 pin.backoff = Some(backoff);
222 return Poll::Pending
223 }
224 }
225
226 pin.queue_next_reqs();
227
228 let mut made_progress_this_iter = false;
229 match pin.in_progress.poll_next_unpin(cx) {
230 Poll::Pending => {}
231 Poll::Ready(None) => return Poll::Ready(None),
232 Poll::Ready(Some((addr, res))) => {
233 made_progress_this_iter = true;
234 match res {
235 Ok(mut metadata) => {
236 if let Some(item) = metadata.items.pop() {
237 return Poll::Ready(Some((addr, item)))
238 }
239 }
240 Err(EtherscanError::RateLimitExceeded) => {
241 warn!(target: "traces::etherscan", "rate limit exceeded on attempt");
242 pin.backoff = Some(tokio::time::interval(pin.timeout));
243 pin.queue.push(addr);
244 }
245 Err(EtherscanError::InvalidApiKey) => {
246 warn!(target: "traces::etherscan", "invalid api key");
247 pin.invalid_api_key.store(true, Ordering::Relaxed);
249 return Poll::Ready(None)
250 }
251 Err(EtherscanError::BlockedByCloudflare) => {
252 warn!(target: "traces::etherscan", "blocked by cloudflare");
253 pin.invalid_api_key.store(true, Ordering::Relaxed);
255 return Poll::Ready(None)
256 }
257 Err(err) => {
258 warn!(target: "traces::etherscan", "could not get etherscan info: {:?}", err);
259 }
260 }
261 }
262 }
263
264 if !made_progress_this_iter {
265 return Poll::Pending
266 }
267 }
268 }
269}