Skip to content

Commit

Permalink
Handle Topic Count to Event Index Mismatch (#6)
Browse files Browse the repository at this point in the history
Fixed #4

This PR implements the required changes to the dynamic
`solabi::value::EventDecoder` so that it correctly handles when Ethereum
log topic counts do not match the expected amount based on the event
descriptor.
  • Loading branch information
nlordell authored Oct 29, 2023
1 parent 40c5030 commit 4725aa9
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 4 deletions.
53 changes: 53 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,59 @@ mod tests {
assert_eq!(transfer.encode(&(from, to), &(value,)), log);
}

#[test]
fn fails_to_decode_event_with_different_indices() {
let selector = hex!("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef");

let transfer = EventEncoder::<(Address, Address), (U256,)>::new(selector);
let log = transfer.encode(
&(
address!("0x0101010101010101010101010101010101010101"),
address!("0x0202020202020202020202020202020202020202"),
),
&(uint!("4_200_000_000_000_000_000"),),
);

let transfer = EventEncoder::<(Address, Address, U256), ()>::new(selector);
assert!(matches!(transfer.decode(&log), Err(ParseError::Index)));

let transfer = EventEncoder::<(Address,), (Address, U256)>::new(selector);
assert!(matches!(transfer.decode(&log), Err(ParseError::Index)));
}

#[test]
fn empty_indices_and_data() {
let event = EventEncoder::<(), (U256,)>::new([1; 32]);
let value = uint!("42");
let log = Log {
topics: Topics::from([event.topic0]),
data: hex!("000000000000000000000000000000000000000000000000000000000000002a")[..]
.into(),
};
assert_eq!(event.decode(&log).unwrap(), ((), (value,)),);
assert_eq!(event.encode(&(), &(value,)), log);

let event = EventEncoder::<(U256,), ()>::new([2; 32]);
let value = uint!("42");
let log = Log {
topics: Topics::from([
event.topic0,
hex!("000000000000000000000000000000000000000000000000000000000000002a"),
]),
data: hex!("")[..].into(),
};
assert_eq!(event.decode(&log).unwrap(), ((value,), ()),);
assert_eq!(event.encode(&(value,), &()), log);

let event = EventEncoder::<(), ()>::new([2; 32]);
let log = Log {
topics: Topics::from([event.topic0]),
data: hex!("")[..].into(),
};
assert_eq!(event.decode(&log).unwrap(), ((), ()),);
assert_eq!(event.encode(&(), &()), log);
}

#[test]
fn anonymous_event_with_indexed_dynamic_field() {
let anon = AnonymousEventEncoder::<
Expand Down
51 changes: 47 additions & 4 deletions src/value/encoders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::{
DecodeError, Decoder,
},
encode::{Encode, Encoder, Size},
event::ParseError,
function::Selector,
log::{Log, Topics},
primitive::Word,
Expand Down Expand Up @@ -167,11 +168,16 @@ impl EventEncoder {
///
/// Note that non-primitive indexed fields will be replaced with a `bytes32`
/// value equal to the hash of its ABI-encoded value.
pub fn decode(&self, log: &Log) -> Result<Vec<Value>, DecodeError> {
pub fn decode(&self, log: &Log) -> Result<Vec<Value>, ParseError> {
if log.topics.len() != self.topic_count() {
return Err(ParseError::Index);
}

let mut topics = log.topics.into_iter();
if let Some(selector) = self.selector {
if !matches!(topics.next(), Some(topic) if topic == selector) {
return Err(DecodeError::InvalidData);
let topic0 = topics.next().expect("unexpected missing topic");
if topic0 != selector {
return Err(ParseError::SelectorMismatch(topic0));
}
}

Expand All @@ -196,7 +202,7 @@ impl EventEncoder {
/// Decodes a Solidity event from an Ethereum log, replacing non-primitive
/// indexed fields with their default values (e.g. empty string for a
/// `string` field).
pub fn decode_lossy(&self, log: &Log) -> Result<Vec<Value>, DecodeError> {
pub fn decode_lossy(&self, log: &Log) -> Result<Vec<Value>, ParseError> {
let mut fields = self.decode(log)?;
for ((_, kind), value) in self
.fields
Expand All @@ -208,6 +214,12 @@ impl EventEncoder {
}
Ok(fields)
}

/// Returns the number of topics of an Ethereum log that encodes this event.
fn topic_count(&self) -> usize {
(self.selector.is_some() as usize)
+ self.fields.iter().filter(|(indexed, _)| *indexed).count()
}
}

/// Internal type for encoding log data, skipping indexed fields.
Expand Down Expand Up @@ -429,6 +441,37 @@ mod tests {
assert_eq!(encoder.decode(&log).unwrap(), fields);
}

#[test]
fn fails_to_decode_event_with_different_indices() {
let decl = |s: &str| {
let event = EventDescriptor::parse_declaration(s).unwrap();
let encoder = EventEncoder::new(&event).unwrap();
(event, encoder)
};

let (event, encoder) =
decl("event Transfer(address indexed to, address indexed from, uint256 value)");

let selector = event.selector().unwrap();
let fields = [
Value::Address(address!("0x0101010101010101010101010101010101010101")),
Value::Address(address!("0x0202020202020202020202020202020202020202")),
Value::Uint(Uint::new(256, uint!("4_200_000_000_000_000_000")).unwrap()),
];
let log = encoder.encode(&fields).unwrap();

// Now try and parse the log to an event with the similar signature, but
// with a different set of indexed fields.
for signature in [
"event Transfer(address indexed to, address from, uint256 value)",
"event Transfer(address indexed to, address indexed from, uint256 indexed value)",
] {
let (event, encoder) = decl(signature);
assert_eq!(selector, event.selector().unwrap());
assert!(matches!(encoder.decode(&log), Err(ParseError::Index)));
}
}

#[test]
fn anonymous_event_with_indexed_dynamic_field() {
let event = EventDescriptor::parse_declaration(
Expand Down

0 comments on commit 4725aa9

Please sign in to comment.