Skip to main content

chains_sdk/ethereum/
proxy.rs

1//! **Proxy contract** interaction helpers for UUPS, Transparent, and Beacon proxies.
2//!
3//! Provides calldata encoding for proxy upgrades, admin management,
4//! EIP-1967 storage slot constants, and Multicall3 batch encoding.
5//!
6//! # Example
7//! ```no_run
8//! use chains_sdk::ethereum::proxy;
9//!
10//! // UUPS upgrade
11//! let calldata = proxy::encode_upgrade_to([0xBB; 20]);
12//!
13//! // Multicall3 batch
14//! let calls = vec![
15//!     proxy::Multicall3Call { target: [0xAA; 20], allow_failure: false, call_data: vec![0x01] },
16//!     proxy::Multicall3Call { target: [0xBB; 20], allow_failure: true, call_data: vec![0x02] },
17//! ];
18//! let batch = proxy::encode_multicall(&calls);
19//! ```
20
21use crate::ethereum::abi::{self, AbiValue};
22
23// ─── EIP-1967 Storage Slots ────────────────────────────────────────
24
25/// EIP-1967 implementation storage slot.
26///
27/// `bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)`
28pub const IMPLEMENTATION_SLOT: [u8; 32] = {
29    // keccak256("eip1967.proxy.implementation") - 1
30    // = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
31    let mut s = [0u8; 32];
32    s[0] = 0x36;
33    s[1] = 0x08;
34    s[2] = 0x94;
35    s[3] = 0xa1;
36    s[4] = 0x3b;
37    s[5] = 0xa1;
38    s[6] = 0xa3;
39    s[7] = 0x21;
40    s[8] = 0x06;
41    s[9] = 0x67;
42    s[10] = 0xc8;
43    s[11] = 0x28;
44    s[12] = 0x49;
45    s[13] = 0x2d;
46    s[14] = 0xb9;
47    s[15] = 0x8d;
48    s[16] = 0xca;
49    s[17] = 0x3e;
50    s[18] = 0x20;
51    s[19] = 0x76;
52    s[20] = 0xcc;
53    s[21] = 0x37;
54    s[22] = 0x35;
55    s[23] = 0xa9;
56    s[24] = 0x20;
57    s[25] = 0xa3;
58    s[26] = 0xca;
59    s[27] = 0x50;
60    s[28] = 0x5d;
61    s[29] = 0x38;
62    s[30] = 0x2b;
63    s[31] = 0xbc;
64    s
65};
66
67/// EIP-1967 admin storage slot.
68///
69/// `bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1)`
70pub const ADMIN_SLOT: [u8; 32] = {
71    // 0xb53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103
72    let mut s = [0u8; 32];
73    s[0] = 0xb5;
74    s[1] = 0x31;
75    s[2] = 0x27;
76    s[3] = 0x68;
77    s[4] = 0x4a;
78    s[5] = 0x56;
79    s[6] = 0x8b;
80    s[7] = 0x31;
81    s[8] = 0x73;
82    s[9] = 0xae;
83    s[10] = 0x13;
84    s[11] = 0xb9;
85    s[12] = 0xf8;
86    s[13] = 0xa6;
87    s[14] = 0x01;
88    s[15] = 0x6e;
89    s[16] = 0x24;
90    s[17] = 0x3e;
91    s[18] = 0x63;
92    s[19] = 0xb6;
93    s[20] = 0xe8;
94    s[21] = 0xee;
95    s[22] = 0x11;
96    s[23] = 0x78;
97    s[24] = 0xd6;
98    s[25] = 0xa7;
99    s[26] = 0x17;
100    s[27] = 0x85;
101    s[28] = 0x0b;
102    s[29] = 0x5d;
103    s[30] = 0x61;
104    s[31] = 0x03;
105    s
106};
107
108/// EIP-1967 beacon storage slot.
109///
110/// `bytes32(uint256(keccak256("eip1967.proxy.beacon")) - 1)`
111pub const BEACON_SLOT: [u8; 32] = {
112    // 0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50
113    let mut s = [0u8; 32];
114    s[0] = 0xa3;
115    s[1] = 0xf0;
116    s[2] = 0xad;
117    s[3] = 0x74;
118    s[4] = 0xe5;
119    s[5] = 0x42;
120    s[6] = 0x3a;
121    s[7] = 0xeb;
122    s[8] = 0xfd;
123    s[9] = 0x80;
124    s[10] = 0xd3;
125    s[11] = 0xef;
126    s[12] = 0x43;
127    s[13] = 0x46;
128    s[14] = 0x57;
129    s[15] = 0x83;
130    s[16] = 0x35;
131    s[17] = 0xa9;
132    s[18] = 0xa7;
133    s[19] = 0x2a;
134    s[20] = 0xea;
135    s[21] = 0xee;
136    s[22] = 0x59;
137    s[23] = 0xff;
138    s[24] = 0x6c;
139    s[25] = 0xb3;
140    s[26] = 0x58;
141    s[27] = 0x2b;
142    s[28] = 0x35;
143    s[29] = 0x13;
144    s[30] = 0x3d;
145    s[31] = 0x50;
146    s
147};
148
149// ─── EIP-1967 Slot Computation ─────────────────────────────────────
150
151/// Compute an EIP-1967 storage slot from its label.
152///
153/// `bytes32(uint256(keccak256(label)) - 1)`
154#[must_use]
155pub fn eip1967_slot(label: &str) -> [u8; 32] {
156    let hash = keccak256(label.as_bytes());
157    // Subtract 1 from the 256-bit hash
158    let mut slot = hash;
159    let mut borrow = true;
160    for byte in slot.iter_mut().rev() {
161        if borrow {
162            if *byte == 0 {
163                *byte = 0xFF;
164            } else {
165                *byte -= 1;
166                borrow = false;
167            }
168        }
169    }
170    slot
171}
172
173// ─── UUPS Proxy ────────────────────────────────────────────────────
174
175/// ABI-encode `upgradeTo(address newImplementation)`.
176///
177/// Standard UUPS upgrade function (OpenZeppelin `UUPSUpgradeable`).
178#[must_use]
179pub fn encode_upgrade_to(new_implementation: [u8; 20]) -> Vec<u8> {
180    let func = abi::Function::new("upgradeTo(address)");
181    func.encode(&[AbiValue::Address(new_implementation)])
182}
183
184/// ABI-encode `upgradeToAndCall(address newImplementation, bytes data)`.
185///
186/// UUPS upgrade with initialization call in the new implementation context.
187#[must_use]
188pub fn encode_upgrade_to_and_call(new_implementation: [u8; 20], data: &[u8]) -> Vec<u8> {
189    let func = abi::Function::new("upgradeToAndCall(address,bytes)");
190    func.encode(&[
191        AbiValue::Address(new_implementation),
192        AbiValue::Bytes(data.to_vec()),
193    ])
194}
195
196/// ABI-encode `proxiableUUID()` for UUPS compliance check.
197///
198/// Returns the implementation slot — compliant contracts return `IMPLEMENTATION_SLOT`.
199#[must_use]
200pub fn encode_proxiable_uuid() -> Vec<u8> {
201    let func = abi::Function::new("proxiableUUID()");
202    func.encode(&[])
203}
204
205/// ABI-encode `implementation()` to query the current implementation address.
206#[must_use]
207pub fn encode_implementation() -> Vec<u8> {
208    let func = abi::Function::new("implementation()");
209    func.encode(&[])
210}
211
212// ─── Transparent Proxy ─────────────────────────────────────────────
213
214/// ABI-encode `changeAdmin(address newAdmin)`.
215#[must_use]
216pub fn encode_change_admin(new_admin: [u8; 20]) -> Vec<u8> {
217    let func = abi::Function::new("changeAdmin(address)");
218    func.encode(&[AbiValue::Address(new_admin)])
219}
220
221/// ABI-encode `admin()` to query the proxy admin address.
222#[must_use]
223pub fn encode_admin() -> Vec<u8> {
224    let func = abi::Function::new("admin()");
225    func.encode(&[])
226}
227
228// ─── Beacon Proxy ──────────────────────────────────────────────────
229
230/// ABI-encode `upgradeTo(address newBeacon)` for beacon upgrades.
231///
232/// Note: Same function signature as UUPS, but called on the beacon contract.
233#[must_use]
234pub fn encode_upgrade_beacon(new_beacon: [u8; 20]) -> Vec<u8> {
235    encode_upgrade_to(new_beacon)
236}
237
238// ─── Initializable ─────────────────────────────────────────────────
239
240/// ABI-encode `initialize(...)` with arbitrary arguments.
241///
242/// Generic initializer encoder for proxy-behind initialization.
243#[must_use]
244pub fn encode_initialize(args: &[AbiValue]) -> Vec<u8> {
245    // The Initialize function signature varies per contract, but
246    // for simple cases, the user provides the full signature
247    let func = abi::Function::new("initialize()");
248    func.encode(args)
249}
250
251/// ABI-encode a custom initializer with a specific function signature.
252#[must_use]
253pub fn encode_initializer(signature: &str, args: &[AbiValue]) -> Vec<u8> {
254    let func = abi::Function::new(signature);
255    func.encode(args)
256}
257
258// ─── Multicall3 ────────────────────────────────────────────────────
259
260/// A single call in a Multicall3 batch.
261#[derive(Debug, Clone)]
262pub struct Multicall3Call {
263    /// Target contract address.
264    pub target: [u8; 20],
265    /// Whether this call is allowed to fail without reverting the batch.
266    pub allow_failure: bool,
267    /// Encoded calldata for this call.
268    pub call_data: Vec<u8>,
269}
270
271/// ABI-encode `aggregate3(Call3[])` for Multicall3.
272///
273/// Multicall3 is deployed at `0xcA11bde05977b3631167028862bE2a173976CA11`
274/// on 70+ chains.
275#[must_use]
276pub fn encode_multicall(calls: &[Multicall3Call]) -> Vec<u8> {
277    let func = abi::Function::new("aggregate3((address,bool,bytes)[])");
278    let call_tuples: Vec<AbiValue> = calls
279        .iter()
280        .map(|c| {
281            AbiValue::Tuple(vec![
282                AbiValue::Address(c.target),
283                AbiValue::Bool(c.allow_failure),
284                AbiValue::Bytes(c.call_data.clone()),
285            ])
286        })
287        .collect();
288    func.encode(&[AbiValue::Array(call_tuples)])
289}
290
291/// Multicall3 canonical deployment address (same on 70+ chains).
292pub const MULTICALL3_ADDRESS: [u8; 20] = [
293    0xca, 0x11, 0xbd, 0xe0, 0x59, 0x77, 0xb3, 0x63, 0x11, 0x67, 0x02, 0x88, 0x62, 0xbe, 0x2a, 0x17,
294    0x39, 0x76, 0xca, 0x11,
295];
296
297/// ABI-encode `aggregate(Call[])` for Multicall2 (legacy).
298#[must_use]
299pub fn encode_multicall_legacy(calls: &[([u8; 20], Vec<u8>)]) -> Vec<u8> {
300    let func = abi::Function::new("aggregate((address,bytes)[])");
301    let call_tuples: Vec<AbiValue> = calls
302        .iter()
303        .map(|(target, data)| {
304            AbiValue::Tuple(vec![
305                AbiValue::Address(*target),
306                AbiValue::Bytes(data.clone()),
307            ])
308        })
309        .collect();
310    func.encode(&[AbiValue::Array(call_tuples)])
311}
312
313// ─── Internal Helpers ──────────────────────────────────────────────
314
315fn keccak256(data: &[u8]) -> [u8; 32] {
316    super::keccak256(data)
317}
318
319// ─── Tests ─────────────────────────────────────────────────────────
320
321#[cfg(test)]
322#[allow(clippy::unwrap_used, clippy::expect_used)]
323mod tests {
324    use super::*;
325
326    // ─── EIP-1967 Slot Constants ──────────────────────────────
327
328    #[test]
329    fn test_implementation_slot_matches_eip1967() {
330        let computed = eip1967_slot("eip1967.proxy.implementation");
331        assert_eq!(computed, IMPLEMENTATION_SLOT);
332    }
333
334    #[test]
335    fn test_admin_slot_matches_eip1967() {
336        let computed = eip1967_slot("eip1967.proxy.admin");
337        assert_eq!(computed, ADMIN_SLOT);
338    }
339
340    #[test]
341    fn test_beacon_slot_matches_eip1967() {
342        let computed = eip1967_slot("eip1967.proxy.beacon");
343        assert_eq!(computed, BEACON_SLOT);
344    }
345
346    #[test]
347    fn test_implementation_slot_hex() {
348        assert_eq!(
349            hex::encode(IMPLEMENTATION_SLOT),
350            "360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
351        );
352    }
353
354    #[test]
355    fn test_admin_slot_hex() {
356        assert_eq!(
357            hex::encode(ADMIN_SLOT),
358            "b53127684a568b3173ae13b9f8a6016e243e63b6e8ee1178d6a717850b5d6103"
359        );
360    }
361
362    #[test]
363    fn test_beacon_slot_hex() {
364        assert_eq!(
365            hex::encode(BEACON_SLOT),
366            "a3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"
367        );
368    }
369
370    // ─── Slot Computation ─────────────────────────────────────
371
372    #[test]
373    fn test_eip1967_slot_deterministic() {
374        let s1 = eip1967_slot("eip1967.proxy.implementation");
375        let s2 = eip1967_slot("eip1967.proxy.implementation");
376        assert_eq!(s1, s2);
377    }
378
379    #[test]
380    fn test_eip1967_slot_different_labels() {
381        let s1 = eip1967_slot("eip1967.proxy.implementation");
382        let s2 = eip1967_slot("eip1967.proxy.admin");
383        assert_ne!(s1, s2);
384    }
385
386    // ─── UUPS Encoding ───────────────────────────────────────
387
388    #[test]
389    fn test_encode_upgrade_to_selector() {
390        let calldata = encode_upgrade_to([0xBB; 20]);
391        let expected = abi::function_selector("upgradeTo(address)");
392        assert_eq!(&calldata[..4], &expected);
393        assert_eq!(calldata.len(), 4 + 32);
394    }
395
396    #[test]
397    fn test_encode_upgrade_to_contains_address() {
398        let addr = [0xBB; 20];
399        let calldata = encode_upgrade_to(addr);
400        // Address should be at bytes 4+12..4+32 (left-padded)
401        assert_eq!(&calldata[4 + 12..4 + 32], &addr);
402    }
403
404    #[test]
405    fn test_encode_upgrade_to_and_call_selector() {
406        let calldata = encode_upgrade_to_and_call([0xBB; 20], &[0xDE, 0xAD]);
407        let expected = abi::function_selector("upgradeToAndCall(address,bytes)");
408        assert_eq!(&calldata[..4], &expected);
409    }
410
411    #[test]
412    fn test_encode_upgrade_to_and_call_with_empty_data() {
413        let calldata = encode_upgrade_to_and_call([0xBB; 20], &[]);
414        let expected = abi::function_selector("upgradeToAndCall(address,bytes)");
415        assert_eq!(&calldata[..4], &expected);
416    }
417
418    #[test]
419    fn test_encode_proxiable_uuid_selector() {
420        let calldata = encode_proxiable_uuid();
421        let expected = abi::function_selector("proxiableUUID()");
422        assert_eq!(&calldata[..4], &expected);
423    }
424
425    #[test]
426    fn test_encode_implementation_selector() {
427        let calldata = encode_implementation();
428        let expected = abi::function_selector("implementation()");
429        assert_eq!(&calldata[..4], &expected);
430    }
431
432    // ─── Transparent Proxy ────────────────────────────────────
433
434    #[test]
435    fn test_encode_change_admin_selector() {
436        let calldata = encode_change_admin([0xCC; 20]);
437        let expected = abi::function_selector("changeAdmin(address)");
438        assert_eq!(&calldata[..4], &expected);
439    }
440
441    #[test]
442    fn test_encode_admin_selector() {
443        let calldata = encode_admin();
444        let expected = abi::function_selector("admin()");
445        assert_eq!(&calldata[..4], &expected);
446    }
447
448    // ─── Beacon Proxy ─────────────────────────────────────────
449
450    #[test]
451    fn test_encode_upgrade_beacon_selector() {
452        let calldata = encode_upgrade_beacon([0xDD; 20]);
453        let expected = abi::function_selector("upgradeTo(address)");
454        assert_eq!(&calldata[..4], &expected);
455    }
456
457    // ─── Initializable ────────────────────────────────────────
458
459    #[test]
460    fn test_encode_initialize_selector() {
461        let calldata = encode_initialize(&[]);
462        let expected = abi::function_selector("initialize()");
463        assert_eq!(&calldata[..4], &expected);
464    }
465
466    #[test]
467    fn test_encode_initializer_custom() {
468        let calldata = encode_initializer(
469            "initialize(address,uint256)",
470            &[AbiValue::Address([0xAA; 20]), AbiValue::from_u64(42)],
471        );
472        let expected = abi::function_selector("initialize(address,uint256)");
473        assert_eq!(&calldata[..4], &expected);
474    }
475
476    // ─── Multicall3 ───────────────────────────────────────────
477
478    #[test]
479    fn test_encode_multicall_selector() {
480        let calls = vec![Multicall3Call {
481            target: [0xAA; 20],
482            allow_failure: false,
483            call_data: vec![0x01],
484        }];
485        let calldata = encode_multicall(&calls);
486        let expected = abi::function_selector("aggregate3((address,bool,bytes)[])");
487        assert_eq!(&calldata[..4], &expected);
488    }
489
490    #[test]
491    fn test_encode_multicall_empty() {
492        let calldata = encode_multicall(&[]);
493        let expected = abi::function_selector("aggregate3((address,bool,bytes)[])");
494        assert_eq!(&calldata[..4], &expected);
495    }
496
497    #[test]
498    fn test_encode_multicall_multiple_calls() {
499        let calls = vec![
500            Multicall3Call {
501                target: [0xAA; 20],
502                allow_failure: false,
503                call_data: vec![0x01, 0x02],
504            },
505            Multicall3Call {
506                target: [0xBB; 20],
507                allow_failure: true,
508                call_data: vec![0x03],
509            },
510        ];
511        let calldata = encode_multicall(&calls);
512        assert!(calldata.len() > 4); // At minimum has selector
513    }
514
515    #[test]
516    fn test_encode_multicall_deterministic() {
517        let calls = vec![Multicall3Call {
518            target: [0xAA; 20],
519            allow_failure: false,
520            call_data: vec![0x01],
521        }];
522        let c1 = encode_multicall(&calls);
523        let c2 = encode_multicall(&calls);
524        assert_eq!(c1, c2);
525    }
526
527    #[test]
528    fn test_encode_multicall_legacy_selector() {
529        let calls = vec![([0xAA; 20], vec![0x01u8])];
530        let calldata = encode_multicall_legacy(&calls);
531        let expected = abi::function_selector("aggregate((address,bytes)[])");
532        assert_eq!(&calldata[..4], &expected);
533    }
534
535    // ─── Multicall3 Address ───────────────────────────────────
536
537    #[test]
538    fn test_multicall3_address() {
539        assert_eq!(
540            hex::encode(MULTICALL3_ADDRESS).to_lowercase(),
541            "ca11bde05977b3631167028862be2a173976ca11"
542        );
543    }
544}