From 5cd331b1f458375186a87313a9d3cbaf6473f35f Mon Sep 17 00:00:00 2001 From: Shiyas Mohammed Date: Fri, 15 Aug 2025 07:57:19 +0530 Subject: [PATCH 1/5] feat(cast): add abi-encode-event command --- crates/cast/src/args.rs | 9 +++++ crates/cast/src/lib.rs | 76 ++++++++++++++++++++++++++++++++++++++++- crates/cast/src/opts.rs | 11 ++++++ 3 files changed, 95 insertions(+), 1 deletion(-) diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 28521d3a51159..6edb7d51dc79a 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -191,6 +191,15 @@ pub async fn run_command(args: CastArgs) -> Result<()> { sh_println!("{}", SimpleCast::abi_encode_packed(&sig, &args)?)? } } + CastSubcommand::AbiEncodeEvent { sig, args } => { + let (topics, data) = SimpleCast::abi_encode_event(&sig, &args)?; + for (i, topic) in topics.iter().enumerate() { + sh_println!("[topic{}]: {}", i, topic)?; + } + if !data.is_empty() { + sh_println!("[data]: {}", data)?; + } + } CastSubcommand::DecodeCalldata { sig, calldata, file } => { let raw_hex = if let Some(file_path) = file { let contents = fs::read_to_string(&file_path)?; diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index 16e9d55f28a8e..d6cb4a40540f1 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -28,7 +28,7 @@ use eyre::{Context, ContextCompat, OptionExt, Result}; use foundry_block_explorers::Client; use foundry_common::{ TransactionReceiptWithRevertReason, - abi::{encode_function_args, get_func}, + abi::{coerce_value, encode_function_args, get_event, get_func}, compile::etherscan_project, fmt::*, fs, get_pretty_tx_receipt_attr, shell, @@ -1844,6 +1844,80 @@ impl SimpleCast { Ok(format!("0x{encoded}")) } + /// Performs ABI encoding of an event to produce the topics and data. + /// + /// # Example + /// + /// ``` + /// use cast::SimpleCast as Cast; + /// + /// let (topics, data) = Cast::abi_encode_event( + /// "Transfer(address indexed from, address indexed to, uint256 value)", + /// &["0x1234567890123456789012345678901234567890", "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", "1000"] + /// ).unwrap(); + /// + /// // topic0 is the event selector + /// assert_eq!(topics.len(), 3); + /// assert_eq!(topics[0], "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); + /// assert_eq!(topics[1], "0x0000000000000000000000001234567890123456789012345678901234567890"); + /// assert_eq!(topics[2], "0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd"); + /// assert_eq!(data, "0x00000000000000000000000000000000000000000000000000000000000003e8"); + /// # Ok::<_, eyre::Report>(()) + /// ``` + pub fn abi_encode_event(sig: &str, args: &[impl AsRef]) -> Result<(Vec, String)> { + let event = get_event(sig)?; + + let tokens: Result> = std::iter::zip(&event.inputs, args) + .map(|(input, arg)| coerce_value(&input.ty, arg.as_ref())) + .collect(); + let tokens = tokens?; + + let mut topics = vec![format!("{:?}", event.selector())]; + let mut data_tokens = Vec::new(); + + for (input, token) in event.inputs.iter().zip(tokens.iter()) { + if input.indexed { + let ty = DynSolType::parse(&input.ty)?; + if matches!( + ty, + DynSolType::String + | DynSolType::Bytes + | DynSolType::Array(_) + | DynSolType::Tuple(_) + ) { + // For dynamic types, hash the encoded value + let encoded = token.abi_encode(); + let hash = keccak256(encoded); + topics.push(format!("{:?}", hash)); + } else { + // For fixed-size types, encode directly to 32 bytes + let mut encoded = [0u8; 32]; + let token_encoded = token.abi_encode(); + if token_encoded.len() <= 32 { + let start = 32 - token_encoded.len(); + encoded[start..].copy_from_slice(&token_encoded); + } + topics.push(format!("{:?}", B256::from(encoded))); + } + } else { + // Non-indexed parameters go into data + data_tokens.push(token.clone()); + } + } + + let data = if !data_tokens.is_empty() { + let mut encoded_data = Vec::new(); + for token in &data_tokens { + encoded_data.extend_from_slice(&token.abi_encode()); + } + hex::encode_prefixed(encoded_data) + } else { + String::new() + }; + + Ok((topics, data)) + } + /// Performs ABI encoding to produce the hexadecimal calldata with the given arguments. /// /// # Example diff --git a/crates/cast/src/opts.rs b/crates/cast/src/opts.rs index bb86b561def32..c8063011a0773 100644 --- a/crates/cast/src/opts.rs +++ b/crates/cast/src/opts.rs @@ -644,6 +644,17 @@ pub enum CastSubcommand { args: Vec, }, + /// ABI encode an event and its arguments to generate topics and data. + #[command(visible_alias = "aee")] + AbiEncodeEvent { + /// The event signature. + sig: String, + + /// The arguments of the event. + #[arg(allow_hyphen_values = true)] + args: Vec, + }, + /// Compute the storage slot for an entry in a mapping. #[command(visible_alias = "in")] Index { From a9630e9ed14ba7ea26df14123e45a2f8fc40a0ab Mon Sep 17 00:00:00 2001 From: Shiyas Mohammed Date: Fri, 15 Aug 2025 08:06:04 +0530 Subject: [PATCH 2/5] test(cast): add test for abi-encode-event command --- crates/cast/tests/cli/main.rs | 44 +++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index d923d5afd82a7..f6697cdcc9e98 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -4011,6 +4011,50 @@ casttest!(cast_call_can_override_several_state_diff, |_prj, cmd| { ... [..] 0x5EA1d9A6dDC3A0329378a327746D71A2019eC332::isOwner(0x2066901073a33ba2500274704aB04763875cF210) ... +// Test cast abi-encode-event with indexed parameters +casttest!(abi_encode_event_indexed, |_prj, cmd| { + cmd.args([ + "abi-encode-event", + "Transfer(address indexed from, address indexed to, uint256 value)", + "0x1234567890123456789012345678901234567890", + "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + "1000" + ]) + .assert_success() + .stdout_eq(str![[r#" +[topic0]: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef +[topic1]: 0x0000000000000000000000001234567890123456789012345678901234567890 +[topic2]: 0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd +[data]: 0x00000000000000000000000000000000000000000000000000000000000003e8 + +"#]]); +}); + +// Test cast abi-encode-event with no indexed parameters +casttest!(abi_encode_event_no_indexed, |_prj, cmd| { + cmd.args([ + "abi-encode-event", + "Approval(address owner, address spender, uint256 value)", + "0x1234567890123456789012345678901234567890", + "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + "2000" + ]) + .assert_success() + .stdout_eq(str![[r#" +[topic0]: 0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 +[data]: 0x0000000000000000000000001234567890123456789012345678901234567890000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd00000000000000000000000000000000000000000000000000000000000007d0 + +"#]]); +}); + +// Test cast abi-encode-event with dynamic indexed parameter (string) +casttest!(abi_encode_event_dynamic_indexed, |_prj, cmd| { + cmd.args(["abi-encode-event", "Log(string indexed message, uint256 data)", "hello", "42"]) + .assert_success() + .stdout_eq(str![[r#" +[topic0]: 0xdd970dd9b5bfe707922155b058a407655cb18288b807e2216442bca8ad83d6b5 +[topic1]: 0x984002fcc0ca639f96622add24c2edd2fe72c65e71ca3faa243e091e0bc7cdab +[data]: 0x000000000000000000000000000000000000000000000000000000000000002a "#]]); }); From 32666e5cd660aaaae671989f18d320d7ade54195 Mon Sep 17 00:00:00 2001 From: Shiyas Mohammed Date: Fri, 15 Aug 2025 08:26:24 +0530 Subject: [PATCH 3/5] fix: cargo fmt, clippy --- crates/cast/src/lib.rs | 11 ++++++++--- crates/cast/tests/cli/main.rs | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index d6cb4a40540f1..4b289b9d98601 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -1853,8 +1853,13 @@ impl SimpleCast { /// /// let (topics, data) = Cast::abi_encode_event( /// "Transfer(address indexed from, address indexed to, uint256 value)", - /// &["0x1234567890123456789012345678901234567890", "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", "1000"] - /// ).unwrap(); + /// &[ + /// "0x1234567890123456789012345678901234567890", + /// "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", + /// "1000", + /// ], + /// ) + /// .unwrap(); /// /// // topic0 is the event selector /// assert_eq!(topics.len(), 3); @@ -1888,7 +1893,7 @@ impl SimpleCast { // For dynamic types, hash the encoded value let encoded = token.abi_encode(); let hash = keccak256(encoded); - topics.push(format!("{:?}", hash)); + topics.push(format!("{hash:?}")); } else { // For fixed-size types, encode directly to 32 bytes let mut encoded = [0u8; 32]; diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index f6697cdcc9e98..93b33eb863ef5 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -4018,7 +4018,7 @@ casttest!(abi_encode_event_indexed, |_prj, cmd| { "Transfer(address indexed from, address indexed to, uint256 value)", "0x1234567890123456789012345678901234567890", "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd", - "1000" + "1000", ]) .assert_success() .stdout_eq(str![[r#" From 6d5fc54143dab0e9268860888419dcdd2c877475 Mon Sep 17 00:00:00 2001 From: Shiyas Mohammed Date: Tue, 2 Sep 2025 12:37:13 +0530 Subject: [PATCH 4/5] refactor(cast): return bytes from abi_encode_event --- crates/cast/src/args.rs | 2 +- crates/cast/src/lib.rs | 52 +++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 6edb7d51dc79a..0a7810d15d1cb 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -197,7 +197,7 @@ pub async fn run_command(args: CastArgs) -> Result<()> { sh_println!("[topic{}]: {}", i, topic)?; } if !data.is_empty() { - sh_println!("[data]: {}", data)?; + sh_println!("[data]: {}", hex::encode_prefixed(data))?; } } CastSubcommand::DecodeCalldata { sig, calldata, file } => { diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index 4b289b9d98601..58715a8b28d89 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -1849,6 +1849,7 @@ impl SimpleCast { /// # Example /// /// ``` + /// use alloy_primitives::hex; /// use cast::SimpleCast as Cast; /// /// let (topics, data) = Cast::abi_encode_event( @@ -1863,24 +1864,35 @@ impl SimpleCast { /// /// // topic0 is the event selector /// assert_eq!(topics.len(), 3); - /// assert_eq!(topics[0], "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"); - /// assert_eq!(topics[1], "0x0000000000000000000000001234567890123456789012345678901234567890"); - /// assert_eq!(topics[2], "0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd"); - /// assert_eq!(data, "0x00000000000000000000000000000000000000000000000000000000000003e8"); + /// assert_eq!( + /// topics[0].to_string(), + /// "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" + /// ); + /// assert_eq!( + /// topics[1].to_string(), + /// "0x0000000000000000000000001234567890123456789012345678901234567890" + /// ); + /// assert_eq!( + /// topics[2].to_string(), + /// "0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd" + /// ); + /// assert_eq!( + /// hex::encode_prefixed(data), + /// "0x00000000000000000000000000000000000000000000000000000000000003e8" + /// ); /// # Ok::<_, eyre::Report>(()) /// ``` - pub fn abi_encode_event(sig: &str, args: &[impl AsRef]) -> Result<(Vec, String)> { + pub fn abi_encode_event(sig: &str, args: &[impl AsRef]) -> Result<(Vec, Vec)> { let event = get_event(sig)?; - let tokens: Result> = std::iter::zip(&event.inputs, args) + let tokens = std::iter::zip(&event.inputs, args) .map(|(input, arg)| coerce_value(&input.ty, arg.as_ref())) - .collect(); - let tokens = tokens?; + .collect::>>()?; - let mut topics = vec![format!("{:?}", event.selector())]; - let mut data_tokens = Vec::new(); + let mut topics = vec![event.selector()]; + let mut data_tokens: Vec = Vec::new(); - for (input, token) in event.inputs.iter().zip(tokens.iter()) { + for (input, token) in event.inputs.iter().zip(tokens.into_iter()) { if input.indexed { let ty = DynSolType::parse(&input.ty)?; if matches!( @@ -1893,7 +1905,7 @@ impl SimpleCast { // For dynamic types, hash the encoded value let encoded = token.abi_encode(); let hash = keccak256(encoded); - topics.push(format!("{hash:?}")); + topics.push(hash); } else { // For fixed-size types, encode directly to 32 bytes let mut encoded = [0u8; 32]; @@ -1902,25 +1914,15 @@ impl SimpleCast { let start = 32 - token_encoded.len(); encoded[start..].copy_from_slice(&token_encoded); } - topics.push(format!("{:?}", B256::from(encoded))); + topics.push(B256::from(encoded)); } } else { // Non-indexed parameters go into data - data_tokens.push(token.clone()); + data_tokens.extend_from_slice(&token.abi_encode()); } } - let data = if !data_tokens.is_empty() { - let mut encoded_data = Vec::new(); - for token in &data_tokens { - encoded_data.extend_from_slice(&token.abi_encode()); - } - hex::encode_prefixed(encoded_data) - } else { - String::new() - }; - - Ok((topics, data)) + Ok((topics, data_tokens)) } /// Performs ABI encoding to produce the hexadecimal calldata with the given arguments. From ee04610acecc2a78b378bbfc0ba4d8cf1ea5fa11 Mon Sep 17 00:00:00 2001 From: Shiyas Mohammed Date: Wed, 17 Sep 2025 12:55:28 +0530 Subject: [PATCH 5/5] refactor: change return type to LogData for abi_encode_event --- crates/cast/src/args.rs | 8 ++++---- crates/cast/src/lib.rs | 19 +++++++++---------- crates/cast/tests/cli/main.rs | 3 +++ 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 0a7810d15d1cb..062ac23bd0ade 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -192,12 +192,12 @@ pub async fn run_command(args: CastArgs) -> Result<()> { } } CastSubcommand::AbiEncodeEvent { sig, args } => { - let (topics, data) = SimpleCast::abi_encode_event(&sig, &args)?; - for (i, topic) in topics.iter().enumerate() { + let log_data = SimpleCast::abi_encode_event(&sig, &args)?; + for (i, topic) in log_data.topics().iter().enumerate() { sh_println!("[topic{}]: {}", i, topic)?; } - if !data.is_empty() { - sh_println!("[data]: {}", hex::encode_prefixed(data))?; + if !log_data.data.is_empty() { + sh_println!("[data]: {}", hex::encode_prefixed(log_data.data))?; } } CastSubcommand::DecodeCalldata { sig, calldata, file } => { diff --git a/crates/cast/src/lib.rs b/crates/cast/src/lib.rs index 58715a8b28d89..532606b7eac46 100644 --- a/crates/cast/src/lib.rs +++ b/crates/cast/src/lib.rs @@ -9,7 +9,7 @@ use alloy_ens::NameOrAddress; use alloy_json_abi::Function; use alloy_network::{AnyNetwork, AnyRpcTransaction}; use alloy_primitives::{ - Address, B256, I256, Keccak256, Selector, TxHash, TxKind, U64, U256, hex, + Address, B256, I256, Keccak256, LogData, Selector, TxHash, TxKind, U64, U256, hex, utils::{ParseUnits, Unit, keccak256}, }; use alloy_provider::{ @@ -1852,7 +1852,7 @@ impl SimpleCast { /// use alloy_primitives::hex; /// use cast::SimpleCast as Cast; /// - /// let (topics, data) = Cast::abi_encode_event( + /// let log_data = Cast::abi_encode_event( /// "Transfer(address indexed from, address indexed to, uint256 value)", /// &[ /// "0x1234567890123456789012345678901234567890", @@ -1863,28 +1863,27 @@ impl SimpleCast { /// .unwrap(); /// /// // topic0 is the event selector - /// assert_eq!(topics.len(), 3); + /// assert_eq!(log_data.topics().len(), 3); /// assert_eq!( - /// topics[0].to_string(), + /// log_data.topics()[0].to_string(), /// "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" /// ); /// assert_eq!( - /// topics[1].to_string(), + /// log_data.topics()[1].to_string(), /// "0x0000000000000000000000001234567890123456789012345678901234567890" /// ); /// assert_eq!( - /// topics[2].to_string(), + /// log_data.topics()[2].to_string(), /// "0x000000000000000000000000abcdefabcdefabcdefabcdefabcdefabcdefabcd" /// ); /// assert_eq!( - /// hex::encode_prefixed(data), + /// hex::encode_prefixed(log_data.data), /// "0x00000000000000000000000000000000000000000000000000000000000003e8" /// ); /// # Ok::<_, eyre::Report>(()) /// ``` - pub fn abi_encode_event(sig: &str, args: &[impl AsRef]) -> Result<(Vec, Vec)> { + pub fn abi_encode_event(sig: &str, args: &[impl AsRef]) -> Result { let event = get_event(sig)?; - let tokens = std::iter::zip(&event.inputs, args) .map(|(input, arg)| coerce_value(&input.ty, arg.as_ref())) .collect::>>()?; @@ -1922,7 +1921,7 @@ impl SimpleCast { } } - Ok((topics, data_tokens)) + Ok(LogData::new_unchecked(topics, data_tokens.into())) } /// Performs ABI encoding to produce the hexadecimal calldata with the given arguments. diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 93b33eb863ef5..45fafdbd78858 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -4011,6 +4011,9 @@ casttest!(cast_call_can_override_several_state_diff, |_prj, cmd| { ... [..] 0x5EA1d9A6dDC3A0329378a327746D71A2019eC332::isOwner(0x2066901073a33ba2500274704aB04763875cF210) ... +"#]]); +}); + // Test cast abi-encode-event with indexed parameters casttest!(abi_encode_event_indexed, |_prj, cmd| { cmd.args([