diff --git a/crates/cast/src/cmd/call.rs b/crates/cast/src/cmd/call.rs index 087e0d842f210..775b31fab0842 100644 --- a/crates/cast/src/cmd/call.rs +++ b/crates/cast/src/cmd/call.rs @@ -368,6 +368,7 @@ impl CallArgs { debug, decode_internal, disable_labels, + None, ) .await?; diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index 79387f01751ce..38fc00b8f4e91 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -47,6 +47,10 @@ pub struct RunArgs { #[arg(long)] decode_internal: bool, + /// Defines the depth of a trace + #[arg(long)] + trace_depth: Option, + /// Print out opcode traces. #[arg(long, short)] trace_printer: bool, @@ -315,6 +319,7 @@ impl RunArgs { debug, decode_internal, disable_labels, + self.trace_depth, ) .await?; diff --git a/crates/cast/src/debug.rs b/crates/cast/src/debug.rs index c761509be1a42..9af69a4167e38 100644 --- a/crates/cast/src/debug.rs +++ b/crates/cast/src/debug.rs @@ -24,6 +24,7 @@ pub(crate) async fn handle_traces( debug: bool, decode_internal: bool, disable_label: bool, + trace_depth: Option, ) -> eyre::Result<()> { let (known_contracts, mut sources) = if with_local_artifacts { let _ = sh_println!("Compiling project to generate artifacts"); @@ -86,7 +87,14 @@ pub(crate) async fn handle_traces( decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources)); } - print_traces(&mut result, &decoder, shell::verbosity() > 0, shell::verbosity() > 4).await?; + print_traces( + &mut result, + &decoder, + shell::verbosity() > 0, + shell::verbosity() > 4, + trace_depth, + ) + .await?; Ok(()) } diff --git a/crates/cli/src/utils/cmd.rs b/crates/cli/src/utils/cmd.rs index 6c39080ef07d6..51cdae3eaedee 100644 --- a/crates/cli/src/utils/cmd.rs +++ b/crates/cli/src/utils/cmd.rs @@ -13,7 +13,7 @@ use foundry_evm::{ opts::EvmOpts, traces::{ CallTraceDecoder, TraceKind, Traces, decode_trace_arena, identifier::SignaturesCache, - render_trace_arena_inner, + prune_trace_depth, render_trace_arena_inner, }, }; use std::{ @@ -329,6 +329,7 @@ pub async fn print_traces( decoder: &CallTraceDecoder, verbose: bool, state_changes: bool, + trace_depth: Option, ) -> Result<()> { let traces = result.traces.as_mut().expect("No traces found"); @@ -338,6 +339,11 @@ pub async fn print_traces( for (_, arena) in traces { decode_trace_arena(arena, decoder).await; + + if let Some(trace_depth) = trace_depth { + prune_trace_depth(arena, trace_depth); + } + sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?; } diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index f09b8d756b93b..301454d43eeab 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -187,6 +187,15 @@ pub fn render_trace_arena(arena: &SparsedTraceArena) -> String { render_trace_arena_inner(arena, false, false) } +/// Prunes trace depth if depth is provided as an argument +pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) { + for node in arena.nodes_mut() { + if node.trace.depth >= depth { + node.ordering.clear(); + } + } +} + /// Render a collection of call traces to a string optionally including contract creation bytecodes /// and in JSON format. pub fn render_trace_arena_inner( diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 0a45e1574f1bd..a37a422e6688b 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -41,7 +41,7 @@ use foundry_config::{ use foundry_debugger::Debugger; use foundry_evm::{ opts::EvmOpts, - traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers}, + traces::{backtrace::BacktraceBuilder, identifier::TraceIdentifiers, prune_trace_depth}, }; use regex::Regex; use std::{ @@ -136,6 +136,10 @@ pub struct TestArgs { #[arg(long, short, env = "FORGE_SUPPRESS_SUCCESSFUL_TRACES", help_heading = "Display options")] suppress_successful_traces: bool, + /// Defines the depth of a trace + #[arg(long)] + trace_depth: Option, + /// Output test results as JUnit XML report. #[arg(long, conflicts_with_all = ["quiet", "json", "gas_report", "summary", "list", "show_progress"], help_heading = "Display options")] pub junit: bool, @@ -652,6 +656,11 @@ impl TestArgs { if should_include { decode_trace_arena(arena, &decoder).await; + + if let Some(trace_depth) = self.trace_depth { + prune_trace_depth(arena, trace_depth); + } + decoded_traces.push(render_trace_arena_inner(arena, false, verbosity > 4)); } } @@ -1037,6 +1046,12 @@ mod tests { assert!(args.fuzz_seed.is_some()); } + #[test] + fn depth_trace() { + let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--trace-depth", "2"]); + assert!(args.trace_depth.is_some()); + } + // #[test] fn fuzz_seed_exists() { diff --git a/crates/forge/tests/cli/test_cmd/trace.rs b/crates/forge/tests/cli/test_cmd/trace.rs index 01108bedd581d..27116c3d97925 100644 --- a/crates/forge/tests/cli/test_cmd/trace.rs +++ b/crates/forge/tests/cli/test_cmd/trace.rs @@ -393,3 +393,183 @@ Ran 1 test suite [ELAPSED]: 2 tests passed, 0 failed, 0 skipped (2 total tests) "#]]); }); + +forgetest_init!(trace_test_detph, |prj, cmd| { + prj.add_test( + "Trace.t.sol", + r#" +pragma solidity ^0.8.18; + +import "forge-std/Test.sol"; + +contract RecursiveCall { + TraceTest factory; + + event Depth(uint256 depth); + event ChildDepth(uint256 childDepth); + event CreatedChild(uint256 childDepth); + + constructor(address _factory) { + factory = TraceTest(_factory); + } + + function recurseCall(uint256 neededDepth, uint256 depth) public returns (uint256) { + if (depth == neededDepth) { + this.negativeNum(); + return neededDepth; + } + + uint256 childDepth = this.recurseCall(neededDepth, depth + 1); + emit ChildDepth(childDepth); + + this.someCall(); + emit Depth(depth); + + return depth; + } + + function recurseCreate(uint256 neededDepth, uint256 depth) public returns (uint256) { + if (depth == neededDepth) { + return neededDepth; + } + + RecursiveCall child = factory.create(); + emit CreatedChild(depth + 1); + + uint256 childDepth = child.recurseCreate(neededDepth, depth + 1); + emit ChildDepth(childDepth); + emit Depth(depth); + + return depth; + } + + function someCall() public pure {} + + function negativeNum() public pure returns (int256) { + return -1000000000; + } +} + +contract TraceTest is Test { + uint256 nodeId = 0; + RecursiveCall first; + + function setUp() public { + first = this.create(); + } + + function create() public returns (RecursiveCall) { + RecursiveCall node = new RecursiveCall(address(this)); + vm.label(address(node), string(abi.encodePacked("Node ", uintToString(nodeId++)))); + + return node; + } + + function testRecurseCall() public { + first.recurseCall(8, 0); + } + + function testRecurseCreate() public { + first.recurseCreate(8, 0); + } +} + +function uintToString(uint256 value) pure returns (string memory) { + // Taken from OpenZeppelin + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); +} +"#, + ); + + cmd.args(["test", "-vvvvv", "--trace-depth", "3"]).assert_success().stdout_eq(str![[r#" +... +Ran 2 tests for test/Trace.t.sol:TraceTest +[PASS] testRecurseCall() ([GAS]) +Traces: + [..] TraceTest::setUp() + ├─ [..] TraceTest::create() + │ ├─ [..] → new Node 0@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ │ └─ ← [Return] 1911 bytes of code + │ ├─ [0] VM::label(Node 0: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], "Node 0") + │ │ └─ ← [Return] + │ └─ ← [Return] Node 0: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f] + └─ ← [Stop] + + [..] TraceTest::testRecurseCall() + ├─ [..] Node 0::recurseCall(8, 0) + │ ├─ [..] Node 0::recurseCall(8, 1) + │ │ ├─ [..] Node 0::recurseCall(8, 2) + │ │ │ └─ ← [Return] 2 + │ │ ├─ emit ChildDepth(childDepth: 2) + │ │ ├─ [..] Node 0::someCall() [staticcall] + │ │ │ └─ ← [Stop] + │ │ ├─ emit Depth(depth: 1) + │ │ └─ ← [Return] 1 + │ ├─ emit ChildDepth(childDepth: 1) + │ ├─ [..] Node 0::someCall() [staticcall] + │ │ └─ ← [Stop] + │ ├─ emit Depth(depth: 0) + │ └─ ← [Return] 0 + └─ ← [Stop] + +[PASS] testRecurseCreate() ([GAS]) +Traces: + [..] TraceTest::setUp() + ├─ [..] TraceTest::create() + │ ├─ [..] → new Node 0@0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f + │ │ └─ ← [Return] 1911 bytes of code + │ ├─ [0] VM::label(Node 0: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f], "Node 0") + │ │ └─ ← [Return] + │ └─ ← [Return] Node 0: [0x5615dEB798BB3E4dFa0139dFa1b3D433Cc23b72f] + └─ ← [Stop] + + [..] TraceTest::testRecurseCreate() + ├─ [..] Node 0::recurseCreate(8, 0) + │ ├─ [..] TraceTest::create() + │ │ ├─ [405132] → new Node 1@0x2e234DAe75C793f67A35089C9d99245E1C58470b + │ │ │ ├─ storage changes: + │ │ │ │ @ 0: 0 → 0x0000000000000000000000007fa9385be102ac3eac297483dd6233d62b3e1496 + │ │ │ └─ ← [Return] 1911 bytes of code + │ │ ├─ [0] VM::label(Node 1: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], "Node 1") + │ │ │ └─ ← [Return] + │ │ ├─ storage changes: + │ │ │ @ 32: 1 → 2 + │ │ └─ ← [Return] Node 1: [0x2e234DAe75C793f67A35089C9d99245E1C58470b] + │ ├─ emit CreatedChild(childDepth: 1) + │ ├─ [..] Node 1::recurseCreate(8, 1) + │ │ ├─ [..] TraceTest::create() + │ │ │ ├─ storage changes: + │ │ │ │ @ 32: 2 → 3 + │ │ │ └─ ← [Return] Node 2: [0xF62849F9A0B5Bf2913b396098F7c7019b51A820a] + │ │ ├─ emit CreatedChild(childDepth: 2) + │ │ ├─ [..] Node 2::recurseCreate(8, 2) + │ │ │ └─ ← [Return] 2 + │ │ ├─ emit ChildDepth(childDepth: 2) + │ │ ├─ emit Depth(depth: 1) + │ │ └─ ← [Return] 1 + │ ├─ emit ChildDepth(childDepth: 1) + │ ├─ emit Depth(depth: 0) + │ └─ ← [Return] 0 + └─ ← [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) + +"#]]); +});