Skip to main content

cast/cmd/safe/
signing.rs

1use alloy_dyn_abi::TypedData;
2use alloy_primitives::{Address, B256, hex};
3use alloy_signer::Signer;
4use eyre::Result;
5use foundry_wallets::WalletSigner;
6use serde_json::json;
7use std::time::{SystemTime, UNIX_EPOCH};
8
9const DELEGATE_TOTP_PERIOD_SECS: u64 = 60 * 60;
10
11pub(super) async fn sign_delegate(
12    signer: &WalletSigner,
13    delegate: Address,
14    chain_id: u64,
15) -> Result<String> {
16    let totp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() / DELEGATE_TOTP_PERIOD_SECS;
17    let typed_data = delegate_typed_data(delegate, chain_id, totp)?;
18    sign_delegate_typed_data(signer, &typed_data, matches!(signer, WalletSigner::Trezor(_))).await
19}
20
21pub(super) async fn sign_safe_hash(signer: &WalletSigner, safe_tx_hash: B256) -> Result<String> {
22    let signature = signer.sign_message(safe_tx_hash.as_slice()).await?;
23    Ok(normalize_signature(&signature.as_bytes(), true))
24}
25
26fn delegate_typed_data(delegate: Address, chain_id: u64, totp: u64) -> Result<TypedData> {
27    Ok(serde_json::from_value(json!({
28        "types": {
29            "EIP712Domain": [
30                { "name": "name", "type": "string" },
31                { "name": "version", "type": "string" },
32                { "name": "chainId", "type": "uint256" }
33            ],
34            "Delegate": [
35                { "name": "delegateAddress", "type": "address" },
36                { "name": "totp", "type": "uint256" }
37            ]
38        },
39        "primaryType": "Delegate",
40        "domain": {
41            "name": "Safe Transaction Service",
42            "version": "1.0",
43            "chainId": chain_id
44        },
45        "message": {
46            "delegateAddress": delegate.to_checksum(None),
47            "totp": totp
48        }
49    }))?)
50}
51
52async fn sign_delegate_typed_data(
53    signer: &WalletSigner,
54    typed_data: &TypedData,
55    safe_eth_sign: bool,
56) -> Result<String> {
57    let signature = if safe_eth_sign {
58        signer.sign_message(typed_data.eip712_signing_hash()?.as_slice()).await?
59    } else {
60        signer.sign_dynamic_typed_data(typed_data).await?
61    };
62    Ok(normalize_signature(&signature.as_bytes(), safe_eth_sign))
63}
64
65fn normalize_signature(signature: &[u8], safe_eth_sign: bool) -> String {
66    let mut signature = signature.to_vec();
67    let v = &mut signature[64];
68    if *v < 27 {
69        *v += 27;
70    }
71    if safe_eth_sign {
72        *v += 4;
73    }
74    hex::encode_prefixed(signature)
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80    use alloy_primitives::Signature;
81
82    #[test]
83    fn normalizes_safe_eth_sign_v() {
84        let mut signature = [0u8; 65];
85        signature[64] = 1;
86        assert!(normalize_signature(&signature, true).ends_with("20"));
87
88        signature[64] = 27;
89        assert!(normalize_signature(&signature, true).ends_with("1f"));
90    }
91
92    #[test]
93    fn normalizes_typed_data_v() {
94        let mut signature = [0u8; 65];
95        signature[64] = 0;
96        assert!(normalize_signature(&signature, false).ends_with("1b"));
97    }
98
99    #[tokio::test]
100    async fn signs_delegate_typed_data_with_supported_safe_signatures() -> Result<()> {
101        let typed_data = delegate_typed_data(Address::repeat_byte(0x11), 1, 1)?;
102        let signer = WalletSigner::from_private_key(&B256::repeat_byte(1))?;
103        let signing_hash = typed_data.eip712_signing_hash()?;
104
105        for safe_eth_sign in [false, true] {
106            let signature = sign_delegate_typed_data(&signer, &typed_data, safe_eth_sign).await?;
107            let mut bytes = hex::decode(signature.strip_prefix("0x").unwrap())?;
108            if safe_eth_sign {
109                assert!(matches!(bytes[64], 31 | 32));
110                bytes[64] -= 4;
111            } else {
112                assert!(matches!(bytes[64], 27 | 28));
113            }
114            let signature = Signature::from_raw(&bytes)?;
115            let recovered = if safe_eth_sign {
116                signature.recover_address_from_msg(signing_hash.as_slice())?
117            } else {
118                signature.recover_address_from_prehash(&signing_hash)?
119            };
120            assert_eq!(recovered, signer.address());
121        }
122        Ok(())
123    }
124}