diff --git a/Cargo.lock b/Cargo.lock index ce2195fa90f01..080597f55270f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "alloy" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d1aecf3cab3d0e7383064ce488616434b4ade10d8904dff422e74203c712f" +checksum = "0cd691724d088287334932cdef99b79ecb89ea01fff33a12552093495fdc99d1" dependencies = [ "alloy-consensus", "alloy-contract", @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "alloy-node-bindings" -version = "1.0.17" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d4f3ab81744547ab8283aa94c9670d3880ac826642e52739e3c4ecbfa84857a" +checksum = "f70cdf0ba711fb6f1a427fd026e149bd6d64c9da4c020c1c96c69245c4721ac1" dependencies = [ "alloy-genesis", "alloy-hardforks", @@ -1553,7 +1553,7 @@ dependencies = [ [[package]] name = "assertion-da-client" version = "0.2.0" -source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=2e62531#2e625312536d187a6216b56bdd69fef8bc772fae" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=d17125a#d17125a44eaf1ead0335fbee4a8e1117a43ab79d" dependencies = [ "alloy", "assertion-da-core", @@ -1570,7 +1570,7 @@ dependencies = [ [[package]] name = "assertion-da-core" version = "0.2.0" -source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=2e62531#2e625312536d187a6216b56bdd69fef8bc772fae" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=d17125a#d17125a44eaf1ead0335fbee4a8e1117a43ab79d" dependencies = [ "alloy", "serde", @@ -1579,7 +1579,7 @@ dependencies = [ [[package]] name = "assertion-executor" version = "0.2.0" -source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=2e62531#2e625312536d187a6216b56bdd69fef8bc772fae" +source = "git+https://github.com/phylaxsystems/credible-sdk.git?rev=d17125a#d17125a44eaf1ead0335fbee4a8e1117a43ab79d" dependencies = [ "alloy", "alloy-consensus", diff --git a/crates/cheatcodes/Cargo.toml b/crates/cheatcodes/Cargo.toml index 37b3471e8fdfb..b307038c5cbe8 100644 --- a/crates/cheatcodes/Cargo.toml +++ b/crates/cheatcodes/Cargo.toml @@ -22,7 +22,8 @@ vergen = { workspace = true, default-features = false, features = [ ] } [dependencies] -assertion-executor = { git = "https://github.com/phylaxsystems/credible-sdk.git", rev = "8145495", default-features = false } +assertion-executor = { git = "https://github.com/phylaxsystems/credible-sdk.git", rev = "d17125a", 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/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index e0deb12f19256..6d65bfc9fbe80 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -19,20 +19,11 @@ sol! { #[derive(Debug, Cheatcode)] // Keep this list small to avoid unnecessary bloat. #[sol(abi)] interface Vm { - /// Simple transaction struct for assertion execution testing - struct AssertionExTransaction { - /// The address of the sender - address from; - /// The address of the receiver - address to; - /// The value of the transaction - uint256 value; - /// The abi encoded calldata of the transaction - bytes data; - } - /// Gets the address for a given private key. + + // Used to execute assertion against the next call or contract creation. + // Will revert if the assertion is not triggered. #[cheatcode(group = Credible, safety = Safe)] - function assertionEx(bytes calldata tx, address assertionAdopter, bytes calldata assertionContract, string calldata assertionContractLabel) external; + function assertion(address adopter, bytes calldata createData, bytes4 fnSelector) external; // ======== Types ======== diff --git a/crates/cheatcodes/src/credible.rs b/crates/cheatcodes/src/credible.rs index c67e4cafe1fcc..6638d74dc778d 100644 --- a/crates/cheatcodes/src/credible.rs +++ b/crates/cheatcodes/src/credible.rs @@ -1,16 +1,24 @@ -use crate::{Cheatcode, CheatcodesExecutor, CheatsCtxt, Result, Vm::*}; -use alloy_primitives::TxKind; -use alloy_sol_types::{Revert, SolError, SolValue}; +use crate::{inspector::Ecx, Cheatcode, Cheatcodes, CheatcodesExecutor, CheatsCtxt, Result, Vm::*}; +use alloy_primitives::{Bytes, FixedBytes, TxKind}; +use alloy_sol_types::{Revert, SolError, SolEvent, SolValue}; use assertion_executor::{ db::{fork_db::ForkDb, DatabaseCommit, DatabaseRef}, - primitives::{AccountInfo, Address, Bytecode, ExecutionResult, TxEnv, B256, U256}, + primitives::{ + AccountInfo, Address, AssertionFunctionExecutionResult, Bytecode, ExecutionResult, TxEnv, + B256, U256, + }, store::{AssertionState, AssertionStore}, ExecutorConfig, }; -use foundry_evm_core::backend::{DatabaseError, DatabaseExt}; -use revm::context_interface::ContextTr; + +use foundry_evm_core::{ + abi::console::ds::Console, + backend::{DatabaseError, DatabaseExt}, +}; +use revm::context_interface::{ContextTr, JournalTr}; use std::{ - collections::HashMap, + cmp::max, + collections::HashSet, sync::{Arc, Mutex}, }; @@ -62,127 +70,203 @@ impl<'a> DatabaseRef for ThreadSafeDb<'a> { } } -impl Cheatcode for assertionExCall { - fn apply_full(&self, ccx: &mut CheatsCtxt, executor: &mut dyn CheatcodesExecutor) -> Result { - let Self { - tx, - assertionAdopter: assertion_adopter, - assertionContract, - assertionContractLabel, - } = self; +impl Cheatcode for assertionCall { + fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { adopter, createData: create_data, fnSelector: fn_selector } = self; - let spec_id = ccx.ecx.cfg.spec; - let block = ccx.ecx.block.clone(); - let state = ccx.ecx.journaled_state.state.clone(); - let chain_id = ccx.ecx.cfg.chain_id; - - // Setup assertion database - let db = ThreadSafeDb::new(ccx.ecx.db()); + ensure!( + ccx.state.assertion.is_none(), + "you must call another function prior to setting another assertion" + ); + let assertion = Assertion { + adopter: *adopter, + create_data: create_data.to_vec(), + fn_selector: *fn_selector, + depth: ccx.ecx.journaled_state.depth(), + }; - // Prepare assertion store - let assertion_contract_bytecode = Bytecode::new_legacy(assertionContract.to_vec().into()); + ccx.state.assertion = Some(assertion); + Ok(Default::default()) + } +} +#[derive(Debug, Clone)] +pub struct Assertion { + pub adopter: Address, + pub create_data: Vec, + pub fn_selector: FixedBytes<4>, + pub depth: usize, +} - let config = ExecutorConfig { spec_id, chain_id, assertion_gas_limit: 100_000 }; +pub struct TxAttributes { + pub value: U256, + pub data: Bytes, + pub caller: Address, + pub kind: TxKind, +} - let store = AssertionStore::new_ephemeral().expect("Failed to create assertion store"); +/// Used to handle assertion execution in inspector in calls after the cheatcode was called. +pub fn execute_assertion( + assertion: &Assertion, + tx_attributes: TxAttributes, + ecx: Ecx, + executor: &mut dyn CheatcodesExecutor, + cheats: &mut Cheatcodes, + is_create: bool, +) -> Result, crate::Error> { + let spec_id = ecx.cfg.spec; + let block = ecx.block.clone(); + let state = ecx.journaled_state.state.clone(); + let chain_id = ecx.cfg.chain_id; + + let nonce = ecx.db().basic(tx_attributes.caller).unwrap_or_default().unwrap_or_default().nonce; + // Setup assertion database + let db = ThreadSafeDb::new(ecx.db()); + + // Prepare assertion store + + let config = ExecutorConfig { spec_id, chain_id, assertion_gas_limit: 100_000 }; + + let store = AssertionStore::new_ephemeral().expect("Failed to create assertion store"); + + let mut assertion_state = + AssertionState::new_active(assertion.create_data.clone().into(), &config) + .expect("Failed to create assertion state"); + + // Filter triggers for one fn selector + for fn_selectors in assertion_state.trigger_recorder.triggers.values_mut() { + if fn_selectors.contains(&assertion.fn_selector) { + *fn_selectors = HashSet::from_iter([assertion.fn_selector]); + } else { + *fn_selectors = HashSet::new(); + } + } - let assertion_state = - AssertionState::new_active(assertion_contract_bytecode.bytes(), &config) - .expect("Failed to create assertion state"); + store.insert(assertion.adopter, assertion_state).expect("Failed to store assertions"); + let tx_env = TxEnv { + caller: tx_attributes.caller, + gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), + gas_price: block.basefee.into(), + chain_id: Some(chain_id), + value: tx_attributes.value, + data: tx_attributes.data, + kind: tx_attributes.kind, + nonce, + ..Default::default() + }; + + let mut assertion_executor = config.build(store); + + // 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); + + // Odysseas: This is a hack to use the new unified codepath for validate_transaction_ext_db + // Effectively, we are applying the transaction in a clone of the currently running database + // which is then used by the fork_db. + // TODO: Remove this once we have a proper way to handle this. + let mut ext_db = revm::database::WrapDatabaseRef(fork_db.clone()); + + // Store assertions + let tx_validation = assertion_executor + .validate_transaction_ext_db(block.clone(), tx_env.clone(), &mut fork_db, &mut ext_db) + .map_err(|e| format!("Assertion Executor Error: {e:#?}"))?; + + ecx.journaled_state.inner.checkpoint(); + + if let Some(expected) = &mut cheats.expected_revert { + expected.max_depth = max(ecx.journaled_state.depth(), expected.max_depth); + } - store.insert(*assertion_adopter, assertion_state).expect("Failed to store assertions"); + let mut inspector = executor.get_inspector(cheats); + // if transaction execution reverted, log the revert reason + if !tx_validation.result_and_state.result.is_success() { + inspector.console_log(&format!( + "Transaction reverted: {}", + decode_invalidated_assertion(&tx_validation.result_and_state.result).reason() + )); + } - let decoded_tx = AssertionExTransaction::abi_decode(tx)?; + // else get information about the assertion execution + let total_assertion_gas = tx_validation.total_assertions_gas(); + let total_assertions_ran = tx_validation.total_assertion_funcs_ran(); + let tx_gas_used = tx_validation.result_and_state.result.gas_used(); - let tx_env = TxEnv { - caller: decoded_tx.from, - gas_limit: block.gas_limit.try_into().unwrap_or(u64::MAX), - kind: TxKind::Call(decoded_tx.to), - value: decoded_tx.value, - data: decoded_tx.data, - chain_id: Some(chain_id), - gas_price: block.basefee.into(), - ..Default::default() - }; + if total_assertions_ran != 1 { + bail!("Expected 1 assertion to be executed, but {total_assertions_ran} were executed."); + } - let mut assertion_executor = config.build(store); - - // 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); - - // Odysseas: This is a hack to use the new unified codepath for validate_transaction_ext_db - // Effectively, we are applying the transaction in a clone of the currently running database - // which is then used by the fork_db. - // TODO: Remove this once we have a proper way to handle this. - let mut ext_db = revm::database::WrapDatabaseRef(fork_db.clone()); - - // Store assertions - 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:#?}"))?; - - // if transaction execution reverted, bail - if !tx_validation.result_and_state.result.is_success() { - let decoded_error = - decode_invalidated_assertion(&tx_validation.result_and_state.result); - executor.console_log(ccx, &format!("Transaction reverted: {}", decoded_error.reason())); - bail!("Transaction Reverted"); + //Expect is safe because we validate above that 1 assertion was ran. + let assertion_contract = tx_validation + .assertions_executions + .first() + .expect("Expected 1 assertion to be executed, but got 0"); + + let assertion_fn_result = assertion_contract + .assertion_fns_results + .first() + .expect("Expected 1 assertion to be executed, but got 0"); + + for log in assertion_fn_result.as_result().logs() { + if Some(&Console::log::SIGNATURE_HASH) == log.topics().first() { + let decoded_log = Console::log::decode_log(log); + if let Ok(log_data) = decoded_log { + inspector.console_log(&format!("{}", log_data.val)); + } } - // else get information about the assertion execution - let assertion_contract = tx_validation.assertions_executions.first().unwrap(); - let total_assertion_gas = tx_validation.total_assertions_gas(); - let total_assertions_ran = tx_validation.total_assertion_funcs_ran(); - let tx_gas_used = tx_validation.result_and_state.result.gas_used(); - let mut assertion_gas_message = format!( - "Transaction gas cost: {tx_gas_used}\n Total Assertion gas cost: {total_assertion_gas}\n Total assertions ran: {total_assertions_ran}\n Assertion Functions gas cost\n " - ); + } - // Format individual assertion function results - for (fn_selector_index, assertion_fn) in - assertion_contract.assertion_fns_results.iter().enumerate() - { - assertion_gas_message.push_str(&format!( - " └─ [selector {}:index {}] gas cost: {}\n", - assertion_fn.id.fn_selector, - fn_selector_index, - assertion_fn.as_result().gas_used() - )); - } - executor.console_log(ccx, &assertion_gas_message); - - if !tx_validation.is_valid() { - let mut error_msg = format!("\n {assertionContractLabel} Enforced Assertions:\n"); - // Collect failed assertions - let reverted_assertions: HashMap<_, _> = assertion_contract - .assertion_fns_results - .iter() - .enumerate() - .filter(|(_, assertion_fn)| !assertion_fn.is_success()) - .map(|(fn_selector_index, assertion_fn)| { - let key = format!( - "[selector {}:index {}]", - assertion_fn.id.fn_selector, fn_selector_index - ); - let revert = decode_invalidated_assertion(assertion_fn.as_result()); - (key, revert) - }) - .collect(); - - // Format error messages - for (key, revert) in reverted_assertions { - error_msg.push_str(&format!( - " └─ {} - Revert Reason: {} \n", - key, - revert.reason() + let assertion_gas_message = format!( + "Transaction gas cost: {tx_gas_used}\n Assertion gas cost: {total_assertion_gas}\n " + ); + inspector.console_log(&assertion_gas_message); + + if !tx_validation.is_valid() { + match &assertion_fn_result.result { + AssertionFunctionExecutionResult::AssertionContractDeployFailure(result) => { + inspector.console_log(&format!( + "Assertion contract deploy failed: {}", + decode_invalidated_assertion(&result).reason() )); + let output = result.output().unwrap_or_default(); + return Err(crate::Error::from(output.clone())); + } + AssertionFunctionExecutionResult::AssertionExecutionResult(result) => { + inspector.console_log(&format!( + "Assertion function reverted: {}", + decode_invalidated_assertion(&result).reason() + )); + let output = result.output().unwrap_or_default(); + return Err(crate::Error::from(output.clone())); } - - executor.console_log(ccx, &error_msg); - bail!("Assertions Reverted"); } - Ok(Default::default()) + } else { + let journaled_state = &mut ecx.journaled_state.state; + for (address, account) in tx_validation.result_and_state.state { + let journaled_acct = journaled_state.get_mut(&address); + match journaled_acct { + Some(journaled_acct) => { + journaled_acct.info = account.info; + journaled_acct.status = journaled_acct.status.union(account.status); + for (index, value) in account.storage.iter() { + journaled_acct.storage.insert(*index, value.clone()); + } + } + None => { + journaled_state.insert(address, account); + } + } + } + } + + if is_create { + let address: Option
= + tx_validation.result_and_state.result.output().map(|output| { + Address::abi_decode(output).expect("Could not decode address from create output") + }); + Ok(address) + } else { + Ok(None) } } @@ -205,8 +289,8 @@ fn decode_invalidated_assertion(execution_result: &ExecutionResult) -> Revert { #[cfg(test)] mod tests { use super::*; - use alloy_primitives::Bytes; use assertion_executor::primitives::HaltReason; + use revm::context::result::{Output, SuccessReason}; #[test] fn test_decode_revert_error_success() { @@ -215,8 +299,8 @@ mod tests { gas_used: 0, gas_refunded: 0, logs: vec![], - output: revm::context::result::Output::Call(Bytes::new()), - reason: revm::context::result::SuccessReason::Return, + output: Output::Call(Bytes::new()), + reason: SuccessReason::Return, }; let revert = decode_invalidated_assertion(&result); assert_eq!(revert.reason(), "Tried to decode invalidated assertion, but result was success. This is a bug in phoundry. Please report to the Phylax team."); diff --git a/crates/cheatcodes/src/inspector.rs b/crates/cheatcodes/src/inspector.rs index e2eb84ec522bf..df6bdcd0ddf3d 100644 --- a/crates/cheatcodes/src/inspector.rs +++ b/crates/cheatcodes/src/inspector.rs @@ -393,6 +393,9 @@ pub struct Cheatcodes { /// Expected revert information pub expected_revert: Option, + /// Assertion information + pub assertion: Option, + /// Assume next call can revert and discard fuzz run if it does. pub assume_no_revert: Option, @@ -529,6 +532,7 @@ impl Cheatcodes { mocked_functions: Default::default(), expected_calls: Default::default(), expected_emits: Default::default(), + assertion: Default::default(), expected_creates: Default::default(), allowed_mem_writes: Default::default(), broadcast: Default::default(), @@ -982,6 +986,43 @@ impl Cheatcodes { }]); } + if let Some(assertion) = self.assertion.take() { + let tx_attributes = crate::credible::TxAttributes { + value: call.call_value(), + data: call.input.bytes(ecx), + caller: call.caller, + kind: TxKind::Call(call.target_address), + }; + + let call_outcome = match crate::credible::execute_assertion( + &assertion, + tx_attributes, + ecx, + executor, + self, + false, + ) { + Ok(_) => Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: Default::default(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }), + Err(err) => Some(CallOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: err.abi_encode().into(), + gas, + }, + memory_offset: call.return_memory_offset.clone(), + }), + }; + + return call_outcome; + } + None } @@ -1147,8 +1188,8 @@ impl Inspector> for Cheatcodes { } } - fn call(&mut self, ecx: Ecx, inputs: &mut CallInputs) -> Option { - Self::call_with_executor(self, ecx, inputs, &mut TransparentCheatcodesExecutor) + fn call(&mut self, ecx: Ecx, call: &mut CallInputs) -> Option { + Self::call_with_executor(self, ecx, call, &mut TransparentCheatcodesExecutor) } fn call_end(&mut self, ecx: Ecx, call: &CallInputs, outcome: &mut CallOutcome) { @@ -1193,10 +1234,10 @@ impl Inspector> for Cheatcodes { assume_no_revert.reverted_by = Some(call.target_address); } - // allow multiple cheatcode calls at the same depth let curr_depth = ecx.journaled_state.depth(); + // allow multiple cheatcode calls at the same depth if curr_depth <= assume_no_revert.depth && !cheatcode_call { - // Discard run if we're at the same depth as cheatcode, call reverted, and no + // discard run if we're at the same depth as cheatcode, call reverted, and no // specific reason was supplied if outcome.result.is_revert() { let assume_no_revert = std::mem::take(&mut self.assume_no_revert).unwrap(); @@ -1663,6 +1704,42 @@ impl Inspector> for Cheatcodes { depth: curr_depth as u64, }]); } + if let Some(assertion) = self.assertion.take() { + let tx_attributes = crate::credible::TxAttributes { + value: input.value(), + data: input.init_code(), + caller: input.caller(), + kind: TxKind::Create, + }; + + let call_outcome = match crate::credible::execute_assertion( + &assertion, + tx_attributes, + ecx, + &mut TransparentCheatcodesExecutor, + self, + true, + ) { + Ok(address) => Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Return, + output: Default::default(), + gas, + }, + address, + }), + Err(err) => Some(CreateOutcome { + result: InterpreterResult { + result: InstructionResult::Revert, + output: err.abi_encode().into(), + gas, + }, + address: None, + }), + }; + + return call_outcome; + } None } diff --git a/crates/cheatcodes/src/lib.rs b/crates/cheatcodes/src/lib.rs index 9ee36570286f3..625bc3c76e578 100644 --- a/crates/cheatcodes/src/lib.rs +++ b/crates/cheatcodes/src/lib.rs @@ -62,7 +62,7 @@ mod toml; mod utils; -mod credible; +pub mod credible; /// Cheatcode implementation. pub(crate) trait Cheatcode: CheatcodeDef + DynCheatcode {