@@ -4,10 +4,14 @@ use std::{
44} ;
55
66use crate :: { Cheatcode , Cheatcodes , CheatsCtxt , Error , Result , Vm :: * } ;
7+ use alloy_dyn_abi:: { DynSolValue , EventExt } ;
8+ use alloy_json_abi:: Event ;
79use alloy_primitives:: {
8- Address , Bytes , LogData as RawLog , U256 ,
10+ Address , Bytes , LogData as RawLog , U256 , hex ,
911 map:: { AddressHashMap , HashMap , hash_map:: Entry } ,
1012} ;
13+ use foundry_common:: { abi:: get_indexed_event, fmt:: format_token} ;
14+ use foundry_evm_traces:: DecodedCallLog ;
1115use revm:: {
1216 context:: JournalTr ,
1317 interpreter:: {
@@ -110,6 +114,8 @@ pub struct ExpectedEmit {
110114 pub found : bool ,
111115 /// Number of times the log is expected to be emitted
112116 pub count : u64 ,
117+ /// Stores mismatch details if a log didn't match
118+ pub mismatch_error : Option < String > ,
113119}
114120
115121#[ derive( Clone , Debug ) ]
@@ -762,8 +768,16 @@ fn expect_emit(
762768 anonymous : bool ,
763769 count : u64 ,
764770) -> 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+ } ;
767781 if let Some ( found_emit_pos) = state. expected_emits . iter ( ) . position ( |( emit, _) | emit. found ) {
768782 // The order of emits already found (back of queue) should not be modified, hence push any
769783 // new emit before first found emit.
@@ -857,11 +871,41 @@ pub(crate) fn handle_expect_emit(
857871
858872 event_to_fill_or_check. found = || -> bool {
859873 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+ ) ) ;
860896 return false ;
861897 }
862898
863899 // 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+ ) ) ;
865909 return false ;
866910 }
867911
@@ -1019,6 +1063,214 @@ fn checks_topics_and_data(checks: [bool; 5], expected: &RawLog, log: &RawLog) ->
10191063 true
10201064}
10211065
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+
10221274fn expect_safe_memory ( state : & mut Cheatcodes , start : u64 , end : u64 , depth : u64 ) -> Result {
10231275 ensure ! ( start < end, "memory range start ({start}) is greater than end ({end})" ) ;
10241276 #[ expect( clippy:: single_range_in_vec_init) ] // Wanted behaviour
0 commit comments