diff --git a/crates/cast/src/args.rs b/crates/cast/src/args.rs index 7d84c0bfcc351..154f79d7766a1 100644 --- a/crates/cast/src/args.rs +++ b/crates/cast/src/args.rs @@ -194,6 +194,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 c081952889a5d..5dc82e8aa4819 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, @@ -1850,6 +1850,85 @@ 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 38aac3b53c13e..feab6f584baa9 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 { diff --git a/crates/cast/tests/cli/main.rs b/crates/cast/tests/cli/main.rs index 69b3181db6686..65a09e771711b 100644 --- a/crates/cast/tests/cli/main.rs +++ b/crates/cast/tests/cli/main.rs @@ -3857,3 +3857,51 @@ casttest!(cast_access_list_negative_numbers, |_prj, cmd| { ]) .assert_success(); }); + +// 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 + +"#]]); +});