foundry_evm_traces/identifier/
etherscan.rs1use 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
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
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 pub async fn get_compiled_contracts(&self) -> eyre::Result<ContractSources> {
72 let outputs_fut = self
73 .contracts
74 .iter()
75 .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 let outputs = join_all(outputs_fut).await;
94
95 let mut sources: ContractSources = Default::default();
96
97 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
166struct EtherscanFetcher {
170 client: Arc<foundry_block_explorers::Client>,
172 timeout: Duration,
174 backoff: Option<Interval>,
176 concurrency: usize,
178 queue: Vec<Address>,
180 in_progress: FuturesUnordered<EtherscanFuture>,
182 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 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 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}