diff --git a/.gitignore b/.gitignore index ea8c4bf7..ccb51663 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /target +.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 987c9958..f552fc1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,18 +4,20 @@ ### Unreleased -* Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) -* +- Use rust types as contract function inputs for human readable abi [#482](https://github.com/gakonst/ethers-rs/pull/482) +- Add EIP-712 `sign_typed_data` signer method; add ethers-core type `Eip712` trait and derive macro in ethers-derive-eip712 [#481](https://github.com/gakonst/ethers-rs/pull/481) + ### 0.5.3 -* Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427) -* Decimal support for `ethers_core::utils::parse_units` [#463](https://github.com/gakonst/ethers-rs/pull/463) -* Fixed Wei unit calculation in `Units` [#460](https://github.com/gakonst/ethers-rs/pull/460) -* Add `ethers_core::utils::get_create2_address_from_hash` [#444](https://github.com/gakonst/ethers-rs/pull/444) -* Bumped ethabi to 0.15.0 and fixing breaking changes [#469](https://github.com/gakonst/ethers-rs/pull/469), [#448](https://github.com/gakonst/ethers-rs/pull/448), [#445](https://github.com/gakonst/ethers-rs/pull/445) +- Allow configuring the optimizer & passing arbitrary arguments to solc [#427](https://github.com/gakonst/ethers-rs/pull/427) +- Decimal support for `ethers_core::utils::parse_units` [#463](https://github.com/gakonst/ethers-rs/pull/463) +- Fixed Wei unit calculation in `Units` [#460](https://github.com/gakonst/ethers-rs/pull/460) +- Add `ethers_core::utils::get_create2_address_from_hash` [#444](https://github.com/gakonst/ethers-rs/pull/444) +- Bumped ethabi to 0.15.0 and fixing breaking changes [#469](https://github.com/gakonst/ethers-rs/pull/469), [#448](https://github.com/gakonst/ethers-rs/pull/448), [#445](https://github.com/gakonst/ethers-rs/pull/445) ### 0.5.2 -* Correctly RLP Encode transactions as received from the mempool ([#415](https://github.com/gakonst/ethers-rs/pull/415)) + +- Correctly RLP Encode transactions as received from the mempool ([#415](https://github.com/gakonst/ethers-rs/pull/415)) ## ethers-providers @@ -23,11 +25,12 @@ ### 0.5.3 -* Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435) -* Add `eth_getProof` [#459](https://github.com/gakonst/ethers-rs/pull/459) +- Expose `ens` module [#435](https://github.com/gakonst/ethers-rs/pull/435) +- Add `eth_getProof` [#459](https://github.com/gakonst/ethers-rs/pull/459) ### 0.5.2 -* Set resolved ENS name during gas estimation ([1e5a9e](https://github.com/gakonst/ethers-rs/commit/1e5a9efb3c678eecd43d5c341b4932da35445831)) + +- Set resolved ENS name during gas estimation ([1e5a9e](https://github.com/gakonst/ethers-rs/commit/1e5a9efb3c678eecd43d5c341b4932da35445831)) ## ethers-signers @@ -38,7 +41,8 @@ ### Unreleased ### 0.5.3 -* (De)Tokenize structs and events with only a single field as `Token:Tuple` ([#417](https://github.com/gakonst/ethers-rs/pull/417)) + +- (De)Tokenize structs and events with only a single field as `Token:Tuple` ([#417](https://github.com/gakonst/ethers-rs/pull/417)) ## ethers-middleware @@ -46,4 +50,4 @@ ### 0.5.3 -* Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457) \ No newline at end of file +- Added Time Lagged middleware [#457](https://github.com/gakonst/ethers-rs/pull/457) diff --git a/Cargo.lock b/Cargo.lock index 7e86fd52..82878e94 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,9 +29,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aead" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3e798aa0c8239776f54415bc06f3d74b1850f3f830b45c35cfc80556973f70" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" dependencies = [ "generic-array 0.14.4", "rand_core 0.6.3", @@ -39,13 +39,13 @@ dependencies = [ [[package]] name = "aes" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" dependencies = [ "cfg-if 1.0.0", "cipher", - "cpufeatures 0.1.5", + "cpufeatures", "opaque-debug 0.3.0", ] @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "arrayref" @@ -191,9 +191,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "base64ct" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" +checksum = "40a96587c05c810ddbb79e2674d519cff1379517e7b91d166dff7a7cc0e9af6e" [[package]] name = "bech32" @@ -240,9 +240,9 @@ dependencies = [ [[package]] name = "blake2" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a5720225ef5daecf08657f23791354e1685a8c91a4c60c7f3d3b2892f978f4" +checksum = "0a4e37d16930f5459780f5621038b6382b9bb37c19016f39fb6b5808d831f174" dependencies = [ "crypto-mac 0.8.0", "digest 0.9.0", @@ -315,15 +315,15 @@ checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "d9df67f7bf9ef8498769f994239c45613ef0c5899415fb58e9add412d2c1a538" [[package]] name = "byte-slice-cast" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c1bf4a04a88c54f589125563643d773f3254b5c38571395e2b591c693bbc81" +checksum = "ca0796d76a983651b4a0ddda16203032759f2fd9103d9181f7c65c06ee8872e6" [[package]] name = "byte-tools" @@ -379,9 +379,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" [[package]] name = "ccm" @@ -522,9 +522,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c32f031ea41b4291d695026c023b95d59db2d8a2c7640800ed56bc8f510f22" +checksum = "fdab415d6744056100f40250a66bc430c1a46f7a02e20bc11c94c79a0f0464df" [[package]] name = "const_fn" @@ -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", diff --git a/Cargo.toml b/Cargo.toml index 5862e0dc..da413e10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" \ No newline at end of file diff --git a/ethers-contract/Cargo.toml b/ethers-contract/Cargo.toml index 41e0523d..b7afff18 100644 --- a/ethers-contract/Cargo.toml +++ b/ethers-contract/Cargo.toml @@ -11,7 +11,7 @@ keywords = ["ethereum", "web3", "celo", "ethers"] [dependencies] ethers-providers = { version = "^0.5.0", path = "../ethers-providers", default-features = false } -ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false } +ethers-core = { version = "^0.5.0", path = "../ethers-core", default-features = false, features = ["eip712"]} ethers-contract-abigen = { version = "^0.5.0", path = "ethers-contract-abigen", optional = true } ethers-contract-derive = { version = "^0.5.0", path = "ethers-contract-derive", optional = true } @@ -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"] } diff --git a/ethers-contract/src/lib.rs b/ethers-contract/src/lib.rs index ff93c8b5..2a285816 100644 --- a/ethers-contract/src/lib.rs +++ b/ethers-contract/src/lib.rs @@ -55,3 +55,6 @@ pub use ethers_contract_derive::{abigen, EthAbiType, EthEvent}; // Hide the Lazy re-export, it's just for convenience #[doc(hidden)] pub use once_cell::sync::Lazy; + +#[cfg(feature = "eip712")] +pub use ethers_derive_eip712::*; diff --git a/ethers-contract/tests/contract.rs b/ethers-contract/tests/contract.rs index 39a4f45c..cc248413 100644 --- a/ethers-contract/tests/contract.rs +++ b/ethers-contract/tests/contract.rs @@ -1,4 +1,4 @@ -use ethers_contract::ContractFactory; +use ethers_contract::{abigen, ContractFactory, EthAbiType}; use ethers_core::types::{Filter, ValueOrArray, H256}; mod common; @@ -9,11 +9,14 @@ mod eth_tests { use super::*; use ethers_contract::{LogMeta, Multicall}; use ethers_core::{ - types::{Address, BlockId, U256}, - utils::Ganache, + types::{transaction::eip712::Eip712, Address, BlockId, I256, U256}, + utils::{keccak256, Ganache}, }; + use ethers_derive_eip712::*; + use ethers_middleware::signer::SignerMiddleware; use ethers_providers::{Http, Middleware, PendingTransaction, Provider, StreamExt}; - use std::{convert::TryFrom, sync::Arc}; + use ethers_signers::{LocalWallet, Signer}; + use std::{convert::TryFrom, sync::Arc, time::Duration}; #[tokio::test] async fn deploy_and_call_contract() { @@ -580,4 +583,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, + buzz: [u8; 32], + far: String, + out: Address, + } + + // get ABI and bytecode for the DeriveEip712Test contract + let (abi, bytecode) = compile_contract("DeriveEip712Test", "DeriveEip712Test.sol"); + + // launch ganache + let ganache = Ganache::new().spawn(); + + let wallet: LocalWallet = ganache.keys()[0].clone().into(); + + let provider = Provider::::try_from(ganache.endpoint()) + .expect("failed to instantiate provider from ganache endpoint") + .interval(Duration::from_millis(10u64)); + + let client = SignerMiddleware::new(provider, wallet.clone()); + let client = Arc::new(client); + + let factory = ContractFactory::new(abi.clone(), bytecode.clone(), client.clone()); + + let contract = factory + .deploy(()) + .expect("failed to deploy DeriveEip712Test contract") + .legacy() + .send() + .await + .expect("failed to instantiate factory for DeriveEip712 contract"); + + let addr = contract.address(); + + let contract = DeriveEip712Test::new(addr, client.clone()); + + let foo_bar = FooBar { + foo: I256::from(10), + bar: U256::from(20), + fizz: b"fizz".to_vec(), + buzz: keccak256("buzz"), + far: String::from("space"), + out: Address::from([0; 20]), + }; + + let derived_foo_bar = deriveeip712test_mod::FooBar { + foo: foo_bar.foo.clone(), + bar: foo_bar.bar.clone(), + fizz: foo_bar.fizz.clone(), + buzz: foo_bar.buzz.clone(), + far: foo_bar.far.clone(), + out: foo_bar.out.clone(), + }; + + let sig = wallet + .sign_typed_data(foo_bar.clone()) + .await + .expect("failed to sign typed data"); + + let r = <[u8; 32]>::try_from(sig.r) + .expect("failed to parse 'r' value from signature into [u8; 32]"); + let s = <[u8; 32]>::try_from(sig.s) + .expect("failed to parse 's' value from signature into [u8; 32]"); + let v = u8::try_from(sig.v).expect("failed to parse 'v' value from signature into u8"); + + let domain_separator = contract + .domain_separator() + .call() + .await + .expect("failed to retrieve domain_separator from contract"); + let type_hash = contract + .type_hash() + .call() + .await + .expect("failed to retrieve type_hash from contract"); + let struct_hash = contract + .struct_hash(derived_foo_bar.clone()) + .call() + .await + .expect("failed to retrieve struct_hash from contract"); + let encoded = contract + .encode_eip_712(derived_foo_bar.clone()) + .call() + .await + .expect("failed to retrieve eip712 encoded hash from contract"); + let verify = contract + .verify_foo_bar(wallet.address(), derived_foo_bar, r, s, v) + .call() + .await + .expect("failed to verify signed typed data eip712 payload"); + + assert_eq!( + domain_separator, + 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!"); + } } diff --git a/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol new file mode 100644 index 00000000..0aced88b --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/DeriveEip712Test.sol @@ -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); + } +} diff --git a/ethers-contract/tests/solidity-contracts/derive_eip712_abi.json b/ethers-contract/tests/solidity-contracts/derive_eip712_abi.json new file mode 100644 index 00000000..a978024e --- /dev/null +++ b/ethers-contract/tests/solidity-contracts/derive_eip712_abi.json @@ -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" + } +] \ No newline at end of file diff --git a/ethers-core/Cargo.toml b/ethers-core/Cargo.toml index 7f77b08f..f6c55531 100644 --- a/ethers-core/Cargo.toml +++ b/ethers-core/Cargo.toml @@ -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 diff --git a/ethers-core/ethers-derive-eip712/Cargo.toml b/ethers-core/ethers-derive-eip712/Cargo.toml new file mode 100644 index 00000000..d8a2efb0 --- /dev/null +++ b/ethers-core/ethers-derive-eip712/Cargo.toml @@ -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" } \ No newline at end of file diff --git a/ethers-core/ethers-derive-eip712/src/lib.rs b/ethers-core/ethers-derive-eip712/src/lib.rs new file mode 100644 index 00000000..e1ceb096 --- /dev/null +++ b/ethers-core/ethers-derive-eip712/src/lib.rs @@ -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::() +//! .expect("failed to parse address"), +//! contributor: "0000000000000000000000000000000000000000" +//! .parse::() +//! .expect("failed to parse address"), +//! commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(), +//! project: "radicle-reward".to_string(), +//! }; +//! +//! let hash = puzzle.encode_eip712()?; +//! ``` +//! +//! # 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 { + 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() +} diff --git a/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs new file mode 100644 index 00000000..4821d669 --- /dev/null +++ b/ethers-core/ethers-derive-eip712/tests/derive_eip712.rs @@ -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::() + .expect("failed to parse address"), + contributor: "0000000000000000000000000000000000000000" + .parse::() + .expect("failed to parse address"), + commit: "5693b7019eb3e4487a81273c6f5e1832d77acb53".to_string(), + project: "radicle-reward".to_string(), + }; + + let hash = puzzle.encode_eip712().expect("failed to encode struct"); + + 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)") + ); +} diff --git a/ethers-core/src/lib.rs b/ethers-core/src/lib.rs index 7a947887..a105e58b 100644 --- a/ethers-core/src/lib.rs +++ b/ethers-core/src/lib.rs @@ -36,6 +36,10 @@ //! via the `GanacheBuilder` struct. In addition, you're able to compile contracts on the //! filesystem by providing a glob to their path, using the `Solc` struct. //! +//! # Features +//! +//! * - ["eip712"] | Provides Eip712 trait for EIP-712 encoding of typed data for derived structs +//! //! # ABI Encoding and Decoding //! //! This crate re-exports the [`ethabi`](ethabi) crate's functions diff --git a/ethers-core/src/types/transaction/eip712.rs b/ethers-core/src/types/transaction/eip712.rs new file mode 100644 index 00000000..57ab7e0e --- /dev/null +++ b/ethers-core/src/types/transaction/eip712.rs @@ -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; + + /// 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 +where + T: Clone + Eip712, +{ + pub domain: EIP712Domain, + pub inner: T, +} + +impl EIP712WithDomain { + pub fn new(inner: T) -> Result { + let domain = inner + .domain() + .map_err(|e| Eip712Error::Inner(e.to_string()))?; + + Ok(Self { domain, inner }) + } + + pub fn set_domain(self, domain: EIP712Domain) -> Self { + Self { + domain, + inner: self.inner, + } + } +} + +impl Eip712 for EIP712WithDomain { + type Error = Eip712Error; + + fn domain(&self) -> Result { + Ok(self.domain.clone()) + } + + fn type_hash() -> Result<[u8; 32], Self::Error> { + let type_hash = T::type_hash().map_err(|e| Self::Error::Inner(e.to_string()))?; + Ok(type_hash) + } + + fn struct_hash(&self) -> Result<[u8; 32], Self::Error> { + let struct_hash = self + .inner + .clone() + .struct_hash() + .map_err(|e| Self::Error::Inner(e.to_string()))?; + Ok(struct_hash) + } +} + +// 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 { + 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() 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 { + 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::() { + 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::, _>>()?; + 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 { + let size = s + .chars() + .skip(1) + .collect::() + .parse::() + .ok()?; + if s.starts_with('u') { + Some(ParamType::Uint(size)) + } else if s.starts_with('i') { + Some(ParamType::Int(size)) + } else { + None + } +} + +/// Return HashMap of the field name and the field type; +pub fn parse_fields(ast: &DeriveInput) -> Result, 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::>() + .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::>(), + )))), + Token::FixedArray(tokens) => Token::Uint(U256::from(keccak256(abi::encode( + &tokens + .into_iter() + .map(encode_eip712_type) + .collect::>(), + )))), + _ => { + // Return the ABI encoded token; + token + } + } +} diff --git a/ethers-core/src/types/transaction/mod.rs b/ethers-core/src/types/transaction/mod.rs index 51c5f9c9..baf9ce17 100644 --- a/ethers-core/src/types/transaction/mod.rs +++ b/ethers-core/src/types/transaction/mod.rs @@ -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 diff --git a/ethers-signers/Cargo.toml b/ethers-signers/Cargo.toml index d9a30469..7a952fb7 100644 --- a/ethers-signers/Cargo.toml +++ b/ethers-signers/Cargo.toml @@ -14,7 +14,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] -ethers-core = { version = "^0.5.0", path = "../ethers-core" } +ethers-core = { version = "^0.5.0", path = "../ethers-core", features = ["eip712"]} thiserror = { version = "1.0.29", default-features = false } coins-bip32 = "0.3.0" coins-bip39 = "0.3.0" diff --git a/ethers-signers/src/aws/mod.rs b/ethers-signers/src/aws/mod.rs index f57f0b11..d53a3c1b 100644 --- a/ethers-signers/src/aws/mod.rs +++ b/ethers-signers/src/aws/mod.rs @@ -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 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( + &self, + payload: T, + ) -> Result { + 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 } diff --git a/ethers-signers/src/ledger/app.rs b/ethers-signers/src/ledger/app.rs index dff7951e..ad6c44f4 100644 --- a/ethers-signers/src/ledger/app.rs +++ b/ethers-signers/src/ledger/app.rs @@ -137,7 +137,7 @@ impl LedgerEthereum { } // Helper function for signing either transaction data or personal messages - async fn sign_payload( + pub async fn sign_payload( &self, command: INS, mut payload: Vec, diff --git a/ethers-signers/src/ledger/mod.rs b/ethers-signers/src/ledger/mod.rs index 52e2cfe6..8c1d7324 100644 --- a/ethers-signers/src/ledger/mod.rs +++ b/ethers-signers/src/ledger/mod.rs @@ -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( + &self, + payload: T, + ) -> Result { + 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 diff --git a/ethers-signers/src/ledger/types.rs b/ethers-signers/src/ledger/types.rs index 6c7edf2b..eee344c2 100644 --- a/ethers-signers/src/ledger/types.rs +++ b/ethers-signers/src/ledger/types.rs @@ -41,6 +41,9 @@ pub enum LedgerError { #[error(transparent)] /// Error when converting from a hex string HexError(#[from] hex::FromHexError), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), } pub const P1_FIRST: u8 = 0x00; diff --git a/ethers-signers/src/lib.rs b/ethers-signers/src/lib.rs index 3af7a2e4..3436a239 100644 --- a/ethers-signers/src/lib.rs +++ b/ethers-signers/src/lib.rs @@ -69,7 +69,9 @@ mod aws; pub use aws::{AwsSigner, AwsSignerError}; use async_trait::async_trait; -use ethers_core::types::{transaction::eip2718::TypedTransaction, Address, Signature}; +use ethers_core::types::{ + transaction::eip2718::TypedTransaction, transaction::eip712::Eip712, Address, Signature, +}; use std::error::Error; /// Applies [EIP155](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md) @@ -93,6 +95,13 @@ pub trait Signer: std::fmt::Debug + Send + Sync { /// Signs the transaction async fn sign_transaction(&self, message: &TypedTransaction) -> Result; + /// Encodes and signs the typed data according EIP-712. + /// Payload must implement Eip712 trait. + async fn sign_typed_data( + &self, + payload: T, + ) -> Result; + /// Returns the signer's Ethereum Address fn address(&self) -> Address; diff --git a/ethers-signers/src/wallet/mod.rs b/ethers-signers/src/wallet/mod.rs index 1f5cb788..4331b389 100644 --- a/ethers-signers/src/wallet/mod.rs +++ b/ethers-signers/src/wallet/mod.rs @@ -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> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl> Signer for Wallet { - type Error = std::convert::Infallible; + type Error = WalletError; async fn sign_message>( &self, @@ -83,6 +86,17 @@ impl> Signer fo Ok(self.sign_transaction_sync(tx)) } + async fn sign_typed_data( + &self, + payload: T, + ) -> Result { + 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 } diff --git a/ethers-signers/src/wallet/private_key.rs b/ethers-signers/src/wallet/private_key.rs index ec2e8615..2314cc81 100644 --- a/ethers-signers/src/wallet/private_key.rs +++ b/ethers-signers/src/wallet/private_key.rs @@ -43,6 +43,9 @@ pub enum WalletError { /// Error propagated from the mnemonic builder module. #[error(transparent)] MnemonicBuilderError(#[from] MnemonicBuilderError), + /// Error type from Eip712Error message + #[error("error encoding eip712 struct: {0:?}")] + Eip712Error(String), } impl Clone for Wallet {