alloy_sol_types/
eip712.rs

1use crate::SolValue;
2use alloc::{borrow::Cow, string::String, vec::Vec};
3use alloy_primitives::{keccak256, Address, FixedBytes, B256, U256};
4
5/// EIP-712 domain attributes used in determining the domain separator.
6///
7/// Unused fields are left out of the struct type.
8///
9/// Protocol designers only need to include the fields that make sense for
10/// their signing domain.
11#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
12#[cfg_attr(feature = "eip712-serde", derive(serde::Serialize, serde::Deserialize))]
13#[cfg_attr(feature = "eip712-serde", serde(rename_all = "camelCase"))]
14pub struct Eip712Domain {
15    /// The user readable name of signing domain, i.e. the name of the DApp or
16    /// the protocol.
17    #[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
18    pub name: Option<Cow<'static, str>>,
19
20    /// The current major version of the signing domain. Signatures from
21    /// different versions are not compatible.
22    #[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
23    pub version: Option<Cow<'static, str>>,
24
25    /// The EIP-155 chain ID. The user-agent should refuse signing if it does
26    /// not match the currently active chain.
27    #[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
28    pub chain_id: Option<U256>,
29
30    /// The address of the contract that will verify the signature.
31    #[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
32    pub verifying_contract: Option<Address>,
33
34    /// A disambiguating salt for the protocol. This can be used as a domain
35    /// separator of last resort.
36    #[cfg_attr(feature = "eip712-serde", serde(default, skip_serializing_if = "Option::is_none"))]
37    pub salt: Option<B256>,
38}
39
40impl Eip712Domain {
41    /// The name of the struct.
42    pub const NAME: &'static str = "EIP712Domain";
43
44    /// Instantiate a new EIP-712 domain.
45    ///
46    /// Use the [`eip712_domain!`](crate::eip712_domain!) macro for easier
47    /// instantiation.
48    #[inline]
49    pub const fn new(
50        name: Option<Cow<'static, str>>,
51        version: Option<Cow<'static, str>>,
52        chain_id: Option<U256>,
53        verifying_contract: Option<Address>,
54        salt: Option<B256>,
55    ) -> Self {
56        Self { name, version, chain_id, verifying_contract, salt }
57    }
58
59    /// Calculate the domain separator for the domain object.
60    #[inline]
61    pub fn separator(&self) -> B256 {
62        self.hash_struct()
63    }
64
65    /// The EIP-712-encoded type string.
66    ///
67    /// See [EIP-712 `encodeType`](https://eips.ethereum.org/EIPS/eip-712#definition-of-encodetype).
68    pub fn encode_type(&self) -> String {
69        // commas not included
70        macro_rules! encode_type {
71            ($($field:ident => $repr:literal),+ $(,)?) => {
72                let mut ty = String::with_capacity(Self::NAME.len() + 2 $(+ $repr.len() * self.$field.is_some() as usize)+);
73                ty.push_str(Self::NAME);
74                ty.push('(');
75
76                $(
77                    if self.$field.is_some() {
78                        ty.push_str($repr);
79                    }
80                )+
81                if ty.ends_with(',') {
82                    ty.pop();
83                }
84
85                ty.push(')');
86                ty
87            };
88        }
89
90        encode_type! {
91            name               => "string name,",
92            version            => "string version,",
93            chain_id           => "uint256 chainId,",
94            verifying_contract => "address verifyingContract,",
95            salt               => "bytes32 salt",
96        }
97    }
98
99    /// Calculates the [EIP-712 `typeHash`](https://eips.ethereum.org/EIPS/eip-712#rationale-for-typehash)
100    /// for this domain.
101    ///
102    /// This is defined as the Keccak-256 hash of the
103    /// [`encodeType`](Self::encode_type) string.
104    #[inline]
105    pub fn type_hash(&self) -> B256 {
106        keccak256(self.encode_type().as_bytes())
107    }
108
109    /// Returns the number of ABI words (32 bytes) that will be used to encode
110    /// the domain.
111    #[inline]
112    pub const fn num_words(&self) -> usize {
113        self.name.is_some() as usize
114            + self.version.is_some() as usize
115            + self.chain_id.is_some() as usize
116            + self.verifying_contract.is_some() as usize
117            + self.salt.is_some() as usize
118    }
119
120    /// Returns the number of bytes that will be used to encode the domain.
121    #[inline]
122    pub const fn abi_encoded_size(&self) -> usize {
123        self.num_words() * 32
124    }
125
126    /// Encodes this domain using [EIP-712 `encodeData`](https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata)
127    /// into the given buffer.
128    pub fn encode_data_to(&self, out: &mut Vec<u8>) {
129        // This only works because all of the fields are encoded as words.
130        macro_rules! encode_opt {
131            ($opt:expr) => {
132                if let Some(t) = $opt {
133                    out.extend_from_slice(t.tokenize().as_slice());
134                }
135            };
136        }
137
138        #[inline]
139        #[allow(clippy::ptr_arg)]
140        fn cow_keccak256(s: &Cow<'_, str>) -> FixedBytes<32> {
141            keccak256(s.as_bytes())
142        }
143
144        out.reserve(self.abi_encoded_size());
145        encode_opt!(self.name.as_ref().map(cow_keccak256));
146        encode_opt!(self.version.as_ref().map(cow_keccak256));
147        encode_opt!(&self.chain_id);
148        encode_opt!(&self.verifying_contract);
149        encode_opt!(&self.salt);
150    }
151
152    /// Encodes this domain using [EIP-712 `encodeData`](https://eips.ethereum.org/EIPS/eip-712#definition-of-encodedata).
153    pub fn encode_data(&self) -> Vec<u8> {
154        let mut out = Vec::new();
155        self.encode_data_to(&mut out);
156        out
157    }
158
159    /// Hashes this domain according to [EIP-712 `hashStruct`](https://eips.ethereum.org/EIPS/eip-712#definition-of-hashstruct).
160    #[inline]
161    pub fn hash_struct(&self) -> B256 {
162        let mut hasher = alloy_primitives::Keccak256::new();
163        hasher.update(self.type_hash());
164        hasher.update(self.encode_data());
165        hasher.finalize()
166    }
167}
168
169/// Convenience macro to instantiate an [EIP-712 domain](Eip712Domain).
170///
171/// This macro allows you to instantiate an [EIP-712 domain](Eip712Domain)
172/// struct without manually writing `None` for unused fields.
173///
174/// It may be used to declare a domain with any combination of fields. Each
175/// field must be labeled with the name of the field, and the fields must be in
176/// order. The fields for the domain are:
177/// - `name`
178/// - `version`
179/// - `chain_id`
180/// - `verifying_contract`
181/// - `salt`
182///
183/// # Examples
184///
185/// ```
186/// # use alloy_sol_types::{Eip712Domain, eip712_domain};
187/// # use alloy_primitives::keccak256;
188/// const MY_DOMAIN: Eip712Domain = eip712_domain! {
189///     name: "MyCoolProtocol",
190/// };
191///
192/// let dynamic_name = String::from("MyCoolProtocol");
193/// let my_other_domain: Eip712Domain = eip712_domain! {
194///     name: dynamic_name,
195///     version: "1.0.0",
196///     salt: keccak256("my domain salt"),
197/// };
198/// ```
199#[macro_export]
200macro_rules! eip712_domain {
201    (@opt) => { $crate::private::None };
202    (@opt $e:expr) => { $crate::private::Some($e) };
203
204    // special case literals to allow calling this in const contexts
205    (@cow) => { $crate::private::None };
206    (@cow $l:literal) => { $crate::private::Some($crate::private::Cow::Borrowed($l)) };
207    (@cow $e:expr) => { $crate::private::Some(<$crate::private::Cow<'static, str> as $crate::private::From<_>>::from($e)) };
208
209    (
210        $(name: $name:expr,)?
211        $(version: $version:expr,)?
212        $(chain_id: $chain_id:expr,)?
213        $(verifying_contract: $verifying_contract:expr,)?
214        $(salt: $salt:expr)?
215        $(,)?
216    ) => {
217        $crate::Eip712Domain::new(
218            $crate::eip712_domain!(@cow $($name)?),
219            $crate::eip712_domain!(@cow $($version)?),
220            $crate::eip712_domain!(@opt $($crate::private::u256($chain_id))?),
221            $crate::eip712_domain!(@opt $($verifying_contract)?),
222            $crate::eip712_domain!(@opt $($salt)?),
223        )
224    };
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230
231    const _: Eip712Domain = eip712_domain! {
232        name: "abcd",
233    };
234    const _: Eip712Domain = eip712_domain! {
235        name: "abcd",
236        version: "1",
237    };
238    const _: Eip712Domain = eip712_domain! {
239        name: "abcd",
240        version: "1",
241        chain_id: 1,
242    };
243    const _: Eip712Domain = eip712_domain! {
244        name: "abcd",
245        version: "1",
246        chain_id: 1,
247        verifying_contract: Address::ZERO,
248    };
249    const _: Eip712Domain = eip712_domain! {
250        name: "abcd",
251        version: "1",
252        chain_id: 1,
253        verifying_contract: Address::ZERO,
254        salt: B256::ZERO // no trailing comma
255    };
256    const _: Eip712Domain = eip712_domain! {
257        name: "abcd",
258        version: "1",
259        chain_id: 1,
260        verifying_contract: Address::ZERO,
261        salt: B256::ZERO, // trailing comma
262    };
263
264    const _: Eip712Domain = eip712_domain! {
265        name: "abcd",
266        version: "1",
267        // chain_id: 1,
268        verifying_contract: Address::ZERO,
269        salt: B256::ZERO,
270    };
271    const _: Eip712Domain = eip712_domain! {
272        name: "abcd",
273        // version: "1",
274        chain_id: 1,
275        verifying_contract: Address::ZERO,
276        salt: B256::ZERO,
277    };
278    const _: Eip712Domain = eip712_domain! {
279        name: "abcd",
280        // version: "1",
281        // chain_id: 1,
282        verifying_contract: Address::ZERO,
283        salt: B256::ZERO,
284    };
285    const _: Eip712Domain = eip712_domain! {
286        name: "abcd",
287        // version: "1",
288        // chain_id: 1,
289        // verifying_contract: Address::ZERO,
290        salt: B256::ZERO,
291    };
292    const _: Eip712Domain = eip712_domain! {
293        // name: "abcd",
294        version: "1",
295        // chain_id: 1,
296        // verifying_contract: Address::ZERO,
297        salt: B256::ZERO,
298    };
299    const _: Eip712Domain = eip712_domain! {
300        // name: "abcd",
301        version: "1",
302        // chain_id: 1,
303        verifying_contract: Address::ZERO,
304        salt: B256::ZERO,
305    };
306
307    #[test]
308    fn runtime_domains() {
309        let _: Eip712Domain = eip712_domain! {
310            name: String::new(),
311            version: String::new(),
312        };
313
314        let my_string = String::from("!@#$%^&*()_+");
315        let _: Eip712Domain = eip712_domain! {
316            name: my_string.clone(),
317            version: my_string,
318        };
319
320        let my_cow = Cow::from("my_cow");
321        let _: Eip712Domain = eip712_domain! {
322            name: my_cow.clone(),
323            version: my_cow.into_owned(),
324        };
325    }
326}