diff --git a/Cargo.lock b/Cargo.lock index c8767e58d614f..d38005c4dd662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1823,6 +1823,7 @@ dependencies = [ name = "foundry-utils" version = "0.2.0" dependencies = [ + "dunce", "ethers", "ethers-addressbook", "ethers-core", @@ -1830,12 +1831,15 @@ dependencies = [ "ethers-providers", "ethers-solc", "eyre", + "foundry-config", "hex", "reqwest", "rlp", "rustc-hex", + "semver", "serde", "serde_json", + "tempfile", "tokio", "tracing-subscriber", ] diff --git a/cli/src/cmd/cast/run.rs b/cli/src/cmd/cast/run.rs index a4a48038135a3..ea8df405eaae5 100644 --- a/cli/src/cmd/cast/run.rs +++ b/cli/src/cmd/cast/run.rs @@ -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 = 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 = BTreeMap::new(); +fn run_debugger( + result: RunResult, + decoder: CallTraceDecoder, + known_contracts: BTreeMap, + sources: BTreeMap, +) -> eyre::Result<()> { let calls: Vec = 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(()), } diff --git a/cli/src/cmd/forge/run.rs b/cli/src/cmd/forge/run.rs index d9c9b6f63afdc..1551bc49d166b 100644 --- a/cli/src/cmd/forge/run.rs +++ b/cli/src/cmd/forge/run.rs @@ -161,13 +161,13 @@ impl Cmd for RunArgs { // TODO: Could we use the Etherscan identifier here? Main issue: Pulling source code and // bytecode. Might be better to wait for an interactive debugger where we can do this on // the fly while retaining access to the database? - let local_identifier = LocalTraceIdentifier::new(&known_contracts); + let mut local_identifier = LocalTraceIdentifier::new(&known_contracts); let mut decoder = CallTraceDecoderBuilder::new() .with_labels(result.labeled_addresses.clone()) .with_events(local_identifier.events()) .build(); for (_, trace) in &mut result.traces { - decoder.identify(trace, &local_identifier); + decoder.identify(trace, &mut local_identifier); } if self.debug { @@ -195,10 +195,14 @@ impl Cmd for RunArgs { 0, decoder.contracts, highlevel_known_contracts + .clone() .into_iter() .map(|(id, artifact)| (id.name, artifact)) .collect(), - source_code, + highlevel_known_contracts + .into_iter() + .map(|(id, _)| (id.name, source_code.clone())) + .collect(), )?; match tui.start().expect("Failed to start tui") { TUIExitReason::CharExit => return Ok(()), diff --git a/cli/src/cmd/forge/test.rs b/cli/src/cmd/forge/test.rs index 07da771fad820..094007774fe4c 100644 --- a/cli/src/cmd/forge/test.rs +++ b/cli/src/cmd/forge/test.rs @@ -513,16 +513,17 @@ fn test( Ok(TestOutcome::new(results, allow_failure)) } else { // Set up identifiers - let local_identifier = LocalTraceIdentifier::new(&runner.known_contracts); + let mut local_identifier = LocalTraceIdentifier::new(&runner.known_contracts); let remote_chain_id = runner.evm_opts.get_remote_chain_id(); // Do not re-query etherscan for contracts that you've already queried today. // TODO: Make this configurable. let cache_ttl = Duration::from_secs(24 * 60 * 60); - let etherscan_identifier = EtherscanIdentifier::new( + let mut etherscan_identifier = EtherscanIdentifier::new( remote_chain_id, config.etherscan_api_key, remote_chain_id.and_then(Config::foundry_etherscan_cache_dir), cache_ttl, + false, ); // Set up test reporter channel @@ -570,8 +571,8 @@ fn test( // Decode the traces let mut decoded_traces = Vec::new(); for (kind, trace) in &mut result.traces { - decoder.identify(trace, &local_identifier); - decoder.identify(trace, ðerscan_identifier); + decoder.identify(trace, &mut local_identifier); + decoder.identify(trace, &mut etherscan_identifier); let should_include = match kind { // At verbosity level 3, we only display traces for failed tests diff --git a/evm/src/trace/decoder.rs b/evm/src/trace/decoder.rs index 7083f09039bb1..c56c78999d909 100644 --- a/evm/src/trace/decoder.rs +++ b/evm/src/trace/decoder.rs @@ -176,7 +176,7 @@ impl CallTraceDecoder { /// Identify unknown addresses in the specified call trace using the specified identifier. /// /// Unknown contracts are contracts that either lack a label or an ABI. - pub fn identify(&mut self, trace: &CallTraceArena, identifier: &impl TraceIdentifier) { + pub fn identify(&mut self, trace: &CallTraceArena, identifier: &mut impl TraceIdentifier) { let unidentified_addresses = trace .addresses() .into_iter() diff --git a/evm/src/trace/identifier/etherscan.rs b/evm/src/trace/identifier/etherscan.rs index 1b2c8d3ca2fc2..c9faacfc9b8ca 100644 --- a/evm/src/trace/identifier/etherscan.rs +++ b/evm/src/trace/identifier/etherscan.rs @@ -2,7 +2,10 @@ 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::{ @@ -10,7 +13,7 @@ use futures::{ 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}; @@ -18,6 +21,10 @@ use tracing::{trace, warn}; pub struct EtherscanIdentifier { /// The Etherscan client client: Option, + // cache_path: Option, + pub contracts: BTreeMap, + pub sources: BTreeMap, + compiles: bool, } impl EtherscanIdentifier { @@ -29,6 +36,7 @@ impl EtherscanIdentifier { etherscan_api_key: Option, cache_path: Option, 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, BTreeMap)> + { + if self.compiles { + 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>)>, ) -> Vec { self.client.as_ref().map_or(Default::default(), |client| { @@ -57,13 +106,17 @@ impl TraceIdentifier for EtherscanIdentifier { for (addr, _) in addresses { fetcher.push(*addr); } - 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)); + + 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> { 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::().expect("runs parse error"), + item.compiler_version, + ))) } } } diff --git a/evm/src/trace/identifier/local.rs b/evm/src/trace/identifier/local.rs index a3828ea7b6251..d4b09ca00a23d 100644 --- a/evm/src/trace/identifier/local.rs +++ b/evm/src/trace/identifier/local.rs @@ -30,7 +30,7 @@ impl LocalTraceIdentifier { impl TraceIdentifier for LocalTraceIdentifier { fn identify_addresses( - &self, + &mut self, addresses: Vec<(&Address, Option<&Vec>)>, ) -> Vec { addresses diff --git a/evm/src/trace/identifier/mod.rs b/evm/src/trace/identifier/mod.rs index 764abf5a78781..46b5809da725a 100644 --- a/evm/src/trace/identifier/mod.rs +++ b/evm/src/trace/identifier/mod.rs @@ -27,7 +27,7 @@ pub trait TraceIdentifier { /// Attempts to identify an address in one or more call traces. #[allow(clippy::type_complexity)] fn identify_addresses( - &self, + &mut self, addresses: Vec<(&Address, Option<&Vec>)>, ) -> Vec; } diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 6131ece10ee00..213c6174a8dc1 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -56,7 +56,7 @@ pub struct Tui { current_step: usize, identified_contracts: HashMap, known_contracts: HashMap, - source_code: BTreeMap, + known_contracts_sources: HashMap>, } impl Tui { @@ -67,7 +67,7 @@ impl Tui { current_step: usize, identified_contracts: HashMap, known_contracts: HashMap, - source_code: BTreeMap, + known_contracts_sources: HashMap>, ) -> Result { enable_raw_mode()?; let mut stdout = io::stdout(); @@ -82,7 +82,7 @@ impl Tui { current_step, identified_contracts, known_contracts, - source_code, + known_contracts_sources, }) } @@ -106,7 +106,7 @@ impl Tui { address: Address, identified_contracts: &HashMap, known_contracts: &HashMap, - source_code: &BTreeMap, + known_contracts_sources: &HashMap>, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -122,7 +122,7 @@ impl Tui { address, identified_contracts, known_contracts, - source_code, + known_contracts_sources, debug_steps, opcode_list, current_step, @@ -137,7 +137,7 @@ impl Tui { address, identified_contracts, known_contracts, - source_code, + known_contracts_sources, debug_steps, opcode_list, current_step, @@ -155,7 +155,7 @@ impl Tui { address: Address, identified_contracts: &HashMap, known_contracts: &HashMap, - source_code: &BTreeMap, + known_contracts_sources: &HashMap>, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -189,7 +189,7 @@ impl Tui { address, identified_contracts, known_contracts, - source_code, + known_contracts_sources, debug_steps[current_step].ic, call_kind, src_pane, @@ -226,7 +226,7 @@ impl Tui { address: Address, identified_contracts: &HashMap, known_contracts: &HashMap, - source_code: &BTreeMap, + known_contracts_sources: &HashMap>, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -266,7 +266,7 @@ impl Tui { address, identified_contracts, known_contracts, - source_code, + known_contracts_sources, debug_steps[current_step].ic, call_kind, src_pane, @@ -328,7 +328,7 @@ impl Tui { address: Address, identified_contracts: &HashMap, known_contracts: &HashMap, - source_code: &BTreeMap, + known_contracts_sources: &HashMap>, ic: usize, call_kind: CallKind, area: Rect, @@ -346,7 +346,10 @@ impl Tui { let mut text_output: Text = Text::from(""); if let Some(contract_name) = identified_contracts.get(&address) { - if let Some(known) = known_contracts.get(contract_name) { + // todo check + if let (Some(known), Some(source_code)) = + (known_contracts.get(contract_name), known_contracts_sources.get(contract_name)) + { // grab either the creation source map or runtime sourcemap if let Some(sourcemap) = if matches!(call_kind, CallKind::Create) { known.bytecode.source_map() @@ -410,7 +413,8 @@ impl Tui { }; let max_line_num = num_lines.to_string().len(); - // We check if there is other text on the same line before the + // We check if there is other text on the same line before + // the // highlight starts if let Some(last) = before.pop() { if !last.ends_with('\n') { @@ -1178,7 +1182,7 @@ impl Ui for Tui { debug_call[draw_memory.inner_call_index].0, &self.identified_contracts, &self.known_contracts, - &self.source_code, + &self.known_contracts_sources, &debug_call[draw_memory.inner_call_index].1[..], &opcode_list, current_step, diff --git a/utils/Cargo.toml b/utils/Cargo.toml index e09a62ad4f232..d1f8160687786 100644 --- a/utils/Cargo.toml +++ b/utils/Cargo.toml @@ -7,6 +7,7 @@ readme = "README.md" repository = "https://github.com/gakonst/foundry" [dependencies] +foundry-config = { path = "../config" } ethers-core = { git = "https://github.com/gakonst/ethers-rs", default-features = false } ethers-etherscan = { git = "https://github.com/gakonst/ethers-rs", default-features = false } ethers-addressbook = { git = "https://github.com/gakonst/ethers-rs", default-features = false } @@ -23,6 +24,9 @@ serde = "1.0.132" serde_json = { version = "1.0.67", default-features = false } tokio = { version = "1.12.0", features = ["rt-multi-thread", "macros"] } rlp = "0.5.1" +dunce = "1.0.2" +tempfile = "3.3.0" +semver = "1.0.5" [dev-dependencies] diff --git a/utils/src/lib.rs b/utils/src/lib.rs index 409462e8ad507..d6cf99ab670d4 100644 --- a/utils/src/lib.rs +++ b/utils/src/lib.rs @@ -10,10 +10,14 @@ use ethers_core::{ }; use ethers_etherscan::Client; use ethers_solc::{ - artifacts::{BytecodeObject, CompactBytecode, CompactContractBytecode}, - ArtifactId, + artifacts::{ + BytecodeObject, CompactBytecode, CompactContractBytecode, ContractBytecodeSome, Source, + Sources, + }, + ArtifactId, Solc, }; use eyre::{Result, WrapErr}; +use semver::Version; use serde::Deserialize; use std::{ collections::{BTreeMap, HashSet}, @@ -22,6 +26,9 @@ use std::{ str::FromStr, }; +pub use foundry_config::Config; +use std::io::Write; +use tempfile::NamedTempFile; use tokio::runtime::{Handle, Runtime}; #[allow(clippy::large_enum_variant)] @@ -1011,6 +1018,67 @@ pub fn init_tracing_subscriber() { .ok(); } +pub async fn compile_contract_source( + contract_name: String, + source: String, + optimization: bool, + runs: u32, + version: String, +) -> Result<(ArtifactId, ContractBytecodeSome)> { + let mut file = NamedTempFile::new()?; + writeln!(file, "{}", source.clone())?; + + let target_contract = dunce::canonicalize(&file.path())?; + let mut project = Config::default().ephemeral_no_artifacts_project()?; + + if optimization { + project.solc_config.settings.optimizer.enable(); + } else { + project.solc_config.settings.optimizer.disable(); + } + + project.solc_config.settings.optimizer.runs(runs as usize); + // todo what about via-ir + + project.solc = if let Some(solc) = Solc::find_svm_installed_version(&version)? { + solc + } else { + let v: Version = version.trim_start_matches('v').parse()?; + Solc::install(&Version::new(v.major, v.minor, v.patch)).await? + }; + + let mut sources = Sources::new(); + sources.insert(target_contract, Source { content: source }); + + let project_output = project.compile_with_version(&project.solc, sources)?; + + if project_output.has_compiler_errors() { + eyre::bail!(project_output.to_string()) + } + + let (artifact_id, bytecode) = project_output + .into_contract_bytecodes() + .filter_map(|(artifact_id, contract)| { + if artifact_id.name != contract_name { + None + } else { + Some(( + artifact_id, + ContractBytecodeSome { + abi: contract.abi.unwrap(), + bytecode: contract.bytecode.unwrap().into(), + deployed_bytecode: contract.deployed_bytecode.unwrap().into(), + }, + )) + } + }) + .into_iter() + .next() + .expect("there should be a contract with bytecode"); + + Ok((artifact_id, bytecode)) +} + #[cfg(test)] mod tests { use super::*;