diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index a036ffa597e0..8334df8b9561 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -849,6 +849,9 @@ impl Inspector for Cheatcodes { debug!(target: "cheatcodes", tx=?self.broadcastable_transactions.back().unwrap(), "broadcastable call"); let prev = account.info.nonce; + + // Touch account to ensure that incremented nonce is committed + account.mark_touch(); account.info.nonce += 1; debug!(target: "cheatcodes", address=%broadcast.new_origin, nonce=prev+1, prev, "incremented nonce"); } else if broadcast.single_call { @@ -1485,9 +1488,10 @@ fn process_broadcast_create( // by the create2_deployer let account = data.journaled_state.state().get_mut(&broadcast_sender).unwrap(); let prev = account.info.nonce; + // Touch account to ensure that incremented nonce is committed + account.mark_touch(); account.info.nonce += 1; debug!(target: "cheatcodes", address=%broadcast_sender, nonce=prev+1, prev, "incremented nonce in create2"); - // Proxy deployer requires the data to be `salt ++ init_code` let calldata = [&salt.to_be_bytes::<32>()[..], &bytecode[..]].concat(); (calldata.into(), Some(DEFAULT_CREATE2_DEPLOYER), prev) diff --git a/crates/common/src/evm.rs b/crates/common/src/evm.rs index a924fe5251a1..2230bd760874 100644 --- a/crates/common/src/evm.rs +++ b/crates/common/src/evm.rs @@ -144,6 +144,13 @@ pub struct EvmArgs { #[clap(flatten)] #[serde(flatten)] pub env: EnvArgs, + + /// Whether to enable isolation of calls. + /// In isolation mode all top-level calls are executed as a separate transaction in a separate + /// EVM context, enabling more precise gas accounting and transaction state changes. + #[clap(long)] + #[serde(skip)] + pub isolate: bool, } // Make this set of options a `figment::Provider` so that it can be merged into the `Config` @@ -166,6 +173,10 @@ impl Provider for EvmArgs { dict.insert("ffi".to_string(), self.ffi.into()); } + if self.isolate { + dict.insert("isolate".to_string(), self.isolate.into()); + } + if self.always_use_create_2_factory { dict.insert( "always_use_create_2_factory".to_string(), diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 3c7cc1d5e376..d5d7ef9eae31 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -374,6 +374,11 @@ pub struct Config { /// Should be removed once EvmVersion Cancun is supported by solc pub cancun: bool, + /// Whether to enable call isolation. + /// + /// Useful for more correct gas accounting and EVM behavior in general. + pub isolate: bool, + /// Address labels pub labels: HashMap, @@ -1810,6 +1815,7 @@ impl Default for Config { profile: Self::DEFAULT_PROFILE, fs_permissions: FsPermissions::new([PathPermission::read("out")]), cancun: false, + isolate: false, __root: Default::default(), src: "src".into(), test: "test".into(), diff --git a/crates/evm/core/src/backend/fuzz.rs b/crates/evm/core/src/backend/fuzz.rs index f4d362ec8d84..11857fa4ab64 100644 --- a/crates/evm/core/src/backend/fuzz.rs +++ b/crates/evm/core/src/backend/fuzz.rs @@ -11,8 +11,8 @@ use alloy_genesis::GenesisAccount; use alloy_primitives::{Address, B256, U256}; use revm::{ db::DatabaseRef, - primitives::{AccountInfo, Bytecode, Env, ResultAndState}, - Database, Inspector, JournaledState, + primitives::{Account, AccountInfo, Bytecode, Env, HashMap as Map, ResultAndState}, + Database, DatabaseCommit, Inspector, JournaledState, }; use std::{borrow::Cow, collections::HashMap}; @@ -279,3 +279,9 @@ impl<'a> Database for FuzzBackendWrapper<'a> { DatabaseRef::block_hash_ref(self, number) } } + +impl<'a> DatabaseCommit for FuzzBackendWrapper<'a> { + fn commit(&mut self, changes: Map) { + self.backend.to_mut().commit(changes) + } +} diff --git a/crates/evm/core/src/opts.rs b/crates/evm/core/src/opts.rs index 49aaa0a2e284..510f14254ad7 100644 --- a/crates/evm/core/src/opts.rs +++ b/crates/evm/core/src/opts.rs @@ -61,6 +61,9 @@ pub struct EvmOpts { /// The memory limit per EVM execution in bytes. /// If this limit is exceeded, a `MemoryLimitOOG` result is thrown. pub memory_limit: u64, + + /// Whether to enable isolation of calls. + pub isolate: bool, } impl EvmOpts { diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index a1fc13d70619..2a66ac76280d 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -3,15 +3,21 @@ use super::{ StackSnapshotType, TracePrinter, TracingInspector, TracingInspectorConfig, }; use alloy_primitives::{Address, Bytes, Log, B256, U256}; -use foundry_evm_core::{backend::DatabaseExt, debug::DebugArena}; +use foundry_evm_core::{ + backend::DatabaseExt, + debug::DebugArena, + utils::{eval_to_instruction_result, halt_to_instruction_result}, +}; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::CallTraceArena; use revm::{ + evm_inner, interpreter::{ - return_revert, CallInputs, CreateInputs, Gas, InstructionResult, Interpreter, Stack, + return_revert, CallInputs, CallScheme, CreateInputs, Gas, InstructionResult, Interpreter, + Stack, }, - primitives::{BlockEnv, Env}, - EVMData, Inspector, + primitives::{BlockEnv, Env, ExecutionResult, Output, State, TransactTo}, + DatabaseCommit, EVMData, Inspector, }; use std::{collections::HashMap, sync::Arc}; @@ -44,6 +50,10 @@ pub struct InspectorStackBuilder { pub print: Option, /// The chisel state inspector. pub chisel_state: Option, + /// Whether to enable call isolation. + /// In isolation mode all top-level calls are executed as a separate transaction in a separate + /// EVM context, enabling more precise gas accounting and transaction state changes. + pub enable_isolation: bool, } impl InspectorStackBuilder { @@ -123,6 +133,14 @@ impl InspectorStackBuilder { self } + /// Set whether to enable the call isolation. + /// For description of call isolation, see [`InspectorStack::enable_isolation`]. + #[inline] + pub fn enable_isolation(mut self, yes: bool) -> Self { + self.enable_isolation = yes; + self + } + /// Builds the stack of inspectors to use when transacting/committing on the EVM. /// /// See also [`revm::Evm::inspect_ref`] and [`revm::Evm::commit_ref`]. @@ -138,6 +156,7 @@ impl InspectorStackBuilder { coverage, print, chisel_state, + enable_isolation, } = self; let mut stack = InspectorStack::new(); @@ -157,6 +176,8 @@ impl InspectorStackBuilder { stack.print(print.unwrap_or(false)); stack.tracing(trace.unwrap_or(false)); + stack.enable_isolation(enable_isolation); + // environment, must come after all of the inspectors if let Some(block) = block { stack.set_block(&block); @@ -180,6 +201,38 @@ macro_rules! call_inspectors { )+}} } +/// Same as [call_inspectors] macro, but with depth adjustment for isolated execution. +macro_rules! call_inspectors_adjust_depth { + ([$($inspector:expr),+ $(,)?], |$id:ident $(,)?| $call:expr, $self:ident, $data:ident $(,)?) => { + if $self.in_inner_context { + $data.journaled_state.depth += 1; + } + {$( + if let Some($id) = $inspector { + if let Some(result) = $call { + if $self.in_inner_context { + $data.journaled_state.depth -= 1; + } + return result; + } + } + )+} + if $self.in_inner_context { + $data.journaled_state.depth -= 1; + } + } +} + +/// Helper method which updates data in the state with the data from the database. +fn update_state(state: &mut State, db: &mut DB) { + for (addr, acc) in state.iter_mut() { + acc.info = db.basic(*addr).unwrap().unwrap_or_default(); + for (key, val) in acc.storage.iter_mut() { + val.present_value = db.storage(*addr, *key).unwrap(); + } + } +} + /// The collected results of [`InspectorStack`]. pub struct InspectorData { pub logs: Vec, @@ -191,6 +244,24 @@ pub struct InspectorData { pub chisel_state: Option<(Stack, Vec, InstructionResult)>, } +/// Contains data about the state of outer/main EVM which created and invoked the inner EVM context. +/// Used to adjust EVM state while in inner context. +/// +/// We need this to avoid breaking changes due to EVM behavior differences in isolated vs +/// non-isolated mode. For descriptions and workarounds for those changes see: https://github.com/foundry-rs/foundry/pull/7186#issuecomment-1959102195 +#[derive(Debug, Clone)] +pub struct InnerContextData { + /// The sender of the inner EVM context. + /// It is also an origin of the transaction that created the inner EVM context. + sender: Address, + /// Nonce of the sender before invocation of the inner EVM context. + original_sender_nonce: u64, + /// Origin of the transaction in the outer EVM context. + original_origin: Address, + /// Whether the inner context was created by a CREATE transaction. + is_create: bool, +} + /// An inspector that calls multiple inspectors in sequence. /// /// If a call to an inspector returns a value other than [InstructionResult::Continue] (or @@ -205,6 +276,11 @@ pub struct InspectorStack { pub log_collector: Option, pub printer: Option, pub tracer: Option, + pub enable_isolation: bool, + + /// Flag marking if we are in the inner EVM context. + pub in_inner_context: bool, + pub inner_context_data: Option, } impl InspectorStack { @@ -271,6 +347,12 @@ impl InspectorStack { self.debugger = yes.then(Default::default); } + /// Set whether to enable call isolation. + #[inline] + pub fn enable_isolation(&mut self, yes: bool) { + self.enable_isolation = yes; + } + /// Set whether to enable the log collector. #[inline] pub fn collect_logs(&mut self, yes: bool) { @@ -327,7 +409,7 @@ impl InspectorStack { status: InstructionResult, retdata: Bytes, ) -> (InstructionResult, Gas, Bytes) { - call_inspectors!( + call_inspectors_adjust_depth!( [ &mut self.fuzzer, &mut self.debugger, @@ -346,19 +428,136 @@ impl InspectorStack { if new_status != status || (new_status == InstructionResult::Revert && new_retdata != retdata) { - return (new_status, new_gas, new_retdata); + Some((new_status, new_gas, new_retdata)) + } else { + None } - } + }, + self, + data ); - (status, remaining_gas, retdata) } + + fn transact_inner( + &mut self, + data: &mut EVMData<'_, DB>, + transact_to: TransactTo, + caller: Address, + input: Bytes, + gas_limit: u64, + value: U256, + ) -> (InstructionResult, Option
, Gas, Bytes) { + data.db.commit(data.journaled_state.state.clone()); + + let nonce = data + .journaled_state + .load_account(caller, data.db) + .expect("failed to load caller") + .0 + .info + .nonce; + + let cached_env = data.env.clone(); + + data.env.block.basefee = U256::ZERO; + data.env.tx.caller = caller; + data.env.tx.transact_to = transact_to.clone(); + data.env.tx.data = input; + data.env.tx.value = value; + data.env.tx.nonce = Some(nonce); + // Add 21000 to the gas limit to account for the base cost of transaction. + // We might have modified block gas limit earlier and revm will reject tx with gas limit > + // block gas limit, so we adjust. + data.env.tx.gas_limit = std::cmp::min(gas_limit + 21000, data.env.block.gas_limit.to()); + data.env.tx.gas_price = U256::ZERO; + + self.inner_context_data = Some(InnerContextData { + sender: data.env.tx.caller, + original_origin: cached_env.tx.caller, + original_sender_nonce: nonce, + is_create: matches!(transact_to, TransactTo::Create(_)), + }); + self.in_inner_context = true; + let res = evm_inner(data.env, data.db, Some(self)).transact(); + self.in_inner_context = false; + self.inner_context_data = None; + + data.env.tx = cached_env.tx; + data.env.block.basefee = cached_env.block.basefee; + + let mut gas = Gas::new(gas_limit); + + let Ok(mut res) = res else { + // Should we match, encode and propagate error as a revert reason? + return (InstructionResult::Revert, None, gas, Bytes::new()); + }; + + // Commit changes after transaction + data.db.commit(res.state.clone()); + + // Update both states with new DB data after commit. + update_state(&mut data.journaled_state.state, data.db); + update_state(&mut res.state, data.db); + + // Merge transaction journal into the active journal. + for (addr, acc) in res.state { + if let Some(acc_mut) = data.journaled_state.state.get_mut(&addr) { + acc_mut.status |= acc.status; + for (key, val) in acc.storage { + if !acc_mut.storage.contains_key(&key) { + acc_mut.storage.insert(key, val); + } + } + } else { + data.journaled_state.state.insert(addr, acc); + } + } + + match res.result { + ExecutionResult::Success { reason, gas_used, gas_refunded, logs: _, output } => { + gas.set_refund(gas_refunded as i64); + gas.record_cost(gas_used); + let address = match output { + Output::Create(_, address) => address, + Output::Call(_) => None, + }; + (eval_to_instruction_result(reason), address, gas, output.into_data()) + } + ExecutionResult::Halt { reason, gas_used } => { + gas.record_cost(gas_used); + (halt_to_instruction_result(reason), None, gas, Bytes::new()) + } + ExecutionResult::Revert { gas_used, output } => { + gas.record_cost(gas_used); + (InstructionResult::Revert, None, gas, output) + } + } + } + + /// Adjusts the EVM data for the inner EVM context. + /// Should be called on the top-level call of inner context (depth == 0 && + /// self.in_inner_context) Decreases sender nonce for CALLs to keep backwards compatibility + /// Updates tx.origin to the value before entering inner context + fn adjust_evm_data_for_inner_context(&mut self, data: &mut EVMData<'_, DB>) { + let inner_context_data = + self.inner_context_data.as_ref().expect("should be called in inner context"); + let sender_acc = data + .journaled_state + .state + .get_mut(&inner_context_data.sender) + .expect("failed to load sender"); + if !inner_context_data.is_create { + sender_acc.info.nonce = inner_context_data.original_sender_nonce; + } + data.env.tx.caller = inner_context_data.original_origin; + } } -impl Inspector for InspectorStack { +impl Inspector for InspectorStack { fn initialize_interp(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { let res = interpreter.instruction_result; - call_inspectors!( + call_inspectors_adjust_depth!( [ &mut self.debugger, &mut self.coverage, @@ -372,16 +571,19 @@ impl Inspector for InspectorStack { // Allow inspectors to exit early if interpreter.instruction_result != res { - #[allow(clippy::needless_return)] - return; + Some(()) + } else { + None } - } + }, + self, + data ); } fn step(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { let res = interpreter.instruction_result; - call_inspectors!( + call_inspectors_adjust_depth!( [ &mut self.fuzzer, &mut self.debugger, @@ -396,10 +598,13 @@ impl Inspector for InspectorStack { // Allow inspectors to exit early if interpreter.instruction_result != res { - #[allow(clippy::needless_return)] - return; + Some(()) + } else { + None } - } + }, + self, + data ); } @@ -410,17 +615,20 @@ impl Inspector for InspectorStack { topics: &[B256], data: &Bytes, ) { - call_inspectors!( + call_inspectors_adjust_depth!( [&mut self.tracer, &mut self.log_collector, &mut self.cheatcodes, &mut self.printer], |inspector| { inspector.log(evm_data, address, topics, data); - } + None + }, + self, + evm_data ); } fn step_end(&mut self, interpreter: &mut Interpreter<'_>, data: &mut EVMData<'_, DB>) { let res = interpreter.instruction_result; - call_inspectors!( + call_inspectors_adjust_depth!( [ &mut self.debugger, &mut self.tracer, @@ -434,10 +642,13 @@ impl Inspector for InspectorStack { // Allow inspectors to exit early if interpreter.instruction_result != res { - #[allow(clippy::needless_return)] - return; + Some(()) + } else { + None } - } + }, + self, + data ); } @@ -446,7 +657,12 @@ impl Inspector for InspectorStack { data: &mut EVMData<'_, DB>, call: &mut CallInputs, ) -> (InstructionResult, Gas, Bytes) { - call_inspectors!( + if self.in_inner_context && data.journaled_state.depth == 0 { + self.adjust_evm_data_for_inner_context(data); + return (InstructionResult::Continue, Gas::new(call.gas_limit), Bytes::new()); + } + + call_inspectors_adjust_depth!( [ &mut self.fuzzer, &mut self.debugger, @@ -460,13 +676,32 @@ impl Inspector for InspectorStack { let (status, gas, retdata) = inspector.call(data, call); // Allow inspectors to exit early - #[allow(clippy::needless_return)] if status != InstructionResult::Continue { - return (status, gas, retdata); + Some((status, gas, retdata)) + } else { + None } - } + }, + self, + data ); + if self.enable_isolation && + call.context.scheme == CallScheme::Call && + !self.in_inner_context && + data.journaled_state.depth == 1 + { + let (res, _, gas, output) = self.transact_inner( + data, + TransactTo::Call(call.contract), + call.context.caller, + call.input.clone(), + call.gas_limit, + call.transfer.value, + ); + return (res, gas, output); + } + (InstructionResult::Continue, Gas::new(call.gas_limit), Bytes::new()) } @@ -478,8 +713,13 @@ impl Inspector for InspectorStack { status: InstructionResult, retdata: Bytes, ) -> (InstructionResult, Gas, Bytes) { - let res = self.do_call_end(data, call, remaining_gas, status, retdata); + // Inner context calls with depth 0 are being dispatched as top-level calls with depth 1. + // Avoid processing twice. + if self.in_inner_context && data.journaled_state.depth == 0 { + return (status, remaining_gas, retdata); + } + let res = self.do_call_end(data, call, remaining_gas, status, retdata); if matches!(res.0, return_revert!()) { // Encountered a revert, since cheatcodes may have altered the evm state in such a way // that violates some constraints, e.g. `deal`, we need to manually roll back on revert @@ -497,7 +737,12 @@ impl Inspector for InspectorStack { data: &mut EVMData<'_, DB>, call: &mut CreateInputs, ) -> (InstructionResult, Option
, Gas, Bytes) { - call_inspectors!( + if self.in_inner_context && data.journaled_state.depth == 0 { + self.adjust_evm_data_for_inner_context(data); + return (InstructionResult::Continue, None, Gas::new(call.gas_limit), Bytes::new()); + } + + call_inspectors_adjust_depth!( [ &mut self.debugger, &mut self.tracer, @@ -511,11 +756,26 @@ impl Inspector for InspectorStack { // Allow inspectors to exit early if status != InstructionResult::Continue { - return (status, addr, gas, retdata); + Some((status, addr, gas, retdata)) + } else { + None } - } + }, + self, + data ); + if self.enable_isolation && !self.in_inner_context && data.journaled_state.depth == 1 { + return self.transact_inner( + data, + TransactTo::Create(call.scheme), + call.caller, + call.init_code.clone(), + call.gas_limit, + call.value, + ); + } + (InstructionResult::Continue, None, Gas::new(call.gas_limit), Bytes::new()) } @@ -528,7 +788,12 @@ impl Inspector for InspectorStack { remaining_gas: Gas, retdata: Bytes, ) -> (InstructionResult, Option
, Gas, Bytes) { - call_inspectors!( + // Inner context calls with depth 0 are being dispatched as top-level calls with depth 1. + // Avoid processing twice. + if self.in_inner_context && data.journaled_state.depth == 0 { + return (status, address, remaining_gas, retdata); + } + call_inspectors_adjust_depth!( [ &mut self.debugger, &mut self.tracer, @@ -548,9 +813,13 @@ impl Inspector for InspectorStack { ); if new_status != status { - return (new_status, new_address, new_gas, new_retdata); + Some((new_status, new_address, new_gas, new_retdata)) + } else { + None } - } + }, + self, + data ); (status, address, remaining_gas, retdata) diff --git a/crates/forge/bin/cmd/script/executor.rs b/crates/forge/bin/cmd/script/executor.rs index e0f8ed8ece1b..071cbcbb219d 100644 --- a/crates/forge/bin/cmd/script/executor.rs +++ b/crates/forge/bin/cmd/script/executor.rs @@ -303,14 +303,17 @@ impl ScriptArgs { if let SimulationStage::Local = stage { builder = builder.inspectors(|stack| { - stack.debug(self.debug).cheatcodes( - CheatsConfig::new( - &script_config.config, - script_config.evm_opts.clone(), - script_wallets, + stack + .debug(self.debug) + .cheatcodes( + CheatsConfig::new( + &script_config.config, + script_config.evm_opts.clone(), + script_wallets, + ) + .into(), ) - .into(), - ) + .enable_isolation(script_config.evm_opts.isolate) }); } diff --git a/crates/forge/bin/cmd/test/mod.rs b/crates/forge/bin/cmd/test/mod.rs index 4f0f9f29940a..1b41f41361a2 100644 --- a/crates/forge/bin/cmd/test/mod.rs +++ b/crates/forge/bin/cmd/test/mod.rs @@ -143,6 +143,11 @@ impl TestArgs { // Merge all configs let (mut config, mut evm_opts) = self.load_config_and_evm_opts_emit_warnings()?; + // Explicitly enable isolation for gas reports for more correct gas accounting + if self.gas_report { + evm_opts.isolate = true; + } + // Set up the project. let mut project = config.project()?; @@ -197,6 +202,7 @@ impl TestArgs { .with_fork(evm_opts.get_fork(&config, env.clone())) .with_cheats_config(CheatsConfig::new(&config, evm_opts.clone(), None)) .with_test_options(test_options.clone()) + .enable_isolation(evm_opts.isolate) .build(project_root, output, env, evm_opts)?; if let Some(debug_test_pattern) = &self.debug { diff --git a/crates/forge/src/gas_report.rs b/crates/forge/src/gas_report.rs index afe41f76dfa5..f6c269d7029e 100644 --- a/crates/forge/src/gas_report.rs +++ b/crates/forge/src/gas_report.rs @@ -7,6 +7,7 @@ use crate::{ }; use comfy_table::{presets::ASCII_MARKDOWN, *}; use foundry_common::{calc, TestFunctionExt}; +use foundry_evm::traces::CallKind; use serde::{Deserialize, Serialize}; use std::{collections::BTreeMap, fmt::Display}; @@ -75,6 +76,16 @@ impl GasReport { return; } + // Only include top-level calls which accout for calldata and base (21.000) cost. + // Only include Calls and Creates as only these calls are isolated in inspector. + if trace.depth != 1 && + (trace.kind == CallKind::Call || + trace.kind == CallKind::Create || + trace.kind == CallKind::Create2) + { + return; + } + let decoded = decoder.decode_function(&node.trace).await; let Some(name) = &decoded.contract else { return }; diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 0651e82d6184..699df0da0566 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -60,6 +60,8 @@ pub struct MultiContractRunner { pub debug: bool, /// Settings related to fuzz and/or invariant tests pub test_options: TestOptions, + /// Whether to enable call isolation + pub isolation: bool, } impl MultiContractRunner { @@ -179,6 +181,7 @@ impl MultiContractRunner { .trace(self.evm_opts.verbosity >= 3 || self.debug) .debug(self.debug) .coverage(self.coverage) + .enable_isolation(self.isolation) }) .spec(self.evm_spec) .gas_limit(self.evm_opts.gas_limit()) @@ -256,6 +259,8 @@ pub struct MultiContractRunnerBuilder { pub coverage: bool, /// Whether or not to collect debug info pub debug: bool, + /// Whether to enable call isolation + pub isolation: bool, /// Settings related to fuzz and/or invariant tests pub test_options: Option, } @@ -301,6 +306,11 @@ impl MultiContractRunnerBuilder { self } + pub fn enable_isolation(mut self, enable: bool) -> Self { + self.isolation = enable; + self + } + /// Given an EVM, proceeds to return a runner which is able to execute all tests /// against that evm pub fn build( @@ -381,6 +391,7 @@ impl MultiContractRunnerBuilder { coverage: self.coverage, debug: self.debug, test_options: self.test_options.unwrap_or_default(), + isolation: self.isolation, }) } } diff --git a/crates/forge/tests/cli/config.rs b/crates/forge/tests/cli/config.rs index 2e4be0c6a8b5..ed6a02030d59 100644 --- a/crates/forge/tests/cli/config.rs +++ b/crates/forge/tests/cli/config.rs @@ -120,6 +120,7 @@ forgetest!(can_extract_config_values, |prj, cmd| { fs_permissions: Default::default(), labels: Default::default(), cancun: true, + isolate: true, __non_exhaustive: (), __warnings: vec![], }; diff --git a/crates/forge/tests/cli/test_cmd.rs b/crates/forge/tests/cli/test_cmd.rs index 25a30a1fd040..49e5e2000477 100644 --- a/crates/forge/tests/cli/test_cmd.rs +++ b/crates/forge/tests/cli/test_cmd.rs @@ -432,3 +432,86 @@ contract CustomTypesTest is Test { .join("tests/fixtures/include_custom_types_in_traces.stdout"), ); }); + +forgetest_init!(can_test_selfdestruct_with_isolation, |prj, cmd| { + prj.wipe_contracts(); + + prj.add_test( + "Contract.t.sol", + r#" +import {Test} from "forge-std/Test.sol"; + +contract Destructing { + function destruct() public { + selfdestruct(payable(address(0))); + } +} + +contract SelfDestructTest is Test { + function test() public { + Destructing d = new Destructing(); + vm.store(address(d), bytes32(0), bytes32(uint256(1))); + d.destruct(); + assertEq(address(d).code.length, 0); + assertEq(vm.load(address(d), bytes32(0)), bytes32(0)); + } +} + "#, + ) + .unwrap(); + + cmd.args(["test", "-vvvv", "--isolate"]).assert_success(); +}); + +forgetest_init!(can_test_transient_storage_with_isolation, |prj, cmd| { + prj.wipe_contracts(); + + prj.add_test( + "Contract.t.sol", + r#"pragma solidity 0.8.24; +import {Test} from "forge-std/Test.sol"; + +contract TransientTester { + function locked() public view returns (bool isLocked) { + assembly { + isLocked := tload(0) + } + } + + modifier lock() { + require(!locked(), "locked"); + assembly { + tstore(0, 1) + } + _; + } + + function maybeReentrant(address target, bytes memory data) public lock { + (bool success, bytes memory ret) = target.call(data); + if (!success) { + // forwards revert reason + assembly { + let ret_size := mload(ret) + revert(add(32, ret), ret_size) + } + } + } +} + +contract TransientTest is Test { + function test() public { + TransientTester t = new TransientTester(); + vm.expectRevert(bytes("locked")); + t.maybeReentrant(address(t), abi.encodeCall(TransientTester.maybeReentrant, (address(0), new bytes(0)))); + + t.maybeReentrant(address(0), new bytes(0)); + assertEq(t.locked(), false); + } +} + + "#, + ) + .unwrap(); + + cmd.args(["test", "-vvvv", "--isolate", "--evm-version", "cancun"]).assert_success(); +}); diff --git a/testdata/repros/Issue3653.t.sol b/testdata/repros/Issue3653.t.sol index 6e52c49f8aa5..5022af67859f 100644 --- a/testdata/repros/Issue3653.t.sol +++ b/testdata/repros/Issue3653.t.sol @@ -11,13 +11,13 @@ contract Issue3653Test is DSTest { Token token; constructor() { - fork = vm.createSelectFork("rpcAlias", 10); + fork = vm.createSelectFork("rpcAlias", 1000000); token = new Token(); vm.makePersistent(address(token)); } function testDummy() public { - assertEq(block.number, 10); + assertEq(block.number, 1000000); } }