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}