Skip to content

Commit

Permalink
feat(cheatcodes): mark unmatched expectedEmits as unemitted
Browse files Browse the repository at this point in the history
If an emitted event is marked as unmatched, it can then be hilighted
as an error in red.
  • Loading branch information
topocount committed Aug 17, 2024
1 parent f8aa4af commit b30842e
Show file tree
Hide file tree
Showing 8 changed files with 68 additions and 9 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -289,4 +289,6 @@ alloy-transport-http = { git = "https://github.com/alloy-rs/alloy", rev = "511ae
alloy-transport-ipc = { git = "https://github.com/alloy-rs/alloy", rev = "511ae98" }
alloy-transport-ws = { git = "https://github.com/alloy-rs/alloy", rev = "511ae98" }
revm = { git = "https://github.com/bluealloy/revm", rev = "caadc71" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "caadc71" }
revm-primitives = { git = "https://github.com/bluealloy/revm", rev = "caadc71" }
# revm-inspectors = { path = "../revm-inspectors" }
revm-inspectors = { git = "https://github.com/topocount/revm-inspectors", rev = "0ec9274" }
7 changes: 6 additions & 1 deletion crates/cheatcodes/assets/cheatcodes.json

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

3 changes: 3 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ interface Vm {
/// Error thrown by cheatcodes.
error CheatcodeError(string message);

/// Error thrown by unemitted events
error UnemittedEventError(uint16 positionExpected);

/// A modification applied to either `msg.sender` or `tx.origin`. Returned by `readCallers`.
enum CallerMode {
/// No caller modification is currently active.
Expand Down
18 changes: 14 additions & 4 deletions crates/cheatcodes/src/inspector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use crate::{
};
use alloy_primitives::{hex, Address, Bytes, Log, TxKind, B256, U256};
use alloy_rpc_types::request::{TransactionInput, TransactionRequest};
use alloy_sol_types::{SolCall, SolInterface, SolValue};
use alloy_sol_types::{SolCall, SolError, SolInterface};
use foundry_common::{evm::Breakpoints, TransactionMaybeSigned, SELECTOR_LEN};
use foundry_config::Config;
use foundry_evm_core::{
Expand Down Expand Up @@ -266,6 +266,8 @@ pub struct Cheatcodes {
pub expected_calls: ExpectedCallTracker,
/// Expected emits
pub expected_emits: VecDeque<ExpectedEmit>,
/// counter for expected emits that have been matched and cleared
pub expected_emits_offset: u16,

/// Map of context depths to memory offset ranges that may be written to within the call depth.
pub allowed_mem_writes: FxHashMap<u64, Vec<Range<u64>>>,
Expand Down Expand Up @@ -347,6 +349,7 @@ impl Cheatcodes {
mocked_calls: Default::default(),
expected_calls: Default::default(),
expected_emits: Default::default(),
expected_emits_offset: Default::default(),
allowed_mem_writes: Default::default(),
broadcast: Default::default(),
broadcastable_transactions: Default::default(),
Expand Down Expand Up @@ -1163,15 +1166,22 @@ impl<DB: DatabaseExt> Inspector<DB> for Cheatcodes {
!call.is_static;
if should_check_emits {
// Not all emits were matched.
if self.expected_emits.iter().any(|expected| !expected.found) {
if let Some(not_found) = self.expected_emits.iter().find(|expected| !expected.found) {
outcome.result.result = InstructionResult::Revert;
outcome.result.output = "log != expected log".abi_encode().into();
// Where the revert is set and where color mode might be
// indicated for a given event that wasn't matched
outcome.result.output = Vm::UnemittedEventError {
positionExpected: not_found.sequence + self.expected_emits_offset,
}
.abi_encode()
.into();
return outcome;
} else {
// All emits were found, we're good.
// Clear the queue, as we expect the user to declare more events for the next call
// if they wanna match further events.
self.expected_emits.clear()
self.expected_emits_offset += self.expected_emits.len() as u16;
self.expected_emits.clear();
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/cheatcodes/src/test/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ pub struct ExpectedEmit {
pub anonymous: bool,
/// Whether the log was actually found in the subcalls
pub found: bool,
/// the order in which the log was expected
pub sequence: u16,
}

impl Cheatcode for expectCall_0Call {
Expand Down Expand Up @@ -440,6 +442,7 @@ fn expect_emit(
found: false,
log: None,
anonymous,
sequence: state.expected_emits.len() as u16,
});
Ok(Default::default())
}
Expand Down
30 changes: 30 additions & 0 deletions crates/evm/core/src/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ use revm::interpreter::InstructionResult;
use rustc_hash::FxHashMap;
use std::sync::OnceLock;

pub enum VmErr {
UnemittedEventError(u16),
None,
}

/// Decode a set of logs, only returning logs from DSTest logging events and Hardhat's `console.log`
pub fn decode_console_logs(logs: &[Log]) -> Vec<String> {
logs.iter().filter_map(decode_console_log).collect()
Expand Down Expand Up @@ -103,10 +108,32 @@ impl RevertDecoder {
})
}

pub fn decode_structured(&self, err: &[u8], status: Option<InstructionResult>) -> VmErr {
if err.len() < SELECTOR_LEN || status != Some(InstructionResult::Revert) {
return VmErr::None;
}

let (selector, data) = err.split_at(SELECTOR_LEN);
let selector: &[u8; 4] = selector.try_into().unwrap();

match *selector {
Vm::UnemittedEventError::SELECTOR => {
let Some(e) = Vm::UnemittedEventError::abi_decode_raw(data, false).ok() else {
return VmErr::None;
};
return VmErr::UnemittedEventError(e.positionExpected);
}
_ => {
return VmErr::None;
}
}
}

/// Tries to decode an error message from the given revert bytes.
///
/// See [`decode`](Self::decode) for more information.
pub fn maybe_decode(&self, err: &[u8], status: Option<InstructionResult>) -> Option<String> {
// TODO: Convert to return an optional structured error as a tuple
if err.len() < SELECTOR_LEN {
if let Some(status) = status {
if !status.is_ok() {
Expand Down Expand Up @@ -149,6 +176,9 @@ impl RevertDecoder {
let e = Vm::expectRevert_1Call::abi_decode_raw(data, false).ok()?;
return self.maybe_decode(&e.revertData[..], status);
}
Vm::UnemittedEventError::SELECTOR => {
return Some("log != expected log".to_string());
}
_ => {}
}

Expand Down
9 changes: 8 additions & 1 deletion crates/evm/traces/src/decoder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use foundry_evm_core::{
CALLER, CHEATCODE_ADDRESS, DEFAULT_CREATE2_DEPLOYER, HARDHAT_CONSOLE_ADDRESS,
TEST_CONTRACT_ADDRESS,
},
decode::RevertDecoder,
decode::{RevertDecoder, VmErr},
precompiles::{
BLAKE_2F, EC_ADD, EC_MUL, EC_PAIRING, EC_RECOVER, IDENTITY, MOD_EXP, POINT_EVALUATION,
RIPEMD_160, SHA_256,
Expand Down Expand Up @@ -325,6 +325,13 @@ impl CallTraceDecoder {
pub async fn populate_traces(&self, traces: &mut Vec<CallTraceNode>) {
for node in traces {
node.trace.decoded = self.decode_function(&node.trace).await;

if let VmErr::UnemittedEventError(error_index) =
self.revert_decoder.decode_structured(&node.trace.output, Some(node.trace.status))
{
node.logs[error_index as usize].unmatched = true;
}

for log in node.logs.iter_mut() {
log.decoded = self.decode_event(&log.raw_log).await;
}
Expand Down

0 comments on commit b30842e

Please sign in to comment.