-
Notifications
You must be signed in to change notification settings - Fork 2.3k
feat(evm): compile etherscan sources and use them on the debugger #1413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ use cast::trace::CallTraceDecoder; | |
| use clap::Parser; | ||
| use ethers::{ | ||
| abi::Address, | ||
| prelude::{Middleware, Provider}, | ||
| prelude::{artifacts::ContractBytecodeSome, ArtifactId, Middleware, Provider}, | ||
| types::H256, | ||
| }; | ||
| use forge::{ | ||
|
|
@@ -14,11 +14,7 @@ use forge::{ | |
| }; | ||
| use foundry_config::Config; | ||
| use foundry_utils::RuntimeOrHandle; | ||
| use std::{ | ||
| collections::{BTreeMap, HashMap}, | ||
| str::FromStr, | ||
| time::Duration, | ||
| }; | ||
| use std::{collections::BTreeMap, str::FromStr, time::Duration}; | ||
| use ui::{TUIExitReason, Tui, Ui}; | ||
|
|
||
| #[derive(Debug, Clone, Parser)] | ||
|
|
@@ -131,11 +127,12 @@ impl RunArgs { | |
| } | ||
| }; | ||
|
|
||
| let etherscan_identifier = EtherscanIdentifier::new( | ||
| let mut etherscan_identifier = EtherscanIdentifier::new( | ||
| evm_opts.get_remote_chain_id(), | ||
| config.etherscan_api_key, | ||
| Config::foundry_etherscan_cache_dir(evm_opts.get_chain_id()), | ||
| Duration::from_secs(24 * 60 * 60), | ||
| true, | ||
| ); | ||
|
|
||
| let labeled_addresses: BTreeMap<Address, String> = self | ||
|
|
@@ -156,11 +153,13 @@ impl RunArgs { | |
| let mut decoder = CallTraceDecoderBuilder::new().with_labels(labeled_addresses).build(); | ||
|
|
||
| for (_, trace) in &mut result.traces { | ||
| decoder.identify(trace, ðerscan_identifier); | ||
| decoder.identify(trace, &mut etherscan_identifier); | ||
| } | ||
|
|
||
| let (sources, bytecode) = | ||
| etherscan_identifier.get_compiled_contracts_with_sources().await?; | ||
| if self.debug { | ||
| run_debugger(result, decoder)?; | ||
| run_debugger(result, decoder, bytecode, sources)?; | ||
| } else { | ||
| print_traces(&mut result, decoder)?; | ||
| } | ||
|
|
@@ -169,12 +168,29 @@ impl RunArgs { | |
| } | ||
| } | ||
|
|
||
| fn run_debugger(result: RunResult, decoder: CallTraceDecoder) -> eyre::Result<()> { | ||
| // TODO Get source from etherscan | ||
| let source_code: BTreeMap<u32, String> = BTreeMap::new(); | ||
| fn run_debugger( | ||
| result: RunResult, | ||
| decoder: CallTraceDecoder, | ||
| known_contracts: BTreeMap<ArtifactId, ContractBytecodeSome>, | ||
| sources: BTreeMap<ArtifactId, String>, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
| ) -> eyre::Result<()> { | ||
| let calls: Vec<DebugArena> = vec![result.debug]; | ||
| let flattened = calls.last().expect("we should have collected debug info").flatten(0); | ||
| let tui = Tui::new(flattened, 0, decoder.contracts, HashMap::new(), source_code)?; | ||
|
|
||
| let tui = Tui::new( | ||
| flattened, | ||
| 0, | ||
| decoder.contracts, | ||
| known_contracts.into_iter().map(|(id, artifact)| (id.name, artifact)).collect(), | ||
| sources | ||
| .into_iter() | ||
| .map(|(id, source)| { | ||
| let mut sources = BTreeMap::new(); | ||
| sources.insert(0, source); | ||
| (id.name, sources) | ||
| }) | ||
| .collect(), | ||
| )?; | ||
| match tui.start().expect("Failed to start tui") { | ||
| TUIExitReason::CharExit => Ok(()), | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,22 +2,29 @@ use super::{AddressIdentity, TraceIdentifier}; | |
| use ethers::{ | ||
| abi::{Abi, Address}, | ||
| etherscan, | ||
| prelude::{contract::ContractMetadata, errors::EtherscanError}, | ||
| prelude::{ | ||
| artifacts::ContractBytecodeSome, contract::ContractMetadata, errors::EtherscanError, | ||
| ArtifactId, | ||
| }, | ||
| types::Chain, | ||
| }; | ||
| use futures::{ | ||
| future::Future, | ||
| stream::{FuturesUnordered, Stream, StreamExt}, | ||
| task::{Context, Poll}, | ||
| }; | ||
| use std::{borrow::Cow, path::PathBuf, pin::Pin}; | ||
| use std::{borrow::Cow, collections::BTreeMap, path::PathBuf, pin::Pin}; | ||
| use tokio::time::{Duration, Interval}; | ||
| use tracing::{trace, warn}; | ||
|
|
||
| /// A trace identifier that tries to identify addresses using Etherscan. | ||
| pub struct EtherscanIdentifier { | ||
| /// The Etherscan client | ||
| client: Option<etherscan::Client>, | ||
| // cache_path: Option<PathBuf>, | ||
| pub contracts: BTreeMap<String, (String, bool, u32, String)>, | ||
| pub sources: BTreeMap<u32, String>, | ||
| compiles: bool, | ||
| } | ||
|
|
||
| impl EtherscanIdentifier { | ||
|
|
@@ -29,6 +36,7 @@ impl EtherscanIdentifier { | |
| etherscan_api_key: Option<String>, | ||
| cache_path: Option<PathBuf>, | ||
| ttl: Duration, | ||
| compiles: bool, | ||
| ) -> Self { | ||
| if let Some(cache_path) = &cache_path { | ||
| if let Err(err) = std::fs::create_dir_all(cache_path.join("sources")) { | ||
|
|
@@ -39,16 +47,57 @@ impl EtherscanIdentifier { | |
| Self { | ||
| client: chain.and_then(|chain| { | ||
| etherscan_api_key.and_then(|key| { | ||
| etherscan::Client::new_cached(chain.into(), key, cache_path, ttl).ok() | ||
| etherscan::Client::new_cached(chain.into(), key, cache_path.clone(), ttl).ok() | ||
| }) | ||
| }), | ||
| contracts: BTreeMap::new(), | ||
| // cache_path, | ||
| compiles, | ||
| sources: BTreeMap::new(), | ||
| } | ||
| } | ||
|
|
||
| pub async fn get_compiled_contracts_with_sources( | ||
| &self, | ||
| ) -> eyre::Result<(BTreeMap<ArtifactId, String>, BTreeMap<ArtifactId, ContractBytecodeSome>)> | ||
| { | ||
| if self.compiles { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason you opted for a bool here instead of just ommitting the call to
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. older code, part of the planned clean-up |
||
| let mut compiled_contracts = BTreeMap::new(); | ||
| let mut sources = BTreeMap::new(); | ||
|
|
||
| for (label, (source_code, optimization, runs, version)) in &self.contracts { | ||
| // todo skip multi-file, v0.4 and vyper for now | ||
| if source_code.starts_with("{{") || | ||
| version.starts_with("v0.4") || | ||
| version.starts_with("vyper") | ||
| { | ||
| continue | ||
| } | ||
|
|
||
| println!("Compiling {label}"); | ||
| let compiled = foundry_utils::compile_contract_source( | ||
| label.to_string(), | ||
| source_code.clone(), | ||
| *optimization, | ||
| *runs, | ||
| version.to_string(), | ||
| ) | ||
| .await?; | ||
|
|
||
| compiled_contracts.insert(compiled.0.clone(), compiled.1.to_owned()); | ||
| sources.insert(compiled.0.to_owned(), source_code.to_owned()); | ||
| } | ||
|
|
||
| Ok((sources, compiled_contracts)) | ||
| } else { | ||
| Ok((BTreeMap::new(), BTreeMap::new())) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl TraceIdentifier for EtherscanIdentifier { | ||
| fn identify_addresses( | ||
| &self, | ||
| &mut self, | ||
| addresses: Vec<(&Address, Option<&Vec<u8>>)>, | ||
| ) -> Vec<AddressIdentity> { | ||
| self.client.as_ref().map_or(Default::default(), |client| { | ||
|
|
@@ -57,13 +106,17 @@ impl TraceIdentifier for EtherscanIdentifier { | |
| for (addr, _) in addresses { | ||
| fetcher.push(*addr); | ||
| } | ||
|
Comment on lines
106
to
108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can probably filter out addresses here if it is already present in |
||
|
|
||
| let fut = fetcher | ||
| .map(|(address, label, abi)| AddressIdentity { | ||
| address, | ||
| label: Some(label.clone()), | ||
| contract: Some(label), | ||
| abi: Some(Cow::Owned(abi)), | ||
| .map(|(address, label, abi, source_code, optimization, runs, version)| { | ||
| self.contracts | ||
| .insert(label.clone(), (source_code, optimization, runs, version)); | ||
|
Comment on lines
+111
to
+112
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should do this keyed by address instead - if we have multiple addresses with the same label (which IIRC is just the contract name for this identifier) then we might overwrite info for two contracts with the same name, but with different implementations. E.g. if you interact with two tokens and both of them show up as "Token", then only one of them would be decoded... I think |
||
|
|
||
| AddressIdentity { | ||
| address, | ||
| label: Some(label.clone()), | ||
| contract: Some(label), | ||
| abi: Some(Cow::Owned(abi)), | ||
| } | ||
| }) | ||
| .collect(); | ||
|
|
||
|
|
@@ -126,7 +179,7 @@ impl EtherscanFetcher { | |
| } | ||
|
|
||
| impl Stream for EtherscanFetcher { | ||
| type Item = (Address, String, Abi); | ||
| type Item = (Address, String, Abi, String, bool, u32, String); | ||
|
|
||
| fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { | ||
| let pin = self.get_mut(); | ||
|
|
@@ -151,7 +204,15 @@ impl Stream for EtherscanFetcher { | |
| Ok(mut metadata) => { | ||
| if let Some(item) = metadata.items.pop() { | ||
| if let Ok(abi) = serde_json::from_str(&item.abi) { | ||
| return Poll::Ready(Some((addr, item.contract_name, abi))) | ||
| return Poll::Ready(Some(( | ||
| addr, | ||
| item.contract_name, | ||
| abi, | ||
| item.source_code, | ||
| item.optimization_used.eq("1"), | ||
| item.runs.parse::<u32>().expect("runs parse error"), | ||
| item.compiler_version, | ||
| ))) | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably merge this at some earlier point instead of passing both of these to the function since they serve the same purpose
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wanted to clarify this real quick, did you mean merge this
contractmap into one object or merge it with thesourcesmap?