diff --git a/crates/sol-macro-expander/src/expand/event.rs b/crates/sol-macro-expander/src/expand/event.rs index de66728a03..a4674280c4 100644 --- a/crates/sol-macro-expander/src/expand/event.rs +++ b/crates/sol-macro-expander/src/expand/event.rs @@ -98,6 +98,18 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, event: &ItemEvent) -> Result .enumerate() .map(|(i, p)| expand_event_topic_field(i, p, p.name.as_ref(), cx)); + let check_signature = (!anonymous).then(|| { + quote! { + #[inline] + fn check_signature(topics: &::RustType) -> alloy_sol_types::Result<()> { + if topics.0 != Self::SIGNATURE_HASH { + return Err(alloy_sol_types::Error::invalid_event_signature_hash(Self::SIGNATURE, topics.0, Self::SIGNATURE_HASH)); + } + Ok(()) + } + } + }); + let tokenize_body_impl = expand_event_tokenize(&event.parameters, cx); let encode_topics_impl = encode_first_topic @@ -173,6 +185,8 @@ pub(super) fn expand(cx: &ExpCtxt<'_>, event: &ItemEvent) -> Result } } + #check_signature + #[inline] fn tokenize_body(&self) -> Self::DataToken<'_> { #tokenize_body_impl diff --git a/crates/sol-types/src/errors.rs b/crates/sol-types/src/errors.rs index 577bbd3d9f..a0cb0ba9a2 100644 --- a/crates/sol-types/src/errors.rs +++ b/crates/sol-types/src/errors.rs @@ -9,7 +9,7 @@ use crate::abi; use alloc::{borrow::Cow, boxed::Box, collections::TryReserveError, string::String}; -use alloy_primitives::LogData; +use alloy_primitives::{LogData, B256}; use core::fmt; /// ABI result type. @@ -147,6 +147,14 @@ impl Error { pub fn unknown_selector(name: &'static str, selector: [u8; 4]) -> Self { Self::UnknownSelector { name, selector: selector.into() } } + + #[doc(hidden)] // Not public API. + #[cold] + pub fn invalid_event_signature_hash(name: &'static str, got: B256, expected: B256) -> Self { + Self::custom(format!( + "invalid signature hash for event {name:?}: got {got}, expected {expected}" + )) + } } impl From for Error { diff --git a/crates/sol-types/src/types/event/mod.rs b/crates/sol-types/src/types/event/mod.rs index 8bf5edc3f8..d1ea749408 100644 --- a/crates/sol-types/src/types/event/mod.rs +++ b/crates/sol-types/src/types/event/mod.rs @@ -54,11 +54,33 @@ pub trait SolEvent: Sized { const ANONYMOUS: bool; /// Convert decoded rust data to the event type. + /// + /// Does not check that `topics[0]` is the correct hash. + /// Use [`new_checked`](Self::new_checked) instead. fn new( topics: ::RustType, data: as SolType>::RustType, ) -> Self; + /// Convert decoded rust data to the event type. + /// + /// Checks that `topics[0]` is the correct hash. + #[inline] + fn new_checked( + topics: ::RustType, + data: as SolType>::RustType, + ) -> Result { + Self::check_signature(&topics).map(|()| Self::new(topics, data)) + } + + /// Check that the event's signature matches the given topics. + #[inline] + fn check_signature(topics: &::RustType) -> Result<()> { + // Overridden for non-anonymous events in `sol!`. + let _ = topics; + Ok(()) + } + /// Tokenize the event's non-indexed parameters. fn tokenize_body(&self) -> Self::DataToken<'_>; @@ -110,14 +132,10 @@ pub trait SolEvent: Sized { /// Encode the topics of this event into a fixed-size array. /// - /// # Panics - /// - /// This method will panic if `LEN` is not equal to - /// `Self::TopicList::COUNT`. + /// This method will not compile if `LEN` is not equal to `Self::TopicList::COUNT`. #[inline] fn encode_topics_array(&self) -> [WordToken; LEN] { - // TODO: make this a compile-time error when `const` blocks are stable - assert_eq!(LEN, Self::TopicList::COUNT, "topic list length mismatch"); + const { assert!(LEN == Self::TopicList::COUNT, "topic list length mismatch") }; let mut out = [WordToken(B256::ZERO); LEN]; self.encode_topics_raw(&mut out).unwrap(); out @@ -163,6 +181,8 @@ pub trait SolEvent: Sized { D: Into, { let topics = Self::decode_topics(topics)?; + // Check signature before decoding the data. + Self::check_signature(&topics)?; let body = Self::abi_decode_data(data, validate)?; Ok(Self::new(topics, body)) } diff --git a/crates/sol-types/src/types/event/topic_list.rs b/crates/sol-types/src/types/event/topic_list.rs index 5225a86b73..21c36411f9 100644 --- a/crates/sol-types/src/types/event/topic_list.rs +++ b/crates/sol-types/src/types/event/topic_list.rs @@ -47,10 +47,9 @@ macro_rules! impl_topic_list_tuples { I: IntoIterator, D: Into { - let err = || Error::Other(Cow::Borrowed("topic list length mismatch")); let mut iter = topics.into_iter(); Ok(($( - <$t>::detokenize(iter.next().ok_or_else(err)?.into()), + <$t>::detokenize(iter.next().ok_or_else(length_mismatch)?.into()), )*)) } } @@ -77,3 +76,8 @@ impl_topic_list_tuples! { 3 => 'a T, 'b U, 'c V; 4 => 'a T, 'b U, 'c V, 'd W; } + +#[cold] +const fn length_mismatch() -> Error { + Error::Other(Cow::Borrowed("topic list length mismatch")) +} diff --git a/crates/sol-types/tests/macros/sol/mod.rs b/crates/sol-types/tests/macros/sol/mod.rs index b4e5e5bda2..9df4be9e2d 100644 --- a/crates/sol-types/tests/macros/sol/mod.rs +++ b/crates/sol-types/tests/macros/sol/mod.rs @@ -1148,3 +1148,26 @@ fn event_indexed_elementary_arrays() { assert_eq!(AddrUdvtDynArray::SIGNATURE, "AddrUdvtDynArray(address[])"); let _ = AddrUdvtDynArray { y: B256::ZERO }; } + +// https://github.com/alloy-rs/core/issues/589 +#[test] +#[allow(clippy::assertions_on_constants)] +fn event_check_signature() { + sol! { + #[derive(Debug)] + event MyEvent(); + event MyEventAnonymous() anonymous; + } + + let no_topics: [B256; 0] = []; + + assert!(!MyEvent::ANONYMOUS); + let e = MyEvent::decode_raw_log(no_topics, &[], false).unwrap_err(); + assert_eq!(e.to_string(), "topic list length mismatch"); + let e = MyEvent::decode_raw_log([B256::ZERO], &[], false).unwrap_err(); + assert!(e.to_string().contains("invalid signature hash"), "{e:?}"); + let MyEvent {} = MyEvent::decode_raw_log([MyEvent::SIGNATURE_HASH], &[], false).unwrap(); + + assert!(MyEventAnonymous::ANONYMOUS); + let MyEventAnonymous {} = MyEventAnonymous::decode_raw_log(no_topics, &[], false).unwrap(); +}