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