Skip to content

Commit a51bbe4

Browse files
casscgrandizzy
authored andcommitted
feat(traces): show state changes in cast run and forge test on -vvvvv (foundry-rs#9013)
* Add options for state changes output and json output in cast run command * fix test * add back serde_json in Cargo.lock * format using nightly * rename parameter * update revm-inspectors * supress clippy warning and merge master * add serde_json * disable some stdout print when --json option is used * remove unnecessary check * replace with sh_println * replace with shell::is_json * Show storage for verbosity > 1, add test * Change verbosity to > 4 for both cast and forge test, add test, fix ci --------- Co-authored-by: grandizzy <[email protected]>
1 parent 70528b3 commit a51bbe4

File tree

14 files changed

+176
-30
lines changed

14 files changed

+176
-30
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cast/bin/cmd/call.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ use foundry_cli::{
88
opts::{EthereumOpts, TransactionOpts},
99
utils::{self, handle_traces, parse_ether_value, TraceResult},
1010
};
11-
use foundry_common::ens::NameOrAddress;
11+
use foundry_common::{ens::NameOrAddress, shell};
1212
use foundry_compilers::artifacts::EvmVersion;
1313
use foundry_config::{
1414
figment::{
@@ -182,8 +182,15 @@ impl CallArgs {
182182
env.cfg.disable_block_gas_limit = true;
183183
env.block.gas_limit = U256::MAX;
184184

185-
let mut executor =
186-
TracingExecutor::new(env, fork, evm_version, debug, decode_internal, alphanet);
185+
let mut executor = TracingExecutor::new(
186+
env,
187+
fork,
188+
evm_version,
189+
debug,
190+
decode_internal,
191+
shell::verbosity() > 4,
192+
alphanet,
193+
);
187194

188195
let value = tx.value.unwrap_or_default();
189196
let input = tx.inner.input.into_input().unwrap_or_default();

crates/cast/bin/cmd/run.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ use foundry_cli::{
1010
opts::{EtherscanOpts, RpcOpts},
1111
utils::{handle_traces, init_progress, TraceResult},
1212
};
13-
use foundry_common::{is_known_system_sender, SYSTEM_TRANSACTION_TYPE};
13+
use foundry_common::{is_known_system_sender, shell, SYSTEM_TRANSACTION_TYPE};
1414
use foundry_compilers::artifacts::EvmVersion;
1515
use foundry_config::{
1616
figment::{
@@ -169,14 +169,17 @@ impl RunArgs {
169169
evm_version,
170170
self.debug,
171171
self.decode_internal,
172+
shell::verbosity() > 4,
172173
alphanet,
173174
);
174175
let mut env =
175176
EnvWithHandlerCfg::new_with_spec_id(Box::new(env.clone()), executor.spec_id());
176177

177178
// Set the state to the moment right before the transaction
178179
if !self.quick {
179-
sh_println!("Executing previous transactions from the block.")?;
180+
if !shell::is_json() {
181+
sh_println!("Executing previous transactions from the block.")?;
182+
}
180183

181184
if let Some(block) = block {
182185
let pb = init_progress(block.transactions.len() as u64, "tx");

crates/cast/tests/cli/main.rs

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ Display options:
6464
- 2 (-vv): Print logs for all tests.
6565
- 3 (-vvv): Print execution traces for failing tests.
6666
- 4 (-vvvv): Print execution traces for all tests, and setup traces for failing tests.
67-
- 5 (-vvvvv): Print execution and setup traces for all tests.
67+
- 5 (-vvvvv): Print execution and setup traces for all tests, including storage changes.
6868
6969
Find more information in the book: http://book.getfoundry.sh/reference/cast/cast.html
7070
@@ -1778,3 +1778,66 @@ Transaction successfully executed.
17781778
17791779
"#]]);
17801780
});
1781+
1782+
// tests cast can decode traces when running with verbosity level > 4
1783+
forgetest_async!(show_state_changes_in_traces, |prj, cmd| {
1784+
let (api, handle) = anvil::spawn(NodeConfig::test()).await;
1785+
1786+
foundry_test_utils::util::initialize(prj.root());
1787+
// Deploy counter contract.
1788+
cmd.args([
1789+
"script",
1790+
"--private-key",
1791+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
1792+
"--rpc-url",
1793+
&handle.http_endpoint(),
1794+
"--broadcast",
1795+
"CounterScript",
1796+
])
1797+
.assert_success();
1798+
1799+
// Send tx to change counter storage value.
1800+
cmd.cast_fuse()
1801+
.args([
1802+
"send",
1803+
"0x5FbDB2315678afecb367f032d93F642f64180aa3",
1804+
"setNumber(uint256)",
1805+
"111",
1806+
"--private-key",
1807+
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
1808+
"--rpc-url",
1809+
&handle.http_endpoint(),
1810+
])
1811+
.assert_success();
1812+
1813+
let tx_hash = api
1814+
.transaction_by_block_number_and_index(BlockNumberOrTag::Latest, Index::from(0))
1815+
.await
1816+
.unwrap()
1817+
.unwrap()
1818+
.tx_hash();
1819+
1820+
// Assert cast with verbosity displays storage changes.
1821+
cmd.cast_fuse()
1822+
.args([
1823+
"run",
1824+
format!("{tx_hash}").as_str(),
1825+
"-vvvvv",
1826+
"--rpc-url",
1827+
&handle.http_endpoint(),
1828+
])
1829+
.assert_success()
1830+
.stdout_eq(str![[r#"
1831+
Executing previous transactions from the block.
1832+
Traces:
1833+
[22287] 0x5FbDB2315678afecb367f032d93F642f64180aa3::setNumber(111)
1834+
├─ storage changes:
1835+
│ @ 0: 0 → 111
1836+
└─ ← [Stop]
1837+
1838+
1839+
Transaction successfully executed.
1840+
[GAS]
1841+
1842+
"#]]);
1843+
});

crates/cli/src/opts/global.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ pub struct GlobalOpts {
1515
/// - 2 (-vv): Print logs for all tests.
1616
/// - 3 (-vvv): Print execution traces for failing tests.
1717
/// - 4 (-vvvv): Print execution traces for all tests, and setup traces for failing tests.
18-
/// - 5 (-vvvvv): Print execution and setup traces for all tests.
18+
/// - 5 (-vvvvv): Print execution and setup traces for all tests, including storage changes.
1919
#[arg(help_heading = "Display options", global = true, short, long, verbatim_doc_comment, conflicts_with = "quiet", action = ArgAction::Count)]
2020
verbosity: Verbosity,
2121

crates/cli/src/utils/cmd.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ use foundry_evm::{
1717
debug::{ContractSources, DebugTraceIdentifier},
1818
decode_trace_arena,
1919
identifier::{CachedSignatures, SignaturesIdentifier, TraceIdentifiers},
20-
render_trace_arena_with_bytecodes, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind,
21-
Traces,
20+
render_trace_arena_inner, CallTraceDecoder, CallTraceDecoderBuilder, TraceKind, Traces,
2221
},
2322
};
2423
use std::{
@@ -450,7 +449,7 @@ pub async fn handle_traces(
450449
decoder.debug_identifier = Some(DebugTraceIdentifier::new(sources));
451450
}
452451

453-
print_traces(&mut result, &decoder, shell::verbosity() > 0).await?;
452+
print_traces(&mut result, &decoder, shell::verbosity() > 0, shell::verbosity() > 4).await?;
454453

455454
Ok(())
456455
}
@@ -459,23 +458,31 @@ pub async fn print_traces(
459458
result: &mut TraceResult,
460459
decoder: &CallTraceDecoder,
461460
verbose: bool,
461+
state_changes: bool,
462462
) -> Result<()> {
463463
let traces = result.traces.as_mut().expect("No traces found");
464464

465-
sh_println!("Traces:")?;
465+
if !shell::is_json() {
466+
sh_println!("Traces:")?;
467+
}
468+
466469
for (_, arena) in traces {
467470
decode_trace_arena(arena, decoder).await?;
468-
sh_println!("{}", render_trace_arena_with_bytecodes(arena, verbose))?;
471+
sh_println!("{}", render_trace_arena_inner(arena, verbose, state_changes))?;
472+
}
473+
474+
if shell::is_json() {
475+
return Ok(());
469476
}
470-
sh_println!()?;
471477

478+
sh_println!()?;
472479
if result.success {
473480
sh_println!("{}", "Transaction successfully executed.".green())?;
474481
} else {
475482
sh_err!("Transaction failed.")?;
476483
}
477-
478484
sh_println!("Gas used: {}", result.gas_used)?;
485+
479486
Ok(())
480487
}
481488

crates/evm/evm/src/executors/trace.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,18 @@ impl TracingExecutor {
1818
version: Option<EvmVersion>,
1919
debug: bool,
2020
decode_internal: bool,
21+
with_state_changes: bool,
2122
alphanet: bool,
2223
) -> Self {
2324
let db = Backend::spawn(fork);
24-
let trace_mode =
25-
TraceMode::Call.with_debug(debug).with_decode_internal(if decode_internal {
25+
let trace_mode = TraceMode::Call
26+
.with_debug(debug)
27+
.with_decode_internal(if decode_internal {
2628
InternalTraceMode::Full
2729
} else {
2830
InternalTraceMode::None
29-
});
31+
})
32+
.with_state_changes(with_state_changes);
3033
Self {
3134
// configures a bare version of the evm executor: no cheatcode inspector is enabled,
3235
// tracing will be enabled only for the targeted transaction

crates/evm/traces/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ eyre.workspace = true
3636
futures.workspace = true
3737
itertools.workspace = true
3838
serde.workspace = true
39+
serde_json.workspace = true
3940
tokio = { workspace = true, features = ["time", "macros"] }
4041
tracing.workspace = true
4142
tempfile.workspace = true

crates/evm/traces/src/lib.rs

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ extern crate foundry_common;
1111
#[macro_use]
1212
extern crate tracing;
1313

14-
use foundry_common::contracts::{ContractsByAddress, ContractsByArtifact};
14+
use foundry_common::{
15+
contracts::{ContractsByAddress, ContractsByArtifact},
16+
shell,
17+
};
1518
use revm::interpreter::OpCode;
1619
use revm_inspectors::tracing::{
1720
types::{DecodedTraceStep, TraceMemberOrder},
@@ -183,15 +186,23 @@ pub async fn decode_trace_arena(
183186

184187
/// Render a collection of call traces to a string.
185188
pub fn render_trace_arena(arena: &SparsedTraceArena) -> String {
186-
render_trace_arena_with_bytecodes(arena, false)
189+
render_trace_arena_inner(arena, false, false)
187190
}
188191

189-
/// Render a collection of call traces to a string optionally including contract creation bytecodes.
190-
pub fn render_trace_arena_with_bytecodes(
192+
/// Render a collection of call traces to a string optionally including contract creation bytecodes
193+
/// and in JSON format.
194+
pub fn render_trace_arena_inner(
191195
arena: &SparsedTraceArena,
192196
with_bytecodes: bool,
197+
with_storage_changes: bool,
193198
) -> String {
194-
let mut w = TraceWriter::new(Vec::<u8>::new()).write_bytecodes(with_bytecodes);
199+
if shell::is_json() {
200+
return serde_json::to_string(&arena.resolve_arena()).expect("Failed to write traces");
201+
}
202+
203+
let mut w = TraceWriter::new(Vec::<u8>::new())
204+
.write_bytecodes(with_bytecodes)
205+
.with_storage_changes(with_storage_changes);
195206
w.write_arena(&arena.resolve_arena()).expect("Failed to write traces");
196207
String::from_utf8(w.into_writer()).expect("trace writer wrote invalid UTF-8")
197208
}
@@ -289,6 +300,8 @@ pub enum TraceMode {
289300
///
290301
/// Used by debugger.
291302
Debug,
303+
/// Debug trace with storage changes.
304+
RecordStateDiff,
292305
}
293306

294307
impl TraceMode {
@@ -308,6 +321,10 @@ impl TraceMode {
308321
matches!(self, Self::Jump)
309322
}
310323

324+
pub const fn record_state_diff(self) -> bool {
325+
matches!(self, Self::RecordStateDiff)
326+
}
327+
311328
pub const fn is_debug(self) -> bool {
312329
matches!(self, Self::Debug)
313330
}
@@ -324,6 +341,14 @@ impl TraceMode {
324341
std::cmp::max(self, mode.into())
325342
}
326343

344+
pub fn with_state_changes(self, yes: bool) -> Self {
345+
if yes {
346+
std::cmp::max(self, Self::RecordStateDiff)
347+
} else {
348+
self
349+
}
350+
}
351+
327352
pub fn with_verbosity(self, verbosiy: u8) -> Self {
328353
if verbosiy >= 3 {
329354
std::cmp::max(self, Self::Call)
@@ -345,7 +370,7 @@ impl TraceMode {
345370
StackSnapshotType::None
346371
},
347372
record_logs: true,
348-
record_state_diff: false,
373+
record_state_diff: self.record_state_diff(),
349374
record_returndata_snapshots: self.is_debug(),
350375
record_opcodes_filter: (self.is_jump() || self.is_jump_simple())
351376
.then(|| OpcodeFilter::new().enabled(OpCode::JUMP).enabled(OpCode::JUMPDEST)),

crates/forge/bin/cmd/test/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use forge::{
1212
debug::{ContractSources, DebugTraceIdentifier},
1313
decode_trace_arena, folded_stack_trace,
1414
identifier::SignaturesIdentifier,
15-
render_trace_arena, CallTraceDecoderBuilder, InternalTraceMode, TraceKind,
15+
CallTraceDecoderBuilder, InternalTraceMode, TraceKind,
1616
},
1717
MultiContractRunner, MultiContractRunnerBuilder, TestFilter, TestOptions, TestOptionsBuilder,
1818
};
@@ -56,7 +56,7 @@ use summary::TestSummaryReporter;
5656

5757
use crate::cmd::test::summary::print_invariant_metrics;
5858
pub use filter::FilterArgs;
59-
use forge::result::TestKind;
59+
use forge::{result::TestKind, traces::render_trace_arena_inner};
6060

6161
// Loads project's figment and merges the build cli arguments into it
6262
foundry_config::merge_impl_figment_convert!(TestArgs, opts, evm_opts);
@@ -652,7 +652,7 @@ impl TestArgs {
652652
// - 0..3: nothing
653653
// - 3: only display traces for failed tests
654654
// - 4: also display the setup trace for failed tests
655-
// - 5..: display all traces for all tests
655+
// - 5..: display all traces for all tests, including storage changes
656656
let should_include = match kind {
657657
TraceKind::Execution => {
658658
(verbosity == 3 && result.status.is_failure()) || verbosity >= 4
@@ -665,7 +665,7 @@ impl TestArgs {
665665

666666
if should_include {
667667
decode_trace_arena(arena, &decoder).await?;
668-
decoded_traces.push(render_trace_arena(arena));
668+
decoded_traces.push(render_trace_arena_inner(arena, false, verbosity > 4));
669669
}
670670
}
671671

0 commit comments

Comments
 (0)