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
80 changes: 80 additions & 0 deletions crates/cheatcodes/assets/cheatcodes.json

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

21 changes: 21 additions & 0 deletions crates/cheatcodes/spec/src/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,27 @@ interface Vm {
#[cheatcode(group = Testing, safety = Unsafe)]
function expectEmit(address emitter) external;

/// Prepare an expected anonymous log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).
/// Call this function, then emit an anonymous event, then call a function. Internally after the call, we check if
/// logs were emitted in the expected order with the expected topics and data (as specified by the booleans).
#[cheatcode(group = Testing, safety = Unsafe)]
function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external;

/// Same as the previous method, but also checks supplied address against emitting contract.
#[cheatcode(group = Testing, safety = Unsafe)]
function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter)
external;

/// Prepare an expected anonymous log with all topic and data checks enabled.
/// Call this function, then emit an anonymous event, then call a function. Internally after the call, we check if
/// logs were emitted in the expected order with the expected topics and data.
#[cheatcode(group = Testing, safety = Unsafe)]
function expectEmitAnonymous() external;

/// Same as the previous method, but also checks supplied address against emitting contract.
#[cheatcode(group = Testing, safety = Unsafe)]
function expectEmitAnonymous(address emitter) external;

/// Expects an error on next call with any revert data.
#[cheatcode(group = Testing, safety = Unsafe)]
function expectRevert() external;
Expand Down
115 changes: 83 additions & 32 deletions crates/cheatcodes/src/test/expect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,16 @@ pub struct ExpectedEmit {
pub log: Option<RawLog>,
/// The checks to perform:
/// ```text
/// ┌───────┬───────┬───────┬────┐
/// │topic 1│topic 2│topic 3│data│
/// └───────┴───────┴───────┴────┘
/// ┌───────┬───────┬───────┬───────┬────
/// │topic 0│topic 1│topic 2│topic 3│data│
/// └───────┴───────┴───────┴───────┴────
/// ```
pub checks: [bool; 4],
pub checks: [bool; 5],
/// If present, check originating address against this
pub address: Option<Address>,
/// If present, relax the requirement that topic 0 must be present. This allows anonymous
/// events with no indexed topics to be matched.
pub anonymous: bool,
/// Whether the log was actually found in the subcalls
pub found: bool,
}
Expand Down Expand Up @@ -202,8 +205,9 @@ impl Cheatcode for expectEmit_0Call {
expect_emit(
ccx.state,
ccx.ecx.journaled_state.depth(),
[checkTopic1, checkTopic2, checkTopic3, checkData],
[true, checkTopic1, checkTopic2, checkTopic3, checkData],
None,
false,
)
}
}
Expand All @@ -214,23 +218,64 @@ impl Cheatcode for expectEmit_1Call {
expect_emit(
ccx.state,
ccx.ecx.journaled_state.depth(),
[checkTopic1, checkTopic2, checkTopic3, checkData],
[true, checkTopic1, checkTopic2, checkTopic3, checkData],
Some(emitter),
false,
)
}
}

impl Cheatcode for expectEmit_2Call {
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self {} = self;
expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 4], None)
expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], None, false)
}
}

impl Cheatcode for expectEmit_3Call {
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { emitter } = *self;
expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 4], Some(emitter))
expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], Some(emitter), false)
}
}

impl Cheatcode for expectEmitAnonymous_0Call {
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { checkTopic0, checkTopic1, checkTopic2, checkTopic3, checkData } = *self;
expect_emit(
ccx.state,
ccx.ecx.journaled_state.depth(),
[checkTopic0, checkTopic1, checkTopic2, checkTopic3, checkData],
None,
true,
)
}
}

impl Cheatcode for expectEmitAnonymous_1Call {
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { checkTopic0, checkTopic1, checkTopic2, checkTopic3, checkData, emitter } = *self;
expect_emit(
ccx.state,
ccx.ecx.journaled_state.depth(),
[checkTopic0, checkTopic1, checkTopic2, checkTopic3, checkData],
Some(emitter),
true,
)
}
}

impl Cheatcode for expectEmitAnonymous_2Call {
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self {} = self;
expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], None, true)
}
}

impl Cheatcode for expectEmitAnonymous_3Call {
fn apply_stateful<DB: DatabaseExt>(&self, ccx: &mut CheatsCtxt<DB>) -> Result {
let Self { emitter } = *self;
expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], Some(emitter), true)
}
}

Expand Down Expand Up @@ -384,15 +429,17 @@ fn expect_call(
fn expect_emit(
state: &mut Cheatcodes,
depth: u64,
checks: [bool; 4],
checks: [bool; 5],
address: Option<Address>,
anonymous: bool,
) -> Result {
state.expected_emits.push_back(ExpectedEmit {
depth,
checks,
address,
found: false,
log: None,
anonymous,
});
Ok(Default::default())
}
Expand All @@ -412,7 +459,7 @@ pub(crate) fn handle_expect_emit(state: &mut Cheatcodes, log: &alloy_primitives:
return
}

// if there's anything to fill, we need to pop back.
// If there's anything to fill, we need to pop back.
// Otherwise, if there are any events that are unmatched, we try to match to match them
// in the order declared, so we start popping from the front (like a queue).
let mut event_to_fill_or_check =
Expand All @@ -424,38 +471,42 @@ pub(crate) fn handle_expect_emit(state: &mut Cheatcodes, log: &alloy_primitives:
.expect("we should have an emit to fill or check");

let Some(expected) = &event_to_fill_or_check.log else {
// Fill the event.
event_to_fill_or_check.log = Some(log.data.clone());
// Unless the caller is trying to match an anonymous event, the first topic must be
// filled.
// TODO: failing this check should probably cause a warning
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'd want to revert here ideally, should be possible after bluealloy/revm#1610

if event_to_fill_or_check.anonymous || log.topics().first().is_some() {
event_to_fill_or_check.log = Some(log.data.clone());
}
state.expected_emits.push_back(event_to_fill_or_check);
return
};

let expected_topic_0 = expected.topics().first();
let log_topic_0 = log.topics().first();

if expected_topic_0
.zip(log_topic_0)
.map_or(false, |(a, b)| a == b && expected.topics().len() == log.topics().len())
{
// Match topics
event_to_fill_or_check.found = log
event_to_fill_or_check.found = || -> bool {
// Topic count must match.
if expected.topics().len() != log.topics().len() {
return false
}
// Match topics according to the checks.
if !log
.topics()
.iter()
.skip(1)
.enumerate()
.filter(|(i, _)| event_to_fill_or_check.checks[*i])
.all(|(i, topic)| topic == &expected.topics()[i + 1]);

// Maybe match source address
if let Some(addr) = event_to_fill_or_check.address {
event_to_fill_or_check.found &= addr == log.address;
.all(|(i, topic)| topic == &expected.topics()[i])
{
return false
}

// Maybe match data
if event_to_fill_or_check.checks[3] {
event_to_fill_or_check.found &= expected.data.as_ref() == log.data.data.as_ref();
// Maybe match source address.
if event_to_fill_or_check.address.map_or(false, |addr| addr != log.address) {
return false;
}
}
// Maybe match data.
if event_to_fill_or_check.checks[4] && expected.data.as_ref() != log.data.data.as_ref() {
return false
}

true
}();

// If we found the event, we can push it to the back of the queue
// and begin expecting the next event.
Expand Down
4 changes: 4 additions & 0 deletions crates/forge/tests/it/repros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,10 @@ test_repro!(6634; |config| {
config.runner.config = Arc::new(prj_config);
});

// https://github.com/foundry-rs/foundry/issues/7457
test_repro!(7457);

// https://github.com/foundry-rs/foundry/issues/7481
test_repro!(7481);

// https://github.com/foundry-rs/foundry/issues/5739
Expand Down
4 changes: 4 additions & 0 deletions testdata/cheats/Vm.sol

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

Loading