From 357c0eeb76fd957cd21e5a64272760d43c3d9a03 Mon Sep 17 00:00:00 2001 From: makemake Date: Thu, 5 Feb 2026 17:35:02 +0100 Subject: [PATCH 1/3] feat: add assertion tracing support --- Cargo.lock | 9 +-- crates/cheatcodes/Cargo.toml | 8 +-- crates/cheatcodes/src/credible.rs | 104 ++++++++++++++++++++++++------ 3 files changed, 91 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 15687e1dc6338..e4b399cfc3706 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1638,8 +1638,7 @@ dependencies = [ [[package]] name = "assertion-da-client" -version = "0.9.2" -source = "git+https://github.com/phylaxsystems/credible-sdk.git?tag=0.9.2#ed8c0fa7d9cdbbc4abc1e6031c959b46c52a2a2e" +version = "1.0.7" dependencies = [ "alloy", "assertion-da-core", @@ -1655,8 +1654,7 @@ dependencies = [ [[package]] name = "assertion-da-core" -version = "0.9.2" -source = "git+https://github.com/phylaxsystems/credible-sdk.git?tag=0.9.2#ed8c0fa7d9cdbbc4abc1e6031c959b46c52a2a2e" +version = "1.0.7" dependencies = [ "alloy", "serde", @@ -1664,8 +1662,7 @@ dependencies = [ [[package]] name = "assertion-executor" -version = "0.9.2" -source = "git+https://github.com/phylaxsystems/credible-sdk.git?tag=0.9.2#ed8c0fa7d9cdbbc4abc1e6031c959b46c52a2a2e" +version = "1.0.7" dependencies = [ "alloy", "alloy-consensus", diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 4f8beadc44a69..eb3a28c978b18 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -17,11 +17,11 @@ workspace = true [build-dependencies] [dependencies] -assertion-executor = { git = "https://github.com/phylaxsystems/credible-sdk.git", tag = "0.9.2", features = [ - "phoundry", -] } +# assertion-executor = { git = "https://github.com/phylaxsystems/credible-sdk.git", tag = "0.9.2", features = [ +# "phoundry", +# ] } foundry-fork-db.workspace = true -# assertion-executor = { path = "../../../credible-sdk/crates/assertion-executor", features = ["phoundry"]} +assertion-executor = { path = "../../../credible-sdk/crates/assertion-executor", features = ["phoundry"]} foundry-cheatcodes-spec.workspace = true foundry-common.workspace = true diff --git a/crates/cheatcodes/src/credible.rs b/crates/cheatcodes/src/credible.rs index 003794bbff7be..645c755d9c6da 100644 --- a/crates/cheatcodes/src/credible.rs +++ b/crates/cheatcodes/src/credible.rs @@ -5,11 +5,14 @@ use assertion_executor::{ db::{DatabaseCommit, DatabaseRef, fork_db::ForkDb}, primitives::{ AccountInfo, Address, AssertionFunctionExecutionResult, B256, Bytecode, ExecutionResult, - TxEnv, U256, + TxEnv, TxValidationResult, U256, }, store::{AssertionState, AssertionStore}, }; use foundry_evm_core::{ContextExt, decode::RevertDecoder}; +use foundry_evm_traces::{ + SparsedTraceArena, TraceMode, TracingInspector, TracingInspectorConfig, render_trace_arena_inner, +}; use foundry_fork_db::DatabaseError; use foundry_evm_core::backend::DatabaseExt; @@ -22,28 +25,38 @@ use std::{ use revm::primitives::eip7825::TX_GAS_LIMIT_CAP; -/// Wrapper around DatabaseExt to make it thread-safe +/// Wrapper around DatabaseExt to make it thread-safe and 'static. +/// Uses a leaked reference internally to satisfy 'static bounds required by +/// the inspector API. This is safe because the wrapper is only used within +/// a single function scope and the original database outlives this usage. #[derive(Clone)] -struct ThreadSafeDb<'a> { - db: Arc>, +struct ThreadSafeDb { + // Im fine with leaking this because it only lives for the duration + // of assex execution + db: Arc>, } -impl std::fmt::Debug for ThreadSafeDb<'_> { +impl std::fmt::Debug for ThreadSafeDb { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "ThreadSafeDb") } } -/// Separate implementation block for constructor and helper methods -impl<'a> ThreadSafeDb<'a> { - /// Creates a new thread-safe database wrapper - pub fn new(db: &'a mut dyn DatabaseExt) -> Self { - Self { db: Arc::new(Mutex::new(db)) } +impl ThreadSafeDb { + /// Creates a new thread-safe database wrapper by leaking the reference. + /// # Safety + /// The caller must ensure the original database outlives all uses of this wrapper. + pub fn new(db: &mut dyn DatabaseExt) -> Self { + // SAFETY: We transmute the lifetime to 'static. This is safe because: + // 1. ThreadSafeDb is only used within execute_assertion's scope + // 2. The original ecx.db_mut() outlives this entire function + // 3. We don't store ThreadSafeDb beyond this function's execution + let static_ref: &'static mut dyn DatabaseExt = unsafe { std::mem::transmute(db) }; + Self { db: Arc::new(Mutex::new(static_ref)) } } } -/// Keep DatabaseRef implementation separate -impl<'a> DatabaseRef for ThreadSafeDb<'a> { +impl DatabaseRef for ThreadSafeDb { type Error = DatabaseError; fn basic_ref( @@ -155,13 +168,12 @@ pub fn execute_assertion( let state = ecx.journaled_state.state.clone(); let chain_id = ecx.cfg.chain_id; let base_tx_env = ecx.tx.clone(); + let verbosity = cheats.config.evm_opts.verbosity; let nonce = { let (db, journal, _) = ecx.as_db_env_and_journal(); journal.load_account(db, tx_attributes.caller).map(|acc| acc.info.nonce).unwrap_or(0) }; - // Setup assertion database - let db = ThreadSafeDb::new(*ecx.db_mut()); // Prepare assertion store let config = ExecutorConfig { spec_id, chain_id, assertion_gas_limit: TX_GAS_LIMIT_CAP }; @@ -192,8 +204,10 @@ pub fn execute_assertion( let mut assertion_executor = config.build(store); - // Commit current journal state so that it is available for assertions and - // triggering tx + // Setup assertion database with 'static lifetime (via transmute in ThreadSafeDb::new) + let db = ThreadSafeDb::new(*ecx.db_mut()); + + // Commit current journal state so that it is available for assertions and triggering tx let mut fork_db = ForkDb::new(db.clone()); fork_db.commit(state); @@ -203,10 +217,37 @@ pub fn execute_assertion( // TODO: Remove this once we have a proper way to handle this. let mut ext_db = revm::database::WrapDatabaseRef(fork_db.clone()); - // Execute assertion validation - let tx_validation = assertion_executor - .validate_transaction_ext_db(block, tx_env, &mut fork_db, &mut ext_db) - .map_err(|e| format!("Assertion Executor Error: {e:#?}"))?; + // Minimum verbosity for tracing (matches Foundry's -vvv) + const TRACING_VERBOSITY: u8 = 3; + + // Execute assertion validation with optional tracing based on verbosity + let (tx_validation, tracing_inspectors): (TxValidationResult, Option>) = + if verbosity >= TRACING_VERBOSITY { + // Create tracing inspector configured based on verbosity + let tracing_config = TraceMode::Call + .with_verbosity(verbosity) + .into_config() + .unwrap_or_else(TracingInspectorConfig::default_parity); + let tracing_inspector = TracingInspector::new(tracing_config); + + let result_with_inspectors = assertion_executor + .validate_transaction_with_inspector( + block, + &tx_env, + &mut fork_db, + &mut ext_db, + tracing_inspector, + ) + .map_err(|e| format!("Assertion Executor Error: {e:#?}"))?; + + (result_with_inspectors.result, Some(result_with_inspectors.inspectors)) + } else { + // No tracing - use the standard path + let result = assertion_executor + .validate_transaction_ext_db(block, &tx_env, &mut fork_db, &mut ext_db) + .map_err(|e| format!("Assertion Executor Error: {e:#?}"))?; + (result, None) + }; let mut inspector = executor.get_inspector(cheats); // if transaction execution reverted, log the revert reason @@ -258,6 +299,29 @@ pub fn execute_assertion( "Transaction gas cost: {tx_gas_used}\n Assertion gas cost: {total_assertion_gas}" )); + // Print traces if tracing was enabled + if let Some(inspectors) = tracing_inspectors { + let with_storage_changes = verbosity > 4; + for (i, tracing_inspector) in inspectors.into_iter().enumerate() { + let arena = tracing_inspector.into_traces(); + if arena.nodes().is_empty() { + continue; + } + + let traces = SparsedTraceArena { arena, ignored: Default::default() }; + let rendered = render_trace_arena_inner(&traces, false, with_storage_changes); + + if !rendered.is_empty() { + if i == 0 { + inspector.console_log("\nTransaction Execution Traces:"); + } else { + inspector.console_log(&format!("\nAssertion Function {} Traces:", i)); + } + inspector.console_log(&rendered); + } + } + } + // Drop the inspector to avoid borrow checker issues std::mem::drop(inspector); From 6946fd6dbb37d62f79a980e546b85929802c40c9 Mon Sep 17 00:00:00 2001 From: makemake Date: Fri, 6 Feb 2026 16:05:50 +0100 Subject: [PATCH 2/3] fix: improve readability --- crates/cheatcodes/src/credible.rs | 35 +++++++-------------------- crates/cheatcodes/src/inspector.rs | 19 ++++++++++++++- crates/evm/traces/src/decoder/mod.rs | 36 ++++++++++++++++++++++++++++ crates/forge/src/cmd/test/mod.rs | 15 +++++++++++- crates/forge/src/result.rs | 11 +++++++-- 5 files changed, 86 insertions(+), 30 deletions(-) diff --git a/crates/cheatcodes/src/credible.rs b/crates/cheatcodes/src/credible.rs index 645c755d9c6da..9253f3c63de39 100644 --- a/crates/cheatcodes/src/credible.rs +++ b/crates/cheatcodes/src/credible.rs @@ -10,9 +10,7 @@ use assertion_executor::{ store::{AssertionState, AssertionStore}, }; use foundry_evm_core::{ContextExt, decode::RevertDecoder}; -use foundry_evm_traces::{ - SparsedTraceArena, TraceMode, TracingInspector, TracingInspectorConfig, render_trace_arena_inner, -}; +use foundry_evm_traces::{TraceMode, TracingInspector, TracingInspectorConfig}; use foundry_fork_db::DatabaseError; use foundry_evm_core::backend::DatabaseExt; @@ -299,29 +297,6 @@ pub fn execute_assertion( "Transaction gas cost: {tx_gas_used}\n Assertion gas cost: {total_assertion_gas}" )); - // Print traces if tracing was enabled - if let Some(inspectors) = tracing_inspectors { - let with_storage_changes = verbosity > 4; - for (i, tracing_inspector) in inspectors.into_iter().enumerate() { - let arena = tracing_inspector.into_traces(); - if arena.nodes().is_empty() { - continue; - } - - let traces = SparsedTraceArena { arena, ignored: Default::default() }; - let rendered = render_trace_arena_inner(&traces, false, with_storage_changes); - - if !rendered.is_empty() { - if i == 0 { - inspector.console_log("\nTransaction Execution Traces:"); - } else { - inspector.console_log(&format!("\nAssertion Function {} Traces:", i)); - } - inspector.console_log(&rendered); - } - } - } - // Drop the inspector to avoid borrow checker issues std::mem::drop(inspector); @@ -334,6 +309,14 @@ pub fn execute_assertion( expected.max_depth = max(ecx.journaled_state.depth(), expected.max_depth); } + if let Some(inspectors) = tracing_inspectors { + let traces = inspectors + .into_iter() + .map(|tracing_inspector| tracing_inspector.into_traces()) + .filter(|arena| !arena.nodes().is_empty()); + cheats.push_assertion_traces(traces); + } + let mut inspector = executor.get_inspector(cheats); let (msg, result) = match &assertion_fn_result.result { AssertionFunctionExecutionResult::AssertionContractDeployFailure(r) => { diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index 6eac8e3ab48db..1a24b7ed5d8e1 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -44,7 +44,7 @@ use foundry_evm_core::{ evm::{FoundryEvm, new_evm_with_existing_context}, }; use foundry_evm_traces::{ - TracingInspector, TracingInspectorConfig, identifier::SignaturesIdentifier, + CallTraceArena, TracingInspector, TracingInspectorConfig, identifier::SignaturesIdentifier, }; use foundry_wallets::wallet_multi::MultiWallet; use itertools::Itertools; @@ -506,6 +506,9 @@ pub struct Cheatcodes { /// Ignored traces. pub ignored_traces: IgnoredTraces, + /// Assertion traces collected during assertion execution. + assertion_traces: Vec, + /// Addresses with arbitrary storage. pub arbitrary_storage: Option, @@ -572,6 +575,7 @@ impl Cheatcodes { intercept_next_create_call: Default::default(), test_runner: Default::default(), ignored_traces: Default::default(), + assertion_traces: Default::default(), arbitrary_storage: Default::default(), deprecated: Default::default(), wallets: Default::default(), @@ -603,6 +607,19 @@ impl Cheatcodes { self.wallets = Some(wallets); } + /// Stores assertion traces collected during execution. + pub fn push_assertion_traces(&mut self, traces: I) + where + I: IntoIterator, + { + self.assertion_traces.extend(traces); + } + + /// Drains assertion traces for inclusion in test output. + pub fn take_assertion_traces(&mut self) -> Vec { + std::mem::take(&mut self.assertion_traces) + } + /// Adds a delegation to the active delegations list. pub fn add_delegation(&mut self, authorization: SignedAuthorization) { self.active_delegations.push(authorization); diff --git a/crates/evm/traces/src/decoder/mod.rs b/crates/evm/traces/src/decoder/mod.rs index 4f377fe03c510..51570c239db8f 100644 --- a/crates/evm/traces/src/decoder/mod.rs +++ b/crates/evm/traces/src/decoder/mod.rs @@ -546,6 +546,17 @@ impl CallTraceDecoder { decoded[1] = DynSolValue::String("".to_string()); Some(decoded.iter().map(format_token).collect()) } + "assertion" => { + if self.verbosity >= 5 { + None + } else { + let mut decoded = func.abi_decode_input(&data[SELECTOR_LEN..]).ok()?; + if decoded.len() >= 2 { + decoded[1] = DynSolValue::String("".to_string()); + } + Some(decoded.iter().map(format_token).collect()) + } + } "parseJson" | "parseJsonUint" | "parseJsonUintArray" | @@ -1256,4 +1267,29 @@ mod tests { assert_eq!(result, expected, "Output case failed for: {function_signature}"); } } + + #[test] + fn test_assertion_bytecode_redacted() { + let decoder = CallTraceDecoder::new(); + let function = Function::parse("assertion(address,bytes,bytes4)").unwrap(); + let address = Address::repeat_byte(0x11); + + let mut selector_word = [0u8; 32]; + selector_word[..4].copy_from_slice(&[0x12, 0x34, 0x56, 0x78]); + let selector = B256::from(selector_word); + + let data = function + .abi_encode_input(&[ + DynSolValue::Address(address), + DynSolValue::Bytes(vec![0xde, 0xad, 0xbe, 0xef]), + DynSolValue::FixedBytes(selector, 4), + ]) + .unwrap(); + + let decoded = decoder.decode_cheatcode_inputs(&function, &data).expect("decoded inputs"); + assert_eq!(decoded.len(), 3); + assert_eq!(decoded[0], format_token(&DynSolValue::Address(address))); + assert_eq!(decoded[1], "\"\""); + assert_eq!(decoded[2], format_token(&DynSolValue::FixedBytes(selector, 4))); + } } diff --git a/crates/forge/src/cmd/test/mod.rs b/crates/forge/src/cmd/test/mod.rs index a37a422e6688b..889d839c3f27a 100644 --- a/crates/forge/src/cmd/test/mod.rs +++ b/crates/forge/src/cmd/test/mod.rs @@ -6,7 +6,7 @@ use crate::{ multi_runner::matches_artifact, result::{SuiteResult, TestOutcome, TestStatus}, traces::{ - CallTraceDecoderBuilder, InternalTraceMode, TraceKind, + CallTraceDecoderBuilder, InternalTraceMode, SparsedTraceArena, TraceKind, debug::{ContractSources, DebugTraceIdentifier}, decode_trace_arena, folded_stack_trace, identifier::SignaturesIdentifier, @@ -53,6 +53,16 @@ use std::{ }; use yansi::Paint; +fn assertion_trace_header(arena: &SparsedTraceArena) -> Option { + let signature = + arena.nodes().first()?.trace.decoded.as_ref()?.call_data.as_ref()?.signature.as_str(); + if !signature.starts_with("assertionCall") { + return None; + } + let name = signature.trim_end_matches("()"); + Some(format!("Assertion trace: {name}")) +} + mod filter; mod summary; use crate::{result::TestKind, traces::render_trace_arena_inner}; @@ -661,6 +671,9 @@ impl TestArgs { prune_trace_depth(arena, trace_depth); } + if let Some(header) = assertion_trace_header(arena) { + decoded_traces.push(header); + } decoded_traces.push(render_trace_arena_inner(arena, false, verbosity > 4)); } } diff --git a/crates/forge/src/result.rs b/crates/forge/src/result.rs index 7a0f42846f4c1..de8fc5554a799 100644 --- a/crates/forge/src/result.rs +++ b/crates/forge/src/result.rs @@ -17,7 +17,7 @@ use foundry_evm::{ decode::SkipReason, executors::{RawCallResult, invariant::InvariantMetrics}, fuzz::{CounterExample, FuzzCase, FuzzFixtures, FuzzTestResult}, - traces::{CallTraceArena, CallTraceDecoder, TraceKind, Traces}, + traces::{CallTraceArena, CallTraceDecoder, SparsedTraceArena, TraceKind, Traces}, }; use serde::{Deserialize, Serialize}; use std::{ @@ -574,7 +574,14 @@ impl TestResult { self.duration = Duration::default(); self.gas_report_traces = Vec::new(); - if let Some(cheatcodes) = raw_call_result.cheatcodes { + if let Some(mut cheatcodes) = raw_call_result.cheatcodes { + let assertion_traces = cheatcodes.take_assertion_traces(); + if !assertion_traces.is_empty() { + self.traces.extend(assertion_traces.into_iter().map(|arena| { + (TraceKind::Execution, SparsedTraceArena { arena, ignored: Default::default() }) + })); + } + self.breakpoints = cheatcodes.breakpoints; self.gas_snapshots = cheatcodes.gas_snapshots; self.deprecated_cheatcodes = cheatcodes.deprecated; From b34be19f8e37e635dd2febe6cdce48b11af27603 Mon Sep 17 00:00:00 2001 From: makemake Date: Fri, 6 Feb 2026 16:17:37 +0100 Subject: [PATCH 3/3] chore: use latest assex --- Cargo.lock | 3 +++ crates/cheatcodes/Cargo.toml | 7 +++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e4b399cfc3706..bc5ac8832eb1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1639,6 +1639,7 @@ dependencies = [ [[package]] name = "assertion-da-client" version = "1.0.7" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=5ace6939c7027f1ba0bcf6f8751904dd4ca4f986#5ace6939c7027f1ba0bcf6f8751904dd4ca4f986" dependencies = [ "alloy", "assertion-da-core", @@ -1655,6 +1656,7 @@ dependencies = [ [[package]] name = "assertion-da-core" version = "1.0.7" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=5ace6939c7027f1ba0bcf6f8751904dd4ca4f986#5ace6939c7027f1ba0bcf6f8751904dd4ca4f986" dependencies = [ "alloy", "serde", @@ -1663,6 +1665,7 @@ dependencies = [ [[package]] name = "assertion-executor" version = "1.0.7" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=5ace6939c7027f1ba0bcf6f8751904dd4ca4f986#5ace6939c7027f1ba0bcf6f8751904dd4ca4f986" dependencies = [ "alloy", "alloy-consensus", diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index eb3a28c978b18..26aa9fcc28478 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -17,11 +17,10 @@ workspace = true [build-dependencies] [dependencies] -# assertion-executor = { git = "https://github.com/phylaxsystems/credible-sdk.git", tag = "0.9.2", features = [ -# "phoundry", -# ] } foundry-fork-db.workspace = true -assertion-executor = { path = "../../../credible-sdk/crates/assertion-executor", features = ["phoundry"]} +# sdk version 1.0.7 with custom inspector commit +# done to not have cyclical version bumps between sdk and phoundry +assertion-executor = { git = "https://github.com/phylaxsystems/credible-sdk.git", rev = "5ace6939c7027f1ba0bcf6f8751904dd4ca4f986", features = ["phoundry"] } foundry-cheatcodes-spec.workspace = true foundry-common.workspace = true