1use alloy_chains::Chain;
4use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt, Specifier};
5use alloy_json_abi::{Error, Event, Function, Param};
6use alloy_primitives::{Address, LogData, hex};
7use eyre::{Context, ContextCompat, Result};
8use foundry_block_explorers::{Client, contract::ContractMetadata, errors::EtherscanError};
9use std::pin::Pin;
10
11pub fn encode_args<I, S>(inputs: &[Param], args: I) -> Result<Vec<DynSolValue>>
12where
13 I: IntoIterator<Item = S>,
14 S: AsRef<str>,
15{
16 let args: Vec<S> = args.into_iter().collect();
17
18 if inputs.len() != args.len() {
19 eyre::bail!("encode length mismatch: expected {} types, got {}", inputs.len(), args.len());
20 }
21
22 std::iter::zip(inputs, args)
23 .map(|(input, arg)| coerce_value(&input.selector_type(), arg.as_ref()))
24 .collect()
25}
26
27pub fn encode_function_args<I, S>(func: &Function, args: I) -> Result<Vec<u8>>
30where
31 I: IntoIterator<Item = S>,
32 S: AsRef<str>,
33{
34 Ok(func.abi_encode_input(&encode_args(&func.inputs, args)?)?)
35}
36
37pub fn encode_function_args_raw<I, S>(func: &Function, args: I) -> Result<Vec<u8>>
40where
41 I: IntoIterator<Item = S>,
42 S: AsRef<str>,
43{
44 Ok(func.abi_encode_input_raw(&encode_args(&func.inputs, args)?)?)
45}
46
47pub fn encode_function_args_packed<I, S>(func: &Function, args: I) -> Result<Vec<u8>>
50where
51 I: IntoIterator<Item = S>,
52 S: AsRef<str>,
53{
54 let args: Vec<S> = args.into_iter().collect();
55
56 if func.inputs.len() != args.len() {
57 eyre::bail!(
58 "encode length mismatch: expected {} types, got {}",
59 func.inputs.len(),
60 args.len(),
61 );
62 }
63
64 let params: Vec<Vec<u8>> = std::iter::zip(&func.inputs, args)
65 .map(|(input, arg)| coerce_value(&input.selector_type(), arg.as_ref()))
66 .collect::<Result<Vec<_>>>()?
67 .into_iter()
68 .map(|v| v.abi_encode_packed())
69 .collect();
70
71 Ok(params.concat())
72}
73
74pub fn abi_decode_calldata(
76 sig: &str,
77 calldata: &str,
78 input: bool,
79 fn_selector: bool,
80) -> Result<Vec<DynSolValue>> {
81 let func = get_func(sig)?;
82 let calldata = hex::decode(calldata)?;
83
84 let mut calldata = calldata.as_slice();
85 if input && fn_selector && calldata.len() >= 4 {
87 calldata = &calldata[4..];
88 }
89
90 let res =
91 if input { func.abi_decode_input(calldata) } else { func.abi_decode_output(calldata) }?;
92
93 if res.is_empty() {
95 eyre::bail!("no data was decoded");
96 }
97
98 Ok(res)
99}
100
101pub fn get_func(sig: &str) -> Result<Function> {
103 Function::parse(sig).wrap_err("could not parse function signature")
104}
105
106pub fn get_event(sig: &str) -> Result<Event> {
108 Event::parse(sig).wrap_err("could not parse event signature")
109}
110
111pub fn abi_decode_event_data(event: &Event, data: &[u8]) -> Result<Vec<DynSolValue>> {
116 let body = DynSolType::Tuple(
117 event
118 .inputs
119 .iter()
120 .filter(|input| !input.indexed)
121 .map(|input| input.resolve())
122 .collect::<Result<_, _>>()?,
123 );
124 Ok(match body.abi_decode_sequence(data)? {
125 DynSolValue::Tuple(values) => values,
126 _ => unreachable!("body is a tuple"),
127 })
128}
129
130pub fn get_error(sig: &str) -> Result<Error> {
132 Error::parse(sig).wrap_err("could not parse error signature")
133}
134
135pub fn get_indexed_event(mut event: Event, raw_log: &LogData) -> Event {
138 if !event.anonymous && raw_log.topics().len() > 1 {
139 let indexed_params = raw_log.topics().len() - 1;
140 let num_inputs = event.inputs.len();
141 let num_address_params = event.inputs.iter().filter(|p| p.ty == "address").count();
142
143 event.inputs.iter_mut().enumerate().for_each(|(index, param)| {
144 if param.name.is_empty() {
145 param.name = format!("param{index}");
146 }
147 if num_inputs == indexed_params
148 || (num_address_params == indexed_params && param.ty == "address")
149 {
150 param.indexed = true;
151 }
152 })
153 }
154 event
155}
156
157pub async fn get_func_etherscan(
160 function_name: &str,
161 contract: Address,
162 args: &[String],
163 chain: Chain,
164 etherscan_api_key: &str,
165 etherscan_api_url: Option<&str>,
166) -> Result<Function> {
167 let client = if let Some(api_url) = etherscan_api_url {
168 Client::builder()
169 .with_api_key(etherscan_api_key)
170 .with_api_url(api_url)?
171 .with_url(api_url)?
172 .build()?
173 } else {
174 Client::new(chain, etherscan_api_key)?
175 };
176 let source = find_source(client, contract).await?;
177 let metadata = source.items.first().wrap_err("etherscan returned empty metadata")?;
178
179 let mut abi = metadata.abi()?;
180 let funcs = abi.functions.remove(function_name).unwrap_or_default();
181
182 for func in funcs {
183 let res = encode_function_args(&func, args);
184 if res.is_ok() {
185 return Ok(func);
186 }
187 }
188
189 Err(eyre::eyre!("Function not found in abi"))
190}
191
192pub fn find_source(
194 client: Client,
195 address: Address,
196) -> Pin<Box<dyn Future<Output = Result<ContractMetadata>>>> {
197 Box::pin(async move {
198 trace!(%address, "find Etherscan source");
199 let source = client.contract_source_code(address).await?;
200 let metadata = source.items.first().wrap_err("Etherscan returned no data")?;
201 let implementation = if metadata.proxy == 0 { None } else { metadata.implementation };
204 if let Some(implementation) = implementation {
205 sh_println!(
206 "Contract at {address} is a proxy, trying to fetch source at {implementation}..."
207 )?;
208 match find_source(client, implementation).await {
209 impl_source @ Ok(_) => impl_source,
210 Err(e)
211 if matches!(
212 e.downcast_ref::<EtherscanError>(),
213 Some(EtherscanError::ContractCodeNotVerified(address)) if *address == implementation
214 ) =>
215 {
216 error!(%e);
217 Ok(source)
218 }
219 Err(e) => Err(e),
220 }
221 } else {
222 if metadata.proxy != 0 {
223 error!(%address, "Etherscan reports this contract as a proxy but returned no implementation address");
224 }
225 Ok(source)
226 }
227 })
228}
229
230pub fn coerce_value(ty: &str, arg: &str) -> Result<DynSolValue> {
232 let ty = DynSolType::parse(ty)?;
233 Ok(DynSolType::coerce_str(&ty, arg)?)
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use alloy_dyn_abi::EventExt;
240 use alloy_primitives::{B256, U256};
241
242 #[test]
245 fn test_proxy_without_implementation_does_not_panic() {
246 use foundry_block_explorers::contract::Metadata;
247
248 let json = serde_json::json!({
249 "SourceCode": "// dummy",
250 "ABI": "[]",
251 "ContractName": "Dummy",
252 "CompilerVersion": "v0.8.0+commit.c7dfd78e",
253 "OptimizationUsed": "0",
254 "Runs": "200",
255 "ConstructorArguments": "",
256 "EVMVersion": "Default",
257 "Library": "",
258 "LicenseType": "None",
259 "Proxy": "1",
260 "Implementation": "",
261 "SwarmSource": ""
262 });
263
264 let metadata: Metadata =
265 serde_json::from_value(json).expect("realistic Etherscan payload must deserialize");
266
267 assert_eq!(metadata.proxy, 1, "Proxy: 1 must deserialize to a nonzero proxy flag");
269 assert_eq!(
270 metadata.implementation, None,
271 "an empty Implementation string must deserialize to None, not a parsed address"
272 );
273
274 let implementation = if metadata.proxy == 0 { None } else { metadata.implementation };
276 assert_eq!(implementation, None);
277 }
278
279 #[test]
280 fn proxy_implementation_decision_follows_real_implementation() {
281 use alloy_primitives::address;
282 use foundry_block_explorers::contract::Metadata;
283
284 let json = serde_json::json!({
285 "SourceCode": "// dummy",
286 "ABI": "[]",
287 "ContractName": "Dummy",
288 "CompilerVersion": "v0.8.0+commit.c7dfd78e",
289 "OptimizationUsed": "0",
290 "Runs": "200",
291 "ConstructorArguments": "",
292 "EVMVersion": "Default",
293 "Library": "",
294 "LicenseType": "None",
295 "Proxy": "1",
296 "Implementation": "0x1F98431c8aD98523631AE4a59f267346ea31F984",
297 "SwarmSource": ""
298 });
299 let metadata: Metadata = serde_json::from_value(json).unwrap();
300
301 let implementation = if metadata.proxy == 0 { None } else { metadata.implementation };
302 assert_eq!(implementation, Some(address!("0x1F98431c8aD98523631AE4a59f267346ea31F984")));
303 }
304
305 #[test]
306 fn test_get_func() {
307 let func = get_func("function foo(uint256 a, uint256 b) returns (uint256)");
308 assert!(func.is_ok());
309 let func = func.unwrap();
310 assert_eq!(func.name, "foo");
311 assert_eq!(func.inputs.len(), 2);
312 assert_eq!(func.inputs[0].ty, "uint256");
313 assert_eq!(func.inputs[1].ty, "uint256");
314
315 let func = get_func("foo(bytes4 a, uint8 b)(bytes4)");
317 assert!(func.is_ok());
318 let func = func.unwrap();
319 assert_eq!(func.name, "foo");
320 assert_eq!(func.inputs.len(), 2);
321 assert_eq!(func.inputs[0].ty, "bytes4");
322 assert_eq!(func.inputs[1].ty, "uint8");
323 assert_eq!(func.outputs[0].ty, "bytes4");
324 }
325
326 #[test]
327 fn test_indexed_only_address() {
328 let event = get_event("event Ev(address,uint256,address)").unwrap();
329
330 let param0 = B256::random();
331 let param1 = vec![3; 32];
332 let param2 = B256::random();
333 let log = LogData::new_unchecked(vec![event.selector(), param0, param2], param1.into());
334 let event = get_indexed_event(event, &log);
335
336 assert_eq!(event.inputs.len(), 3);
337
338 let parsed = event.decode_log(&log).unwrap();
340
341 assert_eq!(event.inputs.iter().filter(|param| param.indexed).count(), 2);
342 assert_eq!(parsed.indexed[0], DynSolValue::Address(Address::from_word(param0)));
343 assert_eq!(parsed.body[0], DynSolValue::Uint(U256::from_be_bytes([3; 32]), 256));
344 assert_eq!(parsed.indexed[1], DynSolValue::Address(Address::from_word(param2)));
345 }
346
347 #[test]
348 fn test_indexed_all() {
349 let event = get_event("event Ev(address,uint256,address)").unwrap();
350
351 let param0 = B256::random();
352 let param1 = vec![3; 32];
353 let param2 = B256::random();
354 let log = LogData::new_unchecked(
355 vec![event.selector(), param0, B256::from_slice(¶m1), param2],
356 vec![].into(),
357 );
358 let event = get_indexed_event(event, &log);
359
360 assert_eq!(event.inputs.len(), 3);
361
362 assert_eq!(event.inputs.iter().filter(|param| param.indexed).count(), 3);
364 let parsed = event.decode_log(&log).unwrap();
365
366 assert_eq!(parsed.indexed[0], DynSolValue::Address(Address::from_word(param0)));
367 assert_eq!(parsed.indexed[1], DynSolValue::Uint(U256::from_be_bytes([3; 32]), 256));
368 assert_eq!(parsed.indexed[2], DynSolValue::Address(Address::from_word(param2)));
369 }
370
371 #[test]
372 fn test_abi_decode_event_data_ignores_indexed() {
373 let event =
374 get_event("event Ev(uint256 indexed a, string b, address indexed c, uint256 d)")
375 .unwrap();
376 let data = DynSolValue::Tuple(vec![
377 DynSolValue::String("hello".into()),
378 DynSolValue::Uint(U256::from(42), 256),
379 ])
380 .abi_encode_params();
381
382 let decoded = abi_decode_event_data(&event, &data).unwrap();
383 assert_eq!(
384 decoded,
385 vec![DynSolValue::String("hello".into()), DynSolValue::Uint(U256::from(42), 256)]
386 );
387 }
388
389 #[test]
390 fn test_abi_decode_event_data_without_indexed() {
391 let event = get_event("event Ev(uint256 a, address b)").unwrap();
392 let addr = Address::random();
393 let data =
394 DynSolValue::Tuple(vec![DynSolValue::Uint(U256::ONE, 256), DynSolValue::Address(addr)])
395 .abi_encode_params();
396
397 let decoded = abi_decode_event_data(&event, &data).unwrap();
398 assert_eq!(decoded, vec![DynSolValue::Uint(U256::ONE, 256), DynSolValue::Address(addr)]);
399 }
400
401 #[test]
402 fn test_encode_args_length_validation() {
403 use alloy_json_abi::Param;
404
405 let params = vec![
406 Param {
407 name: "a".to_string(),
408 ty: "uint256".to_string(),
409 internal_type: None,
410 components: vec![],
411 },
412 Param {
413 name: "b".to_string(),
414 ty: "address".to_string(),
415 internal_type: None,
416 components: vec![],
417 },
418 ];
419
420 let args = vec!["1"];
422 let res = encode_args(¶ms, &args);
423 assert!(res.is_err());
424 assert!(format!("{}", res.unwrap_err()).contains("encode length mismatch"));
425
426 let args = vec!["1", "0x0000000000000000000000000000000000000001"];
428 let res = encode_args(¶ms, &args);
429 assert!(res.is_ok());
430 let values = res.unwrap();
431 assert_eq!(values.len(), 2);
432
433 let args = vec!["1", "0x0000000000000000000000000000000000000001", "extra"];
435 let res = encode_args(¶ms, &args);
436 assert!(res.is_err());
437 assert!(format!("{}", res.unwrap_err()).contains("encode length mismatch"));
438 }
439}