Skip to main content

chains_sdk/ethereum/
safe.rs

1//! **Gnosis Safe (Safe)** multisig transaction encoding, signing, and management.
2//!
3//! Provides typed structs and helpers for interacting with Safe smart contracts:
4//! - EIP-712 typed transaction signing (`safeTxHash`)
5//! - `execTransaction` calldata encoding
6//! - Multi-signature packing in Safe's `r‖s‖v` format
7//! - Owner management: `addOwnerWithThreshold`, `removeOwner`, `changeThreshold`
8//!
9//! # Example
10//! ```no_run
11//! use chains_sdk::ethereum::safe::{SafeTransaction, Operation, safe_domain_separator};
12//! use chains_sdk::ethereum::EthereumSigner;
13//! use chains_sdk::traits::KeyPair;
14//!
15//! let signer = EthereumSigner::generate().unwrap();
16//! let domain = safe_domain_separator(1, &[0xAA; 20]);
17//!
18//! let tx = SafeTransaction {
19//!     to: [0xBB; 20],
20//!     value: [0u8; 32],
21//!     data: vec![],
22//!     operation: Operation::Call,
23//!     safe_tx_gas: [0u8; 32],
24//!     base_gas: [0u8; 32],
25//!     gas_price: [0u8; 32],
26//!     gas_token: [0u8; 20],
27//!     refund_receiver: [0u8; 20],
28//!     nonce: [0u8; 32],
29//! };
30//!
31//! let sig = tx.sign(&signer, &domain).unwrap();
32//! let calldata = tx.encode_exec_transaction(&[sig]);
33//! ```
34
35use crate::error::SignerError;
36use crate::ethereum::abi::{self, AbiValue};
37use sha3::{Digest, Keccak256};
38
39// ─── Types ─────────────────────────────────────────────────────────
40
41/// Safe operation type.
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub enum Operation {
44    /// Standard call (CALL opcode).
45    Call = 0,
46    /// Delegate call (DELEGATECALL opcode).
47    DelegateCall = 1,
48}
49
50/// A Gnosis Safe transaction for EIP-712 typed signing.
51///
52/// All `u256` fields are stored as 32-byte big-endian arrays to avoid
53/// overflow issues and match the ABI encoding directly.
54#[derive(Debug, Clone)]
55pub struct SafeTransaction {
56    /// Target address of the transaction.
57    pub to: [u8; 20],
58    /// ETH value in wei (32-byte BE `uint256`).
59    pub value: [u8; 32],
60    /// Transaction calldata.
61    pub data: Vec<u8>,
62    /// Call type: `Call` or `DelegateCall`.
63    pub operation: Operation,
64    /// Gas allocated for the Safe execution (after `gasleft()` check).
65    pub safe_tx_gas: [u8; 32],
66    /// Gas costs not related to the Safe execution (signatures, base overhead).
67    pub base_gas: [u8; 32],
68    /// Gas price used for the refund calculation. 0 = no refund.
69    pub gas_price: [u8; 32],
70    /// Token address for gas payment (0x0 = ETH).
71    pub gas_token: [u8; 20],
72    /// Address that receives the gas refund (0x0 = `tx.origin`).
73    pub refund_receiver: [u8; 20],
74    /// Safe nonce for replay protection.
75    pub nonce: [u8; 32],
76}
77
78impl SafeTransaction {
79    /// The Safe's `SAFE_TX_TYPEHASH`.
80    ///
81    /// `keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)")`
82    #[must_use]
83    pub fn type_hash() -> [u8; 32] {
84        keccak256(
85            b"SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)",
86        )
87    }
88
89    /// Compute the EIP-712 struct hash for this transaction.
90    ///
91    /// `keccak256(abi.encode(SAFE_TX_TYPEHASH, to, value, keccak256(data), operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce))`
92    #[must_use]
93    pub fn struct_hash(&self) -> [u8; 32] {
94        let data_hash = keccak256(&self.data);
95
96        let mut buf = Vec::with_capacity(11 * 32);
97        buf.extend_from_slice(&Self::type_hash());
98        buf.extend_from_slice(&pad_address(&self.to));
99        buf.extend_from_slice(&self.value);
100        buf.extend_from_slice(&data_hash);
101        buf.extend_from_slice(&pad_u8(self.operation as u8));
102        buf.extend_from_slice(&self.safe_tx_gas);
103        buf.extend_from_slice(&self.base_gas);
104        buf.extend_from_slice(&self.gas_price);
105        buf.extend_from_slice(&pad_address(&self.gas_token));
106        buf.extend_from_slice(&pad_address(&self.refund_receiver));
107        buf.extend_from_slice(&self.nonce);
108
109        keccak256(&buf)
110    }
111
112    /// Compute the EIP-712 signing hash (`safeTxHash`).
113    ///
114    /// `keccak256("\x19\x01" || domainSeparator || structHash)`
115    #[must_use]
116    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
117        let mut buf = Vec::with_capacity(2 + 32 + 32);
118        buf.push(0x19);
119        buf.push(0x01);
120        buf.extend_from_slice(domain_separator);
121        buf.extend_from_slice(&self.struct_hash());
122        keccak256(&buf)
123    }
124
125    /// Sign this Safe transaction using EIP-712.
126    ///
127    /// Returns an `EthereumSignature` that can be packed with `encode_signatures`.
128    pub fn sign(
129        &self,
130        signer: &super::EthereumSigner,
131        domain_separator: &[u8; 32],
132    ) -> Result<super::EthereumSignature, SignerError> {
133        let hash = self.signing_hash(domain_separator);
134        signer.sign_digest(&hash)
135    }
136
137    /// ABI-encode the `execTransaction(...)` calldata.
138    ///
139    /// This produces the full calldata to call `execTransaction` on the Safe contract,
140    /// ready for use in a transaction's `data` field.
141    #[must_use]
142    pub fn encode_exec_transaction(&self, signatures: &[super::EthereumSignature]) -> Vec<u8> {
143        let packed_sigs = encode_signatures(signatures);
144        let func = abi::Function::new(
145            "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
146        );
147        func.encode(&[
148            AbiValue::Address(self.to),
149            AbiValue::Uint256(self.value),
150            AbiValue::Bytes(self.data.clone()),
151            AbiValue::Uint256(pad_u8(self.operation as u8)),
152            AbiValue::Uint256(self.safe_tx_gas),
153            AbiValue::Uint256(self.base_gas),
154            AbiValue::Uint256(self.gas_price),
155            AbiValue::Address(self.gas_token),
156            AbiValue::Address(self.refund_receiver),
157            AbiValue::Bytes(packed_sigs),
158        ])
159    }
160}
161
162// ─── Domain Separator ──────────────────────────────────────────────
163
164/// Compute the Safe's EIP-712 domain separator.
165///
166/// `keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, chainId, safeAddress))`
167///
168/// Uses the Safe v1.3+ domain typehash:
169/// `keccak256("EIP712Domain(uint256 chainId,address verifyingContract)")`
170#[must_use]
171pub fn safe_domain_separator(chain_id: u64, safe_address: &[u8; 20]) -> [u8; 32] {
172    let domain_type_hash =
173        keccak256(b"EIP712Domain(uint256 chainId,address verifyingContract)");
174    let mut buf = Vec::with_capacity(3 * 32);
175    buf.extend_from_slice(&domain_type_hash);
176    buf.extend_from_slice(&pad_u64(chain_id));
177    buf.extend_from_slice(&pad_address(safe_address));
178    keccak256(&buf)
179}
180
181// ─── Signature Packing ─────────────────────────────────────────────
182
183/// Pack multiple ECDSA signatures into Safe's format.
184///
185/// Each signature is encoded as `r (32 bytes) || s (32 bytes) || v (1 byte)`.
186/// Signatures must be sorted by signer address (ascending) — this function
187/// does NOT sort them (the caller is responsible for ordering).
188#[must_use]
189pub fn encode_signatures(signatures: &[super::EthereumSignature]) -> Vec<u8> {
190    let mut packed = Vec::with_capacity(signatures.len() * 65);
191    for sig in signatures {
192        packed.extend_from_slice(&sig.r);
193        packed.extend_from_slice(&sig.s);
194        packed.push(sig.v as u8);
195    }
196    packed
197}
198
199/// Decode packed Safe signatures back into individual signatures.
200///
201/// # Errors
202/// Returns an error if the data length is not a multiple of 65.
203pub fn decode_signatures(data: &[u8]) -> Result<Vec<super::EthereumSignature>, SignerError> {
204    if data.len() % 65 != 0 {
205        return Err(SignerError::EncodingError(format!(
206            "signature data length {} is not a multiple of 65",
207            data.len()
208        )));
209    }
210    let count = data.len() / 65;
211    let mut sigs = Vec::with_capacity(count);
212    for i in 0..count {
213        let offset = i * 65;
214        let mut r = [0u8; 32];
215        let mut s = [0u8; 32];
216        r.copy_from_slice(&data[offset..offset + 32]);
217        s.copy_from_slice(&data[offset + 32..offset + 64]);
218        let v = u64::from(data[offset + 64]);
219        sigs.push(super::EthereumSignature { r, s, v });
220    }
221    Ok(sigs)
222}
223
224// ─── Owner Management ──────────────────────────────────────────────
225
226/// ABI-encode `addOwnerWithThreshold(address owner, uint256 threshold)`.
227#[must_use]
228pub fn encode_add_owner(owner: [u8; 20], threshold: u64) -> Vec<u8> {
229    let func = abi::Function::new("addOwnerWithThreshold(address,uint256)");
230    func.encode(&[
231        AbiValue::Address(owner),
232        AbiValue::from_u64(threshold),
233    ])
234}
235
236/// ABI-encode `removeOwner(address prevOwner, address owner, uint256 threshold)`.
237///
238/// `prevOwner` is the owner that points to `owner` in the linked list.
239/// Use `SENTINEL_OWNERS` (0x1) if `owner` is the first in the list.
240#[must_use]
241pub fn encode_remove_owner(prev_owner: [u8; 20], owner: [u8; 20], threshold: u64) -> Vec<u8> {
242    let func = abi::Function::new("removeOwner(address,address,uint256)");
243    func.encode(&[
244        AbiValue::Address(prev_owner),
245        AbiValue::Address(owner),
246        AbiValue::from_u64(threshold),
247    ])
248}
249
250/// ABI-encode `changeThreshold(uint256 threshold)`.
251#[must_use]
252pub fn encode_change_threshold(threshold: u64) -> Vec<u8> {
253    let func = abi::Function::new("changeThreshold(uint256)");
254    func.encode(&[AbiValue::from_u64(threshold)])
255}
256
257/// ABI-encode `swapOwner(address prevOwner, address oldOwner, address newOwner)`.
258#[must_use]
259pub fn encode_swap_owner(prev_owner: [u8; 20], old_owner: [u8; 20], new_owner: [u8; 20]) -> Vec<u8> {
260    let func = abi::Function::new("swapOwner(address,address,address)");
261    func.encode(&[
262        AbiValue::Address(prev_owner),
263        AbiValue::Address(old_owner),
264        AbiValue::Address(new_owner),
265    ])
266}
267
268/// ABI-encode `enableModule(address module)`.
269#[must_use]
270pub fn encode_enable_module(module: [u8; 20]) -> Vec<u8> {
271    let func = abi::Function::new("enableModule(address)");
272    func.encode(&[AbiValue::Address(module)])
273}
274
275/// ABI-encode `disableModule(address prevModule, address module)`.
276#[must_use]
277pub fn encode_disable_module(prev_module: [u8; 20], module: [u8; 20]) -> Vec<u8> {
278    let func = abi::Function::new("disableModule(address,address)");
279    func.encode(&[
280        AbiValue::Address(prev_module),
281        AbiValue::Address(module),
282    ])
283}
284
285/// ABI-encode `setGuard(address guard)`.
286#[must_use]
287pub fn encode_set_guard(guard: [u8; 20]) -> Vec<u8> {
288    let func = abi::Function::new("setGuard(address)");
289    func.encode(&[AbiValue::Address(guard)])
290}
291
292/// The sentinel address used in Safe's linked list (0x0000...0001).
293pub const SENTINEL_OWNERS: [u8; 20] = {
294    let mut a = [0u8; 20];
295    a[19] = 1;
296    a
297};
298
299/// ABI-encode `getOwners()` calldata.
300#[must_use]
301pub fn encode_get_owners() -> Vec<u8> {
302    let func = abi::Function::new("getOwners()");
303    func.encode(&[])
304}
305
306/// ABI-encode `getThreshold()` calldata.
307#[must_use]
308pub fn encode_get_threshold() -> Vec<u8> {
309    let func = abi::Function::new("getThreshold()");
310    func.encode(&[])
311}
312
313/// ABI-encode `nonce()` calldata.
314#[must_use]
315pub fn encode_nonce() -> Vec<u8> {
316    let func = abi::Function::new("nonce()");
317    func.encode(&[])
318}
319
320/// ABI-encode `getTransactionHash(...)` calldata for on-chain hash computation.
321#[must_use]
322pub fn encode_get_transaction_hash(tx: &SafeTransaction) -> Vec<u8> {
323    let func = abi::Function::new(
324        "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)",
325    );
326    func.encode(&[
327        AbiValue::Address(tx.to),
328        AbiValue::Uint256(tx.value),
329        AbiValue::Bytes(tx.data.clone()),
330        AbiValue::Uint256(pad_u8(tx.operation as u8)),
331        AbiValue::Uint256(tx.safe_tx_gas),
332        AbiValue::Uint256(tx.base_gas),
333        AbiValue::Uint256(tx.gas_price),
334        AbiValue::Address(tx.gas_token),
335        AbiValue::Address(tx.refund_receiver),
336        AbiValue::Uint256(tx.nonce),
337    ])
338}
339
340// ─── Internal Helpers ──────────────────────────────────────────────
341
342fn keccak256(data: &[u8]) -> [u8; 32] {
343    let mut hasher = Keccak256::new();
344    hasher.update(data);
345    hasher.finalize().into()
346}
347
348fn pad_address(addr: &[u8; 20]) -> [u8; 32] {
349    let mut buf = [0u8; 32];
350    buf[12..32].copy_from_slice(addr);
351    buf
352}
353
354fn pad_u8(val: u8) -> [u8; 32] {
355    let mut buf = [0u8; 32];
356    buf[31] = val;
357    buf
358}
359
360fn pad_u64(val: u64) -> [u8; 32] {
361    let mut buf = [0u8; 32];
362    buf[24..32].copy_from_slice(&val.to_be_bytes());
363    buf
364}
365
366// ─── Tests ─────────────────────────────────────────────────────────
367
368#[cfg(test)]
369#[allow(clippy::unwrap_used, clippy::expect_used)]
370mod tests {
371    use super::*;
372    use crate::traits::KeyPair;
373
374    fn zero_tx() -> SafeTransaction {
375        SafeTransaction {
376            to: [0xBB; 20],
377            value: [0u8; 32],
378            data: vec![],
379            operation: Operation::Call,
380            safe_tx_gas: [0u8; 32],
381            base_gas: [0u8; 32],
382            gas_price: [0u8; 32],
383            gas_token: [0u8; 20],
384            refund_receiver: [0u8; 20],
385            nonce: [0u8; 32],
386        }
387    }
388
389    // ─── Type Hash ────────────────────────────────────────────
390
391    #[test]
392    fn test_type_hash_matches_safe_contract() {
393        let th = SafeTransaction::type_hash();
394        // Known Safe v1.3 SAFE_TX_TYPEHASH
395        let expected = keccak256(
396            b"SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)",
397        );
398        assert_eq!(th, expected);
399    }
400
401    // ─── Struct Hash ──────────────────────────────────────────
402
403    #[test]
404    fn test_struct_hash_deterministic() {
405        let tx = zero_tx();
406        assert_eq!(tx.struct_hash(), tx.struct_hash());
407    }
408
409    #[test]
410    fn test_struct_hash_changes_with_to() {
411        let tx1 = zero_tx();
412        let mut tx2 = zero_tx();
413        tx2.to = [0xCC; 20];
414        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
415    }
416
417    #[test]
418    fn test_struct_hash_changes_with_data() {
419        let tx1 = zero_tx();
420        let mut tx2 = zero_tx();
421        tx2.data = vec![0xDE, 0xAD];
422        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
423    }
424
425    #[test]
426    fn test_struct_hash_changes_with_operation() {
427        let tx1 = zero_tx();
428        let mut tx2 = zero_tx();
429        tx2.operation = Operation::DelegateCall;
430        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
431    }
432
433    #[test]
434    fn test_struct_hash_changes_with_nonce() {
435        let tx1 = zero_tx();
436        let mut tx2 = zero_tx();
437        tx2.nonce[31] = 1;
438        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
439    }
440
441    #[test]
442    fn test_struct_hash_changes_with_value() {
443        let tx1 = zero_tx();
444        let mut tx2 = zero_tx();
445        tx2.value[31] = 1;
446        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
447    }
448
449    #[test]
450    fn test_struct_hash_changes_with_gas_fields() {
451        let tx1 = zero_tx();
452        let mut tx2 = zero_tx();
453        tx2.safe_tx_gas[31] = 100;
454        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
455
456        let mut tx3 = zero_tx();
457        tx3.base_gas[31] = 50;
458        assert_ne!(tx1.struct_hash(), tx3.struct_hash());
459
460        let mut tx4 = zero_tx();
461        tx4.gas_price[31] = 10;
462        assert_ne!(tx1.struct_hash(), tx4.struct_hash());
463    }
464
465    #[test]
466    fn test_struct_hash_changes_with_gas_token() {
467        let tx1 = zero_tx();
468        let mut tx2 = zero_tx();
469        tx2.gas_token = [0xFF; 20];
470        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
471    }
472
473    #[test]
474    fn test_struct_hash_changes_with_refund_receiver() {
475        let tx1 = zero_tx();
476        let mut tx2 = zero_tx();
477        tx2.refund_receiver = [0xFF; 20];
478        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
479    }
480
481    // ─── Domain Separator ─────────────────────────────────────
482
483    #[test]
484    fn test_domain_separator_deterministic() {
485        let ds1 = safe_domain_separator(1, &[0xAA; 20]);
486        let ds2 = safe_domain_separator(1, &[0xAA; 20]);
487        assert_eq!(ds1, ds2);
488    }
489
490    #[test]
491    fn test_domain_separator_changes_with_chain_id() {
492        let ds1 = safe_domain_separator(1, &[0xAA; 20]);
493        let ds2 = safe_domain_separator(137, &[0xAA; 20]);
494        assert_ne!(ds1, ds2);
495    }
496
497    #[test]
498    fn test_domain_separator_changes_with_address() {
499        let ds1 = safe_domain_separator(1, &[0xAA; 20]);
500        let ds2 = safe_domain_separator(1, &[0xBB; 20]);
501        assert_ne!(ds1, ds2);
502    }
503
504    // ─── Signing Hash ─────────────────────────────────────────
505
506    #[test]
507    fn test_signing_hash_starts_with_eip712_prefix() {
508        let tx = zero_tx();
509        let domain = safe_domain_separator(1, &[0xAA; 20]);
510        // The signing hash is keccak256("\x19\x01" || domain || struct_hash)
511        // We can verify it's deterministic
512        let h1 = tx.signing_hash(&domain);
513        let h2 = tx.signing_hash(&domain);
514        assert_eq!(h1, h2);
515    }
516
517    #[test]
518    fn test_signing_hash_changes_with_domain() {
519        let tx = zero_tx();
520        let d1 = safe_domain_separator(1, &[0xAA; 20]);
521        let d2 = safe_domain_separator(5, &[0xAA; 20]);
522        assert_ne!(tx.signing_hash(&d1), tx.signing_hash(&d2));
523    }
524
525    // ─── Sign ─────────────────────────────────────────────────
526
527    #[test]
528    fn test_sign_produces_valid_signature() {
529        let signer = super::super::EthereumSigner::generate().unwrap();
530        let tx = zero_tx();
531        let domain = safe_domain_separator(1, &[0xAA; 20]);
532        let sig = tx.sign(&signer, &domain).unwrap();
533        // v should be 27 or 28
534        assert!(sig.v == 27 || sig.v == 28);
535        assert_ne!(sig.r, [0u8; 32]);
536        assert_ne!(sig.s, [0u8; 32]);
537    }
538
539    #[test]
540    fn test_sign_recovers_correct_address() {
541        let signer = super::super::EthereumSigner::generate().unwrap();
542        let tx = zero_tx();
543        let domain = safe_domain_separator(1, &[0xAA; 20]);
544        let sig = tx.sign(&signer, &domain).unwrap();
545        let hash = tx.signing_hash(&domain);
546        let recovered = super::super::ecrecover_digest(&hash, &sig).unwrap();
547        assert_eq!(recovered, signer.address());
548    }
549
550    // ─── Signature Packing ────────────────────────────────────
551
552    #[test]
553    fn test_encode_signatures_empty() {
554        let packed = encode_signatures(&[]);
555        assert!(packed.is_empty());
556    }
557
558    #[test]
559    fn test_encode_signatures_single() {
560        let sig = super::super::EthereumSignature {
561            r: [0xAA; 32],
562            s: [0xBB; 32],
563            v: 27,
564        };
565        let packed = encode_signatures(&[sig]);
566        assert_eq!(packed.len(), 65);
567        assert_eq!(&packed[..32], &[0xAA; 32]);
568        assert_eq!(&packed[32..64], &[0xBB; 32]);
569        assert_eq!(packed[64], 27);
570    }
571
572    #[test]
573    fn test_encode_signatures_multiple() {
574        let sig1 = super::super::EthereumSignature {
575            r: [0x11; 32], s: [0x22; 32], v: 27,
576        };
577        let sig2 = super::super::EthereumSignature {
578            r: [0x33; 32], s: [0x44; 32], v: 28,
579        };
580        let packed = encode_signatures(&[sig1, sig2]);
581        assert_eq!(packed.len(), 130);
582        assert_eq!(packed[64], 27);
583        assert_eq!(packed[129], 28);
584    }
585
586    // ─── Signature Decoding ───────────────────────────────────
587
588    #[test]
589    fn test_decode_signatures_roundtrip() {
590        let sig1 = super::super::EthereumSignature {
591            r: [0xAA; 32], s: [0xBB; 32], v: 27,
592        };
593        let sig2 = super::super::EthereumSignature {
594            r: [0xCC; 32], s: [0xDD; 32], v: 28,
595        };
596        let packed = encode_signatures(&[sig1.clone(), sig2.clone()]);
597        let decoded = decode_signatures(&packed).unwrap();
598        assert_eq!(decoded.len(), 2);
599        assert_eq!(decoded[0], sig1);
600        assert_eq!(decoded[1], sig2);
601    }
602
603    #[test]
604    fn test_decode_signatures_empty() {
605        let decoded = decode_signatures(&[]).unwrap();
606        assert!(decoded.is_empty());
607    }
608
609    #[test]
610    fn test_decode_signatures_invalid_length() {
611        assert!(decode_signatures(&[0u8; 64]).is_err());
612        assert!(decode_signatures(&[0u8; 66]).is_err());
613    }
614
615    // ─── execTransaction Encoding ─────────────────────────────
616
617    #[test]
618    fn test_exec_transaction_has_correct_selector() {
619        let tx = zero_tx();
620        let sig = super::super::EthereumSignature {
621            r: [0xAA; 32], s: [0xBB; 32], v: 27,
622        };
623        let calldata = tx.encode_exec_transaction(&[sig]);
624        // execTransaction selector
625        let expected_selector = abi::function_selector(
626            "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
627        );
628        assert_eq!(&calldata[..4], &expected_selector);
629    }
630
631    #[test]
632    fn test_exec_transaction_includes_signature_data() {
633        let tx = zero_tx();
634        let sig = super::super::EthereumSignature {
635            r: [0xAA; 32], s: [0xBB; 32], v: 27,
636        };
637        let calldata = tx.encode_exec_transaction(&[sig]);
638        // The calldata should contain the packed signatures somewhere
639        assert!(calldata.len() > 4 + 10 * 32); // selector + 10 params
640    }
641
642    // ─── Owner Management Helpers ─────────────────────────────
643
644    #[test]
645    fn test_encode_add_owner_selector() {
646        let calldata = encode_add_owner([0xAA; 20], 2);
647        let expected = abi::function_selector("addOwnerWithThreshold(address,uint256)");
648        assert_eq!(&calldata[..4], &expected);
649        assert_eq!(calldata.len(), 4 + 2 * 32);
650    }
651
652    #[test]
653    fn test_encode_remove_owner_selector() {
654        let calldata = encode_remove_owner(SENTINEL_OWNERS, [0xAA; 20], 1);
655        let expected = abi::function_selector("removeOwner(address,address,uint256)");
656        assert_eq!(&calldata[..4], &expected);
657        assert_eq!(calldata.len(), 4 + 3 * 32);
658    }
659
660    #[test]
661    fn test_encode_change_threshold_selector() {
662        let calldata = encode_change_threshold(3);
663        let expected = abi::function_selector("changeThreshold(uint256)");
664        assert_eq!(&calldata[..4], &expected);
665        assert_eq!(calldata.len(), 4 + 32);
666    }
667
668    #[test]
669    fn test_encode_swap_owner_selector() {
670        let calldata = encode_swap_owner(SENTINEL_OWNERS, [0xAA; 20], [0xBB; 20]);
671        let expected = abi::function_selector("swapOwner(address,address,address)");
672        assert_eq!(&calldata[..4], &expected);
673        assert_eq!(calldata.len(), 4 + 3 * 32);
674    }
675
676    #[test]
677    fn test_encode_enable_module_selector() {
678        let calldata = encode_enable_module([0xAA; 20]);
679        let expected = abi::function_selector("enableModule(address)");
680        assert_eq!(&calldata[..4], &expected);
681    }
682
683    #[test]
684    fn test_encode_disable_module_selector() {
685        let calldata = encode_disable_module(SENTINEL_OWNERS, [0xAA; 20]);
686        let expected = abi::function_selector("disableModule(address,address)");
687        assert_eq!(&calldata[..4], &expected);
688    }
689
690    #[test]
691    fn test_encode_set_guard_selector() {
692        let calldata = encode_set_guard([0xAA; 20]);
693        let expected = abi::function_selector("setGuard(address)");
694        assert_eq!(&calldata[..4], &expected);
695    }
696
697    // ─── Query Helpers ────────────────────────────────────────
698
699    #[test]
700    fn test_encode_get_owners_selector() {
701        let calldata = encode_get_owners();
702        let expected = abi::function_selector("getOwners()");
703        assert_eq!(&calldata[..4], &expected);
704    }
705
706    #[test]
707    fn test_encode_get_threshold_selector() {
708        let calldata = encode_get_threshold();
709        let expected = abi::function_selector("getThreshold()");
710        assert_eq!(&calldata[..4], &expected);
711    }
712
713    #[test]
714    fn test_encode_nonce_selector() {
715        let calldata = encode_nonce();
716        let expected = abi::function_selector("nonce()");
717        assert_eq!(&calldata[..4], &expected);
718    }
719
720    #[test]
721    fn test_encode_get_transaction_hash_selector() {
722        let tx = zero_tx();
723        let calldata = encode_get_transaction_hash(&tx);
724        let expected = abi::function_selector(
725            "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)",
726        );
727        assert_eq!(&calldata[..4], &expected);
728    }
729
730    // ─── Sentinel ─────────────────────────────────────────────
731
732    #[test]
733    fn test_sentinel_owners() {
734        assert_eq!(SENTINEL_OWNERS[19], 1);
735        assert_eq!(SENTINEL_OWNERS[..19], [0u8; 19]);
736    }
737
738    // ─── Operation Enum ───────────────────────────────────────
739
740    #[test]
741    fn test_operation_values() {
742        assert_eq!(Operation::Call as u8, 0);
743        assert_eq!(Operation::DelegateCall as u8, 1);
744    }
745
746    #[test]
747    fn test_operation_eq() {
748        assert_eq!(Operation::Call, Operation::Call);
749        assert_ne!(Operation::Call, Operation::DelegateCall);
750    }
751
752    // ─── Internal Helpers ─────────────────────────────────────
753
754    #[test]
755    fn test_pad_address() {
756        let addr = [0xAA; 20];
757        let padded = pad_address(&addr);
758        assert_eq!(&padded[..12], &[0u8; 12]);
759        assert_eq!(&padded[12..], &[0xAA; 20]);
760    }
761
762    #[test]
763    fn test_pad_u8() {
764        let padded = pad_u8(42);
765        assert_eq!(&padded[..31], &[0u8; 31]);
766        assert_eq!(padded[31], 42);
767    }
768
769    #[test]
770    fn test_pad_u64() {
771        let padded = pad_u64(256);
772        assert_eq!(&padded[..24], &[0u8; 24]);
773        assert_eq!(&padded[24..], &256u64.to_be_bytes());
774    }
775
776    // ─── Delegate Call Transaction ────────────────────────────
777
778    #[test]
779    fn test_delegate_call_transaction() {
780        let mut tx = zero_tx();
781        tx.operation = Operation::DelegateCall;
782        tx.data = vec![0xDE, 0xAD, 0xBE, 0xEF];
783        let domain = safe_domain_separator(1, &[0xAA; 20]);
784        let hash = tx.signing_hash(&domain);
785        assert_ne!(hash, [0u8; 32]);
786    }
787}