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_cli::utils::now;
6use foundry_wallets::WalletSigner;
7use serde_json::json;
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 = now().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_signature_v() {
84        let mut signature = [0u8; 65];
85        signature[64] = 1;
86        assert!(normalize_signature(&signature, true).ends_with("20"));
87        signature[64] = 27;
88        assert!(normalize_signature(&signature, true).ends_with("1f"));
89        signature[64] = 0;
90        assert!(normalize_signature(&signature, false).ends_with("1b"));
91    }
92
93    #[tokio::test]
94    async fn signs_delegate_typed_data_with_supported_safe_signatures() -> Result<()> {
95        let typed_data = delegate_typed_data(Address::repeat_byte(0x11), 1, 1)?;
96        let signer = WalletSigner::from_private_key(&B256::repeat_byte(1))?;
97        let signing_hash = typed_data.eip712_signing_hash()?;
98
99        for safe_eth_sign in [false, true] {
100            let signature = sign_delegate_typed_data(&signer, &typed_data, safe_eth_sign).await?;
101            let mut bytes = hex::decode(signature.strip_prefix("0x").unwrap())?;
102            if safe_eth_sign {
103                assert!(matches!(bytes[64], 31 | 32));
104                bytes[64] -= 4;
105            } else {
106                assert!(matches!(bytes[64], 27 | 28));
107            }
108            let signature = Signature::from_raw(&bytes)?;
109            let recovered = if safe_eth_sign {
110                signature.recover_address_from_msg(signing_hash.as_slice())?
111            } else {
112                signature.recover_address_from_prehash(&signing_hash)?
113            };
114            assert_eq!(recovered, signer.address());
115        }
116        Ok(())
117    }
118}