Skip to main content

chaincodec_evm/
proxy.rs

1//! EVM proxy contract pattern detection and resolution.
2//!
3//! Detects common proxy patterns and identifies the implementation contract address.
4//!
5//! ## Supported Patterns
6//!
7//! | Pattern | Detection | Standard |
8//! |---------|-----------|----------|
9//! | EIP-1967 Logic Proxy | Storage slot | EIP-1967 |
10//! | EIP-1822 UUPS Proxy | `proxiableUUID()` slot | EIP-1822 |
11//! | OpenZeppelin Transparent Proxy | Admin slot + logic slot | OZ |
12//! | EIP-1167 Minimal Proxy (Clone) | Bytecode prefix | EIP-1167 |
13//! | Gnosis Safe | `masterCopy()` call | Gnosis |
14//!
15//! ## Usage with an RPC client
16//!
17//! The detection functions require reading blockchain state (storage slots or bytecode),
18//! so they take an `RpcAdapter` trait object. Provide a concrete implementation
19//! backed by `eth_getStorageAt` and `eth_getCode`.
20
21use serde::{Deserialize, Serialize};
22
23/// The detected proxy pattern.
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case")]
26pub enum ProxyKind {
27    /// EIP-1967 standard logic proxy (most common modern pattern)
28    Eip1967Logic,
29    /// EIP-1967 beacon proxy (implementation fetched from beacon contract)
30    Eip1967Beacon,
31    /// EIP-1822 UUPS (Universal Upgradeable Proxy Standard)
32    Eip1822Uups,
33    /// OpenZeppelin Transparent Proxy (legacy, pre-EIP-1967)
34    OzTransparent,
35    /// EIP-1167 Minimal Proxy (Clone) — cheap non-upgradeable clone
36    Eip1167Clone,
37    /// Gnosis Safe proxy
38    GnosisSafe,
39    /// Unknown proxy — bytecode or storage suggests a proxy but pattern is unrecognized
40    Unknown,
41}
42
43/// Result of proxy detection.
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ProxyInfo {
46    /// The proxy contract address
47    pub proxy_address: String,
48    /// The detected proxy pattern
49    pub kind: ProxyKind,
50    /// The resolved implementation address (if determinable without RPC)
51    pub implementation: Option<String>,
52    /// The storage slot key used to find the implementation (hex)
53    pub slot: Option<String>,
54}
55
56// ─── EIP-1967 Storage Slots ───────────────────────────────────────────────────
57
58/// EIP-1967 implementation slot:
59/// `keccak256("eip1967.proxy.implementation") - 1`
60pub const EIP1967_IMPL_SLOT: &str =
61    "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc";
62
63/// EIP-1967 admin slot:
64/// `keccak256("eip1967.proxy.admin") - 1`
65pub const EIP1967_ADMIN_SLOT: &str =
66    "0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103";
67
68/// EIP-1967 beacon slot:
69/// `keccak256("eip1967.proxy.beacon") - 1`
70pub const EIP1967_BEACON_SLOT: &str =
71    "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50";
72
73/// EIP-1822 UUPS proxiable slot:
74/// `keccak256("PROXIABLE")`
75pub const EIP1822_PROXIABLE_SLOT: &str =
76    "0xc5f16f0fcc639fa48a6947836d9850f504798523bf8c9a3a87d5876cf622bcf7";
77
78/// EIP-1167 minimal proxy bytecode prefix (20-byte address embedded at offset 10)
79pub const EIP1167_BYTECODE_PREFIX: &[u8] = &[
80    0x36, 0x3d, 0x3d, 0x37, 0x3d, 0x3d, 0x3d, 0x36, 0x3d, 0x73,
81];
82
83/// EIP-1167 minimal proxy bytecode suffix
84pub const EIP1167_BYTECODE_SUFFIX: &[u8] = &[
85    0x5a, 0xf4, 0x3d, 0x82, 0x80, 0x3e, 0x90, 0x3d, 0x91, 0x60, 0x2b, 0x57, 0xfd, 0x5b, 0xf3,
86];
87
88// ─── Bytecode Analysis ────────────────────────────────────────────────────────
89
90/// Detect an EIP-1167 minimal proxy from raw bytecode.
91///
92/// Returns the implementation address if the bytecode matches the EIP-1167 pattern.
93/// This requires NO RPC call — it works from `eth_getCode` output alone.
94///
95/// # Arguments
96/// * `bytecode` - raw bytecode bytes (not hex)
97pub fn detect_eip1167_clone(bytecode: &[u8]) -> Option<String> {
98    // EIP-1167 minimal proxy: 45 bytes total
99    // Layout: [10 prefix bytes] [20 address bytes] [15 suffix bytes]
100    if bytecode.len() != 45 {
101        return None;
102    }
103    if &bytecode[..10] != EIP1167_BYTECODE_PREFIX {
104        return None;
105    }
106    if &bytecode[30..] != EIP1167_BYTECODE_SUFFIX {
107        return None;
108    }
109    let addr_bytes = &bytecode[10..30];
110    Some(format!("0x{}", hex::encode(addr_bytes)))
111}
112
113/// Check if a storage slot value looks like a non-zero address.
114///
115/// EVM storage returns 32-byte zero-padded values. An address is stored in the
116/// lower 20 bytes. Returns `Some(address)` if bytes 12-32 are a non-zero address.
117pub fn storage_to_address(slot_value: &str) -> Option<String> {
118    let hex = slot_value.strip_prefix("0x").unwrap_or(slot_value);
119    if hex.len() != 64 {
120        return None;
121    }
122    // Bytes 0-11 should be zero (12 bytes = 24 hex chars)
123    let prefix = &hex[..24];
124    let addr_hex = &hex[24..];
125
126    if prefix.chars().all(|c| c == '0') && addr_hex != "0".repeat(40) {
127        Some(format!("0x{addr_hex}"))
128    } else {
129        None
130    }
131}
132
133/// Classify a proxy based on known storage slot values from an RPC call.
134///
135/// Provide the 32-byte hex values from `eth_getStorageAt` for each slot.
136/// Pass `None` if the slot returned zero or could not be fetched.
137pub fn classify_from_storage(
138    proxy_address: &str,
139    eip1967_impl: Option<&str>,
140    eip1967_beacon: Option<&str>,
141    eip1822_proxiable: Option<&str>,
142) -> ProxyInfo {
143    // EIP-1967 logic proxy
144    if let Some(impl_raw) = eip1967_impl {
145        if let Some(impl_addr) = storage_to_address(impl_raw) {
146            return ProxyInfo {
147                proxy_address: proxy_address.to_string(),
148                kind: ProxyKind::Eip1967Logic,
149                implementation: Some(impl_addr),
150                slot: Some(EIP1967_IMPL_SLOT.to_string()),
151            };
152        }
153    }
154
155    // EIP-1967 beacon proxy
156    if let Some(beacon_raw) = eip1967_beacon {
157        if let Some(beacon_addr) = storage_to_address(beacon_raw) {
158            return ProxyInfo {
159                proxy_address: proxy_address.to_string(),
160                kind: ProxyKind::Eip1967Beacon,
161                // For beacon proxies, the actual impl is in beacon.implementation()
162                // We record the beacon address as the "implementation" for now
163                implementation: Some(beacon_addr),
164                slot: Some(EIP1967_BEACON_SLOT.to_string()),
165            };
166        }
167    }
168
169    // EIP-1822 UUPS
170    if let Some(uups_raw) = eip1822_proxiable {
171        if let Some(impl_addr) = storage_to_address(uups_raw) {
172            return ProxyInfo {
173                proxy_address: proxy_address.to_string(),
174                kind: ProxyKind::Eip1822Uups,
175                implementation: Some(impl_addr),
176                slot: Some(EIP1822_PROXIABLE_SLOT.to_string()),
177            };
178        }
179    }
180
181    ProxyInfo {
182        proxy_address: proxy_address.to_string(),
183        kind: ProxyKind::Unknown,
184        implementation: None,
185        slot: None,
186    }
187}
188
189/// Async-friendly helper: build the storage slots to query for proxy detection.
190///
191/// Returns a list of `(label, slot_hex)` pairs that should be passed to
192/// `eth_getStorageAt(address, slot, "latest")`.
193pub fn proxy_detection_slots() -> Vec<(&'static str, &'static str)> {
194    vec![
195        ("eip1967_impl", EIP1967_IMPL_SLOT),
196        ("eip1967_beacon", EIP1967_BEACON_SLOT),
197        ("eip1822_proxiable", EIP1822_PROXIABLE_SLOT),
198    ]
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn detect_eip1167_valid() {
207        // Build a valid 45-byte EIP-1167 bytecode
208        let mut bytecode = Vec::new();
209        bytecode.extend_from_slice(EIP1167_BYTECODE_PREFIX);
210        // 20-byte implementation address
211        let impl_addr = [0xABu8; 20];
212        bytecode.extend_from_slice(&impl_addr);
213        bytecode.extend_from_slice(EIP1167_BYTECODE_SUFFIX);
214
215        let detected = detect_eip1167_clone(&bytecode);
216        assert!(detected.is_some());
217        assert_eq!(detected.unwrap(), format!("0x{}", "ab".repeat(20)));
218    }
219
220    #[test]
221    fn detect_eip1167_wrong_length() {
222        let bytecode = vec![0u8; 44]; // too short
223        assert!(detect_eip1167_clone(&bytecode).is_none());
224    }
225
226    #[test]
227    fn storage_to_address_valid() {
228        let slot =
229            "0x000000000000000000000000d8da6bf26964af9d7eed9e03e53415d37aa96045";
230        let addr = storage_to_address(slot).unwrap();
231        assert_eq!(addr, "0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
232    }
233
234    #[test]
235    fn storage_to_address_zero_returns_none() {
236        let slot = "0x0000000000000000000000000000000000000000000000000000000000000000";
237        assert!(storage_to_address(slot).is_none());
238    }
239
240    #[test]
241    fn classify_eip1967_impl() {
242        let impl_slot = "0x000000000000000000000000beefbeefbeefbeefbeefbeefbeefbeefbeefbeef";
243        let info = classify_from_storage("0xproxy", Some(impl_slot), None, None);
244        assert_eq!(info.kind, ProxyKind::Eip1967Logic);
245        assert!(info.implementation.is_some());
246    }
247
248    #[test]
249    fn classify_unknown_when_all_zero() {
250        let zero = "0x0000000000000000000000000000000000000000000000000000000000000000";
251        let info = classify_from_storage("0xproxy", Some(zero), Some(zero), Some(zero));
252        assert_eq!(info.kind, ProxyKind::Unknown);
253    }
254
255    #[test]
256    fn detection_slots_non_empty() {
257        let slots = proxy_detection_slots();
258        assert_eq!(slots.len(), 3);
259        assert_eq!(slots[0].1, EIP1967_IMPL_SLOT);
260    }
261}