safe_rs/encoding/
eip712.rs1use alloy::primitives::{keccak256, Address, Bytes, B256, U256};
4
5use crate::contracts::{DOMAIN_SEPARATOR_TYPEHASH, SAFE_TX_TYPEHASH};
6use crate::types::Operation;
7
8#[derive(Debug, Clone)]
10pub struct SafeTxParams {
11 pub to: Address,
13 pub value: U256,
15 pub data: Bytes,
17 pub operation: Operation,
19 pub safe_tx_gas: U256,
21 pub base_gas: U256,
23 pub gas_price: U256,
25 pub gas_token: Address,
27 pub refund_receiver: Address,
29 pub nonce: U256,
31}
32
33impl SafeTxParams {
34 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 pub fn with_safe_tx_gas(mut self, gas: U256) -> Self {
52 self.safe_tx_gas = gas;
53 self
54 }
55
56 pub fn with_nonce(mut self, nonce: U256) -> Self {
58 self.nonce = nonce;
59 self
60 }
61}
62
63pub fn compute_domain_separator(chain_id: u64, safe_address: Address) -> B256 {
67 let mut encoded = Vec::with_capacity(96);
68
69 encoded.extend_from_slice(&DOMAIN_SEPARATOR_TYPEHASH);
71
72 encoded.extend_from_slice(&U256::from(chain_id).to_be_bytes::<32>());
74
75 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
83pub fn compute_safe_tx_hash(params: &SafeTxParams) -> B256 {
91 let mut encoded = Vec::with_capacity(384);
92
93 encoded.extend_from_slice(&SAFE_TX_TYPEHASH);
95
96 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 encoded.extend_from_slice(¶ms.value.to_be_bytes::<32>());
103
104 encoded.extend_from_slice(keccak256(¶ms.data).as_slice());
106
107 let mut op_bytes = [0u8; 32];
109 op_bytes[31] = params.operation.as_u8();
110 encoded.extend_from_slice(&op_bytes);
111
112 encoded.extend_from_slice(¶ms.safe_tx_gas.to_be_bytes::<32>());
114
115 encoded.extend_from_slice(¶ms.base_gas.to_be_bytes::<32>());
117
118 encoded.extend_from_slice(¶ms.gas_price.to_be_bytes::<32>());
120
121 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 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 encoded.extend_from_slice(¶ms.nonce.to_be_bytes::<32>());
133
134 keccak256(&encoded)
135}
136
137pub fn compute_transaction_hash(domain_separator: B256, safe_tx_hash: B256) -> B256 {
141 let mut encoded = Vec::with_capacity(66);
142
143 encoded.extend_from_slice(&[0x19, 0x01]);
145
146 encoded.extend_from_slice(domain_separator.as_slice());
148
149 encoded.extend_from_slice(safe_tx_hash.as_slice());
151
152 keccak256(&encoded)
153}
154
155pub 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 let chain_id = 1u64;
175 let safe = address!("0x1234567890123456789012345678901234567890");
176
177 let domain = compute_domain_separator(chain_id, safe);
178
179 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(¶ms);
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 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), vec![],
227 Operation::Call,
228 )
229 .with_nonce(U256::from(0));
230
231 let hash = compute_safe_transaction_hash(chain_id, safe, ¶ms);
232 assert_eq!(hash.len(), 32);
233 }
234}