From bcbfa1caee7769df5a780c2873ea5a034293c530 Mon Sep 17 00:00:00 2001 From: tskoyo Date: Sun, 23 Nov 2025 16:47:32 +0100 Subject: [PATCH 1/5] Add depth trace tag when running forge test --- crates/evm/traces/src/lib.rs | 9 +++++++++ crates/forge/src/cmd/test/mod.rs | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index f09b8d756b93b..c9568a0be5a2e 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 { + Vec::clear(&mut node.ordering); + } + } +} + /// 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..1e0a2142869be 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, short)] + 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(depth) = self.depth { + prune_trace_depth(arena, 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", "--depth", "2"]); + assert!(args.depth.is_some()); + } + // #[test] fn fuzz_seed_exists() { From 253ed686cdcf7bdbf67c87f9ffd961593d49091b Mon Sep 17 00:00:00 2001 From: tskoyo Date: Tue, 25 Nov 2025 23:40:41 +0100 Subject: [PATCH 2/5] Change the arg name --- crates/evm/traces/src/lib.rs | 2 +- crates/forge/src/cmd/test/mod.rs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/evm/traces/src/lib.rs b/crates/evm/traces/src/lib.rs index c9568a0be5a2e..301454d43eeab 100644 --- a/crates/evm/traces/src/lib.rs +++ b/crates/evm/traces/src/lib.rs @@ -191,7 +191,7 @@ pub fn render_trace_arena(arena: &SparsedTraceArena) -> String { pub fn prune_trace_depth(arena: &mut CallTraceArena, depth: usize) { for node in arena.nodes_mut() { if node.trace.depth >= depth { - Vec::clear(&mut node.ordering); + node.ordering.clear(); } } } diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index 1e0a2142869be..a37a422e6688b 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -137,8 +137,8 @@ pub struct TestArgs { suppress_successful_traces: bool, /// Defines the depth of a trace - #[arg(long, short)] - depth: Option, + #[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")] @@ -657,8 +657,8 @@ impl TestArgs { if should_include { decode_trace_arena(arena, &decoder).await; - if let Some(depth) = self.depth { - prune_trace_depth(arena, depth); + 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)); @@ -1048,8 +1048,8 @@ mod tests { #[test] fn depth_trace() { - let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--depth", "2"]); - assert!(args.depth.is_some()); + let args: TestArgs = TestArgs::parse_from(["foundry-cli", "--trace-depth", "2"]); + assert!(args.trace_depth.is_some()); } // From 6ffa91807c23214c39dc41c6aade1b6080b65fd7 Mon Sep 17 00:00:00 2001 From: tskoyo Date: Wed, 26 Nov 2025 21:44:39 +0100 Subject: [PATCH 3/5] Prune trace depth in cast run --- crates/cast/src/cmd/call.rs | 1 + crates/cast/src/cmd/run.rs | 7 ++++++- crates/cast/src/debug.rs | 10 +++++++++- crates/cli/src/utils/cmd.rs | 8 +++++++- 4 files changed, 23 insertions(+), 3 deletions(-) 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..a745260db6244 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -27,7 +27,7 @@ use foundry_evm::{ core::env::AsEnvMut, executors::{EvmError, Executor, TracingExecutor}, opts::EvmOpts, - traces::{InternalTraceMode, TraceMode, Traces}, + traces::{InternalTraceMode, SparsedTraceArena, TraceMode, Traces}, utils::configure_tx_env, }; use futures::TryFutureExt; @@ -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))?; } From e1a343485eeaa99251a506f3c18194434b9d311c Mon Sep 17 00:00:00 2001 From: tskoyo Date: Wed, 26 Nov 2025 22:31:41 +0100 Subject: [PATCH 4/5] Add trace depth tests --- crates/forge/tests/cli/test_cmd/trace.rs | 180 +++++++++++++++++++++++ 1 file changed, 180 insertions(+) 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) + +"#]]); +}); From 91b03a77682389591cc3f37e26648fad6bb201b7 Mon Sep 17 00:00:00 2001 From: grandizzy Date: Thu, 27 Nov 2025 08:20:38 +0200 Subject: [PATCH 5/5] fix clippy --- crates/cast/src/cmd/run.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/cast/src/cmd/run.rs b/crates/cast/src/cmd/run.rs index a745260db6244..38fc00b8f4e91 100644 --- a/crates/cast/src/cmd/run.rs +++ b/crates/cast/src/cmd/run.rs @@ -27,7 +27,7 @@ use foundry_evm::{ core::env::AsEnvMut, executors::{EvmError, Executor, TracingExecutor}, opts::EvmOpts, - traces::{InternalTraceMode, SparsedTraceArena, TraceMode, Traces}, + traces::{InternalTraceMode, TraceMode, Traces}, utils::configure_tx_env, }; use futures::TryFutureExt;