Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions crates/cheatcodes/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
87 changes: 67 additions & 20 deletions crates/cheatcodes/src/credible.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Mutex<&'a mut dyn DatabaseExt>>,
struct ThreadSafeDb {
// Im fine with leaking this because it only lives for the duration
// of assex execution
db: Arc<Mutex<&'static mut dyn DatabaseExt>>,
}

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(
Expand Down Expand Up @@ -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 };
Expand Down Expand Up @@ -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);

Expand All @@ -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<Vec<TracingInspector>>) =
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
Expand Down Expand Up @@ -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) => {
Expand Down
19 changes: 18 additions & 1 deletion crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -506,6 +506,9 @@ pub struct Cheatcodes {
/// Ignored traces.
pub ignored_traces: IgnoredTraces,

/// Assertion traces collected during assertion execution.
assertion_traces: Vec<CallTraceArena>,

/// Addresses with arbitrary storage.
pub arbitrary_storage: Option<ArbitraryStorage>,

Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -603,6 +607,19 @@ impl Cheatcodes {
self.wallets = Some(wallets);
}

/// Stores assertion traces collected during execution.
pub fn push_assertion_traces<I>(&mut self, traces: I)
where
I: IntoIterator<Item = CallTraceArena>,
{
self.assertion_traces.extend(traces);
}

/// Drains assertion traces for inclusion in test output.
pub fn take_assertion_traces(&mut self) -> Vec<CallTraceArena> {
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);
Expand Down
36 changes: 36 additions & 0 deletions crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,17 @@ impl CallTraceDecoder {
decoded[1] = DynSolValue::String("<pk>".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("<assertion bytecode>".to_string());
}
Some(decoded.iter().map(format_token).collect())
}
}
"parseJson" |
"parseJsonUint" |
"parseJsonUintArray" |
Expand Down Expand Up @@ -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], "\"<assertion bytecode>\"");
assert_eq!(decoded[2], format_token(&DynSolValue::FixedBytes(selector, 4)));
}
}
15 changes: 14 additions & 1 deletion crates/forge/src/cmd/test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -53,6 +53,16 @@ use std::{
};
use yansi::Paint;

fn assertion_trace_header(arena: &SparsedTraceArena) -> Option<String> {
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};
Expand Down Expand Up @@ -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));
}
}
Expand Down
11 changes: 9 additions & 2 deletions crates/forge/src/result.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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;
Expand Down
Loading