@@ -4,10 +4,14 @@ use std::{
4
4
} ;
5
5
6
6
use crate :: { Cheatcode , Cheatcodes , CheatsCtxt , Error , Result , Vm :: * } ;
7
+ use alloy_dyn_abi:: { DynSolValue , EventExt } ;
8
+ use alloy_json_abi:: Event ;
7
9
use alloy_primitives:: {
8
- Address , Bytes , LogData as RawLog , U256 ,
10
+ Address , Bytes , LogData as RawLog , U256 , hex ,
9
11
map:: { AddressHashMap , HashMap , hash_map:: Entry } ,
10
12
} ;
13
+ use foundry_common:: { abi:: get_indexed_event, fmt:: format_token} ;
14
+ use foundry_evm_traces:: DecodedCallLog ;
11
15
use revm:: {
12
16
context:: JournalTr ,
13
17
interpreter:: {
@@ -110,6 +114,8 @@ pub struct ExpectedEmit {
110
114
pub found : bool ,
111
115
/// Number of times the log is expected to be emitted
112
116
pub count : u64 ,
117
+ /// Stores mismatch details if a log didn't match
118
+ pub mismatch_error : Option < String > ,
113
119
}
114
120
115
121
#[ derive( Clone , Debug ) ]
@@ -762,8 +768,16 @@ fn expect_emit(
762
768
anonymous : bool ,
763
769
count : u64 ,
764
770
) -> Result {
765
- let expected_emit =
766
- ExpectedEmit { depth, checks, address, found : false , log : None , anonymous, count } ;
771
+ let expected_emit = ExpectedEmit {
772
+ depth,
773
+ checks,
774
+ address,
775
+ found : false ,
776
+ log : None ,
777
+ anonymous,
778
+ count,
779
+ mismatch_error : None ,
780
+ } ;
767
781
if let Some ( found_emit_pos) = state. expected_emits . iter ( ) . position ( |( emit, _) | emit. found ) {
768
782
// The order of emits already found (back of queue) should not be modified, hence push any
769
783
// new emit before first found emit.
@@ -857,11 +871,41 @@ pub(crate) fn handle_expect_emit(
857
871
858
872
event_to_fill_or_check. found = || -> bool {
859
873
if !checks_topics_and_data ( event_to_fill_or_check. checks , expected, log) {
874
+ // Store detailed mismatch information
875
+
876
+ // Try to decode the events if we have a signature identifier
877
+ let ( expected_decoded, actual_decoded) = if let Some ( signatures_identifier) =
878
+ & state. signatures_identifier
879
+ && !event_to_fill_or_check. anonymous
880
+ {
881
+ (
882
+ decode_event ( signatures_identifier, expected) ,
883
+ decode_event ( signatures_identifier, log) ,
884
+ )
885
+ } else {
886
+ ( None , None )
887
+ } ;
888
+ event_to_fill_or_check. mismatch_error = Some ( get_emit_mismatch_message (
889
+ event_to_fill_or_check. checks ,
890
+ expected,
891
+ log,
892
+ event_to_fill_or_check. anonymous ,
893
+ expected_decoded. as_ref ( ) ,
894
+ actual_decoded. as_ref ( ) ,
895
+ ) ) ;
860
896
return false ;
861
897
}
862
898
863
899
// Maybe match source address.
864
- if event_to_fill_or_check. address . is_some_and ( |addr| addr != log. address ) {
900
+ if event_to_fill_or_check
901
+ . address
902
+ . is_some_and ( |addr| addr. to_checksum ( None ) != log. address . to_checksum ( None ) )
903
+ {
904
+ event_to_fill_or_check. mismatch_error = Some ( format ! (
905
+ "log emitter mismatch: expected={:#x}, got={:#x}" ,
906
+ event_to_fill_or_check. address. unwrap( ) ,
907
+ log. address
908
+ ) ) ;
865
909
return false ;
866
910
}
867
911
@@ -1019,6 +1063,214 @@ fn checks_topics_and_data(checks: [bool; 5], expected: &RawLog, log: &RawLog) ->
1019
1063
true
1020
1064
}
1021
1065
1066
+ fn decode_event (
1067
+ identifier : & foundry_evm_traces:: identifier:: SignaturesIdentifier ,
1068
+ log : & RawLog ,
1069
+ ) -> Option < DecodedCallLog > {
1070
+ let topics = log. topics ( ) ;
1071
+ if topics. is_empty ( ) {
1072
+ return None ;
1073
+ }
1074
+ let t0 = topics[ 0 ] ; // event sig
1075
+ // Try to identify the event
1076
+ let event = foundry_common:: block_on ( identifier. identify_event ( t0) ) ?;
1077
+
1078
+ // Check if event already has indexed information from signatures
1079
+ let has_indexed_info = event. inputs . iter ( ) . any ( |p| p. indexed ) ;
1080
+ // Only use get_indexed_event if the event doesn't have indexing info
1081
+ let indexed_event = if has_indexed_info { event } else { get_indexed_event ( event, log) } ;
1082
+
1083
+ // Try to decode the event
1084
+ if let Ok ( decoded) = indexed_event. decode_log ( log) {
1085
+ let params = reconstruct_params ( & indexed_event, & decoded) ;
1086
+
1087
+ let decoded_params = params
1088
+ . into_iter ( )
1089
+ . zip ( indexed_event. inputs . iter ( ) )
1090
+ . map ( |( param, input) | ( input. name . clone ( ) , format_token ( & param) ) )
1091
+ . collect ( ) ;
1092
+
1093
+ return Some ( DecodedCallLog {
1094
+ name : Some ( indexed_event. name ) ,
1095
+ params : Some ( decoded_params) ,
1096
+ } ) ;
1097
+ }
1098
+
1099
+ None
1100
+ }
1101
+
1102
+ /// Restore the order of the params of a decoded event
1103
+ fn reconstruct_params ( event : & Event , decoded : & alloy_dyn_abi:: DecodedEvent ) -> Vec < DynSolValue > {
1104
+ let mut indexed = 0 ;
1105
+ let mut unindexed = 0 ;
1106
+ let mut inputs = vec ! [ ] ;
1107
+ for input in & event. inputs {
1108
+ if input. indexed && indexed < decoded. indexed . len ( ) {
1109
+ inputs. push ( decoded. indexed [ indexed] . clone ( ) ) ;
1110
+ indexed += 1 ;
1111
+ } else if unindexed < decoded. body . len ( ) {
1112
+ inputs. push ( decoded. body [ unindexed] . clone ( ) ) ;
1113
+ unindexed += 1 ;
1114
+ }
1115
+ }
1116
+ inputs
1117
+ }
1118
+
1119
+ /// Gets a detailed mismatch message for emit assertions
1120
+ pub ( crate ) fn get_emit_mismatch_message (
1121
+ checks : [ bool ; 5 ] ,
1122
+ expected : & RawLog ,
1123
+ actual : & RawLog ,
1124
+ is_anonymous : bool ,
1125
+ expected_decoded : Option < & DecodedCallLog > ,
1126
+ actual_decoded : Option < & DecodedCallLog > ,
1127
+ ) -> String {
1128
+ // Early return for completely different events or incompatible structures
1129
+
1130
+ // 1. Different number of topics
1131
+ if actual. topics ( ) . len ( ) != expected. topics ( ) . len ( ) {
1132
+ return name_mismatched_logs ( expected_decoded, actual_decoded) ;
1133
+ }
1134
+
1135
+ // 2. Different event signatures (for non-anonymous events)
1136
+ if !is_anonymous
1137
+ && checks[ 0 ]
1138
+ && ( !expected. topics ( ) . is_empty ( ) && !actual. topics ( ) . is_empty ( ) )
1139
+ && expected. topics ( ) [ 0 ] != actual. topics ( ) [ 0 ]
1140
+ {
1141
+ return name_mismatched_logs ( expected_decoded, actual_decoded) ;
1142
+ }
1143
+
1144
+ let expected_data = expected. data . as_ref ( ) ;
1145
+ let actual_data = actual. data . as_ref ( ) ;
1146
+
1147
+ // 3. Check data
1148
+ if checks[ 4 ] && expected_data != actual_data {
1149
+ // Different lengths or not ABI-encoded
1150
+ if expected_data. len ( ) != actual_data. len ( )
1151
+ || !expected_data. len ( ) . is_multiple_of ( 32 )
1152
+ || expected_data. is_empty ( )
1153
+ {
1154
+ return name_mismatched_logs ( expected_decoded, actual_decoded) ;
1155
+ }
1156
+ }
1157
+
1158
+ // expected and actual events are the same, so check individual parameters
1159
+ let mut mismatches = Vec :: new ( ) ;
1160
+
1161
+ // Check topics (indexed parameters)
1162
+ for ( i, ( expected_topic, actual_topic) ) in
1163
+ expected. topics ( ) . iter ( ) . zip ( actual. topics ( ) . iter ( ) ) . enumerate ( )
1164
+ {
1165
+ // Skip topic[0] for non-anonymous events (already checked above)
1166
+ if i == 0 && !is_anonymous {
1167
+ continue ;
1168
+ }
1169
+
1170
+ // Only check if the corresponding check flag is set
1171
+ if i < checks. len ( ) && checks[ i] && expected_topic != actual_topic {
1172
+ let param_idx = if is_anonymous {
1173
+ i // For anonymous events, topic[0] is param 0
1174
+ } else {
1175
+ i - 1 // For regular events, topic[0] is event signature, so topic[1] is param 0
1176
+ } ;
1177
+ mismatches
1178
+ . push ( format ! ( "param {param_idx}: expected={expected_topic}, got={actual_topic}" ) ) ;
1179
+ }
1180
+ }
1181
+
1182
+ // Check data (non-indexed parameters)
1183
+ if checks[ 4 ] && expected_data != actual_data {
1184
+ let num_indexed_params = if is_anonymous {
1185
+ expected. topics ( ) . len ( )
1186
+ } else {
1187
+ expected. topics ( ) . len ( ) . saturating_sub ( 1 )
1188
+ } ;
1189
+
1190
+ for ( i, ( expected_chunk, actual_chunk) ) in
1191
+ expected_data. chunks ( 32 ) . zip ( actual_data. chunks ( 32 ) ) . enumerate ( )
1192
+ {
1193
+ if expected_chunk != actual_chunk {
1194
+ let param_idx = num_indexed_params + i;
1195
+ mismatches. push ( format ! (
1196
+ "param {}: expected={}, got={}" ,
1197
+ param_idx,
1198
+ hex:: encode_prefixed( expected_chunk) ,
1199
+ hex:: encode_prefixed( actual_chunk)
1200
+ ) ) ;
1201
+ }
1202
+ }
1203
+ }
1204
+
1205
+ if mismatches. is_empty ( ) {
1206
+ name_mismatched_logs ( expected_decoded, actual_decoded)
1207
+ } else {
1208
+ // Build the error message with event names if available
1209
+ let event_prefix = match ( expected_decoded, actual_decoded) {
1210
+ ( Some ( expected_dec) , Some ( actual_dec) ) if expected_dec. name == actual_dec. name => {
1211
+ format ! (
1212
+ "{} param mismatch" ,
1213
+ expected_dec. name. as_ref( ) . unwrap_or( & "log" . to_string( ) )
1214
+ )
1215
+ }
1216
+ _ => {
1217
+ if is_anonymous {
1218
+ "anonymous log mismatch" . to_string ( )
1219
+ } else {
1220
+ "log mismatch" . to_string ( )
1221
+ }
1222
+ }
1223
+ } ;
1224
+
1225
+ // Add parameter details if available from decoded events
1226
+ let detailed_mismatches = if let ( Some ( expected_dec) , Some ( actual_dec) ) =
1227
+ ( expected_decoded, actual_decoded)
1228
+ && let ( Some ( expected_params) , Some ( actual_params) ) =
1229
+ ( & expected_dec. params , & actual_dec. params )
1230
+ {
1231
+ mismatches
1232
+ . into_iter ( )
1233
+ . map ( |basic_mismatch| {
1234
+ // Try to find the parameter name and decoded value
1235
+ if let Some ( param_idx) = basic_mismatch
1236
+ . split ( ' ' )
1237
+ . nth ( 1 )
1238
+ . and_then ( |s| s. trim_end_matches ( ':' ) . parse :: < usize > ( ) . ok ( ) )
1239
+ && param_idx < expected_params. len ( )
1240
+ && param_idx < actual_params. len ( )
1241
+ {
1242
+ let ( expected_name, expected_value) = & expected_params[ param_idx] ;
1243
+ let ( _actual_name, actual_value) = & actual_params[ param_idx] ;
1244
+ let param_name = if !expected_name. is_empty ( ) {
1245
+ expected_name
1246
+ } else {
1247
+ & format ! ( "param{param_idx}" )
1248
+ } ;
1249
+ return format ! (
1250
+ "{param_name}: expected={expected_value}, got={actual_value}" ,
1251
+ ) ;
1252
+ }
1253
+ basic_mismatch
1254
+ } )
1255
+ . collect :: < Vec < _ > > ( )
1256
+ } else {
1257
+ mismatches
1258
+ } ;
1259
+
1260
+ format ! ( "{} at {}" , event_prefix, detailed_mismatches. join( ", " ) )
1261
+ }
1262
+ }
1263
+
1264
+ /// Formats the generic mismatch message: "log != expected log" to include event names if available
1265
+ fn name_mismatched_logs (
1266
+ expected_decoded : Option < & DecodedCallLog > ,
1267
+ actual_decoded : Option < & DecodedCallLog > ,
1268
+ ) -> String {
1269
+ let expected_name = expected_decoded. and_then ( |d| d. name . as_deref ( ) ) . unwrap_or ( "log" ) ;
1270
+ let actual_name = actual_decoded. and_then ( |d| d. name . as_deref ( ) ) . unwrap_or ( "log" ) ;
1271
+ format ! ( "{actual_name} != expected {expected_name}" )
1272
+ }
1273
+
1022
1274
fn expect_safe_memory ( state : & mut Cheatcodes , start : u64 , end : u64 , depth : u64 ) -> Result {
1023
1275
ensure ! ( start < end, "memory range start ({start}) is greater than end ({end})" ) ;
1024
1276
#[ expect( clippy:: single_range_in_vec_init) ] // Wanted behaviour
0 commit comments