diff --git a/Cargo.lock b/Cargo.lock index 2d827c3854cae..dc586e841b510 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6616,6 +6616,7 @@ dependencies = [ "parking_lot 0.12.4", "proptest", "rand 0.9.2", + "revive-utils", "revm", "revm-inspectors", "semver 1.0.26", @@ -6968,6 +6969,7 @@ dependencies = [ "indicatif", "parking_lot 0.12.4", "proptest", + "revive-utils", "revm", "revm-inspectors", "serde", @@ -15760,6 +15762,18 @@ dependencies = [ "tracing", ] +[[package]] +name = "revive-utils" +version = "1.3.6" +dependencies = [ + "alloy-primitives", + "foundry-evm-core", + "foundry-evm-traces", + "polkadot-sdk", + "revive-env", + "revm", +] + [[package]] name = "revm" version = "27.1.0" diff --git a/Cargo.toml b/Cargo.toml index 59c4c3701a313..0791008a64d8f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ members = [ "crates/script-sequence/", "crates/macros/", "crates/test-utils/", + "crates/revive-utils", "crates/revive-env", "crates/revive-strategy", "crates/lint/", @@ -213,6 +214,7 @@ foundry-wallets = { path = "crates/wallets" } foundry-linking = { path = "crates/linking" } revive-env = { path = "crates/revive-env" } revive-strategy = { path = "crates/revive-strategy" } +revive-utils = { path = "crates/revive-utils" } # solc & compilation utilities foundry-block-explorers = { version = "0.20.0", default-features = false } diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index f62bde4323e71..57e50e1ef0a99 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -23,6 +23,7 @@ foundry-evm-core.workspace = true foundry-evm-traces.workspace = true foundry-wallets.workspace = true forge-script-sequence.workspace = true +revive-utils.workspace = true alloy-dyn-abi.workspace = true alloy-evm.workspace = true diff --git a/crates/cheatcodes/spec/src/lib.rs b/crates/cheatcodes/spec/src/lib.rs index 5ee5c03dea2b6..6919c45ea2c9d 100644 --- a/crates/cheatcodes/spec/src/lib.rs +++ b/crates/cheatcodes/spec/src/lib.rs @@ -15,7 +15,7 @@ pub use function::{Function, Mutability, Visibility}; mod items; pub use items::{Enum, EnumVariant, Error, Event, Struct, StructField}; -mod vm; +pub mod vm; pub use vm::Vm; // The `cheatcodes.json` schema. diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 8ae38cf201e0f..1a9354e382062 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -37,11 +37,12 @@ use foundry_evm_core::{ constants::{CHEATCODE_ADDRESS, HARDHAT_CONSOLE_ADDRESS, MAGIC_ASSUME}, evm::{FoundryEvm, new_evm_with_existing_context}, }; -use foundry_evm_traces::{TracingInspector, TracingInspectorConfig}; +use foundry_evm_traces::TracingInspectorConfig; use foundry_wallets::multi_wallet::MultiWallet; use itertools::Itertools; use proptest::test_runner::{RngAlgorithm, TestRng, TestRunner}; use rand::Rng; +use revive_utils::TraceCollector; use revm::{ Inspector, Journal, bytecode::opcode as op, @@ -108,9 +109,19 @@ pub trait CheatcodesExecutor { } /// Returns a mutable reference to the tracing inspector if it is available. - fn tracing_inspector(&mut self) -> Option<&mut Option> { + fn tracing_inspector(&mut self) -> Option<&mut Option> { None } + + fn trace_revive( + &mut self, + ccx_state: &mut Cheatcodes, + ecx: Ecx, + call_traces: Box, + ) { + let mut inspector = self.get_inspector(ccx_state); + inspector.trace_revive(ecx, call_traces, false); + } } /// Constructs [FoundryEvm] and runs a given closure with it. @@ -924,6 +935,8 @@ impl Cheatcodes { self.expected_creates.swap_remove(index); } } + + self.strategy.runner.revive_remove_duplicate_account_access(self); } pub fn call_with_executor( @@ -1595,6 +1608,8 @@ impl Inspector> for Cheatcodes { } } + self.strategy.runner.revive_remove_duplicate_account_access(self); + // At the end of the call, // we need to check if we've found all the emits. // We know we've found all the expected emits in the right order diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs index 0036004d6f7c3..5fe73d1aaafae 100644 --- a/crates/cheatcodes/src/lib.rs +++ b/crates/cheatcodes/src/lib.rs @@ -65,7 +65,7 @@ pub use strategy::{ mod string; mod test; -pub use test::expect::ExpectedCallTracker; +pub use test::expect::{ExpectedCallTracker, handle_expect_emit}; mod toml; diff --git a/crates/cheatcodes/src/strategy.rs b/crates/cheatcodes/src/strategy.rs index 1d977909a6d79..2623c3fd04893 100644 --- a/crates/cheatcodes/src/strategy.rs +++ b/crates/cheatcodes/src/strategy.rs @@ -257,6 +257,8 @@ pub trait CheatcodeInspectorStrategyExt { ) -> Option { None } + // Remove duplicate accesses in storage_recorder + fn revive_remove_duplicate_account_access(&self, _state: &mut crate::Cheatcodes) {} } // Legacy type aliases for backward compatibility diff --git a/crates/cheatcodes/src/test/expect.rs b/crates/cheatcodes/src/test/expect.rs index 5dc68dc9230b3..280432e14974b 100644 --- a/crates/cheatcodes/src/test/expect.rs +++ b/crates/cheatcodes/src/test/expect.rs @@ -776,7 +776,7 @@ fn expect_emit( Ok(Default::default()) } -pub(crate) fn handle_expect_emit( +pub fn handle_expect_emit( state: &mut Cheatcodes, log: &alloy_primitives::Log, interpreter: &mut Interpreter, diff --git a/crates/evm/core/src/lib.rs b/crates/evm/core/src/lib.rs index 128862e1a61c1..d8bc0fd1497c4 100644 --- a/crates/evm/core/src/lib.rs +++ b/crates/evm/core/src/lib.rs @@ -36,6 +36,8 @@ pub mod precompiles; pub mod state_snapshot; pub mod utils; +pub type Ecx<'a, 'b, 'c> = &'a mut EthEvmContext<&'b mut (dyn DatabaseExt + 'c)>; + /// An extension trait that allows us to add additional hooks to Inspector for later use in /// handlers. #[auto_impl(&mut, Box)] @@ -66,6 +68,14 @@ pub trait InspectorExt: for<'a> Inspector fn create2_deployer(&self) -> Address { DEFAULT_CREATE2_DEPLOYER } + + fn trace_revive( + &mut self, + _context: Ecx<'_, '_, '_>, + _call_traces: Box, + _record_top_call: bool, + ) { + } } impl InspectorExt for NoOpInspector {} diff --git a/crates/evm/evm/Cargo.toml b/crates/evm/evm/Cargo.toml index 7124c9d0bb0ac..a6ddabe6a8552 100644 --- a/crates/evm/evm/Cargo.toml +++ b/crates/evm/evm/Cargo.toml @@ -22,6 +22,7 @@ foundry-evm-core.workspace = true foundry-evm-coverage.workspace = true foundry-evm-fuzz.workspace = true foundry-evm-traces.workspace = true +revive-utils.workspace = true alloy-dyn-abi = { workspace = true, features = ["arbitrary", "eip712"] } alloy-evm.workspace = true @@ -55,3 +56,4 @@ indicatif.workspace = true serde_json.workspace = true serde.workspace = true uuid.workspace = true + diff --git a/crates/evm/evm/src/inspectors/stack.rs b/crates/evm/evm/src/inspectors/stack.rs index af3caedf2c12c..652d990b3314d 100644 --- a/crates/evm/evm/src/inspectors/stack.rs +++ b/crates/evm/evm/src/inspectors/stack.rs @@ -1,6 +1,6 @@ use super::{ Cheatcodes, CheatsConfig, ChiselState, CustomPrintTracer, Fuzzer, LineCoverageCollector, - LogCollector, RevertDiagnostic, ScriptExecutionInspector, TracingInspector, + LogCollector, RevertDiagnostic, ScriptExecutionInspector, }; use alloy_evm::{Evm, eth::EthEvmContext}; use alloy_primitives::{ @@ -9,12 +9,13 @@ use alloy_primitives::{ }; use foundry_cheatcodes::{CheatcodesExecutor, Wallets}; use foundry_evm_core::{ - ContextExt, Env, InspectorExt, + ContextExt, Ecx, Env, InspectorExt, backend::{DatabaseExt, JournaledState}, evm::new_evm_with_inspector, }; use foundry_evm_coverage::HitMaps; use foundry_evm_traces::{SparsedTraceArena, TraceMode}; +use revive_utils::TraceCollector; use revm::{ Inspector, context::{ @@ -308,7 +309,7 @@ pub struct InspectorStackInner { pub printer: Option, pub revert_diag: Option, pub script_execution_inspector: Option, - pub tracer: Option, + pub tracer: Option, // InspectorExt and other internal data. pub enable_isolation: bool, @@ -335,7 +336,7 @@ impl CheatcodesExecutor for InspectorStackInner { Box::new(InspectorStackRefMut { cheatcodes: Some(cheats), inner: self }) } - fn tracing_inspector(&mut self) -> Option<&mut Option> { + fn tracing_inspector(&mut self) -> Option<&mut Option> { Some(&mut self.tracer) } } @@ -1092,6 +1093,20 @@ impl InspectorExt for InspectorStackRefMut<'_> { fn create2_deployer(&self) -> Address { self.inner.create2_deployer } + fn trace_revive( + &mut self, + ecx: Ecx<'_, '_, '_>, + call_traces: Box, /* TODO(merge): should be moved elsewhere, + * represents `Vec` */ + record_top_call: bool, + ) { + call_inspectors!([&mut self.tracer], |inspector| InspectorExt::trace_revive( + inspector, + ecx, + call_traces, + record_top_call + )); + } } impl Inspector> for InspectorStack { @@ -1183,6 +1198,16 @@ impl InspectorExt for InspectorStack { fn create2_deployer(&self) -> Address { self.create2_deployer } + + fn trace_revive( + &mut self, + ecx: Ecx<'_, '_, '_>, + call_traces: Box, /* TODO(merge): should be moved elsewhere, + * represents `Vec` */ + record_top_call: bool, + ) { + self.as_mut().trace_revive(ecx, call_traces, record_top_call); + } } impl<'a> Deref for InspectorStackRefMut<'a> { diff --git a/crates/forge/tests/cli/revive_vm.rs b/crates/forge/tests/cli/revive_vm.rs index 02698066b546d..9b5532e67c4a3 100644 --- a/crates/forge/tests/cli/revive_vm.rs +++ b/crates/forge/tests/cli/revive_vm.rs @@ -304,9 +304,8 @@ contract Load is DSTest { Vm constant vm = Vm(HEVM_ADDRESS); function testFuzz_Load(uint256 x) public { - vm.pvm(true); - Counter counter = new Counter(x); - bytes32 res = vm.load(address(counter), bytes32(uint256(0))); + address counter = address(new Counter(x)); + bytes32 res = vm.load(counter, bytes32(uint256(0))); assertEq(uint256(res), x); } } @@ -314,7 +313,7 @@ function testFuzz_Load(uint256 x) public { ) .unwrap(); - let res = cmd.args(["test", "--resolc", "-vvv"]).assert_success(); + let res = cmd.args(["test", "--resolc", "--resolc-startup", "-vvv"]).assert_success(); res.stderr_eq(str![""]).stdout_eq(str![[r#" [COMPILING_FILES] with [SOLC_VERSION] [SOLC_VERSION] [ELAPSED] @@ -331,3 +330,944 @@ Ran 1 test suite [ELAPSED]: 1 tests passed, 0 failed, 0 skipped (1 total tests) "#]]); }); + +forgetest!(trace_counter_test, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_vm(); + prj.insert_console(); + prj.add_source( + "Counter.sol", + r#" + // SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; + + contract Counter { + uint256 public number = 0; + event Increment(uint256 result); + event SetNumber(uint256 result); + error Revert(string text); + + function setNumber(uint256 newNumber) public { + number = newNumber; + emit SetNumber(number); + } + + function failed_call() public pure { + revert Revert("failure"); + } + + function increment() public { + number = number + 1; + emit Increment(number); + + } + } + "#, + ) + .unwrap(); + prj.add_source( + "CounterTest.t.sol", + r#" +import "./test.sol"; +import "./Vm.sol"; +import {Counter} from "./Counter.sol"; +import {console} from "./console.sol"; + +contract CounterTest is DSTest { +Vm constant vm = Vm(HEVM_ADDRESS); +Counter public counter; + +function setUp() public { + counter = new Counter(); + vm.expectEmit(); + emit Counter.SetNumber(5); + + counter.setNumber(5); + assertEq(counter.number(), 5); +} + +function test_Increment() public { + assertEq(counter.number(), 5); + counter.setNumber(55); + assertEq(counter.number(), 55); + counter.increment(); + assertEq(counter.number(), 56); +} + +function test_expectRevert() public { + vm.expectRevert(abi.encodeWithSelector(Counter.Revert.selector, "failure")); + counter.failed_call(); +} +} +"#, + ) + .unwrap(); + prj.update_config(|config| config.evm_version = EvmVersion::Cancun); + + let res = cmd.args(["test", "--resolc", "--resolc-startup", "-vvvvv"]).assert_success(); + res.stderr_eq("").stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +[COMPILING_FILES] with [RESOLC_VERSION] +[RESOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 2 tests for src/CounterTest.t.sol:CounterTest +[PASS] test_Increment() ([GAS]) +Traces: + [765075403] CounterTest::setUp() + ├─ [262294819] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 7404 bytes of code + ├─ [0] VM::expectEmit() + │ └─ ← [Return] + ├─ emit SetNumber(result: 5) + ├─ [385250826] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::setNumber(5) + │ ├─ emit SetNumber(result: 5) + │ └─ ← [Stop] + ├─ [117489011] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::number() [staticcall] + │ └─ ← [Return] 5 + └─ ← [Stop] + + [737726031] CounterTest::test_Increment() + ├─ [117489011] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::number() [staticcall] + │ └─ ← [Return] 5 + ├─ [385250826] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::setNumber(55) + │ ├─ emit SetNumber(result: 55) + │ └─ ← [Stop] + ├─ [117489011] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::number() [staticcall] + │ └─ ← [Return] 55 + ├─ [0] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::increment() + │ ├─ emit Increment(result: 56) + │ └─ ← [Stop] + ├─ [117489011] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::number() [staticcall] + │ └─ ← [Return] 56 + └─ ← [Stop] + +[PASS] test_expectRevert() ([GAS]) +Traces: + [765075403] CounterTest::setUp() + ├─ [262294819] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 7404 bytes of code + ├─ [0] VM::expectEmit() + │ └─ ← [Return] + ├─ emit SetNumber(result: 5) + ├─ [385250826] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::setNumber(5) + │ ├─ emit SetNumber(result: 5) + │ └─ ← [Stop] + ├─ [117489011] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::number() [staticcall] + │ └─ ← [Return] 5 + └─ ← [Stop] + + [56930227] CounterTest::test_expectRevert() + ├─ [0] VM::expectRevert(custom error 0xf28dceb3: 0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000006456941a80000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000076661696c7572650000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000) + │ └─ ← [Return] + ├─ [56921388] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::failed_call() [staticcall] + │ └─ ← [Revert] Revert("failure") + └─ ← [Stop] + +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]]); +}); + +forgetest!(record_rw, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_vm(); + prj.insert_console(); + prj.add_source( + "Contracts.sol", + r#" + + pragma solidity ^0.8.18; + +contract RecordAccess { + function record(NestedRecordAccess target) public { + assembly { + sstore(1, add(sload(1), 1)) + } + + target.record(); + } +} + +contract NestedRecordAccess { + function record() public { + assembly { + sstore(2, add(sload(2), 1)) + } + } +} +"#, + ) + .unwrap(); + prj.add_source( + "Test.t.sol", + r#" + pragma solidity ^0.8.18; + import "./test.sol"; + import "./Vm.sol"; + import "./Contracts.sol"; + import {console} from "./console.sol"; +contract RecordTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + + function testRecordAccess() public { + RecordAccess target = new RecordAccess(); + NestedRecordAccess inner = new NestedRecordAccess(); + // Start recording + vm.record(); + target.record(inner); + + // Verify Records + (bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(target)); + (bytes32[] memory innerReads, bytes32[] memory innerWrites) = vm.accesses(address(inner)); + + assertEq(reads.length, 2, "number of reads is incorrect"); + assertEq(reads[0], bytes32(uint256(1)), "key for read 0 is incorrect"); + assertEq(reads[1], bytes32(uint256(1)), "key for read 1 is incorrect"); + + assertEq(writes.length, 1, "number of writes is incorrect"); + assertEq(writes[0], bytes32(uint256(1)), "key for write is incorrect"); + + assertEq(innerReads.length, 2, "number of nested reads is incorrect"); + assertEq(innerReads[0], bytes32(uint256(2)), "key for nested read 0 is incorrect"); + assertEq(innerReads[1], bytes32(uint256(2)), "key for nested read 1 is incorrect"); + + assertEq(innerWrites.length, 1, "number of nested writes is incorrect"); + assertEq(innerWrites[0], bytes32(uint256(2)), "key for nested write is incorrect"); + } + + function testStopRecordAccess() public { + RecordAccess target = new RecordAccess(); + NestedRecordAccess inner = new NestedRecordAccess(); + // Start recording + vm.record(); + target.record(inner); + + // Verify Records + (bytes32[] memory reads, bytes32[] memory writes) = vm.accesses(address(target)); + + assertEq(reads.length, 2, "number of reads is incorrect"); + assertEq(reads[0], bytes32(uint256(1)), "key for read 0 is incorrect"); + assertEq(reads[1], bytes32(uint256(1)), "key for read 1 is incorrect"); + + assertEq(writes.length, 1, "number of writes is incorrect"); + assertEq(writes[0], bytes32(uint256(1)), "key for write is incorrect"); + + vm.stopRecord(); + target.record(inner); + + // Verify that there are no new Records + (reads, writes) = vm.accesses(address(target)); + + assertEq(reads.length, 2, "number of reads is incorrect"); + assertEq(reads[0], bytes32(uint256(1)), "key for read 0 is incorrect"); + assertEq(reads[1], bytes32(uint256(1)), "key for read 1 is incorrect"); + + assertEq(writes.length, 1, "number of writes is incorrect"); + assertEq(writes[0], bytes32(uint256(1)), "key for write is incorrect"); + + vm.record(); + vm.stopRecord(); + + // verify reset all records + (reads, writes) = vm.accesses(address(target)); + + assertEq(reads.length, 0, "number of reads is incorrect"); + assertEq(writes.length, 0, "number of writes is incorrect"); + } +} + + "#, + ) + .unwrap(); + prj.update_config(|config| config.evm_version = EvmVersion::Cancun); + + let res = cmd.args(["test", "--resolc", "--resolc-startup", "-vvvvv"]).assert_success(); + res.stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +[COMPILING_FILES] with [RESOLC_VERSION] +[RESOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 2 tests for src/Test.t.sol:RecordTest +[PASS] testRecordAccess() ([GAS]) +Traces: + [961089406] RecordTest::testRecordAccess() + ├─ [16788608] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 4095 bytes of code + ├─ [16788608] → new @0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 2182 bytes of code + ├─ [0] VM::record() + │ └─ ← [Return] + ├─ [927440089] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::record(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f) + │ ├─ [0] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::record() + │ │ └─ ← [Return] + │ └─ ← [Stop] + ├─ [0] VM::accesses(0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC) + │ └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001], [0x0000000000000000000000000000000000000000000000000000000000000001] + ├─ [0] VM::accesses(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f) + │ └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000002], [0x0000000000000000000000000000000000000000000000000000000000000002] + └─ ← [Stop] + +[PASS] testStopRecordAccess() ([GAS]) +Traces: + [961093272] RecordTest::testStopRecordAccess() + ├─ [16788608] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 4095 bytes of code + ├─ [16788608] → new @0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 2182 bytes of code + ├─ [0] VM::record() + │ └─ ← [Return] + ├─ [927440089] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::record(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f) + │ ├─ [0] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::record() + │ │ └─ ← [Return] + │ └─ ← [Stop] + ├─ [0] VM::accesses(0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC) + │ └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001], [0x0000000000000000000000000000000000000000000000000000000000000001] + ├─ [0] VM::stopRecord() + │ └─ ← [Return] + ├─ [0] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::record(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f) + │ ├─ [0] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::record() + │ │ └─ ← [Return] + │ └─ ← [Stop] + ├─ [0] VM::accesses(0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC) + │ └─ ← [Return] [0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000001], [0x0000000000000000000000000000000000000000000000000000000000000001] + ├─ [0] VM::record() + │ └─ ← [Return] + ├─ [0] VM::stopRecord() + │ └─ ← [Return] + ├─ [0] VM::accesses(0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC) + │ └─ ← [Return] [], [] + └─ ← [Stop] + +Suite result: ok. 2 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) + +"#]]); +}); + +forgetest!(record_logs, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_vm(); + prj.insert_console(); + prj.add_source( + "Contracts.sol", + r#" + + pragma solidity ^0.8.18; + + contract Emitter { + event LogAnonymous(bytes data) anonymous; + + event LogTopic0(bytes data); + + event LogTopic1(uint256 indexed topic1, bytes data); + + event LogTopic12(uint256 indexed topic1, uint256 indexed topic2, bytes data); + + event LogTopic123(uint256 indexed topic1, uint256 indexed topic2, uint256 indexed topic3, bytes data); + + function emitAnonymousEvent(bytes memory data) public { + emit LogAnonymous(data); + } + + function emitEvent(bytes memory data) public { + emit LogTopic0(data); + } + + function emitEvent(uint256 topic1, bytes memory data) public { + emit LogTopic1(topic1, data); + } + + function emitEvent(uint256 topic1, uint256 topic2, bytes memory data) public { + emit LogTopic12(topic1, topic2, data); + } + + function emitEvent(uint256 topic1, uint256 topic2, uint256 topic3, bytes memory data) public { + emit LogTopic123(topic1, topic2, topic3, data); + } +} + +contract Emitterv2 { + Emitter emitter = new Emitter(); + + function emitEvent(uint256 topic1, uint256 topic2, uint256 topic3, bytes memory data) public { + emitter.emitEvent(topic1, topic2, topic3, data); + } + + function getEmitterAddr() public view returns (address) { + return address(emitter); + } +} +"#, + ) + .unwrap(); + prj.add_source( + "Test.t.sol", + r#" + pragma solidity ^0.8.18; + import "./test.sol"; + import "./Vm.sol"; + import "./Contracts.sol"; + import {console} from "./console.sol"; + contract RecordLogsTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + Emitter emitter; + bytes32 internal seedTestData = keccak256(abi.encodePacked("Some data")); + + // Used on testRecordOnEmitDifferentDepths() + event LogTopic(uint256 indexed topic1, bytes data); + + function setUp() public { + emitter = new Emitter(); + } + + function generateTestData(uint8 n) internal returns (bytes memory) { + bytes memory output = new bytes(n); + + for (uint8 i = 0; i < n; i++) { + output[i] = seedTestData[i % 32]; + if (i % 32 == 31) { + seedTestData = keccak256(abi.encodePacked(seedTestData)); + } + } + + return output; + } + + function testRecordOffGetsNothing() public { + emitter.emitEvent(1, 2, 3, generateTestData(48)); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 0); + } + + function testRecordOnNoLogs() public { + vm.recordLogs(); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 0); + } + + function testRecordOnSingleLog() public { + bytes memory testData = "Event Data in String"; + + vm.recordLogs(); + emitter.emitEvent(1, 2, 3, testData); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].topics.length, 4); + assertEq(entries[0].topics[0], keccak256("LogTopic123(uint256,uint256,uint256,bytes)")); + assertEq(entries[0].topics[1], bytes32(uint256(1))); + assertEq(entries[0].topics[2], bytes32(uint256(2))); + assertEq(entries[0].topics[3], bytes32(uint256(3))); + assertEq(abi.decode(entries[0].data, (string)), string(testData)); + assertEq(entries[0].emitter, address(emitter)); + } + + // TODO + // This crashes on decoding! + // The application panicked (crashed). + // Message: index out of bounds: the len is 0 but the index is 0 + // Location: /evm/src/trace/decoder.rs:299 + function NOtestRecordOnAnonymousEvent() public { + bytes memory testData = generateTestData(48); + + vm.recordLogs(); + emitter.emitAnonymousEvent(testData); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + } + + function testRecordOnSingleLogTopic0() public { + bytes memory testData = generateTestData(48); + + vm.recordLogs(); + emitter.emitEvent(testData); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1); + assertEq(entries[0].topics.length, 1); + assertEq(entries[0].topics[0], keccak256("LogTopic0(bytes)")); + // While not a proper string, this conversion allows the comparison. + assertEq(abi.decode(entries[0].data, (string)), string(testData)); + assertEq(entries[0].emitter, address(emitter)); + } + + function testEmitRecordEmit() public { + bytes memory testData0 = generateTestData(32); + emitter.emitEvent(1, 2, testData0); + + vm.recordLogs(); + bytes memory testData1 = generateTestData(16); + emitter.emitEvent(3, testData1); + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 1, "entries length"); + assertEq(entries[0].topics.length, 2); + assertEq(entries[0].topics[0], keccak256("LogTopic1(uint256,bytes)")); + assertEq(entries[0].topics[1], bytes32(uint256(3))); + assertEq(abi.decode(entries[0].data, (string)), string(testData1)); + assertEq(entries[0].emitter, address(emitter)); + } + + function testRecordOnEmitDifferentDepths() public { + vm.recordLogs(); + + bytes memory testData0 = generateTestData(16); + emit LogTopic(1, testData0); + + bytes memory testData1 = generateTestData(20); + emitter.emitEvent(2, 3, testData1); + + bytes memory testData2 = generateTestData(24); + Emitterv2 emitter2 = new Emitterv2(); + emitter2.emitEvent(4, 5, 6, testData2); + + Vm.Log[] memory entries = vm.getRecordedLogs(); + + assertEq(entries.length, 3); + + assertEq(entries[0].topics.length, 2); + assertEq(entries[0].topics[0], keccak256("LogTopic(uint256,bytes)")); + assertEq(entries[0].topics[1], bytes32(uint256(1))); + assertEq(abi.decode(entries[0].data, (string)), string(testData0)); + assertEq(entries[0].emitter, address(this)); + + assertEq(entries[1].topics.length, 3); + assertEq(entries[1].topics[0], keccak256("LogTopic12(uint256,uint256,bytes)")); + assertEq(entries[1].topics[1], bytes32(uint256(2))); + assertEq(entries[1].topics[2], bytes32(uint256(3))); + assertEq(abi.decode(entries[1].data, (string)), string(testData1)); + assertEq(entries[1].emitter, address(emitter)); + + assertEq(entries[2].topics.length, 4); + assertEq(entries[2].topics[0], keccak256("LogTopic123(uint256,uint256,uint256,bytes)")); + assertEq(entries[2].topics[1], bytes32(uint256(4))); + assertEq(entries[2].topics[2], bytes32(uint256(5))); + assertEq(entries[2].topics[3], bytes32(uint256(6))); + assertEq(abi.decode(entries[2].data, (string)), string(testData2)); + assertEq(entries[2].emitter, emitter2.getEmitterAddr()); + } + + function testRecordsConsumednAsRead() public { + Vm.Log[] memory entries; + + emitter.emitEvent(1, generateTestData(16)); + + // hit record now + vm.recordLogs(); + + entries = vm.getRecordedLogs(); + assertEq(entries.length, 0); + + // emit after calling .getRecordedLogs() + emitter.emitEvent(2, 3, generateTestData(24)); + + entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + assertEq(entries[0].topics.length, 3); + assertEq(entries[0].emitter, address(emitter)); + + // let's emit two more! + emitter.emitEvent(4, 5, 6, generateTestData(20)); + emitter.emitEvent(generateTestData(32)); + + entries = vm.getRecordedLogs(); + assertEq(entries.length, 2); + assertEq(entries[0].topics.length, 4); + assertEq(entries[1].topics.length, 1); + assertEq(entries[0].emitter, address(emitter)); + assertEq(entries[1].emitter, address(emitter)); + + // the last one + emitter.emitEvent(7, 8, 9, generateTestData(24)); + + entries = vm.getRecordedLogs(); + assertEq(entries.length, 1); + assertEq(entries[0].topics.length, 4); + assertEq(entries[0].emitter, address(emitter)); + } + } + "#, + ) + .unwrap(); + prj.update_config(|config| config.evm_version = EvmVersion::Cancun); + + let res = cmd.args(["test", "--resolc", "--resolc-startup", "-vvvvv"]).assert_success(); + res.stderr_eq("").stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +[COMPILING_FILES] with [RESOLC_VERSION] +[RESOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 7 tests for src/Test.t.sol:RecordLogsTest +[PASS] testEmitRecordEmit() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [357757177] RecordLogsTest::testEmitRecordEmit() + ├─ [183812741] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(1, 2, 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350) + │ ├─ emit LogTopic12(topic1: 1, topic2: 2, data: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350) + │ └─ ← [Stop] + ├─ [0] VM::recordLogs() + │ └─ ← [Return] + ├─ [173888857] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(3, 0x2e38edeff9493e0004540e975027a429) + │ ├─ emit LogTopic1(topic1: 3, data: 0x2e38edeff9493e0004540e975027a429) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0x7c7d81fafce31d4330303f05da0ccb9d970101c475382b40aa072986ee4caaad, 0x0000000000000000000000000000000000000000000000000000000000000003], 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000102e38edeff9493e0004540e975027a42900000000000000000000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC)] + ├─ storage changes: + │ @ 1: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350 → 0x2e38edeff9493e0004540e975027a429ee666d1289f2c7a4232d03ee63e14e30 + └─ ← [Stop] + +[PASS] testRecordOffGetsNothing() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [202674284] RecordLogsTest::testRecordOffGetsNothing() + ├─ [202625294] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(1, 2, 3, 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c1693502e38edeff9493e0004540e975027a429) + │ ├─ emit LogTopic123(topic1: 1, topic2: 2, topic3: 3, data: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c1693502e38edeff9493e0004540e975027a429) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [] + ├─ storage changes: + │ @ 1: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350 → 0x2e38edeff9493e0004540e975027a429ee666d1289f2c7a4232d03ee63e14e30 + └─ ← [Stop] + +[PASS] testRecordOnEmitDifferentDepths() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [999237291] RecordLogsTest::testRecordOnEmitDifferentDepths() + ├─ [0] VM::recordLogs() + │ └─ ← [Return] + ├─ emit LogTopic(topic1: 1, data: 0x43a26051362b8040b289abe93334a5e3) + ├─ [180758801] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(2, 3, 0x43a26051362b8040b289abe93334a5e3662751aa) + │ ├─ emit LogTopic12(topic1: 2, topic2: 3, data: 0x43a26051362b8040b289abe93334a5e3662751aa) + │ └─ ← [Stop] + ├─ [818371229] → new @0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 10554 bytes of code + ├─ [0] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::emitEvent(4, 5, 6, 0x43a26051362b8040b289abe93334a5e3662751aa691185ae) + │ ├─ [0] 0x104fBc016F4bb334D775a19E8A6510109AC63E00::emitEvent(4, 5, 6, 0x43a26051362b8040b289abe93334a5e3662751aa691185ae) + │ │ ├─ emit LogTopic123(topic1: 4, topic2: 5, topic3: 6, data: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae) + │ │ └─ ← [Return] + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0x61fb7db3625c10432927a76bb32400c33a94e9bb6374137c4cd59f6e465bfdcb, 0x0000000000000000000000000000000000000000000000000000000000000001], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001043a26051362b8040b289abe93334a5e300000000000000000000000000000000, 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496), ([0x7af92d5e3102a27d908bb1859fdef71b723f3c438e5d84f3af49dab68e18dc6d, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000003], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001443a26051362b8040b289abe93334a5e3662751aa000000000000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC), ([0xb6d650e5d0bbc0e92ff784e346ada394e49aa2d74a5cee8b099fa1a469bdc452, 0x0000000000000000000000000000000000000000000000000000000000000004, 0x0000000000000000000000000000000000000000000000000000000000000005, 0x0000000000000000000000000000000000000000000000000000000000000006], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001843a26051362b8040b289abe93334a5e3662751aa691185ae0000000000000000, 0x104fBc016F4bb334D775a19E8A6510109AC63E00)] + ├─ [0] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::getEmitterAddr() [staticcall] + │ └─ ← [Return] 0x104fBc016F4bb334D775a19E8A6510109AC63E00 + └─ ← [Stop] + +[PASS] testRecordOnNoLogs() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [4118] RecordLogsTest::testRecordOnNoLogs() + ├─ [0] VM::recordLogs() + │ └─ ← [Return] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [] + └─ ← [Stop] + +[PASS] testRecordOnSingleLog() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [187093023] RecordLogsTest::testRecordOnSingleLog() + ├─ [0] VM::recordLogs() + │ └─ ← [Return] + ├─ [187077066] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(1, 2, 3, 0x4576656e74204461746120696e20537472696e67) + │ ├─ emit LogTopic123(topic1: 1, topic2: 2, topic3: 3, data: 0x4576656e74204461746120696e20537472696e67) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0xb6d650e5d0bbc0e92ff784e346ada394e49aa2d74a5cee8b099fa1a469bdc452, 0x0000000000000000000000000000000000000000000000000000000000000001, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000003], 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000144576656e74204461746120696e20537472696e67000000000000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC)] + └─ ← [Stop] + +[PASS] testRecordOnSingleLogTopic0() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [184656340] RecordLogsTest::testRecordOnSingleLogTopic0() + ├─ [0] VM::recordLogs() + │ └─ ← [Return] + ├─ [184603101] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c1693502e38edeff9493e0004540e975027a429) + │ ├─ emit LogTopic0(data: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c1693502e38edeff9493e0004540e975027a429) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0x0a28c6fad56bcbad1788721e440963b3b762934a3134924733eaf8622cb44279], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003043a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c1693502e38edeff9493e0004540e975027a42900000000000000000000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC)] + ├─ storage changes: + │ @ 1: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350 → 0x2e38edeff9493e0004540e975027a429ee666d1289f2c7a4232d03ee63e14e30 + └─ ← [Stop] + +[PASS] testRecordsConsumednAsRead() ([GAS]) +Traces: + [16868742] RecordLogsTest::setUp() + ├─ [16830999] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 12583 bytes of code + └─ ← [Stop] + + [903065419] RecordLogsTest::testRecordsConsumednAsRead() + ├─ [173888857] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(1, 0x43a26051362b8040b289abe93334a5e3) + │ ├─ emit LogTopic1(topic1: 1, data: 0x43a26051362b8040b289abe93334a5e3) + │ └─ ← [Stop] + ├─ [0] VM::recordLogs() + │ └─ ← [Return] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [] + ├─ [181776781] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(2, 3, 0x43a26051362b8040b289abe93334a5e3662751aa691185ae) + │ ├─ emit LogTopic12(topic1: 2, topic2: 3, data: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0x7af92d5e3102a27d908bb1859fdef71b723f3c438e5d84f3af49dab68e18dc6d, 0x0000000000000000000000000000000000000000000000000000000000000002, 0x0000000000000000000000000000000000000000000000000000000000000003], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001843a26051362b8040b289abe93334a5e3662751aa691185ae0000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC)] + ├─ [187077066] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(4, 5, 6, 0x43a26051362b8040b289abe93334a5e3662751aa) + │ ├─ emit LogTopic123(topic1: 4, topic2: 5, topic3: 6, data: 0x43a26051362b8040b289abe93334a5e3662751aa) + │ └─ ← [Stop] + ├─ [172108813] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350) + │ ├─ emit LogTopic0(data: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0xb6d650e5d0bbc0e92ff784e346ada394e49aa2d74a5cee8b099fa1a469bdc452, 0x0000000000000000000000000000000000000000000000000000000000000004, 0x0000000000000000000000000000000000000000000000000000000000000005, 0x0000000000000000000000000000000000000000000000000000000000000006], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001443a26051362b8040b289abe93334a5e3662751aa000000000000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC), ([0x0a28c6fad56bcbad1788721e440963b3b762934a3134924733eaf8622cb44279], 0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000002043a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC)] + ├─ [188095046] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::emitEvent(7, 8, 9, 0x2e38edeff9493e0004540e975027a429ee666d1289f2c7a4) + │ ├─ emit LogTopic123(topic1: 7, topic2: 8, topic3: 9, data: 0x2e38edeff9493e0004540e975027a429ee666d1289f2c7a4) + │ └─ ← [Stop] + ├─ [0] VM::getRecordedLogs() + │ └─ ← [Return] [([0xb6d650e5d0bbc0e92ff784e346ada394e49aa2d74a5cee8b099fa1a469bdc452, 0x0000000000000000000000000000000000000000000000000000000000000007, 0x0000000000000000000000000000000000000000000000000000000000000008, 0x0000000000000000000000000000000000000000000000000000000000000009], 0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000182e38edeff9493e0004540e975027a429ee666d1289f2c7a40000000000000000, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC)] + ├─ storage changes: + │ @ 1: 0x43a26051362b8040b289abe93334a5e3662751aa691185ae9e9a2e1e0c169350 → 0x2e38edeff9493e0004540e975027a429ee666d1289f2c7a4232d03ee63e14e30 + └─ ← [Stop] + +Suite result: ok. 7 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 7 tests passed, 0 failed, 0 skipped (7 total tests) + +"#]]); +}); + +forgetest!(record_accesses, |prj, cmd| { + prj.insert_ds_test(); + prj.insert_vm(); + prj.insert_console(); + + prj.add_source( + "Contracts.sol", + r#" + contract C { + uint256 internal _reserved; + uint256 public data; + constructor(uint _data) payable { data = _data; } + function setter(uint _data) public { data = _data; } + } + + contract Proxy { + address target; + constructor(address _data) payable { target = _data; } + function proxyCall(uint _data) public { + (bool success,) = address(target).call(abi.encodeWithSelector(C.setter.selector, _data)); + if (!success) { + assert(false); + } + } + } +"#, + ) + .unwrap(); + prj.add_source( + "Test.t.sol", + r#" + pragma solidity ^0.8.18; + import "./test.sol"; + import "./Vm.sol"; + import "./Contracts.sol"; + import {console} from "./console.sol"; + + contract StateDiffTest is DSTest { + Vm constant vm = Vm(HEVM_ADDRESS); + address existing; + address proxy; + + function setUp() public { + existing = address(new C{value: 1 ether}(100)); + proxy = address(new Proxy(existing)); + } + + function testCreateaccesses() public { + vm.startStateDiffRecording(); + C target = new C{value: 1 ether}(100); + Vm.AccountAccess[] memory records = vm.stopAndReturnStateDiff(); + assertEq(records.length, 1, "Records"); + assertEq(records[0].account, address(target), "Account"); + assertEq(records[0].accessor, address(this), "Accessor"); + assertEq(records[0].initialized, true); + assertEq(records[0].oldBalance, 0, "oldBalance"); + assertEq(records[0].newBalance, 1 ether, "newBalance"); + assertEq(records[0].value, 1 ether, "value"); + assertEq(records[0].data, abi.encode(uint(100)), "data"); + assertEq(records[0].reverted, false); + + assertEq(records[0].storageAccesses.length, 2, "accesses"); // check the write + assertEq(records[0].storageAccesses[1].account, address(target), "access address"); + assertEq(records[0].storageAccesses[1].slot, bytes32(uint256(1)), "slot"); + assertEq(records[0].storageAccesses[1].isWrite, true); + assertEq(records[0].storageAccesses[1].previousValue, bytes32(uint(0)), "previousValue"); + assertEq(records[0].storageAccesses[1].newValue, bytes32(uint(100)), "newValue"); + assertEq(records[0].storageAccesses[1].reverted, false); + } + + function testCallaccesses() public { + vm.startStateDiffRecording(); + (bool success,) = address(existing).call(abi.encodeWithSelector(C.setter.selector, 55)); + if (!success) { + assert(false); + } + Vm.AccountAccess[] memory records = vm.stopAndReturnStateDiff(); + assertEq(records.length, 1, "records"); + assertEq(records[0].account, address(existing), "Account"); + assertEq(records[0].accessor, address(this), "Accessor"); + assertEq(records[0].initialized, true); + assertEq(records[0].oldBalance, 1 ether, "oldBalance"); + assertEq(records[0].newBalance, 1 ether, "newBalance"); + assertEq(records[0].value, 0 ether, "value"); + assertEq(records[0].data, abi.encodeWithSelector(C.setter.selector, 55), "data"); + assertEq(records[0].reverted, false); + + assertEq(records[0].storageAccesses.length, 2, "accesses"); // check the write + assertEq(records[0].storageAccesses[1].account, address(existing), "access address"); + assertEq(records[0].storageAccesses[1].slot, bytes32(uint256(1)), "slot"); + assertEq(records[0].storageAccesses[1].isWrite, true); + assertEq(records[0].storageAccesses[1].previousValue, bytes32(uint(100)), "previousValue"); + assertEq(records[0].storageAccesses[1].newValue, bytes32(uint(55)), "newValue"); + assertEq(records[0].storageAccesses[1].reverted, false); + } + function testCallProxyaccesses() public { + vm.startStateDiffRecording(); + (bool success,) = address(proxy).call(abi.encodeWithSelector(Proxy.proxyCall.selector, 55)); + if (!success) { + assert(false); + } + Vm.AccountAccess[] memory records = vm.stopAndReturnStateDiff(); + assertEq(records.length, 2, "records"); + assertEq(records[1].account, address(existing), "Account"); // checks the access from Proxy to C + assertEq(records[1].accessor, address(proxy), "Accessor"); + assertEq(records[1].initialized, true); + assertEq(records[1].oldBalance, 1 ether, "oldBalance"); + assertEq(records[1].newBalance, 1 ether, "newBalance"); + assertEq(records[1].value, 0 ether, "value"); + assertEq(records[1].data, abi.encodeWithSelector(C.setter.selector, 55), "data"); + assertEq(records[1].reverted, false); + + assertEq(records[1].storageAccesses.length, 2, "accesses"); // check the write + assertEq(records[1].storageAccesses[1].account, address(existing), "access address"); + assertEq(records[1].storageAccesses[1].slot, bytes32(uint256(1)), "slot"); + assertEq(records[1].storageAccesses[1].isWrite, true); + assertEq(records[1].storageAccesses[1].previousValue, bytes32(uint(100)), "previousValue"); + assertEq(records[1].storageAccesses[1].newValue, bytes32(uint(55)), "newValue"); + assertEq(records[1].storageAccesses[1].reverted, false); + } + } + "#, + ) + .unwrap(); + prj.update_config(|config| config.evm_version = EvmVersion::Cancun); + + let res = cmd.args(["test", "--resolc", "--resolc-startup", "-vvvvv"]).assert_success(); + res.stdout_eq(str![[r#" +[COMPILING_FILES] with [SOLC_VERSION] +[SOLC_VERSION] [ELAPSED] +Compiler run successful! +[COMPILING_FILES] with [RESOLC_VERSION] +[RESOLC_VERSION] [ELAPSED] +Compiler run successful! + +Ran 3 tests for src/Test.t.sol:StateDiffTest +[PASS] testCallProxyaccesses() ([GAS]) +Traces: + [585251161] StateDiffTest::setUp() + ├─ [292049387] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 5531 bytes of code + ├─ [293109162] → new @0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 6405 bytes of code + └─ ← [Stop] + + [728077974] StateDiffTest::testCallProxyaccesses() + ├─ [0] VM::startStateDiffRecording() + │ └─ ← [Return] + ├─ [728040641] 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f::proxyCall(55) + │ ├─ [0] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::setter(55) + │ │ └─ ← [Return] + │ └─ ← [Stop] + ├─ [0] VM::stopAndReturnStateDiff() + │ └─ ← [Return] [((0, 31337 [3.133e4]), 0, 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, true, 0, 1000000000000000000 [1e18], 0x, 0, 0xac1b14ff0000000000000000000000000000000000000000000000000000000000000037, false, [(0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, 0x0000000000000000000000000000000000000000000000000000000000000000, false, 0x0000000000000000000000007d8cb8f412b3ee9ac79558791333f41d2b1ccdac, 0x0000000000000000000000007d8cb8f412b3ee9ac79558791333f41d2b1ccdac, false)], 1), ((0, 31337 [3.133e4]), 0, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC, 0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f, true, 1000000000000000000 [1e18], 1000000000000000000 [1e18], 0x, 0, 0xd423740b0000000000000000000000000000000000000000000000000000000000000037, false, [(0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC, 0x0000000000000000000000000000000000000000000000000000000000000001, false, 0x0000000000000000000000000000000000000000000000000000000000000064, 0x0000000000000000000000000000000000000000000000000000000000000064, false), (0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC, 0x0000000000000000000000000000000000000000000000000000000000000001, true, 0x0000000000000000000000000000000000000000000000000000000000000064, 0x0000000000000000000000000000000000000000000000000000000000000037, false)], 2)] + └─ ← [Stop] + +[PASS] testCallaccesses() ([GAS]) +Traces: + [585251161] StateDiffTest::setUp() + ├─ [292049387] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 5531 bytes of code + ├─ [293109162] → new @0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 6405 bytes of code + └─ ← [Stop] + + [276825754] StateDiffTest::testCallaccesses() + ├─ [0] VM::startStateDiffRecording() + │ └─ ← [Return] + ├─ [276796934] 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC::setter(55) + │ └─ ← [Stop] + ├─ [0] VM::stopAndReturnStateDiff() + │ └─ ← [Return] [((0, 31337 [3.133e4]), 0, 0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC, 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, true, 1000000000000000000 [1e18], 1000000000000000000 [1e18], 0x, 0, 0xd423740b0000000000000000000000000000000000000000000000000000000000000037, false, [(0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC, 0x0000000000000000000000000000000000000000000000000000000000000001, false, 0x0000000000000000000000000000000000000000000000000000000000000064, 0x0000000000000000000000000000000000000000000000000000000000000064, false), (0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC, 0x0000000000000000000000000000000000000000000000000000000000000001, true, 0x0000000000000000000000000000000000000000000000000000000000000064, 0x0000000000000000000000000000000000000000000000000000000000000037, false)], 1)] + └─ ← [Stop] + +[PASS] testCreateaccesses() ([GAS]) +Traces: + [585251161] StateDiffTest::setUp() + ├─ [292049387] → new @0x7D8CB8F412B3ee9AC79558791333F41d2b1ccDAC + │ └─ ← [Return] 5531 bytes of code + ├─ [293109162] → new @0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ └─ ← [Return] 6405 bytes of code + └─ ← [Stop] + + [292103665] StateDiffTest::testCreateaccesses() + ├─ [0] VM::startStateDiffRecording() + │ └─ ← [Return] + ├─ [292049387] → new @0x2e234DAe75C793f67A35089C9d99245E1C58470b + │ └─ ← [Return] 5531 bytes of code + ├─ [0] VM::stopAndReturnStateDiff() + │ └─ ← [Return] [((0, 31337 [3.133e4]), 4, 0x2e234DAe75C793f67A35089C9d99245E1C58470b, 0x7FA9385bE102ac3EAc297483Dd6233D62b3e1496, true, 0, 1000000000000000000 [1e18], 0x, 1000000000000000000 [1e18], 0x0000000000000000000000000000000000000000000000000000000000000064, false, [(0x2e234DAe75C793f67A35089C9d99245E1C58470b, 0x0000000000000000000000000000000000000000000000000000000000000001, false, 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000000, false), (0x2e234DAe75C793f67A35089C9d99245E1C58470b, 0x0000000000000000000000000000000000000000000000000000000000000001, true, 0x0000000000000000000000000000000000000000000000000000000000000000, 0x0000000000000000000000000000000000000000000000000000000000000064, false)], 1)] + └─ ← [Stop] + +Suite result: ok. 3 passed; 0 failed; 0 skipped; [ELAPSED] + +Ran 1 test suite [ELAPSED]: 3 tests passed, 0 failed, 0 skipped (3 total tests) + +"#]]); +}); diff --git a/crates/revive-strategy/src/cheatcodes/mod.rs b/crates/revive-strategy/src/cheatcodes/mod.rs index 60dce509ffaee..1d6472c566b24 100644 --- a/crates/revive-strategy/src/cheatcodes/mod.rs +++ b/crates/revive-strategy/src/cheatcodes/mod.rs @@ -1,4 +1,4 @@ -use alloy_primitives::{Address, B256, Bytes, hex, ruint::aliases::U256}; +use alloy_primitives::{Address, B256, Bytes, Log, hex, ruint::aliases::U256}; use alloy_rpc_types::BlobTransactionSidecar; use alloy_sol_types::SolValue; use foundry_cheatcodes::{ @@ -19,20 +19,26 @@ use std::{ fmt::Debug, sync::Arc, }; +use tracing::warn; use polkadot_sdk::{ frame_support::traits::{Currency, fungible::Mutate}, pallet_balances, pallet_revive::{ self, AccountInfo, AddressMapper, BalanceOf, BalanceWithDust, Code, Config, ContractInfo, - ExecConfig, Pallet, + ExecConfig, Pallet, evm::CallTrace, }, polkadot_sdk_frame::prelude::OriginFor, sp_core::{self, H160}, sp_weights::Weight, }; -use crate::{execute_with_externalities, trace, tracing::apply_prestate_trace}; +use crate::{ + execute_with_externalities, + tracing::{Tracer, storage_tracer::AccountAccess}, +}; +use foundry_cheatcodes::Vm::{AccountAccess as FAccountAccess, ChainInfo}; + use alloy_eips::eip7702::SignedAuthorization; use revm::{ bytecode::opcode as op, @@ -98,6 +104,7 @@ pub struct PvmCheatcodeInspectorStrategyContext { /// Controls automatic migration to PVM mode pub pvm_startup_migration: PvmStartupMigration, pub dual_compiled_contracts: DualCompiledContracts, + pub remove_recorded_access_at: Option, } impl PvmCheatcodeInspectorStrategyContext { @@ -110,6 +117,7 @@ impl PvmCheatcodeInspectorStrategyContext { PvmStartupMigration::Done // Disabled - never migrate }, dual_compiled_contracts, + remove_recorded_access_at: None, } } } @@ -204,6 +212,96 @@ fn set_timestamp(new_timestamp: U256, ecx: Ecx<'_, '_, '_>) { #[derive(Debug, Default, Clone)] pub struct PvmCheatcodeInspectorStrategyRunner; +impl PvmCheatcodeInspectorStrategyRunner { + fn append_recorded_accesses( + &self, + state: &mut foundry_cheatcodes::Cheatcodes, + ecx: Ecx<'_, '_, '_>, + account_accesses: Vec, + ) { + if state.recording_accesses { + for record in &account_accesses { + for r in &record.storage_accesses { + if !r.isWrite { + state.accesses.record_read( + Address::from(record.account.0), + alloy_primitives::U256::from_be_slice(r.slot.clone().as_slice()), + ); + } else { + state.accesses.record_write( + Address::from(record.account.0), + alloy_primitives::U256::from_be_slice(r.slot.clone().as_slice()), + ); + } + } + } + } + + if let Some(recorded_account_diffs_stack) = state.recorded_account_diffs_stack.as_mut() { + // A duplicate entry is inserted on call/create start by the revm, and updated on + // call/create end. + // + // If we are inside a nested call (stack depth > 1), the placeholder + // lives in the *parent* frame. Its index will be exactly the current + // length of that parent vector (`len()`), so we record that length. + // + // If we are at the root (depth == 1), the placeholder is already the + // last element of the root vector. We therefore record `len() - 1`. + // + // `zksync_fix_recorded_accesses()` uses this index later to drop the + // single duplicate. + // + // TODO(zk): This is currently a hack, as account access recording is + // done in 4 parts - create/create_end and call/call_end. And these must all be + // moved to strategy. + + let stack_insert_index = if recorded_account_diffs_stack.len() > 1 { + recorded_account_diffs_stack + .get(recorded_account_diffs_stack.len() - 2) + .map_or(0, Vec::len) + } else { + // `len() - 1` + recorded_account_diffs_stack.first().map_or(0, |v| v.len().saturating_sub(1)) + }; + + if let Some(last) = recorded_account_diffs_stack.last_mut() { + let ctx = get_context_ref_mut(state.strategy.context.as_mut()); + ctx.remove_recorded_access_at = Some(stack_insert_index); + for record in account_accesses { + let access = FAccountAccess { + chainInfo: ChainInfo { + forkId: ecx + .journaled_state + .database + .active_fork_id() + .unwrap_or_default(), + chainId: U256::from(ecx.cfg.chain_id), + }, + accessor: Address::from(record.accessor.0), + account: Address::from(record.account.0), + kind: record.kind, + initialized: true, + oldBalance: U256::from_limbs(record.old_balance.0), + newBalance: U256::from_limbs(record.new_balance.0), + value: U256::from_limbs(record.value.0), + data: record.data, + reverted: false, + deployedCode: if record.deployed_bytecode_hash.unwrap_or_default().is_zero() + { + Default::default() + } else { + Bytes::from(record.deployed_bytecode_hash.unwrap_or_default().0) + }, + storageAccesses: record.storage_accesses, + depth: record.depth, + }; + last.push(access); + } + } + } + } +} + impl CheatcodeInspectorStrategyRunner for PvmCheatcodeInspectorStrategyRunner { fn apply_full( &self, @@ -607,7 +705,7 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector state: &mut foundry_cheatcodes::Cheatcodes, ecx: Ecx<'_, '_, '_>, input: &dyn CommonCreateInput, - _executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor, + executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor, ) -> Option { let ctx = get_context_ref_mut(state.strategy.context.as_mut()); @@ -648,11 +746,11 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector .unwrap_or_else(|| panic!("failed finding contract for {init_code:?}")); let constructor_args = find_contract.constructor_args(); - let contract = find_contract.contract(); - - let (res, _call_trace, prestate_trace) = execute_with_externalities(|externalities| { + let contract = find_contract.contract().clone(); + let mut tracer = Tracer::new(true); + let res = execute_with_externalities(|externalities| { externalities.execute_with(|| { - trace::(|| { + tracer.trace(|| { let origin = OriginFor::::signed(AccountId::to_fallback_account_id( &H160::from_slice(input.caller().as_slice()), )); @@ -688,8 +786,11 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector }); let mut gas = Gas::new(input.gas_limit()); - - let result = match &res.result { + if res.result.as_ref().is_ok_and(|r| !r.result.did_revert()) { + self.append_recorded_accesses(state, ecx, tracer.get_recorded_accesses()); + } + post_exec(state, ecx, executor, &mut tracer, false); + match &res.result { Ok(result) => { let _ = gas.record_cost(res.gas_required.ref_time()); @@ -706,7 +807,7 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector CreateOutcome { result: InterpreterResult { result: InstructionResult::Return, - output: contract.resolc_bytecode.as_bytes().unwrap().clone(), + output: contract.resolc_bytecode.as_bytes().unwrap().to_owned(), gas, }, address: Some(Address::from_slice(result.addr.as_bytes())), @@ -728,11 +829,7 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector address: None, }) } - }; - - apply_prestate_trace(prestate_trace, ecx); - - result + } } /// Try handling the `CALL` within PVM. @@ -744,7 +841,7 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector state: &mut foundry_cheatcodes::Cheatcodes, ecx: Ecx<'_, '_, '_>, call: &CallInputs, - _executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor, + executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor, ) -> Option { let ctx = get_context_ref_mut(state.strategy.context.as_mut()); @@ -767,13 +864,14 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector } tracing::info!("running call in PVM {:#?}", call); - - let (res, _call_trace, prestate_trace) = execute_with_externalities(|externalities| { + let mut tracer = Tracer::new(true); + let res = execute_with_externalities(|externalities| { externalities.execute_with(|| { - trace::(|| { + tracer.trace(|| { let origin = OriginFor::::signed(AccountId::to_fallback_account_id( &H160::from_slice(call.caller.as_slice()), )); + let evm_value = sp_core::U256::from_little_endian(&call.call_value().as_le_bytes()); @@ -794,12 +892,16 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector }); let mut gas = Gas::new(call.gas_limit); - let result = match res.result { + if res.result.as_ref().is_ok_and(|r| !r.did_revert()) { + self.append_recorded_accesses(state, ecx, tracer.get_recorded_accesses()); + } + post_exec(state, ecx, executor, &mut tracer, call.is_static); + match res.result { Ok(result) => { let _ = gas.record_cost(res.gas_required.ref_time()); let outcome = if result.did_revert() { - tracing::error!("Contract call reverted"); + tracing::info!("Contract call reverted"); CallOutcome { result: InterpreterResult { result: InstructionResult::Revert, @@ -808,6 +910,15 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector }, memory_offset: call.return_memory_offset.clone(), } + } else if result.data.is_empty() { + CallOutcome { + result: InterpreterResult { + result: InstructionResult::Stop, + output: result.data.into(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + } } else { CallOutcome { result: InterpreterResult { @@ -834,11 +945,74 @@ impl foundry_cheatcodes::CheatcodeInspectorStrategyExt for PvmCheatcodeInspector memory_offset: call.return_memory_offset.clone(), }) } + } + } + + fn revive_remove_duplicate_account_access(&self, state: &mut foundry_cheatcodes::Cheatcodes) { + let ctx = get_context_ref_mut(state.strategy.context.as_mut()); + + if let Some(index) = ctx.remove_recorded_access_at.take() + && let Some(recorded_account_diffs_stack) = state.recorded_account_diffs_stack.as_mut() + && let Some(last) = recorded_account_diffs_stack.last_mut() + { + // This entry has been inserted during CREATE/CALL operations in revm's + // cheatcode inspector and must be removed. + if index < last.len() { + let _ = last.remove(index); + } else { + warn!(index, len = last.len(), "skipping duplicate access removal: out of bounds"); + } + } + } +} + +fn post_exec( + state: &mut foundry_cheatcodes::Cheatcodes, + ecx: Ecx<'_, '_, '_>, + executor: &mut dyn foundry_cheatcodes::CheatcodesExecutor, + tracer: &mut Tracer, + is_static_call: bool, +) { + tracer.apply_prestate_trace(ecx); + if let Some(traces) = tracer.collect_call_traces() + && !is_static_call + { + let mut logs = vec![]; + if !state.expected_emits.is_empty() || state.recorded_logs.is_some() { + collect_logs(&mut logs, &traces); + } + if !state.expected_emits.is_empty() { + logs.clone().into_iter().for_each(|log| { + foundry_cheatcodes::handle_expect_emit(state, &log, &mut Default::default()); + }) + } + if let Some(records) = &mut state.recorded_logs { + records.extend(logs.iter().map(|log| foundry_cheatcodes::Vm::Log { + data: log.data.data.clone(), + emitter: log.address, + topics: log.topics().to_owned(), + })); }; + executor.trace_revive(state, ecx, Box::new(traces)); + } - apply_prestate_trace(prestate_trace, ecx); + if let Some(expected_revert) = &mut state.expected_revert { + expected_revert.max_depth = + std::cmp::max(ecx.journaled_state.depth() + 1, expected_revert.max_depth); + } +} - result +fn collect_logs(accumulator: &mut Vec, trace: &CallTrace) { + accumulator.extend(trace.logs.iter().map(|log| { + let log = log.clone(); + Log::new_unchecked( + Address::from(log.address.0), + log.topics.iter().map(|x| U256::from_be_slice(x.as_bytes()).into()).collect(), + Bytes::from(log.data.0), + ) + })); + for call in &trace.calls { + collect_logs(accumulator, call); } } diff --git a/crates/revive-strategy/src/lib.rs b/crates/revive-strategy/src/lib.rs index a89caa796c226..82a1e7e845e19 100644 --- a/crates/revive-strategy/src/lib.rs +++ b/crates/revive-strategy/src/lib.rs @@ -21,7 +21,7 @@ mod cheatcodes; mod executor; mod tracing; -pub use tracing::trace; +pub use cheatcodes::PvmStartupMigration; /// Create Revive strategy for [ExecutorStrategy]. pub trait ReviveExecutorStrategyBuilder { diff --git a/crates/revive-strategy/src/tracing/mod.rs b/crates/revive-strategy/src/tracing/mod.rs index 1ecef23da4df8..ca8fe5c47e850 100644 --- a/crates/revive-strategy/src/tracing/mod.rs +++ b/crates/revive-strategy/src/tracing/mod.rs @@ -1,79 +1,246 @@ use alloy_primitives::{Address, Bytes, U256 as RU256}; use foundry_cheatcodes::Ecx; use polkadot_sdk::pallet_revive::{ - Config, Pallet, U256, + Pallet, U256, Weight, evm::{ - CallTrace, PrestateTrace, PrestateTraceInfo, PrestateTracer, PrestateTracerConfig, Tracer, - TracerType, + CallTrace, CallTracer, PrestateTrace, PrestateTraceInfo, PrestateTracer, + PrestateTracerConfig, Tracer as ReviveTracer, TracerType, }, - tracing::trace as trace_revive, + tracing::{Tracing, trace as trace_revive}, }; -use revm::{context::JournalTr, state::Bytecode}; - -// Traces the execution inside pallet_revive. -// This is a temporary solution to the fact that custom Tracer is not implementable for the time -// being. -pub fn trace R>(f: F) -> (R, Option>, PrestateTrace) { - let mut call_tracer = - match Pallet::::evm_tracer(TracerType::CallTracer(None)) { - Tracer::CallTracer(tracer) => tracer, - _ => unreachable!("Expected CallTracer variant"), - }; - let mut prestate_tracer: PrestateTracer = - PrestateTracer::new(PrestateTracerConfig { - diff_mode: true, - disable_storage: false, - disable_code: false, - }); - - let result = trace_revive(&mut prestate_tracer, || trace_revive(&mut call_tracer, f)); - let prestate_trace = prestate_tracer.collect_trace(); - let calls = call_tracer.collect_trace(); - (result, calls, prestate_trace) +use revive_env::Runtime; +use revm::{context::JournalTr, database::states::StorageSlot, state::Bytecode}; +use storage_tracer::{AccountAccess, StorageTracer}; +pub mod storage_tracer; +use crate::execute_with_externalities; + +pub struct Tracer { + pub call_tracer: CallTracer U256>, + pub prestate_tracer: PrestateTracer, + pub storage_accesses: Option, } -/// Applies `PrestateTrace` diffs to the revm state -pub fn apply_prestate_trace(prestate_trace: PrestateTrace, ecx: Ecx<'_, '_, '_>) { - match prestate_trace { - polkadot_sdk::pallet_revive::evm::PrestateTrace::DiffMode { pre: _, post } => { - for (key, PrestateTraceInfo { balance, nonce, code, storage }) in post { - let address = Address::from_slice(key.as_bytes()); - let account = ecx - .journaled_state - .load_account(address) - .expect("account could not be loaded") - .data; - - account.mark_touch(); - - if let Some(balance) = balance { - account.info.balance = RU256::from_limbs(balance.0); - }; - - if let Some(nonce) = nonce { - account.info.nonce = nonce.into(); - }; - - if let Some(code) = code { - let account = - ecx.journaled_state.state.get_mut(&address).expect("account is loaded"); - let bytecode = Bytecode::new_raw(Bytes::from(code.0)); - account.info.code_hash = bytecode.hash_slow(); - account.info.code = Some(bytecode); - } - ecx.journaled_state.load_account(address).expect("account could not be loaded"); +impl Tracer { + pub fn new(is_recording: bool) -> Self { + let call_tracer = + match Pallet::::evm_tracer(TracerType::CallTracer(None)) { + ReviveTracer::CallTracer(tracer) => tracer, + _ => unreachable!("Expected CallTracer variant"), + }; + + let prestate_tracer: PrestateTracer = + PrestateTracer::new(PrestateTracerConfig { + diff_mode: true, + disable_storage: false, + disable_code: false, + }); + + let storage_tracer = if is_recording { Some(Default::default()) } else { None }; + + Self { call_tracer, prestate_tracer, storage_accesses: storage_tracer } + } + + pub fn trace R>(&mut self, f: F) -> R { + trace_revive(self, f) + } + + /// Collects call traces + pub fn collect_call_traces(&mut self) -> Option { + execute_with_externalities(|externalities| { + externalities.execute_with(|| self.call_tracer.clone().collect_trace()) + }) + } + + /// Collects prestate traces + fn collect_prestate_traces(&mut self) -> PrestateTrace { + execute_with_externalities(|externalities| { + externalities.execute_with(|| self.prestate_tracer.clone().collect_trace()) + }) + } - ecx.journaled_state.touch(address); - for (slot, entry) in storage { - let key = RU256::from_be_slice(&slot.0); - if let Some(e_entry) = entry { - let entry = RU256::from_be_slice(&e_entry.0); + /// Collects recorded accesses + pub fn get_recorded_accesses(&mut self) -> Vec { + self.storage_accesses.take().unwrap_or_default().get_records() + } - ecx.journaled_state.sstore(address, key, entry).expect("to succeed"); + /// Applies `PrestateTrace` diffs to the revm state + pub fn apply_prestate_trace(&mut self, ecx: Ecx<'_, '_, '_>) { + let prestate_trace = self.collect_prestate_traces(); + match prestate_trace { + polkadot_sdk::pallet_revive::evm::PrestateTrace::DiffMode { pre: _, post } => { + for (key, PrestateTraceInfo { balance, nonce, code, storage }) in post { + let address = Address::from_slice(key.as_bytes()); + + let account = ecx + .journaled_state + .load_account(address) + .expect("account could not be loaded") + .data; + + account.mark_touch(); + + if let Some(balance) = balance { + account.info.balance = RU256::from_limbs(balance.0); + }; + + if let Some(nonce) = nonce { + account.info.nonce = nonce.into(); + }; + + if let Some(code) = code { + let account = + ecx.journaled_state.state.get_mut(&address).expect("account is loaded"); + let bytecode = Bytecode::new_raw(Bytes::from(code.0)); + account.info.code_hash = bytecode.hash_slow(); + account.info.code = Some(bytecode); + } + ecx.journaled_state.load_account(address).expect("account could not be loaded"); + + ecx.journaled_state.touch(address); + for (slot, entry) in storage { + let key = RU256::from_be_slice(&slot.0); + let previous = ecx.journaled_state.sload(address, key).expect("to load"); + + if let Some(e_entry) = entry { + let entry = RU256::from_be_slice(&e_entry.0); + let new_slot = StorageSlot::new_changed(previous.data, entry); + ecx.journaled_state + .sstore(address, key, new_slot.present_value) + .expect("to succeed"); + } } } } + _ => panic!("Can't happen"), + }; + } +} + +impl Tracing for Tracer { + fn watch_address(&mut self, addr: &polkadot_sdk::sp_core::H160) { + self.prestate_tracer.watch_address(addr); + self.call_tracer.watch_address(addr); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.watch_address(addr); + } + } + + fn enter_child_span( + &mut self, + from: polkadot_sdk::sp_core::H160, + to: polkadot_sdk::sp_core::H160, + is_delegate_call: bool, + is_read_only: bool, + value: U256, + input: &[u8], + gas: Weight, + ) { + self.prestate_tracer.enter_child_span( + from, + to, + is_delegate_call, + is_read_only, + value, + input, + gas, + ); + self.call_tracer.enter_child_span( + from, + to, + is_delegate_call, + is_read_only, + value, + input, + gas, + ); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.enter_child_span( + from, + to, + is_delegate_call, + is_read_only, + value, + input, + gas, + ) + } + } + + fn instantiate_code( + &mut self, + code: &polkadot_sdk::pallet_revive::Code, + salt: Option<&[u8; 32]>, + ) { + self.prestate_tracer.instantiate_code(code, salt); + self.call_tracer.instantiate_code(code, salt); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.instantiate_code(code, salt); + } + } + + fn balance_read(&mut self, addr: &polkadot_sdk::sp_core::H160, value: U256) { + self.prestate_tracer.balance_read(addr, value); + self.call_tracer.balance_read(addr, value); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.balance_read(addr, value); + } + } + + fn storage_read(&mut self, key: &polkadot_sdk::pallet_revive::Key, value: Option<&[u8]>) { + self.prestate_tracer.storage_read(key, value); + self.call_tracer.storage_read(key, value); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.storage_read(key, value); + } + } + + fn storage_write( + &mut self, + key: &polkadot_sdk::pallet_revive::Key, + old_value: Option>, + new_value: Option<&[u8]>, + ) { + self.prestate_tracer.storage_write(key, old_value.clone(), new_value); + self.call_tracer.storage_write(key, old_value.clone(), new_value); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.storage_write(key, old_value, new_value); + } + } + + fn log_event( + &mut self, + event: polkadot_sdk::sp_core::H160, + topics: &[polkadot_sdk::sp_core::H256], + data: &[u8], + ) { + self.prestate_tracer.log_event(event, topics, data); + self.call_tracer.log_event(event, topics, data); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.log_event(event, topics, data); + } + } + + fn exit_child_span( + &mut self, + output: &polkadot_sdk::pallet_revive::ExecReturnValue, + gas_left: Weight, + ) { + self.prestate_tracer.exit_child_span(output, gas_left); + self.call_tracer.exit_child_span(output, gas_left); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.exit_child_span(output, gas_left); + } + } + + fn exit_child_span_with_error( + &mut self, + error: polkadot_sdk::sp_runtime::DispatchError, + gas_left: Weight, + ) { + self.prestate_tracer.exit_child_span_with_error(error, gas_left); + self.call_tracer.exit_child_span_with_error(error, gas_left); + if let Some(storage_tracer) = &mut self.storage_accesses { + storage_tracer.exit_child_span_with_error(error, gas_left); } - _ => panic!("Can't happen"), - }; + } } diff --git a/crates/revive-strategy/src/tracing/storage_tracer.rs b/crates/revive-strategy/src/tracing/storage_tracer.rs new file mode 100644 index 0000000000000..eea15e5322f57 --- /dev/null +++ b/crates/revive-strategy/src/tracing/storage_tracer.rs @@ -0,0 +1,239 @@ +use alloy_primitives::{Bytes, U256 as RU256}; +use foundry_cheatcodes::Vm::{AccountAccessKind, StorageAccess}; +use polkadot_sdk::{ + pallet_revive::{self, Code, tracing::Tracing}, + sp_core::{H160, H256, U256}, + sp_weights::Weight, +}; +use revive_env::Runtime; + +#[derive(Debug, Default)] +pub(crate) struct StorageTracer { + /// The current address of the contract's which storage is being accessed. + current_addr: H160, + /// Whether the current call is a contract creation. + is_create: Option, + + records: Vec, + pending: Vec, + records_inner: Vec, + /// Track the calls that must be skipped. + /// We track this on a different stack to easily skip the `call_end` + /// instances, if they were marked to be skipped in the `call_start`. + call_skip_tracker: Vec, + /// Mark the next call at a given depth and having the given address accesses. + /// This is useful, for example to skip nested constructor calls after CREATE, + /// to allow us to omit/flatten them like in EVM. + skip_next_call: Option<(u64, CallAddresses)>, +} + +/// Represents the account access during vm execution. +#[derive(Debug, Clone)] +pub struct AccountAccess { + /// Call depth. + pub depth: u64, + /// Call type. + pub kind: AccountAccessKind, + /// Account that was accessed. + pub account: H160, + /// Accessor account. + pub accessor: H160, + /// Call data. + pub data: Bytes, + /// Deployed bytecode hash if CREATE. + pub deployed_bytecode_hash: Option, + /// Call value. + pub value: U256, + /// Previous balance of the accessed account. + pub old_balance: U256, + /// New balance of the accessed account. + pub new_balance: U256, + /// Storage slots that were accessed. + pub storage_accesses: Vec, +} + +#[derive(Debug, Default, Clone)] +struct CallAddresses { + pub to: H160, + pub from: H160, +} + +impl StorageTracer { + pub fn get_records(&self) -> Vec { + assert!( + self.call_skip_tracker.is_empty(), + "call skip tracker is not empty; found calls without matching returns: {:?}", + self.call_skip_tracker + ); + assert!( + self.skip_next_call.is_none(), + "skip next call is not empty: {:?}", + self.skip_next_call + ); + assert!( + self.pending.is_empty(), + "pending call stack is not empty; found calls without matching returns: {:?}", + self.pending + ); + assert!( + self.records_inner.is_empty(), + "inner stack is not empty; found calls without matching returns: {:?}", + self.records_inner + ); + self.records.clone() + } +} + +impl Tracing for StorageTracer { + fn instantiate_code(&mut self, code: &Code, _salt: Option<&[u8; 32]>) { + self.is_create = Some(code.clone()); + } + + fn enter_child_span( + &mut self, + from: H160, + to: H160, + is_delegate_call: bool, + is_read_only: bool, + value: U256, + input: &[u8], + _gas: Weight, + ) { + use pallet_revive::{AccountId32Mapper, AddressMapper}; + let system_addr = AccountId32Mapper::::to_address( + &pallet_revive::Pallet::::account_id(), + ); + if system_addr == from || system_addr == to || is_read_only { + self.call_skip_tracker.push(true); + return; + } + let kind = if self.is_create.is_some() { + AccountAccessKind::Create + } else { + AccountAccessKind::Call + }; + + let last_depth = if !self.pending.is_empty() { + self.pending.last().map(|record| record.depth).expect("must have at least one record") + } else { + self.records.last().map(|record| record.depth).unwrap_or_default() + }; + let new_depth = last_depth.checked_add(1).expect("overflow in recording call depth"); + + // For create we expect another CALL if the constructor is invoked. We need to skip/flatten + // this call so it is consistent with CREATE in the EVM. + match kind { + AccountAccessKind::Create => { + // skip the next nested call to the created address from the caller. + self.skip_next_call = + Some((new_depth.saturating_add(1), CallAddresses { to, from })); + } + AccountAccessKind::Call => { + if let Some((depth, call_addr)) = self.skip_next_call.take() + && depth == new_depth + && call_addr.from == from + && call_addr.to == to + { + self.call_skip_tracker.push(true); + return; + } + } + _ => panic!("cant be matched"), + } + self.call_skip_tracker.push(false); + self.pending.push(AccountAccess { + depth: new_depth, + kind, + account: to, + accessor: from, + data: Bytes::from(input.to_vec()), + deployed_bytecode_hash: None, + value, + old_balance: pallet_revive::Pallet::::evm_balance(&to), + new_balance: U256::zero(), + storage_accesses: Default::default(), + }); + + if !is_delegate_call { + self.current_addr = to; + } + } + + fn exit_child_span_with_error( + &mut self, + _error: polkadot_sdk::sp_runtime::DispatchError, + _gas_left: Weight, + ) { + self.is_create = None + } + + fn exit_child_span( + &mut self, + _output: &polkadot_sdk::pallet_revive::ExecReturnValue, + _gas_left: Weight, + ) { + let skip_call = + self.call_skip_tracker.pop().expect("unexpected return while skipping call recording"); + if skip_call { + return; + } + let mut record = self.pending.pop().expect("unexpected return while recording call"); + record.new_balance = pallet_revive::Pallet::::evm_balance(&self.current_addr); + let is_create = self.is_create.take(); + if is_create.is_some() { + match is_create { + Some(Code::Existing(_)) => (), + Some(Code::Upload(_)) => (), + None => (), + } + } + + if let Some((depth, _)) = &self.skip_next_call + && record.depth < *depth + { + // reset call skip if not encountered (depth has been crossed) + self.skip_next_call = None; + } + + if self.pending.is_empty() { + // no more pending records, append everything recorded so far. + self.records.push(record); + + // also append the inner records. + if !self.records_inner.is_empty() { + self.records.extend(std::mem::take(&mut self.records_inner)); + } + } else { + // we have pending records, so record to inner. + self.records_inner.push(record); + } + } + + fn storage_read(&mut self, key: &polkadot_sdk::pallet_revive::Key, value: Option<&[u8]>) { + let record = self.pending.last_mut().expect("expected at least one record"); + record.storage_accesses.push(StorageAccess { + account: self.current_addr.0.into(), + slot: RU256::from_be_slice(key.unhashed()).into(), + isWrite: false, + previousValue: RU256::from_be_slice(value.unwrap_or_default()).into(), + newValue: RU256::from_be_slice(value.unwrap_or_default()).into(), + reverted: false, + }); + } + fn storage_write( + &mut self, + key: &polkadot_sdk::pallet_revive::Key, + old_value: Option>, + new_value: Option<&[u8]>, + ) { + let record = self.pending.last_mut().expect("expected at least one record"); + record.storage_accesses.push(StorageAccess { + account: self.current_addr.0.into(), + slot: RU256::from_be_slice(key.unhashed()).into(), + isWrite: true, + previousValue: RU256::from_be_slice(old_value.unwrap_or_default().as_slice()).into(), + newValue: RU256::from_be_slice(new_value.unwrap_or_default()).into(), + reverted: false, + }); + } +} diff --git a/crates/revive-utils/Cargo.toml b/crates/revive-utils/Cargo.toml new file mode 100644 index 0000000000000..80e74221d3922 --- /dev/null +++ b/crates/revive-utils/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "revive-utils" +description = "Foundry revive helpers" + +version.workspace = true +edition.workspace = true +rust-version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +exclude.workspace = true + +[dependencies] +foundry-evm-core.workspace = true +foundry-evm-traces.workspace = true +polkadot-sdk = { git = "https://github.com/paritytech/polkadot-sdk.git", branch = "master", features = [ + "experimental", + "runtime", + "polkadot-runtime-common", + "pallet-revive", + "pallet-balances", + "pallet-timestamp" +]} +revive-env.workspace = true + +alloy-primitives.workspace = true +revm.workspace = true diff --git a/crates/revive-utils/src/lib.rs b/crates/revive-utils/src/lib.rs new file mode 100644 index 0000000000000..2fced70425504 --- /dev/null +++ b/crates/revive-utils/src/lib.rs @@ -0,0 +1,330 @@ +use alloy_primitives::{Address, B256, Bytes, Log, U256 as RU256}; +use foundry_evm_core::{Ecx, InspectorExt}; +use foundry_evm_traces::{ + CallTraceArena, GethTraceBuilder, ParityTraceBuilder, TracingInspector, TracingInspectorConfig, +}; +use polkadot_sdk::pallet_revive::evm::{CallTrace, CallType}; +use revm::{ + Inspector, + context::{ContextTr, CreateScheme}, + inspector::JournalExt, + interpreter::{ + CallInputs, CallOutcome, CreateInputs, CreateOutcome, Gas, InstructionResult, Interpreter, + InterpreterResult, + }, +}; + +/// A Wrapper around [TracingInspector] to allow adding zkEVM traces. +#[derive(Clone, Debug, Default)] +pub struct TraceCollector { + inner: TracingInspector, +} + +impl TraceCollector { + /// Returns a new instance for the given config + pub fn new(config: TracingInspectorConfig) -> Self { + Self { inner: TracingInspector::new(config) } + } + + /// Returns the inner [`TracingInspector`] + #[inline] + pub fn inner(&mut self) -> &mut TracingInspector { + &mut self.inner + } + + /// Resets the inspector to its initial state of [Self::new]. + /// This makes the inspector ready to be used again. + /// + /// Note that this method has no effect on the allocated capacity of the vector. + #[inline] + pub fn fuse(&mut self) { + self.inner.fuse() + } + + /// Resets the inspector to it's initial state of [Self::new]. + #[inline] + pub fn fused(self) -> Self { + Self { inner: self.inner.fused() } + } + + /// Returns the config of the inspector. + pub const fn config(&self) -> &TracingInspectorConfig { + self.inner.config() + } + + /// Returns a mutable reference to the config of the inspector. + pub fn config_mut(&mut self) -> &mut TracingInspectorConfig { + self.inner.config_mut() + } + + /// Updates the config of the inspector. + pub fn update_config( + &mut self, + f: impl FnOnce(TracingInspectorConfig) -> TracingInspectorConfig, + ) { + self.inner.update_config(f); + } + + /// Gets a reference to the recorded call traces. + pub const fn traces(&self) -> &CallTraceArena { + self.inner.traces() + } + + /// Gets a mutable reference to the recorded call traces. + pub fn traces_mut(&mut self) -> &mut CallTraceArena { + self.inner.traces_mut() + } + + /// Consumes the inspector and returns the recorded call traces. + pub fn into_traces(self) -> CallTraceArena { + self.inner.into_traces() + } + + /// Manually the gas used of the root trace. + /// + /// This is useful if the root trace's gasUsed should mirror the actual gas used by the + /// transaction. + /// + /// This allows setting it manually by consuming the execution result's gas for example. + #[inline] + pub fn set_transaction_gas_used(&mut self, gas_used: u64) { + self.inner.set_transaction_gas_used(gas_used) + } + + /// Convenience function for [ParityTraceBuilder::set_transaction_gas_used] that consumes the + /// type. + #[inline] + pub fn with_transaction_gas_used(self, gas_used: u64) -> Self { + Self { inner: self.inner.with_transaction_gas_used(gas_used) } + } + + /// Consumes the Inspector and returns a [ParityTraceBuilder]. + #[inline] + pub fn into_parity_builder(self) -> ParityTraceBuilder { + self.inner.into_parity_builder() + } + + /// Consumes the Inspector and returns a [GethTraceBuilder]. + #[inline] + pub fn into_geth_builder(self) -> GethTraceBuilder<'static> { + self.inner.into_geth_builder() + } +} + +impl Inspector for TraceCollector +where + CTX: ContextTr, +{ + #[inline] + fn step(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.inner.step(interp, context) + } + + #[inline] + fn step_end(&mut self, interp: &mut Interpreter, context: &mut CTX) { + self.inner.step_end(interp, context) + } + + fn log(&mut self, interp: &mut Interpreter, context: &mut CTX, log: Log) { + self.inner.log(interp, context, log) + } + + fn call(&mut self, context: &mut CTX, inputs: &mut CallInputs) -> Option { + self.inner.call(context, inputs) + } + + fn call_end(&mut self, context: &mut CTX, inputs: &CallInputs, outcome: &mut CallOutcome) { + self.inner.call_end(context, inputs, outcome) + } + + fn create(&mut self, context: &mut CTX, inputs: &mut CreateInputs) -> Option { + self.inner.create(context, inputs) + } + + fn create_end( + &mut self, + context: &mut CTX, + inputs: &CreateInputs, + outcome: &mut CreateOutcome, + ) { + self.inner.create_end(context, inputs, outcome) + } + + // EOF create hooks were removed in current revm version; only standard create is supported. + + fn selfdestruct(&mut self, contract: Address, target: Address, value: RU256) { + >::selfdestruct(&mut self.inner, contract, target, value) + } +} + +impl InspectorExt for TraceCollector { + fn trace_revive( + &mut self, + context: Ecx<'_, '_, '_>, + call_traces: Box, + record_top_call: bool, + ) { + let call_traces = *call_traces + .downcast::() + .expect("TraceCollector::trace_revive expected call traces to be a CallTrace"); + use revm::Inspector; + fn trace_call_recursive( + tracer: &mut TracingInspector, + context: Ecx<'_, '_, '_>, + call: CallTrace, + suppressed_top_call: bool, + ) -> u64 { + let inputs = &mut CallInputs { + input: revm::interpreter::CallInput::Bytes(call.input.0.clone().into()), + gas_limit: call.gas.try_into().unwrap_or(u64::MAX), + scheme: revm::interpreter::CallScheme::Call, + caller: call.from.0.into(), + value: revm::interpreter::CallValue::Transfer(RU256::from_be_bytes( + call.value.unwrap_or_default().to_big_endian(), + )), + target_address: call.to.0.into(), + bytecode_address: call.to.0.into(), + is_static: false, + return_memory_offset: Default::default(), + }; + let is_first_non_system_call = !suppressed_top_call; + + // We ignore traces from system addresses, the default account abstraction calls on + // caller address, and the original call (identified when neither `to` or + // `from` are system addresses) since it is already included in EVM trace. + let record_trace = + !is_first_non_system_call && inputs.target_address != context.tx.caller; + + let mut outcome = if let Some(reason) = &call.revert_reason { + CallOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: reason.as_bytes().to_owned().into(), + gas: Gas::new_spent(call.gas_used.as_u64()), + }, + memory_offset: Default::default(), + } + } else { + CallOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: call.output.clone().0.into(), + gas: Gas::new_spent(call.gas_used.as_u64()), + }, + memory_offset: Default::default(), + } + }; + + let mut create_inputs = + if matches!(call.call_type, CallType::Create | CallType::Create2) { + let scheme = match call.call_type { + CallType::Create => CreateScheme::Create, + CallType::Create2 => CreateScheme::Create2 { + salt: RU256::from_be_slice(call.input.0.as_ref()), + }, + _ => panic!("impossible"), + }; + Some(CreateInputs { + caller: inputs.caller, + scheme, + value: inputs.value.get(), + init_code: inputs.input.bytes(context), + gas_limit: inputs.gas_limit, + }) + } else { + None + }; + + // start span + if record_trace { + if let Some(inputs) = &mut create_inputs { + tracer.create(context, inputs); + } else { + tracer.call(context, inputs); + } + } + for log in call.logs { + tracer.log( + &mut Default::default(), + context, + Log::new_unchecked( + log.address.0.into(), + log.topics.iter().map(|x| B256::from_slice(x.as_bytes())).collect(), + log.data.0.into(), + ), + ); + } + + // We increment the depth for inner calls as normally traces are processed + // during execution, where the environment takes care of updating the context + let (new_depth, overflow) = context.journaled_state.depth.overflowing_add(1); + if !overflow && record_trace { + context.journaled_state.depth = new_depth; + } + + // recurse into inner calls + // record extra gas from ignored traces, to add it at end + let mut extra_gas = if record_trace { 0u64 } else { call.gas_used.as_u64() }; + for inner_call in call.calls { + let inner_extra_gas = trace_call_recursive( + tracer, + context, + inner_call, + suppressed_top_call || is_first_non_system_call, + ); + extra_gas = extra_gas.saturating_add(inner_extra_gas); + } + + // We then decrement the call depth so `call_end`/`create_end` has the correct context + if !overflow && record_trace { + context.journaled_state.depth = context.journaled_state.depth.saturating_sub(1); + } + + // finish span + if record_trace { + if let Some(inputs) = &mut create_inputs { + let mut outcome = if let Some(reason) = call.revert_reason { + CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: reason.as_bytes().to_owned().into(), + gas: Gas::new_spent(call.gas_used.as_u64() + extra_gas), + }, + address: None, + } + } else { + CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: Bytes::from(call.output.clone().0), + gas: Gas::new_spent(call.gas_used.as_u64() + extra_gas), + }, + address: Some(call.to.0.into()), + } + }; + + tracer.create_end(context, inputs, &mut outcome); + } else { + if extra_gas != 0 { + outcome.result.gas = Gas::new_spent(outcome.result.gas.spent() + extra_gas); + } + tracer.call_end(context, inputs, &mut outcome); + } + } + + extra_gas + } + + let (new_depth, overflow) = context.journaled_state.depth.overflowing_add(1); + // If we are going to record the top call then we don't want to change the call depth + if !overflow && !record_top_call { + context.journaled_state.depth = new_depth; + } + + trace_call_recursive(&mut self.inner, context, call_traces, record_top_call); + + if !overflow && !record_top_call { + context.journaled_state.depth = context.journaled_state.depth.saturating_sub(1); + } + } +}