1use crate::contract::source::ContractSource;
22use crate::error::Result;
23use serde::{Deserialize, Serialize};
24
25pub const EIP1967_IMPL_SLOT: &str =
28 "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
29pub const EIP1967_ADMIN_SLOT: &str =
31 "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103";
32pub const EIP1967_BEACON_SLOT: &str =
34 "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50";
35
36const MINIMAL_PROXY_PREFIX: &str = "363d3d373d3d3d363d73";
38const MINIMAL_PROXY_SUFFIX: &str = "5af43d82803e903d91602b57fd5bf3";
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProxyInfo {
44 pub is_proxy: bool,
46 pub proxy_type: String,
48 pub implementation_address: Option<String>,
50 pub admin_address: Option<String>,
52 pub beacon_address: Option<String>,
54 pub details: Vec<String>,
56}
57
58impl Default for ProxyInfo {
59 fn default() -> Self {
60 Self {
61 is_proxy: false,
62 proxy_type: "None".to_string(),
63 implementation_address: None,
64 admin_address: None,
65 beacon_address: None,
66 details: Vec::new(),
67 }
68 }
69}
70
71pub async fn detect_proxy(
79 address: &str,
80 _chain: &str,
81 bytecode: &str,
82 source: Option<&ContractSource>,
83 client: &dyn crate::chains::ChainClient,
84 _http_client: &reqwest::Client,
85) -> Result<ProxyInfo> {
86 let mut info = ProxyInfo::default();
87
88 if let Some(src) = source
90 && src.is_proxy
91 {
92 info.is_proxy = true;
93 info.proxy_type = "Etherscan-detected proxy".to_string();
94 info.implementation_address = src.implementation_address.clone();
95 info.details
96 .push("Etherscan flags this contract as a proxy.".to_string());
97 }
98
99 let code = bytecode.trim_start_matches("0x").to_lowercase();
101 if code.starts_with(MINIMAL_PROXY_PREFIX) && code.ends_with(MINIMAL_PROXY_SUFFIX) {
102 info.is_proxy = true;
103 info.proxy_type = "EIP-1167 Minimal Proxy (Clone)".to_string();
104 if code.len() >= MINIMAL_PROXY_PREFIX.len() + 40 {
106 let impl_addr = &code[MINIMAL_PROXY_PREFIX.len()..MINIMAL_PROXY_PREFIX.len() + 40];
107 info.implementation_address = Some(format!("0x{}", impl_addr));
108 }
109 info.details
110 .push("EIP-1167 minimal proxy detected from bytecode pattern.".to_string());
111 return Ok(info);
112 }
113
114 if let Ok(impl_slot) = client.get_storage_at(address, EIP1967_IMPL_SLOT).await {
116 let impl_addr = extract_address_from_slot(&impl_slot);
117 if let Some(addr) = impl_addr {
118 info.is_proxy = true;
119 info.implementation_address = Some(addr.clone());
120 info.details
121 .push(format!("EIP-1967 implementation slot points to {}", addr));
122
123 if let Some(src) = source {
125 let code_lower = src.source_code.to_lowercase();
126 if code_lower.contains("uups") || code_lower.contains("_upgradeto") {
127 info.proxy_type = "UUPS (EIP-1822)".to_string();
128 } else {
129 info.proxy_type = "Transparent Proxy (EIP-1967)".to_string();
130 }
131 } else {
132 info.proxy_type = "EIP-1967 Proxy".to_string();
133 }
134 }
135 }
136
137 if let Ok(admin_slot) = client.get_storage_at(address, EIP1967_ADMIN_SLOT).await {
139 let admin_addr = extract_address_from_slot(&admin_slot);
140 if let Some(addr) = admin_addr {
141 info.admin_address = Some(addr.clone());
142 info.details
143 .push(format!("EIP-1967 admin slot points to {}", addr));
144 }
145 }
146
147 if let Ok(beacon_slot) = client.get_storage_at(address, EIP1967_BEACON_SLOT).await {
149 let beacon_addr = extract_address_from_slot(&beacon_slot);
150 if let Some(addr) = beacon_addr {
151 info.is_proxy = true;
152 info.proxy_type = "Beacon Proxy (EIP-1967)".to_string();
153 info.beacon_address = Some(addr.clone());
154 info.details
155 .push(format!("EIP-1967 beacon slot points to {}", addr));
156 }
157 }
158
159 if let Some(src) = source {
161 detect_proxy_from_source(src, &mut info);
162 }
163
164 if !info.is_proxy && code.contains("f4") {
166 if let Some(src) = source
168 && src.source_code.contains("delegatecall")
169 {
170 info.details.push(
171 "Contract uses delegatecall (may be a proxy or proxy-like pattern).".to_string(),
172 );
173 }
174 }
175
176 Ok(info)
177}
178
179fn extract_address_from_slot(slot_value: &str) -> Option<String> {
182 let hex = slot_value.trim_start_matches("0x").to_lowercase();
183 if hex.len() < 40 {
184 return None;
185 }
186 let addr = &hex[hex.len() - 40..];
188 if addr == "0000000000000000000000000000000000000000" {
190 return None;
191 }
192 Some(format!("0x{}", addr))
193}
194
195fn detect_proxy_from_source(source: &ContractSource, info: &mut ProxyInfo) {
197 let code = &source.source_code;
198 let code_lower = code.to_lowercase();
199
200 if code_lower.contains("diamondcut")
202 || code_lower.contains("idiamond")
203 || code_lower.contains("facetcut")
204 {
205 info.is_proxy = true;
206 info.proxy_type = "Diamond Proxy (EIP-2535)".to_string();
207 info.details
208 .push("Diamond/multi-facet proxy pattern detected in source.".to_string());
209 }
210
211 if code_lower.contains("transparentupgradeableproxy") {
213 info.is_proxy = true;
214 info.proxy_type = "OpenZeppelin TransparentUpgradeableProxy".to_string();
215 info.details
216 .push("OpenZeppelin TransparentUpgradeableProxy import detected.".to_string());
217 }
218
219 if code_lower.contains("uupsupgradeable") {
220 info.is_proxy = true;
221 info.proxy_type = "UUPS (OpenZeppelin UUPSUpgradeable)".to_string();
222 info.details
223 .push("OpenZeppelin UUPSUpgradeable import detected.".to_string());
224 }
225
226 if code_lower.contains("beaconproxy") || code_lower.contains("upgradeablebeacon") {
227 info.is_proxy = true;
228 info.proxy_type = "Beacon Proxy (OpenZeppelin)".to_string();
229 info.details
230 .push("OpenZeppelin BeaconProxy pattern detected in source.".to_string());
231 }
232
233 if code_lower.contains("_setimplementation") || code_lower.contains("upgradeto(") {
235 if !info.is_proxy {
236 info.is_proxy = true;
237 info.proxy_type = "Upgradeable Proxy (custom)".to_string();
238 }
239 info.details
240 .push("Upgrade mechanism (upgradeTo/_setImplementation) found in source.".to_string());
241 }
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_extract_address_from_slot() {
250 let slot = "0x000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
251 let addr = extract_address_from_slot(slot);
252 assert_eq!(
253 addr,
254 Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
255 );
256 }
257
258 #[test]
259 fn test_extract_address_from_zero_slot() {
260 let slot = "0x0000000000000000000000000000000000000000000000000000000000000000";
261 let addr = extract_address_from_slot(slot);
262 assert_eq!(addr, None);
263 }
264
265 #[test]
266 fn test_minimal_proxy_detection() {
267 let prefix = MINIMAL_PROXY_PREFIX;
268 let suffix = MINIMAL_PROXY_SUFFIX;
269 let impl_addr = "bebebebebebebebebebebebebebebebebebebebe";
270 let bytecode = format!("0x{}{}{}", prefix, impl_addr, suffix);
271 let code = bytecode.trim_start_matches("0x").to_lowercase();
272 assert!(code.starts_with(MINIMAL_PROXY_PREFIX));
273 assert!(code.ends_with(MINIMAL_PROXY_SUFFIX));
274 }
275
276 #[test]
277 fn test_detect_proxy_from_source_diamond() {
278 let src = ContractSource {
279 contract_name: "DiamondProxy".to_string(),
280 source_code: "contract DiamondProxy { function diamondCut(...) {} }".to_string(),
281 abi: "[]".to_string(),
282 compiler_version: "v0.8.19".to_string(),
283 optimization_used: true,
284 optimization_runs: 200,
285 evm_version: "paris".to_string(),
286 license_type: "MIT".to_string(),
287 is_proxy: false,
288 implementation_address: None,
289 constructor_arguments: String::new(),
290 library: String::new(),
291 swarm_source: String::new(),
292 parsed_abi: vec![],
293 };
294 let mut info = ProxyInfo::default();
295 detect_proxy_from_source(&src, &mut info);
296 assert!(info.is_proxy);
297 assert!(info.proxy_type.contains("Diamond"));
298 }
299
300 #[test]
301 fn test_detect_proxy_from_source_uups() {
302 let src = ContractSource {
303 contract_name: "UUPSToken".to_string(),
304 source_code: "import UUPSUpgradeable; contract Token is UUPSUpgradeable {}".to_string(),
305 abi: "[]".to_string(),
306 compiler_version: "v0.8.19".to_string(),
307 optimization_used: true,
308 optimization_runs: 200,
309 evm_version: "paris".to_string(),
310 license_type: "MIT".to_string(),
311 is_proxy: false,
312 implementation_address: None,
313 constructor_arguments: String::new(),
314 library: String::new(),
315 swarm_source: String::new(),
316 parsed_abi: vec![],
317 };
318 let mut info = ProxyInfo::default();
319 detect_proxy_from_source(&src, &mut info);
320 assert!(info.is_proxy);
321 assert!(info.proxy_type.contains("UUPS"));
322 }
323
324 fn make_source(code: &str) -> ContractSource {
325 ContractSource {
326 contract_name: "Test".to_string(),
327 source_code: code.to_string(),
328 abi: "[]".to_string(),
329 compiler_version: "v0.8.19".to_string(),
330 optimization_used: true,
331 optimization_runs: 200,
332 evm_version: "paris".to_string(),
333 license_type: "MIT".to_string(),
334 is_proxy: false,
335 implementation_address: None,
336 constructor_arguments: String::new(),
337 library: String::new(),
338 swarm_source: String::new(),
339 parsed_abi: vec![],
340 }
341 }
342
343 #[test]
344 fn test_extract_address_short_hex() {
345 assert_eq!(extract_address_from_slot("0xabc"), None);
346 }
347
348 #[test]
349 fn test_extract_address_no_prefix() {
350 let slot = "000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7";
351 let addr = extract_address_from_slot(slot);
352 assert_eq!(
353 addr,
354 Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
355 );
356 }
357
358 #[test]
359 fn test_detect_transparent_upgradeable() {
360 let src = make_source(
361 "import TransparentUpgradeableProxy; contract P is TransparentUpgradeableProxy {}",
362 );
363 let mut info = ProxyInfo::default();
364 detect_proxy_from_source(&src, &mut info);
365 assert!(info.is_proxy);
366 assert!(info.proxy_type.contains("TransparentUpgradeableProxy"));
367 }
368
369 #[test]
370 fn test_detect_beacon_proxy() {
371 let src =
372 make_source("import BeaconProxy; contract P is BeaconProxy { UpgradeableBeacon b; }");
373 let mut info = ProxyInfo::default();
374 detect_proxy_from_source(&src, &mut info);
375 assert!(info.is_proxy);
376 assert!(info.proxy_type.contains("Beacon"));
377 }
378
379 #[test]
380 fn test_detect_upgrade_to_pattern() {
381 let src = make_source("function upgradeTo(address impl) { _setImplementation(impl); }");
382 let mut info = ProxyInfo::default();
383 detect_proxy_from_source(&src, &mut info);
384 assert!(info.is_proxy);
385 assert!(info.details.iter().any(|d| d.contains("upgradeTo")));
386 }
387
388 #[test]
389 fn test_non_proxy_source() {
390 let src = make_source("contract Token { function transfer() {} }");
391 let mut info = ProxyInfo::default();
392 detect_proxy_from_source(&src, &mut info);
393 assert!(!info.is_proxy);
394 }
395
396 #[test]
397 fn test_proxy_info_default() {
398 let info = ProxyInfo::default();
399 assert!(!info.is_proxy);
400 assert_eq!(info.proxy_type, "None");
401 assert!(info.implementation_address.is_none());
402 assert!(info.admin_address.is_none());
403 assert!(info.beacon_address.is_none());
404 assert!(info.details.is_empty());
405 }
406
407 #[test]
408 fn test_detect_proxy_from_source_idiamond() {
409 let src = make_source("import IDiamond; contract D is IDiamond { }");
410 let mut info = ProxyInfo::default();
411 detect_proxy_from_source(&src, &mut info);
412 assert!(info.is_proxy);
413 assert!(info.proxy_type.contains("Diamond"));
414 }
415
416 #[test]
417 fn test_detect_proxy_from_source_facetcut() {
418 let src = make_source(
419 "struct FacetCut { address target; } function addFacet(FacetCut[] calldata)",
420 );
421 let mut info = ProxyInfo::default();
422 detect_proxy_from_source(&src, &mut info);
423 assert!(info.is_proxy);
424 assert!(info.proxy_type.contains("Diamond"));
425 }
426
427 #[test]
428 fn test_detect_proxy_from_source_set_implementation() {
429 let src =
430 make_source("function _setImplementation(address impl) internal { _impl = impl; }");
431 let mut info = ProxyInfo::default();
432 detect_proxy_from_source(&src, &mut info);
433 assert!(info.is_proxy);
434 assert!(
435 info.details
436 .iter()
437 .any(|d| d.contains("_setImplementation"))
438 );
439 }
440
441 #[test]
442 fn test_extract_address_from_slot_uppercase_hex() {
443 let slot = "0x000000000000000000000000DAC17F958D2EE523A2206206994597C13D831EC7";
444 let addr = extract_address_from_slot(slot);
445 assert_eq!(
446 addr,
447 Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
448 );
449 }
450
451 #[test]
452 fn test_proxy_info_serialization() {
453 let info = ProxyInfo {
454 is_proxy: true,
455 proxy_type: "EIP-1967".to_string(),
456 implementation_address: Some("0x1234567890123456789012345678901234567890".to_string()),
457 admin_address: Some("0xadmin".to_string()),
458 beacon_address: None,
459 details: vec!["Detail 1".to_string()],
460 };
461 let json = serde_json::to_string(&info).unwrap();
462 let restored: ProxyInfo = serde_json::from_str(&json).unwrap();
463 assert_eq!(restored.is_proxy, info.is_proxy);
464 assert_eq!(restored.implementation_address, info.implementation_address);
465 assert_eq!(restored.details.len(), 1);
466 }
467
468 #[tokio::test]
469 async fn test_detect_proxy_minimal_eip1167() {
470 let prefix = MINIMAL_PROXY_PREFIX;
471 let suffix = MINIMAL_PROXY_SUFFIX;
472 let impl_addr = "bebebebebebebebebebebebebebebebebebebebe";
473 let bytecode = format!("0x{}{}{}", prefix, impl_addr, suffix);
474 let client = crate::chains::mocks::MockChainClient::new("ethereum", "ETH");
475 let http = reqwest::Client::new();
476 let result = detect_proxy("0xproxy", "ethereum", &bytecode, None, &client, &http)
477 .await
478 .unwrap();
479 assert!(result.is_proxy);
480 assert!(result.proxy_type.contains("EIP-1167"));
481 assert_eq!(
482 result.implementation_address,
483 Some("0xbebebebebebebebebebebebebebebebebebebebe".to_string())
484 );
485 }
486
487 #[tokio::test]
488 async fn test_detect_proxy_etherscan_metadata() {
489 let src = ContractSource {
490 contract_name: "Proxy".to_string(),
491 source_code: "contract Proxy {}".to_string(),
492 abi: "[]".to_string(),
493 compiler_version: "v0.8.19".to_string(),
494 optimization_used: true,
495 optimization_runs: 200,
496 evm_version: "paris".to_string(),
497 license_type: "MIT".to_string(),
498 is_proxy: true,
499 implementation_address: Some("0x1234567890123456789012345678901234567890".to_string()),
500 constructor_arguments: String::new(),
501 library: String::new(),
502 swarm_source: String::new(),
503 parsed_abi: vec![],
504 };
505 let bytecode = "0x6080604052348015600f57600080fd5b50"; let client = crate::chains::mocks::MockChainClient::new("ethereum", "ETH");
507 let http = reqwest::Client::new();
508 let result = detect_proxy("0xproxy", "ethereum", bytecode, Some(&src), &client, &http)
509 .await
510 .unwrap();
511 assert!(result.is_proxy);
512 assert!(result.proxy_type.contains("Etherscan"));
513 assert_eq!(
514 result.implementation_address,
515 Some("0x1234567890123456789012345678901234567890".to_string())
516 );
517 }
518
519 #[test]
520 fn test_extract_address_from_slot_exactly_40_chars() {
521 let slot_str =
522 "0x".to_string() + &"0".repeat(24) + "dac17f958d2ee523a2206206994597c13d831ec7";
523 let addr = extract_address_from_slot(&slot_str);
524 assert_eq!(
525 addr,
526 Some("0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())
527 );
528 }
529
530 struct StorageMockClient {
532 inner: crate::chains::mocks::MockChainClient,
533 impl_slot_value: Option<String>,
534 admin_slot_value: Option<String>,
535 beacon_slot_value: Option<String>,
536 }
537
538 impl StorageMockClient {
539 fn with_impl_slot(addr: &str) -> Self {
540 let padded = format!("0x{}{}", "0".repeat(24), addr.trim_start_matches("0x"));
541 Self {
542 inner: crate::chains::mocks::MockChainClient::new("ethereum", "ETH"),
543 impl_slot_value: Some(padded),
544 admin_slot_value: None,
545 beacon_slot_value: None,
546 }
547 }
548
549 fn with_all_slots(impl_addr: &str, admin_addr: &str, beacon_addr: &str) -> Self {
550 let pad = |a: &str| format!("0x{}{}", "0".repeat(24), a.trim_start_matches("0x"));
551 Self {
552 inner: crate::chains::mocks::MockChainClient::new("ethereum", "ETH"),
553 impl_slot_value: Some(pad(impl_addr)),
554 admin_slot_value: Some(pad(admin_addr)),
555 beacon_slot_value: Some(pad(beacon_addr)),
556 }
557 }
558 }
559
560 #[async_trait::async_trait]
561 impl crate::chains::ChainClient for StorageMockClient {
562 fn chain_name(&self) -> &str {
563 self.inner.chain_name()
564 }
565
566 fn native_token_symbol(&self) -> &str {
567 self.inner.native_token_symbol()
568 }
569
570 async fn get_balance(&self, a: &str) -> crate::error::Result<crate::chains::Balance> {
571 self.inner.get_balance(a).await
572 }
573
574 async fn enrich_balance_usd(&self, b: &mut crate::chains::Balance) {
575 self.inner.enrich_balance_usd(b).await
576 }
577
578 async fn get_transaction(
579 &self,
580 h: &str,
581 ) -> crate::error::Result<crate::chains::Transaction> {
582 self.inner.get_transaction(h).await
583 }
584
585 async fn get_transactions(
586 &self,
587 a: &str,
588 limit: u32,
589 ) -> crate::error::Result<Vec<crate::chains::Transaction>> {
590 self.inner.get_transactions(a, limit).await
591 }
592
593 async fn get_block_number(&self) -> crate::error::Result<u64> {
594 self.inner.get_block_number().await
595 }
596
597 async fn get_token_balances(
598 &self,
599 a: &str,
600 ) -> crate::error::Result<Vec<crate::chains::TokenBalance>> {
601 self.inner.get_token_balances(a).await
602 }
603
604 async fn get_storage_at(&self, _address: &str, slot: &str) -> crate::error::Result<String> {
605 if slot == EIP1967_IMPL_SLOT
606 && let Some(ref v) = self.impl_slot_value
607 {
608 return Ok(v.clone());
609 }
610 if slot == EIP1967_ADMIN_SLOT
611 && let Some(ref v) = self.admin_slot_value
612 {
613 return Ok(v.clone());
614 }
615 if slot == EIP1967_BEACON_SLOT
616 && let Some(ref v) = self.beacon_slot_value
617 {
618 return Ok(v.clone());
619 }
620 Err(crate::error::ScopeError::Chain(
621 "No storage mock".to_string(),
622 ))
623 }
624 }
625
626 #[tokio::test]
627 async fn test_detect_proxy_eip1967_transparent() {
628 let client = StorageMockClient::with_impl_slot("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
629 let http = reqwest::Client::new();
630 let result = detect_proxy("0xproxy", "ethereum", "0x6080604052", None, &client, &http)
631 .await
632 .unwrap();
633 assert!(result.is_proxy);
634 assert!(result.proxy_type.contains("EIP-1967"));
635 assert_eq!(
636 result.implementation_address,
637 Some("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".to_string())
638 );
639 }
640
641 #[tokio::test]
642 async fn test_detect_proxy_eip1967_uups_with_source() {
643 let src = ContractSource {
644 contract_name: "UUPSProxy".to_string(),
645 source_code: "contract UUPSProxy { function _upgradeTo(address) {} }".to_string(),
646 abi: "[]".to_string(),
647 compiler_version: "v0.8.19".to_string(),
648 optimization_used: true,
649 optimization_runs: 200,
650 evm_version: "paris".to_string(),
651 license_type: "MIT".to_string(),
652 is_proxy: false,
653 implementation_address: None,
654 constructor_arguments: String::new(),
655 library: String::new(),
656 swarm_source: String::new(),
657 parsed_abi: vec![],
658 };
659 let client = StorageMockClient::with_impl_slot("a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
660 let http = reqwest::Client::new();
661 let result = detect_proxy(
662 "0xproxy",
663 "ethereum",
664 "0x6080604052",
665 Some(&src),
666 &client,
667 &http,
668 )
669 .await
670 .unwrap();
671 assert!(result.is_proxy);
672 assert!(result.proxy_type.contains("UUPS"));
673 }
674
675 #[tokio::test]
676 async fn test_detect_proxy_eip1967_admin_and_beacon_slots() {
677 let client = StorageMockClient::with_all_slots(
678 "a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
679 "b0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
680 "c0b86991c6218b36c1d19d4a2e9eb0ce3606eb48",
681 );
682 let http = reqwest::Client::new();
683 let result = detect_proxy("0xproxy", "ethereum", "0x6080604052", None, &client, &http)
684 .await
685 .unwrap();
686 assert!(result.is_proxy);
687 assert!(result.admin_address.is_some());
688 assert!(result.beacon_address.is_some());
689 assert!(result.proxy_type.contains("Beacon"));
690 }
691
692 #[tokio::test]
693 async fn test_detect_proxy_delegatecall_in_source() {
694 let src = ContractSource {
695 contract_name: "DelegateProxy".to_string(),
696 source_code: "contract D { function run() { delegatecall(...); } }".to_string(),
697 abi: "[]".to_string(),
698 compiler_version: "v0.8.19".to_string(),
699 optimization_used: true,
700 optimization_runs: 200,
701 evm_version: "paris".to_string(),
702 license_type: "MIT".to_string(),
703 is_proxy: false,
704 implementation_address: None,
705 constructor_arguments: String::new(),
706 library: String::new(),
707 swarm_source: String::new(),
708 parsed_abi: vec![],
709 };
710 let client = crate::chains::mocks::MockChainClient::new("ethereum", "ETH");
711 let http = reqwest::Client::new();
712 let result = detect_proxy(
713 "0xproxy",
714 "ethereum",
715 "0x6080f4604052",
716 Some(&src),
717 &client,
718 &http,
719 )
720 .await
721 .unwrap();
722 assert!(result.details.iter().any(|d| d.contains("delegatecall")));
723 }
724}