ethers_derive_eip712/
lib.rs

1//! # EIP-712 Derive Macro
2//!
3//! This crate provides a derive macro `Eip712` that is used to encode a rust struct
4//! into a payload hash, according to [https://eips.ethereum.org/EIPS/eip-712](https://eips.ethereum.org/EIPS/eip-712)
5//!
6//! The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712`
7//! Both the derive macro and the trait must be in context when using
8//!
9//! This derive macro requires the `#[eip712]` attributes to be included
10//! for specifying the domain separator used in encoding the hash.
11//!
12//! NOTE: In addition to deriving `Eip712` trait, the `EthAbiType` trait must also be derived.
13//! This allows the struct to be parsed into `ethers_core::abi::Token` for encoding.
14//!
15//! # Optional Eip712 Parameters
16//!
17//! The only optional parameter is `salt`, which accepts a string
18//! that is hashed using keccak256 and stored as bytes.
19//!
20//! # Example Usage
21//!
22//! ```ignore
23//! use ethers_contract::EthAbiType;
24//! use ethers_derive_eip712::*;
25//! use ethers_core::types::{transaction::eip712::Eip712, H160};
26//!
27//! #[derive(Debug, Eip712, EthAbiType)]
28//! #[eip712(
29//!     name = "Radicle",
30//!     version = "1",
31//!     chain_id = 1,
32//!     verifying_contract = "0x0000000000000000000000000000000000000000"
33//!     // salt is an optional parameter
34//!     salt = "my-unique-spice"
35//! )]
36//! pub struct Puzzle {
37//!     pub organization: H160,
38//!     pub contributor: H160,
39//!     pub commit: String,
40//!     pub project: String,
41//! }
42//!
43//! let puzzle = Puzzle {
44//!     organization: "0000000000000000000000000000000000000000"
45//!         .parse::<H160>()
46//!         .expect("failed to parse address"),
47//!     contributor: "0000000000000000000000000000000000000000"
48//!         .parse::<H160>()
49//!         .expect("failed to parse address"),
50//!     commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(),
51//!     project: "radicle-reward".to_string(),
52//! };
53//!
54//! let hash = puzzle.encode_eip712().unwrap();
55//! ```
56//!
57//! # Limitations
58//!
59//! At the moment, the derive macro does not recursively encode nested Eip712 structs.
60//!
61//! There is an Inner helper attribute `#[eip712]` for fields that will eventually be used to
62//! determine if there is a nested eip712 struct. However, this work is not yet complete.
63
64#![deny(missing_docs, unsafe_code, rustdoc::broken_intra_doc_links)]
65use ethers_core::{macros::ethers_core_crate, types::transaction::eip712};
66use proc_macro::TokenStream;
67use quote::quote;
68use std::convert::TryFrom;
69use syn::parse_macro_input;
70
71/// Derive macro for `Eip712`
72#[proc_macro_derive(Eip712, attributes(eip712))]
73pub fn eip_712_derive(input: TokenStream) -> TokenStream {
74    let ast = parse_macro_input!(input);
75
76    impl_eip_712_macro(&ast)
77}
78
79// Main implementation macro, used to compute static values and define
80// method for encoding the final eip712 payload;
81fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream {
82    // Primary type should match the type in the ethereum verifying contract;
83    let primary_type = &ast.ident;
84
85    // Instantiate domain from parsed attributes
86    let domain = match eip712::EIP712Domain::try_from(ast) {
87        Ok(attributes) => attributes,
88        Err(e) => return TokenStream::from(e),
89    };
90
91    let domain_separator = hex::encode(domain.separator());
92
93    //
94    let domain_str = match serde_json::to_string(&domain) {
95        Ok(s) => s,
96        Err(e) => {
97            return TokenStream::from(
98                syn::Error::new(ast.ident.span(), e.to_string()).to_compile_error(),
99            )
100        }
101    };
102
103    // Must parse the AST at compile time.
104    let parsed_fields = match eip712::parse_fields(ast) {
105        Ok(fields) => fields,
106        Err(e) => return TokenStream::from(e),
107    };
108
109    // Compute the type hash for the derived struct using the parsed fields from above.
110    let type_hash =
111        hex::encode(eip712::make_type_hash(primary_type.clone().to_string(), &parsed_fields));
112
113    // Use reference to ethers_core instead of directly using the crate itself.
114    let ethers_core = ethers_core_crate();
115
116    let implementation = quote! {
117        impl Eip712 for #primary_type {
118            type Error = #ethers_core::types::transaction::eip712::Eip712Error;
119
120            fn type_hash() -> Result<[u8; 32], Self::Error> {
121                use std::convert::TryFrom;
122                let decoded = #ethers_core::utils::hex::decode(#type_hash)?;
123                let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
124                Ok(byte_array)
125            }
126
127            // Return the pre-computed domain separator from compile time;
128            fn domain_separator(&self) -> Result<[u8; 32], Self::Error> {
129                use std::convert::TryFrom;
130                let decoded = #ethers_core::utils::hex::decode(#domain_separator)?;
131                let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
132                Ok(byte_array)
133            }
134
135            fn domain(&self) -> Result<#ethers_core::types::transaction::eip712::EIP712Domain, Self::Error> {
136                let domain: #ethers_core::types::transaction::eip712::EIP712Domain = # ethers_core::utils::__serde_json::from_str(#domain_str)?;
137
138                Ok(domain)
139            }
140
141            fn struct_hash(&self) -> Result<[u8; 32], Self::Error> {
142                use #ethers_core::abi::Tokenizable;
143                let mut items = vec![#ethers_core::abi::Token::Uint(
144                    #ethers_core::types::U256::from(&Self::type_hash()?[..]),
145                )];
146
147                if let #ethers_core::abi::Token::Tuple(tokens) = self.clone().into_token() {
148                    for token in tokens {
149                        match &token {
150                            #ethers_core::abi::Token::Tuple(t) => {
151                                // TODO: check for nested Eip712 Type;
152                                // Challenge is determining the type hash
153                                return Err(Self::Error::NestedEip712StructNotImplemented);
154                            },
155                            _ => {
156                                items.push(#ethers_core::types::transaction::eip712::encode_eip712_type(token));
157                            }
158                        }
159                    }
160                }
161
162                let struct_hash = #ethers_core::utils::keccak256(#ethers_core::abi::encode(
163                    &items,
164                ));
165
166                Ok(struct_hash)
167            }
168        }
169    };
170
171    implementation.into()
172}