Skip to content

Commit

Permalink
feat: port expectEmit cheatcode
Browse files Browse the repository at this point in the history
  • Loading branch information
onbjerg committed Mar 7, 2022
1 parent 0d6fb2d commit 094b805
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 15 deletions.
81 changes: 78 additions & 3 deletions forge/src/executor/inspector/cheatcodes/expect.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use super::Cheatcodes;
use crate::abi::HEVMCalls;
use bytes::Bytes;
use ethers::{abi::AbiEncode, types::Address};
use ethers::{
abi::{AbiEncode, RawLog},
types::{Address, H256},
};
use once_cell::sync::Lazy;
use revm::{return_ok, Database, EVMData, Return};
use revm::{return_ok, Database, EVMData, Interpreter, Return};
use std::str::FromStr;

/// For some cheatcodes we may internally change the status of the call, i.e. in `expectRevert`.
Expand Down Expand Up @@ -94,6 +97,71 @@ pub fn handle_expect_revert(
}
}

#[derive(Clone, Debug, Default)]
pub struct ExpectedEmit {
/// The depth at which we expect this emit to have occurred
pub depth: u64,
/// The log we expect
pub log: Option<RawLog>,
/// The checks to perform:
///
/// ┌───────┬───────┬───────┬────┐
/// │topic 1│topic 2│topic 3│data│
/// └───────┴───────┴───────┴────┘
pub checks: [bool; 4],
/// Whether the log was actually found in the subcalls
pub found: bool,
}

pub fn handle_expect_emit(state: &mut Cheatcodes, interpreter: &Interpreter, n: u8) {
// Decode the log
let (offset, len) =
(try_or_return!(interpreter.stack().peek(0)), try_or_return!(interpreter.stack().peek(1)));
let data = if len.is_zero() {
Vec::new()
} else {
interpreter.memory.get_slice(as_usize_or_return!(offset), as_usize_or_return!(len)).to_vec()
};

let n = n as usize;
let mut topics = Vec::with_capacity(n);
for i in 0..n {
let mut topic = H256::zero();
try_or_return!(interpreter.stack.peek(2 + i)).to_big_endian(topic.as_bytes_mut());
topics.push(topic);
}

// Fill or check the expected emits
if let Some(next_expect_to_fill) =
state.expected_emits.iter_mut().find(|expect| expect.log.is_none())
{
// We have unfilled expects, so we fill the first one
next_expect_to_fill.log = Some(RawLog { topics, data });
} else if let Some(next_expect) = state.expected_emits.iter_mut().find(|expect| !expect.found) {
// We do not have unfilled expects, so we try to match this log with the first unfound
// log that we expect
let expected =
next_expect.log.as_ref().expect("we should have a log to compare against here");
if expected.topics[0] == topics[0] {
// Topic 0 matches so the amount of topics in the expected and actual log should
// match here
let topics_match = topics
.iter()
.skip(1)
.enumerate()
.filter(|(i, _)| next_expect.checks[*i])
.all(|(i, topic)| topic == &expected.topics[i + 1]);

// Maybe check data
next_expect.found = if next_expect.checks[3] {
expected.data == data && topics_match
} else {
topics_match
};
}
}
}

pub fn apply<DB: Database>(
state: &mut Cheatcodes,
data: &mut EVMData<'_, DB>,
Expand All @@ -106,7 +174,14 @@ pub fn apply<DB: Database>(
HEVMCalls::ExpectRevert1(inner) => {
expect_revert(state, inner.0.to_vec().into(), data.subroutine.depth())
}
/* HEVMCalls::ExpectEmit(_) => {} */
HEVMCalls::ExpectEmit(inner) => {
state.expected_emits.push(ExpectedEmit {
depth: data.subroutine.depth() + 1,
checks: [inner.0, inner.1, inner.2, inner.3],
..Default::default()
});
Ok(Bytes::new())
}
HEVMCalls::ExpectCall(inner) => {
state.expected_calls.entry(inner.0).or_default().push(inner.1.to_vec().into());
Ok(Bytes::new())
Expand Down
61 changes: 49 additions & 12 deletions forge/src/executor/inspector/cheatcodes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ mod env;
pub use env::{Prank, RecordAccess};
/// Assertion helpers (such as `expectEmit`)
mod expect;
pub use expect::ExpectedRevert;
pub use expect::{ExpectedEmit, ExpectedRevert};
/// Cheatcodes that interact with the external environment (FFI etc.)
mod ext;
/// Cheatcodes that configure the fuzzer
mod fuzz;
/// Utility cheatcodes (`sign` etc.)
mod util;

use self::expect::{handle_expect_emit, handle_expect_revert};
use crate::{abi::HEVMCalls, executor::CHEATCODE_ADDRESS};
use bytes::Bytes;
use ethers::{
Expand All @@ -22,12 +23,11 @@ use revm::{
};
use std::collections::BTreeMap;

use self::expect::handle_expect_revert;

/// An inspector that handles calls to various cheatcodes, each with their own behavior.
///
/// Cheatcodes can be called by contracts during execution to modify the VM environment, such as
/// mocking addresses, signatures and altering call reverts.
#[derive(Default)]
pub struct Cheatcodes {
/// Whether FFI is enabled or not
ffi: bool,
Expand All @@ -49,19 +49,14 @@ pub struct Cheatcodes {

/// Expected calls
pub expected_calls: BTreeMap<Address, Vec<Bytes>>,

/// Expected emits
pub expected_emits: Vec<ExpectedEmit>,
}

impl Cheatcodes {
pub fn new(ffi: bool) -> Self {
Self {
ffi,
labels: BTreeMap::new(),
prank: None,
expected_revert: None,
accesses: None,
mocked_calls: BTreeMap::new(),
expected_calls: BTreeMap::new(),
}
Self { ffi, ..Default::default() }
}

fn apply_cheatcode<DB: Database>(
Expand Down Expand Up @@ -150,6 +145,7 @@ where
}

fn step(&mut self, interpreter: &mut Interpreter, _: &mut EVMData<'_, DB>, _: bool) -> Return {
// Record writes and reads if `record` has been called
if let Some(storage_accesses) = &mut self.accesses {
match interpreter.contract.code[interpreter.program_counter()] {
opcode::SLOAD => {
Expand All @@ -172,6 +168,18 @@ where
}
}

// Match logs if `expectEmit` has been called
if !self.expected_emits.is_empty() {
match interpreter.contract.code[interpreter.program_counter()] {
opcode::LOG0 => handle_expect_emit(self, interpreter, 0),
opcode::LOG1 => handle_expect_emit(self, interpreter, 1),
opcode::LOG2 => handle_expect_emit(self, interpreter, 2),
opcode::LOG3 => handle_expect_emit(self, interpreter, 3),
opcode::LOG4 => handle_expect_emit(self, interpreter, 4),
_ => (),
}
}

Return::Continue
}

Expand Down Expand Up @@ -207,6 +215,23 @@ where
}
}

// Handle expected emits at current depth
if !self
.expected_emits
.iter()
.filter(|expected| expected.depth == data.subroutine.depth())
.all(|expected| expected.found)
{
return (
Return::Revert,
remaining_gas,
"Log != expected log".to_string().encode().into(),
)
} else {
// Clear the emits we expected at this depth that have been found
self.expected_emits.retain(|expected| !expected.found)
}

// If the depth is 0, then this is the root call terminating
if data.subroutine.depth() == 0 {
// Handle expected calls that were not fulfilled
Expand All @@ -225,6 +250,18 @@ where
.into(),
)
}

// Check if we have any leftover expected emits
if !self.expected_emits.is_empty() {
return (
Return::Revert,
remaining_gas,
"Expected an emit, but no logs were emitted afterward"
.to_string()
.encode()
.into(),
)
}
}

(status, remaining_gas, retdata)
Expand Down

0 comments on commit 094b805

Please sign in to comment.