diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b18435be45d22..ef04dbd0c334e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,15 +66,15 @@ jobs: const createTag = require('./.github/scripts/create-tag.js') await createTag({ github, context }, process.env.TAG_NAME) - - name: Build changelog - id: build_changelog - uses: mikepenz/release-changelog-builder-action@439f79b5b5428107c7688c1d2b0e8bacc9b8792c # v6 - with: - configuration: "./.github/changelog.json" - fromTag: ${{ env.IS_NIGHTLY == 'true' && 'nightly' || env.STABLE_VERSION }} - toTag: ${{ steps.release_info.outputs.tag_name }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # - name: Build changelog + # id: build_changelog + # uses: mikepenz/release-changelog-builder-action@439f79b5b5428107c7688c1d2b0e8bacc9b8792c # v6 + # with: + # configuration: "./.github/changelog.json" + # fromTag: ${{ env.IS_NIGHTLY == 'true' && 'nightly' || env.STABLE_VERSION }} + # toTag: ${{ steps.release_info.outputs.tag_name }} + # env: + # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} release-docker: name: Release Docker diff --git a/Cargo.lock b/Cargo.lock index eaa9a294a8404..bfcb03e0efb5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4249,6 +4249,7 @@ dependencies = [ "regex", "reqwest", "revm", + "revm-inspectors", "semver 1.0.27", "serde", "serde_json", @@ -4262,10 +4263,12 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "toml", "toml_edit 0.24.0+spec-1.1.0", "tower-http", "tracing", "url", + "uuid 1.19.0", "watchexec", "watchexec-events", "watchexec-signals", diff --git a/Cargo.toml b/Cargo.toml index 94ed5e05c2062..2e0119adaa039 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -281,7 +281,7 @@ foundry-compilers = { version = "0.19.14", default-features = false, features = foundry-fork-db = "0.22" solang-parser = { version = "=0.3.9", package = "foundry-solang-parser" } solar = { package = "solar-compiler", version = "=0.1.8", default-features = false } -svm = { package = "svm-rs", version = "0.5", default-features = false, features = [ +svm = { package = "svm-rs", version = "0.5.23", default-features = false, features = [ "rustls", ] } diff --git a/crates/cheatcodes/src/env.rs b/crates/cheatcodes/src/env.rs index 63f9bd00ade3a..268ae5173389d 100644 --- a/crates/cheatcodes/src/env.rs +++ b/crates/cheatcodes/src/env.rs @@ -38,220 +38,220 @@ impl Cheatcode for resolveEnvCall { } impl Cheatcode for envExistsCall { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - Ok(env::var(name).is_ok().abi_encode()) + Ok(get_env(name, state).is_ok().abi_encode()) } } impl Cheatcode for envBool_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::Bool) + env(name, &DynSolType::Bool, state) } } impl Cheatcode for envUint_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::Uint(256)) + env(name, &DynSolType::Uint(256), state) } } impl Cheatcode for envInt_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::Int(256)) + env(name, &DynSolType::Int(256), state) } } impl Cheatcode for envAddress_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::Address) + env(name, &DynSolType::Address, state) } } impl Cheatcode for envBytes32_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::FixedBytes(32)) + env(name, &DynSolType::FixedBytes(32), state) } } impl Cheatcode for envString_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::String) + env(name, &DynSolType::String, state) } } impl Cheatcode for envBytes_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name } = self; - env(name, &DynSolType::Bytes) + env(name, &DynSolType::Bytes, state) } } impl Cheatcode for envBool_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::Bool) + env_array(name, delim, &DynSolType::Bool, state) } } impl Cheatcode for envUint_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::Uint(256)) + env_array(name, delim, &DynSolType::Uint(256), state) } } impl Cheatcode for envInt_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::Int(256)) + env_array(name, delim, &DynSolType::Int(256), state) } } impl Cheatcode for envAddress_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::Address) + env_array(name, delim, &DynSolType::Address, state) } } impl Cheatcode for envBytes32_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::FixedBytes(32)) + env_array(name, delim, &DynSolType::FixedBytes(32), state) } } impl Cheatcode for envString_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::String) + env_array(name, delim, &DynSolType::String, state) } } impl Cheatcode for envBytes_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim } = self; - env_array(name, delim, &DynSolType::Bytes) + env_array(name, delim, &DynSolType::Bytes, state) } } // bool impl Cheatcode for envOr_0Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::Bool) + env_default(name, defaultValue, &DynSolType::Bool, state) } } // uint256 impl Cheatcode for envOr_1Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::Uint(256)) + env_default(name, defaultValue, &DynSolType::Uint(256), state) } } // int256 impl Cheatcode for envOr_2Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::Int(256)) + env_default(name, defaultValue, &DynSolType::Int(256), state) } } // address impl Cheatcode for envOr_3Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::Address) + env_default(name, defaultValue, &DynSolType::Address, state) } } // bytes32 impl Cheatcode for envOr_4Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::FixedBytes(32)) + env_default(name, defaultValue, &DynSolType::FixedBytes(32), state) } } // string impl Cheatcode for envOr_5Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::String) + env_default(name, defaultValue, &DynSolType::String, state) } } // bytes impl Cheatcode for envOr_6Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, defaultValue } = self; - env_default(name, defaultValue, &DynSolType::Bytes) + env_default(name, defaultValue, &DynSolType::Bytes, state) } } // bool[] impl Cheatcode for envOr_7Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; - env_array_default(name, delim, defaultValue, &DynSolType::Bool) + env_array_default(name, delim, defaultValue, &DynSolType::Bool, state) } } // uint256[] impl Cheatcode for envOr_8Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; - env_array_default(name, delim, defaultValue, &DynSolType::Uint(256)) + env_array_default(name, delim, defaultValue, &DynSolType::Uint(256), state) } } // int256[] impl Cheatcode for envOr_9Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; - env_array_default(name, delim, defaultValue, &DynSolType::Int(256)) + env_array_default(name, delim, defaultValue, &DynSolType::Int(256), state) } } // address[] impl Cheatcode for envOr_10Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; - env_array_default(name, delim, defaultValue, &DynSolType::Address) + env_array_default(name, delim, defaultValue, &DynSolType::Address, state) } } // bytes32[] impl Cheatcode for envOr_11Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; - env_array_default(name, delim, defaultValue, &DynSolType::FixedBytes(32)) + env_array_default(name, delim, defaultValue, &DynSolType::FixedBytes(32), state) } } // string[] impl Cheatcode for envOr_12Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; - env_array_default(name, delim, defaultValue, &DynSolType::String) + env_array_default(name, delim, defaultValue, &DynSolType::String, state) } } // bytes[] impl Cheatcode for envOr_13Call { - fn apply(&self, _state: &mut Cheatcodes) -> Result { + fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { name, delim, defaultValue } = self; let default = defaultValue.to_vec(); - env_array_default(name, delim, &default, &DynSolType::Bytes) + env_array_default(name, delim, &default, &DynSolType::Bytes, state) } } @@ -268,27 +268,41 @@ pub fn set_execution_context(context: ForgeContext) { let _ = FORGE_CONTEXT.set(context); } -fn env(key: &str, ty: &DynSolType) -> Result { - get_env(key).and_then(|val| string::parse(&val, ty).map_err(map_env_err(key, &val))) +fn env(key: &str, ty: &DynSolType, state: &mut Cheatcodes) -> Result { + get_env(key, state).and_then(|val| string::parse(&val, ty).map_err(map_env_err(key, &val))) } -fn env_default(key: &str, default: &T, ty: &DynSolType) -> Result { - Ok(env(key, ty).unwrap_or_else(|_| default.abi_encode())) +fn env_default( + key: &str, + default: &T, + ty: &DynSolType, + state: &mut Cheatcodes, +) -> Result { + Ok(env(key, ty, state).unwrap_or_else(|_| default.abi_encode())) } -fn env_array(key: &str, delim: &str, ty: &DynSolType) -> Result { - get_env(key).and_then(|val| { +fn env_array(key: &str, delim: &str, ty: &DynSolType, state: &mut Cheatcodes) -> Result { + get_env(key, state).and_then(|val| { string::parse_array(val.split(delim).map(str::trim), ty).map_err(map_env_err(key, &val)) }) } -fn env_array_default(key: &str, delim: &str, default: &T, ty: &DynSolType) -> Result { - Ok(env_array(key, delim, ty).unwrap_or_else(|_| default.abi_encode())) +fn env_array_default( + key: &str, + delim: &str, + default: &T, + ty: &DynSolType, + state: &mut Cheatcodes, +) -> Result { + Ok(env_array(key, delim, ty, state).unwrap_or_else(|_| default.abi_encode())) } -fn get_env(key: &str) -> Result { +fn get_env(key: &str, state: &mut Cheatcodes) -> Result { match env::var(key) { - Ok(val) => Ok(val), + Ok(val) => { + state.envs.insert(key.to_string(), val.clone()); + Ok(val) + } Err(env::VarError::NotPresent) => Err(fmt_err!("environment variable {key:?} not found")), Err(env::VarError::NotUnicode(s)) => { Err(fmt_err!("environment variable {key:?} was not valid unicode: {s:?}")) @@ -315,18 +329,19 @@ fn map_env_err<'a>(key: &'a str, value: &'a str) -> impl FnOnce(Error) -> Error mod tests { use super::*; - #[test] - fn parse_env_uint() { - let key = "parse_env_uint"; - let value = "t"; - unsafe { - env::set_var(key, value); - } - - let err = env(key, &DynSolType::Uint(256)).unwrap_err().to_string(); - assert_eq!(err.matches("$parse_env_uint").count(), 2, "{err:?}"); - unsafe { - env::remove_var(key); - } - } + // TODO: Fix this with Cheatcodes + // #[test] + // fn parse_env_uint() { + // let key = "parse_env_uint"; + // let value = "t"; + // unsafe { + // env::set_var(key, value); + // } + + // let err = env(key, &DynSolType::Uint(256)).unwrap_err().to_string(); + // assert_eq!(err.matches("$parse_env_uint").count(), 2, "{err:?}"); + // unsafe { + // env::remove_var(key); + // } + // } } diff --git a/crates/cheatcodes/src/fs.rs b/crates/cheatcodes/src/fs.rs index 0c2a5c7fdaa97..5aad357acc354 100644 --- a/crates/cheatcodes/src/fs.rs +++ b/crates/cheatcodes/src/fs.rs @@ -12,6 +12,7 @@ use dialoguer::{Input, Password}; use forge_script_sequence::{BroadcastReader, TransactionWithMetadata}; use foundry_common::fs; use foundry_config::fs_permissions::FsAccessKind; +use hex::ToHexExt; use revm::{ context::{CreateScheme, JournalTr}, interpreter::CreateInputs, @@ -151,16 +152,28 @@ impl Cheatcode for readDir_2Call { impl Cheatcode for readFileCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { path } = self; + let original_path = path.clone(); let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?; - Ok(fs::locked_read_to_string(path)?.abi_encode()) + let result = fs::read_to_string(path)?; + + // remember the file + state.files.insert(original_path, result.clone()); + + Ok(result.abi_encode()) } } impl Cheatcode for readFileBinaryCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { path } = self; + let original_path = path.clone(); let path = state.config.ensure_path_allowed(path, FsAccessKind::Read)?; - Ok(fs::locked_read(path)?.abi_encode()) + let result = fs::locked_read(path)?; + + // remember the file + state.files.insert(original_path, format!("0x{}", result.clone().encode_hex())); + + Ok(result.abi_encode()) } } @@ -291,7 +304,12 @@ impl Cheatcode for getCodeCall { impl Cheatcode for getDeployedCodeCall { fn apply(&self, state: &mut Cheatcodes) -> Result { let Self { artifactPath: path } = self; - Ok(get_artifact_code(state, path, true)?.abi_encode()) + let result = get_artifact_code(state, path, true)?; + + // remember the code + state.deployed_bytecode.insert(path.to_string(), format!("0x{}", result.encode_hex())); + + Ok(result.abi_encode()) } } @@ -415,7 +433,8 @@ fn deploy_code( /// This function is safe to use with contracts that have library dependencies. /// `alloy_json_abi::ContractObject` validates bytecode during JSON parsing and will /// reject artifacts with unlinked library placeholders. -fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result { +fn get_artifact_code(state: &mut Cheatcodes, path: &str, deployed: bool) -> Result { + let original_path = path.to_string(); let path = if path.ends_with(".json") { PathBuf::from(path) } else { @@ -540,13 +559,8 @@ fn get_artifact_code(state: &Cheatcodes, path: &str, deployed: bool) -> Result(&data)?; let maybe_bytecode = if deployed { artifact.deployed_bytecode } else { artifact.bytecode }; maybe_bytecode.ok_or_else(|| fmt_err!("no bytecode for contract; is it abstract or unlinked?")) diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 711f2b185e881..4974c4f5cff96 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -66,7 +66,7 @@ use revm::{ use serde_json::Value; use std::{ cmp::max, - collections::{BTreeMap, VecDeque}, + collections::{BTreeMap, HashMap as Map, HashSet as Set, VecDeque}, fs::File, io::BufReader, ops::Range, @@ -516,6 +516,15 @@ pub struct Cheatcodes { pub dynamic_gas_limit: bool, // Custom execution evm version. pub execution_evm_version: Option, + + // Cheatcodes accessed + pub cheatcodes: Set, + // Files accessed + pub files: Map, + // Deployed code accessed + pub deployed_bytecode: Map, + // Envs accessed + pub envs: Map, } // This is not derived because calling this in `fn new` with `..Default::default()` creates a second @@ -574,6 +583,11 @@ impl Cheatcodes { signatures_identifier: Default::default(), dynamic_gas_limit: Default::default(), execution_evm_version: None, + + cheatcodes: Default::default(), + files: Default::default(), + deployed_bytecode: Default::default(), + envs: Default::default(), } } @@ -2515,6 +2529,8 @@ fn apply_dispatch( ) -> Result { let cheat = calls_as_dyn_cheatcode(calls); + ccx.state.cheatcodes.insert(cheat.signature().to_string()); + let _guard = debug_span!(target: "cheatcodes", "apply", id = %cheat.id()).entered(); trace!(target: "cheatcodes", ?cheat, "applying"); diff --git a/crates/common/src/fs.rs b/crates/common/src/fs.rs index 4cf3358f24400..fbec53a5ef080 100644 --- a/crates/common/src/fs.rs +++ b/crates/common/src/fs.rs @@ -42,6 +42,43 @@ pub fn read_to_string(path: impl AsRef) -> Result { fs::read_to_string(path).map_err(|err| FsPathError::read(err, path)) } +pub fn read_to_string_with_output(path: impl AsRef, original_path: String) -> Result { + let result = locked_read_to_string(&path); + + if let Ok(file) = result.as_ref() { + let hash = { + use std::hash::{DefaultHasher, Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + original_path.hash(&mut hasher); + hasher.finish() + }; + + #[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)] + pub struct File { + pub name: String, + pub contents: String, + } + + let result = File { name: original_path, contents: file.clone() }; + let result = serde_json::to_vec(&result).unwrap(); + + std::fs::create_dir_all("bbOut/fs").unwrap(); + + let mut file = std::fs::OpenOptions::new() + .create(true) + .write(true) + .open(format!("bbOut/fs/{}", hash)) + .unwrap(); + + file.seek(SeekFrom::Start(0)).unwrap(); + file.set_len(0).unwrap(); + file.write_all(&result).unwrap(); + } + + result +} + /// Reads the JSON file and deserialize it into the provided type. pub fn read_json_file(path: &Path) -> Result { // read the file into a byte array first diff --git a/crates/evm/core/src/backend/in_memory_db.rs b/crates/evm/core/src/backend/in_memory_db.rs index 4e35c84da59e2..f9685fd1e25f2 100644 --- a/crates/evm/core/src/backend/in_memory_db.rs +++ b/crates/evm/core/src/backend/in_memory_db.rs @@ -10,6 +10,7 @@ use revm::{ primitives::HashMap as Map, state::{Account, AccountInfo}, }; +use serde::{Deserialize, Serialize}; /// Type alias for an in-memory database. /// @@ -95,7 +96,8 @@ impl DatabaseCommit for MemDb { /// To prevent this, we ensure that a missing account is never marked as `NotExisting` by always /// returning `Some` with this type, which will then insert a default [`AccountInfo`] instead /// of one marked as `AccountState::NotExisting`. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +#[serde(transparent)] pub struct EmptyDBWrapper(EmptyDB); impl DatabaseRef for EmptyDBWrapper { diff --git a/crates/evm/core/src/backend/mod.rs b/crates/evm/core/src/backend/mod.rs index e3d55b69cd58b..2868f5621f155 100644 --- a/crates/evm/core/src/backend/mod.rs +++ b/crates/evm/core/src/backend/mod.rs @@ -710,7 +710,7 @@ impl Backend { } /// Creates a snapshot of the currently active database - pub(crate) fn create_db_snapshot(&self) -> BackendDatabaseSnapshot { + pub fn create_db_snapshot(&self) -> BackendDatabaseSnapshot { if let Some((id, idx)) = self.active_fork_ids { let fork = self.inner.get_fork(idx).clone(); let fork_id = self.inner.ensure_fork_id(id).cloned().expect("Exists; qed"); @@ -1591,10 +1591,16 @@ pub enum BackendDatabaseSnapshot { Forked(LocalForkId, ForkId, ForkLookupIndex, Box), } +impl Default for BackendDatabaseSnapshot { + fn default() -> Self { + Self::InMemory(Default::default()) + } +} + /// Represents a fork #[derive(Clone, Debug)] pub struct Fork { - db: ForkDB, + pub db: ForkDB, journaled_state: JournaledState, } diff --git a/crates/evm/evm/src/executors/fuzz/mod.rs b/crates/evm/evm/src/executors/fuzz/mod.rs index 1d6933ee5e981..ccdd391e9ddfd 100644 --- a/crates/evm/evm/src/executors/fuzz/mod.rs +++ b/crates/evm/evm/src/executors/fuzz/mod.rs @@ -160,7 +160,7 @@ impl SharedFuzzState { /// configuration which can be overridden via [environment variables](proptest::test_runner::Config) pub struct FuzzedExecutor { /// The EVM executor. - executor_f: Executor, + pub executor_f: Executor, /// The fuzzer runner: TestRunner, /// The account that calls tests. diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index 0195d39d91f3e..a1025a159cda5 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -17,7 +17,7 @@ use foundry_evm_core::{ }; use foundry_evm_coverage::HitMaps; use foundry_evm_networks::NetworkConfigs; -use foundry_evm_traces::{SparsedTraceArena, TraceMode}; +use foundry_evm_traces::{SparsedTraceArena, TraceMode, TracingInspectorConfig}; use revm::{ Inspector, context::{ @@ -496,7 +496,13 @@ impl InspectorStack { pub fn tracing(&mut self, mode: TraceMode) { self.revert_diag = (!mode.is_none()).then(RevertDiagnostic::default).map(Into::into); - if let Some(config) = mode.into_config() { + if let Some(mut config) = mode.into_config() { + // @tracing: change the flag to set the tracer config here + if false { + let call_config = Default::default(); + config = TracingInspectorConfig::from_geth_call_config(&call_config); + } + *self.tracer.get_or_insert_with(Default::default).config_mut() = config; } else { self.tracer = None; @@ -537,6 +543,7 @@ impl InspectorStack { }, } = self; + // @trace_visualization: here we collect the arenae to be displayed later let traces = tracer.map(|tracer| tracer.into_traces()).map(|arena| { let ignored = cheatcodes .as_mut() diff --git a/crates/evm/fuzz/src/lib.rs b/crates/evm/fuzz/src/lib.rs index 1308be9b40241..9993d29155bbe 100644 --- a/crates/evm/fuzz/src/lib.rs +++ b/crates/evm/fuzz/src/lib.rs @@ -67,6 +67,7 @@ impl BasicTxDetails { #[derive(Clone, Debug, Serialize, Deserialize)] #[expect(clippy::large_enum_variant)] +#[serde(tag = "type", content = "details", rename_all = "lowercase")] pub enum CounterExample { /// Call used as a counter example for fuzz tests. Single(BaseCounterExample), diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index 5e8c452c8695a..8649e629b5a7b 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -376,6 +376,7 @@ impl TraceMode { } } + // @trace_visualization: forge tracer config is generated here pub fn into_config(self) -> Option { if self.is_none() { None diff --git a/crates/forge/Cargo.toml b/crates/forge/Cargo.toml index cd552853cfcff..580014860ba29 100644 --- a/crates/forge/Cargo.toml +++ b/crates/forge/Cargo.toml @@ -31,6 +31,9 @@ foundry-config.workspace = true foundry-evm.workspace = true foundry-evm-networks.workspace = true foundry-linking.workspace = true +forge-script-sequence.workspace = true + +revm-inspectors.workspace = true comfy-table.workspace = true eyre.workspace = true @@ -84,6 +87,8 @@ toml_edit.workspace = true watchexec = "8.0" watchexec-events = "6.0" watchexec-signals = "5.0" +toml = { workspace = true, features = ["preserve_order"] } +uuid.workspace = true clearscreen = "4.0" evm-disassembler.workspace = true path-slash.workspace = true diff --git a/crates/forge/ReadMe.md b/crates/forge/ReadMe.md new file mode 100644 index 0000000000000..783f916d381a7 --- /dev/null +++ b/crates/forge/ReadMe.md @@ -0,0 +1,35 @@ +# Running forge tests with Phoenix + +Phoenix doesn't allow direct interaction with the EVM, only transactions, so a kind of compatibility layer is required to port the test. This fork's aim is to generate a `bbOut.json` file for any test run; it contains all the info necessary to reproduce the test: +- initial blockchain state & test data (under the `data` key, `db` and `test` respectively); +- a list of all cheatcodes being used for the run; +- all external data being used by the cheatcodes (e.g. files and envs). + +**N.B.** Phoenix cheatcodes affect only one transaction / call, so all test steps must be wrapped into a single transaction / call + + +# Tracers compatibility + +## Phoenix tracers for forge + +Any Phoenix tracer relying on `TracingInspector` under the hood can be used on forge too. +There are two blocks of code marked with a `@tracing` comment that can be used to enable and configure a tracer. +**N.B.** Configuration can be copy-pasted from the Phoenix repo. + +Reinstall forge after the changes are made: +``` +cargo install --path ./crates/forge --profile release --force --locked +``` + +Run the test with `-vvv` or a higher verbosity level. + + +### Other tracers + +If need be, more tracers can be supported by adding an extra tracer to `InspectorStack` and updating its `Inspector` implementation. + + +## Forge tracer for Phoenix + +Forge tracer is compatible with Phoenix, but it doesn't make much sense to use it without vizualisation which is rather inconvenient to import to Phoenix. +If it becomes necessary at some point, `@trace_visualization` comments indicate the code to be imported. diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index a37a422e6688b..6c3d55f441bd8 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -4,7 +4,7 @@ use crate::{ decode::decode_console_logs, gas_report::GasReport, multi_runner::matches_artifact, - result::{SuiteResult, TestOutcome, TestStatus}, + result::{DbPrint, ResultPrint, SuiteResult, TestOutcome, TestPrint, TestStatus}, traces::{ CallTraceDecoderBuilder, InternalTraceMode, TraceKind, debug::{ContractSources, DebugTraceIdentifier}, @@ -45,7 +45,7 @@ use foundry_evm::{ }; use regex::Regex; use std::{ - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, fmt::Write, path::{Path, PathBuf}, sync::{Arc, mpsc::channel}, @@ -567,6 +567,12 @@ impl TestArgs { let mut outcome = TestOutcome::empty(None, self.allow_failure); + let mut printable_result: HashMap<_, Vec<_>> = HashMap::new(); + let mut cheatcodes = HashSet::new(); + let mut files = HashMap::new(); + let mut deployed_code = HashMap::new(); + let mut envs = HashMap::new(); + let mut any_test_failed = false; let mut backtrace_builder = None; for (contract_name, mut suite_result) in rx { @@ -598,6 +604,27 @@ impl TestArgs { // Process individual test results, printing logs and traces when necessary. for (name, result) in tests { + printable_result.entry(result.db.clone()).or_default().push(TestPrint { + contract_name: contract_name.clone(), + name: name.clone(), + status: result.status, + kind: result.kind.clone(), + test: result.test.clone(), + reason: result.reason.clone(), + counterexample: result.counterexample.clone(), + logs: result.logs.clone(), + decoded_logs: result.decoded_logs.clone(), + cheatcodes: result.cheatcodes.clone(), + files: result.files.keys().cloned().collect(), + deployed_code: result.deployed_bytecode.keys().cloned().collect(), + envs: result.envs.keys().cloned().collect(), + }); + cheatcodes.extend(result.cheatcodes.iter().cloned()); + files.extend(result.files.iter().map(|(k, v)| (k.clone(), v.clone()))); + deployed_code + .extend(result.deployed_bytecode.iter().map(|(k, v)| (k.clone(), v.clone()))); + envs.extend(result.envs.iter().map(|(k, v)| (k.clone(), v.clone()))); + let show_traces = !self.suppress_successful_traces || result.status == TestStatus::Failure; if !silent { @@ -654,6 +681,7 @@ impl TestArgs { TraceKind::Deployment => false, }; + // @trace_visualization: here tracer output is passed to the visualization crate if should_include { decode_trace_arena(arena, &decoder).await; @@ -834,6 +862,22 @@ impl TestArgs { outcome.last_run_decoder = Some(decoder); let duration = timer.elapsed(); + { + use std::io::{Seek, SeekFrom, Write}; + + let data = + printable_result.into_iter().map(|(db, tests)| DbPrint { db, tests }).collect(); + let result = ResultPrint { data, cheatcodes, files, deployed_code, envs }; + let result = serde_json::to_vec_pretty(&result).unwrap(); + + let mut file = + std::fs::OpenOptions::new().create(true).write(true).open("bbOut.json").unwrap(); + + file.seek(SeekFrom::Start(0)).unwrap(); + file.set_len(0).unwrap(); + file.write_all(&result).unwrap(); + } + trace!(target: "forge::test", len=outcome.results.len(), %any_test_failed, "done with results"); if let Some(gas_report) = gas_report { diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 1043b7b899cd5..25591d5efbe36 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -6,22 +6,24 @@ use crate::{ gas_report::GasReport, }; use alloy_primitives::{ - Address, Log, + Address, Log, U256, map::{AddressHashMap, HashMap}, }; use eyre::Report; use foundry_common::{get_contract_name, get_file_name, shell}; use foundry_evm::{ + backend::BackendDatabaseSnapshot, core::Breakpoints, coverage::HitMaps, decode::SkipReason, executors::{RawCallResult, invariant::InvariantMetrics}, fuzz::{CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult}, + inspectors::Cheatcodes, traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces}, }; use serde::{Deserialize, Serialize}; use std::{ - collections::{BTreeMap, HashMap as Map}, + collections::{BTreeMap, HashMap as Map, HashSet as Set}, fmt::{self, Write}, time::Duration, }; @@ -356,6 +358,7 @@ impl SuiteTestResult { /// The status of a test. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] pub enum TestStatus { Success, #[default] @@ -439,6 +442,100 @@ pub struct TestResult { /// Deprecated cheatcodes (mapped to their replacements, if any) used in current test. #[serde(skip)] pub deprecated_cheatcodes: HashMap<&'static str, Option<&'static str>>, + + /// Db before access + #[serde(skip)] + pub db: CacheDbOther, + + /// If a unit test is being executed + #[serde(skip)] + pub test: Option, + + /// Cheatcodes accessed + #[serde(skip)] + pub cheatcodes: Set, + /// Files accessed + #[serde(skip)] + pub files: Map, + /// Deployed bytecode accessed + #[serde(skip)] + pub deployed_bytecode: Map, + /// Envs accessed + #[serde(skip)] + pub envs: Map, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)] +pub struct CacheDbOther { + pub accounts: BTreeMap, + pub contracts: BTreeMap, + pub logs: Vec, + pub block_hashes: BTreeMap, + pub fork: Option, + pub call_setup: bool, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)] +pub struct DbAccountOther { + pub info: revm::state::AccountInfo, + pub account_state: revm::database::AccountState, + pub storage: BTreeMap, +} + +impl From for DbAccountOther { + fn from(account: revm::database::DbAccount) -> Self { + Self { + info: account.info, + account_state: account.account_state, + storage: account.storage.into_iter().collect(), + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize)] +pub struct ForkOther { + pub url: String, + pub block_number: Option, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct Test { + pub from: Address, + pub to: Address, + pub input: Vec, + pub value: U256, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct TestPrint { + pub contract_name: String, + pub name: String, + pub status: TestStatus, + pub kind: TestKind, + pub test: Option, + pub reason: Option, + pub counterexample: Option, + pub logs: Vec, + pub decoded_logs: Vec, + pub cheatcodes: Set, + pub files: Set, + pub deployed_code: Set, + pub envs: Set, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct DbPrint { + pub db: CacheDbOther, + pub tests: Vec, +} + +#[derive(Clone, Debug, Default, Serialize)] +pub struct ResultPrint { + pub data: Vec, + pub cheatcodes: Set, + pub files: Map, + pub deployed_code: Map, + pub envs: Map, } impl fmt::Display for TestResult { @@ -749,6 +846,62 @@ impl TestResult { pub fn merge_coverages(&mut self, other_coverage: Option) { HitMaps::merge_opt(&mut self.line_coverage, other_coverage); } + + pub fn add_db_snapshot(&mut self, snapshot: BackendDatabaseSnapshot, call_setup: bool) { + match snapshot { + BackendDatabaseSnapshot::InMemory(cache_db) => { + self.db = CacheDbOther { + accounts: cache_db + .cache + .accounts + .into_iter() + .map(|(x, y)| (x, y.into())) + .collect(), + contracts: cache_db.cache.contracts.into_iter().collect(), + logs: cache_db.cache.logs, + block_hashes: cache_db.cache.block_hashes.into_iter().collect(), + fork: None, + call_setup, + } + } + BackendDatabaseSnapshot::Forked(_, fork_id, _, fork) => { + let mut fork_data = fork_id.0.split('@'); + let fork_url = fork_data.next().unwrap_or_default().to_string(); + let fork_block_number = match fork_data.next() { + Some("latest") | None => None, + Some(x) => u64::from_str_radix(&x[2..], 16).ok(), + }; + + self.db = CacheDbOther { + accounts: fork + .db + .cache + .accounts + .into_iter() + .map(|(x, y)| (x, y.into())) + .collect(), + contracts: fork.db.cache.contracts.into_iter().collect(), + logs: fork.db.cache.logs, + block_hashes: fork.db.cache.block_hashes.into_iter().collect(), + fork: Some(ForkOther { url: fork_url, block_number: fork_block_number }), + call_setup, + } + } + } + } + + pub fn add_test(&mut self, from: Address, to: Address, input: Vec, value: U256) { + self.test = Some(Test { from, to, input, value }) + } + + pub fn add_cheatcodes(&mut self, cheatcodes: &Option>) { + if let Some(cheatcodes) = cheatcodes { + self.cheatcodes.extend(cheatcodes.cheatcodes.clone()); + self.files.extend(cheatcodes.files.clone()); + self.envs.extend(cheatcodes.envs.clone()); + self.deployed_bytecode.extend(cheatcodes.deployed_bytecode.clone()); + } + } } /// Data report by a test. @@ -825,6 +978,12 @@ impl TestKindReport { /// Various types of tests #[derive(Clone, Debug, Serialize, Deserialize)] +#[serde( + tag = "type", + content = "details", + rename_all = "lowercase", + rename_all_fields = "lowercase" +)] pub enum TestKind { /// A unit test. Unit { gas: u64 }, diff --git a/crates/forge/src/runner.rs b/crates/forge/src/runner.rs index acc5d65bc3d50..89e0ad00a3a73 100644 --- a/crates/forge/src/runner.rs +++ b/crates/forge/src/runner.rs @@ -16,6 +16,7 @@ use foundry_common::{TestFunctionExt, TestFunctionKind, contracts::ContractsByAd use foundry_compilers::utils::canonicalized; use foundry_config::{Config, FuzzCorpusConfig}; use foundry_evm::{ + backend::BackendDatabaseSnapshot, constants::CALLER, decode::RevertDecoder, executors::{ @@ -106,17 +107,18 @@ impl<'a> ContractRunner<'a> { /// Deploys the test contract inside the runner from the sending account, and optionally runs /// the `setUp` function on the test contract. - pub fn setup(&mut self, call_setup: bool) -> TestSetup { + pub fn setup(&mut self, call_setup: bool) -> (TestSetup, BackendDatabaseSnapshot) { + // TODO: create a snapshot even on setup failure self._setup(call_setup).unwrap_or_else(|err| { if err.to_string().contains("skipped") { - TestSetup::skipped(err.to_string()) + (TestSetup::skipped(err.to_string()), Default::default()) } else { - TestSetup::failed(err.to_string()) + (TestSetup::failed(err.to_string()), Default::default()) } }) } - fn _setup(&mut self, call_setup: bool) -> Result { + fn _setup(&mut self, call_setup: bool) -> Result<(TestSetup, BackendDatabaseSnapshot)> { trace!(call_setup, "setting up"); self.apply_contract_inline_config()?; @@ -150,7 +152,7 @@ impl<'a> ContractRunner<'a> { if reason.is_some() { debug!(?reason, "deployment of library failed"); result.reason = reason; - return Ok(result); + return Ok((result, Default::default())); } } @@ -179,7 +181,7 @@ impl<'a> ContractRunner<'a> { if reason.is_some() { debug!(?reason, "deployment of test contract failed"); result.reason = reason; - return Ok(result); + return Ok((result, Default::default())); } // Reset `self.sender`s, `CALLER`s and `LIBRARY_DEPLOYER`'s balance to the initial balance. @@ -189,6 +191,9 @@ impl<'a> ContractRunner<'a> { self.executor.deploy_create2_deployer()?; + // snapshot the db state before running the setUp function + let snapshot = self.executor.backend().create_db_snapshot(); + // Optionally call the `setUp` function if call_setup { trace!("calling setUp"); @@ -198,9 +203,10 @@ impl<'a> ContractRunner<'a> { result.reason = reason; } + // TODO: should it be included in the snapshot? result.fuzz_fixtures = self.fuzz_fixtures(address); - Ok(result) + Ok((result, snapshot)) } fn initial_balance(&self) -> U256 { @@ -347,7 +353,7 @@ impl<'a> ContractRunner<'a> { } let setup_time = Instant::now(); - let setup = self.setup(call_setup); + let (setup, snapshot) = self.setup(call_setup); debug!("finished setting up in {:?}", setup_time.elapsed()); self.executor.inspector_mut().tracer = prev_tracer; @@ -441,6 +447,7 @@ impl<'a> ContractRunner<'a> { identified_contracts.as_ref(), ); res.duration = start.elapsed(); + res.add_db_snapshot(snapshot.clone(), call_setup); // Record test failure for early exit (only triggers if fail-fast is enabled). if res.status.is_failure() { @@ -549,6 +556,7 @@ impl<'a> FunctionRunner<'a> { /// test ends, similar to `eth_call`. fn run_unit_test(mut self, func: &Function) -> TestResult { // Prepare unit test execution. + // TODO: record the preparation step too if self.prepare_test(func).is_err() { return self.result; } @@ -574,9 +582,37 @@ impl<'a> FunctionRunner<'a> { } }; + // @tracing: change the flag to generate a trace exactly like Phoenix does + if false { + if let Some(_) = self.executor.inspector().inner.tracer.as_ref() { + let gas_used = 0; // we don't really need this + let config = alloy_rpc_types::trace::geth::CallConfig { + only_top_call: Some(false), + with_log: Some(true), + }; + + // inspector.traces() are empty by this point, moved to `self.result.traces` + let traces = match &self.result.traces[..] { + [first, second] => { + vec![(first.1.arena.clone(), "setUp"), (second.1.arena.clone(), "test")] + } + [only] => vec![(only.1.arena.clone(), "test")], + traces => panic!("too many traces: {}", traces.len()), // TODO: there may also be preparation traces, right? + }; + + for (trace, label) in traces { + let trace = foundry_evm::traces::GethTraceBuilder::new(trace.into_nodes()) + .geth_call_traces(config, gas_used); + sh_println!("{} trace: {:#?}\n", label, trace).unwrap(); + } + } + } + let success = self.executor.is_raw_call_mut_success(self.address, &mut raw_call_result, false); + self.result.add_cheatcodes(&raw_call_result.cheatcodes); self.result.single_result(success, reason, raw_call_result); + self.result.add_test(self.sender, self.address, func.selector().to_vec(), U256::ZERO); self.result } @@ -1037,6 +1073,7 @@ impl<'a> FunctionRunner<'a> { } } + self.result.add_cheatcodes(&fuzzed_executor.executor_f.inspector().cheatcodes); self.result.fuzz_result(result); self.result } diff --git a/foundryup/foundryup b/foundryup/foundryup index 2e867184de9d2..59d43fe62dde7 100755 --- a/foundryup/foundryup +++ b/foundryup/foundryup @@ -10,7 +10,7 @@ FOUNDRY_DIR=${FOUNDRY_DIR:-"$BASE_DIR/.foundry"} FOUNDRY_VERSIONS_DIR="$FOUNDRY_DIR/versions" FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" FOUNDRY_MAN_DIR="$FOUNDRY_DIR/share/man/man1" -FOUNDRY_BIN_URL="https://raw.githubusercontent.com/foundry-rs/foundry/HEAD/foundryup/foundryup" +FOUNDRY_BIN_URL="https://raw.githubusercontent.com/BuildBearLabs/foundry/master/foundryup/foundryup" FOUNDRY_BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" FOUNDRYUP_JOBS="" FOUNDRYUP_IGNORE_VERIFICATION=false diff --git a/foundryup/install b/foundryup/install index cd5bd1125a89f..9d3b74aa157e6 100755 --- a/foundryup/install +++ b/foundryup/install @@ -8,7 +8,7 @@ FOUNDRY_DIR="${FOUNDRY_DIR:-"$BASE_DIR/.foundry"}" FOUNDRY_BIN_DIR="$FOUNDRY_DIR/bin" FOUNDRY_MAN_DIR="$FOUNDRY_DIR/share/man/man1" -BIN_URL="https://raw.githubusercontent.com/foundry-rs/foundry/HEAD/foundryup/foundryup" +BIN_URL="https://raw.githubusercontent.com/BuildBearLabs/foundry/master/foundryup/foundryup" BIN_PATH="$FOUNDRY_BIN_DIR/foundryup" # Create the .foundry bin directory and foundryup binary if it doesn't exist.