derive-eip712: initial implementation of eip712 derive macro (#481)

* 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<T: Eip712 + Send + Sync>(
        &self,
        payload: &T,
    ) -> Result<Signature, Self::Error>;

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.

* rename to ethers-derive-eip712; move to ethers-core

* refactor of derive-eip712 macro; use ParamType and EthAbiToken

* macro updates; add byte array checker for paramtype; use literal constant for domain type hash

* replace std::convert::Infallible with WalletError as Wallet signer error type

* update workspace members and dev dependencies for examples folder

* add example for eip712 and test against contract

* remove extraneous backward slash in '\x19\x01' prefix; example tests pass

* update unreleased change log

* remove print statements

* use parse_macro_input macro; remove dead code; handle nest struct not implemented error

* move eip712 example to solidity-contract tests folder; update cargo workspace dependencies

* allow optional EIP712Domain parameter when encoding eip712 struct and signing typed data

* add documentation for eip712 feature

* Update ethers-signers/src/ledger/mod.rs

Co-authored-by: Sebastian Martinez <me@sebastinez.dev>

* add error enum for Eip712Error; use sign_payload for ledger signer

* add EIP712WithDomain type for providing a wrapper around custom setting of the domain

* make LedgerWallet sign_payload public

* use optional feature gated dependencies for eip712; add default method for encode_eip712

* add default domain_separator method, pre-compute separator hash

* move derive-eip712 deps to dev deps

* remove invalid sign payload parameter, add await on async method

* remove deprecated comment

* debugging 'bad key handle' error for ledger signer

try using 'sign_message'

* await sign digest for aws signer

* remove extra space, fix fmt warning

* fix test, fmt errors

* use gt 0.6.0 pragma compiler version

* enable ABIEncoderV2 for solidity test contract

* chore: make test constructor public

Co-authored-by: Sebastian Martinez <me@sebastinez.dev>
Co-authored-by: Georgios Konstantopoulos <me@gakonst.com>
This commit is contained in:
Ryan 2021-10-08 08:22:51 -07:00 committed by GitHub
parent 06eb8f223c
commit d7ab229a4c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 1672 additions and 132 deletions

1
.gitignore vendored
View File

@ -1 +1,2 @@
/target
.vscode

View File

@ -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)
- Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457)

229
Cargo.lock generated
View File

@ -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"
@ -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"
@ -554,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"
@ -589,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",
@ -662,9 +659,9 @@ 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",
]
@ -820,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",
@ -833,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",
@ -850,11 +847,13 @@ name = "ethers"
version = "0.5.3"
dependencies = [
"anyhow",
"bytes",
"ethers-contract",
"ethers-core",
"ethers-middleware",
"ethers-providers",
"ethers-signers",
"hex",
"rand 0.8.4",
"serde",
"serde_json",
@ -868,6 +867,8 @@ dependencies = [
"ethers-contract-abigen",
"ethers-contract-derive",
"ethers-core",
"ethers-derive-eip712",
"ethers-middleware",
"ethers-providers",
"ethers-signers",
"futures-util",
@ -921,6 +922,7 @@ dependencies = [
"arrayvec 0.7.1",
"bincode",
"bytes",
"convert_case",
"ecdsa",
"elliptic-curve",
"ethabi",
@ -931,17 +933,36 @@ dependencies = [
"hex-literal",
"k256",
"once_cell",
"proc-macro2",
"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-contract",
"ethers-contract-derive",
"ethers-core",
"ethers-signers",
"hex",
"proc-macro2",
"quote",
"serde",
"serde_json",
"syn",
]
[[package]]
name = "ethers-etherscan"
version = "0.1.0"
@ -1107,9 +1128,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",
@ -1273,9 +1294,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",
@ -1346,9 +1367,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",
@ -1380,9 +1401,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",
@ -1510,9 +1531,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"
@ -1550,9 +1571,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"
@ -1725,9 +1746,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",
]
@ -1772,9 +1793,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",
@ -1806,9 +1827,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",
@ -1820,9 +1841,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",
@ -1857,9 +1878,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",
@ -1938,9 +1959,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",
@ -1948,9 +1969,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"
@ -1973,9 +1994,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",
@ -2019,18 +2040,18 @@ 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",
]
[[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",
]
@ -2363,9 +2384,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"
@ -2482,9 +2503,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",
@ -2495,9 +2516,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",
@ -2553,9 +2574,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 +2593,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 +2604,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",
@ -2606,13 +2627,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",
]
@ -2643,7 +2664,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",
]
@ -2671,9 +2692,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"
@ -2715,15 +2736,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",
@ -2816,9 +2837,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "syn"
version = "1.0.74"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
dependencies = [
"proc-macro2",
"quote",
@ -2945,9 +2966,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",
]
@ -2960,9 +2981,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",
@ -2979,9 +3000,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",
@ -3173,9 +3194,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"
@ -3499,18 +3520,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",

View File

@ -19,7 +19,7 @@ members = [
"ethers-signers",
"ethers-core",
"ethers-middleware",
"ethers-etherscan"
"ethers-etherscan",
]
default-members = [
@ -60,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"]
@ -88,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"

View File

@ -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 }
@ -24,10 +24,12 @@ 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"] }

View File

@ -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::*;

View File

@ -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,151 @@ 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<u8>,
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::<Http>::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,
foo_bar
.domain()
.expect("failed to return domain_separator from Eip712 implemented struct")
.separator(),
"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!");
}
}

View File

@ -0,0 +1,82 @@
// SPDX-License-Identifier: GPL-3.0-only
pragma solidity >=0.6.0;
pragma experimental ABIEncoderV2;
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)"
);
struct FooBar {
int256 foo;
uint256 bar;
bytes fizz;
bytes32 buzz;
string far;
address out;
}
constructor() public {}
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(bytes(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);
}
}

View File

@ -0,0 +1,206 @@
[
{
"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": "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",
"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": "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",
"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": "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",
"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"
}
]

View File

@ -33,6 +33,12 @@ hex = { version = "0.4.3", default-features = false, features = ["std"] }
semver = "1.0.4"
once_cell = "1.8.0"
# 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
tokio = { version = "1.5", default-features = false, optional = true}
@ -49,6 +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 = ["convert_case", "syn", "quote", "proc-macro2"]
[package.metadata.docs.rs]
all-features = true

View File

@ -0,0 +1,22 @@
[package]
name = "ethers-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 = { version = "^0.5.0", path = "../", default-features = false, features = ["eip712"] }
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" }

View File

@ -0,0 +1,170 @@
//! # 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.
//!
//! 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
//! use ethers_contract::EthAbiType;
//! use ethers_derive_eip712::*;
//! use ethers_core::types::{transaction::eip712::Eip712, H160};
//!
//! #[derive(Debug, Eip712, EthAbiType)]
//! #[eip712(
//! name = "Radicle",
//! version = "1",
//! chain_id = 1,
//! verifying_contract = "0x0000000000000000000000000000000000000000"
//! // salt is an optional parameter
//! salt = "my-unique-spice"
//! )]
//! pub struct Puzzle {
//! pub organization: H160,
//! pub contributor: H160,
//! pub commit: String,
//! pub project: String,
//! }
//!
//! let puzzle = Puzzle {
//! organization: "0000000000000000000000000000000000000000"
//! .parse::<H160>()
//! .expect("failed to parse address"),
//! contributor: "0000000000000000000000000000000000000000"
//! .parse::<H160>()
//! .expect("failed to parse address"),
//! commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(),
//! project: "radicle-reward".to_string(),
//! };
//!
//! let hash = puzzle.encode_eip712()?;
//! ```
//!
//! # 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;
use syn::parse_macro_input;
#[proc_macro_derive(Eip712, attributes(eip712))]
pub fn eip_712_derive(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input);
impl_eip_712_macro(&ast)
}
// 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;
// Instantiate domain from parsed attributes
let domain = match eip712::EIP712Domain::try_from(ast) {
Ok(attributes) => attributes,
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) {
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 = 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() -> Result<[u8; 32], Self::Error> {
use std::convert::TryFrom;
let decoded = hex::decode(#type_hash)?;
let byte_array: [u8; 32] = <[u8; 32]>::try_from(&decoded[..])?;
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<ethers_core::types::transaction::eip712::EIP712Domain, Self::Error> {
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> {
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
return Err(Self::Error::NestedEip712StructNotImplemented);
},
_ => {
items.push(ethers_core::types::transaction::eip712::encode_eip712_type(token));
}
}
}
}
let struct_hash = ethers_core::utils::keccak256(ethers_core::abi::encode(
&items,
));
Ok(struct_hash)
}
}
};
implementation.into()
}

View File

@ -0,0 +1,181 @@
use ethers_contract::EthAbiType;
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::*;
#[test]
fn test_derive_eip712() {
#[derive(Debug, Clone, Eip712, EthAbiType)]
#[eip712(
name = "Radicle",
version = "1",
chain_id = 1,
verifying_contract = "0x0000000000000000000000000000000000000001"
)]
pub struct Puzzle {
pub organization: H160,
pub contributor: H160,
pub commit: String,
pub project: String,
}
let puzzle = Puzzle {
organization: "0000000000000000000000000000000000000000"
.parse::<H160>()
.expect("failed to parse address"),
contributor: "0000000000000000000000000000000000000000"
.parse::<H160>()
.expect("failed to parse address"),
commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(),
project: "radicle-reward".to_string(),
};
let hash = puzzle.encode_eip712().expect("failed to encode struct");
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 = "0x0000000000000000000000000000000000000001",
salt = "1234567890"
)]
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]),
salt: None,
};
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 = "0x0000000000000000000000000000000000000001"
)]
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 = "0x0000000000000000000000000000000000000001"
)]
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)
}
#[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)")
);
}

View File

@ -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

View File

@ -0,0 +1,614 @@
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::{ParamType, Token},
types::{Address, H160, U256},
utils::keccak256,
};
/// Pre-computed value of the following statement:
///
/// `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,
155, 15, 250, 202, 169, 167, 93, 82, 43, 57, 64, 15,
];
/// Pre-computed value of the following statement:
///
/// `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,
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 {
#[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,
#[error("Failed to convert slice into byte array")]
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
/// 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.
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
/// 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(&self) -> Result<EIP712Domain, 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() -> 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<[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 = self.domain_separator()?;
let struct_hash = self.struct_hash()?;
let digest_input = [&[0x19, 0x01], &domain_separator[..], &struct_hash[..]].concat();
Ok(keccak256(digest_input))
}
}
/// 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)]
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] {
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))
}
}
#[derive(Debug, Clone)]
pub struct EIP712WithDomain<T>
where
T: Clone + Eip712,
{
pub domain: EIP712Domain,
pub inner: T,
}
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()))?;
Ok(Self { domain, inner })
}
pub fn set_domain(self, domain: EIP712Domain) -> Self {
Self {
domain,
inner: self.inner,
}
}
}
impl<T: Eip712 + Clone> Eip712 for EIP712WithDomain<T> {
type Error = Eip712Error;
fn domain(&self) -> Result<EIP712Domain, Self::Error> {
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)
}
}
// Parse the AST of the struct to determine the domain attributes
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 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 {
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());
}
},
"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(<string>) 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(),
"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());
}
}
}
}
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<ParamType, TokenStream> {
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::<usize>() {
if let ParamType::Uint(_) = param {
return Ok(ParamType::FixedBytes(size));
}
return Ok(ParamType::FixedArray(Box::new(param), size));
}
}
}
Err(
Error::new(ty.span(), "Failed to derive proper ABI from array field")
.to_compile_error(),
)
}
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),
"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(),
format!("Failed to derive proper ABI from field: {})", s),
)
.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)?;
// 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)));
}
}
}
}
Err(Error::new(ty.span(), "Failed to derive proper ABI from fields").to_compile_error())
}
Type::Tuple(ty) => {
let params = ty
.elems
.iter()
.map(|t| find_parameter_type(t))
.collect::<Result<Vec<_>, _>>()?;
Ok(ParamType::Tuple(params))
}
_ => {
Err(Error::new(ty.span(), "Failed to derive proper ABI from fields").to_compile_error())
}
}
}
fn parse_int_param_type(s: &str) -> Option<ParamType> {
let size = s
.chars()
.skip(1)
.collect::<String>()
.parse::<usize>()
.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: &DeriveInput) -> Result<Vec<(String, ParamType)>, TokenStream> {
let mut fields = Vec::new();
let data = match &ast.data {
Data::Struct(s) => s,
_ => {
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 {
Fields::Named(name) => name,
_ => {
return Err(
Error::new(ast.span(), "unnamed fields are not supported").to_compile_error()
)
}
};
for f in named_fields.named.iter() {
let field_name = f
.ident
.clone()
.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)?,
};
fields.push((field_name, field_type));
}
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: &[(String, ParamType)]) -> [u8; 32] {
let parameters = fields
.iter()
.map(|(k, v)| format!("{} {}", v, k))
.collect::<Vec<String>>()
.join(",");
let sig = format!("{}({})", primary_type, parameters);
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::<Vec<Token>>(),
)))),
Token::FixedArray(tokens) => Token::Uint(U256::from(keccak256(abi::encode(
&tokens
.into_iter()
.map(encode_eip712_type)
.collect::<Vec<Token>>(),
)))),
_ => {
// Return the ABI encoded token;
token
}
}
}

View File

@ -5,6 +5,9 @@ pub mod eip1559;
pub mod eip2718;
pub mod eip2930;
#[cfg(feature = "eip712")]
pub mod eip712;
pub(crate) const BASE_NUM_TX_FIELDS: usize = 9;
// Number of tx fields before signing

View File

@ -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"

View File

@ -2,7 +2,10 @@
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::Eip712, Address,
Signature as EthSig, H256,
},
utils::hash_message,
};
use rusoto_core::RusotoError;
@ -83,6 +86,12 @@ pub enum AwsSignerError {
Spki(spki::der::Error),
#[error("{0}")]
Other(String),
#[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<String> for AwsSignerError {
@ -245,6 +254,19 @@ impl<'a> super::Signer for AwsSigner<'a> {
self.sign_digest_with_eip155(sighash).await
}
async fn sign_typed_data<T: Eip712 + Send + Sync>(
&self,
payload: T,
) -> Result<EthSig, Self::Error> {
let hash = payload
.encode_eip712()
.map_err(|e| Self::Error::Eip712Error(e.to_string()))?;
let digest = self.sign_digest_with_eip155(hash.into()).await?;
Ok(digest)
}
fn address(&self) -> Address {
self.address
}

View File

@ -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<u8>,

View File

@ -4,8 +4,12 @@ pub mod types;
use crate::Signer;
use app::LedgerEthereum;
use async_trait::async_trait;
use ethers_core::types::{transaction::eip2718::TypedTransaction, Address, Signature};
use types::LedgerError;
use ethers_core::types::{
transaction::eip2718::TypedTransaction,
transaction::eip712::{EIP712Domain, Eip712},
Address, Signature,
};
use types::{LedgerError, INS};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@ -25,6 +29,19 @@ impl Signer for LedgerEthereum {
self.sign_tx(message).await
}
async fn sign_typed_data<T: Eip712 + Send + Sync>(
&self,
payload: T,
) -> Result<Signature, Self::Error> {
let hash = payload
.encode_eip712()
.map_err(|e| Self::Error::Eip712Error(e.to_string()))?;
let sig = self.sign_message(hash).await?;
Ok(sig)
}
/// Returns the signer's Ethereum Address
fn address(&self) -> Address {
self.address

View File

@ -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;

View File

@ -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,13 @@ pub trait Signer: std::fmt::Debug + Send + Sync {
/// Signs the transaction
async fn sign_transaction(&self, message: &TypedTransaction) -> Result<Signature, Self::Error>;
/// Encodes and signs the typed data according EIP-712.
/// Payload must implement Eip712 trait.
async fn sign_typed_data<T: Eip712 + Send + Sync>(
&self,
payload: T,
) -> Result<Signature, Self::Error>;
/// Returns the signer's Ethereum Address
fn address(&self) -> Address;

View File

@ -16,7 +16,10 @@ 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;
@ -67,7 +70,7 @@ pub struct Wallet<D: DigestSigner<Sha256Proxy, RecoverableSignature>> {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer for Wallet<D> {
type Error = std::convert::Infallible;
type Error = WalletError;
async fn sign_message<S: Send + Sync + AsRef<[u8]>>(
&self,
@ -83,6 +86,17 @@ impl<D: Sync + Send + DigestSigner<Sha256Proxy, RecoverableSignature>> Signer fo
Ok(self.sign_transaction_sync(tx))
}
async fn sign_typed_data<T: Eip712 + Send + Sync>(
&self,
payload: T,
) -> Result<Signature, Self::Error> {
let encoded = payload
.encode_eip712()
.map_err(|e| Self::Error::Eip712Error(e.to_string()))?;
Ok(self.sign_hash(H256::from(encoded), false))
}
fn address(&self) -> Address {
self.address
}

View File

@ -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<SigningKey> {