From 07468d276d1552c869e83daf1c0efbc39815402d Mon Sep 17 00:00:00 2001 From: franfran Date: Mon, 21 Aug 2023 23:15:43 +0300 Subject: [PATCH 01/17] fuzz single refactor --- crates/evm/src/fuzz/mod.rs | 182 ++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 65 deletions(-) diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index 3d7f0c559aaad..572743f6769fc 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -1,5 +1,6 @@ use crate::{ coverage::HitMaps, + debug::DebugArena, decode::{self, decode_console_logs}, executor::{Executor, RawCallResult}, trace::CallTraceArena, @@ -10,10 +11,11 @@ use ethers::{ types::{Address, Bytes, Log}, }; use eyre::Result; -use foundry_common::{calc, contracts::ContractsByAddress}; +use foundry_common::{calc, contracts::ContractsByAddress, evm::Breakpoints}; use foundry_config::FuzzConfig; pub use proptest::test_runner::Reason; use proptest::test_runner::{TestCaseError, TestError, TestRunner}; +use revm::interpreter::InstructionResult; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, collections::BTreeMap, fmt}; use strategies::{ @@ -25,6 +27,28 @@ pub mod error; pub mod invariant; pub mod strategies; +pub struct CaseOutcome { + pub case: FuzzCase, + pub gas_used: u64, + pub stipend: u64, + pub traces: Option, + pub coverage: Option, + pub debug: Option, + pub breakpoints: Breakpoints, +} + +pub struct CounterExampleOutcome { + pub counterexample: (ethers::types::Bytes, RawCallResult), + pub exit_reason: InstructionResult, + pub debug: Option, + pub breakpoints: Breakpoints, +} + +pub enum FuzzOutcome { + Case(CaseOutcome), + CounterExample(CounterExampleOutcome), +} + /// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/). /// /// After instantiation, calling `fuzz` will proceed to hammer the deployed smart contract with @@ -101,72 +125,45 @@ impl<'a> FuzzedExecutor<'a> { let strat = proptest::strategy::Union::new_weighted(weights); debug!(func = ?func.name, should_fail, "fuzzing"); let run_result = self.runner.clone().run(&strat, |calldata| { - let call = self - .executor - .call_raw(self.sender, address, calldata.0.clone(), 0.into()) - .map_err(|_| TestCaseError::fail(FuzzError::FailedContractCall))?; - let state_changeset = call - .state_changeset - .as_ref() - .ok_or_else(|| TestCaseError::fail(FuzzError::EmptyChangeset))?; - - // Build fuzzer state - collect_state_from_call( - &call.logs, - state_changeset, - state.clone(), - &self.config.dictionary, - ); - - // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME" - if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE { - return Err(TestCaseError::reject(FuzzError::AssumeReject)) - } - - let success = self.executor.is_success( - address, - call.reverted, - state_changeset.clone(), - should_fail, - ); - - if success { - let mut first_case = first_case.borrow_mut(); - if first_case.is_none() { - first_case.replace(FuzzCase { - calldata, - gas: call.gas_used, - stipend: call.stipend, - }); + let fuzz_res = self.single_fuzz(&state, address, should_fail, calldata)?; + + match fuzz_res { + FuzzOutcome::Case(case) => { + let mut first_case = first_case.borrow_mut(); + if first_case.is_none() { + first_case.replace(case.case); + } + gas_by_case.borrow_mut().push((case.gas_used, case.stipend)); + + traces.replace(case.traces); + + if let Some(prev) = coverage.take() { + // Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must + // necessarily also be `Some` + coverage.replace(Some(prev.merge(case.coverage.unwrap()))); + } else { + coverage.replace(case.coverage); + } + + Ok(()) } - gas_by_case.borrow_mut().push((call.gas_used, call.stipend)); - - traces.replace(call.traces); - - if let Some(prev) = coverage.take() { - // Safety: If `Option::or` evaluates to `Some`, then `call.coverage` must - // necessarily also be `Some` - coverage.replace(Some(prev.merge(call.coverage.unwrap()))); - } else { - coverage.replace(call.coverage); + FuzzOutcome::CounterExample(CounterExampleOutcome { + exit_reason, + counterexample: _counterexample, + .. + }) => { + let status = exit_reason; + // We cannot use the calldata returned by the test runner in `TestError::Fail`, + // since that input represents the last run case, which may not correspond with + // our failure - when a fuzz case fails, proptest will try + // to run at least one more case to find a minimal failure + // case. + let call_res = _counterexample.1.result.clone(); + *counterexample.borrow_mut() = _counterexample; + Err(TestCaseError::fail( + decode::decode_revert(&call_res, errors, Some(status)).unwrap_or_default(), + )) } - - Ok(()) - } else { - let status = call.exit_reason; - // We cannot use the calldata returned by the test runner in `TestError::Fail`, - // since that input represents the last run case, which may not correspond with our - // failure - when a fuzz case fails, proptest will try to run at least one more - // case to find a minimal failure case. - *counterexample.borrow_mut() = (calldata, call); - Err(TestCaseError::fail( - decode::decode_revert( - counterexample.borrow().1.result.as_ref(), - errors, - Some(status), - ) - .unwrap_or_default(), - )) } }); @@ -216,6 +213,61 @@ impl<'a> FuzzedExecutor<'a> { result } + + pub fn single_fuzz( + &self, + state: &EvmFuzzState, + address: Address, + should_fail: bool, + calldata: ethers::types::Bytes, + ) -> Result { + let call = self + .executor + .call_raw(self.sender, address, calldata.0.clone(), 0.into()) + .map_err(|_| TestCaseError::fail(FuzzError::FailedContractCall))?; + let state_changeset = call + .state_changeset + .as_ref() + .ok_or_else(|| TestCaseError::fail(FuzzError::EmptyChangeset))?; + + // Build fuzzer state + collect_state_from_call( + &call.logs, + state_changeset, + state.clone(), + &self.config.dictionary, + ); + + // When assume cheat code is triggered return a special string "FOUNDRY::ASSUME" + if call.result.as_ref() == ASSUME_MAGIC_RETURN_CODE { + return Err(TestCaseError::reject(FuzzError::AssumeReject)) + } + + let breakpoints = + call.cheatcodes.clone().map_or(Default::default(), |cheats| cheats.breakpoints); + + let success = + self.executor.is_success(address, call.reverted, state_changeset.clone(), should_fail); + + if success { + Ok(FuzzOutcome::Case(CaseOutcome { + case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend }, + gas_used: call.gas_used, + stipend: call.stipend, + traces: call.traces, + coverage: call.coverage, + debug: call.debug, + breakpoints, + })) + } else { + Ok(FuzzOutcome::CounterExample(CounterExampleOutcome { + debug: call.debug.clone(), + exit_reason: call.exit_reason, + counterexample: (calldata, call), + breakpoints, + })) + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] From f470de992a65956e291907435ee2911eddc5f660 Mon Sep 17 00:00:00 2001 From: franfran Date: Tue, 22 Aug 2023 00:39:52 +0300 Subject: [PATCH 02/17] add struct docs --- crates/evm/src/fuzz/mod.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index 572743f6769fc..5ad0a70f48b19 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -27,6 +27,8 @@ pub mod error; pub mod invariant; pub mod strategies; +/// Returned by a single fuzz in the case of a successful run +#[derive(Debug)] pub struct CaseOutcome { pub case: FuzzCase, pub gas_used: u64, @@ -37,6 +39,8 @@ pub struct CaseOutcome { pub breakpoints: Breakpoints, } +/// Returned by a single fuzz when a counterexample has been discovered +#[derive(Debug)] pub struct CounterExampleOutcome { pub counterexample: (ethers::types::Bytes, RawCallResult), pub exit_reason: InstructionResult, @@ -44,6 +48,8 @@ pub struct CounterExampleOutcome { pub breakpoints: Breakpoints, } +/// Outcome of a single fuzz +#[derive(Debug)] pub enum FuzzOutcome { Case(CaseOutcome), CounterExample(CounterExampleOutcome), From e016eaa0c421f587c0f9a7614ea1c3af2b83fb1a Mon Sep 17 00:00:00 2001 From: Franfran <51274081+iFrostizz@users.noreply.github.com> Date: Wed, 23 Aug 2023 17:40:14 +0300 Subject: [PATCH 03/17] Update crates/evm/src/fuzz/mod.rs Co-authored-by: DaniPopes <57450786+DaniPopes@users.noreply.github.com> --- crates/evm/src/fuzz/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index 5ad0a70f48b19..d969a1ac2eda2 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -250,7 +250,7 @@ impl<'a> FuzzedExecutor<'a> { } let breakpoints = - call.cheatcodes.clone().map_or(Default::default(), |cheats| cheats.breakpoints); + call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| cheats.breakpoints.clone()); let success = self.executor.is_success(address, call.reverted, state_changeset.clone(), should_fail); From 78dca6a6c59f191aa2a0b737bc78fa2dc1f9b2c2 Mon Sep 17 00:00:00 2001 From: franfran Date: Fri, 25 Aug 2023 11:21:51 +0300 Subject: [PATCH 04/17] add docs and move types to types.rs --- crates/evm/src/fuzz/mod.rs | 47 ++++-------------------------------- crates/evm/src/fuzz/types.rs | 44 +++++++++++++++++++++++++++++++++ crates/forge/src/result.rs | 2 +- 3 files changed, 50 insertions(+), 43 deletions(-) create mode 100644 crates/evm/src/fuzz/types.rs diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index d969a1ac2eda2..9bb357ef8e4e4 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -1,6 +1,5 @@ use crate::{ coverage::HitMaps, - debug::DebugArena, decode::{self, decode_console_logs}, executor::{Executor, RawCallResult}, trace::CallTraceArena, @@ -11,49 +10,22 @@ use ethers::{ types::{Address, Bytes, Log}, }; use eyre::Result; -use foundry_common::{calc, contracts::ContractsByAddress, evm::Breakpoints}; +use foundry_common::{calc, contracts::ContractsByAddress}; use foundry_config::FuzzConfig; pub use proptest::test_runner::Reason; use proptest::test_runner::{TestCaseError, TestError, TestRunner}; -use revm::interpreter::InstructionResult; use serde::{Deserialize, Serialize}; use std::{cell::RefCell, collections::BTreeMap, fmt}; use strategies::{ build_initial_state, collect_state_from_call, fuzz_calldata, fuzz_calldata_from_state, EvmFuzzState, }; +use types::{CaseOutcome, CounterExampleOutcome, FuzzCase, FuzzOutcome}; pub mod error; pub mod invariant; pub mod strategies; - -/// Returned by a single fuzz in the case of a successful run -#[derive(Debug)] -pub struct CaseOutcome { - pub case: FuzzCase, - pub gas_used: u64, - pub stipend: u64, - pub traces: Option, - pub coverage: Option, - pub debug: Option, - pub breakpoints: Breakpoints, -} - -/// Returned by a single fuzz when a counterexample has been discovered -#[derive(Debug)] -pub struct CounterExampleOutcome { - pub counterexample: (ethers::types::Bytes, RawCallResult), - pub exit_reason: InstructionResult, - pub debug: Option, - pub breakpoints: Breakpoints, -} - -/// Outcome of a single fuzz -#[derive(Debug)] -pub enum FuzzOutcome { - Case(CaseOutcome), - CounterExample(CounterExampleOutcome), -} +pub mod types; /// Wrapper around an [`Executor`] which provides fuzzing support using [`proptest`](https://docs.rs/proptest/1.0.0/proptest/). /// @@ -220,6 +192,8 @@ impl<'a> FuzzedExecutor<'a> { result } + /// Granular and single-step function that runs only one fuzz and returns either a `CaseOutcome` + /// or a `CounterExampleOutcome` pub fn single_fuzz( &self, state: &EvmFuzzState, @@ -502,14 +476,3 @@ impl FuzzedCases { self.lowest().map(|c| c.gas).unwrap_or_default() } } - -/// Data of a single fuzz test case -#[derive(Clone, Debug, Default, Serialize, Deserialize)] -pub struct FuzzCase { - /// The calldata used for this fuzz test - pub calldata: Bytes, - /// Consumed gas - pub gas: u64, - /// The initial gas stipend for the transaction - pub stipend: u64, -} diff --git a/crates/evm/src/fuzz/types.rs b/crates/evm/src/fuzz/types.rs new file mode 100644 index 0000000000000..266bd671d4e84 --- /dev/null +++ b/crates/evm/src/fuzz/types.rs @@ -0,0 +1,44 @@ +use crate::{coverage::HitMaps, debug::DebugArena, executor::RawCallResult, trace::CallTraceArena}; +use ethers::types::Bytes; +use foundry_common::evm::Breakpoints; +use revm::interpreter::InstructionResult; +use serde::{Deserialize, Serialize}; + +/// Data of a single fuzz test case +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct FuzzCase { + /// The calldata used for this fuzz test + pub calldata: Bytes, + /// Consumed gas + pub gas: u64, + /// The initial gas stipend for the transaction + pub stipend: u64, +} + +/// Returned by a single fuzz in the case of a successful run +#[derive(Debug)] +pub struct CaseOutcome { + pub case: FuzzCase, + pub gas_used: u64, + pub stipend: u64, + pub traces: Option, + pub coverage: Option, + pub debug: Option, + pub breakpoints: Breakpoints, +} + +/// Returned by a single fuzz when a counterexample has been discovered +#[derive(Debug)] +pub struct CounterExampleOutcome { + pub counterexample: (ethers::types::Bytes, RawCallResult), + pub exit_reason: InstructionResult, + pub debug: Option, + pub breakpoints: Breakpoints, +} + +/// Outcome of a single fuzz +#[derive(Debug)] +pub enum FuzzOutcome { + Case(CaseOutcome), + CounterExample(CounterExampleOutcome), +} diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index c6363e0695ce2..ca717e61b6484 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -6,7 +6,7 @@ use foundry_common::evm::Breakpoints; use foundry_evm::{ coverage::HitMaps, executor::EvmError, - fuzz::{CounterExample, FuzzCase}, + fuzz::{types::FuzzCase, CounterExample}, trace::{TraceKind, Traces}, }; use serde::{Deserialize, Serialize}; From 15e4709a78aa935c2607a35aad7955c28ca70fce Mon Sep 17 00:00:00 2001 From: franfran Date: Fri, 25 Aug 2023 11:24:19 +0300 Subject: [PATCH 05/17] fmt --- crates/evm/src/fuzz/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index 9bb357ef8e4e4..4bdd997921d6f 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -223,8 +223,10 @@ impl<'a> FuzzedExecutor<'a> { return Err(TestCaseError::reject(FuzzError::AssumeReject)) } - let breakpoints = - call.cheatcodes.as_ref().map_or_else(Default::default, |cheats| cheats.breakpoints.clone()); + let breakpoints = call + .cheatcodes + .as_ref() + .map_or_else(Default::default, |cheats| cheats.breakpoints.clone()); let success = self.executor.is_success(address, call.reverted, state_changeset.clone(), should_fail); From 4b40bc3d1e3d234649e5eb9600d331d743499649 Mon Sep 17 00:00:00 2001 From: franfran Date: Fri, 25 Aug 2023 11:58:46 +0300 Subject: [PATCH 06/17] add new debugger args type --- Cargo.lock | 1 + crates/ui/Cargo.toml | 1 + crates/ui/src/debugger.rs | 53 +++ crates/ui/src/lib.rs | 781 +++++++++++++++++++------------------- 4 files changed, 448 insertions(+), 388 deletions(-) create mode 100644 crates/ui/src/debugger.rs diff --git a/Cargo.lock b/Cargo.lock index d345e736f91dd..ad685cf382bc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7064,6 +7064,7 @@ dependencies = [ "foundry-common", "foundry-evm", "revm", + "tracing", "tui", ] diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 4a7f767d45e4f..1d07d3ec025f6 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -19,3 +19,4 @@ crossterm = "0.26" eyre = "0.6" revm = { version = "3", features = ["std", "serde"] } tui = { version = "0.19", default-features = false, features = ["crossterm"] } +tracing = "0.1" diff --git a/crates/ui/src/debugger.rs b/crates/ui/src/debugger.rs new file mode 100644 index 0000000000000..b0489b1ba62a4 --- /dev/null +++ b/crates/ui/src/debugger.rs @@ -0,0 +1,53 @@ +use ethers::solc::artifacts::ContractBytecodeSome; +use foundry_common::{evm::Breakpoints, get_contract_name}; +use foundry_evm::{debug::DebugArena, trace::CallTraceDecoder}; +use std::collections::HashMap; +use tracing::trace; + +use crate::{TUIExitReason, Tui}; + +/// Map over debugger contract sources name -> file_id -> (source, contract) +pub type ContractSources = HashMap>; + +/// Standardized way of firing up the debugger +pub struct DebuggerArgs<'a> { + /// debug traces returned from the execution + pub debug: Vec, + /// trace decoder + pub decoder: &'a CallTraceDecoder, + /// map of source files + pub sources: ContractSources, + /// map of the debugger breakpoints + pub breakpoints: Breakpoints, +} + +impl DebuggerArgs<'_> { + pub fn run(&self) -> eyre::Result { + trace!(target: "debugger", "running debugger"); + + let flattened = self + .debug + .last() + .map(|arena| arena.flatten(0)) + .expect("We should have collected debug information"); + + let identified_contracts = self + .decoder + .contracts + .iter() + .map(|(addr, identifier)| (*addr, get_contract_name(identifier).to_string())) + .collect(); + + let contract_sources = self.sources.clone(); + + let mut tui = Tui::new( + flattened, + 0, + identified_contracts, + contract_sources, + self.breakpoints.clone(), + )?; + + tui.launch() + } +} diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 0588fc2bd4086..a63870ed9f332 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -38,7 +38,7 @@ use tui::{ /// Trait for starting the UI pub trait Ui { /// Start the agent that will now take over - fn start(self) -> Result; + fn launch(&mut self) -> Result; } /// Used to indicate why the UI stopped @@ -50,6 +50,9 @@ pub enum TUIExitReason { mod op_effects; use op_effects::stack_indices_affected; +use self::debugger::ContractSources; +mod debugger; + pub struct Tui { debug_arena: Vec<(Address, Vec, CallKind)>, terminal: Terminal>, @@ -58,11 +61,17 @@ pub struct Tui { /// Current step in the debug steps current_step: usize, identified_contracts: HashMap, - known_contracts: HashMap, - known_contracts_sources: HashMap>, + /// Source map of contract sources + contracts_sources: ContractSources, /// A mapping of source -> (PC -> IC map for deploy code, PC -> IC map for runtime code) pc_ic_maps: BTreeMap, breakpoints: Breakpoints, + draw_memory: DrawMemory, + opcode_list: Vec, + last_index: usize, + stack_labels: bool, + mem_utf: bool, + show_shortcuts: bool, } impl Tui { @@ -72,8 +81,7 @@ impl Tui { debug_arena: Vec<(Address, Vec, CallKind)>, current_step: usize, identified_contracts: HashMap, - known_contracts: HashMap, - known_contracts_sources: HashMap>, + contracts_sources: ContractSources, breakpoints: Breakpoints, ) -> Result { enable_raw_mode()?; @@ -82,40 +90,47 @@ impl Tui { let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; terminal.hide_cursor(); - let pc_ic_maps = known_contracts + let pc_ic_maps = contracts_sources .iter() - .filter_map(|(contract_name, bytecode)| { - Some(( - contract_name.clone(), - ( - build_pc_ic_map( - SpecId::LATEST, - bytecode.bytecode.object.as_bytes()?.as_ref(), - ), - build_pc_ic_map( - SpecId::LATEST, - bytecode - .deployed_bytecode - .bytecode - .as_ref()? - .object - .as_bytes()? - .as_ref(), + .flat_map(|(contract_name, files_sources)| { + files_sources.iter().filter_map(|(_, (_, contract))| { + Some(( + contract_name.clone(), + ( + build_pc_ic_map( + SpecId::LATEST, + contract.bytecode.object.as_bytes()?.as_ref(), + ), + build_pc_ic_map( + SpecId::LATEST, + contract + .deployed_bytecode + .bytecode + .as_ref()? + .object + .as_bytes()? + .as_ref(), + ), ), - ), - )) + )) + }) }) .collect(); Ok(Tui { - debug_arena, + debug_arena: debug_arena.clone(), terminal, key_buffer: String::new(), current_step, identified_contracts, - known_contracts, - known_contracts_sources, + contracts_sources, pc_ic_maps, breakpoints, + draw_memory: DrawMemory::default(), + opcode_list: debug_arena[0].1.iter().map(|step| step.pretty_opcode()).collect(), + last_index: 0, + mem_utf: false, + stack_labels: false, + show_shortcuts: false, }) } @@ -138,9 +153,8 @@ impl Tui { f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -156,9 +170,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps, opcode_list, current_step, @@ -173,9 +186,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps, opcode_list, current_step, @@ -193,9 +205,8 @@ impl Tui { f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -235,9 +246,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps[current_step].pc, call_kind, src_pane, @@ -273,9 +283,8 @@ impl Tui { f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, debug_steps: &[DebugStep], opcode_list: &[String], current_step: usize, @@ -320,9 +329,8 @@ impl Tui { f, address, identified_contracts, - known_contracts, pc_ic_maps, - known_contracts_sources, + contracts_sources, debug_steps[current_step].pc, call_kind, src_pane, @@ -384,9 +392,8 @@ Spans::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/ f: &mut Frame, address: Address, identified_contracts: &HashMap, - known_contracts: &HashMap, pc_ic_maps: &BTreeMap, - known_contracts_sources: &HashMap>, + contracts_sources: &ContractSources, pc: usize, call_kind: CallKind, area: Rect, @@ -404,286 +411,286 @@ Spans::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/ let mut text_output: Text = Text::from(""); if let Some(contract_name) = identified_contracts.get(&address) { - if let (Some(known), Some(source_code)) = - (known_contracts.get(contract_name), known_contracts_sources.get(contract_name)) - { + if let Some(files_source_code) = contracts_sources.get(contract_name) { let pc_ic_map = pc_ic_maps.get(contract_name); - // grab either the creation source map or runtime sourcemap - if let Some((sourcemap, ic)) = - if matches!(call_kind, CallKind::Create | CallKind::Create2) { - known.bytecode.source_map().zip(pc_ic_map.and_then(|(c, _)| c.get(&pc))) + // find the contract source with the correct source_element's file_id + if let Some((source_element, source_code)) = files_source_code.iter().find_map( + |(file_id, (source_code, contract_source))| { + // grab either the creation source map or runtime sourcemap + if let Some((Ok(source_map), ic)) = + if matches!(call_kind, CallKind::Create | CallKind::Create2) { + contract_source + .bytecode + .source_map() + .zip(pc_ic_map.and_then(|(c, _)| c.get(&pc))) + } else { + contract_source + .deployed_bytecode + .bytecode + .as_ref() + .expect("no bytecode") + .source_map() + .zip(pc_ic_map.and_then(|(_, r)| r.get(&pc))) + } + { + let source_element = source_map[*ic].clone(); + if let Some(index) = source_element.index { + if *file_id == index { + Some((source_element, source_code)) + } else { + None + } + } else { + None + } + } else { + None + } + }, + ) { + // we are handed a vector of SourceElements that give + // us a span of sourcecode that is currently being executed + // This includes an offset and length. This vector is in + // instruction pointer order, meaning the location of + // the instruction - sum(push_bytes[..pc]) + let offset = source_element.offset; + let len = source_element.length; + let max = source_code.len(); + + // split source into before, relevant, and after chunks + // split by line as well to do some formatting stuff + let mut before = source_code[..std::cmp::min(offset, max)] + .split_inclusive('\n') + .collect::>(); + let actual = source_code + [std::cmp::min(offset, max)..std::cmp::min(offset + len, max)] + .split_inclusive('\n') + .map(|s| s.to_string()) + .collect::>(); + let mut after = source_code[std::cmp::min(offset + len, max)..] + .split_inclusive('\n') + .collect::>(); + + let mut line_number = 0; + + let num_lines = before.len() + actual.len() + after.len(); + let height = area.height as usize; + let needed_highlight = actual.len(); + let mid_len = before.len() + actual.len(); + + // adjust what text we show of the source code + let (start_line, end_line) = if needed_highlight > height { + // highlighted section is more lines than we have avail + (before.len(), before.len() + needed_highlight) + } else if height > num_lines { + // we can fit entire source + (0, num_lines) } else { - known - .deployed_bytecode - .bytecode - .as_ref() - .expect("no bytecode") - .source_map() - .zip(pc_ic_map.and_then(|(_, r)| r.get(&pc))) - } - { - match sourcemap { - Ok(sourcemap) => { - // we are handed a vector of SourceElements that give - // us a span of sourcecode that is currently being executed - // This includes an offset and length. This vector is in - // instruction pointer order, meaning the location of - // the instruction - sum(push_bytes[..pc]) - if let Some(source_idx) = sourcemap[*ic].index { - if let Some(source) = source_code.get(&source_idx) { - let offset = sourcemap[*ic].offset; - let len = sourcemap[*ic].length; - let max = source.len(); - - // split source into before, relevant, and after chunks - // split by line as well to do some formatting stuff - let mut before = source[..std::cmp::min(offset, max)] - .split_inclusive('\n') - .collect::>(); - let actual = source[std::cmp::min(offset, max).. - std::cmp::min(offset + len, max)] - .split_inclusive('\n') - .map(|s| s.to_string()) - .collect::>(); - let mut after = source[std::cmp::min(offset + len, max)..] - .split_inclusive('\n') - .collect::>(); - - let mut line_number = 0; - - let num_lines = before.len() + actual.len() + after.len(); - let height = area.height as usize; - let needed_highlight = actual.len(); - let mid_len = before.len() + actual.len(); - - // adjust what text we show of the source code - let (start_line, end_line) = if needed_highlight > height { - // highlighted section is more lines than we have avail - (before.len(), before.len() + needed_highlight) - } else if height > num_lines { - // we can fit entire source - (0, num_lines) - } else { - let remaining = height - needed_highlight; - let mut above = remaining / 2; - let mut below = remaining / 2; - if below > after.len() { - // unused space below the highlight - above += below - after.len(); - } else if above > before.len() { - // we have unused space above the highlight - below += above - before.len(); - } else { - // no unused space - } + let remaining = height - needed_highlight; + let mut above = remaining / 2; + let mut below = remaining / 2; + if below > after.len() { + // unused space below the highlight + above += below - after.len(); + } else if above > before.len() { + // we have unused space above the highlight + below += above - before.len(); + } else { + // no unused space + } - (before.len().saturating_sub(above), mid_len + below) - }; - - let max_line_num = num_lines.to_string().len(); - // 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') { - before.iter().skip(start_line).for_each(|line| { - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Gray) - .bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default() - .add_modifier(Modifier::DIM), - ), - ])); - line_number += 1; - }); - - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::raw(last), - Span::styled( - actual[0].to_string(), - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - - actual.iter().skip(1).for_each(|s| { - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - // this is a hack to add coloring - // because tui does weird trimming - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); + (before.len().saturating_sub(above), mid_len + below) + }; + + let max_line_num = num_lines.to_string().len(); + // 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') { + before.iter().skip(start_line).for_each(|line| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default().fg(Color::Gray).bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + line_number += 1; + }); + + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::raw(last), + Span::styled( + actual[0].to_string(), + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + + actual.iter().skip(1).for_each(|s| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + // this is a hack to add coloring + // because tui does weird trimming + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() } else { - before.push(last); - before.iter().skip(start_line).for_each(|line| { - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Gray) - .bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default() - .add_modifier(Modifier::DIM), - ), - ])); - - line_number += 1; - }); - actual.iter().for_each(|s| { - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } else { + before.push(last); + before.iter().skip(start_line).for_each(|line| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default().fg(Color::Gray).bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + + line_number += 1; + }); + actual.iter().for_each(|s| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() + } else { + s.to_string() + }, + Style::default() + .fg(Color::Cyan) + .add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } + } else { + actual.iter().for_each(|s| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default() + .fg(Color::Cyan) + .bg(Color::DarkGray) + .add_modifier(Modifier::BOLD), + ), + Span::raw("\u{2800} "), + Span::styled( + if s.is_empty() || s == "\n" { + "\u{2800} \n".to_string() } else { - actual.iter().for_each(|s| { - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Cyan) - .bg(Color::DarkGray) - .add_modifier(Modifier::BOLD), - ), - Span::raw("\u{2800} "), - Span::styled( - if s.is_empty() || s == "\n" { - "\u{2800} \n".to_string() - } else { - s.to_string() - }, - Style::default() - .fg(Color::Cyan) - .add_modifier(Modifier::BOLD), - ), - ])); - line_number += 1; - }); - } - - // fill in the rest of the line as unhighlighted - if let Some(last) = actual.last() { - if !last.ends_with('\n') { - if let Some(post) = after.pop_front() { - if let Some(last) = text_output.lines.last_mut() { - last.0.push(Span::raw(post)); - } - } - } - } + s.to_string() + }, + Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), + ), + ])); + line_number += 1; + }); + } - // add after highlighted text - while mid_len + after.len() > end_line { - after.pop_back(); - } - after.iter().for_each(|line| { - text_output.lines.push(Spans::from(vec![ - Span::styled( - format!( - "{: >max_line_num$}", - line_number.to_string(), - max_line_num = max_line_num - ), - Style::default() - .fg(Color::Gray) - .bg(Color::DarkGray), - ), - Span::styled( - "\u{2800} ".to_string() + line, - Style::default().add_modifier(Modifier::DIM), - ), - ])); - line_number += 1; - }); - } else { - text_output.extend(Text::from("No source for srcmap index")); + // fill in the rest of the line as unhighlighted + if let Some(last) = actual.last() { + if !last.ends_with('\n') { + if let Some(post) = after.pop_front() { + if let Some(last) = text_output.lines.last_mut() { + last.0.push(Span::raw(post)); } - } else { - text_output.extend(Text::from("No srcmap index")); } } - Err(e) => text_output.extend(Text::from(format!( - "Error in source map parsing: '{e}', please open an issue" - ))), } + + // add after highlighted text + while mid_len + after.len() > end_line { + after.pop_back(); + } + after.iter().for_each(|line| { + text_output.lines.push(Spans::from(vec![ + Span::styled( + format!( + "{: >max_line_num$}", + line_number.to_string(), + max_line_num = max_line_num + ), + Style::default().fg(Color::Gray).bg(Color::DarkGray), + ), + Span::styled( + "\u{2800} ".to_string() + line, + Style::default().add_modifier(Modifier::DIM), + ), + ])); + line_number += 1; + }); + // TODO: if file_id not here + // } else { + // text_output.extend(Text::from("No srcmap index")); + // } + // TODO: if source_element don't match + // Err(e) => text_output.extend(Text::from(format!( + // "Error in source map parsing: '{e}', please open an issue" + // ))), } else { text_output.extend(Text::from("No sourcemap for contract")); } @@ -984,7 +991,7 @@ Spans::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/ } impl Ui for Tui { - fn start(mut self) -> Result { + fn launch(&mut self) -> eyre::Result { // If something panics inside here, we should do everything we can to // not corrupt the user's terminal. std::panic::set_hook(Box::new(|e| { @@ -1001,8 +1008,8 @@ impl Ui for Tui { thread::spawn(move || { let mut last_tick = Instant::now(); loop { - // Poll events since last tick - if last tick is greater than tick_rate, we demand - // immediate availability of the event. This may affect + // Poll events since last tick - if last tick is greater than tick_rate, we + // demand immediate availability of the event. This may affect // interactivity, but I'm not sure as it is hard to test. if event::poll(tick_rate.saturating_sub(last_tick.elapsed())).unwrap() { let event = event::read().unwrap(); @@ -1027,25 +1034,16 @@ impl Ui for Tui { }); self.terminal.clear()?; - let mut draw_memory: DrawMemory = DrawMemory::default(); - - let debug_call: Vec<(Address, Vec, CallKind)> = self.debug_arena.clone(); - let mut opcode_list: Vec = - debug_call[0].1.iter().map(|step| step.pretty_opcode()).collect(); - let mut last_index = 0; - let mut stack_labels = false; - let mut mem_utf = false; - let mut show_shortcuts = true; // UI thread that manages drawing loop { - if last_index != draw_memory.inner_call_index { - opcode_list = debug_call[draw_memory.inner_call_index] + if self.last_index != self.draw_memory.inner_call_index { + self.opcode_list = self.debug_arena[self.draw_memory.inner_call_index] .1 .iter() .map(|step| step.pretty_opcode()) .collect(); - last_index = draw_memory.inner_call_index; + self.last_index = self.draw_memory.inner_call_index; } // Grab interrupt @@ -1053,15 +1051,15 @@ impl Ui for Tui { if let Some(c) = receiver.char_press() { if self.key_buffer.ends_with('\'') { - // Find the location of the called breakpoint in the whole debug arena (at this - // address with this pc) + // Find the location of the called breakpoint in the whole debug arena (at + // this address with this pc) if let Some((caller, pc)) = self.breakpoints.get(&c) { - for (i, (_caller, debug_steps, _)) in debug_call.iter().enumerate() { + for (i, (_caller, debug_steps, _)) in self.debug_arena.iter().enumerate() { if _caller == caller { if let Some(step) = debug_steps.iter().position(|step| step.pc == *pc) { - draw_memory.inner_call_index = i; + self.draw_memory.inner_call_index = i; self.current_step = step; break } @@ -1086,19 +1084,22 @@ impl Ui for Tui { // Grab number of times to do it for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { if event.modifiers.contains(KeyModifiers::CONTROL) { - let max_mem = (debug_call[draw_memory.inner_call_index].1 - [self.current_step] + let max_mem = (self.debug_arena + [self.draw_memory.inner_call_index] + .1[self.current_step] .memory .len() / 32) .saturating_sub(1); - if draw_memory.current_mem_startline < max_mem { - draw_memory.current_mem_startline += 1; + if self.draw_memory.current_mem_startline < max_mem { + self.draw_memory.current_mem_startline += 1; } - } else if self.current_step < opcode_list.len() - 1 { + } else if self.current_step < self.opcode_list.len() - 1 { self.current_step += 1; - } else if draw_memory.inner_call_index < debug_call.len() - 1 { - draw_memory.inner_call_index += 1; + } else if self.draw_memory.inner_call_index < + self.debug_arena.len() - 1 + { + self.draw_memory.inner_call_index += 1; self.current_step = 0; } } @@ -1106,13 +1107,13 @@ impl Ui for Tui { } KeyCode::Char('J') => { for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { - let max_stack = debug_call[draw_memory.inner_call_index].1 - [self.current_step] + let max_stack = self.debug_arena[self.draw_memory.inner_call_index] + .1[self.current_step] .stack .len() .saturating_sub(1); - if draw_memory.current_stack_startline < max_stack { - draw_memory.current_stack_startline += 1; + if self.draw_memory.current_stack_startline < max_stack { + self.draw_memory.current_stack_startline += 1; } } self.key_buffer.clear(); @@ -1121,50 +1122,53 @@ impl Ui for Tui { KeyCode::Char('k') | KeyCode::Up => { for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { if event.modifiers.contains(KeyModifiers::CONTROL) { - draw_memory.current_mem_startline = - draw_memory.current_mem_startline.saturating_sub(1); + self.draw_memory.current_mem_startline = + self.draw_memory.current_mem_startline.saturating_sub(1); } else if self.current_step > 0 { self.current_step -= 1; - } else if draw_memory.inner_call_index > 0 { - draw_memory.inner_call_index -= 1; - self.current_step = - debug_call[draw_memory.inner_call_index].1.len() - 1; + } else if self.draw_memory.inner_call_index > 0 { + self.draw_memory.inner_call_index -= 1; + self.current_step = self.debug_arena + [self.draw_memory.inner_call_index] + .1 + .len() - + 1; } } self.key_buffer.clear(); } KeyCode::Char('K') => { for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { - draw_memory.current_stack_startline = - draw_memory.current_stack_startline.saturating_sub(1); + self.draw_memory.current_stack_startline = + self.draw_memory.current_stack_startline.saturating_sub(1); } self.key_buffer.clear(); } // Go to top of file KeyCode::Char('g') => { - draw_memory.inner_call_index = 0; + self.draw_memory.inner_call_index = 0; self.current_step = 0; self.key_buffer.clear(); } // Go to bottom of file KeyCode::Char('G') => { - draw_memory.inner_call_index = debug_call.len() - 1; + self.draw_memory.inner_call_index = self.debug_arena.len() - 1; self.current_step = - debug_call[draw_memory.inner_call_index].1.len() - 1; + self.debug_arena[self.draw_memory.inner_call_index].1.len() - 1; self.key_buffer.clear(); } // Go to previous call KeyCode::Char('c') => { - draw_memory.inner_call_index = - draw_memory.inner_call_index.saturating_sub(1); + self.draw_memory.inner_call_index = + self.draw_memory.inner_call_index.saturating_sub(1); self.current_step = - debug_call[draw_memory.inner_call_index].1.len() - 1; + self.debug_arena[self.draw_memory.inner_call_index].1.len() - 1; self.key_buffer.clear(); } // Go to next call KeyCode::Char('C') => { - if debug_call.len() > draw_memory.inner_call_index + 1 { - draw_memory.inner_call_index += 1; + if self.debug_arena.len() > self.draw_memory.inner_call_index + 1 { + self.draw_memory.inner_call_index += 1; self.current_step = 0; } self.key_buffer.clear(); @@ -1173,7 +1177,7 @@ impl Ui for Tui { KeyCode::Char('s') => { for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { let remaining_ops = - opcode_list[self.current_step..].to_vec().clone(); + self.opcode_list[self.current_step..].to_vec().clone(); self.current_step += remaining_ops .iter() .enumerate() @@ -1190,9 +1194,9 @@ impl Ui for Tui { None } }) - .unwrap_or(opcode_list.len() - 1); - if self.current_step > opcode_list.len() { - self.current_step = opcode_list.len() - 1 + .unwrap_or(self.opcode_list.len() - 1); + if self.current_step > self.opcode_list.len() { + self.current_step = self.opcode_list.len() - 1 }; } self.key_buffer.clear(); @@ -1200,7 +1204,8 @@ impl Ui for Tui { // Step backwards KeyCode::Char('a') => { for _ in 0..Tui::buffer_as_number(&self.key_buffer, 1) { - let prev_ops = opcode_list[..self.current_step].to_vec().clone(); + let prev_ops = + self.opcode_list[..self.current_step].to_vec().clone(); self.current_step = prev_ops .iter() .enumerate() @@ -1225,15 +1230,15 @@ impl Ui for Tui { } // toggle stack labels KeyCode::Char('t') => { - stack_labels = !stack_labels; + self.stack_labels = !self.stack_labels; } // toggle memory utf8 decoding KeyCode::Char('m') => { - mem_utf = !mem_utf; + self.mem_utf = !self.mem_utf; } // toggle help notice KeyCode::Char('h') => { - show_shortcuts = !show_shortcuts; + self.show_shortcuts = !self.show_shortcuts; } KeyCode::Char(other) => match other { '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '\'' => { @@ -1255,21 +1260,22 @@ impl Ui for Tui { MouseEventKind::ScrollUp => { if self.current_step > 0 { self.current_step -= 1; - } else if draw_memory.inner_call_index > 0 { - draw_memory.inner_call_index -= 1; - draw_memory.current_mem_startline = 0; - draw_memory.current_stack_startline = 0; + } else if self.draw_memory.inner_call_index > 0 { + self.draw_memory.inner_call_index -= 1; + self.draw_memory.current_mem_startline = 0; + self.draw_memory.current_stack_startline = 0; self.current_step = - debug_call[draw_memory.inner_call_index].1.len() - 1; + self.debug_arena[self.draw_memory.inner_call_index].1.len() - 1; } } MouseEventKind::ScrollDown => { - if self.current_step < opcode_list.len() - 1 { + if self.current_step < self.opcode_list.len() - 1 { self.current_step += 1; - } else if draw_memory.inner_call_index < debug_call.len() - 1 { - draw_memory.inner_call_index += 1; - draw_memory.current_mem_startline = 0; - draw_memory.current_stack_startline = 0; + } else if self.draw_memory.inner_call_index < self.debug_arena.len() - 1 + { + self.draw_memory.inner_call_index += 1; + self.draw_memory.current_mem_startline = 0; + self.draw_memory.current_stack_startline = 0; self.current_step = 0; } } @@ -1285,19 +1291,18 @@ impl Ui for Tui { self.terminal.draw(|f| { Tui::draw_layout( f, - debug_call[draw_memory.inner_call_index].0, + self.debug_arena[self.draw_memory.inner_call_index].0, &self.identified_contracts, - &self.known_contracts, &self.pc_ic_maps, - &self.known_contracts_sources, - &debug_call[draw_memory.inner_call_index].1[..], - &opcode_list, + &self.contracts_sources, + &self.debug_arena[self.draw_memory.inner_call_index].1[..], + &self.opcode_list, current_step, - debug_call[draw_memory.inner_call_index].2, - &mut draw_memory, - stack_labels, - mem_utf, - show_shortcuts, + self.debug_arena[self.draw_memory.inner_call_index].2, + &mut self.draw_memory, + self.stack_labels, + self.mem_utf, + self.show_shortcuts, ) })?; } From e9f89420f7191a2a5403695ad59b8188eb1d0abb Mon Sep 17 00:00:00 2001 From: franfran Date: Fri, 25 Aug 2023 12:38:41 +0300 Subject: [PATCH 07/17] add minimal debugger-refactor changes --- crates/evm/src/trace/identifier/etherscan.rs | 19 +- crates/forge/bin/cmd/debug.rs | 7 +- crates/forge/bin/cmd/script/build.rs | 93 +--- crates/forge/bin/cmd/script/cmd.rs | 17 +- crates/forge/bin/cmd/script/executor.rs | 1 + crates/forge/bin/cmd/script/mod.rs | 46 +- crates/forge/bin/cmd/script/runner.rs | 4 + crates/forge/bin/cmd/test/mod.rs | 449 ++++++++++++------- crates/forge/bin/main.rs | 4 +- crates/forge/src/multi_runner.rs | 40 +- crates/forge/src/result.rs | 3 + crates/forge/src/runner.rs | 199 +++++--- crates/ui/src/lib.rs | 4 +- 13 files changed, 488 insertions(+), 398 deletions(-) diff --git a/crates/evm/src/trace/identifier/etherscan.rs b/crates/evm/src/trace/identifier/etherscan.rs index b9bdf43feb100..b56ba70ef94a6 100644 --- a/crates/evm/src/trace/identifier/etherscan.rs +++ b/crates/evm/src/trace/identifier/etherscan.rs @@ -17,7 +17,7 @@ use futures::{ }; use std::{ borrow::Cow, - collections::BTreeMap, + collections::{BTreeMap, HashMap}, pin::Pin, sync::{ atomic::{AtomicBool, Ordering}, @@ -60,10 +60,14 @@ impl EtherscanIdentifier { /// Etherscan and compiles them locally, for usage in the debugger. pub async fn get_compiled_contracts( &self, - ) -> eyre::Result<(BTreeMap, BTreeMap)> - { + ) -> eyre::Result<( + // TODO should use `ContractSources` but has circular import. + // Maybe move it lower + HashMap>, + BTreeMap, + )> { let mut compiled_contracts = BTreeMap::new(); - let mut sources = BTreeMap::new(); + let mut sources = HashMap::new(); // TODO: Add caching so we dont double-fetch contracts. let contracts_iter = self @@ -90,9 +94,10 @@ impl EtherscanIdentifier { // construct the map for (results, (_, metadata)) in artifacts.into_iter().zip(contracts_iter) { // get the inner type - let (artifact_id, bytecode) = results?; - compiled_contracts.insert(artifact_id.clone(), bytecode); - sources.insert(artifact_id, metadata.source_code()); + let (artifact_id, file_id, bytecode) = results?; + compiled_contracts.insert(artifact_id.clone(), bytecode.clone()); + let inner_map = sources.entry(artifact_id.clone().name).or_insert_with(HashMap::new); + inner_map.insert(file_id, (metadata.source_code(), bytecode)); } Ok((sources, compiled_contracts)) diff --git a/crates/forge/bin/cmd/debug.rs b/crates/forge/bin/cmd/debug.rs index abd7362720870..bdcea1c0db60c 100644 --- a/crates/forge/bin/cmd/debug.rs +++ b/crates/forge/bin/cmd/debug.rs @@ -1,8 +1,7 @@ use super::{build::BuildArgs, retry::RETRY_VERIFY_ON_CREATE, script::ScriptArgs}; use clap::{Parser, ValueHint}; -use eyre::Result; use foundry_cli::opts::CoreBuildArgs; -use foundry_common::evm::{Breakpoints, EvmArgs}; +use foundry_common::evm::EvmArgs; use std::path::PathBuf; // Loads project's figment and merges the build cli arguments into it @@ -41,7 +40,7 @@ pub struct DebugArgs { } impl DebugArgs { - pub async fn debug(self, breakpoints: Breakpoints) -> Result<()> { + pub async fn run(self) -> eyre::Result<()> { let script = ScriptArgs { path: self.path.to_str().expect("Invalid path string.").to_string(), args: self.args, @@ -54,6 +53,6 @@ impl DebugArgs { retry: RETRY_VERIFY_ON_CREATE, ..Default::default() }; - script.run_script(breakpoints).await + script.run_script().await } } diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index c905662af0b1a..ac71d6b6507c5 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -13,7 +13,7 @@ use ethers::{ }; use eyre::{Context, ContextCompat, Result}; use foundry_cli::utils::get_cached_entry_by_name; -use foundry_common::compile; +use foundry_common::{compact_to_contract, compile}; use foundry_utils::{PostLinkInput, ResolvedDependency}; use std::{collections::BTreeMap, fs, str::FromStr}; use tracing::{trace, warn}; @@ -31,7 +31,8 @@ impl ScriptArgs { let (project, output) = self.get_project_and_output(script_config)?; let output = output.with_stripped_file_prefixes(project.root()); - let mut sources: BTreeMap = BTreeMap::new(); + let mut sources: HashMap> = + HashMap::new(); let contracts = output .into_artifacts() @@ -39,13 +40,15 @@ impl ScriptArgs { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { - sources.insert( - source.id, - source - .ast - .ok_or(eyre::eyre!("Source from artifact has no AST."))? - .absolute_path, - ); + let inner_map = sources.entry(id.clone().name).or_default(); + let abs_path = source + .ast + .ok_or(eyre::eyre!("Source from artifact has no AST."))? + .absolute_path; + let source_code = fs::read_to_string(abs_path).unwrap(); + let contract = artifact.clone().into_contract_bytecode(); + let source_contract = compact_to_contract(contract); + inner_map.insert(source.id, (source_code, source_contract)); } else { warn!("source not found for artifact={:?}", id); } @@ -193,7 +196,7 @@ impl ScriptArgs { known_contracts: contracts, highlevel_known_contracts: ArtifactContracts(highlevel_known_contracts), predeploy_libraries, - sources: BTreeMap::new(), + sources: HashMap::new(), project, libraries: new_libraries, }) @@ -259,74 +262,6 @@ impl ScriptArgs { } } -/// Resolve the import tree of our target path, and get only the artifacts and -/// sources we need. If it's a standalone script, don't filter anything out. -pub fn filter_sources_and_artifacts( - target: &str, - sources: BTreeMap, - highlevel_known_contracts: ArtifactContracts, - project: Project, -) -> Result<(BTreeMap, HashMap)> { - // Find all imports - let graph = Graph::resolve(&project.paths)?; - let target_path = project.root().join(target); - let mut target_tree = BTreeMap::new(); - let mut is_standalone = false; - - if let Some(target_index) = graph.files().get(&target_path) { - target_tree.extend( - graph - .all_imported_nodes(*target_index) - .map(|index| graph.node(index).unpack()) - .collect::>(), - ); - - // Add our target into the tree as well. - let (target_path, target_source) = graph.node(*target_index).unpack(); - target_tree.insert(target_path, target_source); - } else { - is_standalone = true; - } - - let sources = sources - .into_iter() - .filter_map(|(id, path)| { - let mut resolved = project - .paths - .resolve_library_import(project.root(), &PathBuf::from(&path)) - .unwrap_or_else(|| PathBuf::from(&path)); - - if !resolved.is_absolute() { - resolved = project.root().join(&resolved); - } - - if !is_standalone { - target_tree.get(&resolved).map(|source| (id, source.content.as_str().to_string())) - } else { - Some(( - id, - fs::read_to_string(&resolved).unwrap_or_else(|_| { - panic!("Something went wrong reading the source file: {path:?}") - }), - )) - } - }) - .collect(); - - let artifacts = highlevel_known_contracts - .into_iter() - .filter_map(|(id, artifact)| { - if !is_standalone { - target_tree.get(&id.source).map(|_| (id.name, artifact)) - } else { - Some((id.name, artifact)) - } - }) - .collect(); - - Ok((sources, artifacts)) -} - struct ExtraLinkingInfo<'a> { no_target_name: bool, target_fname: String, @@ -344,5 +279,5 @@ pub struct BuildOutput { pub highlevel_known_contracts: ArtifactContracts, pub libraries: Libraries, pub predeploy_libraries: Vec, - pub sources: BTreeMap, + pub sources: HashMap>, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 70c836877cdcb..144addf4630b1 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -8,13 +8,14 @@ use foundry_cli::utils::LoadConfig; use foundry_common::{contracts::flatten_contracts, try_get_http_provider}; use std::sync::Arc; use tracing::trace; +use ui::DebuggerArgs; /// Helper alias type for the collection of data changed due to the new sender. type NewSenderChanges = (CallTraceDecoder, Libraries, ArtifactContracts); impl ScriptArgs { /// Executes the script - pub async fn run_script(mut self, breakpoints: Breakpoints) -> Result<()> { + pub async fn run_script(mut self) -> eyre::Result<()> { trace!(target: "script", "executing script command"); let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; @@ -23,6 +24,7 @@ impl ScriptArgs { sender_nonce: U256::one(), config, evm_opts, + debug: self.debug, ..Default::default() }; @@ -83,14 +85,13 @@ impl ScriptArgs { let mut decoder = self.decode_traces(&script_config, &mut result, &known_contracts)?; if self.debug { - return self.run_debugger( - &decoder, + let debugger = DebuggerArgs { + debug: result.debug.clone().unwrap_or(vec![]), + decoder: &decoder, sources, - result, - project, - highlevel_known_contracts, - breakpoints, - ) + breakpoints: result.breakpoints.clone(), + }; + debugger.run()?; } if let Some((new_traces, updated_libraries, updated_contracts)) = self diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index ac42e3c2a9ee1..cbfb08e51e2e7 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -73,6 +73,7 @@ impl ScriptArgs { result.labeled_addresses.extend(script_result.labeled_addresses); result.returned = script_result.returned; result.script_wallets.extend(script_result.script_wallets); + result.breakpoints = script_result.breakpoints; match (&mut result.transactions, script_result.transactions) { (Some(txs), Some(new_txs)) => { diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 4e10066332f3c..4512f6333d280 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -449,49 +449,6 @@ impl ScriptArgs { .collect() } - fn run_debugger( - &self, - decoder: &CallTraceDecoder, - sources: BTreeMap, - result: ScriptResult, - project: Project, - highlevel_known_contracts: ArtifactContracts, - breakpoints: Breakpoints, - ) -> Result<()> { - trace!(target: "script", "debugging script"); - - let (sources, artifacts) = filter_sources_and_artifacts( - &self.path, - sources, - highlevel_known_contracts.clone(), - project, - )?; - let flattened = result - .debug - .and_then(|arena| arena.last().map(|arena| arena.flatten(0))) - .expect("We should have collected debug information"); - let identified_contracts = decoder - .contracts - .iter() - .map(|(addr, identifier)| (*addr, get_contract_name(identifier).to_string())) - .collect(); - - let tui = Tui::new( - flattened, - 0, - identified_contracts, - artifacts, - highlevel_known_contracts - .into_iter() - .map(|(id, _)| (id.name, sources.clone())) - .collect(), - breakpoints, - )?; - match tui.start().expect("Failed to start tui") { - TUIExitReason::CharExit => Ok(()), - } - } - /// Returns the Function and calldata based on the signature /// /// If the `sig` is a valid human-readable function we find the corresponding function in the @@ -667,6 +624,7 @@ pub struct ScriptResult { pub returned: bytes::Bytes, pub address: Option
, pub script_wallets: Vec, + pub breakpoints: Breakpoints, } #[derive(Serialize, Deserialize)] @@ -697,6 +655,8 @@ pub struct ScriptConfig { pub total_rpcs: HashSet, /// If true, one of the transactions did not have a rpc pub missing_rpc: bool, + /// Should return some debug information + pub debug: bool, } impl ScriptConfig { diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/forge/bin/cmd/script/runner.rs index 8a50bb21eaf32..aa9dd2c04b0f0 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/forge/bin/cmd/script/runner.rs @@ -167,6 +167,7 @@ impl ScriptRunner { debug, address: None, script_wallets, + breakpoints: Default::default(), }, )) } @@ -240,6 +241,7 @@ impl ScriptRunner { transactions: Default::default(), address: Some(address), script_wallets: vec![], + breakpoints: Default::default(), }) } else { eyre::bail!("ENS not supported."); @@ -284,6 +286,7 @@ impl ScriptRunner { script_wallets, .. } = res; + let breakpoints = res.cheatcodes.map(|cheats| cheats.breakpoints).unwrap_or_default(); Ok(ScriptResult { returned: result, @@ -302,6 +305,7 @@ impl ScriptRunner { transactions, address: None, script_wallets, + breakpoints, }) } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index d1686f166a3a0..2f821165cd3bc 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -1,4 +1,4 @@ -use super::{debug::DebugArgs, install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs}; +use super::{install, test::filter::ProjectPathsAwareFilter, watch::WatchArgs}; use clap::Parser; use ethers::types::U256; use eyre::Result; @@ -6,7 +6,7 @@ use forge::{ decode::decode_console_logs, executor::inspector::CheatsConfig, gas_report::GasReport, - result::{SuiteResult, TestKind, TestResult, TestStatus}, + result::{SuiteResult, TestResult, TestStatus}, trace::{ identifier::{EtherscanIdentifier, LocalTraceIdentifier, SignaturesIdentifier}, CallTraceDecoderBuilder, TraceKind, @@ -18,6 +18,7 @@ use foundry_cli::{ utils::{self, LoadConfig}, }; use foundry_common::{ + compact_to_contract, compile::{self, ProjectCompiler}, evm::EvmArgs, get_contract_name, get_file_name, shell, @@ -32,8 +33,14 @@ use foundry_config::{ }; use foundry_evm::{fuzz::CounterExample, utils::evm_spec}; use regex::Regex; -use std::{collections::BTreeMap, path::PathBuf, sync::mpsc::channel, time::Duration}; +use std::{ + collections::{BTreeMap, HashMap}, + fs, + sync::mpsc::channel, + time::Duration, +}; use tracing::trace; +use ui::DebuggerArgs; use watchexec::config::{InitConfig, RuntimeConfig}; use yansi::Paint; @@ -178,65 +185,154 @@ impl TestArgs { // Prepare the test builder let evm_spec = evm_spec(config.evm_version); + let should_debug = self.debug.is_some(); - let mut runner = MultiContractRunnerBuilder::default() + let mut runner_builder = MultiContractRunnerBuilder::default() + .set_debug(should_debug) .initial_balance(evm_opts.initial_balance) .evm_spec(evm_spec) .sender(evm_opts.sender) .with_fork(evm_opts.get_fork(&config, env.clone())) .with_cheats_config(CheatsConfig::new(&config, &evm_opts)) - .with_test_options(test_options.clone()) - .build(project_root, output, env, evm_opts)?; + .with_test_options(test_options.clone()); + + let mut runner = runner_builder.clone().build( + project_root, + output.clone(), + env.clone(), + evm_opts.clone(), + )?; + + if should_debug { + filter.args_mut().test_pattern = self.debug.clone(); + let n = runner.count_filtered_tests(&filter); + if n != 1 { + return Err( + eyre::eyre!("{n} tests matched your criteria, but exactly 1 test must match in order to run the debugger.\n + \n + Use --match-contract and --match-path to further limit the search.")); + } + let test_funcs = runner.get_typed_tests(&filter); + // if we debug a fuzz test, we should not collect data on the first run + if !test_funcs.get(0).unwrap().inputs.is_empty() { + runner_builder = runner_builder.set_debug(false); + runner = runner_builder.clone().build( + project_root, + output.clone(), + env.clone(), + evm_opts.clone(), + )?; + } + } - if self.debug.is_some() { - filter.args_mut().test_pattern = self.debug; - - match runner.count_filtered_tests(&filter) { - 1 => { - // Run the test - let results = runner.test(&filter, None, test_options).await; - - // Get the result of the single test - let (id, sig, test_kind, counterexample, breakpoints) = results.iter().map(|(id, SuiteResult{ test_results, .. })| { - let (sig, result) = test_results.iter().next().unwrap(); - - (id.clone(), sig.clone(), result.kind.clone(), result.counterexample.clone(), result.breakpoints.clone()) - }).next().unwrap(); - - // Build debugger args if this is a fuzz test - let sig = match test_kind { - TestKind::Fuzz { first_case, .. } => { - if let Some(CounterExample::Single(counterexample)) = counterexample { - counterexample.calldata.to_string() - } else { - first_case.calldata.to_string() - } - }, - _ => sig, - }; + let mut local_identifier = LocalTraceIdentifier::new(&runner.known_contracts); + let remote_chain_id = runner.evm_opts.get_remote_chain_id(); - // Run the debugger - let mut opts = self.opts.clone(); - opts.silent = true; - let debugger = DebugArgs { - path: PathBuf::from(runner.source_paths.get(&id).unwrap()), - target_contract: Some(get_contract_name(&id).to_string()), - sig, - args: Vec::new(), - debug: true, - opts, - evm_opts: self.evm_opts, + let outcome = self.run_tests(runner, &config, filter.clone(), test_options.clone()).await?; + let tests = outcome.clone().into_tests(); + + let mut decoded_traces = Vec::new(); + let mut decoders = Vec::new(); + for test in tests { + let mut result = test.result; + // Identify addresses in each trace + let mut decoder = CallTraceDecoderBuilder::new() + .with_labels(result.labeled_addresses.clone()) + .with_events(local_identifier.events().cloned()) + .with_verbosity(verbosity) + .build(); + if !result.traces.is_empty() { + // Signatures are of no value for gas reports + if !self.gas_report { + let sig_identifier = + SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; + decoder.add_signature_identifier(sig_identifier.clone()); + } + + // Set up identifiers + // Do not re-query etherscan for contracts that you've already queried today. + let mut etherscan_identifier = EtherscanIdentifier::new(&config, remote_chain_id)?; + + // Decode the traces + for (kind, trace) in &mut result.traces { + 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 + // At verbosity level 4, we also display the setup trace for failed + // tests At verbosity level 5, we display + // all traces for all tests + TraceKind::Setup => { + (verbosity >= 5) || + (verbosity == 4 && result.status == TestStatus::Failure) + } + TraceKind::Execution => { + verbosity > 3 || + (verbosity == 3 && result.status == TestStatus::Failure) + } + _ => false, }; - debugger.debug(breakpoints).await?; - Ok(TestOutcome::new(results, self.allow_failure)) + // We decode the trace if we either need to build a gas report or we need + // to print it + if should_include || self.gas_report { + decoder.decode(trace).await; + } + + if should_include { + decoded_traces.push(trace.to_string()); + } } - n => - Err( - eyre::eyre!("{n} tests matched your criteria, but exactly 1 test must match in order to run the debugger.\n - \n - Use --match-contract and --match-path to further limit the search.")) } + + decoders.push(decoder); + } + + if should_debug { + let mut sources = HashMap::new(); + output.into_artifacts().for_each(|(id, artifact)| { + // Sources are only required for the debugger, but it *might* mean that there's + // something wrong with the build and/or artifacts. + if let Some(source) = artifact.source_file() { + let inner_map = sources.entry(id.clone().name).or_insert_with(HashMap::new); + let abs_path = source.ast.unwrap().absolute_path; + let source_code = fs::read_to_string(abs_path).unwrap(); + let contract = artifact.clone().into_contract_bytecode(); + let source_contract = compact_to_contract(contract); + inner_map.insert(source.id, (source_code, source_contract)); + } + }); + + let test = outcome.clone().into_tests().next().unwrap(); + let result = test.result; + + // Run the debugger + let debugger = DebuggerArgs { + debug: result.debug.map_or(vec![], |debug| vec![debug]), + decoder: decoders.first().unwrap(), + sources, + breakpoints: result.breakpoints, + }; + debugger.run()?; + } + + Ok(outcome) + } + + pub async fn run_tests( + &self, + mut runner: MultiContractRunner, + config: &Config, + mut filter: ProjectPathsAwareFilter, + test_options: TestOptions, + ) -> eyre::Result { + if self.debug.is_some() { + filter.args_mut().test_pattern = self.debug.clone(); + // Run the test + let results = runner.test(&filter, None, test_options).await; + + Ok(TestOutcome::new(results, self.allow_failure)) } else if self.list { list(runner, filter, self.json) } else { @@ -329,6 +425,7 @@ impl Test { } /// Represents the bundled results of all tests +#[derive(Clone)] pub struct TestOutcome { /// Whether failures are allowed pub allow_failure: bool, @@ -502,16 +599,12 @@ fn list( /// Runs all the tests #[allow(clippy::too_many_arguments)] async fn test( - config: Config, + &self, mut runner: MultiContractRunner, - verbosity: u8, + config: &Config, filter: ProjectPathsAwareFilter, - json: bool, - allow_failure: bool, test_options: TestOptions, - gas_reporting: bool, - fail_fast: bool, -) -> Result { +) -> eyre::Result { trace!(target: "forge::test", "running all tests"); if runner.count_filtered_tests(&filter) == 0 { let filter_str = filter.to_string(); @@ -533,149 +626,157 @@ async fn test( } } - if json { + if self.json { let results = runner.test(filter, None, test_options).await; println!("{}", serde_json::to_string(&results)?); - return Ok(TestOutcome::new(results, allow_failure)) - } - - // Set up identifiers - let known_contracts = runner.known_contracts.clone(); - let mut local_identifier = LocalTraceIdentifier::new(&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. - let mut etherscan_identifier = EtherscanIdentifier::new(&config, remote_chain_id)?; - - // Set up test reporter channel - let (tx, rx) = channel::<(String, SuiteResult)>(); + Ok(TestOutcome::new(results, self.allow_failure)) + } else { + // Set up identifiers + 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. + let mut etherscan_identifier = EtherscanIdentifier::new(config, remote_chain_id)?; - // Run tests - let handle = - tokio::task::spawn(async move { runner.test(filter, Some(tx), test_options).await }); + // Set up test reporter channel + let (tx, rx) = channel::<(String, SuiteResult)>(); - let mut results: BTreeMap = BTreeMap::new(); - let mut gas_report = GasReport::new(config.gas_reports, config.gas_reports_ignore); - let sig_identifier = SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; + // Run tests + let handle = + tokio::task::spawn(async move { runner.test(filter, Some(tx), test_options).await }); - let mut total_passed = 0; - let mut total_failed = 0; - let mut total_skipped = 0; + let mut results: BTreeMap = BTreeMap::new(); + let mut gas_report = + GasReport::new(config.gas_reports.clone(), config.gas_reports_ignore.clone()); + let sig_identifier = + SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; - 'outer: for (contract_name, suite_result) in rx { - results.insert(contract_name.clone(), suite_result.clone()); + let mut total_passed = 0; + let mut total_failed = 0; + let mut total_skipped = 0; - let mut tests = suite_result.test_results.clone(); - println!(); - for warning in suite_result.warnings.iter() { - eprintln!("{} {warning}", Paint::yellow("Warning:").bold()); - } - if !tests.is_empty() { - let term = if tests.len() > 1 { "tests" } else { "test" }; - println!("Running {} {term} for {contract_name}", tests.len()); - } - for (name, result) in &mut tests { - short_test_result(name, result); + 'outer: for (contract_name, suite_result) in rx { + results.insert(contract_name.clone(), suite_result.clone()); - // If the test failed, we want to stop processing the rest of the tests - if fail_fast && result.status == TestStatus::Failure { - break 'outer - } - - // We only display logs at level 2 and above - if verbosity >= 2 { - // We only decode logs from Hardhat and DS-style console events - let console_logs = decode_console_logs(&result.logs); - if !console_logs.is_empty() { - println!("Logs:"); - for log in console_logs { - println!(" {log}"); - } - println!(); - } + let mut tests = suite_result.test_results.clone(); + println!(); + for warning in suite_result.warnings.iter() { + eprintln!("{} {warning}", Paint::yellow("Warning:").bold()); } - - if result.traces.is_empty() { - continue + if !tests.is_empty() { + let term = if tests.len() > 1 { "tests" } else { "test" }; + println!("Running {} {term} for {contract_name}", tests.len()); } + for (name, result) in &mut tests { + short_test_result(name, result); - // Identify addresses in each trace - let mut builder = CallTraceDecoderBuilder::new() - .with_labels(result.labeled_addresses.iter().map(|(a, s)| (*a, s.clone()))) - .with_events(local_identifier.events().cloned()) - .with_verbosity(verbosity); + // If the test failed, we want to stop processing the rest of the tests + if self.fail_fast && result.status == TestStatus::Failure { + break 'outer + } - // Signatures are of no value for gas reports - if !gas_reporting { - builder = builder.with_signature_identifier(sig_identifier.clone()); - } + // We only display logs at level 2 and above + if config.verbosity >= 2 { + // We only decode logs from Hardhat and DS-style console events + let console_logs = decode_console_logs(&result.logs); + if !console_logs.is_empty() { + println!("Logs:"); + for log in console_logs { + println!(" {log}"); + } + println!(); + } + } - let mut decoder = builder.build(); - - // Decode the traces - let mut decoded_traces = Vec::with_capacity(result.traces.len()); - for (kind, trace) in &mut result.traces { - decoder.identify(trace, &mut local_identifier); - decoder.identify(trace, &mut etherscan_identifier); - - // verbosity: - // - 0..3: nothing - // - 3: only display traces for failed tests - // - 4: also display the setup trace for failed tests - // - 5..: display all traces for all tests - let should_include = match kind { - TraceKind::Execution => { - (verbosity == 3 && result.status.is_failure()) || verbosity >= 4 + if !result.traces.is_empty() { + // Identify addresses in each trace + let mut decoder = CallTraceDecoderBuilder::new() + .with_labels(result.labeled_addresses.clone()) + .with_events(local_identifier.events().cloned()) + .with_verbosity(config.verbosity) + .build(); + + // Signatures are of no value for gas reports + if !self.gas_report { + decoder.add_signature_identifier(sig_identifier.clone()); } - TraceKind::Setup => { - (verbosity == 4 && result.status.is_failure()) || verbosity >= 5 + + // Decode the traces + let mut decoded_traces = Vec::new(); + for (kind, trace) in &mut result.traces { + 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 + // At verbosity level 4, we also display the setup trace for failed + // tests At verbosity level 5, we display + // all traces for all tests + TraceKind::Setup => { + (config.verbosity >= 5) || + (config.verbosity == 4 && + result.status == TestStatus::Failure) + } + TraceKind::Execution => { + config.verbosity > 3 || + (config.verbosity == 3 && + result.status == TestStatus::Failure) + } + _ => false, + }; + + // We decode the trace if we either need to build a gas report or we + // need to print it + if should_include || self.gas_report { + decoder.decode(trace).await; + } + + if should_include { + decoded_traces.push(trace.to_string()); + } } - TraceKind::Deployment => false, - }; - // Decode the trace if we either need to build a gas report or we need to print it - if should_include || gas_reporting { - decoder.decode(trace).await; - } + if !decoded_traces.is_empty() { + println!("Traces:"); + decoded_traces.into_iter().for_each(|trace| println!("{trace}")); + } - if should_include { - decoded_traces.push(trace.to_string()); + if self.gas_report { + gas_report.analyze(&result.traces); + } } } + let block_outcome = + TestOutcome::new([(contract_name, suite_result)].into(), self.allow_failure); - if !decoded_traces.is_empty() { - println!("Traces:"); - decoded_traces.into_iter().for_each(|trace| println!("{trace}")); - } + total_passed += block_outcome.successes().count(); + total_failed += block_outcome.failures().count(); + total_skipped += block_outcome.skips().count(); - if gas_reporting { - gas_report.analyze(&result.traces); - } + println!("{}", block_outcome.summary()); } - let block_outcome = TestOutcome::new([(contract_name, suite_result)].into(), allow_failure); - total_passed += block_outcome.successes().count(); - total_failed += block_outcome.failures().count(); - total_skipped += block_outcome.skips().count(); + if self.gas_report { + println!("{}", gas_report.finalize()); + } - println!("{}", block_outcome.summary()); - } + let num_test_suites = results.len(); - if gas_reporting { - println!("{}", gas_report.finalize()); - } + if num_test_suites > 0 { + println!( + "{}", + format_aggregated_summary( + num_test_suites, + total_passed, + total_failed, + total_skipped + ) + ); + } - let num_test_suites = results.len(); + // reattach the thread + let _results = handle.await?; - if num_test_suites > 0 { - println!( - "{}", - format_aggregated_summary(num_test_suites, total_passed, total_failed, total_skipped) - ); + trace!(target: "forge::test", "received {} results", results.len()); + Ok(TestOutcome::new(results, self.allow_failure)) } - - // reattach the thread - let _results = handle.await?; - - trace!(target: "forge::test", "received {} results", results.len()); - Ok(TestOutcome::new(results, allow_failure)) } diff --git a/crates/forge/bin/main.rs b/crates/forge/bin/main.rs index 633b7b8002ef0..08f1afefc168e 100644 --- a/crates/forge/bin/main.rs +++ b/crates/forge/bin/main.rs @@ -31,7 +31,7 @@ fn main() -> Result<()> { cmd.opts.args.silent, cmd.json, ))?; - utils::block_on(cmd.run_script(Default::default())) + utils::block_on(cmd.run_script()) } Subcommands::Coverage(cmd) => utils::block_on(cmd.run()), Subcommands::Bind(cmd) => cmd.run(), @@ -42,7 +42,7 @@ fn main() -> Result<()> { cmd.run().map(|_| ()) } } - Subcommands::Debug(cmd) => utils::block_on(cmd.debug(Default::default())), + Subcommands::Debug(cmd) => utils::block_on(cmd.run()), Subcommands::VerifyContract(args) => utils::block_on(args.run()), Subcommands::VerifyCheck(args) => utils::block_on(args.run()), Subcommands::Cache(cmd) => match cmd.sub { diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 24b06d63f088d..7639a1e22e2c4 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -1,6 +1,6 @@ use crate::{result::SuiteResult, ContractRunner, TestFilter, TestOptions}; use ethers::{ - abi::Abi, + abi::{Abi, Function}, prelude::{artifacts::CompactContractBytecode, ArtifactId, ArtifactOutput}, solc::{contracts::ArtifactContracts, Artifact, ProjectCompileOutput}, types::{Address, Bytes, U256}, @@ -19,6 +19,7 @@ use rayon::prelude::*; use revm::primitives::SpecId; use std::{ collections::{BTreeMap, HashSet}, + iter::Iterator, path::Path, sync::{mpsc::Sender, Arc}, }; @@ -51,6 +52,8 @@ pub struct MultiContractRunner { pub cheats_config: Arc, /// Whether to collect coverage info pub coverage: bool, + /// Whether to collect debug info + pub debug: bool, /// Settings related to fuzz and/or invariant tests pub test_options: TestOptions, } @@ -70,19 +73,31 @@ impl MultiContractRunner { .count() } - // Get all tests of matching path and contract - pub fn get_tests(&self, filter: &impl TestFilter) -> Vec { + fn filtered_tests<'a>( + &'a self, + filter: &'a impl TestFilter, + ) -> impl Iterator { self.contracts .iter() .filter(|(id, _)| { filter.matches_path(id.source.to_string_lossy()) && filter.matches_contract(&id.name) }) - .flat_map(|(_, (abi, _, _))| abi.functions().map(|func| func.name.clone())) - .filter(|sig| sig.is_test()) + .flat_map(|(_, (abi, _, _))| abi.functions()) + } + + // Get all tests of matching path and contract + pub fn get_tests(&self, filter: &impl TestFilter) -> Vec { + self.filtered_tests(filter) + .map(|func| func.name.clone()) + .filter(|name| name.is_test()) .collect() } + pub fn get_typed_tests<'a>(&'a self, filter: &'a impl TestFilter) -> Vec<&Function> { + self.filtered_tests(filter).filter(|func| func.name.is_test()).collect() + } + /// Returns all matching tests grouped by contract grouped by file (file -> (contract -> tests)) pub fn list( &self, @@ -143,7 +158,8 @@ impl MultiContractRunner { .inspectors(|stack| { stack .cheatcodes(self.cheats_config.clone()) - .trace(self.evm_opts.verbosity >= 3) + .trace(self.evm_opts.verbosity >= 3 || self.debug) + .debug(self.debug) .coverage(self.coverage) }) .spec(self.evm_spec) @@ -193,13 +209,14 @@ impl MultiContractRunner { self.sender, self.errors.as_ref(), libs, + self.debug, ); runner.run_tests(filter, test_options, Some(&self.known_contracts)) } } /// Builder used for instantiating the multi-contract runner -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct MultiContractRunnerBuilder { /// The address which will be used to deploy the initial contracts and send all /// transactions @@ -214,6 +231,8 @@ pub struct MultiContractRunnerBuilder { pub cheats_config: Option, /// Whether or not to collect coverage info pub coverage: bool, + /// Whether or not to collect debug info + pub debug: bool, /// Settings related to fuzz and/or invariant tests pub test_options: Option, } @@ -324,6 +343,7 @@ impl MultiContractRunnerBuilder { fork: self.fork, cheats_config: self.cheats_config.unwrap_or_default().into(), coverage: self.coverage, + debug: self.debug, test_options: self.test_options.unwrap_or_default(), }) } @@ -369,4 +389,10 @@ impl MultiContractRunnerBuilder { self.coverage = enable; self } + + #[must_use] + pub fn set_debug(mut self, enable: bool) -> Self { + self.debug = enable; + self + } } diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index ca717e61b6484..c06ec53e0c217 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -122,6 +122,9 @@ pub struct TestResult { /// Labeled addresses pub labeled_addresses: BTreeMap, + /// The debug nodes of the call + pub debug: Option, + /// pc breakpoint char map pub breakpoints: Breakpoints, } diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index b81ce598aca64..41e62f8703369 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -20,7 +20,8 @@ use foundry_evm::{ replay_run, InvariantContract, InvariantExecutor, InvariantFuzzError, InvariantFuzzTestResult, }, - FuzzedExecutor, + types::{CaseOutcome, CounterExampleOutcome, FuzzOutcome}, + CounterExample, FuzzedExecutor, }, trace::{load_contracts, TraceKind}, CALLER, @@ -51,6 +52,8 @@ pub struct ContractRunner<'a> { pub initial_balance: U256, /// The address which will be used as the `from` field in all EVM calls pub sender: Address, + /// Should generate debug traces + pub debug: bool, } impl<'a> ContractRunner<'a> { @@ -64,6 +67,7 @@ impl<'a> ContractRunner<'a> { sender: Option
, errors: Option<&'a Abi>, predeploy_libs: &'a [Bytes], + debug: bool, ) -> Self { Self { name, @@ -74,6 +78,7 @@ impl<'a> ContractRunner<'a> { sender: sender.unwrap_or_default(), errors, predeploy_libs, + debug, } } } @@ -233,6 +238,7 @@ impl<'a> ContractRunner<'a> { coverage: None, labeled_addresses: setup.labeled_addresses, breakpoints: Default::default(), + debug: Default::default(), }, )] .into(), @@ -240,18 +246,20 @@ impl<'a> ContractRunner<'a> { ) } - let functions: Vec<_> = self.contract.functions().collect(); - let mut test_results = functions + let mut test_results = self + .contract + .functions .par_iter() - .filter(|&&func| func.is_test() && filter.matches_test(func.signature())) - .map(|&func| { + .flat_map(|(_, f)| f) + .filter(|&func| func.is_test() && filter.matches_test(func.signature())) + .map(|func| { let should_fail = func.is_test_fail(); let res = if func.is_fuzz_test() { let runner = test_options.fuzz_runner(self.name, &func.name); let fuzz_config = test_options.fuzz_config(self.name, &func.name); self.run_fuzz_test(func, should_fail, runner, setup.clone(), *fuzz_config) } else { - self.run_test(func, should_fail, setup.clone()) + self.clone().run_test(func, should_fail, setup.clone()) }; (func.signature(), res) }) @@ -301,75 +309,72 @@ impl<'a> ContractRunner<'a> { /// State modifications are not committed to the evm database but discarded after the call, /// similar to `eth_call`. #[instrument(name = "test", skip_all, fields(name = %func.signature(), %should_fail))] - pub fn run_test(&self, func: &Function, should_fail: bool, setup: TestSetup) -> TestResult { + pub fn run_test(mut self, func: &Function, should_fail: bool, setup: TestSetup) -> TestResult { let TestSetup { address, mut logs, mut traces, mut labeled_addresses, .. } = setup; // Run unit test - let mut executor = self.executor.clone(); let start = Instant::now(); - let (reverted, reason, gas, stipend, coverage, state_changeset, breakpoints) = - match executor.execute_test::<(), _, _>( - self.sender, - address, - func.clone(), - (), - 0.into(), - self.errors, - ) { - Ok(CallResult { - reverted, - gas_used: gas, - stipend, - logs: execution_logs, - traces: execution_trace, - coverage, - labels: new_labels, - state_changeset, - breakpoints, - .. - }) => { - traces.extend(execution_trace.map(|traces| (TraceKind::Execution, traces))); - labeled_addresses.extend(new_labels); - logs.extend(execution_logs); - (reverted, None, gas, stipend, coverage, state_changeset, breakpoints) - } - Err(EvmError::Execution(err)) => { - traces.extend(err.traces.map(|traces| (TraceKind::Execution, traces))); - labeled_addresses.extend(err.labels); - logs.extend(err.logs); - ( - err.reverted, - Some(err.reason), - err.gas_used, - err.stipend, - None, - err.state_changeset, - HashMap::new(), - ) - } - Err(EvmError::SkipError) => { - return TestResult { - status: TestStatus::Skipped, - reason: None, - decoded_logs: decode_console_logs(&logs), - traces, - labeled_addresses, - kind: TestKind::Standard(0), - ..Default::default() - } + let mut debug_arena = None; + let (reverted, reason, gas, stipend, coverage, state_changeset, breakpoints) = match self + .executor + .execute_test::<(), _, _>(self.sender, address, func.clone(), (), 0.into(), self.errors) + { + Ok(CallResult { + reverted, + gas_used: gas, + stipend, + logs: execution_logs, + traces: execution_trace, + coverage, + labels: new_labels, + state_changeset, + debug, + breakpoints, + .. + }) => { + traces.extend(execution_trace.map(|traces| (TraceKind::Execution, traces))); + labeled_addresses.extend(new_labels); + logs.extend(execution_logs); + debug_arena = debug; + (reverted, None, gas, stipend, coverage, state_changeset, breakpoints) + } + Err(EvmError::Execution(err)) => { + traces.extend(err.traces.map(|traces| (TraceKind::Execution, traces))); + labeled_addresses.extend(err.labels); + logs.extend(err.logs); + ( + err.reverted, + Some(err.reason), + err.gas_used, + err.stipend, + None, + err.state_changeset, + HashMap::new(), + ) + } + Err(EvmError::SkipError) => { + return TestResult { + status: TestStatus::Skipped, + reason: None, + decoded_logs: decode_console_logs(&logs), + traces, + labeled_addresses, + kind: TestKind::Standard(0), + ..Default::default() } - Err(err) => { - return TestResult { - status: TestStatus::Failure, - reason: Some(err.to_string()), - decoded_logs: decode_console_logs(&logs), - traces, - labeled_addresses, - kind: TestKind::Standard(0), - ..Default::default() - } + } + Err(err) => { + return TestResult { + status: TestStatus::Failure, + reason: Some(err.to_string()), + decoded_logs: decode_console_logs(&logs), + traces, + labeled_addresses, + kind: TestKind::Standard(0), + ..Default::default() } - }; + } + }; let success = executor.is_success( setup.address, @@ -398,6 +403,7 @@ impl<'a> ContractRunner<'a> { traces, coverage, labeled_addresses, + debug: debug_arena, breakpoints, } } @@ -547,8 +553,16 @@ impl<'a> ContractRunner<'a> { // Run fuzz test let start = Instant::now(); - let mut result = FuzzedExecutor::new(&self.executor, runner, self.sender, fuzz_config) - .fuzz(func, address, should_fail, self.errors); + let mut result = FuzzedExecutor::new( + &self.executor, + runner.clone(), + self.sender, + fuzz_config, + ) + .fuzz(func, address, should_fail, self.errors); + + let mut debug = Default::default(); + let mut breakpoints = Default::default(); // Check the last test result and skip the test // if it's marked as so. @@ -560,10 +574,50 @@ impl<'a> ContractRunner<'a> { traces, labeled_addresses, kind: TestKind::Standard(0), + debug, + breakpoints, ..Default::default() } } + // if should debug + if self.debug { + let mut debug_executor = self.executor.clone(); + // turn the debug traces on + debug_executor.inspector.enable_debugger(true); + debug_executor.inspector.tracing(true); + let calldata = if let Some(counterexample) = result.counterexample.as_ref() { + match counterexample { + CounterExample::Single(ce) => ce.calldata.clone(), + _ => unimplemented!(), + } + } else { + result.first_case.calldata.clone() + }; + // rerun the last relevant test with traces + let debug_result = FuzzedExecutor::new( + &debug_executor, + runner, + self.sender, + fuzz_config, + ) + .single_fuzz(&result.state, address, should_fail, calldata); + + (debug, breakpoints) = match debug_result { + Ok(fuzz_outcome) => match fuzz_outcome { + FuzzOutcome::Case(CaseOutcome { debug, breakpoints, .. }) => { + (debug, breakpoints) + } + FuzzOutcome::CounterExample(CounterExampleOutcome { + debug, + breakpoints, + .. + }) => (debug, breakpoints), + }, + Err(_) => (Default::default(), Default::default()), + }; + } + let kind = TestKind::Fuzz { median_gas: result.median_gas(false), mean_gas: result.mean_gas(false), @@ -595,7 +649,8 @@ impl<'a> ContractRunner<'a> { traces, coverage: result.coverage, labeled_addresses, - breakpoints: Default::default(), + debug, + breakpoints, } } } diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index a63870ed9f332..9cd23d598d143 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -8,7 +8,7 @@ use crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; -use ethers::{solc::artifacts::ContractBytecodeSome, types::Address}; +use ethers::types::Address; use eyre::Result; use foundry_common::evm::Breakpoints; use foundry_evm::{ @@ -991,7 +991,7 @@ Spans::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/ } impl Ui for Tui { - fn launch(&mut self) -> eyre::Result { + fn launch(&mut self) -> Result { // If something panics inside here, we should do everything we can to // not corrupt the user's terminal. std::panic::set_hook(Box::new(|e| { From 29aa8ab461190182c08561b8396f3459a897990d Mon Sep 17 00:00:00 2001 From: franfran Date: Thu, 31 Aug 2023 00:29:10 +0300 Subject: [PATCH 08/17] finish him! --- crates/cli/src/utils/cmd.rs | 46 ++--- crates/common/src/compile.rs | 19 +- crates/common/src/contracts.rs | 14 +- crates/evm/src/debug.rs | 6 +- crates/evm/src/fuzz/mod.rs | 18 +- crates/evm/src/fuzz/types.rs | 11 +- crates/forge/bin/cmd/script/mod.rs | 12 +- crates/forge/bin/cmd/test/mod.rs | 298 +++++++++++++++-------------- crates/forge/src/result.rs | 1 + crates/forge/src/runner.rs | 152 +++++++-------- crates/ui/src/debugger.rs | 1 + crates/ui/src/lib.rs | 2 +- 12 files changed, 290 insertions(+), 290 deletions(-) diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 4323ddb2c6463..396dccadd28d3 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -2,11 +2,11 @@ use ethers::{ abi::Abi, core::types::Chain, solc::{ - artifacts::{CompactBytecode, CompactDeployedBytecode, ContractBytecodeSome}, + artifacts::{CompactBytecode, CompactDeployedBytecode}, cache::{CacheEntry, SolFilesCache}, info::ContractInfo, utils::read_json_file, - Artifact, ArtifactId, ProjectCompileOutput, + Artifact, ProjectCompileOutput, }, }; use eyre::{Result, WrapErr}; @@ -20,9 +20,9 @@ use foundry_evm::{ CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces, }, }; -use std::{collections::BTreeMap, fmt::Write, path::PathBuf, str::FromStr}; +use std::{fmt::Write, path::PathBuf, str::FromStr}; use tracing::trace; -use ui::{TUIExitReason, Tui, Ui}; +use ui::DebuggerArgs; use yansi::Paint; /// Given a `Project`'s output, removes the matching ABI, Bytecode and @@ -391,8 +391,14 @@ pub async fn handle_traces( } if debug { - let (sources, bytecode) = etherscan_identifier.get_compiled_contracts().await?; - run_debugger(result, decoder, bytecode, sources)?; + let (sources, _compiled_contracts) = etherscan_identifier.get_compiled_contracts().await?; + let debugger = DebuggerArgs { + debug: vec![result.debug], + decoder: &decoder, + sources, + breakpoints: Default::default(), + }; + debugger.run()?; } else { print_traces(&mut result, &decoder, verbose).await?; } @@ -429,31 +435,3 @@ pub async fn print_traces( println!("Gas used: {}", result.gas_used); Ok(()) } - -pub fn run_debugger( - result: TraceResult, - decoder: CallTraceDecoder, - known_contracts: BTreeMap, - sources: BTreeMap, -) -> 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, - 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(), - Default::default(), - )?; - match tui.start().expect("Failed to start tui") { - TUIExitReason::CharExit => Ok(()), - } -} diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 37d765ae2b747..c50da3b76b08a 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -1,5 +1,5 @@ //! Support for compiling [ethers::solc::Project] -use crate::{glob::GlobMatcher, term, TestFunctionExt}; +use crate::{compact_to_contract, glob::GlobMatcher, term, TestFunctionExt}; use comfy_table::{presets::ASCII_MARKDOWN, *}; use ethers_etherscan::contract::Metadata; use ethers_solc::{ @@ -401,7 +401,7 @@ pub fn compile_target_with_filter( /// Creates and compiles a project from an Etherscan source. pub async fn compile_from_source( metadata: &Metadata, -) -> Result<(ArtifactId, ContractBytecodeSome)> { +) -> Result<(ArtifactId, u32, ContractBytecodeSome)> { let root = tempfile::tempdir()?; let root_path = root.path(); let project = etherscan_project(metadata, root_path)?; @@ -412,19 +412,18 @@ pub async fn compile_from_source( eyre::bail!(project_output.to_string()) } - let (artifact_id, contract) = project_output - .into_contract_bytecodes() + let (artifact_id, file_id, contract) = project_output + .into_artifacts() .find(|(artifact_id, _)| artifact_id.name == metadata.contract_name) + .map(|(aid, art)| { + (aid, art.source_file().expect("no source file").id, art.into_contract_bytecode()) + }) .expect("there should be a contract with bytecode"); - let bytecode = ContractBytecodeSome { - abi: contract.abi.unwrap(), - bytecode: contract.bytecode.unwrap().into(), - deployed_bytecode: contract.deployed_bytecode.unwrap().into(), - }; + let bytecode = compact_to_contract(contract); root.close()?; - Ok((artifact_id, bytecode)) + Ok((artifact_id, file_id, bytecode)) } /// Creates a [Project] from an Etherscan source. diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index 0440b000d9821..faff763282806 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -5,7 +5,10 @@ use ethers_core::{ types::{Address, H256}, utils::hex, }; -use ethers_solc::{artifacts::ContractBytecodeSome, ArtifactId, ProjectPathsConfig}; +use ethers_solc::{ + artifacts::{CompactContractBytecode, ContractBytecodeSome}, + ArtifactId, ProjectPathsConfig, +}; use once_cell::sync::Lazy; use regex::Regex; use std::{ @@ -265,3 +268,12 @@ mod tests { let _decoded = abi::decode(¶ms, args).unwrap(); } } + +/// Helper function to convert CompactContractBytecode ~> ContractBytecodeSome +pub fn compact_to_contract(contract: CompactContractBytecode) -> ContractBytecodeSome { + ContractBytecodeSome { + abi: contract.abi.unwrap(), + bytecode: contract.bytecode.unwrap().into(), + deployed_bytecode: contract.deployed_bytecode.unwrap().into(), + } +} diff --git a/crates/evm/src/debug.rs b/crates/evm/src/debug.rs index 885830abb4da1..c229dbca90b3e 100644 --- a/crates/evm/src/debug.rs +++ b/crates/evm/src/debug.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; /// An arena of [DebugNode]s -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct DebugArena { /// The arena of nodes pub arena: Vec, @@ -78,7 +78,7 @@ impl DebugArena { } /// A node in the arena -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, Serialize, Deserialize)] pub struct DebugNode { /// Parent node index in the arena pub parent: Option, @@ -109,7 +109,7 @@ impl DebugNode { /// It holds the current program counter (where in the program you are), /// the stack and memory (prior to the opcodes execution), any bytes to be /// pushed onto the stack, and the instruction counter for use with sourcemap. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct DebugStep { /// Stack *prior* to running the associated opcode pub stack: Vec, diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index 4bdd997921d6f..573fc5a9dda6c 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -82,11 +82,7 @@ impl<'a> FuzzedExecutor<'a> { let coverage: RefCell> = RefCell::default(); // Stores fuzz state for use with [fuzz_calldata_from_state] - let state: EvmFuzzState = if let Some(fork_db) = self.executor.backend.active_fork_db() { - build_initial_state(fork_db, &self.config.dictionary) - } else { - build_initial_state(self.executor.backend.mem_db(), &self.config.dictionary) - }; + let state = self.build_fuzz_state(); let mut weights = vec![]; let dictionary_weight = self.config.dictionary.dictionary_weight.min(100); @@ -108,10 +104,10 @@ impl<'a> FuzzedExecutor<'a> { match fuzz_res { FuzzOutcome::Case(case) => { let mut first_case = first_case.borrow_mut(); + gas_by_case.borrow_mut().push((case.case.gas, case.case.stipend)); if first_case.is_none() { first_case.replace(case.case); } - gas_by_case.borrow_mut().push((case.gas_used, case.stipend)); traces.replace(case.traces); @@ -234,8 +230,6 @@ impl<'a> FuzzedExecutor<'a> { if success { Ok(FuzzOutcome::Case(CaseOutcome { case: FuzzCase { calldata, gas: call.gas_used, stipend: call.stipend }, - gas_used: call.gas_used, - stipend: call.stipend, traces: call.traces, coverage: call.coverage, debug: call.debug, @@ -250,6 +244,14 @@ impl<'a> FuzzedExecutor<'a> { })) } } + + pub fn build_fuzz_state(&self) -> EvmFuzzState { + if let Some(fork_db) = self.executor.backend.active_fork_db() { + build_initial_state(fork_db, &self.config.dictionary) + } else { + build_initial_state(self.executor.backend.mem_db(), &self.config.dictionary) + } + } } #[derive(Clone, Debug, Serialize, Deserialize)] diff --git a/crates/evm/src/fuzz/types.rs b/crates/evm/src/fuzz/types.rs index 266bd671d4e84..7178376a67a74 100644 --- a/crates/evm/src/fuzz/types.rs +++ b/crates/evm/src/fuzz/types.rs @@ -18,21 +18,28 @@ pub struct FuzzCase { /// Returned by a single fuzz in the case of a successful run #[derive(Debug)] pub struct CaseOutcome { + /// Data of a single fuzz test case pub case: FuzzCase, - pub gas_used: u64, - pub stipend: u64, + /// The traces of the call pub traces: Option, + /// The coverage info collected during the call pub coverage: Option, + /// The debug nodes of the call pub debug: Option, + /// Breakpoints char pc map pub breakpoints: Breakpoints, } /// Returned by a single fuzz when a counterexample has been discovered #[derive(Debug)] pub struct CounterExampleOutcome { + /// Minimal reproduction test case for failing test pub counterexample: (ethers::types::Bytes, RawCallResult), + /// The status of the call pub exit_reason: InstructionResult, + /// The debug nodes of the call pub debug: Option, + /// Breakpoints char pc map pub breakpoints: Breakpoints, } diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 4512f6333d280..6515883edf083 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -1,7 +1,4 @@ -use self::{ - build::{filter_sources_and_artifacts, BuildOutput}, - runner::ScriptRunner, -}; +use self::{build::BuildOutput, runner::ScriptRunner}; use super::{build::BuildArgs, retry::RetryArgs}; use clap::{Parser, ValueHint}; use dialoguer::Confirm; @@ -56,12 +53,7 @@ use foundry_evm::{ }; use futures::future; use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap, HashSet, VecDeque}, - path::PathBuf, -}; -use tracing::log::trace; -use ui::{TUIExitReason, Tui, Ui}; +use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use yansi::Paint; mod artifacts; diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 2f821165cd3bc..95e135f0b9ea2 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -225,10 +225,13 @@ impl TestArgs { } } - let mut local_identifier = LocalTraceIdentifier::new(&runner.known_contracts); + let known_contracts = runner.known_contracts.clone(); + let mut local_identifier = LocalTraceIdentifier::new(&known_contracts); let remote_chain_id = runner.evm_opts.get_remote_chain_id(); - let outcome = self.run_tests(runner, &config, filter.clone(), test_options.clone()).await?; + let outcome = self + .run_tests(runner, &config, verbosity, filter.clone(), test_options.clone()) + .await?; let tests = outcome.clone().into_tests(); let mut decoded_traces = Vec::new(); @@ -236,19 +239,21 @@ impl TestArgs { for test in tests { let mut result = test.result; // Identify addresses in each trace - let mut decoder = CallTraceDecoderBuilder::new() + let mut builder = CallTraceDecoderBuilder::new() .with_labels(result.labeled_addresses.clone()) .with_events(local_identifier.events().cloned()) - .with_verbosity(verbosity) - .build(); - if !result.traces.is_empty() { - // Signatures are of no value for gas reports - if !self.gas_report { - let sig_identifier = - SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; - decoder.add_signature_identifier(sig_identifier.clone()); - } + .with_verbosity(verbosity); + + // Signatures are of no value for gas reports + if !self.gas_report { + let sig_identifier = + SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; + builder = builder.with_signature_identifier(sig_identifier.clone()); + } + let mut decoder = builder.build(); + + if !result.traces.is_empty() { // Set up identifiers // Do not re-query etherscan for contracts that you've already queried today. let mut etherscan_identifier = EtherscanIdentifier::new(&config, remote_chain_id)?; @@ -324,6 +329,7 @@ impl TestArgs { &self, mut runner: MultiContractRunner, config: &Config, + verbosity: u8, mut filter: ProjectPathsAwareFilter, test_options: TestOptions, ) -> eyre::Result { @@ -337,7 +343,7 @@ impl TestArgs { list(runner, filter, self.json) } else { test( - config, + config.clone(), runner, verbosity, filter, @@ -494,13 +500,13 @@ impl TestOutcome { } println!(); } - let successes = self.successes().count(); println!( "Encountered a total of {} failing tests, {} tests succeeded", Paint::red(failures.to_string()), Paint::green(successes.to_string()) ); + std::process::exit(1); } @@ -516,9 +522,9 @@ impl TestOutcome { format!( "Test result: {}. {} passed; {} failed; {} skipped; finished in {:.2?}", result, - self.successes().count(), - failed, - self.skips().count(), + Paint::green(self.successes().count()), + Paint::red(failed), + Paint::yellow(self.skips().count()), self.duration() ) } @@ -569,8 +575,12 @@ fn format_aggregated_summary( ) -> String { let total_tests = total_passed + total_failed + total_skipped; format!( - "Ran {} test suites: {} tests passed, {} failed, {} skipped ({} total tests)", - num_test_suites, total_passed, total_failed, total_skipped, total_tests + " \nRan {} test suites: {} tests passed, {} failed, {} skipped ({} total tests)", + num_test_suites, + Paint::green(total_passed), + Paint::red(total_failed), + Paint::yellow(total_skipped), + total_tests ) } @@ -599,12 +609,16 @@ fn list( /// Runs all the tests #[allow(clippy::too_many_arguments)] async fn test( - &self, + config: Config, mut runner: MultiContractRunner, - config: &Config, + verbosity: u8, filter: ProjectPathsAwareFilter, + json: bool, + allow_failure: bool, test_options: TestOptions, -) -> eyre::Result { + gas_reporting: bool, + fail_fast: bool, +) -> Result { trace!(target: "forge::test", "running all tests"); if runner.count_filtered_tests(&filter) == 0 { let filter_str = filter.to_string(); @@ -626,157 +640,149 @@ async fn test( } } - if self.json { + if json { let results = runner.test(filter, None, test_options).await; println!("{}", serde_json::to_string(&results)?); - Ok(TestOutcome::new(results, self.allow_failure)) - } else { - // Set up identifiers - 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. - let mut etherscan_identifier = EtherscanIdentifier::new(config, remote_chain_id)?; + return Ok(TestOutcome::new(results, allow_failure)) + } - // Set up test reporter channel - let (tx, rx) = channel::<(String, SuiteResult)>(); + // Set up identifiers + let known_contracts = runner.known_contracts.clone(); + let mut local_identifier = LocalTraceIdentifier::new(&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. + let mut etherscan_identifier = EtherscanIdentifier::new(&config, remote_chain_id)?; - // Run tests - let handle = - tokio::task::spawn(async move { runner.test(filter, Some(tx), test_options).await }); + // Set up test reporter channel + let (tx, rx) = channel::<(String, SuiteResult)>(); - let mut results: BTreeMap = BTreeMap::new(); - let mut gas_report = - GasReport::new(config.gas_reports.clone(), config.gas_reports_ignore.clone()); - let sig_identifier = - SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; + // Run tests + let handle = + tokio::task::spawn(async move { runner.test(filter, Some(tx), test_options).await }); - let mut total_passed = 0; - let mut total_failed = 0; - let mut total_skipped = 0; + let mut results: BTreeMap = BTreeMap::new(); + let mut gas_report = GasReport::new(config.gas_reports, config.gas_reports_ignore); + let sig_identifier = SignaturesIdentifier::new(Config::foundry_cache_dir(), config.offline)?; - 'outer: for (contract_name, suite_result) in rx { - results.insert(contract_name.clone(), suite_result.clone()); + let mut total_passed = 0; + let mut total_failed = 0; + let mut total_skipped = 0; - let mut tests = suite_result.test_results.clone(); - println!(); - for warning in suite_result.warnings.iter() { - eprintln!("{} {warning}", Paint::yellow("Warning:").bold()); - } - if !tests.is_empty() { - let term = if tests.len() > 1 { "tests" } else { "test" }; - println!("Running {} {term} for {contract_name}", tests.len()); - } - for (name, result) in &mut tests { - short_test_result(name, result); + 'outer: for (contract_name, suite_result) in rx { + results.insert(contract_name.clone(), suite_result.clone()); - // If the test failed, we want to stop processing the rest of the tests - if self.fail_fast && result.status == TestStatus::Failure { - break 'outer - } + let mut tests = suite_result.test_results.clone(); + println!(); + for warning in suite_result.warnings.iter() { + eprintln!("{} {warning}", Paint::yellow("Warning:").bold()); + } + if !tests.is_empty() { + let term = if tests.len() > 1 { "tests" } else { "test" }; + println!("Running {} {term} for {contract_name}", tests.len()); + } + for (name, result) in &mut tests { + short_test_result(name, result); - // We only display logs at level 2 and above - if config.verbosity >= 2 { - // We only decode logs from Hardhat and DS-style console events - let console_logs = decode_console_logs(&result.logs); - if !console_logs.is_empty() { - println!("Logs:"); - for log in console_logs { - println!(" {log}"); - } - println!(); + // If the test failed, we want to stop processing the rest of the tests + if fail_fast && result.status == TestStatus::Failure { + break 'outer + } + + // We only display logs at level 2 and above + if verbosity >= 2 { + // We only decode logs from Hardhat and DS-style console events + let console_logs = decode_console_logs(&result.logs); + if !console_logs.is_empty() { + println!("Logs:"); + for log in console_logs { + println!(" {log}"); } + println!(); } + } - if !result.traces.is_empty() { - // Identify addresses in each trace - let mut decoder = CallTraceDecoderBuilder::new() - .with_labels(result.labeled_addresses.clone()) - .with_events(local_identifier.events().cloned()) - .with_verbosity(config.verbosity) - .build(); - - // Signatures are of no value for gas reports - if !self.gas_report { - decoder.add_signature_identifier(sig_identifier.clone()); - } + if result.traces.is_empty() { + continue + } - // Decode the traces - let mut decoded_traces = Vec::new(); - for (kind, trace) in &mut result.traces { - 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 - // At verbosity level 4, we also display the setup trace for failed - // tests At verbosity level 5, we display - // all traces for all tests - TraceKind::Setup => { - (config.verbosity >= 5) || - (config.verbosity == 4 && - result.status == TestStatus::Failure) - } - TraceKind::Execution => { - config.verbosity > 3 || - (config.verbosity == 3 && - result.status == TestStatus::Failure) - } - _ => false, - }; - - // We decode the trace if we either need to build a gas report or we - // need to print it - if should_include || self.gas_report { - decoder.decode(trace).await; - } + // Identify addresses in each trace + let mut builder = CallTraceDecoderBuilder::new() + .with_labels(result.labeled_addresses.iter().map(|(a, s)| (*a, s.clone()))) + .with_events(local_identifier.events().cloned()) + .with_verbosity(verbosity); - if should_include { - decoded_traces.push(trace.to_string()); - } - } + // Signatures are of no value for gas reports + if !gas_reporting { + builder = builder.with_signature_identifier(sig_identifier.clone()); + } - if !decoded_traces.is_empty() { - println!("Traces:"); - decoded_traces.into_iter().for_each(|trace| println!("{trace}")); + let mut decoder = builder.build(); + + // Decode the traces + let mut decoded_traces = Vec::with_capacity(result.traces.len()); + for (kind, trace) in &mut result.traces { + decoder.identify(trace, &mut local_identifier); + decoder.identify(trace, &mut etherscan_identifier); + + // verbosity: + // - 0..3: nothing + // - 3: only display traces for failed tests + // - 4: also display the setup trace for failed tests + // - 5..: display all traces for all tests + let should_include = match kind { + TraceKind::Execution => { + (verbosity == 3 && result.status.is_failure()) || verbosity >= 4 } - - if self.gas_report { - gas_report.analyze(&result.traces); + TraceKind::Setup => { + (verbosity == 4 && result.status.is_failure()) || verbosity >= 5 } + TraceKind::Deployment => false, + }; + + // Decode the trace if we either need to build a gas report or we need to print it + if should_include || gas_reporting { + decoder.decode(trace).await; + } + + if should_include { + decoded_traces.push(trace.to_string()); } } - let block_outcome = - TestOutcome::new([(contract_name, suite_result)].into(), self.allow_failure); - total_passed += block_outcome.successes().count(); - total_failed += block_outcome.failures().count(); - total_skipped += block_outcome.skips().count(); + if !decoded_traces.is_empty() { + println!("Traces:"); + decoded_traces.into_iter().for_each(|trace| println!("{trace}")); + } - println!("{}", block_outcome.summary()); + if gas_reporting { + gas_report.analyze(&result.traces); + } } + let block_outcome = TestOutcome::new([(contract_name, suite_result)].into(), allow_failure); - if self.gas_report { - println!("{}", gas_report.finalize()); - } + total_passed += block_outcome.successes().count(); + total_failed += block_outcome.failures().count(); + total_skipped += block_outcome.skips().count(); - let num_test_suites = results.len(); + println!("{}", block_outcome.summary()); + } - if num_test_suites > 0 { - println!( - "{}", - format_aggregated_summary( - num_test_suites, - total_passed, - total_failed, - total_skipped - ) - ); - } + if gas_reporting { + println!("{}", gas_report.finalize()); + } - // reattach the thread - let _results = handle.await?; + let num_test_suites = results.len(); - trace!(target: "forge::test", "received {} results", results.len()); - Ok(TestOutcome::new(results, self.allow_failure)) + if num_test_suites > 0 { + println!( + "{}", + format_aggregated_summary(num_test_suites, total_passed, total_failed, total_skipped) + ); } + + // reattach the thread + let _results = handle.await?; + + trace!(target: "forge::test", "received {} results", results.len()); + Ok(TestOutcome::new(results, allow_failure)) } diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index c06ec53e0c217..bd80de843c6c0 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -5,6 +5,7 @@ use ethers::prelude::Log; use foundry_common::evm::Breakpoints; use foundry_evm::{ coverage::HitMaps, + debug::DebugArena, executor::EvmError, fuzz::{types::FuzzCase, CounterExample}, trace::{TraceKind, Traces}, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 41e62f8703369..87cf796eb3d5e 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -47,7 +47,6 @@ pub struct ContractRunner<'a> { pub contract: &'a Abi, /// All known errors, used to decode reverts pub errors: Option<&'a Abi>, - /// The initial balance of the test contract pub initial_balance: U256, /// The address which will be used as the `from` field in all EVM calls @@ -246,20 +245,18 @@ impl<'a> ContractRunner<'a> { ) } - let mut test_results = self - .contract - .functions + let functions: Vec<_> = self.contract.functions().collect(); + let mut test_results = functions .par_iter() - .flat_map(|(_, f)| f) - .filter(|&func| func.is_test() && filter.matches_test(func.signature())) - .map(|func| { + .filter(|&&func| func.is_test() && filter.matches_test(func.signature())) + .map(|&func| { let should_fail = func.is_test_fail(); let res = if func.is_fuzz_test() { let runner = test_options.fuzz_runner(self.name, &func.name); let fuzz_config = test_options.fuzz_config(self.name, &func.name); self.run_fuzz_test(func, should_fail, runner, setup.clone(), *fuzz_config) } else { - self.clone().run_test(func, should_fail, setup.clone()) + self.run_test(func, should_fail, setup.clone()) }; (func.signature(), res) }) @@ -309,72 +306,78 @@ impl<'a> ContractRunner<'a> { /// State modifications are not committed to the evm database but discarded after the call, /// similar to `eth_call`. #[instrument(name = "test", skip_all, fields(name = %func.signature(), %should_fail))] - pub fn run_test(mut self, func: &Function, should_fail: bool, setup: TestSetup) -> TestResult { + pub fn run_test(&self, func: &Function, should_fail: bool, setup: TestSetup) -> TestResult { let TestSetup { address, mut logs, mut traces, mut labeled_addresses, .. } = setup; // Run unit test + let mut executor = self.executor.clone(); let start = Instant::now(); let mut debug_arena = None; - let (reverted, reason, gas, stipend, coverage, state_changeset, breakpoints) = match self - .executor - .execute_test::<(), _, _>(self.sender, address, func.clone(), (), 0.into(), self.errors) - { - Ok(CallResult { - reverted, - gas_used: gas, - stipend, - logs: execution_logs, - traces: execution_trace, - coverage, - labels: new_labels, - state_changeset, - debug, - breakpoints, - .. - }) => { - traces.extend(execution_trace.map(|traces| (TraceKind::Execution, traces))); - labeled_addresses.extend(new_labels); - logs.extend(execution_logs); - debug_arena = debug; - (reverted, None, gas, stipend, coverage, state_changeset, breakpoints) - } - Err(EvmError::Execution(err)) => { - traces.extend(err.traces.map(|traces| (TraceKind::Execution, traces))); - labeled_addresses.extend(err.labels); - logs.extend(err.logs); - ( - err.reverted, - Some(err.reason), - err.gas_used, - err.stipend, - None, - err.state_changeset, - HashMap::new(), - ) - } - Err(EvmError::SkipError) => { - return TestResult { - status: TestStatus::Skipped, - reason: None, - decoded_logs: decode_console_logs(&logs), - traces, - labeled_addresses, - kind: TestKind::Standard(0), - ..Default::default() + let (reverted, reason, gas, stipend, coverage, state_changeset, breakpoints) = + match executor.execute_test::<(), _, _>( + self.sender, + address, + func.clone(), + (), + 0.into(), + self.errors, + ) { + Ok(CallResult { + reverted, + gas_used: gas, + stipend, + logs: execution_logs, + traces: execution_trace, + coverage, + labels: new_labels, + state_changeset, + debug, + breakpoints, + .. + }) => { + traces.extend(execution_trace.map(|traces| (TraceKind::Execution, traces))); + labeled_addresses.extend(new_labels); + logs.extend(execution_logs); + debug_arena = debug; + (reverted, None, gas, stipend, coverage, state_changeset, breakpoints) } - } - Err(err) => { - return TestResult { - status: TestStatus::Failure, - reason: Some(err.to_string()), - decoded_logs: decode_console_logs(&logs), - traces, - labeled_addresses, - kind: TestKind::Standard(0), - ..Default::default() + Err(EvmError::Execution(err)) => { + traces.extend(err.traces.map(|traces| (TraceKind::Execution, traces))); + labeled_addresses.extend(err.labels); + logs.extend(err.logs); + ( + err.reverted, + Some(err.reason), + err.gas_used, + err.stipend, + None, + err.state_changeset, + HashMap::new(), + ) } - } - }; + Err(EvmError::SkipError) => { + return TestResult { + status: TestStatus::Skipped, + reason: None, + decoded_logs: decode_console_logs(&logs), + traces, + labeled_addresses, + kind: TestKind::Standard(0), + ..Default::default() + } + } + Err(err) => { + return TestResult { + status: TestStatus::Failure, + reason: Some(err.to_string()), + decoded_logs: decode_console_logs(&logs), + traces, + labeled_addresses, + kind: TestKind::Standard(0), + ..Default::default() + } + } + }; let success = executor.is_success( setup.address, @@ -537,6 +540,8 @@ impl<'a> ContractRunner<'a> { traces, labeled_addresses: labeled_addresses.clone(), breakpoints: Default::default(), + // TODO + debug: Default::default(), } } @@ -553,13 +558,10 @@ impl<'a> ContractRunner<'a> { // Run fuzz test let start = Instant::now(); - let mut result = FuzzedExecutor::new( - &self.executor, - runner.clone(), - self.sender, - fuzz_config, - ) - .fuzz(func, address, should_fail, self.errors); + let fuzzed_executor = + FuzzedExecutor::new(&self.executor, runner.clone(), self.sender, fuzz_config); + let state = fuzzed_executor.build_fuzz_state(); + let mut result = fuzzed_executor.fuzz(func, address, should_fail, self.errors); let mut debug = Default::default(); let mut breakpoints = Default::default(); @@ -601,7 +603,7 @@ impl<'a> ContractRunner<'a> { self.sender, fuzz_config, ) - .single_fuzz(&result.state, address, should_fail, calldata); + .single_fuzz(&state, address, should_fail, calldata); (debug, breakpoints) = match debug_result { Ok(fuzz_outcome) => match fuzz_outcome { diff --git a/crates/ui/src/debugger.rs b/crates/ui/src/debugger.rs index b0489b1ba62a4..071765e3ca15e 100644 --- a/crates/ui/src/debugger.rs +++ b/crates/ui/src/debugger.rs @@ -1,3 +1,4 @@ +use crate::Ui; use ethers::solc::artifacts::ContractBytecodeSome; use foundry_common::{evm::Breakpoints, get_contract_name}; use foundry_evm::{debug::DebugArena, trace::CallTraceDecoder}; diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index 9cd23d598d143..9fa5482219948 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -50,8 +50,8 @@ pub enum TUIExitReason { mod op_effects; use op_effects::stack_indices_affected; -use self::debugger::ContractSources; mod debugger; +pub use debugger::*; pub struct Tui { debug_arena: Vec<(Address, Vec, CallKind)>, From f41cf33ac5b4d043d08370c1458762839b13d281 Mon Sep 17 00:00:00 2001 From: franfran Date: Thu, 31 Aug 2023 00:34:48 +0300 Subject: [PATCH 09/17] fmt --- crates/forge/bin/cmd/script/build.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index ac71d6b6507c5..56fa7d1d15314 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -1,8 +1,7 @@ use super::*; use ethers::{ prelude::{ - artifacts::Libraries, cache::SolFilesCache, ArtifactId, Graph, Project, - ProjectCompileOutput, + artifacts::Libraries, cache::SolFilesCache, ArtifactId, Project, ProjectCompileOutput, }, solc::{ artifacts::{CompactContractBytecode, ContractBytecode, ContractBytecodeSome}, From 00582010d5fcbb460f061a4a25327dcaaae48186 Mon Sep 17 00:00:00 2001 From: franfran Date: Thu, 31 Aug 2023 01:08:24 +0300 Subject: [PATCH 10/17] remove TODO --- crates/ui/src/lib.rs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index e33893ed11518..dbc07120d3d78 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -671,19 +671,11 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k ])); line_number += 1; }); - // TODO: if file_id not here - // } else { - // text_output.extend(Text::from("No srcmap index")); - // } - // TODO: if source_element don't match - // Err(e) => text_output.extend(Text::from(format!( - // "Error in source map parsing: '{e}', please open an issue" - // ))), } else { text_output.extend(Text::from("No sourcemap for contract")); } } else { - text_output.extend(Text::from(format!("Unknown contract at address {address:?}"))); + text_output.extend(Text::from("No srcmap index for contract {contract_name}")); } } else { text_output.extend(Text::from(format!("Unknown contract at address {address:?}"))); From 466d24a4be210ff3be7b7eedb8e62c76e4630d6e Mon Sep 17 00:00:00 2001 From: franfran Date: Thu, 31 Aug 2023 01:20:10 +0300 Subject: [PATCH 11/17] minimal diff --- crates/forge/bin/cmd/script/cmd.rs | 2 +- crates/forge/bin/cmd/test/mod.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 144addf4630b1..7e4f77370ceb0 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -15,7 +15,7 @@ type NewSenderChanges = (CallTraceDecoder, Libraries, ArtifactContracts eyre::Result<()> { + pub async fn run_script(mut self) -> Result<()> { trace!(target: "script", "executing script command"); let (config, evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 95e135f0b9ea2..f15be1153b041 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -230,7 +230,7 @@ impl TestArgs { let remote_chain_id = runner.evm_opts.get_remote_chain_id(); let outcome = self - .run_tests(runner, &config, verbosity, filter.clone(), test_options.clone()) + .run_tests(runner, config.clone(), verbosity, filter.clone(), test_options.clone()) .await?; let tests = outcome.clone().into_tests(); @@ -328,7 +328,7 @@ impl TestArgs { pub async fn run_tests( &self, mut runner: MultiContractRunner, - config: &Config, + config: Config, verbosity: u8, mut filter: ProjectPathsAwareFilter, test_options: TestOptions, @@ -343,7 +343,7 @@ impl TestArgs { list(runner, filter, self.json) } else { test( - config.clone(), + config, runner, verbosity, filter, From 7b71916e0843bcf3eb86e1682800e9504ce7720f Mon Sep 17 00:00:00 2001 From: franfran Date: Sat, 2 Sep 2023 12:58:13 +0300 Subject: [PATCH 12/17] apply review suggestions --- crates/common/src/compile.rs | 12 +++++++++--- crates/common/src/contracts.rs | 17 +++++++++++------ crates/evm/src/fuzz/mod.rs | 2 +- crates/evm/src/trace/identifier/etherscan.rs | 9 ++------- crates/forge/bin/cmd/script/build.rs | 13 ++++++++----- crates/forge/bin/cmd/script/cmd.rs | 2 +- crates/forge/bin/cmd/test/mod.rs | 6 +++--- crates/ui/src/debugger.rs | 7 +------ crates/ui/src/lib.rs | 2 +- 9 files changed, 37 insertions(+), 33 deletions(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index c50da3b76b08a..3c51ca544164f 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -11,7 +11,7 @@ use ethers_solc::{ }; use eyre::Result; use std::{ - collections::BTreeMap, + collections::{BTreeMap, HashMap}, convert::Infallible, fmt::Display, path::{Path, PathBuf}, @@ -398,10 +398,16 @@ pub fn compile_target_with_filter( } } +/// Id to map from the bytecode to the source file +pub type FileId = u32; + +/// Map over artifcats contract sources name -> file_id -> (source, contract) +pub type ContractSources = HashMap>; + /// Creates and compiles a project from an Etherscan source. pub async fn compile_from_source( metadata: &Metadata, -) -> Result<(ArtifactId, u32, ContractBytecodeSome)> { +) -> Result<(ArtifactId, FileId, ContractBytecodeSome)> { let root = tempfile::tempdir()?; let root_path = root.path(); let project = etherscan_project(metadata, root_path)?; @@ -419,7 +425,7 @@ pub async fn compile_from_source( (aid, art.source_file().expect("no source file").id, art.into_contract_bytecode()) }) .expect("there should be a contract with bytecode"); - let bytecode = compact_to_contract(contract); + let bytecode = compact_to_contract(contract)?; root.close()?; diff --git a/crates/common/src/contracts.rs b/crates/common/src/contracts.rs index faff763282806..c819c41aefc3f 100644 --- a/crates/common/src/contracts.rs +++ b/crates/common/src/contracts.rs @@ -270,10 +270,15 @@ mod tests { } /// Helper function to convert CompactContractBytecode ~> ContractBytecodeSome -pub fn compact_to_contract(contract: CompactContractBytecode) -> ContractBytecodeSome { - ContractBytecodeSome { - abi: contract.abi.unwrap(), - bytecode: contract.bytecode.unwrap().into(), - deployed_bytecode: contract.deployed_bytecode.unwrap().into(), - } +pub fn compact_to_contract( + contract: CompactContractBytecode, +) -> eyre::Result { + Ok(ContractBytecodeSome { + abi: contract.abi.ok_or(eyre::eyre!("No contract abi"))?, + bytecode: contract.bytecode.ok_or(eyre::eyre!("No contract bytecode"))?.into(), + deployed_bytecode: contract + .deployed_bytecode + .ok_or(eyre::eyre!("No contract deployed bytecode"))? + .into(), + }) } diff --git a/crates/evm/src/fuzz/mod.rs b/crates/evm/src/fuzz/mod.rs index 573fc5a9dda6c..f704a0696f88c 100644 --- a/crates/evm/src/fuzz/mod.rs +++ b/crates/evm/src/fuzz/mod.rs @@ -81,7 +81,6 @@ impl<'a> FuzzedExecutor<'a> { // Stores coverage information for all fuzz cases let coverage: RefCell> = RefCell::default(); - // Stores fuzz state for use with [fuzz_calldata_from_state] let state = self.build_fuzz_state(); let mut weights = vec![]; @@ -245,6 +244,7 @@ impl<'a> FuzzedExecutor<'a> { } } + /// Stores fuzz state for use with [fuzz_calldata_from_state] pub fn build_fuzz_state(&self) -> EvmFuzzState { if let Some(fork_db) = self.executor.backend.active_fork_db() { build_initial_state(fork_db, &self.config.dictionary) diff --git a/crates/evm/src/trace/identifier/etherscan.rs b/crates/evm/src/trace/identifier/etherscan.rs index b56ba70ef94a6..af38b7bd64edc 100644 --- a/crates/evm/src/trace/identifier/etherscan.rs +++ b/crates/evm/src/trace/identifier/etherscan.rs @@ -7,7 +7,7 @@ use ethers::{ prelude::{artifacts::ContractBytecodeSome, errors::EtherscanError, ArtifactId}, types::H160, }; -use foundry_common::compile; +use foundry_common::compile::{self, ContractSources}; use foundry_config::{Chain, Config}; use futures::{ future::{join_all, Future}, @@ -60,12 +60,7 @@ impl EtherscanIdentifier { /// Etherscan and compiles them locally, for usage in the debugger. pub async fn get_compiled_contracts( &self, - ) -> eyre::Result<( - // TODO should use `ContractSources` but has circular import. - // Maybe move it lower - HashMap>, - BTreeMap, - )> { + ) -> eyre::Result<(ContractSources, BTreeMap)> { let mut compiled_contracts = BTreeMap::new(); let mut sources = HashMap::new(); diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index a4b3e68e80f22..2be8e698a8134 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -12,7 +12,10 @@ use ethers::{ }; use eyre::{Context, ContextCompat, Result}; use foundry_cli::utils::get_cached_entry_by_name; -use foundry_common::{compact_to_contract, compile}; +use foundry_common::{ + compact_to_contract, + compile::{self, FileId}, +}; use foundry_utils::{PostLinkInput, ResolvedDependency}; use std::{collections::BTreeMap, fs, str::FromStr}; use tracing::{trace, warn}; @@ -30,7 +33,7 @@ impl ScriptArgs { let (project, output) = self.get_project_and_output(script_config)?; let output = output.with_stripped_file_prefixes(project.root()); - let mut sources: HashMap> = + let mut sources: HashMap> = HashMap::new(); let contracts = output @@ -44,9 +47,9 @@ impl ScriptArgs { .ast .ok_or(eyre::eyre!("Source from artifact has no AST."))? .absolute_path; - let source_code = fs::read_to_string(abs_path).unwrap(); + let source_code = fs::read_to_string(abs_path)?; let contract = artifact.clone().into_contract_bytecode(); - let source_contract = compact_to_contract(contract); + let source_contract = compact_to_contract(contract)?; inner_map.insert(source.id, (source_code, source_contract)); } else { warn!("source not found for artifact={:?}", id); @@ -280,5 +283,5 @@ pub struct BuildOutput { pub highlevel_known_contracts: ArtifactContracts, pub libraries: Libraries, pub predeploy_libraries: Vec, - pub sources: HashMap>, + pub sources: HashMap>, } diff --git a/crates/forge/bin/cmd/script/cmd.rs b/crates/forge/bin/cmd/script/cmd.rs index 7e4f77370ceb0..8185e6e9e7e9d 100644 --- a/crates/forge/bin/cmd/script/cmd.rs +++ b/crates/forge/bin/cmd/script/cmd.rs @@ -86,7 +86,7 @@ impl ScriptArgs { if self.debug { let debugger = DebuggerArgs { - debug: result.debug.clone().unwrap_or(vec![]), + debug: result.debug.clone().unwrap_or_default(), decoder: &decoder, sources, breakpoints: result.breakpoints.clone(), diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index f15be1153b041..80fae8ff6e2d5 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -296,7 +296,7 @@ impl TestArgs { if should_debug { let mut sources = HashMap::new(); - output.into_artifacts().for_each(|(id, artifact)| { + for (id, artifact) in output.into_artifacts() { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { @@ -304,10 +304,10 @@ impl TestArgs { let abs_path = source.ast.unwrap().absolute_path; let source_code = fs::read_to_string(abs_path).unwrap(); let contract = artifact.clone().into_contract_bytecode(); - let source_contract = compact_to_contract(contract); + let source_contract = compact_to_contract(contract)?; inner_map.insert(source.id, (source_code, source_contract)); } - }); + } let test = outcome.clone().into_tests().next().unwrap(); let result = test.result; diff --git a/crates/ui/src/debugger.rs b/crates/ui/src/debugger.rs index 4c490b5ddb7ac..9b293be5c1740 100644 --- a/crates/ui/src/debugger.rs +++ b/crates/ui/src/debugger.rs @@ -1,15 +1,10 @@ use crate::Ui; -use ethers::solc::artifacts::ContractBytecodeSome; -use foundry_common::{evm::Breakpoints, get_contract_name}; +use foundry_common::{compile::ContractSources, evm::Breakpoints, get_contract_name}; use foundry_evm::{debug::DebugArena, trace::CallTraceDecoder}; -use std::collections::HashMap; use tracing::trace; use crate::{TUIExitReason, Tui}; -/// Map over debugger contract sources name -> file_id -> (source, contract) -pub type ContractSources = HashMap>; - /// Standardized way of firing up the debugger pub struct DebuggerArgs<'a> { /// debug traces returned from the execution diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index dbc07120d3d78..f78ba82f8e54f 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -10,7 +10,7 @@ use crossterm::{ }; use ethers::types::Address; use eyre::Result; -use foundry_common::evm::Breakpoints; +use foundry_common::{compile::ContractSources, evm::Breakpoints}; use foundry_evm::{ debug::{DebugStep, Instruction}, utils::{build_pc_ic_map, PCICMap}, From d336bfe2cbbad9f019f25c2b258f807085aa52fd Mon Sep 17 00:00:00 2001 From: franfran Date: Sat, 2 Sep 2023 12:59:33 +0300 Subject: [PATCH 13/17] add TODO --- crates/forge/src/runner.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 87cf796eb3d5e..6e1fab6836869 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -540,8 +540,7 @@ impl<'a> ContractRunner<'a> { traces, labeled_addresses: labeled_addresses.clone(), breakpoints: Default::default(), - // TODO - debug: Default::default(), + debug: Default::default(), // TODO collect debug traces on the last run or error } } From c6a66fb33097b431626515f1257715e6c1d2555d Mon Sep 17 00:00:00 2001 From: franfran Date: Sat, 2 Sep 2023 13:08:05 +0300 Subject: [PATCH 14/17] looks better --- crates/forge/bin/cmd/script/build.rs | 7 +++---- crates/forge/bin/cmd/script/mod.rs | 1 + crates/forge/bin/cmd/script/runner.rs | 7 ++----- crates/forge/src/runner.rs | 6 ++---- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index 2be8e698a8134..f9af5e57346f5 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -14,7 +14,7 @@ use eyre::{Context, ContextCompat, Result}; use foundry_cli::utils::get_cached_entry_by_name; use foundry_common::{ compact_to_contract, - compile::{self, FileId}, + compile::{self, ContractSources}, }; use foundry_utils::{PostLinkInput, ResolvedDependency}; use std::{collections::BTreeMap, fs, str::FromStr}; @@ -33,8 +33,7 @@ impl ScriptArgs { let (project, output) = self.get_project_and_output(script_config)?; let output = output.with_stripped_file_prefixes(project.root()); - let mut sources: HashMap> = - HashMap::new(); + let mut sources: ContractSources = HashMap::new(); let contracts = output .into_artifacts() @@ -283,5 +282,5 @@ pub struct BuildOutput { pub highlevel_known_contracts: ArtifactContracts, pub libraries: Libraries, pub predeploy_libraries: Vec, - pub sources: HashMap>, + pub sources: ContractSources, } diff --git a/crates/forge/bin/cmd/script/mod.rs b/crates/forge/bin/cmd/script/mod.rs index 6515883edf083..519b35da2d570 100644 --- a/crates/forge/bin/cmd/script/mod.rs +++ b/crates/forge/bin/cmd/script/mod.rs @@ -605,6 +605,7 @@ impl Provider for ScriptArgs { } } +#[derive(Default)] pub struct ScriptResult { pub success: bool, pub logs: Vec, diff --git a/crates/forge/bin/cmd/script/runner.rs b/crates/forge/bin/cmd/script/runner.rs index aa9dd2c04b0f0..1781d9f9f5d57 100644 --- a/crates/forge/bin/cmd/script/runner.rs +++ b/crates/forge/bin/cmd/script/runner.rs @@ -167,7 +167,7 @@ impl ScriptRunner { debug, address: None, script_wallets, - breakpoints: Default::default(), + ..Default::default() }, )) } @@ -237,11 +237,8 @@ impl ScriptRunner { }) .unwrap_or_default(), debug: vec![debug].into_iter().collect(), - labeled_addresses: Default::default(), - transactions: Default::default(), address: Some(address), - script_wallets: vec![], - breakpoints: Default::default(), + ..Default::default() }) } else { eyre::bail!("ENS not supported."); diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index 6e1fab6836869..1d1bd5e13ded0 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -236,8 +236,7 @@ impl<'a> ContractRunner<'a> { traces: setup.traces, coverage: None, labeled_addresses: setup.labeled_addresses, - breakpoints: Default::default(), - debug: Default::default(), + ..Default::default() }, )] .into(), @@ -539,8 +538,7 @@ impl<'a> ContractRunner<'a> { coverage: None, // TODO ? traces, labeled_addresses: labeled_addresses.clone(), - breakpoints: Default::default(), - debug: Default::default(), // TODO collect debug traces on the last run or error + ..Default::default() // TODO collect debug traces on the last run or error } } From 17ddabcc1a61f9cee13c09806c01431a8182c6f7 Mon Sep 17 00:00:00 2001 From: franfran Date: Mon, 4 Sep 2023 01:32:09 +0200 Subject: [PATCH 15/17] make ContractSources wrapper --- crates/cli/src/utils/cmd.rs | 2 +- crates/common/src/compile.rs | 12 ++++------ crates/evm/src/trace/identifier/etherscan.rs | 23 +++++++++--------- crates/forge/bin/cmd/script/build.rs | 11 +++++---- crates/forge/bin/cmd/test/mod.rs | 25 ++++++++++---------- crates/ui/src/debugger.rs | 4 +--- crates/ui/src/lib.rs | 3 ++- 7 files changed, 40 insertions(+), 40 deletions(-) diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 396dccadd28d3..8e58fe8d9bc85 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -391,7 +391,7 @@ pub async fn handle_traces( } if debug { - let (sources, _compiled_contracts) = etherscan_identifier.get_compiled_contracts().await?; + let sources = etherscan_identifier.get_compiled_contracts().await?; let debugger = DebuggerArgs { debug: vec![result.debug], decoder: &decoder, diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index 3c51ca544164f..be9bdbb9f192e 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -171,6 +171,10 @@ impl ProjectCompiler { } } +/// Map over artifcats contract sources name -> file_id -> (source, contract) +#[derive(Default, Debug, Clone)] +pub struct ContractSources(pub HashMap>); + // https://eips.ethereum.org/EIPS/eip-170 const CONTRACT_SIZE_LIMIT: usize = 24576; @@ -398,16 +402,10 @@ pub fn compile_target_with_filter( } } -/// Id to map from the bytecode to the source file -pub type FileId = u32; - -/// Map over artifcats contract sources name -> file_id -> (source, contract) -pub type ContractSources = HashMap>; - /// Creates and compiles a project from an Etherscan source. pub async fn compile_from_source( metadata: &Metadata, -) -> Result<(ArtifactId, FileId, ContractBytecodeSome)> { +) -> Result<(ArtifactId, u32, ContractBytecodeSome)> { let root = tempfile::tempdir()?; let root_path = root.path(); let project = etherscan_project(metadata, root_path)?; diff --git a/crates/evm/src/trace/identifier/etherscan.rs b/crates/evm/src/trace/identifier/etherscan.rs index af38b7bd64edc..2658c032aac37 100644 --- a/crates/evm/src/trace/identifier/etherscan.rs +++ b/crates/evm/src/trace/identifier/etherscan.rs @@ -4,7 +4,7 @@ use ethers::{ abi::Address, etherscan, etherscan::contract::{ContractMetadata, Metadata}, - prelude::{artifacts::ContractBytecodeSome, errors::EtherscanError, ArtifactId}, + prelude::errors::EtherscanError, types::H160, }; use foundry_common::compile::{self, ContractSources}; @@ -17,7 +17,7 @@ use futures::{ }; use std::{ borrow::Cow, - collections::{BTreeMap, HashMap}, + collections::BTreeMap, pin::Pin, sync::{ atomic::{AtomicBool, Ordering}, @@ -58,12 +58,7 @@ impl EtherscanIdentifier { /// Goes over the list of contracts we have pulled from the traces, clones their source from /// Etherscan and compiles them locally, for usage in the debugger. - pub async fn get_compiled_contracts( - &self, - ) -> eyre::Result<(ContractSources, BTreeMap)> { - let mut compiled_contracts = BTreeMap::new(); - let mut sources = HashMap::new(); - + pub async fn get_compiled_contracts(&self) -> eyre::Result { // TODO: Add caching so we dont double-fetch contracts. let contracts_iter = self .contracts @@ -86,16 +81,20 @@ impl EtherscanIdentifier { // poll all the futures concurrently let artifacts = join_all(outputs_fut).await; + let mut sources: ContractSources = Default::default(); + // construct the map for (results, (_, metadata)) in artifacts.into_iter().zip(contracts_iter) { // get the inner type let (artifact_id, file_id, bytecode) = results?; - compiled_contracts.insert(artifact_id.clone(), bytecode.clone()); - let inner_map = sources.entry(artifact_id.clone().name).or_insert_with(HashMap::new); - inner_map.insert(file_id, (metadata.source_code(), bytecode)); + sources + .0 + .entry(artifact_id.clone().name) + .or_default() + .insert(file_id, (metadata.source_code(), bytecode)); } - Ok((sources, compiled_contracts)) + Ok(sources) } } diff --git a/crates/forge/bin/cmd/script/build.rs b/crates/forge/bin/cmd/script/build.rs index f9af5e57346f5..1ed67955aba11 100644 --- a/crates/forge/bin/cmd/script/build.rs +++ b/crates/forge/bin/cmd/script/build.rs @@ -33,7 +33,7 @@ impl ScriptArgs { let (project, output) = self.get_project_and_output(script_config)?; let output = output.with_stripped_file_prefixes(project.root()); - let mut sources: ContractSources = HashMap::new(); + let mut sources: ContractSources = Default::default(); let contracts = output .into_artifacts() @@ -41,7 +41,6 @@ impl ScriptArgs { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { - let inner_map = sources.entry(id.clone().name).or_default(); let abs_path = source .ast .ok_or(eyre::eyre!("Source from artifact has no AST."))? @@ -49,7 +48,11 @@ impl ScriptArgs { let source_code = fs::read_to_string(abs_path)?; let contract = artifact.clone().into_contract_bytecode(); let source_contract = compact_to_contract(contract)?; - inner_map.insert(source.id, (source_code, source_contract)); + sources + .0 + .entry(id.clone().name) + .or_default() + .insert(source.id, (source_code, source_contract)); } else { warn!("source not found for artifact={:?}", id); } @@ -199,7 +202,7 @@ impl ScriptArgs { known_contracts: contracts, highlevel_known_contracts: ArtifactContracts(highlevel_known_contracts), predeploy_libraries, - sources: HashMap::new(), + sources: Default::default(), project, libraries: new_libraries, }) diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 80fae8ff6e2d5..d4dde2e6ba9c9 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -19,7 +19,7 @@ use foundry_cli::{ }; use foundry_common::{ compact_to_contract, - compile::{self, ProjectCompiler}, + compile::{self, ContractSources, ProjectCompiler}, evm::EvmArgs, get_contract_name, get_file_name, shell, }; @@ -33,12 +33,7 @@ use foundry_config::{ }; use foundry_evm::{fuzz::CounterExample, utils::evm_spec}; use regex::Regex; -use std::{ - collections::{BTreeMap, HashMap}, - fs, - sync::mpsc::channel, - time::Duration, -}; +use std::{collections::BTreeMap, fs, sync::mpsc::channel, time::Duration}; use tracing::trace; use ui::DebuggerArgs; use watchexec::config::{InitConfig, RuntimeConfig}; @@ -295,17 +290,23 @@ impl TestArgs { } if should_debug { - let mut sources = HashMap::new(); + let mut sources: ContractSources = Default::default(); for (id, artifact) in output.into_artifacts() { // Sources are only required for the debugger, but it *might* mean that there's // something wrong with the build and/or artifacts. if let Some(source) = artifact.source_file() { - let inner_map = sources.entry(id.clone().name).or_insert_with(HashMap::new); - let abs_path = source.ast.unwrap().absolute_path; - let source_code = fs::read_to_string(abs_path).unwrap(); + let abs_path = source + .ast + .ok_or(eyre::eyre!("Source from artifact has no AST."))? + .absolute_path; + let source_code = fs::read_to_string(abs_path)?; let contract = artifact.clone().into_contract_bytecode(); let source_contract = compact_to_contract(contract)?; - inner_map.insert(source.id, (source_code, source_contract)); + sources + .0 + .entry(id.clone().name) + .or_default() + .insert(source.id, (source_code, source_contract)); } } diff --git a/crates/ui/src/debugger.rs b/crates/ui/src/debugger.rs index 9b293be5c1740..55ddcbd92a29e 100644 --- a/crates/ui/src/debugger.rs +++ b/crates/ui/src/debugger.rs @@ -34,13 +34,11 @@ impl DebuggerArgs<'_> { .map(|(addr, identifier)| (*addr, get_contract_name(identifier).to_string())) .collect(); - let contract_sources = self.sources.clone(); - let tui = Tui::new( flattened, 0, identified_contracts, - contract_sources, + self.sources.clone(), self.breakpoints.clone(), )?; diff --git a/crates/ui/src/lib.rs b/crates/ui/src/lib.rs index f78ba82f8e54f..7b25d2aa0d0b2 100644 --- a/crates/ui/src/lib.rs +++ b/crates/ui/src/lib.rs @@ -85,6 +85,7 @@ impl Tui { let mut terminal = Terminal::new(backend)?; terminal.hide_cursor(); let pc_ic_maps = contracts_sources + .0 .iter() .flat_map(|(contract_name, files_sources)| { files_sources.iter().filter_map(|(_, (_, contract))| { @@ -399,7 +400,7 @@ Line::from(Span::styled("[t]: stack labels | [m]: memory decoding | [shift + j/k let mut text_output: Text = Text::from(""); if let Some(contract_name) = identified_contracts.get(&address) { - if let Some(files_source_code) = contracts_sources.get(contract_name) { + if let Some(files_source_code) = contracts_sources.0.get(contract_name) { let pc_ic_map = pc_ic_maps.get(contract_name); // find the contract source with the correct source_element's file_id if let Some((source_element, source_code)) = files_source_code.iter().find_map( From 0aec987a5f577c0f6e1b4bdca70fe61109db470c Mon Sep 17 00:00:00 2001 From: franfran Date: Mon, 4 Sep 2023 18:00:55 +0200 Subject: [PATCH 16/17] add more docki docs --- crates/common/src/compile.rs | 2 +- crates/forge/bin/cmd/test/mod.rs | 1 + crates/forge/src/multi_runner.rs | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index be9bdbb9f192e..bb043850b575a 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -171,7 +171,7 @@ impl ProjectCompiler { } } -/// Map over artifcats contract sources name -> file_id -> (source, contract) +/// Map over artifacts contract sources name -> file_id -> (source, contract) #[derive(Default, Debug, Clone)] pub struct ContractSources(pub HashMap>); diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index d4dde2e6ba9c9..134f825fd64af 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -326,6 +326,7 @@ impl TestArgs { Ok(outcome) } + /// Run all tests that matches the filter predicate from a test runner pub async fn run_tests( &self, mut runner: MultiContractRunner, diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 4f4c9f2c14f7a..ad2c2846f413d 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -73,6 +73,7 @@ impl MultiContractRunner { .count() } + /// Get an iterator over all test functions that matches the filter path and contract name fn filtered_tests<'a>( &'a self, filter: &'a impl TestFilter, @@ -86,7 +87,7 @@ impl MultiContractRunner { .flat_map(|(_, (abi, _, _))| abi.functions()) } - // Get all tests of matching path and contract + /// Get all test names matching the filter pub fn get_tests(&self, filter: &impl TestFilter) -> Vec { self.filtered_tests(filter) .map(|func| func.name.clone()) @@ -94,6 +95,7 @@ impl MultiContractRunner { .collect() } + /// Returns all test functions matching the filter pub fn get_typed_tests<'a>(&'a self, filter: &'a impl TestFilter) -> Vec<&Function> { self.filtered_tests(filter).filter(|func| func.name.is_test()).collect() } From b513de33a604ea0b6a46102d8fa1a4cf8b83aa67 Mon Sep 17 00:00:00 2001 From: franfran Date: Mon, 4 Sep 2023 20:33:53 +0200 Subject: [PATCH 17/17] write file_id docs! --- crates/common/src/compile.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/common/src/compile.rs b/crates/common/src/compile.rs index bb043850b575a..bfe9ec9901a30 100644 --- a/crates/common/src/compile.rs +++ b/crates/common/src/compile.rs @@ -402,7 +402,8 @@ pub fn compile_target_with_filter( } } -/// Creates and compiles a project from an Etherscan source. +/// Compiles an Etherscan source from metadata by creating a project. +/// Returns the artifact_id, the file_id, and the bytecode pub async fn compile_from_source( metadata: &Metadata, ) -> Result<(ArtifactId, u32, ContractBytecodeSome)> {