Skip to main content

ethcli/bytecode/
proxy.rs

1//! Proxy contract detection
2//!
3//! Detects common proxy patterns:
4//! - EIP-1167: Minimal proxy clones
5//! - EIP-1967: Transparent proxy (implementation slot)
6//! - EIP-1822: UUPS proxy
7//! - OpenZeppelin patterns
8
9use alloy::primitives::{Address, B256, U256};
10
11/// Well-known storage slots for proxy implementations
12pub mod slots {
13    use alloy::primitives::B256;
14
15    /// EIP-1967 implementation slot: keccak256("eip1967.proxy.implementation") - 1
16    pub const EIP1967_IMPLEMENTATION: B256 = B256::new([
17        0x36, 0x08, 0x94, 0xa1, 0x3b, 0xa1, 0xa3, 0x21, 0x06, 0x67, 0xc8, 0x28, 0x49, 0x2d, 0xb9,
18        0x8d, 0xca, 0x3e, 0x20, 0x76, 0xcc, 0x37, 0x35, 0xa9, 0x20, 0xa3, 0xca, 0x50, 0x5d, 0x38,
19        0x2b, 0xbc,
20    ]);
21
22    /// EIP-1967 beacon slot: keccak256("eip1967.proxy.beacon") - 1
23    pub const EIP1967_BEACON: B256 = B256::new([
24        0xa3, 0xf0, 0xad, 0x74, 0xe5, 0x42, 0x3a, 0xeb, 0xfd, 0x80, 0xd3, 0xef, 0x4a, 0xe1, 0x47,
25        0x9b, 0xe7, 0x7d, 0x9a, 0x59, 0x50, 0x79, 0x28, 0xea, 0x95, 0x26, 0x91, 0xf4, 0x76, 0x82,
26        0x65, 0x43,
27    ]);
28
29    /// EIP-1967 admin slot: keccak256("eip1967.proxy.admin") - 1
30    pub const EIP1967_ADMIN: B256 = B256::new([
31        0xb5, 0x31, 0x27, 0x68, 0x4a, 0x56, 0x8b, 0x31, 0x73, 0xae, 0x13, 0xb9, 0xf8, 0xa6, 0x01,
32        0x6e, 0x24, 0x3e, 0x63, 0xb6, 0xe8, 0xee, 0x17, 0x66, 0x78, 0x3a, 0x7e, 0x2e, 0x70, 0x72,
33        0xef, 0xfc,
34    ]);
35
36    /// OpenZeppelin legacy implementation slot (before EIP-1967)
37    pub const OZ_LEGACY_IMPLEMENTATION: B256 = B256::new([
38        0x7e, 0x36, 0x7f, 0x9d, 0x1d, 0x41, 0xdb, 0x98, 0x6f, 0xbe, 0x8e, 0x56, 0x2a, 0xf1, 0x89,
39        0x1c, 0xc6, 0x4f, 0x2e, 0x06, 0x3f, 0x73, 0x12, 0xbe, 0x55, 0x4b, 0x12, 0x3b, 0xa7, 0x35,
40        0x87, 0xb0,
41    ]);
42}
43
44/// Detected proxy type
45#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
46#[serde(rename_all = "snake_case")]
47pub enum ProxyType {
48    /// EIP-1167 minimal proxy clone
49    Eip1167 {
50        /// The implementation address embedded in the bytecode
51        implementation: Address,
52    },
53    /// EIP-1967 transparent proxy
54    Eip1967,
55    /// EIP-1822 UUPS proxy
56    Eip1822,
57    /// OpenZeppelin legacy proxy
58    OpenZeppelinLegacy,
59    /// Generic proxy (detected via DELEGATECALL pattern but unknown type)
60    Generic,
61}
62
63impl ProxyType {
64    pub fn name(&self) -> &'static str {
65        match self {
66            ProxyType::Eip1167 { .. } => "EIP-1167 Minimal Proxy",
67            ProxyType::Eip1967 => "EIP-1967 Transparent Proxy",
68            ProxyType::Eip1822 => "EIP-1822 UUPS Proxy",
69            ProxyType::OpenZeppelinLegacy => "OpenZeppelin Legacy Proxy",
70            ProxyType::Generic => "Generic Proxy",
71        }
72    }
73
74    pub fn description(&self) -> &'static str {
75        match self {
76            ProxyType::Eip1167 { .. } => {
77                "Minimal proxy clone with implementation address embedded in bytecode"
78            }
79            ProxyType::Eip1967 => "Transparent proxy with implementation stored at EIP-1967 slot",
80            ProxyType::Eip1822 => {
81                "Universal Upgradeable Proxy Standard with upgrade logic in implementation"
82            }
83            ProxyType::OpenZeppelinLegacy => "Legacy OpenZeppelin proxy pattern (pre-EIP-1967)",
84            ProxyType::Generic => "Proxy detected via DELEGATECALL but pattern not recognized",
85        }
86    }
87}
88
89/// Result of proxy detection
90#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
91pub struct ProxyInfo {
92    /// Whether this contract is a proxy
93    pub is_proxy: bool,
94    /// The detected proxy type (if any)
95    pub proxy_type: Option<ProxyType>,
96    /// Implementation address if known (embedded in bytecode for EIP-1167,
97    /// or fetched from storage slot for EIP-1967/etc.)
98    pub implementation: Option<Address>,
99}
100
101/// Detect if bytecode is an EIP-1167 minimal proxy
102///
103/// EIP-1167 bytecode pattern:
104/// `363d3d373d3d3d363d73<20-byte-address>5af43d82803e903d91602b57fd5bf3`
105pub fn detect_eip1167(bytecode: &[u8]) -> Option<Address> {
106    // EIP-1167 exact bytecode is 45 bytes
107    if bytecode.len() < 45 {
108        return None;
109    }
110
111    // Check prefix: 363d3d373d3d3d363d73 (10 bytes)
112    let prefix = [0x36, 0x3d, 0x3d, 0x37, 0x3d, 0x3d, 0x3d, 0x36, 0x3d, 0x73];
113    if !bytecode.starts_with(&prefix) {
114        return None;
115    }
116
117    // Check suffix: 5af43d82803e903d91602b57fd5bf3 (15 bytes)
118    let suffix = [
119        0x5a, 0xf4, 0x3d, 0x82, 0x80, 0x3e, 0x90, 0x3d, 0x91, 0x60, 0x2b, 0x57, 0xfd, 0x5b, 0xf3,
120    ];
121    if !bytecode[30..45].starts_with(&suffix) {
122        return None;
123    }
124
125    // Extract the 20-byte implementation address
126    let addr_bytes: [u8; 20] = bytecode[10..30].try_into().ok()?;
127    Some(Address::from(addr_bytes))
128}
129
130/// Detect proxy patterns from bytecode alone
131///
132/// This only detects patterns that are visible in bytecode (like EIP-1167).
133/// For storage-based proxies (EIP-1967), use `detect_proxy_with_storage`.
134#[must_use]
135pub fn detect_proxy(bytecode: &[u8]) -> ProxyInfo {
136    // Check for EIP-1167 minimal proxy
137    if let Some(impl_addr) = detect_eip1167(bytecode) {
138        return ProxyInfo {
139            is_proxy: true,
140            proxy_type: Some(ProxyType::Eip1167 {
141                implementation: impl_addr,
142            }),
143            implementation: Some(impl_addr),
144        };
145    }
146
147    // Check for DELEGATECALL opcode (0xf4) which suggests proxy behavior
148    // This is a heuristic - not all contracts with DELEGATECALL are proxies
149    let has_delegatecall = bytecode.contains(&0xf4);
150
151    // Look for patterns that suggest EIP-1967/storage-based proxy:
152    // - Small bytecode with DELEGATECALL
153    // - Contains SLOAD opcode (0x54) for reading implementation slot
154    let has_sload = bytecode.contains(&0x54);
155
156    if has_delegatecall && has_sload && bytecode.len() < 2000 {
157        // Likely a storage-based proxy, but we need storage access to confirm
158        return ProxyInfo {
159            is_proxy: true,
160            proxy_type: Some(ProxyType::Generic),
161            implementation: None,
162        };
163    }
164
165    ProxyInfo::default()
166}
167
168/// Extract implementation address from storage slot value
169pub fn address_from_storage(value: B256) -> Option<Address> {
170    // Storage values are left-padded, address is in the last 20 bytes
171    let bytes = value.as_slice();
172
173    // Check if first 12 bytes are zero (properly formatted address)
174    if bytes[..12].iter().all(|&b| b == 0) {
175        let addr_bytes: [u8; 20] = bytes[12..32].try_into().ok()?;
176        let addr = Address::from(addr_bytes);
177
178        // Return None for zero address
179        if addr.is_zero() {
180            return None;
181        }
182        Some(addr)
183    } else {
184        None
185    }
186}
187
188/// Convert U256 to B256 for storage value
189pub fn u256_to_b256(value: U256) -> B256 {
190    B256::from(value)
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    #[test]
198    fn test_detect_eip1167() {
199        // EIP-1167 minimal proxy pointing to 0x1234...5678
200        let impl_addr = Address::repeat_byte(0x12);
201        let mut bytecode = vec![0x36, 0x3d, 0x3d, 0x37, 0x3d, 0x3d, 0x3d, 0x36, 0x3d, 0x73];
202        bytecode.extend_from_slice(impl_addr.as_slice());
203        bytecode.extend_from_slice(&[
204            0x5a, 0xf4, 0x3d, 0x82, 0x80, 0x3e, 0x90, 0x3d, 0x91, 0x60, 0x2b, 0x57, 0xfd, 0x5b,
205            0xf3,
206        ]);
207
208        let detected = detect_eip1167(&bytecode);
209        assert_eq!(detected, Some(impl_addr));
210    }
211
212    #[test]
213    fn test_detect_eip1167_real() {
214        // Real EIP-1167 bytecode from mainnet
215        let bytecode = hex::decode(
216            "363d3d373d3d3d363d73bebebebebebebebebebebebebebebebebebebebe5af43d82803e903d91602b57fd5bf3"
217        ).unwrap();
218
219        let result = detect_proxy(&bytecode);
220        assert!(result.is_proxy);
221        assert!(matches!(result.proxy_type, Some(ProxyType::Eip1167 { .. })));
222    }
223
224    #[test]
225    fn test_not_proxy() {
226        // Regular contract bytecode
227        let bytecode = hex::decode("6060604052600080fd").unwrap();
228        let result = detect_proxy(&bytecode);
229        assert!(!result.is_proxy);
230        assert!(result.proxy_type.is_none());
231    }
232
233    #[test]
234    fn test_address_from_storage() {
235        // Storage value with address in last 20 bytes
236        let addr = Address::repeat_byte(0xab);
237        let mut value = [0u8; 32];
238        value[12..32].copy_from_slice(addr.as_slice());
239        let b256 = B256::from(value);
240
241        assert_eq!(address_from_storage(b256), Some(addr));
242    }
243
244    #[test]
245    fn test_address_from_storage_zero() {
246        let b256 = B256::ZERO;
247        assert_eq!(address_from_storage(b256), None);
248    }
249}