Skip to main content

safe_rs/encoding/
eip712.rs

1//! EIP-712 signing support for Safe transactions
2
3use alloy::primitives::{keccak256, Address, Bytes, B256, U256};
4
5use crate::contracts::{DOMAIN_SEPARATOR_TYPEHASH, SAFE_TX_TYPEHASH};
6use crate::types::Operation;
7
8/// Safe transaction parameters for hashing
9#[derive(Debug, Clone)]
10pub struct SafeTxParams {
11    /// Target address
12    pub to: Address,
13    /// Value to send
14    pub value: U256,
15    /// Calldata
16    pub data: Bytes,
17    /// Operation type
18    pub operation: Operation,
19    /// Gas limit for the Safe transaction
20    pub safe_tx_gas: U256,
21    /// Base gas (overhead)
22    pub base_gas: U256,
23    /// Gas price for refund calculation
24    pub gas_price: U256,
25    /// Token used for gas refund (address(0) for ETH)
26    pub gas_token: Address,
27    /// Address to receive gas refund
28    pub refund_receiver: Address,
29    /// Safe nonce
30    pub nonce: U256,
31}
32
33impl SafeTxParams {
34    /// Creates new SafeTxParams with minimal parameters
35    pub fn new(to: Address, value: U256, data: impl Into<Bytes>, operation: Operation) -> Self {
36        Self {
37            to,
38            value,
39            data: data.into(),
40            operation,
41            safe_tx_gas: U256::ZERO,
42            base_gas: U256::ZERO,
43            gas_price: U256::ZERO,
44            gas_token: Address::ZERO,
45            refund_receiver: Address::ZERO,
46            nonce: U256::ZERO,
47        }
48    }
49
50    /// Sets the safe transaction gas
51    pub fn with_safe_tx_gas(mut self, gas: U256) -> Self {
52        self.safe_tx_gas = gas;
53        self
54    }
55
56    /// Sets the nonce
57    pub fn with_nonce(mut self, nonce: U256) -> Self {
58        self.nonce = nonce;
59        self
60    }
61}
62
63/// Computes the domain separator for a Safe
64///
65/// domain_separator = keccak256(abi.encode(DOMAIN_SEPARATOR_TYPEHASH, chainId, safeAddress))
66pub fn compute_domain_separator(chain_id: u64, safe_address: Address) -> B256 {
67    let mut encoded = Vec::with_capacity(96);
68
69    // DOMAIN_SEPARATOR_TYPEHASH (32 bytes)
70    encoded.extend_from_slice(&DOMAIN_SEPARATOR_TYPEHASH);
71
72    // chainId (32 bytes, left-padded)
73    encoded.extend_from_slice(&U256::from(chain_id).to_be_bytes::<32>());
74
75    // verifyingContract (32 bytes, left-padded address)
76    let mut addr_bytes = [0u8; 32];
77    addr_bytes[12..].copy_from_slice(safe_address.as_slice());
78    encoded.extend_from_slice(&addr_bytes);
79
80    keccak256(&encoded)
81}
82
83/// Computes the struct hash for SafeTx
84///
85/// safeTxHash = keccak256(abi.encode(
86///     SAFE_TX_TYPEHASH,
87///     to, value, keccak256(data), operation,
88///     safeTxGas, baseGas, gasPrice, gasToken, refundReceiver, nonce
89/// ))
90pub fn compute_safe_tx_hash(params: &SafeTxParams) -> B256 {
91    let mut encoded = Vec::with_capacity(384);
92
93    // SAFE_TX_TYPEHASH (32 bytes)
94    encoded.extend_from_slice(&SAFE_TX_TYPEHASH);
95
96    // to (32 bytes, left-padded address)
97    let mut to_bytes = [0u8; 32];
98    to_bytes[12..].copy_from_slice(params.to.as_slice());
99    encoded.extend_from_slice(&to_bytes);
100
101    // value (32 bytes)
102    encoded.extend_from_slice(&params.value.to_be_bytes::<32>());
103
104    // keccak256(data) (32 bytes)
105    encoded.extend_from_slice(keccak256(&params.data).as_slice());
106
107    // operation (32 bytes, left-padded)
108    let mut op_bytes = [0u8; 32];
109    op_bytes[31] = params.operation.as_u8();
110    encoded.extend_from_slice(&op_bytes);
111
112    // safeTxGas (32 bytes)
113    encoded.extend_from_slice(&params.safe_tx_gas.to_be_bytes::<32>());
114
115    // baseGas (32 bytes)
116    encoded.extend_from_slice(&params.base_gas.to_be_bytes::<32>());
117
118    // gasPrice (32 bytes)
119    encoded.extend_from_slice(&params.gas_price.to_be_bytes::<32>());
120
121    // gasToken (32 bytes, left-padded address)
122    let mut gas_token_bytes = [0u8; 32];
123    gas_token_bytes[12..].copy_from_slice(params.gas_token.as_slice());
124    encoded.extend_from_slice(&gas_token_bytes);
125
126    // refundReceiver (32 bytes, left-padded address)
127    let mut refund_bytes = [0u8; 32];
128    refund_bytes[12..].copy_from_slice(params.refund_receiver.as_slice());
129    encoded.extend_from_slice(&refund_bytes);
130
131    // nonce (32 bytes)
132    encoded.extend_from_slice(&params.nonce.to_be_bytes::<32>());
133
134    keccak256(&encoded)
135}
136
137/// Computes the final EIP-712 hash to sign
138///
139/// hash = keccak256("\x19\x01" || domainSeparator || safeTxHash)
140pub fn compute_transaction_hash(domain_separator: B256, safe_tx_hash: B256) -> B256 {
141    let mut encoded = Vec::with_capacity(66);
142
143    // EIP-712 prefix
144    encoded.extend_from_slice(&[0x19, 0x01]);
145
146    // Domain separator
147    encoded.extend_from_slice(domain_separator.as_slice());
148
149    // SafeTx hash
150    encoded.extend_from_slice(safe_tx_hash.as_slice());
151
152    keccak256(&encoded)
153}
154
155/// Computes the complete transaction hash for signing
156pub fn compute_safe_transaction_hash(
157    chain_id: u64,
158    safe_address: Address,
159    params: &SafeTxParams,
160) -> B256 {
161    let domain_separator = compute_domain_separator(chain_id, safe_address);
162    let safe_tx_hash = compute_safe_tx_hash(params);
163    compute_transaction_hash(domain_separator, safe_tx_hash)
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use alloy::primitives::{address, hex};
170
171    #[test]
172    fn test_domain_separator() {
173        // Test against known values
174        let chain_id = 1u64;
175        let safe = address!("0x1234567890123456789012345678901234567890");
176
177        let domain = compute_domain_separator(chain_id, safe);
178
179        // The domain separator should be 32 bytes
180        assert_eq!(domain.len(), 32);
181    }
182
183    #[test]
184    fn test_safe_tx_hash() {
185        let params = SafeTxParams {
186            to: address!("0x1234567890123456789012345678901234567890"),
187            value: U256::from(1000),
188            data: Bytes::from(vec![0x01, 0x02, 0x03]),
189            operation: Operation::Call,
190            safe_tx_gas: U256::from(100000),
191            base_gas: U256::from(21000),
192            gas_price: U256::ZERO,
193            gas_token: Address::ZERO,
194            refund_receiver: Address::ZERO,
195            nonce: U256::from(5),
196        };
197
198        let hash = compute_safe_tx_hash(&params);
199        assert_eq!(hash.len(), 32);
200    }
201
202    #[test]
203    fn test_transaction_hash_prefix() {
204        let domain = B256::ZERO;
205        let safe_tx_hash = B256::ZERO;
206
207        let hash = compute_transaction_hash(domain, safe_tx_hash);
208
209        // The result should be keccak256("\x19\x01" + 64 zero bytes)
210        let expected_input = hex!("1901").iter()
211            .chain([0u8; 64].iter())
212            .copied()
213            .collect::<Vec<u8>>();
214
215        assert_eq!(hash, keccak256(&expected_input));
216    }
217
218    #[test]
219    fn test_complete_hash() {
220        let chain_id = 1u64;
221        let safe = address!("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
222
223        let params = SafeTxParams::new(
224            address!("0x1111111111111111111111111111111111111111"),
225            U256::from(1_000_000_000_000_000_000u64), // 1 ETH
226            vec![],
227            Operation::Call,
228        )
229        .with_nonce(U256::from(0));
230
231        let hash = compute_safe_transaction_hash(chain_id, safe, &params);
232        assert_eq!(hash.len(), 32);
233    }
234}