Skip to main content

cast/cmd/safe/
service.rs

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    /// Safe Transaction Service URL. Inferred from the RPC chain ID when omitted.
20    /// The `/api` suffix is optional.
21    #[arg(long, env = "SAFE_TRANSACTION_SERVICE_URL")]
22    pub(super) service_url: Option<Url>,
23
24    /// Safe Transaction Service API key.
25    #[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}