From 9ec7f2ae2e1266474a7c3dc53fb0b1e463021d03 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 1 Oct 2021 11:02:04 -0700 Subject: [PATCH 01/30] derive-eip712: initial implementation of eip712 derive macro This commit provides an initial implementation for a derive macro to encode typed data according to EIP-712, https://eips.ethereum.org/EIPS/eip-712 Additionally, this commit introduces a new signer trait method: async fn sign_typed_data( &self, payload: &T, ) -> Result; And implements the new method for each of the signers (wallet, ledger, aws). Additionally, these changes include using `WalletError` for the Wallet signer error type At the moment, derive does not recurse the primary type to find nested Eip712 structs. This is something that is noted in the source and currently responds with an error regarding custom types. A subsequent PR should be opened once this issue becomes needed. For the moment, the current implementation should satisfy non-nested, basic struct types. --- Cargo.lock | 42 ++- Cargo.toml | 4 +- derive-eip712/Cargo.toml | 19 + derive-eip712/src/lib.rs | 399 ++++++++++++++++++++ derive-eip712/tests/derive_eip712.rs | 39 ++ ethers-core/src/types/transaction/eip712.rs | 46 +++ ethers-core/src/types/transaction/mod.rs | 1 + ethers-signers/src/aws/mod.rs | 15 + ethers-signers/src/ledger/mod.rs | 10 + ethers-signers/src/lib.rs | 19 +- ethers-signers/src/wallet/mod.rs | 23 +- 11 files changed, 604 insertions(+), 13 deletions(-) create mode 100644 derive-eip712/Cargo.toml create mode 100644 derive-eip712/src/lib.rs create mode 100644 derive-eip712/tests/derive_eip712.rs create mode 100644 ethers-core/src/types/transaction/eip712.rs diff --git a/Cargo.lock b/Cargo.lock index 6a1e0caed..981261861 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -538,6 +538,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "core-foundation" version = "0.9.1" @@ -669,6 +675,21 @@ dependencies = [ "const-oid", ] +[[package]] +name = "derive-eip712" +version = "0.1.0" +dependencies = [ + "convert_case", + "ethers-core", + "hex", + "proc-macro2", + "quote", + "serde", + "serde_json", + "syn", + "thiserror", +] + [[package]] name = "digest" version = "0.8.1" @@ -850,6 +871,7 @@ name = "ethers" version = "0.5.3" dependencies = [ "anyhow", + "derive-eip712", "ethers-contract", "ethers-core", "ethers-middleware", @@ -2019,9 +2041,9 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" dependencies = [ "unicode-xid", ] @@ -2553,9 +2575,9 @@ checksum = "930c0acf610d3fdb5e2ab6213019aaa04e227ebe9547b0649ba599b16d788bd7" [[package]] name = "serde" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] @@ -2572,9 +2594,9 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -2583,9 +2605,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -2816,9 +2838,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.74" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 5862e0dc6..efe34840b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,8 @@ members = [ "ethers-signers", "ethers-core", "ethers-middleware", - "ethers-etherscan" + "ethers-etherscan", + "derive-eip712" ] default-members = [ @@ -78,6 +79,7 @@ ethers-core = { version = "^0.5.0", default-features = false, path = "./ethers-c ethers-providers = { version = "^0.5.0", default-features = false, path = "./ethers-providers" } ethers-signers = { version = "^0.5.0", default-features = false, path = "./ethers-signers" } ethers-middleware = { version = "^0.5.0", default-features = false, path = "./ethers-middleware" } +derive-eip712 = { version = "0.1", path = "./derive-eip712" } [dev-dependencies] ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract", features = ["abigen"] } diff --git a/derive-eip712/Cargo.toml b/derive-eip712/Cargo.toml new file mode 100644 index 000000000..3f5c06d47 --- /dev/null +++ b/derive-eip712/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "derive-eip712" +version = "0.1.0" +edition = "2018" +description = "Custom derive macro for EIP-712 typed data" + +[lib] +proc-macro = true + +[dependencies] +quote = "1.0.9" +syn = "1.0.77" +ethers-core = { path = "../ethers-core" } +convert_case = "0.4.0" +proc-macro2 = "1.0.29" +hex = "0.4.3" +serde = "1.0.130" +serde_json = "1.0.68" +thiserror = "1.0.29" diff --git a/derive-eip712/src/lib.rs b/derive-eip712/src/lib.rs new file mode 100644 index 000000000..d3ca2229b --- /dev/null +++ b/derive-eip712/src/lib.rs @@ -0,0 +1,399 @@ +//! # EIP-712 Derive Macro +//! This crate provides a derive macro `Eip712` that is used to encode a rust struct +//! into a payload hash, according to [https://eips.ethereum.org/EIPS/eip-712](https://eips.ethereum.org/EIPS/eip-712) +//! +//! The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712` +//! Both the derive macro and the trait must be in context when using +//! +//! This derive macro requires the `#[eip712]` attributes to be included +//! for specifying the domain separator used in encoding the hash. +//! +//! All String values returned by the implemented methods are hex encoded and should be +//! decoded into `[u8; 32]` for signing. See example for decoding. +//! +//! # Example Usage +//! +//! ```rust +//! use derive_eip712::*; +//! use ethers_core::types::{transaction::eip712::Eip712, H160}; +//! use serde::Serialize; +//! +//! #[derive(Debug, Eip712, Serialize)] +//! #[eip712( +//! name = "Radicle", +//! version = "1", +//! chain_id = 1, +//! verifying_contract = "0x0000000000000000000000000000000000000000" +//! )] +//! pub struct Puzzle { +//! pub organization: H160, +//! pub contributor: H160, +//! pub commit: String, +//! pub project: String, +//! } +//! +//! let puzzle = Puzzle { +//! organization: "0000000000000000000000000000000000000000" +//! .parse::() +//! .expect("failed to parse address"), +//! contributor: "0000000000000000000000000000000000000000" +//! .parse::() +//! .expect("failed to parse address"), +//! commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(), +//! project: "radicle-reward".to_string(), +//! }; +//! +//! let hash = puzzle.encode_eip712()?; +//! +//! let decoded: Vec = hex::decode(hash).expect("failed to decode") +//! let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; +//! ``` +//! +//! # Limitations +//! +//! At the moment, the derive macro does not recursively encode nested Eip712 structs. +//! + +use std::collections::HashMap; + +use convert_case::{Case, Casing}; +use proc_macro::TokenStream; +use quote::quote; + +use ethers_core::{ + abi, + abi::Token, + types::{Address, H160, U256}, + utils::keccak256, +}; + +/// This method provides the hex encoded domain type hash for EIP712Domain type; +/// This is used by all Eip712 structs. +fn eip712_domain_type_hash() -> [u8; 32] { + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") +} + +/// Eip712 Domain attributes used in determining the domain separator; +#[derive(Debug, Default)] +struct Eip712Domain { + name: String, + version: String, + chain_id: U256, + verifying_contract: Address, +} + +impl Eip712Domain { + // Compute the domain separator; + // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L41 + pub fn separator(&self) -> String { + hex::encode(keccak256(abi::encode(&[ + Token::Uint(U256::from(eip712_domain_type_hash())), + Token::Uint(U256::from(keccak256(&self.name))), + Token::Uint(U256::from(keccak256(&self.version))), + Token::Uint(self.chain_id), + Token::Address(self.verifying_contract), + ]))) + } +} + +// Parse the AST of the struct to determine the domain attributes +impl From<&syn::DeriveInput> for Eip712Domain { + fn from(input: &syn::DeriveInput) -> Eip712Domain { + let mut domain = Eip712Domain::default(); + + let attributes = input.attrs.first().expect("missing macro arguments"); + + let is_segment_valid = attributes + .path + .segments + .first() + .map(|s| s.ident == "eip712") + .expect("missing eip712 macro arguments"); + + if !is_segment_valid { + panic!("invalid path segment, identity does not match 'eip712'") + } + + let mut token_stream = attributes.tokens.clone().into_iter(); + + if let Some(quote::__private::TokenTree::Group(g)) = token_stream.next() { + let group_stream = g.stream().into_iter(); + let mut current_arg = String::new(); + for item in group_stream { + if let quote::__private::TokenTree::Ident(ident) = item { + current_arg = ident.to_string(); + } else if let quote::__private::TokenTree::Literal(literal) = item { + match current_arg.as_ref() { + "name" => { + domain.name = literal.to_string().replace("\"", ""); + } + "version" => { + domain.version = literal.to_string().replace("\"", ""); + } + "chain_id" => { + domain.chain_id = literal + .to_string() + .parse::() + .expect("failed to parse chain id from macro arguments"); + } + "verifying_contract" => { + domain.verifying_contract = literal + .to_string() + .replace("\"", "") + .parse::() + .expect("failed to parse verifying contract"); + } + _ => { + panic!("expected arguments: 'name', 'version', 'chain_id' and 'verifying_contract'; found: {}", current_arg); + } + } + } + } + }; + + domain + } +} + +// Convert rust types to enc types. This is used in determining the type hash; +// NOTE: this is not an exhaustive list +fn parse_field_type(field_type: String) -> String { + match field_type.as_ref() { + "U128" => "uint128", + "U256" => "uint256", + "H128" => "bytes16", + "H160" => "address", + "H256" => "bytes32", + "String" => "string", + "Bytes" => "bytes", + "Vec" => "bytes", + "Vec" => "uint128[]", + "Vec" => "uint256[]", + "Vec "bytes16[]", + "Vec" => "address[]", + "Vec "bytes32[]", + "Vec" => "string[]", + "Vec" => "bytes[]", + _ => { + // NOTE: This will fail if the field type does not match an ethereum type; + &field_type + } + } + .to_string() +} + +// Parse the field type from the derived struct +fn parse_field(field: &syn::Field) -> String { + let field_path = match &field.ty { + syn::Type::Path(p) => p, + _ => { + panic!("field type must be a path") + } + }; + + let segment = field_path + .path + .segments + .first() + .expect("field must have a type"); + + let mut field_type = segment.ident.to_string(); + + if let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments { + field_type.push('<'); + for arg in &arguments.args { + if let syn::GenericArgument::Type(syn::Type::Path(p)) = arg { + let arg_identity = p + .path + .segments + .first() + .map(|s| s.ident.to_string()) + .expect("argument must have an identity"); + + field_type.push_str(&arg_identity); + } + } + field_type.push('>'); + } + + parse_field_type(field_type) +} + +// Return HashMap of the field name and the field type; +fn parse_fields(ast: &syn::DeriveInput) -> HashMap { + let mut parsed_fields = HashMap::new(); + + let data = match &ast.data { + syn::Data::Struct(s) => s, + _ => { + panic!("Eip712 can only be derived for a struct") + } + }; + + let named_fields = match &data.fields { + syn::Fields::Named(name) => name, + _ => { + panic!("unnamed fields are not supported") + } + }; + + named_fields.named.iter().for_each(|f| { + let field_name = f + .ident + .clone() + .expect("field must be named") + .to_string() + .to_case(Case::Camel); + + let field_type = parse_field(f); + + parsed_fields.insert(field_name, field_type); + }); + + parsed_fields +} + +// Convert hash map of field names and types into a type hash corresponding to enc types; +fn make_type_hash(primary_type: String, fields: &HashMap) -> String { + let parameters = fields + .iter() + .map(|(k, v)| format!("{} {}", v, k)) + .collect::>() + .join(","); + + let sig = format!("{}({})", primary_type, parameters); + + hex::encode(keccak256(sig)) +} + +// Main implementation macro, used to compute static values and define +// method for encoding the final eip712 payload; +fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { + // Primary type should match the type in the ethereum verifying contract; + let primary_type = &ast.ident; + + // Computer domain separator + let domain_attributes: Eip712Domain = Eip712Domain::from(ast); + let domain_separator = domain_attributes.separator(); + let domain_type_hash = hex::encode(eip712_domain_type_hash()); + + // Must parse the AST at compile time. + let parsed_fields = parse_fields(ast); + + // JSON Stringify the field names and types to pass into the + // derived encode_eip712() method as a static string; + // the AST of the struct is not available at runtime, so this is + // a work around for passing in the struct fields; + let fields: String = serde_json::to_string(&parsed_fields) + .expect("failed to serialize parsed fields into JSON string"); + + // Compute the type hash for the derived struct using the parsed fields from above; + let type_hash = make_type_hash(primary_type.clone().to_string(), &parsed_fields); + + let implementation = quote! { + #[derive(Debug, thiserror::Error)] + pub enum Eip712Error { + #[error("Failed to serialize serde JSON object")] + SerdeJsonError(#[from] serde_json::Error), + #[error("Failed to decode hex value")] + FromHexError(#[from] hex::FromHexError), + #[error("Failed to make struct hash from values")] + FailedToEncodeStruct + } + + fn make_struct_hash( + data: &T, + domain_separator: &'static str, + type_hash: &'static str, + _fields: &'static str, + ) -> Result { + let _fields: serde_json::Value = serde_json::from_str(_fields)?; + + if let serde_json::Value::Object(fields) = _fields { + let mut keys = fields.keys().map(|f| f.to_string()).collect::>(); + + // sort the fields alphabetically; + // NOTE: the solidity type hash should also use the same convention; + keys.sort(); + + let _values: serde_json::Value = serde_json::to_value(data)?; + + if let serde_json::Value::Object(obj) = _values { + // Initialize the items with the type hash + let mut items = vec![ethers_core::abi::Token::Uint( + ethers_core::types::U256::from(&hex::decode(type_hash)?[..]), + )]; + + for key in keys { + if let Some(v) = obj.get(&key) { + if let Some(ty) = fields.get(&key) { + if let serde_json::Value::String(value) = v{ + if let serde_json::Value::String(field_type) = ty { + // convert encoded type; + let item = match field_type.as_ref() { + // TODO: This following enc types are not exhaustive; + // Check types against solidity abi.encodePacked() + "uint128" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), + "uint256" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), + "address" => ethers_core::abi::Token::Address(value.parse::().expect("failed to parse address")), + _ => { ethers_core::abi::Token::Uint(ethers_core::types::U256::from(ethers_core::utils::keccak256(value))) } + }; + + // Add the parsed field type to the items to be encoded; + items.push(item); + } + } + } + } + } + + let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode( + &items, + )); + + // encode the digest to be compatible with solidity abi.encodePacked() + // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 + let digest_input = [ + &[0x19, 0x01], + &hex::decode(domain_separator)?[..], + &struct_hash[..] + ].concat(); + + return Ok(hex::encode(ethers_core::utils::keccak256(digest_input))); + } + } + + // Reached Error: + Err(Eip712Error::FailedToEncodeStruct) + } + + impl Eip712 for #primary_type { + type Error = Eip712Error; + + fn type_hash() -> String { + #type_hash.to_string() + } + + fn domain_separator() -> String { + #domain_separator.to_string() + } + + fn encode_eip712(&self) -> Result { + Ok(make_struct_hash(self, #domain_separator, #type_hash, #fields)?.to_string()) + } + + fn eip712_domain_type_hash() -> String { + #domain_type_hash.to_string() + } + } + }; + + implementation.into() +} + +#[proc_macro_derive(Eip712, attributes(eip712))] +pub fn eip_712_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).expect("failed to parse token stream for Eip712 derived struct"); + + impl_eip_712_macro(&ast) +} diff --git a/derive-eip712/tests/derive_eip712.rs b/derive-eip712/tests/derive_eip712.rs new file mode 100644 index 000000000..ba2979273 --- /dev/null +++ b/derive-eip712/tests/derive_eip712.rs @@ -0,0 +1,39 @@ +use derive_eip712::*; +use ethers_core::types::{transaction::eip712::Eip712, H160}; +use serde::Serialize; + +#[derive(Debug, Eip712, Serialize)] +#[eip712( + name = "Radicle", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000000" +)] +pub struct Puzzle { + pub organization: H160, + pub contributor: H160, + pub commit: String, + pub project: String, +} + +#[test] +fn test_derive_eip712() { + let puzzle = Puzzle { + organization: "0000000000000000000000000000000000000000" + .parse::() + .expect("failed to parse address"), + contributor: "0000000000000000000000000000000000000000" + .parse::() + .expect("failed to parse address"), + commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(), + project: "radicle-reward".to_string(), + }; + + let hash = puzzle.encode_eip712().expect("failed to encode struct"); + + // TODO: Compare against solidity computed hash + + println!("Hash: {:?}", hash); + + assert_eq!(hash.len(), 64) +} diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs new file mode 100644 index 000000000..ab8a5a51f --- /dev/null +++ b/ethers-core/src/types/transaction/eip712.rs @@ -0,0 +1,46 @@ +/// The Eip712 trait provides helper methods for computing +/// the typed data hash used in `eth_signTypedData`. +/// +/// The ethers-rs `derive_eip712` crate provides a derive macro to +/// implement the trait for a given struct. See documentation +/// for `derive_eip712` for more information and example usage. +/// +/// For those who wish to manually implement this trait, see: +/// https://eips.ethereum.org/EIPS/eip-712 +/// +/// Any rust struct implementing Eip712 must also have a corresponding +/// struct in the verifying ethereum contract that matches its signature. +/// +/// NOTE: Due to limitations of the derive macro not supporting return types of +/// [u8; 32] or Vec, all methods should return the hex encoded values of the keccak256 +/// byte array. +pub trait Eip712 { + /// User defined error type; + type Error: std::error::Error + Send + Sync + std::fmt::Debug; + + /// The eip712 domain is the same for all Eip712 implementations, + /// This method does not need to be manually implemented, but may be overridden + /// if needed. + fn eip712_domain_type_hash() -> String { + hex::encode(crate::utils::keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)", + )) + } + + /// The domain separator depends on the contract and unique domain + /// for which the user is targeting. In the derive macro, these attributes + /// are passed in as arguments to the macro. When manually deriving, the user + /// will need to know the name of the domain, version of the contract, chain ID of + /// where the contract lives and the address of the verifying contract. + fn domain_separator() -> String; + + /// This method is used for calculating the hash of the type signature of the + /// struct. The field types of the struct must map to primitive + /// ethereum types or custom types defined in the contract. + fn type_hash() -> String; + + /// When using the derive macro, this is the primary method used for computing the final + /// EIP-712 encoded payload. This method relies on the aforementioned methods for computing + /// the final encoded payload. + fn encode_eip712(&self) -> Result; +} diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs index 51c5f9c9c..f4594437c 100644 --- a/ethers-core/src/types/transaction/mod.rs +++ b/ethers-core/src/types/transaction/mod.rs @@ -4,6 +4,7 @@ pub mod response; pub mod eip1559; pub mod eip2718; pub mod eip2930; +pub mod eip712; pub(crate) const BASE_NUM_TX_FIELDS: usize = 9; diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index f57f0b115..0aeecd201 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -83,6 +83,9 @@ pub enum AwsSignerError { Spki(spki::der::Error), #[error("{0}")] Other(String), + #[error(transparent)] + /// Error when converting from a hex string + HexError(#[from] hex::FromHexError), } impl From for AwsSignerError { @@ -245,6 +248,18 @@ impl<'a> super::Signer for AwsSigner<'a> { self.sign_digest_with_eip155(sighash).await } + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result, Self::Error> { + let decoded = hex::decode(payload.encode_eip712())?; + let hash = <[u8; 32]>::try_from(&decoded[..])?; + + let digest = self.sign_digest_with_eip155(hash.into()); + + Ok(Some(digest)) + } + fn address(&self) -> Address { self.address } diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 52e2cfe66..abf76ace8 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -25,6 +25,16 @@ impl Signer for LedgerEthereum { self.sign_tx(message).await } + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result, Self::Error> { + let decoded = hex::decode(payload.encode_eip712())?; + let hash = <[u8; 32]>::try_from(&decoded[..])?; + + Ok(Some(self.sign_hash(hash.into(), false))) + } + /// Returns the signer's Ethereum Address fn address(&self) -> Address { self.address diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 3af7a2e46..46ace1160 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -69,7 +69,9 @@ mod aws; pub use aws::{AwsSigner, AwsSignerError}; use async_trait::async_trait; -use ethers_core::types::{transaction::eip2718::TypedTransaction, Address, Signature}; +use ethers_core::types::{ + transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, +}; use std::error::Error; /// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) @@ -93,6 +95,21 @@ pub trait Signer: std::fmt::Debug + Send + Sync { /// Signs the transaction async fn sign_transaction(&self, message: &TypedTransaction) -> Result; + /// Encodes and signs the typed data according EIP-712 + /// payload must implement Eip712 trait. + /// + /// NOTE: Returning is Option instead of Signature + /// due to use of std::convert::Infallible as an error type + /// in the Wallet implementation, which conflicts with + /// the error types used within the method, which cannot be mapped + /// to Infallible type. To avoid breaking changes and changing the error + /// type, this method returns `None` if there is an error that restricts + /// the payload from being encoded, and signed. + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result, Self::Error>; + /// Returns the signer's Ethereum Address fn address(&self) -> Address; diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index 1f5cb788b..a3da5f9f2 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -16,12 +16,16 @@ use ethers_core::{ elliptic_curve::FieldBytes, Secp256k1, }, - types::{transaction::eip2718::TypedTransaction, Address, Signature, H256, U256}, + types::{ + transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, + H256, U256, + }, utils::hash_message, }; use hash::Sha256Proxy; use async_trait::async_trait; +use std::convert::TryFrom; use std::fmt; /// An Ethereum private-public key pair which can be used for signing messages. @@ -83,6 +87,23 @@ impl> Signer fo Ok(self.sign_transaction_sync(tx)) } + async fn sign_typed_data( + &self, + payload: &T, + ) -> Result, Self::Error> { + if let Ok(encoded) = payload.encode_eip712() { + if let Some(decoded) = hex::decode(encoded).ok() { + if let Some(hash) = <[u8; 32]>::try_from(&decoded[..]).ok() { + return Ok(Some(self.sign_hash(hash.into(), false))); + } + } + } + + // Returning none here due to use of infallible error; + // The wallet signing should return an error rather than infallible or None. + Ok(None) + } + fn address(&self) -> Address { self.address } From 61bacb309551e0a18ea90d081ea0bf1753bc97c3 Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 1 Oct 2021 16:27:33 -0700 Subject: [PATCH 02/30] rename to ethers-derive-eip712; move to ethers-core --- Cargo.lock | 226 +++++----- Cargo.toml | 5 +- derive-eip712/src/lib.rs | 399 ------------------ ethers-core/Cargo.toml | 3 + .../ethers-derive-eip712}/Cargo.toml | 7 +- ethers-core/ethers-derive-eip712/src/lib.rs | 176 ++++++++ .../tests/derive_eip712.rs | 2 +- ethers-core/src/types/transaction/eip712.rs | 230 ++++++++++ 8 files changed, 522 insertions(+), 526 deletions(-) delete mode 100644 derive-eip712/src/lib.rs rename {derive-eip712 => ethers-core/ethers-derive-eip712}/Cargo.toml (63%) create mode 100644 ethers-core/ethers-derive-eip712/src/lib.rs rename {derive-eip712 => ethers-core/ethers-derive-eip712}/tests/derive_eip712.rs (97%) diff --git a/Cargo.lock b/Cargo.lock index 981261861..162e1430d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3e798aa0c8239776f54415bc06f3d74b1850f3f830b45c35cfc80556973f70" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array 0.14.4", "rand_core 0.6.3", @@ -39,13 +39,13 @@ dependencies = [ [[package]] name = "aes" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if 1.0.0", "cipher", - "cpufeatures 0.1.5", + "cpufeatures", "opaque-debug 0.3.0", ] @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "arrayref" @@ -191,9 +191,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" +checksum = "40a96587c05c810ddbb79e2674d519cff1379517e7b91d166dff7a7cc0e9af6e" [[package]] name = "bech32" @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "blake2" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" dependencies = [ "crypto-mac 0.8.0", "digest 0.9.0", @@ -315,15 +315,15 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" [[package]] name = "byte-slice-cast" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c1bf4a04a88c54f589125563643d773f3254b5c38571395e2b591c693bbc81" +checksum = "ca0796d76a983651b4a0ddda16203032759f2fd9103d9181f7c65c06ee8872e6" [[package]] name = "byte-tools" @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "ccm" @@ -522,9 +522,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" +checksum = "fdab415d6744056100f40250a66bc430c1a46f7a02e20bc11c94c79a0f0464df" [[package]] name = "const_fn" @@ -560,15 +560,6 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" -[[package]] -name = "cpufeatures" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.2.1" @@ -595,9 +586,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "crypto-bigint" -version = "0.2.4" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc209804a22c34a98fe26a32d997ac64d4284816f65cf1a529c4e31a256218a0" +checksum = "d12477e115c0d570c12a2dfd859f80b55b60ddb5075df210d3af06d133a69f45" dependencies = [ "generic-array 0.14.4", "rand_core 0.6.3", @@ -668,28 +659,13 @@ dependencies = [ [[package]] name = "der" -version = "0.4.1" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e21d2d0f22cde6e88694108429775c0219760a07779bf96503b434a03d7412" +checksum = "2adca118c71ecd9ae094d4b68257b3fdfcb711a612b9eec7b5a0d27a5a70a5b4" dependencies = [ "const-oid", ] -[[package]] -name = "derive-eip712" -version = "0.1.0" -dependencies = [ - "convert_case", - "ethers-core", - "hex", - "proc-macro2", - "quote", - "serde", - "serde_json", - "syn", - "thiserror", -] - [[package]] name = "digest" version = "0.8.1" @@ -841,9 +817,9 @@ dependencies = [ [[package]] name = "ethbloom" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779864b9c7f7ead1f092972c3257496c6a84b46dba2ce131dd8a282cb2cc5972" +checksum = "bfb684ac8fa8f6c5759f788862bb22ec6fe3cb392f6bfd08e3c64b603661e3f8" dependencies = [ "crunchy", "fixed-hash", @@ -854,9 +830,9 @@ dependencies = [ [[package]] name = "ethereum-types" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd6bde671199089e601e8d47e153368b893ef885f11f365a3261ec58153c211" +checksum = "05136f7057fe789f06e6d41d07b34e6f70d8c86e5693b60f97aaa6553553bdaf" dependencies = [ "ethbloom", "fixed-hash", @@ -871,7 +847,6 @@ name = "ethers" version = "0.5.3" dependencies = [ "anyhow", - "derive-eip712", "ethers-contract", "ethers-core", "ethers-middleware", @@ -943,6 +918,7 @@ dependencies = [ "arrayvec 0.7.1", "bincode", "bytes", + "convert_case", "ecdsa", "elliptic-curve", "ethabi", @@ -953,17 +929,31 @@ dependencies = [ "hex-literal", "k256", "once_cell", + "quote", "rand 0.8.4", "rlp", "rlp-derive", "semver 1.0.4", "serde", "serde_json", + "syn", "thiserror", "tiny-keccak", "tokio", ] +[[package]] +name = "ethers-derive-eip712" +version = "0.1.0" +dependencies = [ + "ethers-core", + "hex", + "quote", + "serde", + "serde_json", + "syn", +] + [[package]] name = "ethers-etherscan" version = "0.1.0" @@ -1129,9 +1119,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "futures" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" dependencies = [ "futures-channel", "futures-core", @@ -1295,9 +1285,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "825343c4eef0b63f541f8903f395dc5beb362a979b5799a84062527ef1e37726" +checksum = "6c06815895acec637cd6ed6e9662c935b866d20a106f8361892893a7d9234964" dependencies = [ "bytes", "fnv", @@ -1368,9 +1358,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -1402,9 +1392,9 @@ checksum = "6456b8a6c8f33fee7d958fcd1b60d55b11940a79e63ae87013e6d22e26034440" [[package]] name = "hyper" -version = "0.14.11" +version = "0.14.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +checksum = "15d1cfb9e4f68655fa04c01f59edb405b6074a0f7118ea881e5026e4a1cd8593" dependencies = [ "bytes", "futures-channel", @@ -1532,9 +1522,9 @@ checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "js-sys" @@ -1572,9 +1562,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.101" +version = "0.2.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb00336871be5ed2c8ed44b60ae9959dc5b9f08539422ed43f09e34ecaeba21" +checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" [[package]] name = "libusb1-sys" @@ -1747,9 +1737,9 @@ dependencies = [ [[package]] name = "object" -version = "0.26.1" +version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2766204889d09937d00bfbb7fec56bb2a199e2ade963cab19185d8a6104c7c" +checksum = "39f37e50073ccad23b6d09bcb5b263f4e76d3bb6038e4a3c08e52162ffa8abc2" dependencies = [ "memchr", ] @@ -1794,9 +1784,9 @@ checksum = "28988d872ab76095a6e6ac88d99b54fd267702734fd7ffe610ca27f533ddb95a" [[package]] name = "openssl-sys" -version = "0.9.66" +version = "0.9.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1996d2d305e561b70d1ee0c53f1542833f4e1ac6ce9a6708b6ff2738ca67dc82" +checksum = "69df2d8dfc6ce3aaf44b40dec6f487d5a886516cf6879c49e98e0710f310a058" dependencies = [ "autocfg", "cc", @@ -1828,9 +1818,9 @@ dependencies = [ [[package]] name = "parity-scale-codec" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8975095a2a03bbbdc70a74ab11a4f76a6d0b84680d87c68d722531b0ac28e8a9" +checksum = "373b1a4c1338d9cd3d1fa53b3a11bdab5ab6bd80a20f7f7becd76953ae2be909" dependencies = [ "arrayvec 0.7.1", "bitvec 0.20.4", @@ -1842,9 +1832,9 @@ dependencies = [ [[package]] name = "parity-scale-codec-derive" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40dbbfef7f0a1143c5b06e0d76a6278e25dac0bc1af4be51a0fbb73f07e7ad09" +checksum = "1557010476e0595c9b568d16dcfb81b93cdeb157612726f5170d31aa707bed27" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1879,9 +1869,9 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd482dfb8cfba5a93ec0f91e1c0f66967cb2fdc1a8dba646c4f9202c5d05d785" +checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" dependencies = [ "base64ct", "rand_core 0.6.3", @@ -1960,9 +1950,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkcs8" -version = "0.7.5" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbee84ed13e44dd82689fa18348a49934fa79cc774a344c42fc9b301c71b140a" +checksum = "ee3ef9b64d26bad0536099c816c6734379e45bbd5f14798def6809e5cc350447" dependencies = [ "der", "spki", @@ -1970,9 +1960,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "7c9b1041b4387893b91ee6746cddfc28516aff326a3519fb2adf820932c5e6cb" [[package]] name = "ppv-lite86" @@ -1995,9 +1985,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ "thiserror", "toml", @@ -2050,9 +2040,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -2385,9 +2375,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dead70b0b5e03e9c814bcb6b01e03e68f7c57a80aa48c72ec92152ab3e818d49" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "rustc-hex" @@ -2504,9 +2494,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.3.1" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" dependencies = [ "bitflags", "core-foundation", @@ -2517,9 +2507,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.3.0" +version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4effb91b4b8b6fb7732e670b6cee160278ff8e6bf485c7805d9e319d76e284" +checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" dependencies = [ "core-foundation-sys", "libc", @@ -2628,13 +2618,13 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.1.5", + "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", ] @@ -2665,7 +2655,7 @@ checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa" dependencies = [ "block-buffer 0.9.0", "cfg-if 1.0.0", - "cpufeatures 0.2.1", + "cpufeatures", "digest 0.9.0", "opaque-debug 0.3.0", ] @@ -2693,9 +2683,9 @@ dependencies = [ [[package]] name = "shlex" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a568c8f2cd051a4d283bd6eb0343ac214c1b0f1ac19f93e1175b2dee38c73d" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" [[package]] name = "signal-hook-registry" @@ -2737,15 +2727,15 @@ checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", "winapi", @@ -2838,9 +2828,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.77" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5239bc68e0fef57495900cfea4e8dc75596d9a319d7e16b1e0a440d24e6fe0a0" +checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0" dependencies = [ "proc-macro2", "quote", @@ -2967,9 +2957,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -2982,9 +2972,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" dependencies = [ "autocfg", "bytes", @@ -3001,9 +2991,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb" dependencies = [ "proc-macro2", "quote", @@ -3093,9 +3083,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.16" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98863d0dd09fa59a1b79c6750ad80dbda6b75f4e71c437a6a1a8cb91a8bcbd77" +checksum = "f4f480b8f81512e825f337ad51e94c1eb5d3bbdf2b363dcd01e2b19a9ffe3f8e" dependencies = [ "proc-macro2", "quote", @@ -3104,9 +3094,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46125608c26121c81b0c6d693eab5a420e416da7e43c426d2e8f7df8da8a3acf" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ "lazy_static", ] @@ -3144,9 +3134,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.2.24" +version = "0.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd0568dbfe3baf7048b7908d2b32bca0d81cd56bec6d2a8f894b01d74f86be3" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" dependencies = [ "ansi_term", "chrono", @@ -3195,9 +3185,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "ucd-trie" @@ -3523,18 +3513,18 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.4.1" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "377db0846015f7ae377174787dd452e1c5f5a9050bc6f954911d01f116daa0cd" +checksum = "bf68b08513768deaa790264a7fac27a58cbf2705cfcdc9448362229217d7e970" dependencies = [ "zeroize_derive", ] [[package]] name = "zeroize_derive" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2c1e130bebaeab2f23886bf9acbaca14b092408c452543c857f66399cd6dab1" +checksum = "bdff2024a851a322b08f179173ae2ba620445aef1e838f0c196820eade4ae0c7" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index efe34840b..6bb43debd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ "ethers-core", "ethers-middleware", "ethers-etherscan", - "derive-eip712" + "ethers-core/ethers-derive-eip712", ] default-members = [ @@ -34,6 +34,7 @@ default-members = [ exclude = [ "examples/ethers-wasm", + "ethers-core/derive-eip712" ] [package.metadata.docs.rs] @@ -72,14 +73,12 @@ yubi = ["ethers-signers/yubi"] ## contracts abigen = ["ethers-contract/abigen"] - [dependencies] ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract" } ethers-core = { version = "^0.5.0", default-features = false, path = "./ethers-core", features = ["setup"] } ethers-providers = { version = "^0.5.0", default-features = false, path = "./ethers-providers" } ethers-signers = { version = "^0.5.0", default-features = false, path = "./ethers-signers" } ethers-middleware = { version = "^0.5.0", default-features = false, path = "./ethers-middleware" } -derive-eip712 = { version = "0.1", path = "./derive-eip712" } [dev-dependencies] ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract", features = ["abigen"] } diff --git a/derive-eip712/src/lib.rs b/derive-eip712/src/lib.rs deleted file mode 100644 index d3ca2229b..000000000 --- a/derive-eip712/src/lib.rs +++ /dev/null @@ -1,399 +0,0 @@ -//! # EIP-712 Derive Macro -//! This crate provides a derive macro `Eip712` that is used to encode a rust struct -//! into a payload hash, according to [https://eips.ethereum.org/EIPS/eip-712](https://eips.ethereum.org/EIPS/eip-712) -//! -//! The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712` -//! Both the derive macro and the trait must be in context when using -//! -//! This derive macro requires the `#[eip712]` attributes to be included -//! for specifying the domain separator used in encoding the hash. -//! -//! All String values returned by the implemented methods are hex encoded and should be -//! decoded into `[u8; 32]` for signing. See example for decoding. -//! -//! # Example Usage -//! -//! ```rust -//! use derive_eip712::*; -//! use ethers_core::types::{transaction::eip712::Eip712, H160}; -//! use serde::Serialize; -//! -//! #[derive(Debug, Eip712, Serialize)] -//! #[eip712( -//! name = "Radicle", -//! version = "1", -//! chain_id = 1, -//! verifying_contract = "0x0000000000000000000000000000000000000000" -//! )] -//! pub struct Puzzle { -//! pub organization: H160, -//! pub contributor: H160, -//! pub commit: String, -//! pub project: String, -//! } -//! -//! let puzzle = Puzzle { -//! organization: "0000000000000000000000000000000000000000" -//! .parse::() -//! .expect("failed to parse address"), -//! contributor: "0000000000000000000000000000000000000000" -//! .parse::() -//! .expect("failed to parse address"), -//! commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(), -//! project: "radicle-reward".to_string(), -//! }; -//! -//! let hash = puzzle.encode_eip712()?; -//! -//! let decoded: Vec = hex::decode(hash).expect("failed to decode") -//! let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; -//! ``` -//! -//! # Limitations -//! -//! At the moment, the derive macro does not recursively encode nested Eip712 structs. -//! - -use std::collections::HashMap; - -use convert_case::{Case, Casing}; -use proc_macro::TokenStream; -use quote::quote; - -use ethers_core::{ - abi, - abi::Token, - types::{Address, H160, U256}, - utils::keccak256, -}; - -/// This method provides the hex encoded domain type hash for EIP712Domain type; -/// This is used by all Eip712 structs. -fn eip712_domain_type_hash() -> [u8; 32] { - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") -} - -/// Eip712 Domain attributes used in determining the domain separator; -#[derive(Debug, Default)] -struct Eip712Domain { - name: String, - version: String, - chain_id: U256, - verifying_contract: Address, -} - -impl Eip712Domain { - // Compute the domain separator; - // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L41 - pub fn separator(&self) -> String { - hex::encode(keccak256(abi::encode(&[ - Token::Uint(U256::from(eip712_domain_type_hash())), - Token::Uint(U256::from(keccak256(&self.name))), - Token::Uint(U256::from(keccak256(&self.version))), - Token::Uint(self.chain_id), - Token::Address(self.verifying_contract), - ]))) - } -} - -// Parse the AST of the struct to determine the domain attributes -impl From<&syn::DeriveInput> for Eip712Domain { - fn from(input: &syn::DeriveInput) -> Eip712Domain { - let mut domain = Eip712Domain::default(); - - let attributes = input.attrs.first().expect("missing macro arguments"); - - let is_segment_valid = attributes - .path - .segments - .first() - .map(|s| s.ident == "eip712") - .expect("missing eip712 macro arguments"); - - if !is_segment_valid { - panic!("invalid path segment, identity does not match 'eip712'") - } - - let mut token_stream = attributes.tokens.clone().into_iter(); - - if let Some(quote::__private::TokenTree::Group(g)) = token_stream.next() { - let group_stream = g.stream().into_iter(); - let mut current_arg = String::new(); - for item in group_stream { - if let quote::__private::TokenTree::Ident(ident) = item { - current_arg = ident.to_string(); - } else if let quote::__private::TokenTree::Literal(literal) = item { - match current_arg.as_ref() { - "name" => { - domain.name = literal.to_string().replace("\"", ""); - } - "version" => { - domain.version = literal.to_string().replace("\"", ""); - } - "chain_id" => { - domain.chain_id = literal - .to_string() - .parse::() - .expect("failed to parse chain id from macro arguments"); - } - "verifying_contract" => { - domain.verifying_contract = literal - .to_string() - .replace("\"", "") - .parse::() - .expect("failed to parse verifying contract"); - } - _ => { - panic!("expected arguments: 'name', 'version', 'chain_id' and 'verifying_contract'; found: {}", current_arg); - } - } - } - } - }; - - domain - } -} - -// Convert rust types to enc types. This is used in determining the type hash; -// NOTE: this is not an exhaustive list -fn parse_field_type(field_type: String) -> String { - match field_type.as_ref() { - "U128" => "uint128", - "U256" => "uint256", - "H128" => "bytes16", - "H160" => "address", - "H256" => "bytes32", - "String" => "string", - "Bytes" => "bytes", - "Vec" => "bytes", - "Vec" => "uint128[]", - "Vec" => "uint256[]", - "Vec "bytes16[]", - "Vec" => "address[]", - "Vec "bytes32[]", - "Vec" => "string[]", - "Vec" => "bytes[]", - _ => { - // NOTE: This will fail if the field type does not match an ethereum type; - &field_type - } - } - .to_string() -} - -// Parse the field type from the derived struct -fn parse_field(field: &syn::Field) -> String { - let field_path = match &field.ty { - syn::Type::Path(p) => p, - _ => { - panic!("field type must be a path") - } - }; - - let segment = field_path - .path - .segments - .first() - .expect("field must have a type"); - - let mut field_type = segment.ident.to_string(); - - if let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments { - field_type.push('<'); - for arg in &arguments.args { - if let syn::GenericArgument::Type(syn::Type::Path(p)) = arg { - let arg_identity = p - .path - .segments - .first() - .map(|s| s.ident.to_string()) - .expect("argument must have an identity"); - - field_type.push_str(&arg_identity); - } - } - field_type.push('>'); - } - - parse_field_type(field_type) -} - -// Return HashMap of the field name and the field type; -fn parse_fields(ast: &syn::DeriveInput) -> HashMap { - let mut parsed_fields = HashMap::new(); - - let data = match &ast.data { - syn::Data::Struct(s) => s, - _ => { - panic!("Eip712 can only be derived for a struct") - } - }; - - let named_fields = match &data.fields { - syn::Fields::Named(name) => name, - _ => { - panic!("unnamed fields are not supported") - } - }; - - named_fields.named.iter().for_each(|f| { - let field_name = f - .ident - .clone() - .expect("field must be named") - .to_string() - .to_case(Case::Camel); - - let field_type = parse_field(f); - - parsed_fields.insert(field_name, field_type); - }); - - parsed_fields -} - -// Convert hash map of field names and types into a type hash corresponding to enc types; -fn make_type_hash(primary_type: String, fields: &HashMap) -> String { - let parameters = fields - .iter() - .map(|(k, v)| format!("{} {}", v, k)) - .collect::>() - .join(","); - - let sig = format!("{}({})", primary_type, parameters); - - hex::encode(keccak256(sig)) -} - -// Main implementation macro, used to compute static values and define -// method for encoding the final eip712 payload; -fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { - // Primary type should match the type in the ethereum verifying contract; - let primary_type = &ast.ident; - - // Computer domain separator - let domain_attributes: Eip712Domain = Eip712Domain::from(ast); - let domain_separator = domain_attributes.separator(); - let domain_type_hash = hex::encode(eip712_domain_type_hash()); - - // Must parse the AST at compile time. - let parsed_fields = parse_fields(ast); - - // JSON Stringify the field names and types to pass into the - // derived encode_eip712() method as a static string; - // the AST of the struct is not available at runtime, so this is - // a work around for passing in the struct fields; - let fields: String = serde_json::to_string(&parsed_fields) - .expect("failed to serialize parsed fields into JSON string"); - - // Compute the type hash for the derived struct using the parsed fields from above; - let type_hash = make_type_hash(primary_type.clone().to_string(), &parsed_fields); - - let implementation = quote! { - #[derive(Debug, thiserror::Error)] - pub enum Eip712Error { - #[error("Failed to serialize serde JSON object")] - SerdeJsonError(#[from] serde_json::Error), - #[error("Failed to decode hex value")] - FromHexError(#[from] hex::FromHexError), - #[error("Failed to make struct hash from values")] - FailedToEncodeStruct - } - - fn make_struct_hash( - data: &T, - domain_separator: &'static str, - type_hash: &'static str, - _fields: &'static str, - ) -> Result { - let _fields: serde_json::Value = serde_json::from_str(_fields)?; - - if let serde_json::Value::Object(fields) = _fields { - let mut keys = fields.keys().map(|f| f.to_string()).collect::>(); - - // sort the fields alphabetically; - // NOTE: the solidity type hash should also use the same convention; - keys.sort(); - - let _values: serde_json::Value = serde_json::to_value(data)?; - - if let serde_json::Value::Object(obj) = _values { - // Initialize the items with the type hash - let mut items = vec![ethers_core::abi::Token::Uint( - ethers_core::types::U256::from(&hex::decode(type_hash)?[..]), - )]; - - for key in keys { - if let Some(v) = obj.get(&key) { - if let Some(ty) = fields.get(&key) { - if let serde_json::Value::String(value) = v{ - if let serde_json::Value::String(field_type) = ty { - // convert encoded type; - let item = match field_type.as_ref() { - // TODO: This following enc types are not exhaustive; - // Check types against solidity abi.encodePacked() - "uint128" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), - "uint256" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), - "address" => ethers_core::abi::Token::Address(value.parse::().expect("failed to parse address")), - _ => { ethers_core::abi::Token::Uint(ethers_core::types::U256::from(ethers_core::utils::keccak256(value))) } - }; - - // Add the parsed field type to the items to be encoded; - items.push(item); - } - } - } - } - } - - let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode( - &items, - )); - - // encode the digest to be compatible with solidity abi.encodePacked() - // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 - let digest_input = [ - &[0x19, 0x01], - &hex::decode(domain_separator)?[..], - &struct_hash[..] - ].concat(); - - return Ok(hex::encode(ethers_core::utils::keccak256(digest_input))); - } - } - - // Reached Error: - Err(Eip712Error::FailedToEncodeStruct) - } - - impl Eip712 for #primary_type { - type Error = Eip712Error; - - fn type_hash() -> String { - #type_hash.to_string() - } - - fn domain_separator() -> String { - #domain_separator.to_string() - } - - fn encode_eip712(&self) -> Result { - Ok(make_struct_hash(self, #domain_separator, #type_hash, #fields)?.to_string()) - } - - fn eip712_domain_type_hash() -> String { - #domain_type_hash.to_string() - } - } - }; - - implementation.into() -} - -#[proc_macro_derive(Eip712, attributes(eip712))] -pub fn eip_712_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).expect("failed to parse token stream for Eip712 derived struct"); - - impl_eip_712_macro(&ast) -} diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 7f77b08fa..464c9406d 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -32,6 +32,9 @@ bytes = { version = "1.1.0", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["std"] } semver = "1.0.4" once_cell = "1.8.0" +convert_case = "0.4.0" +syn = "1.0.77" +quote = "1.0.9" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # async diff --git a/derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml similarity index 63% rename from derive-eip712/Cargo.toml rename to ethers-core/ethers-derive-eip712/Cargo.toml index 3f5c06d47..f8c7633df 100644 --- a/derive-eip712/Cargo.toml +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "derive-eip712" +name = "ethers-derive-eip712" version = "0.1.0" edition = "2018" description = "Custom derive macro for EIP-712 typed data" @@ -10,10 +10,7 @@ proc-macro = true [dependencies] quote = "1.0.9" syn = "1.0.77" -ethers-core = { path = "../ethers-core" } -convert_case = "0.4.0" -proc-macro2 = "1.0.29" +ethers-core = { version = "^0.5.0", path = "../"} hex = "0.4.3" serde = "1.0.130" serde_json = "1.0.68" -thiserror = "1.0.29" diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs new file mode 100644 index 000000000..8ca47b4d3 --- /dev/null +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -0,0 +1,176 @@ +//! # EIP-712 Derive Macro +//! This crate provides a derive macro `Eip712` that is used to encode a rust struct +//! into a payload hash, according to [https://eips.ethereum.org/EIPS/eip-712](https://eips.ethereum.org/EIPS/eip-712) +//! +//! The trait used to derive the macro is found in `ethers_core::transaction::eip712::Eip712` +//! Both the derive macro and the trait must be in context when using +//! +//! This derive macro requires the `#[eip712]` attributes to be included +//! for specifying the domain separator used in encoding the hash. +//! +//! All String values returned by the implemented methods are hex encoded and should be +//! decoded into `[u8; 32]` for signing. See example for decoding. +//! +//! # Example Usage +//! +//! ```rust +//! use ethers_derive_eip712::*; +//! use ethers_core::types::{transaction::eip712::Eip712, H160}; +//! use serde::Serialize; +//! +//! #[derive(Debug, Eip712, Serialize)] +//! #[eip712( +//! name = "Radicle", +//! version = "1", +//! chain_id = 1, +//! verifying_contract = "0x0000000000000000000000000000000000000000" +//! )] +//! pub struct Puzzle { +//! pub organization: H160, +//! pub contributor: H160, +//! pub commit: String, +//! pub project: String, +//! } +//! +//! let puzzle = Puzzle { +//! organization: "0000000000000000000000000000000000000000" +//! .parse::() +//! .expect("failed to parse address"), +//! contributor: "0000000000000000000000000000000000000000" +//! .parse::() +//! .expect("failed to parse address"), +//! commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(), +//! project: "radicle-reward".to_string(), +//! }; +//! +//! let hash = puzzle.encode_eip712()?; +//! +//! let decoded: Vec = hex::decode(hash).expect("failed to decode") +//! let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; +//! ``` +//! +//! # Limitations +//! +//! At the moment, the derive macro does not recursively encode nested Eip712 structs. +//! + +use proc_macro::TokenStream; +use quote::quote; + +// import eip712 utilities from ethers_core::types::transaction::eip712 +use ethers_core::types::transaction::eip712; + +// Main implementation macro, used to compute static values and define +// method for encoding the final eip712 payload; +fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { + // Primary type should match the type in the ethereum verifying contract; + let primary_type = &ast.ident; + + // Computer domain separator + let domain_attributes: eip712::Eip712Domain = eip712::Eip712Domain::from(ast); + let domain_separator = domain_attributes.separator(); + let domain_type_hash = hex::encode(eip712::eip712_domain_type_hash()); + + // Must parse the AST at compile time. + let parsed_fields = eip712::parse_fields(ast); + + // JSON Stringify the field names and types to pass into the + // derived encode_eip712() method as a static string; + // the AST of the struct is not available at runtime, so this is + // a work around for passing in the struct fields; + let fields: String = serde_json::to_string(&parsed_fields) + .expect("failed to serialize parsed fields into JSON string"); + + // Compute the type hash for the derived struct using the parsed fields from above; + let type_hash = eip712::make_type_hash(primary_type.clone().to_string(), &parsed_fields); + + let implementation = quote! { + impl Eip712 for #primary_type { + type Error = ethers_core::types::transaction::eip712::Eip712Error; + + fn type_hash() -> String { + #type_hash.to_string() + } + + fn domain_separator() -> String { + #domain_separator.to_string() + } + + fn encode_eip712(&self) -> Result { + // Ok(make_struct_hash(self, #domain_separator, #type_hash, #fields)?.to_string()) + let json: serde_json::Value = serde_json::from_str(#fields)?; + + if let serde_json::Value::Object(fields) = json { + let mut keys = fields.keys().map(|f| f.to_string()).collect::>(); + + // sort the fields alphabetically; + // NOTE: the solidity type hash should also use the same convention; + keys.sort(); + + let _values: serde_json::Value = serde_json::to_value(self)?; + + if let serde_json::Value::Object(obj) = _values { + // Initialize the items with the type hash + let mut items = vec![ethers_core::abi::Token::Uint( + ethers_core::types::U256::from(&hex::decode(#type_hash)?[..]), + )]; + + for key in keys { + if let Some(v) = obj.get(&key) { + if let Some(ty) = fields.get(&key) { + if let serde_json::Value::String(value) = v{ + if let serde_json::Value::String(field_type) = ty { + // convert encoded type; + let item = match field_type.as_ref() { + // TODO: This following enc types are not exhaustive; + // Check types against solidity abi.encodePacked() + "uint128" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), + "uint256" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), + "address" => ethers_core::abi::Token::Address(value.parse::().expect("failed to parse address")), + _ => { ethers_core::abi::Token::Uint(ethers_core::types::U256::from(ethers_core::utils::keccak256(value))) } + }; + + // Add the parsed field type to the items to be encoded; + items.push(item); + } + } + } + } + } + + let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode( + &items, + )); + + // encode the digest to be compatible with solidity abi.encodePacked() + // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 + let digest_input = [ + &[0x19, 0x01], + &hex::decode(#domain_separator)?[..], + &struct_hash[..] + ].concat(); + + return Ok(hex::encode(ethers_core::utils::keccak256(digest_input))); + } + } + + // Reached Error: + Err(ethers_core::types::transaction::eip712::Eip712Error::FailedToEncodeStruct) + + } + + fn eip712_domain_type_hash() -> String { + #domain_type_hash.to_string() + } + } + }; + + implementation.into() +} + +#[proc_macro_derive(Eip712, attributes(eip712))] +pub fn eip_712_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).expect("failed to parse token stream for Eip712 derived struct"); + + impl_eip_712_macro(&ast) +} diff --git a/derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs similarity index 97% rename from derive-eip712/tests/derive_eip712.rs rename to ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index ba2979273..14738c015 100644 --- a/derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -1,5 +1,5 @@ -use derive_eip712::*; use ethers_core::types::{transaction::eip712::Eip712, H160}; +use ethers_derive_eip712::*; use serde::Serialize; #[derive(Debug, Eip712, Serialize)] diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index ab8a5a51f..02af791ce 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -1,3 +1,32 @@ +//! TL;DR you're probably looking for `ethers-derive-eip712` Eip712 derive macro. +//! +//! The eip712 module contains helper methods and types mainly used +//! by the derive-eip712 procedural macro. Note that many of the methods +//! used in this module may panic!. While this is desired behavior for a +//! procedural macro, it may not be the behavior you wish to use in your +//! application if using these methods manually. +use std::collections::HashMap; + +use convert_case::{Case, Casing}; + +use crate::{ + abi, + abi::Token, + types::{Address, H160, U256}, + utils::keccak256, +}; + +/// Error typed used by Eip712 derive macro +#[derive(Debug, thiserror::Error)] +pub enum Eip712Error { + #[error("Failed to serialize serde JSON object")] + SerdeJsonError(#[from] serde_json::Error), + #[error("Failed to decode hex value")] + FromHexError(#[from] hex::FromHexError), + #[error("Failed to make struct hash from values")] + FailedToEncodeStruct, +} + /// The Eip712 trait provides helper methods for computing /// the typed data hash used in `eth_signTypedData`. /// @@ -44,3 +73,204 @@ pub trait Eip712 { /// the final encoded payload. fn encode_eip712(&self) -> Result; } + +/// This method provides the hex encoded domain type hash for EIP712Domain type; +/// This is used by all Eip712 structs. +pub fn eip712_domain_type_hash() -> [u8; 32] { + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") +} + +/// Eip712 Domain attributes used in determining the domain separator; +#[derive(Debug, Default)] +pub struct Eip712Domain { + name: String, + version: String, + chain_id: U256, + verifying_contract: Address, +} + +impl Eip712Domain { + // Compute the domain separator; + // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L41 + pub fn separator(&self) -> String { + hex::encode(keccak256(abi::encode(&[ + Token::Uint(U256::from(eip712_domain_type_hash())), + Token::Uint(U256::from(keccak256(&self.name))), + Token::Uint(U256::from(keccak256(&self.version))), + Token::Uint(self.chain_id), + Token::Address(self.verifying_contract), + ]))) + } +} + +// Parse the AST of the struct to determine the domain attributes +impl From<&syn::DeriveInput> for Eip712Domain { + fn from(input: &syn::DeriveInput) -> Eip712Domain { + let mut domain = Eip712Domain::default(); + + let attributes = input.attrs.first().expect("missing macro arguments"); + + let is_segment_valid = attributes + .path + .segments + .first() + .map(|s| s.ident == "eip712") + .expect("missing eip712 macro arguments"); + + if !is_segment_valid { + panic!("invalid path segment, identity does not match 'eip712'") + } + + let mut token_stream = attributes.tokens.clone().into_iter(); + + if let Some(quote::__private::TokenTree::Group(g)) = token_stream.next() { + let group_stream = g.stream().into_iter(); + let mut current_arg = String::new(); + for item in group_stream { + if let quote::__private::TokenTree::Ident(ident) = item { + current_arg = ident.to_string(); + } else if let quote::__private::TokenTree::Literal(literal) = item { + match current_arg.as_ref() { + "name" => { + domain.name = literal.to_string().replace("\"", ""); + } + "version" => { + domain.version = literal.to_string().replace("\"", ""); + } + "chain_id" => { + domain.chain_id = literal + .to_string() + .parse::() + .expect("failed to parse chain id from macro arguments"); + } + "verifying_contract" => { + domain.verifying_contract = literal + .to_string() + .replace("\"", "") + .parse::() + .expect("failed to parse verifying contract"); + } + _ => { + panic!("expected arguments: 'name', 'version', 'chain_id' and 'verifying_contract'; found: {}", current_arg); + } + } + } + } + }; + + domain + } +} + +// Convert rust types to enc types. This is used in determining the type hash; +// NOTE: this is not an exhaustive list, and there may already be an existing mapping +// in another library. +pub fn parse_field_type(field_type: String) -> String { + match field_type.as_ref() { + "U128" => "uint128", + "U256" => "uint256", + "H128" => "bytes16", + "H160" => "address", + "H256" => "bytes32", + "String" => "string", + "Boolean" => "boolean", + "Bytes" => "bytes", + "Vec" => "bytes", + "Vec" => "uint128[]", + "Vec" => "uint256[]", + "Vec "bytes16[]", + "Vec" => "address[]", + "Vec "bytes32[]", + "Vec" => "string[]", + "Vec" => "bytes[]", + _ => { + // NOTE: This will fail if the field type does not match an ethereum type; + &field_type + } + } + .to_string() +} + +/// Parse the field type from the derived struct +pub fn parse_field(field: &syn::Field) -> String { + let field_path = match &field.ty { + syn::Type::Path(p) => p, + _ => { + panic!("field type must be a path") + } + }; + + let segment = field_path + .path + .segments + .first() + .expect("field must have a type"); + + let mut field_type = segment.ident.to_string(); + + if let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments { + field_type.push('<'); + for arg in &arguments.args { + if let syn::GenericArgument::Type(syn::Type::Path(p)) = arg { + let arg_identity = p + .path + .segments + .first() + .map(|s| s.ident.to_string()) + .expect("argument must have an identity"); + + field_type.push_str(&arg_identity); + } + } + field_type.push('>'); + } + + parse_field_type(field_type) +} + +/// Return HashMap of the field name and the field type; +pub fn parse_fields(ast: &syn::DeriveInput) -> HashMap { + let mut parsed_fields = HashMap::new(); + + let data = match &ast.data { + syn::Data::Struct(s) => s, + _ => { + panic!("Eip712 can only be derived for a struct") + } + }; + + let named_fields = match &data.fields { + syn::Fields::Named(name) => name, + _ => { + panic!("unnamed fields are not supported") + } + }; + + named_fields.named.iter().for_each(|f| { + let field_name = f + .ident + .clone() + .expect("field must be named") + .to_string() + .to_case(Case::Camel); + + let field_type = parse_field(f); + + parsed_fields.insert(field_name, field_type); + }); + + parsed_fields +} + +/// Convert hash map of field names and types into a type hash corresponding to enc types; +pub fn make_type_hash(primary_type: String, fields: &HashMap) -> String { + let parameters = fields + .iter() + .map(|(k, v)| format!("{} {}", v, k)) + .collect::>() + .join(","); + + let sig = format!("{}({})", primary_type, parameters); + + hex::encode(keccak256(sig)) +} From 6e17e394ef70129b3105d90d831bd45aa80b3f74 Mon Sep 17 00:00:00 2001 From: Ryan Date: Sat, 2 Oct 2021 22:22:14 -0700 Subject: [PATCH 03/30] refactor of derive-eip712 macro; use ParamType and EthAbiToken --- Cargo.lock | 3 + ethers-core/Cargo.toml | 1 + ethers-core/ethers-derive-eip712/Cargo.toml | 2 + ethers-core/ethers-derive-eip712/src/lib.rs | 160 +++--- .../tests/derive_eip712.rs | 123 ++++- ethers-core/src/types/transaction/eip712.rs | 471 ++++++++++++------ ethers-signers/src/aws/mod.rs | 10 +- ethers-signers/src/ledger/mod.rs | 9 +- ethers-signers/src/lib.rs | 12 +- ethers-signers/src/wallet/mod.rs | 21 +- 10 files changed, 516 insertions(+), 296 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 162e1430d..69de5f985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -929,6 +929,7 @@ dependencies = [ "hex-literal", "k256", "once_cell", + "proc-macro2", "quote", "rand 0.8.4", "rlp", @@ -946,8 +947,10 @@ dependencies = [ name = "ethers-derive-eip712" version = "0.1.0" dependencies = [ + "ethers-contract", "ethers-core", "hex", + "proc-macro2", "quote", "serde", "serde_json", diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 464c9406d..b05a77dcb 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -35,6 +35,7 @@ once_cell = "1.8.0" convert_case = "0.4.0" syn = "1.0.77" quote = "1.0.9" +proc-macro2 = "1.0.29" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # async diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml index f8c7633df..aaa7eb8c9 100644 --- a/ethers-core/ethers-derive-eip712/Cargo.toml +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -11,6 +11,8 @@ proc-macro = true quote = "1.0.9" syn = "1.0.77" ethers-core = { version = "^0.5.0", path = "../"} +ethers-contract = { version = "^0.5.0", path = "../../ethers-contract"} hex = "0.4.3" serde = "1.0.130" serde_json = "1.0.68" +proc-macro2 = "1.0.29" diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 8ca47b4d3..5a205d76d 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -8,17 +8,17 @@ //! This derive macro requires the `#[eip712]` attributes to be included //! for specifying the domain separator used in encoding the hash. //! -//! All String values returned by the implemented methods are hex encoded and should be -//! decoded into `[u8; 32]` for signing. See example for decoding. +//! NOTE: In addition to deriving `Eip712` trait, the `EthAbiType` trait must also be derived. +//! This allows the struct to be parsed into `ethers_core::abi::Token` for encoding. //! //! # Example Usage //! //! ```rust +//! use ethers_contract::EthAbiType; //! use ethers_derive_eip712::*; //! use ethers_core::types::{transaction::eip712::Eip712, H160}; -//! use serde::Serialize; //! -//! #[derive(Debug, Eip712, Serialize)] +//! #[derive(Debug, Eip712, EthAbiType)] //! #[eip712( //! name = "Radicle", //! version = "1", @@ -44,21 +44,27 @@ //! }; //! //! let hash = puzzle.encode_eip712()?; -//! -//! let decoded: Vec = hex::decode(hash).expect("failed to decode") -//! let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; //! ``` //! //! # Limitations //! //! At the moment, the derive macro does not recursively encode nested Eip712 structs. //! +//! There is an Inner helper attribute `#[eip712]` for fields that will eventually be used to +//! determine if there is a nested eip712 struct. However, this work is not yet complete. +//! +use std::convert::TryFrom; +use ethers_core::types::transaction::eip712; use proc_macro::TokenStream; use quote::quote; -// import eip712 utilities from ethers_core::types::transaction::eip712 -use ethers_core::types::transaction::eip712; +#[proc_macro_derive(Eip712, attributes(eip712))] +pub fn eip_712_derive(input: TokenStream) -> TokenStream { + let ast = syn::parse(input).expect("failed to parse token stream for Eip712 derived struct"); + + impl_eip_712_macro(&ast) +} // Main implementation macro, used to compute static values and define // method for encoding the final eip712 payload; @@ -67,110 +73,86 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { let primary_type = &ast.ident; // Computer domain separator - let domain_attributes: eip712::Eip712Domain = eip712::Eip712Domain::from(ast); - let domain_separator = domain_attributes.separator(); - let domain_type_hash = hex::encode(eip712::eip712_domain_type_hash()); + let domain = match eip712::EIP712Domain::try_from(ast) { + Ok(attributes) => attributes, + Err(e) => return TokenStream::from(e), + }; - // Must parse the AST at compile time. - let parsed_fields = eip712::parse_fields(ast); + let domain_separator = hex::encode(domain.separator()); - // JSON Stringify the field names and types to pass into the - // derived encode_eip712() method as a static string; - // the AST of the struct is not available at runtime, so this is - // a work around for passing in the struct fields; - let fields: String = serde_json::to_string(&parsed_fields) - .expect("failed to serialize parsed fields into JSON string"); + // Must parse the AST at compile time. + let parsed_fields = match eip712::parse_fields(ast) { + Ok(fields) => fields, + Err(e) => return TokenStream::from(e), + }; // Compute the type hash for the derived struct using the parsed fields from above; - let type_hash = eip712::make_type_hash(primary_type.clone().to_string(), &parsed_fields); + let type_hash = hex::encode(eip712::make_type_hash( + primary_type.clone().to_string(), + &parsed_fields, + )); let implementation = quote! { impl Eip712 for #primary_type { type Error = ethers_core::types::transaction::eip712::Eip712Error; - fn type_hash() -> String { - #type_hash.to_string() + fn type_hash() -> Result<[u8; 32], Self::Error> { + use std::convert::TryFrom; + let decoded = hex::decode(#type_hash.to_string())?; + let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; + Ok(byte_array) } - fn domain_separator() -> String { - #domain_separator.to_string() + fn domain_separator() -> Result<[u8; 32], Self::Error> { + use std::convert::TryFrom; + let decoded = hex::decode(#domain_separator.to_string())?; + let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; + Ok(byte_array) } - fn encode_eip712(&self) -> Result { - // Ok(make_struct_hash(self, #domain_separator, #type_hash, #fields)?.to_string()) - let json: serde_json::Value = serde_json::from_str(#fields)?; - - if let serde_json::Value::Object(fields) = json { - let mut keys = fields.keys().map(|f| f.to_string()).collect::>(); - - // sort the fields alphabetically; - // NOTE: the solidity type hash should also use the same convention; - keys.sort(); - - let _values: serde_json::Value = serde_json::to_value(self)?; - - if let serde_json::Value::Object(obj) = _values { - // Initialize the items with the type hash - let mut items = vec![ethers_core::abi::Token::Uint( - ethers_core::types::U256::from(&hex::decode(#type_hash)?[..]), - )]; - - for key in keys { - if let Some(v) = obj.get(&key) { - if let Some(ty) = fields.get(&key) { - if let serde_json::Value::String(value) = v{ - if let serde_json::Value::String(field_type) = ty { - // convert encoded type; - let item = match field_type.as_ref() { - // TODO: This following enc types are not exhaustive; - // Check types against solidity abi.encodePacked() - "uint128" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), - "uint256" => ethers_core::abi::Token::Uint(ethers_core::types::U256::from(value.parse::().expect("failed to parse unsigned integer"))), - "address" => ethers_core::abi::Token::Address(value.parse::().expect("failed to parse address")), - _ => { ethers_core::abi::Token::Uint(ethers_core::types::U256::from(ethers_core::utils::keccak256(value))) } - }; - - // Add the parsed field type to the items to be encoded; - items.push(item); - } - } - } + fn struct_hash(self) -> Result<[u8; 32], Self::Error> { + use ethers_core::abi::Tokenizable; + let mut items = vec![ethers_core::abi::Token::Uint( + ethers_core::types::U256::from(&Self::type_hash()?[..]), + )]; + + if let ethers_core::abi::Token::Tuple(tokens) = self.clone().into_token() { + for token in tokens { + match &token { + ethers_core::abi::Token::Tuple(t) => { + // TODO: check for nested Eip712 Type; + // Challenge is determining the type hash + }, + _ => { + items.push(ethers_core::types::transaction::eip712::encode_eip712_type(token)); } } - - let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode( - &items, - )); - - // encode the digest to be compatible with solidity abi.encodePacked() - // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 - let digest_input = [ - &[0x19, 0x01], - &hex::decode(#domain_separator)?[..], - &struct_hash[..] - ].concat(); - - return Ok(hex::encode(ethers_core::utils::keccak256(digest_input))); } } - // Reached Error: - Err(ethers_core::types::transaction::eip712::Eip712Error::FailedToEncodeStruct) + let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode( + &items, + )); + Ok(struct_hash) } - fn eip712_domain_type_hash() -> String { - #domain_type_hash.to_string() + fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { + let struct_hash = self.struct_hash()?; + + // encode the digest to be compatible with solidity abi.encodePacked() + // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 + let digest_input = [ + &[0x19, 0x01], + &Self::domain_separator()?[..], + &struct_hash[..] + ].concat(); + + return Ok(ethers_core::utils::keccak256(digest_input)); + } } }; implementation.into() } - -#[proc_macro_derive(Eip712, attributes(eip712))] -pub fn eip_712_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).expect("failed to parse token stream for Eip712 derived struct"); - - impl_eip_712_macro(&ast) -} diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index 14738c015..1ad67e668 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -1,23 +1,26 @@ -use ethers_core::types::{transaction::eip712::Eip712, H160}; +use ethers_contract::EthAbiType; +use ethers_core::types::{ + transaction::eip712::{eip712_domain_type_hash, EIP712Domain as Domain, Eip712}, + Address, H160, U256, +}; use ethers_derive_eip712::*; -use serde::Serialize; - -#[derive(Debug, Eip712, Serialize)] -#[eip712( - name = "Radicle", - version = "1", - chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000000" -)] -pub struct Puzzle { - pub organization: H160, - pub contributor: H160, - pub commit: String, - pub project: String, -} #[test] fn test_derive_eip712() { + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "Radicle", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000000" + )] + pub struct Puzzle { + pub organization: H160, + pub contributor: H160, + pub commit: String, + pub project: String, + } + let puzzle = Puzzle { organization: "0000000000000000000000000000000000000000" .parse::() @@ -31,9 +34,91 @@ fn test_derive_eip712() { let hash = puzzle.encode_eip712().expect("failed to encode struct"); - // TODO: Compare against solidity computed hash - println!("Hash: {:?}", hash); - assert_eq!(hash.len(), 64) + assert_eq!(hash.len(), 32) +} + +#[test] +fn test_struct_hash() { + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "Radicle", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000000" + )] + pub struct EIP712Domain { + name: String, + version: String, + chain_id: U256, + verifying_contract: Address, + } + + let domain = Domain { + name: "Radicle".to_string(), + version: "1".to_string(), + chain_id: U256::from(1), + verifying_contract: H160::from(&[0; 20]), + }; + + let domain_test = EIP712Domain { + name: "Radicle".to_string(), + version: "1".to_string(), + chain_id: U256::from(1), + verifying_contract: H160::from(&[0; 20]), + }; + + assert_eq!( + eip712_domain_type_hash(), + EIP712Domain::type_hash().unwrap() + ); + + assert_eq!(domain.separator(), domain_test.struct_hash().unwrap()); +} + +#[test] +fn test_derive_eip712_nested() { + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "MyDomain", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000000" + )] + pub struct MyStruct { + foo: String, + bar: U256, + addr: Address, + // #[eip712] // Todo: Support nested Eip712 structs + // nested: MyNestedStruct, + } + + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "MyDomain", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000000" + )] + pub struct MyNestedStruct { + foo: String, + bar: U256, + addr: Address, + } + + let my_struct = MyStruct { + foo: "foo".to_string(), + bar: U256::from(1), + addr: Address::from(&[0; 20]), + // nested: MyNestedStruct { + // foo: "foo".to_string(), + // bar: U256::from(1), + // addr: Address::from(&[0; 20]), + // }, + }; + + let hash = my_struct.struct_hash().expect("failed to hash struct"); + + assert_eq!(hash.len(), 32) } diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 02af791ce..671ee628a 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -1,17 +1,15 @@ -//! TL;DR you're probably looking for `ethers-derive-eip712` Eip712 derive macro. -//! -//! The eip712 module contains helper methods and types mainly used -//! by the derive-eip712 procedural macro. Note that many of the methods -//! used in this module may panic!. While this is desired behavior for a -//! procedural macro, it may not be the behavior you wish to use in your -//! application if using these methods manually. -use std::collections::HashMap; - use convert_case::{Case, Casing}; +use core::convert::TryFrom; +use proc_macro2::TokenStream; +use syn::spanned::Spanned as _; +use syn::{ + parse::Error, AttrStyle, Data, DeriveInput, Expr, Fields, GenericArgument, Lit, NestedMeta, + PathArguments, Type, +}; use crate::{ abi, - abi::Token, + abi::{ParamType, Token}, types::{Address, H160, U256}, utils::keccak256, }; @@ -25,6 +23,8 @@ pub enum Eip712Error { FromHexError(#[from] hex::FromHexError), #[error("Failed to make struct hash from values")] FailedToEncodeStruct, + #[error("Failed to convert slice into byte array")] + TryFromSliceError(#[from] std::array::TryFromSliceError), } /// The Eip712 trait provides helper methods for computing @@ -39,10 +39,6 @@ pub enum Eip712Error { /// /// Any rust struct implementing Eip712 must also have a corresponding /// struct in the verifying ethereum contract that matches its signature. -/// -/// NOTE: Due to limitations of the derive macro not supporting return types of -/// [u8; 32] or Vec, all methods should return the hex encoded values of the keccak256 -/// byte array. pub trait Eip712 { /// User defined error type; type Error: std::error::Error + Send + Sync + std::fmt::Debug; @@ -50,10 +46,8 @@ pub trait Eip712 { /// The eip712 domain is the same for all Eip712 implementations, /// This method does not need to be manually implemented, but may be overridden /// if needed. - fn eip712_domain_type_hash() -> String { - hex::encode(crate::utils::keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)", - )) + fn eip712_domain_type_hash() -> Result<[u8; 32], Self::Error> { + Ok(eip712_domain_type_hash()) } /// The domain separator depends on the contract and unique domain @@ -61,17 +55,20 @@ pub trait Eip712 { /// are passed in as arguments to the macro. When manually deriving, the user /// will need to know the name of the domain, version of the contract, chain ID of /// where the contract lives and the address of the verifying contract. - fn domain_separator() -> String; + fn domain_separator() -> Result<[u8; 32], Self::Error>; /// This method is used for calculating the hash of the type signature of the /// struct. The field types of the struct must map to primitive /// ethereum types or custom types defined in the contract. - fn type_hash() -> String; + fn type_hash() -> Result<[u8; 32], Self::Error>; + + /// Hash of the struct, according to EIP-712 definition of `hashStruct` + fn struct_hash(self) -> Result<[u8; 32], Self::Error>; /// When using the derive macro, this is the primary method used for computing the final /// EIP-712 encoded payload. This method relies on the aforementioned methods for computing /// the final encoded payload. - fn encode_eip712(&self) -> Result; + fn encode_eip712(self) -> Result<[u8; 32], Self::Error>; } /// This method provides the hex encoded domain type hash for EIP712Domain type; @@ -82,188 +79,317 @@ pub fn eip712_domain_type_hash() -> [u8; 32] { /// Eip712 Domain attributes used in determining the domain separator; #[derive(Debug, Default)] -pub struct Eip712Domain { - name: String, - version: String, - chain_id: U256, - verifying_contract: Address, +pub struct EIP712Domain { + pub name: String, + pub version: String, + pub chain_id: U256, + pub verifying_contract: Address, } -impl Eip712Domain { +impl EIP712Domain { // Compute the domain separator; // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L41 - pub fn separator(&self) -> String { - hex::encode(keccak256(abi::encode(&[ + pub fn separator(&self) -> [u8; 32] { + keccak256(abi::encode(&[ Token::Uint(U256::from(eip712_domain_type_hash())), Token::Uint(U256::from(keccak256(&self.name))), Token::Uint(U256::from(keccak256(&self.version))), Token::Uint(self.chain_id), Token::Address(self.verifying_contract), - ]))) + ])) } } // Parse the AST of the struct to determine the domain attributes -impl From<&syn::DeriveInput> for Eip712Domain { - fn from(input: &syn::DeriveInput) -> Eip712Domain { - let mut domain = Eip712Domain::default(); - - let attributes = input.attrs.first().expect("missing macro arguments"); - - let is_segment_valid = attributes - .path - .segments - .first() - .map(|s| s.ident == "eip712") - .expect("missing eip712 macro arguments"); - - if !is_segment_valid { - panic!("invalid path segment, identity does not match 'eip712'") - } - - let mut token_stream = attributes.tokens.clone().into_iter(); - - if let Some(quote::__private::TokenTree::Group(g)) = token_stream.next() { - let group_stream = g.stream().into_iter(); - let mut current_arg = String::new(); - for item in group_stream { - if let quote::__private::TokenTree::Ident(ident) = item { - current_arg = ident.to_string(); - } else if let quote::__private::TokenTree::Literal(literal) = item { - match current_arg.as_ref() { - "name" => { - domain.name = literal.to_string().replace("\"", ""); - } - "version" => { - domain.version = literal.to_string().replace("\"", ""); - } - "chain_id" => { - domain.chain_id = literal - .to_string() - .parse::() - .expect("failed to parse chain id from macro arguments"); - } - "verifying_contract" => { - domain.verifying_contract = literal - .to_string() - .replace("\"", "") - .parse::() - .expect("failed to parse verifying contract"); - } - _ => { - panic!("expected arguments: 'name', 'version', 'chain_id' and 'verifying_contract'; found: {}", current_arg); +impl TryFrom<&syn::DeriveInput> for EIP712Domain { + type Error = TokenStream; + fn try_from(input: &syn::DeriveInput) -> Result { + let mut domain = EIP712Domain::default(); + + for attribute in input.attrs.iter() { + if let AttrStyle::Outer = attribute.style { + if let Ok(syn::Meta::List(meta)) = attribute.parse_meta() { + if meta.path.is_ident("eip712") { + for n in meta.nested.iter() { + if let NestedMeta::Meta(meta) = n { + match meta { + syn::Meta::NameValue(meta) => { + let ident = meta.path.get_ident().ok_or_else(|| { + Error::new( + meta.path.span(), + "unrecognized eip712 parameter", + ) + .to_compile_error() + })?; + + match ident.to_string().as_ref() { + "name" => match meta.lit { + syn::Lit::Str(ref lit_str) => { + if domain.name != String::default() { + return Err(Error::new( + meta.path.span(), + "domain name already specified", + ) + .to_compile_error()); + } + + domain.name = lit_str.value(); + } + _ => { + return Err(Error::new( + meta.path.span(), + "domain name must be a string", + ) + .to_compile_error()); + } + }, + "version" => match meta.lit { + syn::Lit::Str(ref lit_str) => { + if domain.version != String::default() { + return Err(Error::new( + meta.path.span(), + "domain version already specified", + ) + .to_compile_error()); + } + + domain.version = lit_str.value(); + } + _ => { + return Err(Error::new( + meta.path.span(), + "domain version must be a string", + ) + .to_compile_error()); + } + }, + "chain_id" => match meta.lit { + syn::Lit::Int(ref lit_int) => { + if domain.chain_id != U256::default() { + return Err(Error::new( + meta.path.span(), + "domain chain_id already specified", + ) + .to_compile_error()); + } + + domain.chain_id = lit_int + .base10_digits() + .parse() + .map_err(|_| { + Error::new( + meta.path.span(), + "failed to parse chain id", + ) + .to_compile_error() + })?; + } + _ => { + return Err(Error::new( + meta.path.span(), + "domain chain_id must be a positive integer", + ) + .to_compile_error()); + } + }, + "verifying_contract" => match meta.lit { + syn::Lit::Str(ref lit_str) => { + if domain.verifying_contract != H160::default() + { + return Err(Error::new( + meta.path.span(), + "domain verifying_contract already specified", + ) + .to_compile_error()); + } + + domain.verifying_contract = lit_str.value().parse().map_err(|_| { + Error::new( + meta.path.span(), + "failed to parse verifying contract into Address", + ) + .to_compile_error() + })?; + } + _ => { + return Err(Error::new( + meta.path.span(), + "domain verifying_contract must be a string", + ) + .to_compile_error()); + } + }, + _ => { + return Err(Error::new( + meta.path.span(), + "unrecognized eip712 parameter; must be one of 'name', 'version', 'chain_id', or 'verifying_contract'", + ) + .to_compile_error()); + } + } + } + syn::Meta::Path(path) => { + return Err(Error::new( + path.span(), + "unrecognized eip712 parameter", + ) + .to_compile_error()); + } + syn::Meta::List(meta) => { + return Err(Error::new( + meta.path.span(), + "unrecognized eip712 parameter", + ) + .to_compile_error()); + } + } + } } } } } - }; + } - domain + Ok(domain) } } -// Convert rust types to enc types. This is used in determining the type hash; -// NOTE: this is not an exhaustive list, and there may already be an existing mapping -// in another library. -pub fn parse_field_type(field_type: String) -> String { - match field_type.as_ref() { - "U128" => "uint128", - "U256" => "uint256", - "H128" => "bytes16", - "H160" => "address", - "H256" => "bytes32", - "String" => "string", - "Boolean" => "boolean", - "Bytes" => "bytes", - "Vec" => "bytes", - "Vec" => "uint128[]", - "Vec" => "uint256[]", - "Vec "bytes16[]", - "Vec" => "address[]", - "Vec "bytes32[]", - "Vec" => "string[]", - "Vec" => "bytes[]", - _ => { - // NOTE: This will fail if the field type does not match an ethereum type; - &field_type +/// Parse the eth abi parameter type based on the syntax type; +/// this method is copied from https://github.com/gakonst/ethers-rs/blob/master/ethers-contract/ethers-contract-derive/src/lib.rs#L600 +pub fn find_parameter_type(ty: &Type) -> Result { + match ty { + Type::Array(ty) => { + let param = find_parameter_type(ty.elem.as_ref())?; + if let Expr::Lit(ref expr) = ty.len { + if let Lit::Int(ref len) = expr.lit { + if let Ok(size) = len.base10_parse::() { + return Ok(ParamType::FixedArray(Box::new(param), size)); + } + } + } + Err( + Error::new(ty.span(), "Failed to derive proper ABI from array field") + .to_compile_error(), + ) } - } - .to_string() -} + Type::Path(ty) => { + if let Some(ident) = ty.path.get_ident() { + return match ident.to_string().to_lowercase().as_str() { + "address" => Ok(ParamType::Address), + "string" => Ok(ParamType::String), + "bool" => Ok(ParamType::Bool), + "int" | "uint" => Ok(ParamType::Uint(256)), + "h160" => Ok(ParamType::FixedBytes(20)), + "h256" | "secret" | "hash" => Ok(ParamType::FixedBytes(32)), + "h512" | "public" => Ok(ParamType::FixedBytes(64)), + s => parse_int_param_type(s).ok_or_else(|| { + Error::new(ty.span(), "Failed to derive proper ABI from field") + .to_compile_error() + }), + }; + } + // check for `Vec` + if ty.path.segments.len() == 1 && ty.path.segments[0].ident == "Vec" { + if let PathArguments::AngleBracketed(ref args) = ty.path.segments[0].arguments { + if args.args.len() == 1 { + if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() { + let kind = find_parameter_type(ty)?; + return Ok(ParamType::Array(Box::new(kind))); + } + } + } + } -/// Parse the field type from the derived struct -pub fn parse_field(field: &syn::Field) -> String { - let field_path = match &field.ty { - syn::Type::Path(p) => p, - _ => { - panic!("field type must be a path") + Err(Error::new(ty.span(), "Failed to derive proper ABI from fields").to_compile_error()) } - }; - - let segment = field_path - .path - .segments - .first() - .expect("field must have a type"); - - let mut field_type = segment.ident.to_string(); - - if let syn::PathArguments::AngleBracketed(arguments) = &segment.arguments { - field_type.push('<'); - for arg in &arguments.args { - if let syn::GenericArgument::Type(syn::Type::Path(p)) = arg { - let arg_identity = p - .path - .segments - .first() - .map(|s| s.ident.to_string()) - .expect("argument must have an identity"); - - field_type.push_str(&arg_identity); - } + Type::Tuple(ty) => { + let params = ty + .elems + .iter() + .map(|t| find_parameter_type(t)) + .collect::, _>>()?; + Ok(ParamType::Tuple(params)) + } + _ => { + Err(Error::new(ty.span(), "Failed to derive proper ABI from fields").to_compile_error()) } - field_type.push('>'); } +} - parse_field_type(field_type) +fn parse_int_param_type(s: &str) -> Option { + let size = s + .chars() + .skip(1) + .collect::() + .parse::() + .ok()?; + if s.starts_with('u') { + Some(ParamType::Uint(size)) + } else if s.starts_with('i') { + Some(ParamType::Int(size)) + } else { + None + } } /// Return HashMap of the field name and the field type; -pub fn parse_fields(ast: &syn::DeriveInput) -> HashMap { - let mut parsed_fields = HashMap::new(); +pub fn parse_fields(ast: &DeriveInput) -> Result, TokenStream> { + let mut fields = Vec::new(); let data = match &ast.data { - syn::Data::Struct(s) => s, + Data::Struct(s) => s, _ => { - panic!("Eip712 can only be derived for a struct") + return Err(Error::new( + ast.span(), + "invalid data type. can only derive Eip712 for a struct", + ) + .to_compile_error()) } }; let named_fields = match &data.fields { - syn::Fields::Named(name) => name, + Fields::Named(name) => name, _ => { - panic!("unnamed fields are not supported") + return Err( + Error::new(ast.span(), "unnamed fields are not supported").to_compile_error() + ) } }; - named_fields.named.iter().for_each(|f| { + for f in named_fields.named.iter() { let field_name = f .ident .clone() - .expect("field must be named") - .to_string() - .to_case(Case::Camel); - - let field_type = parse_field(f); + .map(|i| i.to_string().to_case(Case::Camel)) + .ok_or_else(|| { + Error::new(named_fields.span(), "fields must be named").to_compile_error() + })?; + + let field_type = match f + .attrs + .iter() + .find(|a| a.path.segments.iter().any(|s| s.ident == "eip712")) + { + // Found nested Eip712 Struct + // TODO: Implement custom + Some(a) => { + return Err( + Error::new(a.span(), "nested Eip712 struct are not yet supported") + .to_compile_error(), + ) + } + // Not a nested eip712 struct, return the field param type; + None => find_parameter_type(&f.ty)?, + }; - parsed_fields.insert(field_name, field_type); - }); + fields.push((field_name, field_type)); + } - parsed_fields + Ok(fields) } /// Convert hash map of field names and types into a type hash corresponding to enc types; -pub fn make_type_hash(primary_type: String, fields: &HashMap) -> String { +pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [u8; 32] { let parameters = fields .iter() .map(|(k, v)| format!("{} {}", v, k)) @@ -272,5 +398,40 @@ pub fn make_type_hash(primary_type: String, fields: &HashMap) -> let sig = format!("{}({})", primary_type, parameters); - hex::encode(keccak256(sig)) + keccak256(sig) +} + +/// Parse token into Eip712 compliant ABI encoding +/// NOTE: Token::Tuple() is currently not supported for solidity structs; +/// this is needed for nested Eip712 types, but is not implemented. +pub fn encode_eip712_type(token: Token) -> Token { + match token { + Token::Bytes(t) => Token::Uint(U256::from(keccak256(t))), + Token::FixedBytes(t) => Token::Uint(U256::from(&t[..])), + Token::String(t) => Token::Uint(U256::from(keccak256(t))), + Token::Bool(t) => { + // Boolean false and true are encoded as uint256 values 0 and 1 respectively + Token::Uint(U256::from(t as i32)) + } + Token::Int(t) => { + // Integer values are sign-extended to 256-bit and encoded in big endian order. + Token::Uint(t) + } + Token::Array(tokens) => Token::Uint(U256::from(keccak256(abi::encode( + &tokens + .into_iter() + .map(encode_eip712_type) + .collect::>(), + )))), + Token::FixedArray(tokens) => Token::Uint(U256::from(keccak256(abi::encode( + &tokens + .into_iter() + .map(encode_eip712_type) + .collect::>(), + )))), + _ => { + // Return the ABI encoded token; + token.clone() + } + } } diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index 0aeecd201..fc1db029a 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -250,14 +250,12 @@ impl<'a> super::Signer for AwsSigner<'a> { async fn sign_typed_data( &self, - payload: &T, - ) -> Result, Self::Error> { - let decoded = hex::decode(payload.encode_eip712())?; - let hash = <[u8; 32]>::try_from(&decoded[..])?; - + payload: T, + ) -> Result { + let hash = payload.encode_eip712()?; let digest = self.sign_digest_with_eip155(hash.into()); - Ok(Some(digest)) + Ok(digest) } fn address(&self) -> Address { diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index abf76ace8..ec9d83615 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -27,12 +27,11 @@ impl Signer for LedgerEthereum { async fn sign_typed_data( &self, - payload: &T, - ) -> Result, Self::Error> { - let decoded = hex::decode(payload.encode_eip712())?; - let hash = <[u8; 32]>::try_from(&decoded[..])?; + payload: T, + ) -> Result { + let hash = payload.encode_eip712()?; - Ok(Some(self.sign_hash(hash.into(), false))) + Ok(self.sign_hash(hash.into(), false)) } /// Returns the signer's Ethereum Address diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 46ace1160..ec6548eda 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -97,18 +97,10 @@ pub trait Signer: std::fmt::Debug + Send + Sync { /// Encodes and signs the typed data according EIP-712 /// payload must implement Eip712 trait. - /// - /// NOTE: Returning is Option instead of Signature - /// due to use of std::convert::Infallible as an error type - /// in the Wallet implementation, which conflicts with - /// the error types used within the method, which cannot be mapped - /// to Infallible type. To avoid breaking changes and changing the error - /// type, this method returns `None` if there is an error that restricts - /// the payload from being encoded, and signed. async fn sign_typed_data( &self, - payload: &T, - ) -> Result, Self::Error>; + payload: T, + ) -> Result; /// Returns the signer's Ethereum Address fn address(&self) -> Address; diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index a3da5f9f2..eee13236d 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -25,7 +25,6 @@ use ethers_core::{ use hash::Sha256Proxy; use async_trait::async_trait; -use std::convert::TryFrom; use std::fmt; /// An Ethereum private-public key pair which can be used for signing messages. @@ -89,19 +88,17 @@ impl> Signer fo async fn sign_typed_data( &self, - payload: &T, - ) -> Result, Self::Error> { - if let Ok(encoded) = payload.encode_eip712() { - if let Some(decoded) = hex::decode(encoded).ok() { - if let Some(hash) = <[u8; 32]>::try_from(&decoded[..]).ok() { - return Ok(Some(self.sign_hash(hash.into(), false))); - } + payload: T, + ) -> Result { + let encoded = match payload.encode_eip712() { + Ok(e) => e, + Err(_) => { + // NOTE: Still looking for a better solution for mapping to Infallible error type; + unreachable!() } - } + }; - // Returning none here due to use of infallible error; - // The wallet signing should return an error rather than infallible or None. - Ok(None) + Ok(self.sign_hash(H256::from(encoded), false)) } fn address(&self) -> Address { From 70f9fbdcf974f5320541536ee622c51ad8178551 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 12:19:33 -0700 Subject: [PATCH 04/30] macro updates; add byte array checker for paramtype; use literal constant for domain type hash --- .gitignore | 1 + ethers-core/ethers-derive-eip712/Cargo.toml | 1 + ethers-core/ethers-derive-eip712/src/lib.rs | 11 +- .../tests/derive_eip712.rs | 81 +++++++-- ethers-core/src/types/transaction/eip712.rs | 154 +++++++++++++++--- 5 files changed, 215 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index ea8c4bf7f..ccb51663d 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode \ No newline at end of file diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml index aaa7eb8c9..7d939be89 100644 --- a/ethers-core/ethers-derive-eip712/Cargo.toml +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -12,6 +12,7 @@ quote = "1.0.9" syn = "1.0.77" ethers-core = { version = "^0.5.0", path = "../"} ethers-contract = { version = "^0.5.0", path = "../../ethers-contract"} +ethers-signers = { version = "^0.5.0", path = "../../ethers-signers" } hex = "0.4.3" serde = "1.0.130" serde_json = "1.0.68" diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 5a205d76d..0737dee0d 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -11,6 +11,11 @@ //! NOTE: In addition to deriving `Eip712` trait, the `EthAbiType` trait must also be derived. //! This allows the struct to be parsed into `ethers_core::abi::Token` for encoding. //! +//! # Optional Eip712 Parameters +//! +//! The only optional parameter is `salt`, which accepts a string +//! that is hashed using keccak256 and stored as bytes. +//! //! # Example Usage //! //! ```rust @@ -24,6 +29,8 @@ //! version = "1", //! chain_id = 1, //! verifying_contract = "0x0000000000000000000000000000000000000000" +//! // salt is an optional parameter +//! salt = "my-unique-spice" //! )] //! pub struct Puzzle { //! pub organization: H160, @@ -138,14 +145,12 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { } fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { - let struct_hash = self.struct_hash()?; - // encode the digest to be compatible with solidity abi.encodePacked() // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 let digest_input = [ &[0x19, 0x01], &Self::domain_separator()?[..], - &struct_hash[..] + &self.struct_hash()?[..] ].concat(); return Ok(ethers_core::utils::keccak256(digest_input)); diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index 1ad67e668..d90c4f8ad 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -1,7 +1,13 @@ use ethers_contract::EthAbiType; -use ethers_core::types::{ - transaction::eip712::{eip712_domain_type_hash, EIP712Domain as Domain, Eip712}, - Address, H160, U256, +use ethers_core::{ + types::{ + transaction::eip712::{ + EIP712Domain as Domain, Eip712, EIP712_DOMAIN_TYPE_HASH, + EIP712_DOMAIN_TYPE_HASH_WITH_SALT, + }, + Address, H160, U256, + }, + utils::{keccak256, parse_ether}, }; use ethers_derive_eip712::*; @@ -12,7 +18,7 @@ fn test_derive_eip712() { name = "Radicle", version = "1", chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000000" + verifying_contract = "0x0000000000000000000000000000000000000001" )] pub struct Puzzle { pub organization: H160, @@ -46,7 +52,8 @@ fn test_struct_hash() { name = "Radicle", version = "1", chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000000" + verifying_contract = "0x0000000000000000000000000000000000000001", + salt = "1234567890" )] pub struct EIP712Domain { name: String, @@ -60,6 +67,7 @@ fn test_struct_hash() { version: "1".to_string(), chain_id: U256::from(1), verifying_contract: H160::from(&[0; 20]), + salt: None, }; let domain_test = EIP712Domain { @@ -69,10 +77,7 @@ fn test_struct_hash() { verifying_contract: H160::from(&[0; 20]), }; - assert_eq!( - eip712_domain_type_hash(), - EIP712Domain::type_hash().unwrap() - ); + assert_eq!(EIP712_DOMAIN_TYPE_HASH, EIP712Domain::type_hash().unwrap()); assert_eq!(domain.separator(), domain_test.struct_hash().unwrap()); } @@ -84,7 +89,7 @@ fn test_derive_eip712_nested() { name = "MyDomain", version = "1", chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000000" + verifying_contract = "0x0000000000000000000000000000000000000001" )] pub struct MyStruct { foo: String, @@ -99,7 +104,7 @@ fn test_derive_eip712_nested() { name = "MyDomain", version = "1", chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000000" + verifying_contract = "0x0000000000000000000000000000000000000001" )] pub struct MyNestedStruct { foo: String, @@ -122,3 +127,57 @@ fn test_derive_eip712_nested() { assert_eq!(hash.len(), 32) } + +#[test] +fn test_uniswap_v2_permit_hash() { + // See examples/permit_hash.rs for comparison + // the following produces the same permit_hash as in the example + + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "Uniswap V2", + version = "1", + chain_id = 1, + verifying_contract = "0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc" + )] + struct Permit { + owner: Address, + spender: Address, + value: U256, + nonce: U256, + deadline: U256, + } + + let permit = Permit { + owner: "0x617072Cb2a1897192A9d301AC53fC541d35c4d9D" + .parse() + .unwrap(), + spender: "0x2819c144D5946404C0516B6f817a960dB37D4929" + .parse() + .unwrap(), + value: parse_ether(10).unwrap(), + nonce: U256::from(1), + deadline: U256::from(3133728498 as u32), + }; + + let permit_hash = permit.encode_eip712().unwrap(); + + assert_eq!( + hex::encode(permit_hash), + "7b90248477de48c0b971e0af8951a55974733455191480e1e117c86cc2a6cd03" + ); +} + +#[test] +fn test_domain_hash_constants() { + assert_eq!( + EIP712_DOMAIN_TYPE_HASH, + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ) + ); + assert_eq!( + EIP712_DOMAIN_TYPE_HASH_WITH_SALT, + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") + ); +} diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 671ee628a..c0499f9cb 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -14,6 +14,28 @@ use crate::{ utils::keccak256, }; +/// Pre-computed value of the following statement: +/// +/// ``` +/// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") +/// ``` +/// +pub const EIP712_DOMAIN_TYPE_HASH: [u8; 32] = [ + 139, 115, 195, 198, 155, 184, 254, 61, 81, 46, 204, 76, 247, 89, 204, 121, 35, 159, 123, 23, + 155, 15, 250, 202, 169, 167, 93, 82, 43, 57, 64, 15, +]; + +/// Pre-computed value of the following statement: +/// +/// ``` +/// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") +/// ``` +/// +pub const EIP712_DOMAIN_TYPE_HASH_WITH_SALT: [u8; 32] = [ + 216, 124, 214, 239, 121, 212, 226, 185, 94, 21, 206, 138, 191, 115, 45, 181, 30, 199, 113, 241, + 202, 46, 220, 207, 34, 164, 108, 114, 154, 197, 100, 114, +]; + /// Error typed used by Eip712 derive macro #[derive(Debug, thiserror::Error)] pub enum Eip712Error { @@ -43,13 +65,6 @@ pub trait Eip712 { /// User defined error type; type Error: std::error::Error + Send + Sync + std::fmt::Debug; - /// The eip712 domain is the same for all Eip712 implementations, - /// This method does not need to be manually implemented, but may be overridden - /// if needed. - fn eip712_domain_type_hash() -> Result<[u8; 32], Self::Error> { - Ok(eip712_domain_type_hash()) - } - /// The domain separator depends on the contract and unique domain /// for which the user is targeting. In the derive macro, these attributes /// are passed in as arguments to the macro. When manually deriving, the user @@ -71,32 +86,50 @@ pub trait Eip712 { fn encode_eip712(self) -> Result<[u8; 32], Self::Error>; } -/// This method provides the hex encoded domain type hash for EIP712Domain type; -/// This is used by all Eip712 structs. -pub fn eip712_domain_type_hash() -> [u8; 32] { - keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") -} - /// Eip712 Domain attributes used in determining the domain separator; +/// Unused fields are left out of the struct type. #[derive(Debug, Default)] pub struct EIP712Domain { + /// The user readable name of signing domain, i.e. the name of the DApp or the protocol. pub name: String, + + /// The current major version of the signing domain. Signatures from different versions are not compatible. pub version: String, + + /// The EIP-155 chain id. The user-agent should refuse signing if it does not match the currently active chain. pub chain_id: U256, + + /// The address of the contract that will verify the signature. pub verifying_contract: Address, + + /// A disambiguating salt for the protocol. This can be used as a domain separator of last resort. + pub salt: Option<[u8; 32]>, } impl EIP712Domain { // Compute the domain separator; // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L41 pub fn separator(&self) -> [u8; 32] { - keccak256(abi::encode(&[ - Token::Uint(U256::from(eip712_domain_type_hash())), + let domain_type_hash = if self.salt.is_some() { + EIP712_DOMAIN_TYPE_HASH_WITH_SALT + } else { + EIP712_DOMAIN_TYPE_HASH + }; + + let mut tokens = vec![ + Token::Uint(U256::from(domain_type_hash)), Token::Uint(U256::from(keccak256(&self.name))), Token::Uint(U256::from(keccak256(&self.version))), Token::Uint(self.chain_id), Token::Address(self.verifying_contract), - ])) + ]; + + // Add the salt to the struct to be hashed if it exists; + if let Some(salt) = &self.salt { + tokens.push(Token::Uint(U256::from(salt))); + } + + keccak256(abi::encode(&tokens)) } } @@ -105,11 +138,14 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain { type Error = TokenStream; fn try_from(input: &syn::DeriveInput) -> Result { let mut domain = EIP712Domain::default(); + let mut found_eip712_attribute = false; for attribute in input.attrs.iter() { if let AttrStyle::Outer = attribute.style { if let Ok(syn::Meta::List(meta)) = attribute.parse_meta() { if meta.path.is_ident("eip712") { + found_eip712_attribute = true; + for n in meta.nested.iter() { if let NestedMeta::Meta(meta) = n { match meta { @@ -219,6 +255,29 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain { .to_compile_error()); } }, + "salt" => match meta.lit { + syn::Lit::Str(ref lit_str) => { + if domain.salt != Option::None { + return Err(Error::new( + meta.path.span(), + "domain salt already specified", + ) + .to_compile_error()); + } + + // keccak256() to compute bytes32 encoded domain salt + let salt = keccak256(lit_str.value()); + + domain.salt = Some(salt); + } + _ => { + return Err(Error::new( + meta.path.span(), + "domain salt must be a string", + ) + .to_compile_error()); + } + }, _ => { return Err(Error::new( meta.path.span(), @@ -245,17 +304,56 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain { } } } + + if domain.name == String::default() { + return Err(Error::new( + meta.path.span(), + "missing required domain attribute: 'name'".to_string(), + ) + .to_compile_error()); + } + if domain.version == String::default() { + return Err(Error::new( + meta.path.span(), + "missing required domain attribute: 'version'".to_string(), + ) + .to_compile_error()); + } + if domain.chain_id == U256::default() { + return Err(Error::new( + meta.path.span(), + "missing required domain attribute: 'chain_id'".to_string(), + ) + .to_compile_error()); + } + if domain.verifying_contract == H160::default() { + return Err(Error::new( + meta.path.span(), + "missing required domain attribute: 'verifying_contract'" + .to_string(), + ) + .to_compile_error()); + } } } } } + if !found_eip712_attribute { + return Err(Error::new_spanned( + input, + "missing required derive attribute: '#[eip712( ... )]'".to_string(), + ) + .to_compile_error()); + } + Ok(domain) } } /// Parse the eth abi parameter type based on the syntax type; /// this method is copied from https://github.com/gakonst/ethers-rs/blob/master/ethers-contract/ethers-contract-derive/src/lib.rs#L600 +/// with additional modifications for finding byte arrays pub fn find_parameter_type(ty: &Type) -> Result { match ty { Type::Array(ty) => { @@ -263,6 +361,10 @@ pub fn find_parameter_type(ty: &Type) -> Result { if let Expr::Lit(ref expr) = ty.len { if let Lit::Int(ref len) = expr.lit { if let Ok(size) = len.base10_parse::() { + if let ParamType::Uint(_) = param { + return Ok(ParamType::FixedBytes(size)); + } + return Ok(ParamType::FixedArray(Box::new(param), size)); } } @@ -278,13 +380,17 @@ pub fn find_parameter_type(ty: &Type) -> Result { "address" => Ok(ParamType::Address), "string" => Ok(ParamType::String), "bool" => Ok(ParamType::Bool), - "int" | "uint" => Ok(ParamType::Uint(256)), + "int256" | "int" | "uint" | "uint256" => Ok(ParamType::Uint(256)), "h160" => Ok(ParamType::FixedBytes(20)), "h256" | "secret" | "hash" => Ok(ParamType::FixedBytes(32)), "h512" | "public" => Ok(ParamType::FixedBytes(64)), + "bytes" => Ok(ParamType::Bytes), s => parse_int_param_type(s).ok_or_else(|| { - Error::new(ty.span(), "Failed to derive proper ABI from field") - .to_compile_error() + Error::new( + ty.span(), + format!("Failed to derive proper ABI from field: {})", s), + ) + .to_compile_error() }), }; } @@ -294,6 +400,14 @@ pub fn find_parameter_type(ty: &Type) -> Result { if args.args.len() == 1 { if let GenericArgument::Type(ref ty) = args.args.iter().next().unwrap() { let kind = find_parameter_type(ty)?; + + // Check if byte array is found + if let ParamType::Uint(size) = kind { + if size == 8 { + return Ok(ParamType::Bytes); + } + } + return Ok(ParamType::Array(Box::new(kind))); } } @@ -398,6 +512,8 @@ pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [ let sig = format!("{}({})", primary_type, parameters); + println!("Type Hash: {:?}", sig); + keccak256(sig) } From 55e2cb67d51f96b9922edcebfe1ca5cf3be0e620 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 12:20:09 -0700 Subject: [PATCH 05/30] replace std::convert::Infallible with WalletError as Wallet signer error type --- ethers-signers/src/wallet/mod.rs | 12 ++++-------- ethers-signers/src/wallet/private_key.rs | 3 +++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index eee13236d..4331b3890 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -70,7 +70,7 @@ pub struct Wallet> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl> Signer for Wallet { - type Error = std::convert::Infallible; + type Error = WalletError; async fn sign_message>( &self, @@ -90,13 +90,9 @@ impl> Signer fo &self, payload: T, ) -> Result { - let encoded = match payload.encode_eip712() { - Ok(e) => e, - Err(_) => { - // NOTE: Still looking for a better solution for mapping to Infallible error type; - unreachable!() - } - }; + let encoded = payload + .encode_eip712() + .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; Ok(self.sign_hash(H256::from(encoded), false)) } diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs index ec2e86151..2314cc81e 100644 --- a/ethers-signers/src/wallet/private_key.rs +++ b/ethers-signers/src/wallet/private_key.rs @@ -43,6 +43,9 @@ pub enum WalletError { /// Error propagated from the mnemonic builder module. #[error(transparent)] MnemonicBuilderError(#[from] MnemonicBuilderError), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), } impl Clone for Wallet { From c98321c1c1d99e76e8b936d8c2457685a79bf988 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 12:20:46 -0700 Subject: [PATCH 06/30] update workspace members and dev dependencies for examples folder --- Cargo.lock | 4 ++++ Cargo.toml | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69de5f985..c43bacd18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -847,11 +847,14 @@ name = "ethers" version = "0.5.3" dependencies = [ "anyhow", + "bytes", "ethers-contract", "ethers-core", + "ethers-derive-eip712", "ethers-middleware", "ethers-providers", "ethers-signers", + "hex", "rand 0.8.4", "serde", "serde_json", @@ -949,6 +952,7 @@ version = "0.1.0" dependencies = [ "ethers-contract", "ethers-core", + "ethers-signers", "hex", "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 6bb43debd..24a2d5317 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,7 +34,6 @@ default-members = [ exclude = [ "examples/ethers-wasm", - "ethers-core/derive-eip712" ] [package.metadata.docs.rs] @@ -79,6 +78,7 @@ ethers-core = { version = "^0.5.0", default-features = false, path = "./ethers-c ethers-providers = { version = "^0.5.0", default-features = false, path = "./ethers-providers" } ethers-signers = { version = "^0.5.0", default-features = false, path = "./ethers-signers" } ethers-middleware = { version = "^0.5.0", default-features = false, path = "./ethers-middleware" } +ethers-derive-eip712 = { version = "0.1.0", path = "./ethers-core/ethers-derive-eip712"} [dev-dependencies] ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract", features = ["abigen"] } @@ -89,4 +89,5 @@ rand = "0.8.4" serde = { version = "1.0.124", features = ["derive"] } serde_json = "1.0.64" tokio = { version = "1.5", features = ["macros", "rt-multi-thread"] } - +hex = "0.4.3" +bytes = "1.1.0" \ No newline at end of file From 4975f672cd2d5b004b4b0ae93c15a879b16a52e6 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 12:21:04 -0700 Subject: [PATCH 07/30] add example for eip712 and test against contract --- examples/DeriveEip712Test.sol | 82 ++++++++++++++++++++ examples/derive_eip712_abi.json | 131 ++++++++++++++++++++++++++++++++ examples/eip712.rs | 128 +++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 examples/DeriveEip712Test.sol create mode 100644 examples/derive_eip712_abi.json create mode 100644 examples/eip712.rs diff --git a/examples/DeriveEip712Test.sol b/examples/DeriveEip712Test.sol new file mode 100644 index 000000000..ea7d507ed --- /dev/null +++ b/examples/DeriveEip712Test.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity ^0.8.0; + +contract DeriveEip712Test { + uint256 constant chainId = 1; + bytes32 constant salt = keccak256("eip712-test-75F0CCte"); + bytes32 constant EIP712_DOMAIN_TYPEHASH = + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)" + ); + + bytes32 constant FOOBAR_DOMAIN_TYPEHASH = + keccak256( + // "FooBar(int256 foo,uint256 bar,bytes fizz,bytes32 buzz,string far,address out)" + "FooBar(int256 foo)" + ); + + struct FooBar { + int256 foo; + // uint256 bar; + // bytes fizz; + // bytes32 buzz; + // string far; + // address out; + } + + constructor() {} + + function domainSeparator() public pure returns (bytes32) { + return + keccak256( + abi.encode( + EIP712_DOMAIN_TYPEHASH, + keccak256("Eip712Test"), + keccak256("1"), + chainId, + address(0x0000000000000000000000000000000000000001), + salt + ) + ); + } + + function typeHash() public pure returns (bytes32) { + return FOOBAR_DOMAIN_TYPEHASH; + } + + function structHash(FooBar memory fooBar) public pure returns (bytes32) { + return + keccak256( + abi.encode( + typeHash(), + uint256(fooBar.foo) + // fooBar.bar, + // keccak256(fooBar.fizz), + // fooBar.buzz, + // keccak256(abi.encodePacked(fooBar.far)), + // fooBar.out + ) + ); + } + + function encodeEip712(FooBar memory fooBar) public pure returns (bytes32) { + return + keccak256( + abi.encodePacked( + "\\x19\\x01", + domainSeparator(), + structHash(fooBar) + ) + ); + } + + function verifyFooBar( + address signer, + FooBar memory fooBar, + bytes32 r, + bytes32 s, + uint8 v + ) public pure returns (bool) { + return signer == ecrecover(encodeEip712(fooBar), v, r, s); + } +} diff --git a/examples/derive_eip712_abi.json b/examples/derive_eip712_abi.json new file mode 100644 index 000000000..087a553be --- /dev/null +++ b/examples/derive_eip712_abi.json @@ -0,0 +1,131 @@ +[ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "domainSeparator", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "foo", + "type": "int256" + } + ], + "internalType": "struct DeriveEip712Test.FooBar", + "name": "fooBar", + "type": "tuple" + } + ], + "name": "encodeEip712", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "int256", + "name": "foo", + "type": "int256" + } + ], + "internalType": "struct DeriveEip712Test.FooBar", + "name": "fooBar", + "type": "tuple" + } + ], + "name": "structHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [], + "name": "typeHash", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "pure", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "components": [ + { + "internalType": "int256", + "name": "foo", + "type": "int256" + } + ], + "internalType": "struct DeriveEip712Test.FooBar", + "name": "fooBar", + "type": "tuple" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + } + ], + "name": "verifyFooBar", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/examples/eip712.rs b/examples/eip712.rs new file mode 100644 index 000000000..1d6edd683 --- /dev/null +++ b/examples/eip712.rs @@ -0,0 +1,128 @@ +use std::{convert::TryFrom, sync::Arc, time::Duration}; + +use ethers::{ + contract::EthAbiType, + prelude::*, + types::{transaction::eip712::Eip712, Address, I256, U256}, + utils::{compile_and_launch_ganache, keccak256, Ganache, Solc}, +}; +use ethers_derive_eip712::*; + +#[derive(Debug, Clone, Eip712, EthAbiType)] +#[eip712( + name = "Eip712Test", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000001", + salt = "eip712-test-75F0CCte" +)] +struct FooBar { + foo: I256, + // bar: U256, + // fizz: Vec, + // buzz: [u8; 32], + // far: String, + // out: Address, +} + +abigen!( + DeriveEip712Test, + "./examples/derive_eip712_abi.json", + event_derives(serde::Deserialize, serde::Serialize) +); + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let contract_name = "DeriveEip712Test".to_string(); + let (compiled, ganache) = + compile_and_launch_ganache(Solc::new("**/DeriveEip712Test.sol"), Ganache::new()).await?; + + let wallet: LocalWallet = ganache.keys()[0].clone().into(); + + let contract = compiled.get(&contract_name).unwrap(); + + let provider = + Provider::::try_from(ganache.endpoint())?.interval(Duration::from_millis(10u64)); + + let client = SignerMiddleware::new(provider, wallet.clone()); + let client = Arc::new(client); + + let factory = ContractFactory::new( + contract.abi.clone(), + contract.bytecode.clone(), + client.clone(), + ); + + let contract = factory.deploy(())?.legacy().send().await?; + + let addr = contract.address(); + + let contract = DeriveEip712Test::new(addr, client.clone()); + + let foo_bar = FooBar { + foo: I256::from(10), + // bar: U256::from(20), + // fizz: b"fizz".to_vec(), + // buzz: keccak256("buzz"), + // far: String::from("space"), + // out: Address::from([0; 20]), + }; + + let derived_foo_bar = deriveeip712test_mod::FooBar { + foo: foo_bar.foo.clone(), + // bar: foo_bar.bar.clone(), + // fizz: foo_bar.fizz.clone(), + // buzz: foo_bar.buzz.clone(), + // far: foo_bar.far.clone(), + // out: foo_bar.out.clone(), + }; + + let sig = wallet.sign_typed_data(foo_bar.clone()).await?; + + let mut r = [0; 32]; + let mut s = [0; 32]; + let v = u8::try_from(sig.v)?; + + sig.r.to_big_endian(&mut r); + sig.r.to_big_endian(&mut s); + + let domain_separator = contract.domain_separator().call().await?; + let type_hash = contract.type_hash().call().await?; + let struct_hash = contract.struct_hash(derived_foo_bar.clone()).call().await?; + let encoded = contract + .encode_eip_712(derived_foo_bar.clone()) + .call() + .await?; + let verify = contract + .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v) + .call() + .await?; + + assert_eq!( + domain_separator, + FooBar::domain_separator()?, + "domain separator does not match contract domain separator!" + ); + + assert_eq!( + type_hash, + FooBar::type_hash()?, + "type hash does not match contract struct type hash!" + ); + + assert_eq!( + struct_hash, + foo_bar.clone().struct_hash()?, + "struct hash does not match contract struct struct hash!" + ); + + assert_eq!( + encoded, + foo_bar.encode_eip712()?, + "Encoded value does not match!" + ); + + assert_eq!(verify, true, "typed data signature failed!"); + + Ok(()) +} From 714e2fbe853063eaa23e34bdb9dfe083e6cb338a Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 17:30:52 -0700 Subject: [PATCH 08/30] remove extraneous backward slash in '\x19\x01' prefix; example tests pass --- ethers-core/ethers-derive-eip712/src/lib.rs | 2 + examples/DeriveEip712Test.sol | 27 ++++---- examples/derive_eip712_abi.json | 75 +++++++++++++++++++++ examples/eip712.rs | 37 +++++----- 4 files changed, 107 insertions(+), 34 deletions(-) diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 0737dee0d..118dc7bf8 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -153,6 +153,8 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { &self.struct_hash()?[..] ].concat(); + // let digest_input = &[0x19, 0x01]; + return Ok(ethers_core::utils::keccak256(digest_input)); } diff --git a/examples/DeriveEip712Test.sol b/examples/DeriveEip712Test.sol index ea7d507ed..73a591299 100644 --- a/examples/DeriveEip712Test.sol +++ b/examples/DeriveEip712Test.sol @@ -11,17 +11,16 @@ contract DeriveEip712Test { bytes32 constant FOOBAR_DOMAIN_TYPEHASH = keccak256( - // "FooBar(int256 foo,uint256 bar,bytes fizz,bytes32 buzz,string far,address out)" - "FooBar(int256 foo)" + "FooBar(int256 foo,uint256 bar,bytes fizz,bytes32 buzz,string far,address out)" ); struct FooBar { int256 foo; - // uint256 bar; - // bytes fizz; - // bytes32 buzz; - // string far; - // address out; + uint256 bar; + bytes fizz; + bytes32 buzz; + string far; + address out; } constructor() {} @@ -49,12 +48,12 @@ contract DeriveEip712Test { keccak256( abi.encode( typeHash(), - uint256(fooBar.foo) - // fooBar.bar, - // keccak256(fooBar.fizz), - // fooBar.buzz, - // keccak256(abi.encodePacked(fooBar.far)), - // fooBar.out + uint256(fooBar.foo), + fooBar.bar, + keccak256(fooBar.fizz), + fooBar.buzz, + keccak256(bytes(fooBar.far)), + fooBar.out ) ); } @@ -63,7 +62,7 @@ contract DeriveEip712Test { return keccak256( abi.encodePacked( - "\\x19\\x01", + "\x19\x01", domainSeparator(), structHash(fooBar) ) diff --git a/examples/derive_eip712_abi.json b/examples/derive_eip712_abi.json index 087a553be..a978024eb 100644 --- a/examples/derive_eip712_abi.json +++ b/examples/derive_eip712_abi.json @@ -25,6 +25,31 @@ "internalType": "int256", "name": "foo", "type": "int256" + }, + { + "internalType": "uint256", + "name": "bar", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "fizz", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "buzz", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "far", + "type": "string" + }, + { + "internalType": "address", + "name": "out", + "type": "address" } ], "internalType": "struct DeriveEip712Test.FooBar", @@ -51,6 +76,31 @@ "internalType": "int256", "name": "foo", "type": "int256" + }, + { + "internalType": "uint256", + "name": "bar", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "fizz", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "buzz", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "far", + "type": "string" + }, + { + "internalType": "address", + "name": "out", + "type": "address" } ], "internalType": "struct DeriveEip712Test.FooBar", @@ -95,6 +145,31 @@ "internalType": "int256", "name": "foo", "type": "int256" + }, + { + "internalType": "uint256", + "name": "bar", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "fizz", + "type": "bytes" + }, + { + "internalType": "bytes32", + "name": "buzz", + "type": "bytes32" + }, + { + "internalType": "string", + "name": "far", + "type": "string" + }, + { + "internalType": "address", + "name": "out", + "type": "address" } ], "internalType": "struct DeriveEip712Test.FooBar", diff --git a/examples/eip712.rs b/examples/eip712.rs index 1d6edd683..a96b3e20c 100644 --- a/examples/eip712.rs +++ b/examples/eip712.rs @@ -18,11 +18,11 @@ use ethers_derive_eip712::*; )] struct FooBar { foo: I256, - // bar: U256, - // fizz: Vec, - // buzz: [u8; 32], - // far: String, - // out: Address, + bar: U256, + fizz: Vec, + buzz: [u8; 32], + far: String, + out: Address, } abigen!( @@ -61,31 +61,28 @@ async fn main() -> anyhow::Result<()> { let foo_bar = FooBar { foo: I256::from(10), - // bar: U256::from(20), - // fizz: b"fizz".to_vec(), - // buzz: keccak256("buzz"), - // far: String::from("space"), - // out: Address::from([0; 20]), + bar: U256::from(20), + fizz: b"fizz".to_vec(), + buzz: keccak256("buzz"), + far: String::from("space"), + out: Address::from([0; 20]), }; let derived_foo_bar = deriveeip712test_mod::FooBar { foo: foo_bar.foo.clone(), - // bar: foo_bar.bar.clone(), - // fizz: foo_bar.fizz.clone(), - // buzz: foo_bar.buzz.clone(), - // far: foo_bar.far.clone(), - // out: foo_bar.out.clone(), + bar: foo_bar.bar.clone(), + fizz: foo_bar.fizz.clone(), + buzz: foo_bar.buzz.clone(), + far: foo_bar.far.clone(), + out: foo_bar.out.clone(), }; let sig = wallet.sign_typed_data(foo_bar.clone()).await?; - let mut r = [0; 32]; - let mut s = [0; 32]; + let r = <[u8; 32]>::try_from(sig.r)?; + let s = <[u8; 32]>::try_from(sig.s)?; let v = u8::try_from(sig.v)?; - sig.r.to_big_endian(&mut r); - sig.r.to_big_endian(&mut s); - let domain_separator = contract.domain_separator().call().await?; let type_hash = contract.type_hash().call().await?; let struct_hash = contract.struct_hash(derived_foo_bar.clone()).call().await?; From 51860ea1229f10b736648193dffa0269724bf0a8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 18:05:18 -0700 Subject: [PATCH 09/30] update unreleased change log --- CHANGELOG.md | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 987c99585..f552fc1c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,20 @@ ### Unreleased -* Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) -* +- Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) +- Add EIP-712 `sign_typed_data` signer method; add ethers-core type `Eip712` trait and derive macro in ethers-derive-eip712 [#481](https://github.com/gakonst/ethers-rs/pull/481) + ### 0.5.3 -* Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427) -* Decimal support for `ethers_core::utils::parse_units` [#463](https://github.com/gakonst/ethers-rs/pull/463) -* Fixed Wei unit calculation in `Units` [#460](https://github.com/gakonst/ethers-rs/pull/460) -* Add `ethers_core::utils::get_create2_address_from_hash` [#444](https://github.com/gakonst/ethers-rs/pull/444) -* Bumped ethabi to 0.15.0 and fixing breaking changes [#469](https://github.com/gakonst/ethers-rs/pull/469), [#448](https://github.com/gakonst/ethers-rs/pull/448), [#445](https://github.com/gakonst/ethers-rs/pull/445) +- Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427) +- Decimal support for `ethers_core::utils::parse_units` [#463](https://github.com/gakonst/ethers-rs/pull/463) +- Fixed Wei unit calculation in `Units` [#460](https://github.com/gakonst/ethers-rs/pull/460) +- Add `ethers_core::utils::get_create2_address_from_hash` [#444](https://github.com/gakonst/ethers-rs/pull/444) +- Bumped ethabi to 0.15.0 and fixing breaking changes [#469](https://github.com/gakonst/ethers-rs/pull/469), [#448](https://github.com/gakonst/ethers-rs/pull/448), [#445](https://github.com/gakonst/ethers-rs/pull/445) ### 0.5.2 -* Correctly RLP Encode transactions as received from the mempool ([#415](https://github.com/gakonst/ethers-rs/pull/415)) + +- Correctly RLP Encode transactions as received from the mempool ([#415](https://github.com/gakonst/ethers-rs/pull/415)) ## ethers-providers @@ -23,11 +25,12 @@ ### 0.5.3 -* Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435) -* Add `eth_getProof` [#459](https://github.com/gakonst/ethers-rs/pull/459) +- Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435) +- Add `eth_getProof` [#459](https://github.com/gakonst/ethers-rs/pull/459) ### 0.5.2 -* Set resolved ENS name during gas estimation ([1e5a9e](https://github.com/gakonst/ethers-rs/commit/1e5a9efb3c678eecd43d5c341b4932da35445831)) + +- Set resolved ENS name during gas estimation ([1e5a9e](https://github.com/gakonst/ethers-rs/commit/1e5a9efb3c678eecd43d5c341b4932da35445831)) ## ethers-signers @@ -38,7 +41,8 @@ ### Unreleased ### 0.5.3 -* (De)Tokenize structs and events with only a single field as `Token:Tuple` ([#417](https://github.com/gakonst/ethers-rs/pull/417)) + +- (De)Tokenize structs and events with only a single field as `Token:Tuple` ([#417](https://github.com/gakonst/ethers-rs/pull/417)) ## ethers-middleware @@ -46,4 +50,4 @@ ### 0.5.3 -* Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457) \ No newline at end of file +- Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457) From 801a661ea71f438f8e0ef2bd079f30667a384f3c Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 18:13:30 -0700 Subject: [PATCH 10/30] remove print statements --- ethers-core/ethers-derive-eip712/tests/derive_eip712.rs | 2 -- ethers-core/src/types/transaction/eip712.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index d90c4f8ad..2519fa6c7 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -40,8 +40,6 @@ fn test_derive_eip712() { let hash = puzzle.encode_eip712().expect("failed to encode struct"); - println!("Hash: {:?}", hash); - assert_eq!(hash.len(), 32) } diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index c0499f9cb..4c01c77fb 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -512,8 +512,6 @@ pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [ let sig = format!("{}({})", primary_type, parameters); - println!("Type Hash: {:?}", sig); - keccak256(sig) } From 1f99caca6c2e24faa71f7a76c117ec92755e8e1f Mon Sep 17 00:00:00 2001 From: Ryan Date: Tue, 5 Oct 2021 21:18:37 -0700 Subject: [PATCH 11/30] use parse_macro_input macro; remove dead code; handle nest struct not implemented error --- ethers-core/ethers-derive-eip712/src/lib.rs | 10 +++++----- ethers-core/src/types/transaction/eip712.rs | 2 ++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 118dc7bf8..6726ab681 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -65,10 +65,11 @@ use std::convert::TryFrom; use ethers_core::types::transaction::eip712; use proc_macro::TokenStream; use quote::quote; +use syn::parse_macro_input; #[proc_macro_derive(Eip712, attributes(eip712))] pub fn eip_712_derive(input: TokenStream) -> TokenStream { - let ast = syn::parse(input).expect("failed to parse token stream for Eip712 derived struct"); + let ast = parse_macro_input!(input); impl_eip_712_macro(&ast) } @@ -105,14 +106,14 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { fn type_hash() -> Result<[u8; 32], Self::Error> { use std::convert::TryFrom; - let decoded = hex::decode(#type_hash.to_string())?; + let decoded = hex::decode(#type_hash)?; let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; Ok(byte_array) } fn domain_separator() -> Result<[u8; 32], Self::Error> { use std::convert::TryFrom; - let decoded = hex::decode(#domain_separator.to_string())?; + let decoded = hex::decode(#domain_separator)?; let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; Ok(byte_array) } @@ -129,6 +130,7 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { ethers_core::abi::Token::Tuple(t) => { // TODO: check for nested Eip712 Type; // Challenge is determining the type hash + return Err(Self::Error::NestedEip712StructNotImplemented); }, _ => { items.push(ethers_core::types::transaction::eip712::encode_eip712_type(token)); @@ -153,8 +155,6 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { &self.struct_hash()?[..] ].concat(); - // let digest_input = &[0x19, 0x01]; - return Ok(ethers_core::utils::keccak256(digest_input)); } diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 4c01c77fb..cd70945db 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -47,6 +47,8 @@ pub enum Eip712Error { FailedToEncodeStruct, #[error("Failed to convert slice into byte array")] TryFromSliceError(#[from] std::array::TryFromSliceError), + #[error("Nested Eip712 struct not implemented. Failed to parse.")] + NestedEip712StructNotImplemented, } /// The Eip712 trait provides helper methods for computing From 76d3f05703c7f771098e8df413bda32b960cbc37 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 11:23:27 -0700 Subject: [PATCH 12/30] move eip712 example to solidity-contract tests folder; update cargo workspace dependencies --- Cargo.lock | 3 +- Cargo.toml | 4 +- ethers-contract/Cargo.toml | 3 + ethers-contract/tests/contract.rs | 156 +++++++++++++++++- .../solidity-contracts}/DeriveEip712Test.sol | 0 .../derive_eip712_abi.json | 0 ethers-core/Cargo.toml | 1 + ethers-core/ethers-derive-eip712/Cargo.toml | 2 +- ethers-core/src/lib.rs | 1 + ethers-core/src/types/transaction/eip712.rs | 2 +- ethers-core/src/types/transaction/mod.rs | 2 + ethers-signers/src/lib.rs | 4 +- examples/eip712.rs | 125 -------------- 13 files changed, 167 insertions(+), 136 deletions(-) rename {examples => ethers-contract/tests/solidity-contracts}/DeriveEip712Test.sol (100%) rename {examples => ethers-contract/tests/solidity-contracts}/derive_eip712_abi.json (100%) delete mode 100644 examples/eip712.rs diff --git a/Cargo.lock b/Cargo.lock index c43bacd18..b6a96ca2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -850,7 +850,6 @@ dependencies = [ "bytes", "ethers-contract", "ethers-core", - "ethers-derive-eip712", "ethers-middleware", "ethers-providers", "ethers-signers", @@ -868,6 +867,8 @@ dependencies = [ "ethers-contract-abigen", "ethers-contract-derive", "ethers-core", + "ethers-derive-eip712", + "ethers-middleware", "ethers-providers", "ethers-signers", "futures-util", diff --git a/Cargo.toml b/Cargo.toml index 24a2d5317..da413e10c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,6 @@ members = [ "ethers-core", "ethers-middleware", "ethers-etherscan", - "ethers-core/ethers-derive-eip712", ] default-members = [ @@ -61,6 +60,7 @@ legacy = [ # individual features per sub-crate ## core setup = ["ethers-core/setup"] +eip712 = ["ethers-core/eip712"] ## providers ws = ["ethers-providers/ws"] ipc = ["ethers-providers/ipc"] @@ -72,13 +72,13 @@ yubi = ["ethers-signers/yubi"] ## contracts abigen = ["ethers-contract/abigen"] + [dependencies] ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract" } ethers-core = { version = "^0.5.0", default-features = false, path = "./ethers-core", features = ["setup"] } ethers-providers = { version = "^0.5.0", default-features = false, path = "./ethers-providers" } ethers-signers = { version = "^0.5.0", default-features = false, path = "./ethers-signers" } ethers-middleware = { version = "^0.5.0", default-features = false, path = "./ethers-middleware" } -ethers-derive-eip712 = { version = "0.1.0", path = "./ethers-core/ethers-derive-eip712"} [dev-dependencies] ethers-contract = { version = "^0.5.0", default-features = false, path = "./ethers-contract", features = ["abigen"] } diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 41e0523d0..e08209565 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -24,10 +24,13 @@ futures-util = { version = "0.3.17" } hex = { version = "0.4.3", default-features = false, features = ["std"] } [dev-dependencies] +ethers-middleware = { version = "^0.5.0", path = "../ethers-middleware" } ethers-providers = { version = "^0.5.0", path = "../ethers-providers", default-features = false, features = ["ws"] } ethers-signers = { version = "^0.5.0", path = "../ethers-signers" } ethers-contract-abigen = { version = "^0.5.0", path = "ethers-contract-abigen" } ethers-contract-derive = { version = "^0.5.0", path = "ethers-contract-derive" } +ethers-derive-eip712 = { version = "0.1.0", path = "../ethers-core/ethers-derive-eip712"} + [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.5", default-features = false, features = ["macros"] } diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 39a4f45c6..b7ac17282 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -1,4 +1,4 @@ -use ethers_contract::ContractFactory; +use ethers_contract::{abigen, ContractFactory, EthAbiType}; use ethers_core::types::{Filter, ValueOrArray, H256}; mod common; @@ -9,11 +9,14 @@ mod eth_tests { use super::*; use ethers_contract::{LogMeta, Multicall}; use ethers_core::{ - types::{Address, BlockId, U256}, - utils::Ganache, + types::{transaction::eip712::Eip712, Address, BlockId, I256, U256}, + utils::{keccak256, Ganache}, }; + use ethers_derive_eip712::*; + use ethers_middleware::signer::SignerMiddleware; use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt}; - use std::{convert::TryFrom, sync::Arc}; + use ethers_signers::{LocalWallet, Signer}; + use std::{convert::TryFrom, sync::Arc, time::Duration}; #[tokio::test] async fn deploy_and_call_contract() { @@ -580,4 +583,149 @@ mod eth_tests { assert_eq!(balances.1, U256::from(100000000000000000000u128)); assert_eq!(balances.2, U256::from(100000000000000000000u128)); } + + #[tokio::test] + async fn test_derive_eip712() { + // Generate Contract ABI Bindings + abigen!( + DeriveEip712Test, + "./ethers-contract/tests/solidity-contracts/derive_eip712_abi.json", + event_derives(serde::Deserialize, serde::Serialize) + ); + + // Create derived structs + + #[derive(Debug, Clone, Eip712, EthAbiType)] + #[eip712( + name = "Eip712Test", + version = "1", + chain_id = 1, + verifying_contract = "0x0000000000000000000000000000000000000001", + salt = "eip712-test-75F0CCte" + )] + struct FooBar { + foo: I256, + bar: U256, + fizz: Vec, + buzz: [u8; 32], + far: String, + out: Address, + } + + // get ABI and bytecode for the DeriveEip712Test contract + let (abi, bytecode) = compile_contract("DeriveEip712Test", "DeriveEip712Test.sol"); + + // launch ganache + let ganache = Ganache::new().spawn(); + + let wallet: LocalWallet = ganache.keys()[0].clone().into(); + + let provider = Provider::::try_from(ganache.endpoint()) + .expect("failed to instantiate provider from ganache endpoint") + .interval(Duration::from_millis(10u64)); + + let client = SignerMiddleware::new(provider, wallet.clone()); + let client = Arc::new(client); + + let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone()); + + let contract = factory + .deploy(()) + .expect("failed to deploy DeriveEip712Test contract") + .legacy() + .send() + .await + .expect("failed to instantiate factory for DeriveEip712 contract"); + + let addr = contract.address(); + + let contract = DeriveEip712Test::new(addr, client.clone()); + + let foo_bar = FooBar { + foo: I256::from(10), + bar: U256::from(20), + fizz: b"fizz".to_vec(), + buzz: keccak256("buzz"), + far: String::from("space"), + out: Address::from([0; 20]), + }; + + let derived_foo_bar = deriveeip712test_mod::FooBar { + foo: foo_bar.foo.clone(), + bar: foo_bar.bar.clone(), + fizz: foo_bar.fizz.clone(), + buzz: foo_bar.buzz.clone(), + far: foo_bar.far.clone(), + out: foo_bar.out.clone(), + }; + + let sig = wallet + .sign_typed_data(foo_bar.clone()) + .await + .expect("failed to sign typed data"); + + let r = <[u8; 32]>::try_from(sig.r) + .expect("failed to parse 'r' value from signature into [u8; 32]"); + let s = <[u8; 32]>::try_from(sig.s) + .expect("failed to parse 's' value from signature into [u8; 32]"); + let v = u8::try_from(sig.v).expect("failed to parse 'v' value from signature into u8"); + + let domain_separator = contract + .domain_separator() + .call() + .await + .expect("failed to retrieve domain_separator from contract"); + let type_hash = contract + .type_hash() + .call() + .await + .expect("failed to retrieve type_hash from contract"); + let struct_hash = contract + .struct_hash(derived_foo_bar.clone()) + .call() + .await + .expect("failed to retrieve struct_hash from contract"); + let encoded = contract + .encode_eip_712(derived_foo_bar.clone()) + .call() + .await + .expect("failed to retrieve eip712 encoded hash from contract"); + let verify = contract + .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v) + .call() + .await + .expect("failed to verify signed typed data eip712 payload"); + + assert_eq!( + domain_separator, + FooBar::domain_separator() + .expect("failed to return domain_separator from Eip712 implemented struct"), + "domain separator does not match contract domain separator!" + ); + + assert_eq!( + type_hash, + FooBar::type_hash().expect("failed to return type_hash from Eip712 implemented struct"), + "type hash does not match contract struct type hash!" + ); + + assert_eq!( + struct_hash, + foo_bar + .clone() + .struct_hash() + .expect("failed to return struct_hash from Eip712 implemented struct"), + "struct hash does not match contract struct hash!" + ); + + assert_eq!( + encoded, + foo_bar + .encode_eip712() + .expect("failed to return domain_separator from Eip712 implemented struct"), + "Encoded value does not match!" + ); + + assert_eq!(verify, true, "typed data signature failed!"); + } } diff --git a/examples/DeriveEip712Test.sol b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol similarity index 100% rename from examples/DeriveEip712Test.sol rename to ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol diff --git a/examples/derive_eip712_abi.json b/ethers-contract/tests/solidity-contracts/derive_eip712_abi.json similarity index 100% rename from examples/derive_eip712_abi.json rename to ethers-contract/tests/solidity-contracts/derive_eip712_abi.json diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index b05a77dcb..a5665f2cf 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -53,6 +53,7 @@ futures-util = { version = "0.3.17" } celo = ["legacy"] # celo support extends the transaction format with extra fields setup = ["tokio", "futures-util"] # async support for concurrent setup legacy = [] +eip712 = [] [package.metadata.docs.rs] all-features = true diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml index 7d939be89..c9f01a802 100644 --- a/ethers-core/ethers-derive-eip712/Cargo.toml +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -10,7 +10,7 @@ proc-macro = true [dependencies] quote = "1.0.9" syn = "1.0.77" -ethers-core = { version = "^0.5.0", path = "../"} +ethers-core = { version = "^0.5.0", path = "../", default-features = false, features = ["eip712"] } ethers-contract = { version = "^0.5.0", path = "../../ethers-contract"} ethers-signers = { version = "^0.5.0", path = "../../ethers-signers" } hex = "0.4.3" diff --git a/ethers-core/src/lib.rs b/ethers-core/src/lib.rs index 7a9478871..2d1ee51f5 100644 --- a/ethers-core/src/lib.rs +++ b/ethers-core/src/lib.rs @@ -53,3 +53,4 @@ pub use rand; // re-export k256 pub use k256; + diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index cd70945db..93c68ee38 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -547,7 +547,7 @@ pub fn encode_eip712_type(token: Token) -> Token { )))), _ => { // Return the ABI encoded token; - token.clone() + token } } } diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs index f4594437c..baf9ce174 100644 --- a/ethers-core/src/types/transaction/mod.rs +++ b/ethers-core/src/types/transaction/mod.rs @@ -4,6 +4,8 @@ pub mod response; pub mod eip1559; pub mod eip2718; pub mod eip2930; + +#[cfg(feature = "eip712")] pub mod eip712; pub(crate) const BASE_NUM_TX_FIELDS: usize = 9; diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index ec6548eda..3436a2399 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -95,8 +95,8 @@ pub trait Signer: std::fmt::Debug + Send + Sync { /// Signs the transaction async fn sign_transaction(&self, message: &TypedTransaction) -> Result; - /// Encodes and signs the typed data according EIP-712 - /// payload must implement Eip712 trait. + /// Encodes and signs the typed data according EIP-712. + /// Payload must implement Eip712 trait. async fn sign_typed_data( &self, payload: T, diff --git a/examples/eip712.rs b/examples/eip712.rs deleted file mode 100644 index a96b3e20c..000000000 --- a/examples/eip712.rs +++ /dev/null @@ -1,125 +0,0 @@ -use std::{convert::TryFrom, sync::Arc, time::Duration}; - -use ethers::{ - contract::EthAbiType, - prelude::*, - types::{transaction::eip712::Eip712, Address, I256, U256}, - utils::{compile_and_launch_ganache, keccak256, Ganache, Solc}, -}; -use ethers_derive_eip712::*; - -#[derive(Debug, Clone, Eip712, EthAbiType)] -#[eip712( - name = "Eip712Test", - version = "1", - chain_id = 1, - verifying_contract = "0x0000000000000000000000000000000000000001", - salt = "eip712-test-75F0CCte" -)] -struct FooBar { - foo: I256, - bar: U256, - fizz: Vec, - buzz: [u8; 32], - far: String, - out: Address, -} - -abigen!( - DeriveEip712Test, - "./examples/derive_eip712_abi.json", - event_derives(serde::Deserialize, serde::Serialize) -); - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let contract_name = "DeriveEip712Test".to_string(); - let (compiled, ganache) = - compile_and_launch_ganache(Solc::new("**/DeriveEip712Test.sol"), Ganache::new()).await?; - - let wallet: LocalWallet = ganache.keys()[0].clone().into(); - - let contract = compiled.get(&contract_name).unwrap(); - - let provider = - Provider::::try_from(ganache.endpoint())?.interval(Duration::from_millis(10u64)); - - let client = SignerMiddleware::new(provider, wallet.clone()); - let client = Arc::new(client); - - let factory = ContractFactory::new( - contract.abi.clone(), - contract.bytecode.clone(), - client.clone(), - ); - - let contract = factory.deploy(())?.legacy().send().await?; - - let addr = contract.address(); - - let contract = DeriveEip712Test::new(addr, client.clone()); - - let foo_bar = FooBar { - foo: I256::from(10), - bar: U256::from(20), - fizz: b"fizz".to_vec(), - buzz: keccak256("buzz"), - far: String::from("space"), - out: Address::from([0; 20]), - }; - - let derived_foo_bar = deriveeip712test_mod::FooBar { - foo: foo_bar.foo.clone(), - bar: foo_bar.bar.clone(), - fizz: foo_bar.fizz.clone(), - buzz: foo_bar.buzz.clone(), - far: foo_bar.far.clone(), - out: foo_bar.out.clone(), - }; - - let sig = wallet.sign_typed_data(foo_bar.clone()).await?; - - let r = <[u8; 32]>::try_from(sig.r)?; - let s = <[u8; 32]>::try_from(sig.s)?; - let v = u8::try_from(sig.v)?; - - let domain_separator = contract.domain_separator().call().await?; - let type_hash = contract.type_hash().call().await?; - let struct_hash = contract.struct_hash(derived_foo_bar.clone()).call().await?; - let encoded = contract - .encode_eip_712(derived_foo_bar.clone()) - .call() - .await?; - let verify = contract - .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v) - .call() - .await?; - - assert_eq!( - domain_separator, - FooBar::domain_separator()?, - "domain separator does not match contract domain separator!" - ); - - assert_eq!( - type_hash, - FooBar::type_hash()?, - "type hash does not match contract struct type hash!" - ); - - assert_eq!( - struct_hash, - foo_bar.clone().struct_hash()?, - "struct hash does not match contract struct struct hash!" - ); - - assert_eq!( - encoded, - foo_bar.encode_eip712()?, - "Encoded value does not match!" - ); - - assert_eq!(verify, true, "typed data signature failed!"); - - Ok(()) -} From c98ba371a90edd21aa19afe1b8a0af8e95c5e9d4 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 11:51:24 -0700 Subject: [PATCH 13/30] allow optional EIP712Domain parameter when encoding eip712 struct and signing typed data --- ethers-contract/tests/contract.rs | 4 ++-- ethers-core/ethers-derive-eip712/src/lib.rs | 12 ++++++++++-- .../ethers-derive-eip712/tests/derive_eip712.rs | 4 ++-- ethers-core/src/types/transaction/eip712.rs | 3 ++- ethers-signers/Cargo.toml | 2 +- ethers-signers/src/aws/mod.rs | 9 +++++++-- ethers-signers/src/ledger/mod.rs | 11 ++++++++--- ethers-signers/src/lib.rs | 6 +++++- ethers-signers/src/wallet/mod.rs | 8 +++++--- 9 files changed, 42 insertions(+), 17 deletions(-) diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index b7ac17282..1a6ad349b 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -660,7 +660,7 @@ mod eth_tests { }; let sig = wallet - .sign_typed_data(foo_bar.clone()) + .sign_typed_data(foo_bar.clone(), Default::default()) .await .expect("failed to sign typed data"); @@ -721,7 +721,7 @@ mod eth_tests { assert_eq!( encoded, foo_bar - .encode_eip712() + .encode_eip712(Default::default()) .expect("failed to return domain_separator from Eip712 implemented struct"), "Encoded value does not match!" ); diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 6726ab681..6b8787a82 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -146,12 +146,20 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(struct_hash) } - fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { + fn encode_eip712(self, domain: Option) -> Result<[u8; 32], Self::Error> { // encode the digest to be compatible with solidity abi.encodePacked() // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 + + let domain_separator = if let Some(d) = domain { + d.separator() + } else { + Self::domain_separator()? + }; + + let digest_input = [ &[0x19, 0x01], - &Self::domain_separator()?[..], + &domain_separator[..], &self.struct_hash()?[..] ].concat(); diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index 2519fa6c7..9e68775cd 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -38,7 +38,7 @@ fn test_derive_eip712() { project: "radicle-reward".to_string(), }; - let hash = puzzle.encode_eip712().expect("failed to encode struct"); + let hash = puzzle.encode_eip712(Default::default()).expect("failed to encode struct"); assert_eq!(hash.len(), 32) } @@ -158,7 +158,7 @@ fn test_uniswap_v2_permit_hash() { deadline: U256::from(3133728498 as u32), }; - let permit_hash = permit.encode_eip712().unwrap(); + let permit_hash = permit.encode_eip712(Default::default()).unwrap(); assert_eq!( hex::encode(permit_hash), diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 93c68ee38..1d35b9dff 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -85,7 +85,8 @@ pub trait Eip712 { /// When using the derive macro, this is the primary method used for computing the final /// EIP-712 encoded payload. This method relies on the aforementioned methods for computing /// the final encoded payload. - fn encode_eip712(self) -> Result<[u8; 32], Self::Error>; + /// * `domain` - Optional Eip712 domain struct to override eip712 macro attribute helpers; + fn encode_eip712(self, domain: Option) -> Result<[u8; 32], Self::Error>; } /// Eip712 Domain attributes used in determining the domain separator; diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index 0f7a88596..e0e54cc88 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -14,7 +14,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -ethers-core = { version = "^0.5.0", path = "../ethers-core" } +ethers-core = { version = "^0.5.0", path = "../ethers-core", features = ["eip712"]} thiserror = { version = "1.0.29", default-features = false } coins-bip32 = "0.3.0" coins-bip39 = "0.3.0" diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index fc1db029a..a28f75168 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -2,7 +2,11 @@ use ethers_core::{ k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey}, - types::{transaction::eip2718::TypedTransaction, Address, Signature as EthSig, H256}, + types::{ + transaction::eip2718::TypedTransaction, + transaction::eip712::{EIP712Domain, Eip712}, + Address, Signature as EthSig, H256, + }, utils::hash_message, }; use rusoto_core::RusotoError; @@ -251,8 +255,9 @@ impl<'a> super::Signer for AwsSigner<'a> { async fn sign_typed_data( &self, payload: T, + domain: Option, ) -> Result { - let hash = payload.encode_eip712()?; + let hash = payload.encode_eip712(domain)?; let digest = self.sign_digest_with_eip155(hash.into()); Ok(digest) diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index ec9d83615..36f5cd37e 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -4,7 +4,11 @@ pub mod types; use crate::Signer; use app::LedgerEthereum; use async_trait::async_trait; -use ethers_core::types::{transaction::eip2718::TypedTransaction, Address, Signature}; +use ethers_core::types::{ + transaction::eip2718::TypedTransaction, + transaction::eip712::{EIP712Domain, EIP712}, + Address, Signature, +}; use types::LedgerError; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -28,9 +32,10 @@ impl Signer for LedgerEthereum { async fn sign_typed_data( &self, payload: T, + domain: Option, ) -> Result { - let hash = payload.encode_eip712()?; - + let hash = payload.encode_eip712(domain)?; + Ok(self.sign_hash(hash.into(), false)) } diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 3436a2399..c511c4e7e 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -70,7 +70,9 @@ pub use aws::{AwsSigner, AwsSignerError}; use async_trait::async_trait; use ethers_core::types::{ - transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, + transaction::eip2718::TypedTransaction, + transaction::eip712::{EIP712Domain, Eip712}, + Address, Signature, }; use std::error::Error; @@ -97,9 +99,11 @@ pub trait Signer: std::fmt::Debug + Send + Sync { /// Encodes and signs the typed data according EIP-712. /// Payload must implement Eip712 trait. + /// * `domain` - Optional Eip712 domain struct to override eip712 macro attribute helpers for Eip712 Type `T`; async fn sign_typed_data( &self, payload: T, + domain: Option, ) -> Result; /// Returns the signer's Ethereum Address diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index 4331b3890..0e5cb44a8 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -17,8 +17,9 @@ use ethers_core::{ Secp256k1, }, types::{ - transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, - H256, U256, + transaction::eip2718::TypedTransaction, + transaction::eip712::{EIP712Domain, Eip712}, + Address, Signature, H256, U256, }, utils::hash_message, }; @@ -89,9 +90,10 @@ impl> Signer fo async fn sign_typed_data( &self, payload: T, + domain: Option, ) -> Result { let encoded = payload - .encode_eip712() + .encode_eip712(domain) .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; Ok(self.sign_hash(H256::from(encoded), false)) From c91108718bd3035102d254199731122d874703b8 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 12:06:54 -0700 Subject: [PATCH 14/30] add documentation for eip712 feature --- ethers-core/src/lib.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ethers-core/src/lib.rs b/ethers-core/src/lib.rs index 2d1ee51f5..a105e58b4 100644 --- a/ethers-core/src/lib.rs +++ b/ethers-core/src/lib.rs @@ -36,6 +36,10 @@ //! via the `GanacheBuilder` struct. In addition, you're able to compile contracts on the //! filesystem by providing a glob to their path, using the `Solc` struct. //! +//! # Features +//! +//! * - ["eip712"] | Provides Eip712 trait for EIP-712 encoding of typed data for derived structs +//! //! # ABI Encoding and Decoding //! //! This crate re-exports the [`ethabi`](ethabi) crate's functions @@ -53,4 +57,3 @@ pub use rand; // re-export k256 pub use k256; - From db99fe0bec7a9897d2e3529e23c9cf55138bb6ef Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 15:15:14 -0700 Subject: [PATCH 15/30] Update ethers-signers/src/ledger/mod.rs Co-authored-by: Sebastian Martinez --- ethers-signers/src/ledger/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 36f5cd37e..500174759 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -6,7 +6,7 @@ use app::LedgerEthereum; use async_trait::async_trait; use ethers_core::types::{ transaction::eip2718::TypedTransaction, - transaction::eip712::{EIP712Domain, EIP712}, + transaction::eip712::{EIP712Domain, Eip712}, Address, Signature, }; use types::LedgerError; From e8cf636dd748df6efaada7e039459ef970568501 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 15:37:48 -0700 Subject: [PATCH 16/30] add error enum for Eip712Error; use sign_payload for ledger signer --- ethers-signers/src/aws/mod.rs | 8 +++++++- ethers-signers/src/ledger/mod.rs | 8 +++++--- ethers-signers/src/ledger/types.rs | 3 +++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index a28f75168..f87796a7d 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -90,6 +90,9 @@ pub enum AwsSignerError { #[error(transparent)] /// Error when converting from a hex string HexError(#[from] hex::FromHexError), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), } impl From for AwsSignerError { @@ -257,7 +260,10 @@ impl<'a> super::Signer for AwsSigner<'a> { payload: T, domain: Option, ) -> Result { - let hash = payload.encode_eip712(domain)?; + let hash = payload + .encode_eip712(domain) + .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; + let digest = self.sign_digest_with_eip155(hash.into()); Ok(digest) diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 36f5cd37e..ef4b87bb1 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -9,7 +9,7 @@ use ethers_core::types::{ transaction::eip712::{EIP712Domain, EIP712}, Address, Signature, }; -use types::LedgerError; +use types::{LedgerError, INS}; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] @@ -34,9 +34,11 @@ impl Signer for LedgerEthereum { payload: T, domain: Option, ) -> Result { - let hash = payload.encode_eip712(domain)?; + let hash = payload + .encode_eip712(domain) + .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; - Ok(self.sign_hash(hash.into(), false)) + Ok(self.sign_payload(INS::SIGN, hash.into(), false)) } /// Returns the signer's Ethereum Address diff --git a/ethers-signers/src/ledger/types.rs b/ethers-signers/src/ledger/types.rs index 6c7edf2b5..eee344c28 100644 --- a/ethers-signers/src/ledger/types.rs +++ b/ethers-signers/src/ledger/types.rs @@ -41,6 +41,9 @@ pub enum LedgerError { #[error(transparent)] /// Error when converting from a hex string HexError(#[from] hex::FromHexError), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), } pub const P1_FIRST: u8 = 0x00; From e12a90752194294357441832cf884f55e8085974 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 16:54:55 -0700 Subject: [PATCH 17/30] add EIP712WithDomain type for providing a wrapper around custom setting of the domain --- ethers-contract/tests/contract.rs | 10 +-- ethers-core/ethers-derive-eip712/src/lib.rs | 29 ++++---- .../tests/derive_eip712.rs | 4 +- ethers-core/src/types/transaction/eip712.rs | 69 +++++++++++++++++-- ethers-signers/src/aws/mod.rs | 8 +-- ethers-signers/src/ledger/mod.rs | 3 +- ethers-signers/src/lib.rs | 5 +- ethers-signers/src/wallet/mod.rs | 8 +-- 8 files changed, 96 insertions(+), 40 deletions(-) diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 1a6ad349b..cc2484130 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -660,7 +660,7 @@ mod eth_tests { }; let sig = wallet - .sign_typed_data(foo_bar.clone(), Default::default()) + .sign_typed_data(foo_bar.clone()) .await .expect("failed to sign typed data"); @@ -698,8 +698,10 @@ mod eth_tests { assert_eq!( domain_separator, - FooBar::domain_separator() - .expect("failed to return domain_separator from Eip712 implemented struct"), + foo_bar + .domain() + .expect("failed to return domain_separator from Eip712 implemented struct") + .separator(), "domain separator does not match contract domain separator!" ); @@ -721,7 +723,7 @@ mod eth_tests { assert_eq!( encoded, foo_bar - .encode_eip712(Default::default()) + .encode_eip712() .expect("failed to return domain_separator from Eip712 implemented struct"), "Encoded value does not match!" ); diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 6b8787a82..4b6d8091a 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -86,7 +86,14 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Err(e) => return TokenStream::from(e), }; - let domain_separator = hex::encode(domain.separator()); + let domain_str = match serde_json::to_string(&domain) { + Ok(s) => s, + Err(e) => { + return TokenStream::from( + syn::Error::new(ast.ident.span(), e.to_string()).to_compile_error(), + ); + } + }; // Must parse the AST at compile time. let parsed_fields = match eip712::parse_fields(ast) { @@ -111,11 +118,10 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(byte_array) } - fn domain_separator() -> Result<[u8; 32], Self::Error> { - use std::convert::TryFrom; - let decoded = hex::decode(#domain_separator)?; - let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; - Ok(byte_array) + fn domain(&self) -> Result { + let domain: ethers_core::types::transaction::eip712::EIP712Domain = serde_json::from_str(#domain_str)?; + + Ok(domain) } fn struct_hash(self) -> Result<[u8; 32], Self::Error> { @@ -146,20 +152,15 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(struct_hash) } - fn encode_eip712(self, domain: Option) -> Result<[u8; 32], Self::Error> { + fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { // encode the digest to be compatible with solidity abi.encodePacked() // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 - let domain_separator = if let Some(d) = domain { - d.separator() - } else { - Self::domain_separator()? - }; - + let domain = self.domain()?; let digest_input = [ &[0x19, 0x01], - &domain_separator[..], + &domain.separator()[..], &self.struct_hash()?[..] ].concat(); diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index 9e68775cd..2519fa6c7 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -38,7 +38,7 @@ fn test_derive_eip712() { project: "radicle-reward".to_string(), }; - let hash = puzzle.encode_eip712(Default::default()).expect("failed to encode struct"); + let hash = puzzle.encode_eip712().expect("failed to encode struct"); assert_eq!(hash.len(), 32) } @@ -158,7 +158,7 @@ fn test_uniswap_v2_permit_hash() { deadline: U256::from(3133728498 as u32), }; - let permit_hash = permit.encode_eip712(Default::default()).unwrap(); + let permit_hash = permit.encode_eip712().unwrap(); assert_eq!( hex::encode(permit_hash), diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 1d35b9dff..4d8d7363b 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -49,6 +49,8 @@ pub enum Eip712Error { TryFromSliceError(#[from] std::array::TryFromSliceError), #[error("Nested Eip712 struct not implemented. Failed to parse.")] NestedEip712StructNotImplemented, + #[error("Error from Eip712 struct: {0:?}")] + Inner(String), } /// The Eip712 trait provides helper methods for computing @@ -67,12 +69,12 @@ pub trait Eip712 { /// User defined error type; type Error: std::error::Error + Send + Sync + std::fmt::Debug; - /// The domain separator depends on the contract and unique domain + /// Returns the current domain. The domain depends on the contract and unique domain /// for which the user is targeting. In the derive macro, these attributes /// are passed in as arguments to the macro. When manually deriving, the user /// will need to know the name of the domain, version of the contract, chain ID of /// where the contract lives and the address of the verifying contract. - fn domain_separator() -> Result<[u8; 32], Self::Error>; + fn domain(&self) -> Result; /// This method is used for calculating the hash of the type signature of the /// struct. The field types of the struct must map to primitive @@ -86,12 +88,12 @@ pub trait Eip712 { /// EIP-712 encoded payload. This method relies on the aforementioned methods for computing /// the final encoded payload. /// * `domain` - Optional Eip712 domain struct to override eip712 macro attribute helpers; - fn encode_eip712(self, domain: Option) -> Result<[u8; 32], Self::Error>; + fn encode_eip712(self) -> Result<[u8; 32], Self::Error>; } /// Eip712 Domain attributes used in determining the domain separator; /// Unused fields are left out of the struct type. -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] pub struct EIP712Domain { /// The user readable name of signing domain, i.e. the name of the DApp or the protocol. pub name: String, @@ -136,6 +138,65 @@ impl EIP712Domain { } } +#[derive(Debug, Clone)] +pub struct EIP712WithDomain +where + T: Clone + Eip712, +{ + pub domain: EIP712Domain, + pub inner: T, +} + +impl EIP712WithDomain { + pub fn new(inner: T) -> Result { + let domain = inner + .domain() + .map_err(|e| Eip712Error::Inner(e.to_string()))?; + + Ok(Self { domain, inner }) + } + + pub fn set_domain(self, domain: EIP712Domain) -> Self { + Self { + domain, + inner: self.inner, + } + } +} + +impl Eip712 for EIP712WithDomain { + type Error = Eip712Error; + + fn domain(&self) -> Result { + Ok(self.domain.clone()) + } + + fn type_hash() -> Result<[u8; 32], Self::Error> { + let type_hash = T::type_hash().map_err(|e| Self::Error::Inner(e.to_string()))?; + Ok(type_hash) + } + + fn struct_hash(self) -> Result<[u8; 32], Self::Error> { + let struct_hash = self + .inner + .clone() + .struct_hash() + .map_err(|e| Self::Error::Inner(e.to_string()))?; + Ok(struct_hash) + } + + fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { + let digest_input = [ + &[0x19, 0x01], + &self.domain.separator()[..], + &self.struct_hash()?[..], + ] + .concat(); + + return Ok(keccak256(digest_input)); + } +} + // Parse the AST of the struct to determine the domain attributes impl TryFrom<&syn::DeriveInput> for EIP712Domain { type Error = TokenStream; diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index f87796a7d..6a07547b1 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -3,9 +3,8 @@ use ethers_core::{ k256::ecdsa::{Error as K256Error, Signature as KSig, VerifyingKey}, types::{ - transaction::eip2718::TypedTransaction, - transaction::eip712::{EIP712Domain, Eip712}, - Address, Signature as EthSig, H256, + transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, + Signature as EthSig, H256, }, utils::hash_message, }; @@ -258,10 +257,9 @@ impl<'a> super::Signer for AwsSigner<'a> { async fn sign_typed_data( &self, payload: T, - domain: Option, ) -> Result { let hash = payload - .encode_eip712(domain) + .encode_eip712() .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; let digest = self.sign_digest_with_eip155(hash.into()); diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 9deded24a..3c08d8c7d 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -32,10 +32,9 @@ impl Signer for LedgerEthereum { async fn sign_typed_data( &self, payload: T, - domain: Option, ) -> Result { let hash = payload - .encode_eip712(domain) + .encode_eip712() .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; Ok(self.sign_payload(INS::SIGN, hash.into(), false)) diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index c511c4e7e..224d147ff 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -70,9 +70,7 @@ pub use aws::{AwsSigner, AwsSignerError}; use async_trait::async_trait; use ethers_core::types::{ - transaction::eip2718::TypedTransaction, - transaction::eip712::{EIP712Domain, Eip712}, - Address, Signature, + transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, }; use std::error::Error; @@ -103,7 +101,6 @@ pub trait Signer: std::fmt::Debug + Send + Sync { async fn sign_typed_data( &self, payload: T, - domain: Option, ) -> Result; /// Returns the signer's Ethereum Address diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index 0e5cb44a8..4331b3890 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -17,9 +17,8 @@ use ethers_core::{ Secp256k1, }, types::{ - transaction::eip2718::TypedTransaction, - transaction::eip712::{EIP712Domain, Eip712}, - Address, Signature, H256, U256, + transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, + H256, U256, }, utils::hash_message, }; @@ -90,10 +89,9 @@ impl> Signer fo async fn sign_typed_data( &self, payload: T, - domain: Option, ) -> Result { let encoded = payload - .encode_eip712(domain) + .encode_eip712() .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; Ok(self.sign_hash(H256::from(encoded), false)) From 792234133ac95ca175347fa1bcde637986b3d626 Mon Sep 17 00:00:00 2001 From: Ryan Date: Wed, 6 Oct 2021 17:07:32 -0700 Subject: [PATCH 18/30] make LedgerWallet sign_payload public --- ethers-signers/src/ledger/app.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index dff7951e9..ad6c44f4b 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -137,7 +137,7 @@ impl LedgerEthereum { } // Helper function for signing either transaction data or personal messages - async fn sign_payload( + pub async fn sign_payload( &self, command: INS, mut payload: Vec, From bc2fc1f36bb63740b5fbe8deb81e61ae88d5280e Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 08:46:42 -0700 Subject: [PATCH 19/30] use optional feature gated dependencies for eip712; add default method for encode_eip712 --- Cargo.lock | 1 + ethers-contract/Cargo.toml | 2 +- ethers-core/Cargo.toml | 12 +++++---- ethers-core/ethers-derive-eip712/Cargo.toml | 1 + ethers-core/ethers-derive-eip712/src/lib.rs | 22 +++------------- ethers-core/src/types/transaction/eip712.rs | 28 ++++++++++----------- 6 files changed, 27 insertions(+), 39 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b6a96ca2f..cc4fc8d51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -952,6 +952,7 @@ name = "ethers-derive-eip712" version = "0.1.0" dependencies = [ "ethers-contract", + "ethers-contract-derive", "ethers-core", "ethers-signers", "hex", diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index e08209565..2c10c218f 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["ethereum", "web3", "celo", "ethers"] [dependencies] ethers-providers = { version = "^0.5.0", path = "../ethers-providers", default-features = false } -ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false } +ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false, features = ["eip712"]} ethers-contract-abigen = { version = "^0.5.0", path = "ethers-contract-abigen", optional = true } ethers-contract-derive = { version = "^0.5.0", path = "ethers-contract-derive", optional = true } diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index a5665f2cf..f6c55531d 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -32,10 +32,12 @@ bytes = { version = "1.1.0", features = ["serde"] } hex = { version = "0.4.3", default-features = false, features = ["std"] } semver = "1.0.4" once_cell = "1.8.0" -convert_case = "0.4.0" -syn = "1.0.77" -quote = "1.0.9" -proc-macro2 = "1.0.29" + +# eip712 feature enabled dependencies +convert_case = { version = "0.4.0", optional = true } +syn = { version = "1.0.77", optional = true } +quote = { version = "1.0.9", optional = true } +proc-macro2 = { version = "1.0.29", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # async @@ -53,7 +55,7 @@ futures-util = { version = "0.3.17" } celo = ["legacy"] # celo support extends the transaction format with extra fields setup = ["tokio", "futures-util"] # async support for concurrent setup legacy = [] -eip712 = [] +eip712 = ["convert_case", "syn", "quote", "proc-macro2"] [package.metadata.docs.rs] all-features = true diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml index c9f01a802..79bac8cf3 100644 --- a/ethers-core/ethers-derive-eip712/Cargo.toml +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -12,6 +12,7 @@ quote = "1.0.9" syn = "1.0.77" ethers-core = { version = "^0.5.0", path = "../", default-features = false, features = ["eip712"] } ethers-contract = { version = "^0.5.0", path = "../../ethers-contract"} +ethers-contract-derive = { version = "^0.5.0", path = "../../ethers-contract/ethers-contract-derive" } ethers-signers = { version = "^0.5.0", path = "../../ethers-signers" } hex = "0.4.3" serde = "1.0.130" diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index 4b6d8091a..d15bf1933 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -63,6 +63,7 @@ use std::convert::TryFrom; use ethers_core::types::transaction::eip712; + use proc_macro::TokenStream; use quote::quote; use syn::parse_macro_input; @@ -80,12 +81,13 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { // Primary type should match the type in the ethereum verifying contract; let primary_type = &ast.ident; - // Computer domain separator + // Instantiate domain from parsed attributes let domain = match eip712::EIP712Domain::try_from(ast) { Ok(attributes) => attributes, Err(e) => return TokenStream::from(e), }; + // let domain_str = match serde_json::to_string(&domain) { Ok(s) => s, Err(e) => { @@ -124,7 +126,7 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(domain) } - fn struct_hash(self) -> Result<[u8; 32], Self::Error> { + fn struct_hash(&self) -> Result<[u8; 32], Self::Error> { use ethers_core::abi::Tokenizable; let mut items = vec![ethers_core::abi::Token::Uint( ethers_core::types::U256::from(&Self::type_hash()?[..]), @@ -151,22 +153,6 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(struct_hash) } - - fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { - // encode the digest to be compatible with solidity abi.encodePacked() - // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 - - let domain = self.domain()?; - - let digest_input = [ - &[0x19, 0x01], - &domain.separator()[..], - &self.struct_hash()?[..] - ].concat(); - - return Ok(ethers_core::utils::keccak256(digest_input)); - - } } }; diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 4d8d7363b..85abf2ff6 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -82,13 +82,22 @@ pub trait Eip712 { fn type_hash() -> Result<[u8; 32], Self::Error>; /// Hash of the struct, according to EIP-712 definition of `hashStruct` - fn struct_hash(self) -> Result<[u8; 32], Self::Error>; + fn struct_hash(&self) -> Result<[u8; 32], Self::Error>; /// When using the derive macro, this is the primary method used for computing the final /// EIP-712 encoded payload. This method relies on the aforementioned methods for computing /// the final encoded payload. - /// * `domain` - Optional Eip712 domain struct to override eip712 macro attribute helpers; - fn encode_eip712(self) -> Result<[u8; 32], Self::Error>; + fn encode_eip712(&self) -> Result<[u8; 32], Self::Error> { + // encode the digest to be compatible with solidity abi.encodePacked() + // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 + + let domain = self.domain()?; + let struct_hash = self.struct_hash()?; + + let digest_input = [&[0x19, 0x01], &domain.separator()[..], &struct_hash[..]].concat(); + + return Ok(keccak256(digest_input)); + } } /// Eip712 Domain attributes used in determining the domain separator; @@ -176,7 +185,7 @@ impl Eip712 for EIP712WithDomain { Ok(type_hash) } - fn struct_hash(self) -> Result<[u8; 32], Self::Error> { + fn struct_hash(&self) -> Result<[u8; 32], Self::Error> { let struct_hash = self .inner .clone() @@ -184,17 +193,6 @@ impl Eip712 for EIP712WithDomain { .map_err(|e| Self::Error::Inner(e.to_string()))?; Ok(struct_hash) } - - fn encode_eip712(self) -> Result<[u8; 32], Self::Error> { - let digest_input = [ - &[0x19, 0x01], - &self.domain.separator()[..], - &self.struct_hash()?[..], - ] - .concat(); - - return Ok(keccak256(digest_input)); - } } // Parse the AST of the struct to determine the domain attributes From 1f2b433583f5c6fb6392e1b5ce0843fa0d7122a4 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 08:59:02 -0700 Subject: [PATCH 20/30] add default domain_separator method, pre-compute separator hash --- ethers-core/ethers-derive-eip712/src/lib.rs | 10 ++++++++++ ethers-core/src/types/transaction/eip712.rs | 9 +++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs index d15bf1933..e1ceb0966 100644 --- a/ethers-core/ethers-derive-eip712/src/lib.rs +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -87,6 +87,8 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Err(e) => return TokenStream::from(e), }; + let domain_separator = hex::encode(domain.separator()); + // let domain_str = match serde_json::to_string(&domain) { Ok(s) => s, @@ -120,6 +122,14 @@ fn impl_eip_712_macro(ast: &syn::DeriveInput) -> TokenStream { Ok(byte_array) } + // Return the pre-computed domain separator from compile time; + fn domain_separator(&self) -> Result<[u8; 32], Self::Error> { + use std::convert::TryFrom; + let decoded = hex::decode(#domain_separator)?; + let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?; + Ok(byte_array) + } + fn domain(&self) -> Result { let domain: ethers_core::types::transaction::eip712::EIP712Domain = serde_json::from_str(#domain_str)?; diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index 85abf2ff6..b745d3ff1 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -69,6 +69,11 @@ pub trait Eip712 { /// User defined error type; type Error: std::error::Error + Send + Sync + std::fmt::Debug; + /// Default implementation of the domain separator; + fn domain_separator(&self) -> Result<[u8; 32], Self::Error> { + Ok(self.domain()?.separator()) + } + /// Returns the current domain. The domain depends on the contract and unique domain /// for which the user is targeting. In the derive macro, these attributes /// are passed in as arguments to the macro. When manually deriving, the user @@ -91,10 +96,10 @@ pub trait Eip712 { // encode the digest to be compatible with solidity abi.encodePacked() // See: https://github.com/gakonst/ethers-rs/blob/master/examples/permit_hash.rs#L72 - let domain = self.domain()?; + let domain_separator = self.domain_separator()?; let struct_hash = self.struct_hash()?; - let digest_input = [&[0x19, 0x01], &domain.separator()[..], &struct_hash[..]].concat(); + let digest_input = [&[0x19, 0x01], &domain_separator[..], &struct_hash[..]].concat(); return Ok(keccak256(digest_input)); } From 0e48b7928731f0d0b80e50115c353e0b6710d362 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 09:36:22 -0700 Subject: [PATCH 21/30] move derive-eip712 deps to dev deps --- ethers-contract/Cargo.toml | 1 - ethers-contract/src/lib.rs | 3 +++ ethers-core/ethers-derive-eip712/Cargo.toml | 8 +++++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 2c10c218f..b7afff185 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -31,7 +31,6 @@ ethers-contract-abigen = { version = "^0.5.0", path = "ethers-contract-abigen" } ethers-contract-derive = { version = "^0.5.0", path = "ethers-contract-derive" } ethers-derive-eip712 = { version = "0.1.0", path = "../ethers-core/ethers-derive-eip712"} - [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] tokio = { version = "1.5", default-features = false, features = ["macros"] } diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index ff93c8b5b..2a2858166 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -55,3 +55,6 @@ pub use ethers_contract_derive::{abigen, EthAbiType, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] pub use once_cell::sync::Lazy; + +#[cfg(feature = "eip712")] +pub use ethers_derive_eip712::*; diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml index 79bac8cf3..d8a2efb00 100644 --- a/ethers-core/ethers-derive-eip712/Cargo.toml +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -11,10 +11,12 @@ proc-macro = true quote = "1.0.9" syn = "1.0.77" ethers-core = { version = "^0.5.0", path = "../", default-features = false, features = ["eip712"] } -ethers-contract = { version = "^0.5.0", path = "../../ethers-contract"} -ethers-contract-derive = { version = "^0.5.0", path = "../../ethers-contract/ethers-contract-derive" } -ethers-signers = { version = "^0.5.0", path = "../../ethers-signers" } hex = "0.4.3" serde = "1.0.130" serde_json = "1.0.68" proc-macro2 = "1.0.29" + +[dev-dependencies] +ethers-contract = { version = "^0.5.0", path = "../../ethers-contract"} +ethers-contract-derive = { version = "^0.5.0", path = "../../ethers-contract/ethers-contract-derive" } +ethers-signers = { version = "^0.5.0", path = "../../ethers-signers" } \ No newline at end of file From 6cb3fed1b48543c48da3f7e0d5b70303d9895607 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 09:36:55 -0700 Subject: [PATCH 22/30] remove invalid sign payload parameter, add await on async method --- ethers-signers/src/ledger/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 3c08d8c7d..51a1426a1 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -37,7 +37,9 @@ impl Signer for LedgerEthereum { .encode_eip712() .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; - Ok(self.sign_payload(INS::SIGN, hash.into(), false)) + let sig = self.sign_payload(INS::SIGN, hash.into()).await?; + + Ok(sig) } /// Returns the signer's Ethereum Address From 61a153deea46521471b8d8a01f9ab8a85ba1b941 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 09:53:33 -0700 Subject: [PATCH 23/30] remove deprecated comment --- ethers-signers/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 224d147ff..3436a2399 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -97,7 +97,6 @@ pub trait Signer: std::fmt::Debug + Send + Sync { /// Encodes and signs the typed data according EIP-712. /// Payload must implement Eip712 trait. - /// * `domain` - Optional Eip712 domain struct to override eip712 macro attribute helpers for Eip712 Type `T`; async fn sign_typed_data( &self, payload: T, From 909c4918c0d24f4328064f0262dbe94417a21409 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 10:01:33 -0700 Subject: [PATCH 24/30] debugging 'bad key handle' error for ledger signer try using 'sign_message' --- ethers-signers/src/ledger/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 51a1426a1..8c1d73243 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -37,7 +37,7 @@ impl Signer for LedgerEthereum { .encode_eip712() .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; - let sig = self.sign_payload(INS::SIGN, hash.into()).await?; + let sig = self.sign_message(hash).await?; Ok(sig) } From 8f46be9d5e61e787c661dcc344ba8eb7a2ae1a30 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 11:17:43 -0700 Subject: [PATCH 25/30] await sign digest for aws signer --- ethers-signers/src/aws/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index 6a07547b1..d53a3c1b3 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -262,7 +262,7 @@ impl<'a> super::Signer for AwsSigner<'a> { .encode_eip712() .map_err(|e| Self::Error::Eip712Error(e.to_string()))?; - let digest = self.sign_digest_with_eip155(hash.into()); + let digest = self.sign_digest_with_eip155(hash.into()).await?; Ok(digest) } From 6691f8e26cbcc8e9217bba3e6ec2185495f84597 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 11:27:28 -0700 Subject: [PATCH 26/30] remove extra space, fix fmt warning --- ethers-core/ethers-derive-eip712/tests/derive_eip712.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs index 2519fa6c7..4821d669c 100644 --- a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -175,7 +175,7 @@ fn test_domain_hash_constants() { ) ); assert_eq!( - EIP712_DOMAIN_TYPE_HASH_WITH_SALT, + EIP712_DOMAIN_TYPE_HASH_WITH_SALT, keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") ); } From e3e96d1d2bd900327030fd2f764b9b5929f0fa50 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 11:49:01 -0700 Subject: [PATCH 27/30] fix test, fmt errors --- ethers-core/src/types/transaction/eip712.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs index b745d3ff1..57ab7e0e6 100644 --- a/ethers-core/src/types/transaction/eip712.rs +++ b/ethers-core/src/types/transaction/eip712.rs @@ -16,9 +16,7 @@ use crate::{ /// Pre-computed value of the following statement: /// -/// ``` -/// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)") -/// ``` +/// `ethers_core::utils::keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)")` /// pub const EIP712_DOMAIN_TYPE_HASH: [u8; 32] = [ 139, 115, 195, 198, 155, 184, 254, 61, 81, 46, 204, 76, 247, 89, 204, 121, 35, 159, 123, 23, @@ -27,9 +25,7 @@ pub const EIP712_DOMAIN_TYPE_HASH: [u8; 32] = [ /// Pre-computed value of the following statement: /// -/// ``` -/// keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)") -/// ``` +/// `ethers_core::utils::keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)")` /// pub const EIP712_DOMAIN_TYPE_HASH_WITH_SALT: [u8; 32] = [ 216, 124, 214, 239, 121, 212, 226, 185, 94, 21, 206, 138, 191, 115, 45, 181, 30, 199, 113, 241, @@ -101,7 +97,7 @@ pub trait Eip712 { let digest_input = [&[0x19, 0x01], &domain_separator[..], &struct_hash[..]].concat(); - return Ok(keccak256(digest_input)); + Ok(keccak256(digest_input)) } } From 7241d2be5ea0125fc1dabe9c550eb54ce67a88d4 Mon Sep 17 00:00:00 2001 From: Ryan Date: Thu, 7 Oct 2021 16:19:33 -0700 Subject: [PATCH 28/30] use gt 0.6.0 pragma compiler version --- ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol index 73a591299..379b31a26 100644 --- a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol +++ b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol @@ -1,5 +1,5 @@ // SPDX-License-Identifier: GPL-3.0-only -pragma solidity ^0.8.0; +pragma solidity >=0.6.0; contract DeriveEip712Test { uint256 constant chainId = 1; From e0a0d9253f97d99be374d781d79ea1f25b2db26a Mon Sep 17 00:00:00 2001 From: Ryan Date: Fri, 8 Oct 2021 06:55:41 -0700 Subject: [PATCH 29/30] enable ABIEncoderV2 for solidity test contract --- ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol index 379b31a26..b938d39ab 100644 --- a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol +++ b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol @@ -1,5 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only pragma solidity >=0.6.0; +pragma experimental ABIEncoderV2; contract DeriveEip712Test { uint256 constant chainId = 1; From 208949a18297220fa248223baf7c6c26f9a98684 Mon Sep 17 00:00:00 2001 From: Georgios Konstantopoulos Date: Fri, 8 Oct 2021 15:19:52 +0100 Subject: [PATCH 30/30] chore: make test constructor public --- ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol index b938d39ab..0aced88b6 100644 --- a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol +++ b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol @@ -24,7 +24,7 @@ contract DeriveEip712Test { address out; } - constructor() {} + constructor() public {} function domainSeparator() public pure returns (bytes32) { return