1use super::contracts::ISafe;
2use alloy_network::Ethereum;
3use alloy_primitives::{Address, B256, Bytes, U256};
4use clap::Args;
5use eyre::{Context, Result, ensure};
6use foundry_common::sh_status;
7use reqwest::{Client, Method, StatusCode, Url, header};
8use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned};
9use serde_json::{Value, json};
10use std::str::FromStr;
11
12const SAFE_SIGNATURE_LENGTH: usize = 65;
13const CONTRACT_SIGNATURE_HEADER_LENGTH: usize = SAFE_SIGNATURE_LENGTH + U256::BYTES;
14const P256_SIGNATURE_DATA_LENGTH: usize = 128;
15const P256_SIGNATURE_LENGTH: usize = SAFE_SIGNATURE_LENGTH + P256_SIGNATURE_DATA_LENGTH;
16
17#[derive(Args, Clone, Debug)]
18pub struct SafeServiceOpts {
19 #[arg(long, env = "SAFE_TRANSACTION_SERVICE_URL")]
22 pub(super) service_url: Option<Url>,
23
24 #[arg(long, env = "SAFE_API_KEY")]
26 api_key: Option<String>,
27}
28
29#[derive(Debug, Deserialize)]
30#[serde(rename_all = "camelCase")]
31pub(super) struct SafeTransaction {
32 pub(super) safe: Address,
33 pub(super) to: Address,
34 #[serde(deserialize_with = "deserialize_number_string")]
35 pub(super) value: String,
36 #[serde(default, deserialize_with = "deserialize_null_default")]
37 pub(super) data: Bytes,
38 pub(super) operation: u8,
39 #[serde(deserialize_with = "deserialize_number_string")]
40 pub(super) safe_tx_gas: String,
41 #[serde(deserialize_with = "deserialize_number_string")]
42 pub(super) base_gas: String,
43 #[serde(deserialize_with = "deserialize_number_string")]
44 pub(super) gas_price: String,
45 #[serde(default, deserialize_with = "deserialize_null_default")]
46 pub(super) gas_token: Address,
47 #[serde(default, deserialize_with = "deserialize_null_default")]
48 pub(super) refund_receiver: Address,
49 #[serde(deserialize_with = "deserialize_number_string")]
50 pub(super) nonce: String,
51 #[serde(alias = "contractTransactionHash")]
52 pub(super) safe_tx_hash: B256,
53 #[serde(default, deserialize_with = "deserialize_null_default")]
54 pub(super) confirmations: Vec<SafeConfirmation>,
55 #[serde(default)]
56 pub(super) is_executed: bool,
57 #[serde(default)]
58 pub(super) transaction_hash: Option<B256>,
59}
60
61#[derive(Debug, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub(super) struct SafeConfirmation {
64 owner: Address,
65 #[serde(default)]
66 signature: Option<Bytes>,
67}
68
69#[derive(Debug, Deserialize, Serialize)]
70#[serde(rename_all = "camelCase")]
71pub(super) struct SafeDelegate {
72 safe: Option<Address>,
73 delegate: Address,
74 delegator: Address,
75 label: String,
76}
77
78#[derive(Debug, Deserialize)]
79pub(super) struct SafeDelegatesResponse {
80 #[serde(default)]
81 pub(super) next: Option<String>,
82 pub(super) results: Vec<SafeDelegate>,
83}
84
85impl SafeTransaction {
86 pub(super) fn number(value: &str, field: &str) -> Result<U256> {
87 U256::from_str(value).wrap_err_with(|| format!("invalid {field} in transaction"))
88 }
89
90 pub(super) async fn calculate_hash<P>(&self, provider: &P) -> Result<B256>
91 where
92 P: alloy_provider::Provider<Ethereum>,
93 {
94 ISafe::new(self.safe, provider)
95 .getTransactionHash(
96 self.to,
97 Self::number(&self.value, "value")?,
98 self.data.clone(),
99 self.operation,
100 Self::number(&self.safe_tx_gas, "safeTxGas")?,
101 Self::number(&self.base_gas, "baseGas")?,
102 Self::number(&self.gas_price, "gasPrice")?,
103 self.gas_token,
104 self.refund_receiver,
105 Self::number(&self.nonce, "nonce")?,
106 )
107 .call()
108 .await
109 .wrap_err("failed to calculate Safe transaction hash")
110 }
111
112 pub(super) async fn verify_hash<P>(&self, expected_safe: Address, provider: &P) -> Result<()>
113 where
114 P: alloy_provider::Provider<Ethereum>,
115 {
116 ensure!(
117 self.safe == expected_safe,
118 "Transaction Service returned Safe {}, expected {expected_safe}",
119 self.safe
120 );
121 ensure!(self.operation <= 1, "invalid Safe operation: {}", self.operation);
122 let calculated = self.calculate_hash(provider).await?;
123 ensure!(
124 calculated == self.safe_tx_hash,
125 "Safe transaction hash mismatch: service/file returned {}, calculated {calculated}",
126 self.safe_tx_hash
127 );
128 Ok(())
129 }
130
131 pub(super) fn proposal_body(
132 &self,
133 sender: Address,
134 signature: String,
135 origin: Option<String>,
136 ) -> Value {
137 let mut body = json!({
138 "to": self.to.to_checksum(None),
139 "value": self.value,
140 "data": self.data,
141 "operation": self.operation,
142 "safeTxGas": self.safe_tx_gas,
143 "baseGas": self.base_gas,
144 "gasPrice": self.gas_price,
145 "gasToken": self.gas_token.to_checksum(None),
146 "refundReceiver": self.refund_receiver.to_checksum(None),
147 "nonce": self.nonce,
148 "contractTransactionHash": self.safe_tx_hash,
149 "sender": sender.to_checksum(None),
150 "signature": signature,
151 });
152 if let Some(origin) = origin {
153 body["origin"] = Value::String(origin);
154 }
155 body
156 }
157
158 pub(super) fn packed_signatures(&self) -> Result<Bytes> {
159 let mut confirmations = self.confirmations.iter().collect::<Vec<_>>();
160 confirmations.sort_unstable_by_key(|confirmation| confirmation.owner);
161 let static_len = confirmations.len() * SAFE_SIGNATURE_LENGTH;
162 let mut signatures = Vec::with_capacity(static_len);
163 let mut dynamic = Vec::new();
164 for confirmation in confirmations {
165 let signature = confirmation.signature.as_ref().ok_or_else(|| {
166 eyre::eyre!("confirmation from {} does not contain a signature", confirmation.owner)
167 })?;
168 ensure!(
169 signature.len() >= SAFE_SIGNATURE_LENGTH,
170 "invalid signature from {}: expected at least {SAFE_SIGNATURE_LENGTH} bytes, got {}",
171 confirmation.owner,
172 signature.len()
173 );
174 match signature[SAFE_SIGNATURE_LENGTH - 1] {
175 0 => {
176 ensure!(
177 signature.len() >= CONTRACT_SIGNATURE_HEADER_LENGTH,
178 "contract signature from {} does not contain a length",
179 confirmation.owner
180 );
181 let offset =
182 U256::from_be_slice(&signature[U256::BYTES..SAFE_SIGNATURE_LENGTH - 1]);
183 ensure!(
184 offset == U256::from(SAFE_SIGNATURE_LENGTH),
185 "invalid contract signature offset from {}: expected {SAFE_SIGNATURE_LENGTH}, got {offset}",
186 confirmation.owner
187 );
188 let data_len = U256::from_be_slice(
189 &signature[SAFE_SIGNATURE_LENGTH..CONTRACT_SIGNATURE_HEADER_LENGTH],
190 );
191 ensure!(
192 data_len == U256::from(signature.len() - CONTRACT_SIGNATURE_HEADER_LENGTH),
193 "invalid contract signature length from {}: expected {}, got {data_len}",
194 confirmation.owner,
195 signature.len() - CONTRACT_SIGNATURE_HEADER_LENGTH
196 );
197
198 signatures.extend_from_slice(&signature[..U256::BYTES]);
199 signatures.extend_from_slice(
200 &U256::from(static_len + dynamic.len()).to_be_bytes::<{ U256::BYTES }>(),
201 );
202 signatures.push(0);
203 dynamic.extend_from_slice(&signature[SAFE_SIGNATURE_LENGTH..]);
204 }
205 1 => {
206 eyre::bail!(
207 "approved-hash signatures (v = 1) are not supported by `cast safe execute`"
208 );
209 }
210 2 => {
211 ensure!(
212 signature.len() == P256_SIGNATURE_LENGTH,
213 "invalid P-256 signature from {}: expected {P256_SIGNATURE_LENGTH} bytes, got {}",
214 confirmation.owner,
215 signature.len()
216 );
217 let offset =
218 U256::from_be_slice(&signature[U256::BYTES..SAFE_SIGNATURE_LENGTH - 1]);
219 ensure!(
220 offset == U256::from(SAFE_SIGNATURE_LENGTH),
221 "invalid P-256 signature offset from {}: expected {SAFE_SIGNATURE_LENGTH}, got {offset}",
222 confirmation.owner
223 );
224
225 signatures.extend_from_slice(&signature[..U256::BYTES]);
226 signatures.extend_from_slice(
227 &U256::from(static_len + dynamic.len()).to_be_bytes::<{ U256::BYTES }>(),
228 );
229 signatures.push(2);
230 dynamic.extend_from_slice(&signature[SAFE_SIGNATURE_LENGTH..]);
231 }
232 _ => {
233 ensure!(
234 signature.len() == SAFE_SIGNATURE_LENGTH,
235 "invalid signature from {}: expected {SAFE_SIGNATURE_LENGTH} bytes, got {}",
236 confirmation.owner,
237 signature.len()
238 );
239 signatures.extend_from_slice(signature);
240 }
241 }
242 }
243 ensure!(!signatures.is_empty(), "Safe transaction has no confirmations");
244 signatures.extend_from_slice(&dynamic);
245 Ok(signatures.into())
246 }
247
248 pub(super) fn show_transaction_summary(&self) -> Result<()> {
249 let operation = if self.operation == 0 { "CALL" } else { "DELEGATECALL" };
250 sh_status!("Safe transaction: {}", self.safe_tx_hash)?;
251 sh_status!(" Safe: {}", self.safe)?;
252 sh_status!(" To: {}", self.to)?;
253 sh_status!(" Value: {}", self.value)?;
254 sh_status!(" Operation: {} ({operation})", self.operation)?;
255 sh_status!(" Safe tx gas: {}", self.safe_tx_gas)?;
256 sh_status!(" Base gas: {}", self.base_gas)?;
257 sh_status!(" Gas price: {}", self.gas_price)?;
258 sh_status!(" Gas token: {}", self.gas_token)?;
259 sh_status!(" Refund receiver: {}", self.refund_receiver)?;
260 sh_status!(" Nonce: {}", self.nonce)?;
261 sh_status!(" Data: {}", self.data)?;
262 Ok(())
263 }
264}
265
266impl SafeServiceOpts {
267 pub(super) fn endpoint(&self, chain_id: u64, path: &str) -> Result<Url> {
268 let mut url = match &self.service_url {
269 Some(url) => url.clone(),
270 None => default_service_url(chain_id)?,
271 };
272 let mut endpoint = url.path().trim_end_matches('/').to_string();
273 if !endpoint.ends_with("/api") {
274 endpoint.push_str("/api");
275 }
276 endpoint.push('/');
277 endpoint.push_str(path.trim_start_matches('/'));
278 url.set_path(&endpoint);
279 url.set_query(None);
280 url.set_fragment(None);
281 Ok(url)
282 }
283
284 pub(super) fn request(&self, method: Method, url: Url) -> reqwest::RequestBuilder {
285 let request = Client::new()
286 .request(method, url)
287 .header(header::ACCEPT, "application/json")
288 .header(header::CONTENT_TYPE, "application/json");
289 if let Some(api_key) = &self.api_key { request.bearer_auth(api_key) } else { request }
290 }
291
292 pub(super) async fn response<T: DeserializeOwned>(
293 &self,
294 request: reqwest::RequestBuilder,
295 ) -> Result<T> {
296 let response = request.send().await.wrap_err("Safe Transaction Service request failed")?;
297 let status = response.status();
298 let text = response.text().await.wrap_err("failed to read Transaction Service response")?;
299 ensure_success(status, &text)?;
300 serde_json::from_str(&text).wrap_err("invalid Transaction Service response")
301 }
302
303 pub(super) async fn empty_response(&self, request: reqwest::RequestBuilder) -> Result<()> {
304 let response = request.send().await.wrap_err("Safe Transaction Service request failed")?;
305 let status = response.status();
306 let text = response.text().await.wrap_err("failed to read Transaction Service response")?;
307 ensure_success(status, &text)
308 }
309
310 pub(super) async fn get_transaction(
311 &self,
312 chain_id: u64,
313 api_version: &str,
314 safe_tx_hash: B256,
315 ) -> Result<SafeTransaction> {
316 let url = self
317 .endpoint(chain_id, &format!("{api_version}/multisig-transactions/{safe_tx_hash}/"))?;
318 let transaction: SafeTransaction = self.response(self.request(Method::GET, url)).await?;
319 ensure!(
320 transaction.safe_tx_hash == safe_tx_hash,
321 "Transaction Service returned a different Safe transaction hash"
322 );
323 Ok(transaction)
324 }
325
326 pub(super) async fn next_nonce(
327 &self,
328 chain_id: u64,
329 safe: Address,
330 onchain_nonce: U256,
331 ) -> Result<U256> {
332 let response: Value = self
333 .response(self.request(Method::GET, self.next_nonce_endpoint(chain_id, safe)?))
334 .await?;
335 let Some(nonce) = response
336 .get("results")
337 .and_then(Value::as_array)
338 .and_then(|results| results.first())
339 .and_then(|tx| tx.get("nonce"))
340 else {
341 return Ok(onchain_nonce);
342 };
343 let nonce = match nonce {
344 Value::String(nonce) => U256::from_str(nonce),
345 Value::Number(nonce) => U256::from_str(&nonce.to_string()),
346 _ => eyre::bail!("invalid nonce returned by Safe Transaction Service"),
347 }
348 .wrap_err("invalid nonce returned by Safe Transaction Service")?;
349 Ok(std::cmp::max(onchain_nonce, nonce.saturating_add(U256::from(1))))
350 }
351
352 fn next_nonce_endpoint(&self, chain_id: u64, safe: Address) -> Result<Url> {
353 let mut url = self.endpoint(
354 chain_id,
355 &format!("v1/safes/{}/multisig-transactions/", safe.to_checksum(None)),
356 )?;
357 url.query_pairs_mut()
358 .append_pair("executed", "false")
359 .append_pair("ordering", "-nonce")
360 .append_pair("limit", "1");
361 Ok(url)
362 }
363}
364
365fn deserialize_number_string<'de, D>(deserializer: D) -> Result<String, D::Error>
366where
367 D: Deserializer<'de>,
368{
369 match Value::deserialize(deserializer)? {
370 Value::String(value) => Ok(value),
371 Value::Number(value) => Ok(value.to_string()),
372 value => Err(serde::de::Error::custom(format!("expected number or string, got {value}"))),
373 }
374}
375
376fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
377where
378 D: Deserializer<'de>,
379 T: Deserialize<'de> + Default,
380{
381 Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
382}
383
384fn ensure_success(status: StatusCode, body: &str) -> Result<()> {
385 ensure!(status.is_success(), "Safe Transaction Service returned {status}: {}", body.trim());
386 Ok(())
387}
388
389fn default_service_url(chain_id: u64) -> Result<Url> {
390 let short_name = match chain_id {
391 1 => "eth",
392 10 => "oeth",
393 56 => "bnb",
394 100 => "gno",
395 130 => "unichain",
396 137 => "pol",
397 146 => "sonic",
398 196 => "okb",
399 232 => "lens",
400 324 => "zksync",
401 480 => "wc",
402 4217 => "tempo",
403 5000 => "mantle",
404 8453 => "base",
405 9745 => "plasma",
406 10200 => "chi",
407 42161 => "arb1",
408 42220 => "celo",
409 42431 => "tempo-moderato",
410 43114 => "avax",
411 43111 => "hemi",
412 57073 => "ink",
413 59144 => "linea",
414 747474 => "katana",
415 80094 => "berachain",
416 84532 => "basesep",
417 534352 => "scr",
418 11155111 => "sep",
419 1313161554 => "aurora",
420 _ => eyre::bail!(
421 "no known Safe Transaction Service for chain ID {chain_id}; pass --service-url"
422 ),
423 };
424 format!("https://api.safe.global/tx-service/{short_name}")
425 .parse()
426 .wrap_err("invalid built-in Safe Transaction Service URL")
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use alloy_primitives::address;
433 use alloy_provider::{ProviderBuilder, mock::Asserter};
434
435 fn transaction() -> SafeTransaction {
436 SafeTransaction {
437 safe: Address::ZERO,
438 to: Address::ZERO,
439 value: "1".to_string(),
440 data: Bytes::new(),
441 operation: 0,
442 safe_tx_gas: "0".to_string(),
443 base_gas: "0".to_string(),
444 gas_price: "0".to_string(),
445 gas_token: Address::ZERO,
446 refund_receiver: Address::ZERO,
447 nonce: "7".to_string(),
448 safe_tx_hash: B256::ZERO,
449 confirmations: Vec::new(),
450 is_executed: false,
451 transaction_hash: None,
452 }
453 }
454
455 fn eoa_signature(byte: u8) -> Bytes {
456 let mut signature = vec![byte; SAFE_SIGNATURE_LENGTH];
457 signature[SAFE_SIGNATURE_LENGTH - 1] = 27;
458 signature.into()
459 }
460
461 fn contract_signature(owner: Address, payload: &[u8]) -> Bytes {
462 let mut signature = Vec::with_capacity(CONTRACT_SIGNATURE_HEADER_LENGTH + payload.len());
463 signature.extend_from_slice(owner.into_word().as_slice());
464 signature.extend_from_slice(&U256::from(SAFE_SIGNATURE_LENGTH).to_be_bytes::<32>());
465 signature.push(0);
466 signature.extend_from_slice(&U256::from(payload.len()).to_be_bytes::<32>());
467 signature.extend_from_slice(payload);
468 signature.into()
469 }
470
471 fn p256_signature(owner: Address, payload: &[u8; 128]) -> Bytes {
472 let mut signature = Vec::with_capacity(SAFE_SIGNATURE_LENGTH + payload.len());
473 signature.extend_from_slice(owner.into_word().as_slice());
474 signature.extend_from_slice(&U256::from(SAFE_SIGNATURE_LENGTH).to_be_bytes::<32>());
475 signature.push(2);
476 signature.extend_from_slice(payload);
477 signature.into()
478 }
479
480 #[test]
481 fn normalizes_transaction_service_urls() {
482 for base in [
483 "https://api.safe.global/tx-service/tempo-moderato/",
484 "https://api.safe.global/tx-service/tempo-moderato/api",
485 ] {
486 let service =
487 SafeServiceOpts { service_url: Some(base.parse().unwrap()), api_key: None };
488 assert_eq!(
489 service.endpoint(42431, "v2/delegates/").unwrap().as_str(),
490 "https://api.safe.global/tx-service/tempo-moderato/api/v2/delegates/"
491 );
492 }
493 }
494
495 #[test]
496 fn infers_tempo_transaction_service_urls() {
497 assert_eq!(
498 default_service_url(4217).unwrap().as_str(),
499 "https://api.safe.global/tx-service/tempo"
500 );
501 assert_eq!(
502 default_service_url(42431).unwrap().as_str(),
503 "https://api.safe.global/tx-service/tempo-moderato"
504 );
505 for chain_id in [1101, 81457, 31337] {
506 assert!(default_service_url(chain_id).is_err());
507 }
508 }
509
510 #[test]
511 fn builds_pending_nonce_query_as_url_parameters() {
512 let service = SafeServiceOpts { service_url: None, api_key: None };
513 assert_eq!(
514 service.next_nonce_endpoint(42431, Address::ZERO).unwrap().as_str(),
515 "https://api.safe.global/tx-service/tempo-moderato/api/v1/safes/0x0000000000000000000000000000000000000000/multisig-transactions/?executed=false&ordering=-nonce&limit=1"
516 );
517 }
518
519 #[test]
520 fn parses_transaction_service_response() {
521 let transaction: SafeTransaction = serde_json::from_value(json!({
522 "safe": Address::ZERO,
523 "to": Address::ZERO,
524 "value": "1",
525 "data": null,
526 "operation": 0,
527 "safeTxGas": 0,
528 "baseGas": "0",
529 "gasPrice": 0,
530 "gasToken": null,
531 "refundReceiver": null,
532 "nonce": 7,
533 "safeTxHash": B256::ZERO,
534 }))
535 .unwrap();
536
537 assert_eq!(transaction.data, Bytes::new());
538 assert_eq!(transaction.nonce, "7");
539 assert_eq!(transaction.gas_token, Address::ZERO);
540 assert_eq!(transaction.refund_receiver, Address::ZERO);
541 }
542
543 #[test]
544 fn serializes_proposal_addresses_as_checksums() {
545 let mut transaction = transaction();
546 transaction.to = address!("5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed");
547 transaction.gas_token = address!("fB6916095ca1df60bB79Ce92cE3Ea74c37c5d359");
548 transaction.refund_receiver = address!("52908400098527886E0F7030069857D2E4169EE7");
549
550 assert_eq!(
551 transaction.proposal_body(
552 address!("8617E340B3D01FA5F11F306F4090FD50E238070D"),
553 "0x1234".to_string(),
554 Some("cast".to_string()),
555 ),
556 json!({
557 "to": "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed",
558 "value": "1",
559 "data": "0x",
560 "operation": 0,
561 "safeTxGas": "0",
562 "baseGas": "0",
563 "gasPrice": "0",
564 "gasToken": "0xfB6916095ca1df60bB79Ce92cE3Ea74c37c5d359",
565 "refundReceiver": "0x52908400098527886E0F7030069857D2E4169EE7",
566 "nonce": "7",
567 "contractTransactionHash": B256::ZERO,
568 "sender": "0x8617E340B3D01FA5F11F306F4090FD50E238070D",
569 "signature": "0x1234",
570 "origin": "cast",
571 })
572 );
573 }
574
575 #[test]
576 fn packs_confirmations_in_owner_order() {
577 let mut transaction = transaction();
578 transaction.confirmations = vec![
579 SafeConfirmation { owner: Address::repeat_byte(2), signature: Some(eoa_signature(2)) },
580 SafeConfirmation { owner: Address::repeat_byte(1), signature: Some(eoa_signature(1)) },
581 ];
582
583 let signatures = transaction.packed_signatures().unwrap();
584 assert_eq!(&signatures[..64], &[1; 64]);
585 assert_eq!(signatures[64], 27);
586 assert_eq!(&signatures[65..129], &[2; 64]);
587 assert_eq!(signatures[129], 27);
588 }
589
590 #[test]
591 fn packs_contract_signatures_after_static_signatures() {
592 let first_owner = Address::repeat_byte(1);
593 let third_owner = Address::repeat_byte(3);
594 let first_payload = [4, 5, 6];
595 let third_payload = [7, 8, 9, 10];
596
597 let mut transaction = transaction();
598 transaction.confirmations = vec![
599 SafeConfirmation {
600 owner: third_owner,
601 signature: Some(contract_signature(third_owner, &third_payload)),
602 },
603 SafeConfirmation { owner: Address::repeat_byte(2), signature: Some(eoa_signature(2)) },
604 SafeConfirmation {
605 owner: first_owner,
606 signature: Some(contract_signature(first_owner, &first_payload)),
607 },
608 ];
609
610 let signatures = transaction.packed_signatures().unwrap();
611 assert_eq!(&signatures[..32], first_owner.into_word().as_slice());
612 assert_eq!(U256::from_be_slice(&signatures[32..64]), U256::from(195));
613 assert_eq!(signatures[64], 0);
614 assert_eq!(&signatures[65..130], &eoa_signature(2));
615 assert_eq!(&signatures[130..162], third_owner.into_word().as_slice());
616 assert_eq!(U256::from_be_slice(&signatures[162..194]), U256::from(230));
617 assert_eq!(signatures[194], 0);
618 assert_eq!(U256::from_be_slice(&signatures[195..227]), U256::from(3));
619 assert_eq!(&signatures[227..230], &first_payload);
620 assert_eq!(U256::from_be_slice(&signatures[230..262]), U256::from(4));
621 assert_eq!(&signatures[262..], &third_payload);
622 }
623
624 #[test]
625 fn packs_p256_and_contract_signatures_after_static_signatures() {
626 let p256_owner = Address::repeat_byte(1);
627 let contract_owner = Address::repeat_byte(3);
628 let p256_payload = [4; 128];
629 let contract_payload = [5, 6, 7];
630 let mut transaction = transaction();
631 transaction.confirmations = vec![
632 SafeConfirmation {
633 owner: contract_owner,
634 signature: Some(contract_signature(contract_owner, &contract_payload)),
635 },
636 SafeConfirmation { owner: Address::repeat_byte(2), signature: Some(eoa_signature(2)) },
637 SafeConfirmation {
638 owner: p256_owner,
639 signature: Some(p256_signature(p256_owner, &p256_payload)),
640 },
641 ];
642
643 let signatures = transaction.packed_signatures().unwrap();
644 assert_eq!(&signatures[..32], p256_owner.into_word().as_slice());
645 assert_eq!(U256::from_be_slice(&signatures[32..64]), U256::from(195));
646 assert_eq!(signatures[64], 2);
647 assert_eq!(&signatures[65..130], &eoa_signature(2));
648 assert_eq!(&signatures[130..162], contract_owner.into_word().as_slice());
649 assert_eq!(U256::from_be_slice(&signatures[162..194]), U256::from(323));
650 assert_eq!(signatures[194], 0);
651 assert_eq!(&signatures[195..323], &p256_payload);
652 assert_eq!(U256::from_be_slice(&signatures[323..355]), U256::from(3));
653 assert_eq!(&signatures[355..], &contract_payload);
654 }
655
656 #[test]
657 fn rejects_malformed_confirmation_signatures() {
658 let owner = Address::repeat_byte(1);
659 let mut wrong_offset = vec![0; 97];
660 wrong_offset[32..64].copy_from_slice(&U256::from(66).to_be_bytes::<32>());
661 let mut wrong_length = vec![0; 97];
662 wrong_length[32..64].copy_from_slice(&U256::from(65).to_be_bytes::<32>());
663 wrong_length[65..97].copy_from_slice(&U256::from(1).to_be_bytes::<32>());
664
665 for signature in [vec![1; 66], vec![0; 65], wrong_offset, wrong_length] {
666 let mut transaction = transaction();
667 transaction.confirmations =
668 vec![SafeConfirmation { owner, signature: Some(signature.into()) }];
669 assert!(transaction.packed_signatures().is_err());
670 }
671 }
672
673 #[test]
674 fn rejects_malformed_p256_signatures() {
675 let owner = Address::repeat_byte(1);
676 let signature = |len: usize, offset: usize| {
677 let mut signature = vec![0; len];
678 signature[..U256::BYTES].copy_from_slice(owner.into_word().as_slice());
679 signature[U256::BYTES..SAFE_SIGNATURE_LENGTH - 1]
680 .copy_from_slice(&U256::from(offset).to_be_bytes::<32>());
681 signature[SAFE_SIGNATURE_LENGTH - 1] = 2;
682 signature
683 };
684
685 for signature in [
686 signature(SAFE_SIGNATURE_LENGTH, SAFE_SIGNATURE_LENGTH),
687 signature(P256_SIGNATURE_LENGTH - 1, SAFE_SIGNATURE_LENGTH),
688 signature(P256_SIGNATURE_LENGTH + 1, SAFE_SIGNATURE_LENGTH),
689 signature(P256_SIGNATURE_LENGTH, SAFE_SIGNATURE_LENGTH + 1),
690 ] {
691 let mut transaction = transaction();
692 transaction.confirmations =
693 vec![SafeConfirmation { owner, signature: Some(signature.into()) }];
694 assert!(transaction.packed_signatures().is_err());
695 }
696 }
697
698 #[test]
699 fn rejects_approved_hash_signatures() {
700 let mut signature = vec![1; SAFE_SIGNATURE_LENGTH];
701 signature[SAFE_SIGNATURE_LENGTH - 1] = 1;
702 let mut transaction = transaction();
703 transaction.confirmations = vec![SafeConfirmation {
704 owner: Address::repeat_byte(1),
705 signature: Some(signature.into()),
706 }];
707
708 let error = transaction.packed_signatures().unwrap_err();
709 assert_eq!(
710 error.to_string(),
711 "approved-hash signatures (v = 1) are not supported by `cast safe execute`"
712 );
713 }
714
715 #[tokio::test]
716 async fn calculates_hash_with_safe_contract() {
717 let expected = B256::repeat_byte(0x42);
718 let asserter = Asserter::new();
719 asserter.push_success(&expected);
720 let provider = ProviderBuilder::new().connect_mocked_client(asserter);
721
722 assert_eq!(transaction().calculate_hash(&provider).await.unwrap(), expected);
723 }
724}