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}