diff --git a/crates/cheatcodes/assets/cheatcodes.json b/crates/cheatcodes/assets/cheatcodes.json index 9c7e36965be0a..a6a9c94795ee1 100644 --- a/crates/cheatcodes/assets/cheatcodes.json +++ b/crates/cheatcodes/assets/cheatcodes.json @@ -4531,6 +4531,86 @@ "status": "stable", "safety": "unsafe" }, + { + "func": { + "id": "expectEmitAnonymous_0", + "description": "Prepare an expected anonymous log with (bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData.).\nCall this function, then emit an anonymous event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data (as specified by the booleans).", + "declaration": "function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external;", + "visibility": "external", + "mutability": "", + "signature": "expectEmitAnonymous(bool,bool,bool,bool,bool)", + "selector": "0xc948db5e", + "selectorBytes": [ + 201, + 72, + 219, + 94 + ] + }, + "group": "testing", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "expectEmitAnonymous_1", + "description": "Same as the previous method, but also checks supplied address against emitting contract.", + "declaration": "function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) external;", + "visibility": "external", + "mutability": "", + "signature": "expectEmitAnonymous(bool,bool,bool,bool,bool,address)", + "selector": "0x71c95899", + "selectorBytes": [ + 113, + 201, + 88, + 153 + ] + }, + "group": "testing", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "expectEmitAnonymous_2", + "description": "Prepare an expected anonymous log with all topic and data checks enabled.\nCall this function, then emit an anonymous event, then call a function. Internally after the call, we check if\nlogs were emitted in the expected order with the expected topics and data.", + "declaration": "function expectEmitAnonymous() external;", + "visibility": "external", + "mutability": "", + "signature": "expectEmitAnonymous()", + "selector": "0x2e5f270c", + "selectorBytes": [ + 46, + 95, + 39, + 12 + ] + }, + "group": "testing", + "status": "stable", + "safety": "unsafe" + }, + { + "func": { + "id": "expectEmitAnonymous_3", + "description": "Same as the previous method, but also checks supplied address against emitting contract.", + "declaration": "function expectEmitAnonymous(address emitter) external;", + "visibility": "external", + "mutability": "", + "signature": "expectEmitAnonymous(address)", + "selector": "0x6fc68705", + "selectorBytes": [ + 111, + 198, + 135, + 5 + ] + }, + "group": "testing", + "status": "stable", + "safety": "unsafe" + }, { "func": { "id": "expectEmit_0", diff --git a/crates/cheatcodes/spec/src/vm.rs b/crates/cheatcodes/spec/src/vm.rs index 54f1fe8fe77f7..a009c9f598e25 100644 --- a/crates/cheatcodes/spec/src/vm.rs +++ b/crates/cheatcodes/spec/src/vm.rs @@ -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; diff --git a/crates/cheatcodes/src/test/expect.rs b/crates/cheatcodes/src/test/expect.rs index 9c070a7ca8874..f2b7cbc065466 100644 --- a/crates/cheatcodes/src/test/expect.rs +++ b/crates/cheatcodes/src/test/expect.rs @@ -84,13 +84,16 @@ pub struct ExpectedEmit { pub log: Option, /// 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
, + /// 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, } @@ -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, ) } } @@ -214,8 +218,9 @@ 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, ) } } @@ -223,14 +228,54 @@ impl Cheatcode for expectEmit_1Call { impl Cheatcode for expectEmit_2Call { fn apply_stateful(&self, ccx: &mut CheatsCtxt) -> 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(&self, ccx: &mut CheatsCtxt) -> 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(&self, ccx: &mut CheatsCtxt) -> 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(&self, ccx: &mut CheatsCtxt) -> 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(&self, ccx: &mut CheatsCtxt) -> 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(&self, ccx: &mut CheatsCtxt) -> Result { + let Self { emitter } = *self; + expect_emit(ccx.state, ccx.ecx.journaled_state.depth(), [true; 5], Some(emitter), true) } } @@ -384,8 +429,9 @@ fn expect_call( fn expect_emit( state: &mut Cheatcodes, depth: u64, - checks: [bool; 4], + checks: [bool; 5], address: Option
, + anonymous: bool, ) -> Result { state.expected_emits.push_back(ExpectedEmit { depth, @@ -393,6 +439,7 @@ fn expect_emit( address, found: false, log: None, + anonymous, }); Ok(Default::default()) } @@ -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 = @@ -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 + 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. diff --git a/crates/forge/tests/it/repros.rs b/crates/forge/tests/it/repros.rs index b7b6c1063cf0c..f1b1c8a96ef4b 100644 --- a/crates/forge/tests/it/repros.rs +++ b/crates/forge/tests/it/repros.rs @@ -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 diff --git a/testdata/cheats/Vm.sol b/testdata/cheats/Vm.sol index cf72e88475d8e..9c008c4254f4a 100644 --- a/testdata/cheats/Vm.sol +++ b/testdata/cheats/Vm.sol @@ -222,6 +222,10 @@ interface Vm { function expectCall(address callee, uint256 msgValue, bytes calldata data, uint64 count) external; function expectCall(address callee, uint256 msgValue, uint64 gas, bytes calldata data) external; function expectCall(address callee, uint256 msgValue, uint64 gas, bytes calldata data, uint64 count) external; + function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; + function expectEmitAnonymous(bool checkTopic0, bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) external; + function expectEmitAnonymous() external; + function expectEmitAnonymous(address emitter) external; function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData) external; function expectEmit(bool checkTopic1, bool checkTopic2, bool checkTopic3, bool checkData, address emitter) external; function expectEmit() external; diff --git a/testdata/default/repros/Issue7457.t.sol b/testdata/default/repros/Issue7457.t.sol new file mode 100644 index 0000000000000..8d9d6f0753d5c --- /dev/null +++ b/testdata/default/repros/Issue7457.t.sol @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity 0.8.18; + +import "ds-test/test.sol"; +import "cheats/Vm.sol"; + +interface ITarget { + event AnonymousEventEmpty() anonymous; + event AnonymousEventNonIndexed(uint256 a) anonymous; + + event DifferentAnonymousEventEmpty() anonymous; + event DifferentAnonymousEventNonIndexed(string a) anonymous; + + event AnonymousEventWith1Topic(uint256 indexed a, uint256 b) anonymous; + event AnonymousEventWith2Topics(uint256 indexed a, uint256 indexed b, uint256 c) anonymous; + event AnonymousEventWith3Topics(uint256 indexed a, uint256 indexed b, uint256 indexed c, uint256 d) anonymous; + event AnonymousEventWith4Topics( + uint256 indexed a, uint256 indexed b, uint256 indexed c, uint256 indexed d, uint256 e + ) anonymous; +} + +contract Target is ITarget { + function emitAnonymousEventEmpty() external { + emit AnonymousEventEmpty(); + } + + function emitAnonymousEventNonIndexed(uint256 a) external { + emit AnonymousEventNonIndexed(a); + } + + function emitAnonymousEventWith1Topic(uint256 a, uint256 b) external { + emit AnonymousEventWith1Topic(a, b); + } + + function emitAnonymousEventWith2Topics(uint256 a, uint256 b, uint256 c) external { + emit AnonymousEventWith2Topics(a, b, c); + } + + function emitAnonymousEventWith3Topics(uint256 a, uint256 b, uint256 c, uint256 d) external { + emit AnonymousEventWith3Topics(a, b, c, d); + } + + function emitAnonymousEventWith4Topics(uint256 a, uint256 b, uint256 c, uint256 d, uint256 e) external { + emit AnonymousEventWith4Topics(a, b, c, d, e); + } +} + +// https://github.com/foundry-rs/foundry/issues/7457 +contract Issue7457Test is DSTest, ITarget { + Vm constant vm = Vm(HEVM_ADDRESS); + + Target public target; + + function setUp() external { + target = new Target(); + } + + function testEmitEvent() public { + vm.expectEmitAnonymous(false, false, false, false, true); + emit AnonymousEventEmpty(); + target.emitAnonymousEventEmpty(); + } + + function testFailEmitEventNonIndexed() public { + vm.expectEmit(false, false, false, true); + emit AnonymousEventNonIndexed(1); + target.emitAnonymousEventNonIndexed(1); + } + + function testEmitEventNonIndexed() public { + vm.expectEmitAnonymous(false, false, false, false, true); + emit AnonymousEventNonIndexed(1); + target.emitAnonymousEventNonIndexed(1); + } + + // function testFailEmitDifferentEvent() public { + // vm.expectEmitAnonymous(false, false, false, true); + // emit DifferentAnonymousEventEmpty(); + // target.emitAnonymousEventEmpty(); + // } + + function testFailEmitDifferentEventNonIndexed() public { + vm.expectEmitAnonymous(false, false, false, false, true); + emit DifferentAnonymousEventNonIndexed("1"); + target.emitAnonymousEventNonIndexed(1); + } + + function testEmitEventWith1Topic() public { + vm.expectEmitAnonymous(true, false, false, false, true); + emit AnonymousEventWith1Topic(1, 2); + target.emitAnonymousEventWith1Topic(1, 2); + } + + function testEmitEventWith2Topics() public { + vm.expectEmitAnonymous(true, true, false, false, true); + emit AnonymousEventWith2Topics(1, 2, 3); + target.emitAnonymousEventWith2Topics(1, 2, 3); + } + + function testEmitEventWith3Topics() public { + vm.expectEmitAnonymous(true, true, true, false, true); + emit AnonymousEventWith3Topics(1, 2, 3, 4); + target.emitAnonymousEventWith3Topics(1, 2, 3, 4); + } + + function testEmitEventWith4Topics() public { + vm.expectEmitAnonymous(true, true, true, true, true); + emit AnonymousEventWith4Topics(1, 2, 3, 4, 5); + target.emitAnonymousEventWith4Topics(1, 2, 3, 4, 5); + } +}