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, 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            let v = signature[SAFE_SIGNATURE_LENGTH - 1];
175            match v {
176                // Contract (v = 0) and P-256 (v = 2) signatures carry dynamic data after the
177                // static part, which points at the dynamic offset.
178                0 | 2 => {
179                    let kind = if v == 0 { "contract" } else { "P-256" };
180                    if v == 0 {
181                        ensure!(
182                            signature.len() >= CONTRACT_SIGNATURE_HEADER_LENGTH,
183                            "contract signature from {} does not contain a length",
184                            confirmation.owner
185                        );
186                        let data_len = U256::from_be_slice(
187                            &signature[SAFE_SIGNATURE_LENGTH..CONTRACT_SIGNATURE_HEADER_LENGTH],
188                        );
189                        ensure!(
190                            data_len
191                                == U256::from(signature.len() - CONTRACT_SIGNATURE_HEADER_LENGTH),
192                            "invalid contract signature length from {}: expected {}, got {data_len}",
193                            confirmation.owner,
194                            signature.len() - CONTRACT_SIGNATURE_HEADER_LENGTH
195                        );
196                    } else {
197                        ensure!(
198                            signature.len() == P256_SIGNATURE_LENGTH,
199                            "invalid P-256 signature from {}: expected {P256_SIGNATURE_LENGTH} bytes, got {}",
200                            confirmation.owner,
201                            signature.len()
202                        );
203                    }
204                    let offset =
205                        U256::from_be_slice(&signature[U256::BYTES..SAFE_SIGNATURE_LENGTH - 1]);
206                    ensure!(
207                        offset == U256::from(SAFE_SIGNATURE_LENGTH),
208                        "invalid {kind} signature offset from {}: expected {SAFE_SIGNATURE_LENGTH}, got {offset}",
209                        confirmation.owner
210                    );
211
212                    signatures.extend_from_slice(&signature[..U256::BYTES]);
213                    signatures.extend_from_slice(
214                        &U256::from(static_len + dynamic.len()).to_be_bytes::<{ U256::BYTES }>(),
215                    );
216                    signatures.push(v);
217                    dynamic.extend_from_slice(&signature[SAFE_SIGNATURE_LENGTH..]);
218                }
219                1 => {
220                    eyre::bail!(
221                        "approved-hash signatures (v = 1) are not supported by `cast safe execute`"
222                    );
223                }
224                _ => {
225                    ensure!(
226                        signature.len() == SAFE_SIGNATURE_LENGTH,
227                        "invalid signature from {}: expected {SAFE_SIGNATURE_LENGTH} bytes, got {}",
228                        confirmation.owner,
229                        signature.len()
230                    );
231                    signatures.extend_from_slice(signature);
232                }
233            }
234        }
235        ensure!(!signatures.is_empty(), "Safe transaction has no confirmations");
236        signatures.extend_from_slice(&dynamic);
237        Ok(signatures.into())
238    }
239
240    pub(super) fn show_transaction_summary(&self) -> Result<()> {
241        let operation = if self.operation == 0 { "CALL" } else { "DELEGATECALL" };
242        sh_status!("Safe transaction: {}", self.safe_tx_hash)?;
243        sh_status!("  Safe:            {}", self.safe)?;
244        sh_status!("  To:              {}", self.to)?;
245        sh_status!("  Value:           {}", self.value)?;
246        sh_status!("  Operation:       {} ({operation})", self.operation)?;
247        sh_status!("  Safe tx gas:     {}", self.safe_tx_gas)?;
248        sh_status!("  Base gas:        {}", self.base_gas)?;
249        sh_status!("  Gas price:       {}", self.gas_price)?;
250        sh_status!("  Gas token:       {}", self.gas_token)?;
251        sh_status!("  Refund receiver: {}", self.refund_receiver)?;
252        sh_status!("  Nonce:           {}", self.nonce)?;
253        sh_status!("  Data:            {}", self.data)?;
254        Ok(())
255    }
256}
257
258impl SafeServiceOpts {
259    pub(super) fn endpoint(&self, chain_id: u64, path: &str) -> Result<Url> {
260        let mut url = match &self.service_url {
261            Some(url) => url.clone(),
262            None => default_service_url(chain_id)?,
263        };
264        let mut endpoint = url.path().trim_end_matches('/').to_string();
265        if !endpoint.ends_with("/api") {
266            endpoint.push_str("/api");
267        }
268        endpoint.push('/');
269        endpoint.push_str(path.trim_start_matches('/'));
270        url.set_path(&endpoint);
271        url.set_query(None);
272        url.set_fragment(None);
273        Ok(url)
274    }
275
276    pub(super) fn request(&self, method: Method, url: Url) -> reqwest::RequestBuilder {
277        let request = Client::new()
278            .request(method, url)
279            .header(header::ACCEPT, "application/json")
280            .header(header::CONTENT_TYPE, "application/json");
281        if let Some(api_key) = &self.api_key { request.bearer_auth(api_key) } else { request }
282    }
283
284    /// Sends `request` and returns the successful response body.
285    async fn send(&self, request: reqwest::RequestBuilder) -> Result<String> {
286        let response = request.send().await.wrap_err("Safe Transaction Service request failed")?;
287        let status = response.status();
288        let text = response.text().await.wrap_err("failed to read Transaction Service response")?;
289        ensure!(status.is_success(), "Safe Transaction Service returned {status}: {}", text.trim());
290        Ok(text)
291    }
292
293    pub(super) async fn response<T: DeserializeOwned>(
294        &self,
295        request: reqwest::RequestBuilder,
296    ) -> Result<T> {
297        serde_json::from_str(&self.send(request).await?)
298            .wrap_err("invalid Transaction Service response")
299    }
300
301    pub(super) async fn empty_response(&self, request: reqwest::RequestBuilder) -> Result<()> {
302        self.send(request).await.map(drop)
303    }
304
305    pub(super) async fn get_transaction(
306        &self,
307        chain_id: u64,
308        api_version: &str,
309        safe_tx_hash: B256,
310    ) -> Result<SafeTransaction> {
311        let url = self
312            .endpoint(chain_id, &format!("{api_version}/multisig-transactions/{safe_tx_hash}/"))?;
313        let transaction: SafeTransaction = self.response(self.request(Method::GET, url)).await?;
314        ensure!(
315            transaction.safe_tx_hash == safe_tx_hash,
316            "Transaction Service returned a different Safe transaction hash"
317        );
318        Ok(transaction)
319    }
320
321    pub(super) async fn next_nonce(
322        &self,
323        chain_id: u64,
324        safe: Address,
325        onchain_nonce: U256,
326    ) -> Result<U256> {
327        let response: Value = self
328            .response(self.request(Method::GET, self.next_nonce_endpoint(chain_id, safe)?))
329            .await?;
330        let Some(nonce) = response
331            .get("results")
332            .and_then(Value::as_array)
333            .and_then(|results| results.first())
334            .and_then(|tx| tx.get("nonce"))
335        else {
336            return Ok(onchain_nonce);
337        };
338        let nonce = match nonce {
339            Value::String(nonce) => U256::from_str(nonce),
340            Value::Number(nonce) => U256::from_str(&nonce.to_string()),
341            _ => eyre::bail!("invalid nonce returned by Safe Transaction Service"),
342        }
343        .wrap_err("invalid nonce returned by Safe Transaction Service")?;
344        Ok(std::cmp::max(onchain_nonce, nonce.saturating_add(U256::from(1))))
345    }
346
347    fn next_nonce_endpoint(&self, chain_id: u64, safe: Address) -> Result<Url> {
348        let mut url = self.endpoint(
349            chain_id,
350            &format!("v1/safes/{}/multisig-transactions/", safe.to_checksum(None)),
351        )?;
352        url.query_pairs_mut()
353            .append_pair("executed", "false")
354            .append_pair("ordering", "-nonce")
355            .append_pair("limit", "1");
356        Ok(url)
357    }
358}
359
360fn deserialize_number_string<'de, D>(deserializer: D) -> Result<String, D::Error>
361where
362    D: Deserializer<'de>,
363{
364    match Value::deserialize(deserializer)? {
365        Value::String(value) => Ok(value),
366        Value::Number(value) => Ok(value.to_string()),
367        value => Err(serde::de::Error::custom(format!("expected number or string, got {value}"))),
368    }
369}
370
371fn deserialize_null_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
372where
373    D: Deserializer<'de>,
374    T: Deserialize<'de> + Default,
375{
376    Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
377}
378
379fn default_service_url(chain_id: u64) -> Result<Url> {
380    let short_name = match chain_id {
381        1 => "eth",
382        10 => "oeth",
383        56 => "bnb",
384        100 => "gno",
385        130 => "unichain",
386        137 => "pol",
387        146 => "sonic",
388        196 => "okb",
389        232 => "lens",
390        324 => "zksync",
391        480 => "wc",
392        4217 => "tempo",
393        5000 => "mantle",
394        8453 => "base",
395        9745 => "plasma",
396        10200 => "chi",
397        42161 => "arb1",
398        42220 => "celo",
399        42431 => "tempo-moderato",
400        43114 => "avax",
401        43111 => "hemi",
402        57073 => "ink",
403        59144 => "linea",
404        747474 => "katana",
405        80094 => "berachain",
406        84532 => "basesep",
407        534352 => "scr",
408        11155111 => "sep",
409        1313161554 => "aurora",
410        _ => eyre::bail!(
411            "no known Safe Transaction Service for chain ID {chain_id}; pass --service-url"
412        ),
413    };
414    format!("https://api.safe.global/tx-service/{short_name}")
415        .parse()
416        .wrap_err("invalid built-in Safe Transaction Service URL")
417}
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    fn transaction() -> SafeTransaction {
424        SafeTransaction {
425            safe: Address::ZERO,
426            to: Address::ZERO,
427            value: "1".to_string(),
428            data: Bytes::new(),
429            operation: 0,
430            safe_tx_gas: "0".to_string(),
431            base_gas: "0".to_string(),
432            gas_price: "0".to_string(),
433            gas_token: Address::ZERO,
434            refund_receiver: Address::ZERO,
435            nonce: "7".to_string(),
436            safe_tx_hash: B256::ZERO,
437            confirmations: Vec::new(),
438            is_executed: false,
439            transaction_hash: None,
440        }
441    }
442
443    fn eoa_signature(byte: u8) -> Bytes {
444        let mut signature = vec![byte; SAFE_SIGNATURE_LENGTH];
445        signature[SAFE_SIGNATURE_LENGTH - 1] = 27;
446        signature.into()
447    }
448
449    fn contract_signature(owner: Address, payload: &[u8]) -> Bytes {
450        let mut signature = Vec::with_capacity(CONTRACT_SIGNATURE_HEADER_LENGTH + payload.len());
451        signature.extend_from_slice(owner.into_word().as_slice());
452        signature.extend_from_slice(&U256::from(SAFE_SIGNATURE_LENGTH).to_be_bytes::<32>());
453        signature.push(0);
454        signature.extend_from_slice(&U256::from(payload.len()).to_be_bytes::<32>());
455        signature.extend_from_slice(payload);
456        signature.into()
457    }
458
459    fn p256_signature(owner: Address, payload: &[u8; 128]) -> Bytes {
460        let mut signature = Vec::with_capacity(SAFE_SIGNATURE_LENGTH + payload.len());
461        signature.extend_from_slice(owner.into_word().as_slice());
462        signature.extend_from_slice(&U256::from(SAFE_SIGNATURE_LENGTH).to_be_bytes::<32>());
463        signature.push(2);
464        signature.extend_from_slice(payload);
465        signature.into()
466    }
467
468    #[test]
469    fn normalizes_transaction_service_urls() {
470        for base in [
471            "https://api.safe.global/tx-service/tempo-moderato/",
472            "https://api.safe.global/tx-service/tempo-moderato/api",
473        ] {
474            let service =
475                SafeServiceOpts { service_url: Some(base.parse().unwrap()), api_key: None };
476            assert_eq!(
477                service.endpoint(42431, "v2/delegates/").unwrap().as_str(),
478                "https://api.safe.global/tx-service/tempo-moderato/api/v2/delegates/"
479            );
480        }
481    }
482
483    #[test]
484    fn infers_tempo_transaction_service_urls() {
485        assert_eq!(
486            default_service_url(4217).unwrap().as_str(),
487            "https://api.safe.global/tx-service/tempo"
488        );
489        assert_eq!(
490            default_service_url(42431).unwrap().as_str(),
491            "https://api.safe.global/tx-service/tempo-moderato"
492        );
493        for chain_id in [1101, 81457, 31337] {
494            assert!(default_service_url(chain_id).is_err());
495        }
496    }
497
498    #[test]
499    fn builds_pending_nonce_query_as_url_parameters() {
500        let service = SafeServiceOpts { service_url: None, api_key: None };
501        assert_eq!(
502            service.next_nonce_endpoint(42431, Address::ZERO).unwrap().as_str(),
503            "https://api.safe.global/tx-service/tempo-moderato/api/v1/safes/0x0000000000000000000000000000000000000000/multisig-transactions/?executed=false&ordering=-nonce&limit=1"
504        );
505    }
506
507    #[test]
508    fn parses_transaction_service_response() {
509        let transaction: SafeTransaction = serde_json::from_value(json!({
510            "safe": Address::ZERO,
511            "to": Address::ZERO,
512            "value": "1",
513            "data": null,
514            "operation": 0,
515            "safeTxGas": 0,
516            "baseGas": "0",
517            "gasPrice": 0,
518            "gasToken": null,
519            "refundReceiver": null,
520            "nonce": 7,
521            "safeTxHash": B256::ZERO,
522        }))
523        .unwrap();
524
525        assert_eq!(transaction.data, Bytes::new());
526        assert_eq!(transaction.nonce, "7");
527        assert_eq!(transaction.gas_token, Address::ZERO);
528        assert_eq!(transaction.refund_receiver, Address::ZERO);
529    }
530
531    #[test]
532    fn packs_confirmations_in_owner_order() {
533        let mut transaction = transaction();
534        transaction.confirmations = vec![
535            SafeConfirmation { owner: Address::repeat_byte(2), signature: Some(eoa_signature(2)) },
536            SafeConfirmation { owner: Address::repeat_byte(1), signature: Some(eoa_signature(1)) },
537        ];
538
539        let signatures = transaction.packed_signatures().unwrap();
540        assert_eq!(&signatures[..64], &[1; 64]);
541        assert_eq!(signatures[64], 27);
542        assert_eq!(&signatures[65..129], &[2; 64]);
543        assert_eq!(signatures[129], 27);
544    }
545
546    #[test]
547    fn packs_contract_signatures_after_static_signatures() {
548        let first_owner = Address::repeat_byte(1);
549        let third_owner = Address::repeat_byte(3);
550        let first_payload = [4, 5, 6];
551        let third_payload = [7, 8, 9, 10];
552
553        let mut transaction = transaction();
554        transaction.confirmations = vec![
555            SafeConfirmation {
556                owner: third_owner,
557                signature: Some(contract_signature(third_owner, &third_payload)),
558            },
559            SafeConfirmation { owner: Address::repeat_byte(2), signature: Some(eoa_signature(2)) },
560            SafeConfirmation {
561                owner: first_owner,
562                signature: Some(contract_signature(first_owner, &first_payload)),
563            },
564        ];
565
566        let signatures = transaction.packed_signatures().unwrap();
567        assert_eq!(&signatures[..32], first_owner.into_word().as_slice());
568        assert_eq!(U256::from_be_slice(&signatures[32..64]), U256::from(195));
569        assert_eq!(signatures[64], 0);
570        assert_eq!(&signatures[65..130], &eoa_signature(2));
571        assert_eq!(&signatures[130..162], third_owner.into_word().as_slice());
572        assert_eq!(U256::from_be_slice(&signatures[162..194]), U256::from(230));
573        assert_eq!(signatures[194], 0);
574        assert_eq!(U256::from_be_slice(&signatures[195..227]), U256::from(3));
575        assert_eq!(&signatures[227..230], &first_payload);
576        assert_eq!(U256::from_be_slice(&signatures[230..262]), U256::from(4));
577        assert_eq!(&signatures[262..], &third_payload);
578    }
579
580    #[test]
581    fn packs_p256_and_contract_signatures_after_static_signatures() {
582        let p256_owner = Address::repeat_byte(1);
583        let contract_owner = Address::repeat_byte(3);
584        let p256_payload = [4; 128];
585        let contract_payload = [5, 6, 7];
586        let mut transaction = transaction();
587        transaction.confirmations = vec![
588            SafeConfirmation {
589                owner: contract_owner,
590                signature: Some(contract_signature(contract_owner, &contract_payload)),
591            },
592            SafeConfirmation { owner: Address::repeat_byte(2), signature: Some(eoa_signature(2)) },
593            SafeConfirmation {
594                owner: p256_owner,
595                signature: Some(p256_signature(p256_owner, &p256_payload)),
596            },
597        ];
598
599        let signatures = transaction.packed_signatures().unwrap();
600        assert_eq!(&signatures[..32], p256_owner.into_word().as_slice());
601        assert_eq!(U256::from_be_slice(&signatures[32..64]), U256::from(195));
602        assert_eq!(signatures[64], 2);
603        assert_eq!(&signatures[65..130], &eoa_signature(2));
604        assert_eq!(&signatures[130..162], contract_owner.into_word().as_slice());
605        assert_eq!(U256::from_be_slice(&signatures[162..194]), U256::from(323));
606        assert_eq!(signatures[194], 0);
607        assert_eq!(&signatures[195..323], &p256_payload);
608        assert_eq!(U256::from_be_slice(&signatures[323..355]), U256::from(3));
609        assert_eq!(&signatures[355..], &contract_payload);
610    }
611
612    #[test]
613    fn rejects_malformed_confirmation_signatures() {
614        let owner = Address::repeat_byte(1);
615        let mut wrong_offset = vec![0; 97];
616        wrong_offset[32..64].copy_from_slice(&U256::from(66).to_be_bytes::<32>());
617        let mut wrong_length = vec![0; 97];
618        wrong_length[32..64].copy_from_slice(&U256::from(65).to_be_bytes::<32>());
619        wrong_length[65..97].copy_from_slice(&U256::from(1).to_be_bytes::<32>());
620
621        for signature in [vec![1; 66], vec![0; 65], wrong_offset, wrong_length] {
622            let mut transaction = transaction();
623            transaction.confirmations =
624                vec![SafeConfirmation { owner, signature: Some(signature.into()) }];
625            assert!(transaction.packed_signatures().is_err());
626        }
627    }
628
629    #[test]
630    fn rejects_malformed_p256_signatures() {
631        let owner = Address::repeat_byte(1);
632        let signature = |len: usize, offset: usize| {
633            let mut signature = vec![0; len];
634            signature[..U256::BYTES].copy_from_slice(owner.into_word().as_slice());
635            signature[U256::BYTES..SAFE_SIGNATURE_LENGTH - 1]
636                .copy_from_slice(&U256::from(offset).to_be_bytes::<32>());
637            signature[SAFE_SIGNATURE_LENGTH - 1] = 2;
638            signature
639        };
640
641        for signature in [
642            signature(SAFE_SIGNATURE_LENGTH, SAFE_SIGNATURE_LENGTH),
643            signature(P256_SIGNATURE_LENGTH - 1, SAFE_SIGNATURE_LENGTH),
644            signature(P256_SIGNATURE_LENGTH + 1, SAFE_SIGNATURE_LENGTH),
645            signature(P256_SIGNATURE_LENGTH, SAFE_SIGNATURE_LENGTH + 1),
646        ] {
647            let mut transaction = transaction();
648            transaction.confirmations =
649                vec![SafeConfirmation { owner, signature: Some(signature.into()) }];
650            assert!(transaction.packed_signatures().is_err());
651        }
652    }
653}