Skip to main content

scope/contract/
proxy.rs

1//! # Proxy Pattern Detection
2//!
3//! Detects common Ethereum proxy patterns by analyzing bytecode,
4//! storage slots, and source code metadata.
5//!
6//! ## Supported Patterns
7//!
8//! - **EIP-1967** - Transparent Proxy (implementation slot: `0x360894...`)
9//! - **EIP-1822** - UUPS (Universal Upgradeable Proxy Standard)
10//! - **Transparent Proxy** - OpenZeppelin TransparentUpgradeableProxy
11//! - **Beacon Proxy** - EIP-1967 beacon slot
12//! - **Minimal Proxy** (EIP-1167) - Clone factory pattern (bytecode detection)
13//! - **Diamond** (EIP-2535) - Multi-facet proxy
14//!
15//! ## Detection Methods
16//!
17//! 1. **Etherscan metadata** - `Proxy` and `Implementation` fields from getsourcecode
18//! 2. **Storage slot reads** - Check well-known EIP-1967 slots
19//! 3. **Bytecode patterns** - EIP-1167 minimal proxy prefix/suffix
20
21use crate::contract::source::ContractSource;
22use crate::error::Result;
23use serde::{Deserialize, Serialize};
24
25/// Well-known EIP-1967 storage slots.
26/// Implementation slot: bytes32(uint256(keccak256('eip1967.proxy.implementation')) - 1)
27pub const EIP1967_IMPL_SLOT: &str =
28    "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
29/// Admin slot: bytes32(uint256(keccak256('eip1967.proxy.admin')) - 1)
30pub const EIP1967_ADMIN_SLOT: &str =
31    "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103";
32/// Beacon slot: bytes32(uint256(keccak256('eip1967.proxy.beacon')) - 1)
33pub const EIP1967_BEACON_SLOT: &str =
34    "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50";
35
36/// EIP-1167 minimal proxy bytecode prefix (first 10 bytes).
37const MINIMAL_PROXY_PREFIX: &str = "363d3d373d3d3d363d73";
38/// EIP-1167 minimal proxy bytecode suffix (last 15 bytes).
39const MINIMAL_PROXY_SUFFIX: &str = "5af43d82803e903d91602b57fd5bf3";
40
41/// Result of proxy pattern detection.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ProxyInfo {
44    /// Whether the contract is a proxy.
45    pub is_proxy: bool,
46    /// Type of proxy pattern detected.
47    pub proxy_type: String,
48    /// Implementation contract address (if resolvable).
49    pub implementation_address: Option<String>,
50    /// Admin address that can upgrade the proxy (if detectable).
51    pub admin_address: Option<String>,
52    /// Beacon address (for beacon proxies).
53    pub beacon_address: Option<String>,
54    /// Detailed findings about the proxy pattern.
55    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
71/// Detect proxy patterns using all available data sources.
72///
73/// Checks (in order):
74/// 1. Etherscan source metadata (Proxy/Implementation fields)
75/// 2. Bytecode analysis (EIP-1167 minimal proxy)
76/// 3. Storage slot reads (EIP-1967 implementation/admin/beacon)
77/// 4. Source code patterns (if verified)
78pub 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    // 1. Check Etherscan metadata
89    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    // 2. Check EIP-1167 minimal proxy (bytecode-only detection)
100    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        // Extract implementation address from bytecode (20 bytes after prefix)
105        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    // 3. Check EIP-1967 storage slots
115    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            // Determine proxy subtype
124            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    // Check admin slot
138    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    // Check beacon slot
148    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    // 4. Source code pattern analysis
160    if let Some(src) = source {
161        detect_proxy_from_source(src, &mut info);
162    }
163
164    // Check bytecode for DELEGATECALL pattern (generic proxy indicator)
165    if !info.is_proxy && code.contains("f4") {
166        // 0xf4 = DELEGATECALL opcode; not definitive alone but notable
167        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
179/// Extract an Ethereum address from a 32-byte storage slot value.
180/// Returns None if the slot is empty (all zeros).
181fn 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    // Address is in the last 40 hex chars of the 32-byte slot
187    let addr = &hex[hex.len() - 40..];
188    // Check if it's not the zero address
189    if addr == "0000000000000000000000000000000000000000" {
190        return None;
191    }
192    Some(format!("0x{}", addr))
193}
194
195/// Detect proxy patterns from source code analysis.
196fn detect_proxy_from_source(source: &ContractSource, info: &mut ProxyInfo) {
197    let code = &source.source_code;
198    let code_lower = code.to_lowercase();
199
200    // Diamond proxy (EIP-2535)
201    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    // OpenZeppelin upgradeable patterns
212    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    // Generic proxy/upgrade patterns
234    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"; // Non-minimal-proxy bytecode
506        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    /// Mock that returns configurable storage slot data for EIP-1967 proxy tests.
531    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}