feat: add support for EIP-712 typed data (#1510)

* feat: add support for typeddata

* change eip712 trait

* feat: impl eip712 trait

* chore(clippy): make clippy  happy

* better numeric parsing

* fix: fix a bunch of encoding bugs

* Update ethers-core/Cargo.toml

Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Matthias Seitz 2022-07-27 22:56:02 +02:00 committed by GitHub
parent 9cc0fb0036
commit 3d76ce816a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 911 additions and 77 deletions

View File

@ -66,3 +66,5 @@ pub use fee::*;
mod other;
pub use other::OtherFields;
pub mod serde_helpers;

View File

@ -0,0 +1,147 @@
//! Some convenient serde helpers
use crate::types::{BlockNumber, U256};
use ethabi::ethereum_types::FromDecStrErr;
use serde::{Deserialize, Deserializer};
/// Helper type to parse both `u64` and `U256`
#[derive(Deserialize)]
#[serde(untagged)]
pub enum Numeric {
U256(U256),
Num(u64),
}
impl From<Numeric> for U256 {
fn from(n: Numeric) -> U256 {
match n {
Numeric::U256(n) => n,
Numeric::Num(n) => U256::from(n),
}
}
}
/// Helper type to parse both `u64` and `U256`
#[derive(Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum StringifiedNumeric {
String(String),
U256(U256),
Num(u64),
}
impl TryFrom<StringifiedNumeric> for U256 {
type Error = FromDecStrErr;
fn try_from(value: StringifiedNumeric) -> Result<Self, Self::Error> {
match value {
StringifiedNumeric::U256(n) => Ok(n),
StringifiedNumeric::Num(n) => Ok(U256::from(n)),
StringifiedNumeric::String(s) => {
if let Ok(val) = s.parse::<u128>() {
Ok(val.into())
} else {
U256::from_dec_str(&s)
}
}
}
}
}
/// Helper type to deserialize sequence of numbers
#[derive(Deserialize)]
#[serde(untagged)]
pub enum NumericSeq {
Seq([Numeric; 1]),
U256(U256),
Num(u64),
}
/// Deserializes a number from hex or int
pub fn deserialize_number<'de, D>(deserializer: D) -> Result<U256, D::Error>
where
D: Deserializer<'de>,
{
Numeric::deserialize(deserializer).map(Into::into)
}
/// Deserializes a number from hex or int, but optionally
pub fn deserialize_number_opt<'de, D>(deserializer: D) -> Result<Option<U256>, D::Error>
where
D: Deserializer<'de>,
{
let num = match Option::<Numeric>::deserialize(deserializer)? {
Some(Numeric::U256(n)) => Some(n),
Some(Numeric::Num(n)) => Some(U256::from(n)),
_ => None,
};
Ok(num)
}
/// Deserializes single integer params: `1, [1], ["0x01"]`
pub fn deserialize_number_seq<'de, D>(deserializer: D) -> Result<U256, D::Error>
where
D: Deserializer<'de>,
{
let num = match NumericSeq::deserialize(deserializer)? {
NumericSeq::Seq(seq) => seq.into_iter().next().unwrap().into(),
NumericSeq::U256(n) => n,
NumericSeq::Num(n) => U256::from(n),
};
Ok(num)
}
/// Various block number representations, See [`lenient_block_number()`]
#[derive(Deserialize)]
#[serde(untagged)]
pub enum LenientBlockNumber {
BlockNumber(BlockNumber),
Num(u64),
}
impl From<LenientBlockNumber> for BlockNumber {
fn from(b: LenientBlockNumber) -> Self {
match b {
LenientBlockNumber::BlockNumber(b) => b,
LenientBlockNumber::Num(b) => b.into(),
}
}
}
/// Following the spec the block parameter is either:
///
/// > HEX String - an integer block number
/// > String "earliest" for the earliest/genesis block
/// > String "latest" - for the latest mined block
/// > String "pending" - for the pending state/transactions
///
/// and with EIP-1898:
/// > blockNumber: QUANTITY - a block number
/// > blockHash: DATA - a block hash
///
/// https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1898.md
///
/// EIP-1898 does not all calls that use `BlockNumber` like `eth_getBlockByNumber` and doesn't list
/// raw integers as supported.
///
/// However, there are dev node implementations that support integers, such as ganache: <https://github.com/foundry-rs/foundry/issues/1868>
///
/// N.B.: geth does not support ints in `eth_getBlockByNumber`
pub fn lenient_block_number<'de, D>(deserializer: D) -> Result<BlockNumber, D::Error>
where
D: Deserializer<'de>,
{
LenientBlockNumber::deserialize(deserializer).map(Into::into)
}
/// Same as `lenient_block_number` but requires to be `[num; 1]`
pub fn lenient_block_number_seq<'de, D>(deserializer: D) -> Result<BlockNumber, D::Error>
where
D: Deserializer<'de>,
{
let num =
<[LenientBlockNumber; 1]>::deserialize(deserializer)?.into_iter().next().unwrap().into();
Ok(num)
}

View File

@ -1,17 +1,22 @@
use crate::{
abi,
abi::{HumanReadableParser, ParamType, Token},
types::{serde_helpers::StringifiedNumeric, Address, Bytes, U256},
utils::keccak256,
};
use convert_case::{Case, Casing};
use core::convert::TryFrom;
use ethabi::encode;
use proc_macro2::TokenStream;
use serde::{Deserialize, Deserializer, Serialize};
use std::collections::{BTreeMap, HashSet};
use syn::{
parse::Error, spanned::Spanned as _, AttrStyle, Data, DeriveInput, Expr, Fields,
GenericArgument, Lit, NestedMeta, PathArguments, Type,
};
use crate::{
abi,
abi::{ParamType, Token},
types::{Address, U256},
utils::keccak256,
};
/// Custom types for `TypedData`
pub type Types = BTreeMap<String, Vec<Eip712DomainType>>;
/// Pre-computed value of the following statement:
///
@ -45,7 +50,7 @@ pub enum Eip712Error {
#[error("Nested Eip712 struct not implemented. Failed to parse.")]
NestedEip712StructNotImplemented,
#[error("Error from Eip712 struct: {0:?}")]
Inner(String),
Message(String),
}
/// The Eip712 trait provides helper methods for computing
@ -102,24 +107,37 @@ pub trait Eip712 {
/// Eip712 Domain attributes used in determining the domain separator;
/// Unused fields are left out of the struct type.
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
///
/// Protocol designers only need to include the fields that make sense for their signing domain.
/// Unused fields are left out of the struct type.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EIP712Domain {
/// The user readable name of signing domain, i.e. the name of the DApp or the protocol.
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
/// The current major version of the signing domain. Signatures from different versions are not
/// compatible.
pub version: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<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,
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "crate::types::serde_helpers::deserialize_number_opt"
)]
pub chain_id: Option<U256>,
/// The address of the contract that will verify the signature.
pub verifying_contract: Address,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verifying_contract: Option<Address>,
/// A disambiguating salt for the protocol. This can be used as a domain separator of last
/// resort.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub salt: Option<[u8; 32]>,
}
@ -127,26 +145,58 @@ 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] {
let domain_type_hash = if self.salt.is_some() {
EIP712_DOMAIN_TYPE_HASH_WITH_SALT
} else {
EIP712_DOMAIN_TYPE_HASH
};
// full name is `EIP712Domain(string name,string version,uint256 chainId,address
// verifyingContract,bytes32 salt)`
let mut ty = "EIP712Domain(".to_string();
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),
];
let mut tokens = Vec::new();
let mut needs_comma = false;
if let Some(ref name) = self.name {
ty += "string name";
tokens.push(Token::Uint(U256::from(keccak256(name))));
needs_comma = true;
}
// Add the salt to the struct to be hashed if it exists;
if let Some(salt) = &self.salt {
if let Some(ref version) = self.version {
if needs_comma {
ty.push(',');
}
ty += "string version";
tokens.push(Token::Uint(U256::from(keccak256(version))));
needs_comma = true;
}
if let Some(chain_id) = self.chain_id {
if needs_comma {
ty.push(',');
}
ty += "uint256 chainId";
tokens.push(Token::Uint(chain_id));
needs_comma = true;
}
if let Some(verifying_contract) = self.verifying_contract {
if needs_comma {
ty.push(',');
}
ty += "address verifyingContract";
tokens.push(Token::Address(verifying_contract));
needs_comma = true;
}
if let Some(salt) = self.salt {
if needs_comma {
ty.push(',');
}
ty += "bytes32 salt";
tokens.push(Token::Uint(U256::from(salt)));
}
keccak256(abi::encode(&tokens))
ty.push(')');
tokens.insert(0, Token::Uint(U256::from(keccak256(ty))));
keccak256(encode(&tokens))
}
}
@ -161,7 +211,7 @@ where
impl<T: Eip712 + Clone> EIP712WithDomain<T> {
pub fn new(inner: T) -> Result<Self, Eip712Error> {
let domain = inner.domain().map_err(|e| Eip712Error::Inner(e.to_string()))?;
let domain = inner.domain().map_err(|e| Eip712Error::Message(e.to_string()))?;
Ok(Self { domain, inner })
}
@ -180,13 +230,13 @@ impl<T: Eip712 + Clone> Eip712 for EIP712WithDomain<T> {
}
fn type_hash() -> Result<[u8; 32], Self::Error> {
let type_hash = T::type_hash().map_err(|e| Self::Error::Inner(e.to_string()))?;
let type_hash = T::type_hash().map_err(|e| Self::Error::Message(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()))?;
self.inner.clone().struct_hash().map_err(|e| Self::Error::Message(e.to_string()))?;
Ok(struct_hash)
}
}
@ -196,10 +246,6 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
type Error = TokenStream;
fn try_from(input: &syn::DeriveInput) -> Result<EIP712Domain, Self::Error> {
let mut domain = EIP712Domain::default();
let mut domain_name = None;
let mut domain_version = None;
let mut chain_id = None;
let mut verifying_contract = None;
let mut found_eip712_attribute = false;
@ -224,7 +270,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
match ident.to_string().as_ref() {
"name" => match meta.lit {
syn::Lit::Str(ref lit_str) => {
if domain_name.is_some() {
if domain.name.is_some() {
return Err(Error::new(
meta.path.span(),
"domain name already specified",
@ -232,7 +278,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
.to_compile_error())
}
domain_name = Some(lit_str.value());
domain.name = Some(lit_str.value());
}
_ => {
return Err(Error::new(
@ -244,7 +290,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
},
"version" => match meta.lit {
syn::Lit::Str(ref lit_str) => {
if domain_version.is_some() {
if domain.version.is_some() {
return Err(Error::new(
meta.path.span(),
"domain version already specified",
@ -252,7 +298,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
.to_compile_error())
}
domain_version = Some(lit_str.value());
domain.version = Some(lit_str.value());
}
_ => {
return Err(Error::new(
@ -264,7 +310,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
},
"chain_id" => match meta.lit {
syn::Lit::Int(ref lit_int) => {
if chain_id.is_some() {
if domain.chain_id.is_some() {
return Err(Error::new(
meta.path.span(),
"domain chain_id already specified",
@ -272,7 +318,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
.to_compile_error())
}
chain_id = Some(U256::from(
domain.chain_id = Some(U256::from(
lit_int.base10_parse::<u64>().map_err(
|_| {
Error::new(
@ -294,7 +340,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
},
"verifying_contract" => match meta.lit {
syn::Lit::Str(ref lit_str) => {
if verifying_contract.is_some() {
if domain.verifying_contract.is_some() {
return Err(Error::new(
meta.path.span(),
"domain verifying_contract already specified",
@ -302,7 +348,7 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
.to_compile_error());
}
verifying_contract = Some(lit_str.value().parse().map_err(|_| {
domain.verifying_contract = Some(lit_str.value().parse().map_err(|_| {
Error::new(
meta.path.span(),
"failed to parse verifying contract into Address",
@ -368,39 +414,6 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
}
}
}
domain.name = domain_name.ok_or_else(|| {
Error::new(
meta.path.span(),
"missing required domain attribute: 'name'".to_string(),
)
.to_compile_error()
})?;
domain.version = domain_version.ok_or_else(|| {
Error::new(
meta.path.span(),
"missing required domain attribute: 'version'".to_string(),
)
.to_compile_error()
})?;
domain.chain_id = chain_id.ok_or_else(|| {
Error::new(
meta.path.span(),
"missing required domain attribute: 'chain_id'".to_string(),
)
.to_compile_error()
})?;
domain.verifying_contract = verifying_contract.ok_or_else(|| {
Error::new(
meta.path.span(),
"missing required domain attribute: 'verifying_contract'"
.to_string(),
)
.to_compile_error()
})?;
}
break 'attribute_search
@ -420,6 +433,341 @@ impl TryFrom<&syn::DeriveInput> for EIP712Domain {
}
}
/// Represents the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) typed data object.
///
/// Typed data is a JSON object containing type information, domain separator parameters and the
/// message object which has the following schema
///
/// ```js
/// {
// type: 'object',
// properties: {
// types: {
// type: 'object',
// properties: {
// EIP712Domain: {type: 'array'},
// },
// additionalProperties: {
// type: 'array',
// items: {
// type: 'object',
// properties: {
// name: {type: 'string'},
// type: {type: 'string'}
// },
// required: ['name', 'type']
// }
// },
// required: ['EIP712Domain']
// },
// primaryType: {type: 'string'},
// domain: {type: 'object'},
// message: {type: 'object'}
// },
// required: ['types', 'primaryType', 'domain', 'message']
// }
/// ```
///
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(deny_unknown_fields)]
pub struct TypedData {
/// Signing domain metadata. The signing domain is the intended context for the signature (e.g.
/// the dapp, protocol, etc. that it's intended for). This data is used to construct the domain
/// seperator of the message.
pub domain: EIP712Domain,
/// The custom types used by this message.
pub types: Types,
#[serde(rename = "primaryType")]
/// The type of the message.
pub primary_type: String,
/// The message to be signed.
pub message: BTreeMap<String, serde_json::Value>,
}
/// According to the MetaMask implementation,
/// the message parameter may be JSON stringified in versions later than V1
/// See https://github.com/MetaMask/metamask-extension/blob/0dfdd44ae7728ed02cbf32c564c75b74f37acf77/app/scripts/metamask-controller.js#L1736
/// In fact, ethers.js JSON stringifies the message at the time of writing.
impl<'de> Deserialize<'de> for TypedData {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct TypedDataHelper {
domain: EIP712Domain,
types: Types,
#[serde(rename = "primaryType")]
primary_type: String,
message: BTreeMap<String, serde_json::Value>,
}
#[derive(Deserialize)]
#[serde(untagged)]
enum Type {
Val(TypedDataHelper),
String(String),
}
match Type::deserialize(deserializer)? {
Type::Val(v) => {
let TypedDataHelper { domain, types, primary_type, message } = v;
Ok(TypedData { domain, types, primary_type, message })
}
Type::String(s) => {
let TypedDataHelper { domain, types, primary_type, message } =
serde_json::from_str(&s).map_err(serde::de::Error::custom)?;
Ok(TypedData { domain, types, primary_type, message })
}
}
}
}
// === impl TypedData ===
impl Eip712 for TypedData {
type Error = Eip712Error;
fn domain(&self) -> Result<EIP712Domain, Self::Error> {
Ok(self.domain.clone())
}
fn type_hash() -> Result<[u8; 32], Self::Error> {
Err(Eip712Error::Message("dynamic type".to_string()))
}
fn struct_hash(&self) -> Result<[u8; 32], Self::Error> {
let tokens = encode_data(
&self.primary_type,
&serde_json::Value::Object(serde_json::Map::from_iter(self.message.clone())),
&self.types,
)?;
Ok(keccak256(encode(&tokens)))
}
/// Hash a typed message according to EIP-712. The returned message starts with the EIP-712
/// prefix, which is "1901", followed by the hash of the domain separator, then the data (if
/// any). The result is hashed again and returned.
fn encode_eip712(&self) -> Result<[u8; 32], Self::Error> {
let domain_separator = self.domain.separator();
let mut digest_input = [&[0x19, 0x01], &domain_separator[..]].concat().to_vec();
if self.primary_type != "EIP712Domain" {
// compatibility with <https://github.com/MetaMask/eth-sig-util>
digest_input.extend(&self.struct_hash()?[..])
}
Ok(keccak256(digest_input))
}
}
/// Represents the name and type pair
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Eip712DomainType {
pub name: String,
#[serde(rename = "type")]
pub r#type: String,
}
/// Encodes an object by encoding and concatenating each of its members.
///
/// The encoding of a struct instance is `enc(value₁) ‖ enc(value₂) ‖ … ‖ enc(valueₙ)`, i.e. the
/// concatenation of the encoded member values in the order that they appear in the type. Each
/// encoded member value is exactly 32-byte long.
///
/// - `primaryType`: The root type.
/// - `data`: The object to encode.
/// - `types`: Type definitions for all types included in the message.
///
/// Returns an encoded representation of an object
pub fn encode_data(
primary_type: &str,
data: &serde_json::Value,
types: &Types,
) -> Result<Vec<Token>, Eip712Error> {
let hash = hash_type(primary_type, types)?;
let mut tokens = vec![Token::Uint(U256::from(hash))];
if let Some(fields) = types.get(primary_type) {
for field in fields {
// handle recursive types
if let Some(value) = data.get(&field.name) {
let field = encode_field(types, &field.name, &field.r#type, value)?;
tokens.push(field);
} else if types.contains_key(&field.r#type) {
tokens.push(Token::Uint(U256::zero()));
} else {
return Err(Eip712Error::Message(format!("No data found for: `{}`", field.name)))
}
}
}
Ok(tokens)
}
/// Hashes an object
///
/// - `primary_type`: The root type to encode.
/// - `data`: The object to hash.
/// - `types`: All type definitions.
///
/// Returns the hash of the `primary_type` object
pub fn hash_struct(
primary_type: &str,
data: &serde_json::Value,
types: &Types,
) -> Result<[u8; 32], Eip712Error> {
let tokens = encode_data(primary_type, data, types)?;
let encoded = encode(&tokens);
Ok(keccak256(encoded))
}
/// Returns the hashed encoded type of `primary_type`
pub fn hash_type(primary_type: &str, types: &Types) -> Result<[u8; 32], Eip712Error> {
encode_type(primary_type, types).map(keccak256)
}
/// Encodes the type of an object by encoding a comma delimited list of its members.
///
/// - `primary_type`: The root type to encode.
/// - `types`: All type definitions.
///
/// Returns the encoded representation of the field.
pub fn encode_type(primary_type: &str, types: &Types) -> Result<String, Eip712Error> {
let mut names = HashSet::new();
find_type_dependencies(primary_type, types, &mut names);
// need to ensure primary_type is first in the list
names.remove(primary_type);
let mut deps: Vec<_> = names.into_iter().collect();
deps.sort_unstable();
deps.insert(0, primary_type);
let mut res = String::new();
for dep in deps.into_iter() {
let fields = types.get(dep).ok_or_else(|| {
Eip712Error::Message(format!("No type definition found for: `{dep}`"))
})?;
res += dep;
res.push('(');
res += &fields
.iter()
.map(|ty| format!("{} {}", ty.r#type, ty.name))
.collect::<Vec<_>>()
.join(",");
res.push(')');
}
Ok(res)
}
/// Returns all the custom types used in the `primary_type`
fn find_type_dependencies<'a>(
primary_type: &'a str,
types: &'a Types,
found: &mut HashSet<&'a str>,
) {
if found.contains(primary_type) {
return
}
if let Some(fields) = types.get(primary_type) {
found.insert(primary_type);
for field in fields {
// need to strip the array tail
let ty = field.r#type.split('[').next().unwrap();
find_type_dependencies(ty, types, found)
}
}
}
/// Encode a single field.
///
/// - `types`: All type definitions.
/// - `field`: The name and type of the field being encoded.
/// - `value`: The value to encode.
///
/// Returns the encoded representation of the field.
pub fn encode_field(
types: &Types,
_field_name: &str,
field_type: &str,
value: &serde_json::Value,
) -> Result<Token, Eip712Error> {
let token = {
// check if field is custom data type
if types.contains_key(field_type) {
let tokens = encode_data(field_type, value, types)?;
let encoded = encode(&tokens);
encode_eip712_type(Token::Bytes(encoded.to_vec()))
} else {
match field_type {
s if s.contains('[') => {
let (stripped_type, _) = s.rsplit_once('[').unwrap();
// ensure value is an array
let values = value.as_array().ok_or_else(|| {
Eip712Error::Message(format!(
"Expected array for type `{s}`, but got `{value}`",
))
})?;
let tokens = values
.iter()
.map(|value| encode_field(types, _field_name, stripped_type, value))
.collect::<Result<Vec<_>, _>>()?;
let encoded = encode(&tokens);
encode_eip712_type(Token::Bytes(encoded))
}
s => {
// parse as param type
let param = HumanReadableParser::parse_type(s).map_err(|err| {
Eip712Error::Message(format!("Failed to parse type {s}: {err}",))
})?;
match param {
ParamType::Address => {
Token::Address(serde_json::from_value(value.clone())?)
}
ParamType::Bytes => {
let data: Bytes = serde_json::from_value(value.clone())?;
encode_eip712_type(Token::Bytes(data.to_vec()))
}
ParamType::Int(_) => Token::Uint(serde_json::from_value(value.clone())?),
ParamType::Uint(_) => {
// uints are commonly stringified due to how ethers-js encodes
let val: StringifiedNumeric = serde_json::from_value(value.clone())?;
let val = val.try_into().map_err(|err| {
Eip712Error::Message(format!("Failed to parse uint {}", err))
})?;
Token::Uint(val)
}
ParamType::Bool => {
encode_eip712_type(Token::Bool(serde_json::from_value(value.clone())?))
}
ParamType::String => {
let s: String = serde_json::from_value(value.clone())?;
encode_eip712_type(Token::String(s))
}
ParamType::FixedArray(_, _) | ParamType::Array(_) => {
unreachable!("is handled in separate arm")
}
ParamType::FixedBytes(_) => {
let data: Bytes = serde_json::from_value(value.clone())?;
encode_eip712_type(Token::FixedBytes(data.to_vec()))
}
ParamType::Tuple(_) => {
return Err(Eip712Error::Message(format!("Unexpected tuple type {s}",)))
}
}
}
}
}
};
Ok(token)
}
/// 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
@ -562,8 +910,6 @@ pub fn make_type_hash(primary_type: String, fields: &[(String, ParamType)]) -> [
}
/// 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))),
@ -583,9 +929,348 @@ pub fn encode_eip712_type(token: Token) -> Token {
Token::FixedArray(tokens) => Token::Uint(U256::from(keccak256(abi::encode(
&tokens.into_iter().map(encode_eip712_type).collect::<Vec<Token>>(),
)))),
Token::Tuple(tuple) => {
let tokens = tuple.into_iter().map(encode_eip712_type).collect::<Vec<Token>>();
let encoded = encode(&tokens);
Token::Uint(U256::from(keccak256(encoded)))
}
_ => {
// Return the ABI encoded token;
token
}
}
}
// Adapted tests from <https://github.com/MetaMask/eth-sig-util/blob/main/src/sign-typed-data.test.ts>
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_full_domain() {
let json = serde_json::json!({
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
},
{
"name": "salt",
"type": "bytes32"
}
]
},
"primaryType": "EIP712Domain",
"domain": {
"name": "example.metamask.io",
"version": "1",
"chainId": 1,
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {}
});
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"122d1c8ef94b76dad44dcb03fa772361e20855c63311a15d5afe02d1b38f6077",
hex::encode(&hash[..])
);
}
#[test]
fn test_minimal_message() {
let json = serde_json::json!( {"types":{"EIP712Domain":[]},"primaryType":"EIP712Domain","domain":{},"message":{}});
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"8d4a3f4082945b7879e2b55f181c31a77c8c0a464b70669458abbaaf99de4c38",
hex::encode(&hash[..])
);
}
#[test]
fn test_encode_custom_array_type() {
let json = serde_json::json!({"domain":{},"types":{"EIP712Domain":[],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address[]"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person[]"},{"name":"contents","type":"string"}]},"primaryType":"Mail","message":{"from":{"name":"Cow","wallet":["0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826","0xDD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"]},"to":[{"name":"Bob","wallet":["0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"]}],"contents":"Hello, Bob!"}});
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"80a3aeb51161cfc47884ddf8eac0d2343d6ae640efe78b6a69be65e3045c1321",
hex::encode(&hash[..])
);
}
#[test]
fn test_hash_typed_message_with_data() {
let json = serde_json::json!( {
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"Message": [
{
"name": "data",
"type": "string"
}
]
},
"primaryType": "Message",
"domain": {
"name": "example.metamask.io",
"version": "1",
"chainId": "1",
"verifyingContract": "0x0000000000000000000000000000000000000000"
},
"message": {
"data": "Hello!"
}
});
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"232cd3ec058eb935a709f093e3536ce26cc9e8e193584b0881992525f6236eef",
hex::encode(&hash[..])
);
}
#[test]
fn test_hash_custom_data_type() {
let json = serde_json::json!( {"domain":{},"types":{"EIP712Domain":[],"Person":[{"name":"name","type":"string"},{"name":"wallet","type":"address"}],"Mail":[{"name":"from","type":"Person"},{"name":"to","type":"Person"},{"name":"contents","type":"string"}]},"primaryType":"Mail","message":{"from":{"name":"Cow","wallet":"0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"},"to":{"name":"Bob","wallet":"0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"},"contents":"Hello, Bob!"}});
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"25c3d40a39e639a4d0b6e4d2ace5e1281e039c88494d97d8d08f99a6ea75d775",
hex::encode(&hash[..])
);
}
#[test]
fn test_hash_recursive_types() {
let json = serde_json::json!( {
"domain": {},
"types": {
"EIP712Domain": [],
"Person": [
{
"name": "name",
"type": "string"
},
{
"name": "wallet",
"type": "address"
}
],
"Mail": [
{
"name": "from",
"type": "Person"
},
{
"name": "to",
"type": "Person"
},
{
"name": "contents",
"type": "string"
},
{
"name": "replyTo",
"type": "Mail"
}
]
},
"primaryType": "Mail",
"message": {
"from": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"to": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello, Bob!",
"replyTo": {
"to": {
"name": "Cow",
"wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"
},
"from": {
"name": "Bob",
"wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"
},
"contents": "Hello!"
}
}
});
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"0808c17abba0aef844b0470b77df9c994bc0fa3e244dc718afd66a3901c4bd7b",
hex::encode(&hash[..])
);
}
#[test]
fn test_hash_nested_struct_array() {
let json = serde_json::json!({
"types": {
"EIP712Domain": [
{
"name": "name",
"type": "string"
},
{
"name": "version",
"type": "string"
},
{
"name": "chainId",
"type": "uint256"
},
{
"name": "verifyingContract",
"type": "address"
}
],
"OrderComponents": [
{
"name": "offerer",
"type": "address"
},
{
"name": "zone",
"type": "address"
},
{
"name": "offer",
"type": "OfferItem[]"
},
{
"name": "startTime",
"type": "uint256"
},
{
"name": "endTime",
"type": "uint256"
},
{
"name": "zoneHash",
"type": "bytes32"
},
{
"name": "salt",
"type": "uint256"
},
{
"name": "conduitKey",
"type": "bytes32"
},
{
"name": "counter",
"type": "uint256"
}
],
"OfferItem": [
{
"name": "token",
"type": "address"
}
],
"ConsiderationItem": [
{
"name": "token",
"type": "address"
},
{
"name": "identifierOrCriteria",
"type": "uint256"
},
{
"name": "startAmount",
"type": "uint256"
},
{
"name": "endAmount",
"type": "uint256"
},
{
"name": "recipient",
"type": "address"
}
]
},
"primaryType": "OrderComponents",
"domain": {
"name": "Seaport",
"version": "1.1",
"chainId": "1",
"verifyingContract": "0x00000000006c3852cbEf3e08E8dF289169EdE581"
},
"message": {
"offerer": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"offer": [
{
"token": "0xA604060890923Ff400e8c6f5290461A83AEDACec"
}
],
"startTime": "1658645591",
"endTime": "1659250386",
"zone": "0x004C00500000aD104D7DBd00e3ae0A5C00560C00",
"zoneHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"salt": "16178208897136618",
"conduitKey": "0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000",
"totalOriginalConsiderationItems": "2",
"counter": "0"
}
}
);
let typed_data: TypedData = serde_json::from_value(json).unwrap();
let hash = typed_data.encode_eip712().unwrap();
assert_eq!(
"0b8aa9f3712df0034bc29fe5b24dd88cfdba02c7f499856ab24632e2969709a8",
hex::encode(&hash[..])
);
}
}