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]).unwrap();
33//! ```
34
35use crate::error::SignerError;
36use crate::ethereum::abi::{self, AbiValue};
37
38// ─── Types ─────────────────────────────────────────────────────────
39
40/// Safe operation type.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum Operation {
43    /// Standard call (CALL opcode).
44    Call = 0,
45    /// Delegate call (DELEGATECALL opcode).
46    DelegateCall = 1,
47}
48
49/// A Gnosis Safe transaction for EIP-712 typed signing.
50///
51/// All `u256` fields are stored as 32-byte big-endian arrays to avoid
52/// overflow issues and match the ABI encoding directly.
53#[derive(Debug, Clone)]
54pub struct SafeTransaction {
55    /// Target address of the transaction.
56    pub to: [u8; 20],
57    /// ETH value in wei (32-byte BE `uint256`).
58    pub value: [u8; 32],
59    /// Transaction calldata.
60    pub data: Vec<u8>,
61    /// Call type: `Call` or `DelegateCall`.
62    pub operation: Operation,
63    /// Gas allocated for the Safe execution (after `gasleft()` check).
64    pub safe_tx_gas: [u8; 32],
65    /// Gas costs not related to the Safe execution (signatures, base overhead).
66    pub base_gas: [u8; 32],
67    /// Gas price used for the refund calculation. 0 = no refund.
68    pub gas_price: [u8; 32],
69    /// Token address for gas payment (0x0 = ETH).
70    pub gas_token: [u8; 20],
71    /// Address that receives the gas refund (0x0 = `tx.origin`).
72    pub refund_receiver: [u8; 20],
73    /// Safe nonce for replay protection.
74    pub nonce: [u8; 32],
75}
76
77impl SafeTransaction {
78    /// The Safe's `SAFE_TX_TYPEHASH`.
79    ///
80    /// `keccak256("SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)")`
81    #[must_use]
82    pub fn type_hash() -> [u8; 32] {
83        keccak256(
84            b"SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)",
85        )
86    }
87
88    /// Compute the EIP-712 struct hash for this transaction.
89    ///
90    /// `keccak256(abi.encode(SAFE_TX_TYPEHASH, to, value, keccak256(data), operation, safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce))`
91    #[must_use]
92    pub fn struct_hash(&self) -> [u8; 32] {
93        let data_hash = keccak256(&self.data);
94
95        let mut buf = Vec::with_capacity(11 * 32);
96        buf.extend_from_slice(&Self::type_hash());
97        buf.extend_from_slice(&pad_address(&self.to));
98        buf.extend_from_slice(&self.value);
99        buf.extend_from_slice(&data_hash);
100        buf.extend_from_slice(&pad_u8(self.operation as u8));
101        buf.extend_from_slice(&self.safe_tx_gas);
102        buf.extend_from_slice(&self.base_gas);
103        buf.extend_from_slice(&self.gas_price);
104        buf.extend_from_slice(&pad_address(&self.gas_token));
105        buf.extend_from_slice(&pad_address(&self.refund_receiver));
106        buf.extend_from_slice(&self.nonce);
107
108        keccak256(&buf)
109    }
110
111    /// Compute the EIP-712 signing hash (`safeTxHash`).
112    ///
113    /// `keccak256("\x19\x01" || domainSeparator || structHash)`
114    #[must_use]
115    pub fn signing_hash(&self, domain_separator: &[u8; 32]) -> [u8; 32] {
116        let mut buf = Vec::with_capacity(2 + 32 + 32);
117        buf.push(0x19);
118        buf.push(0x01);
119        buf.extend_from_slice(domain_separator);
120        buf.extend_from_slice(&self.struct_hash());
121        keccak256(&buf)
122    }
123
124    /// Sign this Safe transaction using EIP-712.
125    ///
126    /// Returns an `EthereumSignature` that can be packed with `encode_signatures`.
127    pub fn sign(
128        &self,
129        signer: &super::EthereumSigner,
130        domain_separator: &[u8; 32],
131    ) -> Result<super::EthereumSignature, SignerError> {
132        let hash = self.signing_hash(domain_separator);
133        signer.sign_digest(&hash)
134    }
135
136    /// ABI-encode the `execTransaction(...)` calldata.
137    ///
138    /// This produces the full calldata to call `execTransaction` on the Safe contract,
139    /// ready for use in a transaction's `data` field.
140    pub fn encode_exec_transaction(
141        &self,
142        signatures: &[super::EthereumSignature],
143    ) -> Result<Vec<u8>, SignerError> {
144        let packed_sigs = encode_signatures(signatures)?;
145        let func = abi::Function::new(
146            "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
147        );
148        Ok(func.encode(&[
149            AbiValue::Address(self.to),
150            AbiValue::Uint256(self.value),
151            AbiValue::Bytes(self.data.clone()),
152            AbiValue::Uint256(pad_u8(self.operation as u8)),
153            AbiValue::Uint256(self.safe_tx_gas),
154            AbiValue::Uint256(self.base_gas),
155            AbiValue::Uint256(self.gas_price),
156            AbiValue::Address(self.gas_token),
157            AbiValue::Address(self.refund_receiver),
158            AbiValue::Bytes(packed_sigs),
159        ]))
160    }
161}
162
163// ─── Domain Separator ──────────────────────────────────────────────
164
165/// Compute the Safe's EIP-712 domain separator.
166///
167/// `keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, chainId, safeAddress))`
168///
169/// Uses the Safe v1.3+ domain typehash:
170/// `keccak256("EIP712Domain(uint256 chainId,address verifyingContract)")`
171#[must_use]
172pub fn safe_domain_separator(chain_id: u64, safe_address: &[u8; 20]) -> [u8; 32] {
173    let domain_type_hash = 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).
188pub fn encode_signatures(signatures: &[super::EthereumSignature]) -> Result<Vec<u8>, SignerError> {
189    let mut packed = Vec::with_capacity(signatures.len() * 65);
190    for sig in signatures {
191        let v = safe_signature_v_byte(sig.v)?;
192        packed.extend_from_slice(&sig.r);
193        packed.extend_from_slice(&sig.s);
194        packed.push(v);
195    }
196    Ok(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 mut sigs = Vec::with_capacity(data.len() / 65);
211    for chunk in data.chunks_exact(65) {
212        let mut r = [0u8; 32];
213        let mut s = [0u8; 32];
214        r.copy_from_slice(&chunk[..32]);
215        s.copy_from_slice(&chunk[32..64]);
216        let v = u64::from(chunk[64]);
217        sigs.push(super::EthereumSignature { r, s, v });
218    }
219    Ok(sigs)
220}
221
222// ─── Owner Management ──────────────────────────────────────────────
223
224/// ABI-encode `addOwnerWithThreshold(address owner, uint256 threshold)`.
225#[must_use]
226pub fn encode_add_owner(owner: [u8; 20], threshold: u64) -> Vec<u8> {
227    let func = abi::Function::new("addOwnerWithThreshold(address,uint256)");
228    func.encode(&[AbiValue::Address(owner), AbiValue::from_u64(threshold)])
229}
230
231/// ABI-encode `removeOwner(address prevOwner, address owner, uint256 threshold)`.
232///
233/// `prevOwner` is the owner that points to `owner` in the linked list.
234/// Use `SENTINEL_OWNERS` (0x1) if `owner` is the first in the list.
235#[must_use]
236pub fn encode_remove_owner(prev_owner: [u8; 20], owner: [u8; 20], threshold: u64) -> Vec<u8> {
237    let func = abi::Function::new("removeOwner(address,address,uint256)");
238    func.encode(&[
239        AbiValue::Address(prev_owner),
240        AbiValue::Address(owner),
241        AbiValue::from_u64(threshold),
242    ])
243}
244
245/// ABI-encode `changeThreshold(uint256 threshold)`.
246#[must_use]
247pub fn encode_change_threshold(threshold: u64) -> Vec<u8> {
248    let func = abi::Function::new("changeThreshold(uint256)");
249    func.encode(&[AbiValue::from_u64(threshold)])
250}
251
252/// ABI-encode `swapOwner(address prevOwner, address oldOwner, address newOwner)`.
253#[must_use]
254pub fn encode_swap_owner(
255    prev_owner: [u8; 20],
256    old_owner: [u8; 20],
257    new_owner: [u8; 20],
258) -> Vec<u8> {
259    let func = abi::Function::new("swapOwner(address,address,address)");
260    func.encode(&[
261        AbiValue::Address(prev_owner),
262        AbiValue::Address(old_owner),
263        AbiValue::Address(new_owner),
264    ])
265}
266
267/// ABI-encode `enableModule(address module)`.
268#[must_use]
269pub fn encode_enable_module(module: [u8; 20]) -> Vec<u8> {
270    let func = abi::Function::new("enableModule(address)");
271    func.encode(&[AbiValue::Address(module)])
272}
273
274/// ABI-encode `disableModule(address prevModule, address module)`.
275#[must_use]
276pub fn encode_disable_module(prev_module: [u8; 20], module: [u8; 20]) -> Vec<u8> {
277    let func = abi::Function::new("disableModule(address,address)");
278    func.encode(&[AbiValue::Address(prev_module), AbiValue::Address(module)])
279}
280
281/// ABI-encode `setGuard(address guard)`.
282#[must_use]
283pub fn encode_set_guard(guard: [u8; 20]) -> Vec<u8> {
284    let func = abi::Function::new("setGuard(address)");
285    func.encode(&[AbiValue::Address(guard)])
286}
287
288/// The sentinel address used in Safe's linked list (0x0000...0001).
289pub const SENTINEL_OWNERS: [u8; 20] = {
290    let mut a = [0u8; 20];
291    a[19] = 1;
292    a
293};
294
295/// ABI-encode `getOwners()` calldata.
296#[must_use]
297pub fn encode_get_owners() -> Vec<u8> {
298    let func = abi::Function::new("getOwners()");
299    func.encode(&[])
300}
301
302/// ABI-encode `getThreshold()` calldata.
303#[must_use]
304pub fn encode_get_threshold() -> Vec<u8> {
305    let func = abi::Function::new("getThreshold()");
306    func.encode(&[])
307}
308
309/// ABI-encode `nonce()` calldata.
310#[must_use]
311pub fn encode_nonce() -> Vec<u8> {
312    let func = abi::Function::new("nonce()");
313    func.encode(&[])
314}
315
316/// ABI-encode `getTransactionHash(...)` calldata for on-chain hash computation.
317#[must_use]
318pub fn encode_get_transaction_hash(tx: &SafeTransaction) -> Vec<u8> {
319    let func = abi::Function::new(
320        "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)",
321    );
322    func.encode(&[
323        AbiValue::Address(tx.to),
324        AbiValue::Uint256(tx.value),
325        AbiValue::Bytes(tx.data.clone()),
326        AbiValue::Uint256(pad_u8(tx.operation as u8)),
327        AbiValue::Uint256(tx.safe_tx_gas),
328        AbiValue::Uint256(tx.base_gas),
329        AbiValue::Uint256(tx.gas_price),
330        AbiValue::Address(tx.gas_token),
331        AbiValue::Address(tx.refund_receiver),
332        AbiValue::Uint256(tx.nonce),
333    ])
334}
335
336// ─── Sorted Signature Packing ──────────────────────────────────────
337
338/// A signer-signature pair for auto-sorting.
339///
340/// The Safe contract requires signatures sorted by signer address (ascending).
341/// Use [`sign_and_sort`] to automatically handle this.
342#[derive(Debug, Clone)]
343pub struct SignerSignature {
344    /// The signer's 20-byte Ethereum address.
345    pub signer: [u8; 20],
346    /// The ECDSA signature.
347    pub signature: super::EthereumSignature,
348}
349
350/// Sign a Safe transaction with multiple signers and auto-sort by address.
351///
352/// This is the recommended high-level API for multi-owner signing.
353/// Signatures are returned sorted by signer address (ascending), ready
354/// for `encode_exec_transaction`.
355///
356/// # Example
357/// ```no_run
358/// use chains_sdk::ethereum::safe::*;
359/// use chains_sdk::ethereum::EthereumSigner;
360/// use chains_sdk::traits::KeyPair;
361///
362/// let owner1 = EthereumSigner::generate().unwrap();
363/// let owner2 = EthereumSigner::generate().unwrap();
364/// let domain = safe_domain_separator(1, &[0xAA; 20]);
365/// let tx = SafeTransaction {
366///     to: [0xBB; 20], value: [0u8; 32], data: vec![],
367///     operation: Operation::Call,
368///     safe_tx_gas: [0u8; 32], base_gas: [0u8; 32], gas_price: [0u8; 32],
369///     gas_token: [0u8; 20], refund_receiver: [0u8; 20], nonce: [0u8; 32],
370/// };
371/// let sorted_sigs = sign_and_sort(&tx, &[&owner1, &owner2], &domain).unwrap();
372/// let calldata = tx.encode_exec_transaction(&sorted_sigs).unwrap();
373/// ```
374pub fn sign_and_sort(
375    tx: &SafeTransaction,
376    signers: &[&super::EthereumSigner],
377    domain_separator: &[u8; 32],
378) -> Result<Vec<super::EthereumSignature>, SignerError> {
379    let mut pairs: Vec<SignerSignature> = Vec::with_capacity(signers.len());
380    for signer in signers {
381        let sig = tx.sign(signer, domain_separator)?;
382        pairs.push(SignerSignature {
383            signer: signer.address(),
384            signature: sig,
385        });
386    }
387    // Sort by signer address (ascending) — required by Safe contract
388    pairs.sort_by(|a, b| a.signer.cmp(&b.signer));
389    Ok(pairs.into_iter().map(|p| p.signature).collect())
390}
391
392/// Encode signatures auto-sorted by recovering their addresses from the hash.
393///
394/// This is the safest approach — it recovers each signer address via `ecrecover`
395/// and sorts automatically. Requires the `safeTxHash` to recover addresses.
396pub fn encode_signatures_sorted(
397    signatures: &[super::EthereumSignature],
398    safe_tx_hash: &[u8; 32],
399) -> Result<Vec<u8>, SignerError> {
400    let mut pairs: Vec<([u8; 20], &super::EthereumSignature)> =
401        Vec::with_capacity(signatures.len());
402    for sig in signatures {
403        let addr = super::ecrecover_digest(safe_tx_hash, sig)?;
404        pairs.push((addr, sig));
405    }
406    // Sort by recovered address (ascending)
407    pairs.sort_by(|a, b| a.0.cmp(&b.0));
408
409    let mut packed = Vec::with_capacity(pairs.len() * 65);
410    for (_, sig) in &pairs {
411        let v = safe_signature_v_byte(sig.v)?;
412        packed.extend_from_slice(&sig.r);
413        packed.extend_from_slice(&sig.s);
414        packed.push(v);
415    }
416    Ok(packed)
417}
418
419fn safe_signature_v_byte(v: u64) -> Result<u8, SignerError> {
420    u8::try_from(v).map_err(|_| {
421        SignerError::InvalidSignature(format!(
422            "safe signature v value {v} does not fit in one byte"
423        ))
424    })
425}
426
427// ─── On-Chain Approval (approveHash) ───────────────────────────────
428
429/// ABI-encode `approveHash(bytes32 hashToApprove)`.
430///
431/// Alternative to off-chain signing: an owner sends a transaction to the Safe
432/// calling `approveHash` to register their approval on-chain.
433/// Then `execTransaction` is called with a pre-validated signature type (v=1).
434#[must_use]
435pub fn encode_approve_hash(hash: &[u8; 32]) -> Vec<u8> {
436    let func = abi::Function::new("approveHash(bytes32)");
437    func.encode(&[AbiValue::Uint256(*hash)])
438}
439
440/// Create a pre-validated signature for an owner who approved the hash on-chain.
441///
442/// In Safe's signature format, `v=1` means "approval-based":
443/// - `r` = owner address (left-padded to 32 bytes)
444/// - `s` = zero
445/// - `v` = 1
446pub fn pre_validated_signature(owner: [u8; 20]) -> super::EthereumSignature {
447    super::EthereumSignature {
448        r: pad_address(&owner),
449        s: [0u8; 32],
450        v: 1,
451    }
452}
453
454/// ABI-encode `approvedHashes(address owner, bytes32 hash)`.
455///
456/// Query whether an owner has approved a specific hash.
457/// Returns uint256(1) if approved, 0 if not.
458#[must_use]
459pub fn encode_approved_hashes(owner: [u8; 20], hash: &[u8; 32]) -> Vec<u8> {
460    let func = abi::Function::new("approvedHashes(address,bytes32)");
461    func.encode(&[AbiValue::Address(owner), AbiValue::Uint256(*hash)])
462}
463
464// ─── Contract Signature (EIP-1271) ─────────────────────────────────
465
466/// Create a contract signature for a smart-contract owner (EIP-1271).
467///
468/// In Safe's signature format, `v=0` means "contract signature":
469/// - `r` = contract address (left-padded to 32 bytes)
470/// - `s` = offset into the signatures where the contract sig data starts
471/// - `v` = 0
472///
473/// The actual contract signature data is appended after the fixed-size
474/// signature block.
475pub fn contract_signature(contract_owner: [u8; 20], data_offset: u32) -> super::EthereumSignature {
476    let mut s = [0u8; 32];
477    s[28..32].copy_from_slice(&data_offset.to_be_bytes());
478    super::EthereumSignature {
479        r: pad_address(&contract_owner),
480        s,
481        v: 0,
482    }
483}
484
485// ─── Module Execution ──────────────────────────────────────────────
486
487/// ABI-encode `execTransactionFromModule(address to, uint256 value, bytes data, uint8 operation)`.
488///
489/// Called by an enabled module to execute a transaction without signatures.
490#[must_use]
491pub fn encode_exec_from_module(
492    to: [u8; 20],
493    value: &[u8; 32],
494    data: &[u8],
495    operation: Operation,
496) -> Vec<u8> {
497    let func = abi::Function::new("execTransactionFromModule(address,uint256,bytes,uint8)");
498    func.encode(&[
499        AbiValue::Address(to),
500        AbiValue::Uint256(*value),
501        AbiValue::Bytes(data.to_vec()),
502        AbiValue::Uint256(pad_u8(operation as u8)),
503    ])
504}
505
506/// ABI-encode `execTransactionFromModuleReturnData(...)`.
507///
508/// Same as `execTransactionFromModule` but returns `(bool success, bytes returnData)`.
509#[must_use]
510pub fn encode_exec_from_module_return_data(
511    to: [u8; 20],
512    value: &[u8; 32],
513    data: &[u8],
514    operation: Operation,
515) -> Vec<u8> {
516    let func =
517        abi::Function::new("execTransactionFromModuleReturnData(address,uint256,bytes,uint8)");
518    func.encode(&[
519        AbiValue::Address(to),
520        AbiValue::Uint256(*value),
521        AbiValue::Bytes(data.to_vec()),
522        AbiValue::Uint256(pad_u8(operation as u8)),
523    ])
524}
525
526// ─── Additional Queries ────────────────────────────────────────────
527
528/// ABI-encode `isOwner(address owner)` calldata.
529#[must_use]
530pub fn encode_is_owner(owner: [u8; 20]) -> Vec<u8> {
531    let func = abi::Function::new("isOwner(address)");
532    func.encode(&[AbiValue::Address(owner)])
533}
534
535/// ABI-encode `domainSeparator()` calldata.
536///
537/// Query the Safe's on-chain domain separator (useful when chain_id is unknown).
538#[must_use]
539pub fn encode_domain_separator() -> Vec<u8> {
540    let func = abi::Function::new("domainSeparator()");
541    func.encode(&[])
542}
543
544/// ABI-encode `isModuleEnabled(address module)` calldata.
545#[must_use]
546pub fn encode_is_module_enabled(module: [u8; 20]) -> Vec<u8> {
547    let func = abi::Function::new("isModuleEnabled(address)");
548    func.encode(&[AbiValue::Address(module)])
549}
550
551/// ABI-encode `getModulesPaginated(address start, uint256 pageSize)` calldata.
552#[must_use]
553pub fn encode_get_modules_paginated(start: [u8; 20], page_size: u64) -> Vec<u8> {
554    let func = abi::Function::new("getModulesPaginated(address,uint256)");
555    func.encode(&[AbiValue::Address(start), AbiValue::from_u64(page_size)])
556}
557
558// ─── Safe Deployment ───────────────────────────────────────────────
559
560/// ABI-encode `setup(address[] calldata _owners, uint256 _threshold, ...)`.
561///
562/// The initializer called when deploying a new Safe proxy.
563#[allow(clippy::too_many_arguments)]
564pub fn encode_setup(
565    owners: &[[u8; 20]],
566    threshold: u64,
567    to: [u8; 20],
568    data: &[u8],
569    fallback_handler: [u8; 20],
570    payment_token: [u8; 20],
571    payment: u128,
572    payment_receiver: [u8; 20],
573) -> Vec<u8> {
574    let func = abi::Function::new(
575        "setup(address[],uint256,address,bytes,address,address,uint256,address)",
576    );
577    let owner_values: Vec<AbiValue> = owners.iter().map(|o| AbiValue::Address(*o)).collect();
578    func.encode(&[
579        AbiValue::Array(owner_values),
580        AbiValue::from_u64(threshold),
581        AbiValue::Address(to),
582        AbiValue::Bytes(data.to_vec()),
583        AbiValue::Address(fallback_handler),
584        AbiValue::Address(payment_token),
585        AbiValue::from_u128(payment),
586        AbiValue::Address(payment_receiver),
587    ])
588}
589
590/// ABI-encode `createProxyWithNonce(address singleton, bytes initializer, uint256 saltNonce)`.
591///
592/// Deploy a new Safe via the ProxyFactory.
593#[must_use]
594pub fn encode_create_proxy_with_nonce(
595    singleton: [u8; 20],
596    initializer: &[u8],
597    salt_nonce: u64,
598) -> Vec<u8> {
599    let func = abi::Function::new("createProxyWithNonce(address,bytes,uint256)");
600    func.encode(&[
601        AbiValue::Address(singleton),
602        AbiValue::Bytes(initializer.to_vec()),
603        AbiValue::from_u64(salt_nonce),
604    ])
605}
606
607// ─── Internal Helpers ──────────────────────────────────────────────
608
609fn keccak256(data: &[u8]) -> [u8; 32] {
610    super::keccak256(data)
611}
612
613fn pad_address(addr: &[u8; 20]) -> [u8; 32] {
614    let mut buf = [0u8; 32];
615    buf[12..32].copy_from_slice(addr);
616    buf
617}
618
619fn pad_u8(val: u8) -> [u8; 32] {
620    let mut buf = [0u8; 32];
621    buf[31] = val;
622    buf
623}
624
625fn pad_u64(val: u64) -> [u8; 32] {
626    let mut buf = [0u8; 32];
627    buf[24..32].copy_from_slice(&val.to_be_bytes());
628    buf
629}
630
631// ─── Tests ─────────────────────────────────────────────────────────
632
633#[cfg(test)]
634#[allow(clippy::unwrap_used, clippy::expect_used)]
635mod tests {
636    use super::*;
637    use crate::traits::KeyPair;
638
639    fn zero_tx() -> SafeTransaction {
640        SafeTransaction {
641            to: [0xBB; 20],
642            value: [0u8; 32],
643            data: vec![],
644            operation: Operation::Call,
645            safe_tx_gas: [0u8; 32],
646            base_gas: [0u8; 32],
647            gas_price: [0u8; 32],
648            gas_token: [0u8; 20],
649            refund_receiver: [0u8; 20],
650            nonce: [0u8; 32],
651        }
652    }
653
654    // ─── Type Hash ────────────────────────────────────────────
655
656    #[test]
657    fn test_type_hash_matches_safe_contract() {
658        let th = SafeTransaction::type_hash();
659        let expected = keccak256(
660            b"SafeTx(address to,uint256 value,bytes data,uint8 operation,uint256 safeTxGas,uint256 baseGas,uint256 gasPrice,address gasToken,address refundReceiver,uint256 nonce)",
661        );
662        assert_eq!(th, expected);
663    }
664
665    // ─── Struct Hash ──────────────────────────────────────────
666
667    #[test]
668    fn test_struct_hash_deterministic() {
669        let tx = zero_tx();
670        assert_eq!(tx.struct_hash(), tx.struct_hash());
671    }
672
673    #[test]
674    fn test_struct_hash_changes_with_to() {
675        let tx1 = zero_tx();
676        let mut tx2 = zero_tx();
677        tx2.to = [0xCC; 20];
678        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
679    }
680
681    #[test]
682    fn test_struct_hash_changes_with_data() {
683        let tx1 = zero_tx();
684        let mut tx2 = zero_tx();
685        tx2.data = vec![0xDE, 0xAD];
686        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
687    }
688
689    #[test]
690    fn test_struct_hash_changes_with_operation() {
691        let tx1 = zero_tx();
692        let mut tx2 = zero_tx();
693        tx2.operation = Operation::DelegateCall;
694        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
695    }
696
697    #[test]
698    fn test_struct_hash_changes_with_nonce() {
699        let tx1 = zero_tx();
700        let mut tx2 = zero_tx();
701        tx2.nonce[31] = 1;
702        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
703    }
704
705    #[test]
706    fn test_struct_hash_changes_with_value() {
707        let tx1 = zero_tx();
708        let mut tx2 = zero_tx();
709        tx2.value[31] = 1;
710        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
711    }
712
713    #[test]
714    fn test_struct_hash_changes_with_gas_fields() {
715        let tx1 = zero_tx();
716        let mut tx2 = zero_tx();
717        tx2.safe_tx_gas[31] = 100;
718        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
719
720        let mut tx3 = zero_tx();
721        tx3.base_gas[31] = 50;
722        assert_ne!(tx1.struct_hash(), tx3.struct_hash());
723
724        let mut tx4 = zero_tx();
725        tx4.gas_price[31] = 10;
726        assert_ne!(tx1.struct_hash(), tx4.struct_hash());
727    }
728
729    #[test]
730    fn test_struct_hash_changes_with_gas_token() {
731        let tx1 = zero_tx();
732        let mut tx2 = zero_tx();
733        tx2.gas_token = [0xFF; 20];
734        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
735    }
736
737    #[test]
738    fn test_struct_hash_changes_with_refund_receiver() {
739        let tx1 = zero_tx();
740        let mut tx2 = zero_tx();
741        tx2.refund_receiver = [0xFF; 20];
742        assert_ne!(tx1.struct_hash(), tx2.struct_hash());
743    }
744
745    // ─── Domain Separator ─────────────────────────────────────
746
747    #[test]
748    fn test_domain_separator_deterministic() {
749        let ds1 = safe_domain_separator(1, &[0xAA; 20]);
750        let ds2 = safe_domain_separator(1, &[0xAA; 20]);
751        assert_eq!(ds1, ds2);
752    }
753
754    #[test]
755    fn test_domain_separator_changes_with_chain_id() {
756        let ds1 = safe_domain_separator(1, &[0xAA; 20]);
757        let ds2 = safe_domain_separator(137, &[0xAA; 20]);
758        assert_ne!(ds1, ds2);
759    }
760
761    #[test]
762    fn test_domain_separator_changes_with_address() {
763        let ds1 = safe_domain_separator(1, &[0xAA; 20]);
764        let ds2 = safe_domain_separator(1, &[0xBB; 20]);
765        assert_ne!(ds1, ds2);
766    }
767
768    // ─── Signing Hash ─────────────────────────────────────────
769
770    #[test]
771    fn test_signing_hash_deterministic() {
772        let tx = zero_tx();
773        let domain = safe_domain_separator(1, &[0xAA; 20]);
774        assert_eq!(tx.signing_hash(&domain), tx.signing_hash(&domain));
775    }
776
777    #[test]
778    fn test_signing_hash_changes_with_domain() {
779        let tx = zero_tx();
780        let d1 = safe_domain_separator(1, &[0xAA; 20]);
781        let d2 = safe_domain_separator(5, &[0xAA; 20]);
782        assert_ne!(tx.signing_hash(&d1), tx.signing_hash(&d2));
783    }
784
785    // ─── Sign ─────────────────────────────────────────────────
786
787    #[test]
788    fn test_sign_produces_valid_signature() {
789        let signer = super::super::EthereumSigner::generate().unwrap();
790        let tx = zero_tx();
791        let domain = safe_domain_separator(1, &[0xAA; 20]);
792        let sig = tx.sign(&signer, &domain).unwrap();
793        assert!(sig.v == 27 || sig.v == 28);
794        assert_ne!(sig.r, [0u8; 32]);
795        assert_ne!(sig.s, [0u8; 32]);
796    }
797
798    #[test]
799    fn test_sign_recovers_correct_address() {
800        let signer = super::super::EthereumSigner::generate().unwrap();
801        let tx = zero_tx();
802        let domain = safe_domain_separator(1, &[0xAA; 20]);
803        let sig = tx.sign(&signer, &domain).unwrap();
804        let hash = tx.signing_hash(&domain);
805        let recovered = super::super::ecrecover_digest(&hash, &sig).unwrap();
806        assert_eq!(recovered, signer.address());
807    }
808
809    // ─── sign_and_sort ────────────────────────────────────────
810
811    #[test]
812    fn test_sign_and_sort_signatures_ordered_by_address() {
813        let s1 = super::super::EthereumSigner::generate().unwrap();
814        let s2 = super::super::EthereumSigner::generate().unwrap();
815        let s3 = super::super::EthereumSigner::generate().unwrap();
816        let tx = zero_tx();
817        let domain = safe_domain_separator(1, &[0xAA; 20]);
818        let sigs = sign_and_sort(&tx, &[&s1, &s2, &s3], &domain).unwrap();
819        assert_eq!(sigs.len(), 3);
820
821        // Verify addresses are sorted
822        let hash = tx.signing_hash(&domain);
823        let a1 = super::super::ecrecover_digest(&hash, &sigs[0]).unwrap();
824        let a2 = super::super::ecrecover_digest(&hash, &sigs[1]).unwrap();
825        let a3 = super::super::ecrecover_digest(&hash, &sigs[2]).unwrap();
826        assert!(a1 < a2);
827        assert!(a2 < a3);
828    }
829
830    #[test]
831    fn test_sign_and_sort_single_signer() {
832        let s1 = super::super::EthereumSigner::generate().unwrap();
833        let tx = zero_tx();
834        let domain = safe_domain_separator(1, &[0xAA; 20]);
835        let sigs = sign_and_sort(&tx, &[&s1], &domain).unwrap();
836        assert_eq!(sigs.len(), 1);
837    }
838
839    // ─── encode_signatures_sorted ─────────────────────────────
840
841    #[test]
842    fn test_encode_signatures_sorted_ecrecover() {
843        let s1 = super::super::EthereumSigner::generate().unwrap();
844        let s2 = super::super::EthereumSigner::generate().unwrap();
845        let tx = zero_tx();
846        let domain = safe_domain_separator(1, &[0xAA; 20]);
847        let hash = tx.signing_hash(&domain);
848        let sig1 = tx.sign(&s1, &domain).unwrap();
849        let sig2 = tx.sign(&s2, &domain).unwrap();
850
851        let packed = encode_signatures_sorted(&[sig1, sig2], &hash).unwrap();
852        assert_eq!(packed.len(), 130);
853
854        // Decode and verify sorted
855        let decoded = decode_signatures(&packed).unwrap();
856        let a1 = super::super::ecrecover_digest(&hash, &decoded[0]).unwrap();
857        let a2 = super::super::ecrecover_digest(&hash, &decoded[1]).unwrap();
858        assert!(a1 < a2);
859    }
860
861    // ─── Signature Packing ────────────────────────────────────
862
863    #[test]
864    fn test_encode_signatures_empty() {
865        let packed = encode_signatures(&[]).unwrap();
866        assert!(packed.is_empty());
867    }
868
869    #[test]
870    fn test_encode_signatures_single() {
871        let sig = super::super::EthereumSignature {
872            r: [0xAA; 32],
873            s: [0xBB; 32],
874            v: 27,
875        };
876        let packed = encode_signatures(&[sig]).unwrap();
877        assert_eq!(packed.len(), 65);
878        assert_eq!(&packed[..32], &[0xAA; 32]);
879        assert_eq!(&packed[32..64], &[0xBB; 32]);
880        assert_eq!(packed[64], 27);
881    }
882
883    #[test]
884    fn test_encode_signatures_multiple() {
885        let sig1 = super::super::EthereumSignature {
886            r: [0x11; 32],
887            s: [0x22; 32],
888            v: 27,
889        };
890        let sig2 = super::super::EthereumSignature {
891            r: [0x33; 32],
892            s: [0x44; 32],
893            v: 28,
894        };
895        let packed = encode_signatures(&[sig1, sig2]).unwrap();
896        assert_eq!(packed.len(), 130);
897        assert_eq!(packed[64], 27);
898        assert_eq!(packed[129], 28);
899    }
900
901    #[test]
902    fn test_encode_signatures_rejects_large_v() {
903        let sig = super::super::EthereumSignature {
904            r: [0xAA; 32],
905            s: [0xBB; 32],
906            v: 1_000,
907        };
908        assert!(encode_signatures(&[sig]).is_err());
909    }
910
911    // ─── Signature Decoding ───────────────────────────────────
912
913    #[test]
914    fn test_decode_signatures_roundtrip() {
915        let sig1 = super::super::EthereumSignature {
916            r: [0xAA; 32],
917            s: [0xBB; 32],
918            v: 27,
919        };
920        let sig2 = super::super::EthereumSignature {
921            r: [0xCC; 32],
922            s: [0xDD; 32],
923            v: 28,
924        };
925        let packed = encode_signatures(&[sig1.clone(), sig2.clone()]).unwrap();
926        let decoded = decode_signatures(&packed).unwrap();
927        assert_eq!(decoded.len(), 2);
928        assert_eq!(decoded[0], sig1);
929        assert_eq!(decoded[1], sig2);
930    }
931
932    #[test]
933    fn test_decode_signatures_empty() {
934        let decoded = decode_signatures(&[]).unwrap();
935        assert!(decoded.is_empty());
936    }
937
938    #[test]
939    fn test_decode_signatures_invalid_length() {
940        assert!(decode_signatures(&[0u8; 64]).is_err());
941        assert!(decode_signatures(&[0u8; 66]).is_err());
942    }
943
944    // ─── execTransaction Encoding ─────────────────────────────
945
946    #[test]
947    fn test_exec_transaction_has_correct_selector() {
948        let tx = zero_tx();
949        let sig = super::super::EthereumSignature {
950            r: [0xAA; 32],
951            s: [0xBB; 32],
952            v: 27,
953        };
954        let calldata = tx.encode_exec_transaction(&[sig]).unwrap();
955        let expected_selector = abi::function_selector(
956            "execTransaction(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,bytes)",
957        );
958        assert_eq!(&calldata[..4], &expected_selector);
959    }
960
961    #[test]
962    fn test_exec_transaction_includes_signature_data() {
963        let tx = zero_tx();
964        let sig = super::super::EthereumSignature {
965            r: [0xAA; 32],
966            s: [0xBB; 32],
967            v: 27,
968        };
969        let calldata = tx.encode_exec_transaction(&[sig]).unwrap();
970        assert!(calldata.len() > 4 + 10 * 32);
971    }
972
973    // ─── approveHash ──────────────────────────────────────────
974
975    #[test]
976    fn test_encode_approve_hash_selector() {
977        let calldata = encode_approve_hash(&[0xAA; 32]);
978        let expected = abi::function_selector("approveHash(bytes32)");
979        assert_eq!(&calldata[..4], &expected);
980        assert_eq!(calldata.len(), 4 + 32);
981    }
982
983    #[test]
984    fn test_encode_approved_hashes_selector() {
985        let calldata = encode_approved_hashes([0xBB; 20], &[0xAA; 32]);
986        let expected = abi::function_selector("approvedHashes(address,bytes32)");
987        assert_eq!(&calldata[..4], &expected);
988        assert_eq!(calldata.len(), 4 + 2 * 32);
989    }
990
991    #[test]
992    fn test_pre_validated_signature() {
993        let owner = [0xAA; 20];
994        let sig = pre_validated_signature(owner);
995        assert_eq!(sig.v, 1);
996        assert_eq!(&sig.r[12..32], &owner);
997        assert_eq!(&sig.r[..12], &[0u8; 12]);
998        assert_eq!(sig.s, [0u8; 32]);
999    }
1000
1001    // ─── Contract Signature ───────────────────────────────────
1002
1003    #[test]
1004    fn test_contract_signature() {
1005        let contract = [0xCC; 20];
1006        let sig = contract_signature(contract, 130);
1007        assert_eq!(sig.v, 0);
1008        assert_eq!(&sig.r[12..32], &contract);
1009        assert_eq!(sig.s[28..32], 130u32.to_be_bytes());
1010    }
1011
1012    // ─── Module Execution ─────────────────────────────────────
1013
1014    #[test]
1015    fn test_encode_exec_from_module_selector() {
1016        let calldata =
1017            encode_exec_from_module([0xBB; 20], &[0u8; 32], &[0xDE, 0xAD], Operation::Call);
1018        let expected =
1019            abi::function_selector("execTransactionFromModule(address,uint256,bytes,uint8)");
1020        assert_eq!(&calldata[..4], &expected);
1021    }
1022
1023    #[test]
1024    fn test_encode_exec_from_module_delegate_call() {
1025        let calldata =
1026            encode_exec_from_module([0xBB; 20], &[0u8; 32], &[], Operation::DelegateCall);
1027        assert!(calldata.len() > 4);
1028    }
1029
1030    #[test]
1031    fn test_encode_exec_from_module_return_data_selector() {
1032        let calldata =
1033            encode_exec_from_module_return_data([0xBB; 20], &[0u8; 32], &[], Operation::Call);
1034        let expected = abi::function_selector(
1035            "execTransactionFromModuleReturnData(address,uint256,bytes,uint8)",
1036        );
1037        assert_eq!(&calldata[..4], &expected);
1038    }
1039
1040    // ─── Owner Management Helpers ─────────────────────────────
1041
1042    #[test]
1043    fn test_encode_add_owner_selector() {
1044        let calldata = encode_add_owner([0xAA; 20], 2);
1045        let expected = abi::function_selector("addOwnerWithThreshold(address,uint256)");
1046        assert_eq!(&calldata[..4], &expected);
1047        assert_eq!(calldata.len(), 4 + 2 * 32);
1048    }
1049
1050    #[test]
1051    fn test_encode_remove_owner_selector() {
1052        let calldata = encode_remove_owner(SENTINEL_OWNERS, [0xAA; 20], 1);
1053        let expected = abi::function_selector("removeOwner(address,address,uint256)");
1054        assert_eq!(&calldata[..4], &expected);
1055        assert_eq!(calldata.len(), 4 + 3 * 32);
1056    }
1057
1058    #[test]
1059    fn test_encode_change_threshold_selector() {
1060        let calldata = encode_change_threshold(3);
1061        let expected = abi::function_selector("changeThreshold(uint256)");
1062        assert_eq!(&calldata[..4], &expected);
1063        assert_eq!(calldata.len(), 4 + 32);
1064    }
1065
1066    #[test]
1067    fn test_encode_swap_owner_selector() {
1068        let calldata = encode_swap_owner(SENTINEL_OWNERS, [0xAA; 20], [0xBB; 20]);
1069        let expected = abi::function_selector("swapOwner(address,address,address)");
1070        assert_eq!(&calldata[..4], &expected);
1071        assert_eq!(calldata.len(), 4 + 3 * 32);
1072    }
1073
1074    #[test]
1075    fn test_encode_enable_module_selector() {
1076        let calldata = encode_enable_module([0xAA; 20]);
1077        let expected = abi::function_selector("enableModule(address)");
1078        assert_eq!(&calldata[..4], &expected);
1079    }
1080
1081    #[test]
1082    fn test_encode_disable_module_selector() {
1083        let calldata = encode_disable_module(SENTINEL_OWNERS, [0xAA; 20]);
1084        let expected = abi::function_selector("disableModule(address,address)");
1085        assert_eq!(&calldata[..4], &expected);
1086    }
1087
1088    #[test]
1089    fn test_encode_set_guard_selector() {
1090        let calldata = encode_set_guard([0xAA; 20]);
1091        let expected = abi::function_selector("setGuard(address)");
1092        assert_eq!(&calldata[..4], &expected);
1093    }
1094
1095    // ─── Query Helpers ────────────────────────────────────────
1096
1097    #[test]
1098    fn test_encode_get_owners_selector() {
1099        let calldata = encode_get_owners();
1100        let expected = abi::function_selector("getOwners()");
1101        assert_eq!(&calldata[..4], &expected);
1102    }
1103
1104    #[test]
1105    fn test_encode_get_threshold_selector() {
1106        let calldata = encode_get_threshold();
1107        let expected = abi::function_selector("getThreshold()");
1108        assert_eq!(&calldata[..4], &expected);
1109    }
1110
1111    #[test]
1112    fn test_encode_nonce_selector() {
1113        let calldata = encode_nonce();
1114        let expected = abi::function_selector("nonce()");
1115        assert_eq!(&calldata[..4], &expected);
1116    }
1117
1118    #[test]
1119    fn test_encode_get_transaction_hash_selector() {
1120        let tx = zero_tx();
1121        let calldata = encode_get_transaction_hash(&tx);
1122        let expected = abi::function_selector(
1123            "getTransactionHash(address,uint256,bytes,uint8,uint256,uint256,uint256,address,address,uint256)",
1124        );
1125        assert_eq!(&calldata[..4], &expected);
1126    }
1127
1128    #[test]
1129    fn test_encode_is_owner_selector() {
1130        let calldata = encode_is_owner([0xAA; 20]);
1131        let expected = abi::function_selector("isOwner(address)");
1132        assert_eq!(&calldata[..4], &expected);
1133    }
1134
1135    #[test]
1136    fn test_encode_domain_separator_selector() {
1137        let calldata = encode_domain_separator();
1138        let expected = abi::function_selector("domainSeparator()");
1139        assert_eq!(&calldata[..4], &expected);
1140    }
1141
1142    #[test]
1143    fn test_encode_is_module_enabled_selector() {
1144        let calldata = encode_is_module_enabled([0xAA; 20]);
1145        let expected = abi::function_selector("isModuleEnabled(address)");
1146        assert_eq!(&calldata[..4], &expected);
1147    }
1148
1149    #[test]
1150    fn test_encode_get_modules_paginated_selector() {
1151        let calldata = encode_get_modules_paginated(SENTINEL_OWNERS, 10);
1152        let expected = abi::function_selector("getModulesPaginated(address,uint256)");
1153        assert_eq!(&calldata[..4], &expected);
1154    }
1155
1156    // ─── Deployment ───────────────────────────────────────────
1157
1158    #[test]
1159    fn test_encode_setup_selector() {
1160        let calldata = encode_setup(
1161            &[[0xAA; 20], [0xBB; 20]],
1162            2,
1163            [0u8; 20],
1164            &[],
1165            [0xCC; 20],
1166            [0u8; 20],
1167            0,
1168            [0u8; 20],
1169        );
1170        let expected = abi::function_selector(
1171            "setup(address[],uint256,address,bytes,address,address,uint256,address)",
1172        );
1173        assert_eq!(&calldata[..4], &expected);
1174    }
1175
1176    #[test]
1177    fn test_encode_create_proxy_with_nonce_selector() {
1178        let calldata = encode_create_proxy_with_nonce([0xAA; 20], &[0x01, 0x02], 42);
1179        let expected = abi::function_selector("createProxyWithNonce(address,bytes,uint256)");
1180        assert_eq!(&calldata[..4], &expected);
1181    }
1182
1183    // ─── Sentinel ─────────────────────────────────────────────
1184
1185    #[test]
1186    fn test_sentinel_owners() {
1187        assert_eq!(SENTINEL_OWNERS[19], 1);
1188        assert_eq!(SENTINEL_OWNERS[..19], [0u8; 19]);
1189    }
1190
1191    // ─── Operation Enum ───────────────────────────────────────
1192
1193    #[test]
1194    fn test_operation_values() {
1195        assert_eq!(Operation::Call as u8, 0);
1196        assert_eq!(Operation::DelegateCall as u8, 1);
1197    }
1198
1199    #[test]
1200    fn test_operation_eq() {
1201        assert_eq!(Operation::Call, Operation::Call);
1202        assert_ne!(Operation::Call, Operation::DelegateCall);
1203    }
1204
1205    // ─── Internal Helpers ─────────────────────────────────────
1206
1207    #[test]
1208    fn test_pad_address() {
1209        let addr = [0xAA; 20];
1210        let padded = pad_address(&addr);
1211        assert_eq!(&padded[..12], &[0u8; 12]);
1212        assert_eq!(&padded[12..], &[0xAA; 20]);
1213    }
1214
1215    #[test]
1216    fn test_pad_u8() {
1217        let padded = pad_u8(42);
1218        assert_eq!(&padded[..31], &[0u8; 31]);
1219        assert_eq!(padded[31], 42);
1220    }
1221
1222    #[test]
1223    fn test_pad_u64() {
1224        let padded = pad_u64(256);
1225        assert_eq!(&padded[..24], &[0u8; 24]);
1226        assert_eq!(&padded[24..], &256u64.to_be_bytes());
1227    }
1228
1229    // ─── Delegate Call ────────────────────────────────────────
1230
1231    #[test]
1232    fn test_delegate_call_transaction() {
1233        let mut tx = zero_tx();
1234        tx.operation = Operation::DelegateCall;
1235        tx.data = vec![0xDE, 0xAD, 0xBE, 0xEF];
1236        let domain = safe_domain_separator(1, &[0xAA; 20]);
1237        let hash = tx.signing_hash(&domain);
1238        assert_ne!(hash, [0u8; 32]);
1239    }
1240
1241    // ─── Full Multi-Owner Flow ────────────────────────────────
1242
1243    #[test]
1244    fn test_full_2_of_3_signing_flow() {
1245        // 3 owners, 2-of-3 threshold
1246        let o1 = super::super::EthereumSigner::generate().unwrap();
1247        let _o2 = super::super::EthereumSigner::generate().unwrap();
1248        let o3 = super::super::EthereumSigner::generate().unwrap();
1249
1250        let domain = safe_domain_separator(1, &[0xAA; 20]);
1251        let tx = zero_tx();
1252
1253        // Only 2 sign
1254        let sorted = sign_and_sort(&tx, &[&o1, &o3], &domain).unwrap();
1255        assert_eq!(sorted.len(), 2);
1256
1257        // Build exec calldata
1258        let calldata = tx.encode_exec_transaction(&sorted).unwrap();
1259        assert!(calldata.len() > 4 + 10 * 32 + 2 * 65);
1260    }
1261
1262    // ─── Mixed Signature Types ────────────────────────────────
1263
1264    #[test]
1265    fn test_mixed_ecdsa_and_prevalidated() {
1266        let signer = super::super::EthereumSigner::generate().unwrap();
1267        let tx = zero_tx();
1268        let domain = safe_domain_separator(1, &[0xAA; 20]);
1269        let ecdsa_sig = tx.sign(&signer, &domain).unwrap();
1270        let pre_sig = pre_validated_signature([0x01; 20]);
1271
1272        let packed = encode_signatures(&[pre_sig, ecdsa_sig]).unwrap();
1273        assert_eq!(packed.len(), 2 * 65);
1274
1275        // First sig should be v=1 (pre-validated)
1276        assert_eq!(packed[64], 1);
1277    }
1278}