diff --git a/Cargo.lock b/Cargo.lock index 15687e1dc6338..bc5ac8832eb1d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1638,8 +1638,8 @@ 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" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=5ace6939c7027f1ba0bcf6f8751904dd4ca4f986#5ace6939c7027f1ba0bcf6f8751904dd4ca4f986" dependencies = [ "alloy", "assertion-da-core", @@ -1655,8 +1655,8 @@ 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" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=5ace6939c7027f1ba0bcf6f8751904dd4ca4f986#5ace6939c7027f1ba0bcf6f8751904dd4ca4f986" dependencies = [ "alloy", "serde", @@ -1664,8 +1664,8 @@ 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" +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 4f8beadc44a69..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 diff --git a/crates/cheatcodes/src/credible.rs b/crates/cheatcodes/src/credible.rs index 003794bbff7be..9253f3c63de39 100644 --- a/crates/cheatcodes/src/credible.rs +++ b/crates/cheatcodes/src/credible.rs @@ -5,11 +5,12 @@ 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::{TraceMode, TracingInspector, TracingInspectorConfig}; use foundry_fork_db::DatabaseError; use foundry_evm_core::backend::DatabaseExt; @@ -22,28 +23,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 +166,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 +202,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 +215,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 @@ -270,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;