diff --git a/Cargo.lock b/Cargo.lock index d7e52881..e9472a95 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,7 +711,7 @@ checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -750,6 +750,18 @@ dependencies = [ "winapi", ] +[[package]] +name = "auto_impl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -876,13 +888,13 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", - "prettyplease 0.2.6", + "prettyplease 0.2.16", "proc-macro2", "quote", "regex", "rustc-hash", "shlex", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -1108,6 +1120,17 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", +] + [[package]] name = "bstr" version = "1.5.0" @@ -1216,6 +1239,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "case" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c" + [[package]] name = "cc" version = "1.0.79" @@ -1418,7 +1447,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2110,7 +2139,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2406,7 +2435,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2446,7 +2475,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2463,7 +2492,7 @@ checksum = "50c49547d73ba8dcfd4ad7325d64c6d5391ff4224d498fc39a6f3f49825a530d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2734,7 +2763,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -2781,7 +2810,7 @@ dependencies = [ "proc-macro2", "quote", "regex", - "syn 2.0.39", + "syn 2.0.48", "termcolor", "toml 0.7.6", "walkdir", @@ -2993,7 +3022,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3004,7 +3033,7 @@ checksum = "c2ad8cef1d801a4686bfd8919f0b30eac4c8e48968c437a6405ded4fb5272d2b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3053,12 +3082,119 @@ dependencies = [ "libc", ] +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04d24d20b8ff2235cffbf242d5092de3aa45f77c5270ddbfadd2778ca13fea" +dependencies = [ + "bytes", + "ethereum-types", + "hash-db", + "hash256-std-hasher", + "parity-scale-codec", + "rlp", + "scale-info", + "serde", + "sha3", + "trie-root", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-codec", + "impl-rlp", + "impl-serde", + "primitive-types", + "scale-info", + "uint", +] + [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "evm" +version = "0.41.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "767f43e9630cc36cf8ff2777cbb0121b055f0d1fd6eaaa13b46a1808f0d0e7e9" +dependencies = [ + "auto_impl", + "environmental", + "ethereum", + "evm-core", + "evm-gasometer", + "evm-runtime", + "log", + "parity-scale-codec", + "primitive-types", + "rlp", + "scale-info", + "serde", + "sha3", +] + +[[package]] +name = "evm-core" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da6cedc5cedb4208e59467106db0d1f50db01b920920589f8e672c02fdc04f" +dependencies = [ + "parity-scale-codec", + "primitive-types", + "scale-info", + "serde", +] + +[[package]] +name = "evm-gasometer" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dc0eb591abc5cd7b05bef6a036c2bb6c66ab6c5e0c5ce94bfe377ab670b1fd7" +dependencies = [ + "environmental", + "evm-core", + "evm-runtime", + "primitive-types", +] + +[[package]] +name = "evm-runtime" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84bbe09b64ae13a29514048c1bb6fda6374ac0b4f6a1f15a443348ab88ef42cd" +dependencies = [ + "auto_impl", + "environmental", + "evm-core", + "primitive-types", + "sha3", +] + [[package]] name = "exit-future" version = "0.2.0" @@ -3090,7 +3226,7 @@ dependencies = [ "fs-err", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3105,6 +3241,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "faster-hex" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e2ce894d53b295cf97b05685aa077950ff3e8541af83217fc720a6437169f8" + [[package]] name = "fastrand" version = "1.9.0" @@ -3299,6 +3441,41 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fp-account" +version = "1.0.0-dev" +source = "git+https://github.com/paritytech/frontier?branch=polkadot-v1.3.0#e262e80f3083ea8df90bd71a827789bef810f6aa" +dependencies = [ + "hex", + "impl-serde", + "libsecp256k1", + "log", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-runtime-interface", + "sp-std", +] + +[[package]] +name = "fp-evm" +version = "3.0.0-dev" +source = "git+https://github.com/paritytech/frontier?branch=polkadot-v1.3.0#e262e80f3083ea8df90bd71a827789bef810f6aa" +dependencies = [ + "evm", + "frame-support", + "num_enum", + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "fragile" version = "2.0.0" @@ -3386,7 +3563,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3513,7 +3690,7 @@ dependencies = [ "proc-macro2", "quote", "sp-core-hashing", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3525,7 +3702,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3535,7 +3712,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3745,7 +3922,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -3912,7 +4089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick 0.7.20", - "bstr", + "bstr 1.5.0", "fnv", "log", "regex", @@ -4324,6 +4501,15 @@ dependencies = [ "parity-scale-codec", ] +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + [[package]] name = "impl-serde" version = "0.4.0" @@ -5443,7 +5629,7 @@ dependencies = [ "macro_magic_core", "macro_magic_macros", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5457,7 +5643,7 @@ dependencies = [ "macro_magic_core_macros", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5468,7 +5654,7 @@ checksum = "9ea73aa640dc01d62a590d48c0c3521ed739d53b27f919b25c3551e233481654" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -5479,7 +5665,7 @@ checksum = "ef9d79ae96aaba821963320eb2b6e34d17df1e5a83d8a1985c29cc5be59577b3" dependencies = [ "macro_magic_core", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -6235,6 +6421,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -6781,6 +6988,147 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-evm" +version = "6.0.0-dev" +source = "git+https://github.com/paritytech/frontier?branch=polkadot-v1.3.0#e262e80f3083ea8df90bd71a827789bef810f6aa" +dependencies = [ + "environmental", + "evm", + "fp-account", + "fp-evm", + "frame-benchmarking", + "frame-support", + "frame-system", + "hash-db", + "hex", + "hex-literal 0.4.1", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "rlp", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-evm-precompile-balances-erc20" +version = "0.1.0" +dependencies = [ + "derive_more", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal 0.3.4", + "libsecp256k1", + "log", + "num_enum", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "paste", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "slices", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-evm-precompile-batch" +version = "0.1.0" +dependencies = [ + "derive_more", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal 0.3.4", + "log", + "num_enum", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "paste", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "slices", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-evm-precompile-call-permit" +version = "0.1.0" +dependencies = [ + "derive_more", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "hex-literal 0.3.4", + "libsecp256k1", + "log", + "num_enum", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "parity-scale-codec", + "paste", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "slices", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "pallet-evm-precompile-xcm-utils" +version = "0.1.0" +dependencies = [ + "cumulus-primitives-core", + "derive_more", + "fp-evm", + "frame-support", + "frame-system", + "num_enum", + "pallet-balances", + "pallet-evm", + "pallet-timestamp", + "pallet-xcm", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "serde", + "sha3", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-weights", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", + "xcm-primitives", +] + [[package]] name = "pallet-fast-unstake" version = "4.0.0-dev" @@ -7367,7 +7715,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -7759,9 +8107,9 @@ checksum = "7924d1d0ad836f665c9065e26d016c673ece3993f30d340068b16f282afc1156" [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "pbkdf2" @@ -7850,7 +8198,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -7891,7 +8239,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -9061,6 +9409,50 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "precompile-utils" +version = "0.1.0" +source = "git+https://github.com/paritytech/frontier?branch=polkadot-v1.3.0#e262e80f3083ea8df90bd71a827789bef810f6aa" +dependencies = [ + "derive_more", + "environmental", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "hex", + "hex-literal 0.4.1", + "impl-trait-for-tuples", + "log", + "num_enum", + "pallet-evm", + "parity-scale-codec", + "precompile-utils-macro", + "scale-info", + "serde", + "similar-asserts", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "sp-weights", + "staging-xcm", +] + +[[package]] +name = "precompile-utils-macro" +version = "0.1.0" +source = "git+https://github.com/paritytech/frontier?branch=polkadot-v1.3.0#e262e80f3083ea8df90bd71a827789bef810f6aa" +dependencies = [ + "case", + "num_enum", + "prettyplease 0.2.16", + "proc-macro2", + "quote", + "sp-core-hashing", + "syn 1.0.109", +] + [[package]] name = "predicates" version = "2.1.5" @@ -9115,12 +9507,12 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.6" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +checksum = "a41cf62165e97c7f814d2221421dbb9afcbcdb0a88068e5ea206e19951c2cbb5" dependencies = [ "proc-macro2", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -9131,6 +9523,7 @@ checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" dependencies = [ "fixed-hash", "impl-codec", + "impl-rlp", "impl-serde", "scale-info", "uint", @@ -9200,14 +9593,14 @@ checksum = "9b698b0b09d40e9b7c1a47b132d66a8b54bcd20583d9b6d06e4535e383b4405c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] name = "proc-macro2" -version = "1.0.69" +version = "1.0.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" +checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" dependencies = [ "unicode-ident", ] @@ -9371,9 +9764,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -9586,7 +9979,7 @@ checksum = "8d2275aab483050ab2a7364c1a46604865ee7d6906684e08db0f090acf74f9e7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -9695,6 +10088,28 @@ dependencies = [ "winapi", ] +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rlp-derive", + "rustc-hex", +] + +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rocksdb" version = "0.21.0" @@ -10195,7 +10610,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -11188,7 +11603,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -11500,7 +11915,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -11679,6 +12094,26 @@ dependencies = [ "wide", ] +[[package]] +name = "similar" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +dependencies = [ + "bstr 0.2.17", + "unicode-segmentation", +] + +[[package]] +name = "similar-asserts" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e041bb827d1bfca18f213411d51b665309f1afb37a04a5d1464530e13779fc0f" +dependencies = [ + "console", + "similar", +] + [[package]] name = "siphasher" version = "0.3.10" @@ -11700,6 +12135,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" +[[package]] +name = "slices" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2086e458a369cdca838e9f6ed04b4cc2e3ce636d99abb80c9e2eada107749cf" +dependencies = [ + "faster-hex", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "slot-range-helper" version = "1.0.0" @@ -11926,7 +12373,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -12165,7 +12612,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "quote", "sp-core-hashing", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -12184,7 +12631,7 @@ source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -12413,7 +12860,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -12606,7 +13053,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -13006,9 +13453,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.39" +version = "2.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23e78b90f2fcf45d3e842032ce32e3f2d1545ba6636271dcbf24fa306d87be7a" +checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" dependencies = [ "proc-macro2", "quote", @@ -13126,7 +13573,7 @@ checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -13295,7 +13742,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -13462,7 +13909,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -13506,7 +13953,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -13760,6 +14207,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "unicode-width" version = "0.1.10" @@ -13951,7 +14404,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-shared", ] @@ -13985,7 +14438,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -15018,7 +15471,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] @@ -15067,7 +15520,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.39", + "syn 2.0.48", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index b218b4f0..b92faadc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "client/consensus/nimbus-consensus", + "precompiles/*", "pallets/*", "primitives/*", "template/node", @@ -36,6 +37,11 @@ schnorrkel = { version = "0.9.1", features = ["preaudit_deprecated", "u64_backen serde = { version = "1.0.101", default-features = false } smallvec = "1.6.1" tracing = "0.1.22" +num_enum = { version = "0.7.2", default-features = false } +paste = "1.0.14" +slices = "0.2.0" +libsecp256k1 = { version = "0.7.1", default-features = false } +sha3 = { version = "0.10.8", default-features = false } # Crates.io (template only) clap = { version = "4.0.9" } @@ -96,6 +102,7 @@ sp-session = { git = "https://github.com/paritytech/polkadot-sdk", branch = "rel sp-std = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } sp-transaction-pool = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } sp-version = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } +sp-weights = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } substrate-wasm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0" } sp-blockchain = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } sp-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } @@ -176,6 +183,9 @@ polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot-sd staging-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } staging-xcm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } staging-xcm-executor = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } +xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } +xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } +xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0", default-features = false } # Polkadot (client) kusama-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0" } @@ -187,7 +197,19 @@ rococo-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = westend-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0" } xcm-simulator = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.3.0" } +# Frontier (wasm) +fp-evm = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v1.3.0", default-features = false } +pallet-evm = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v1.3.0", default-features = false } +precompile-utils = { git = "https://github.com/paritytech/frontier", branch = "polkadot-v1.3.0", default-features = false } + +# EVM +evm = { version = "0.41.1", default-features = false } + # Local (wasm) +pallet-evm-precompile-balances-erc20 = { path = "precompiles/balances-erc20", default-features = false } +pallet-evm-precompile-batch = { path = "precompiles/batch", default-features = false } +pallet-evm-precompile-call-permit = { path = "precompiles/call-permit", default-features = false } +pallet-evm-precompile-xcm-utils = { path = "precompiles/xcm-utils", default-features = false } async-backing-primitives = { path = "primitives/async-backing", default-features = false } pallet-author-inherent = { path = "pallets/author-inherent", default-features = false } pallet-author-mapping = { path = "pallets/author-mapping", default-features = false } diff --git a/precompiles/balances-erc20/Cargo.toml b/precompiles/balances-erc20/Cargo.toml new file mode 100644 index 00000000..8d622e0d --- /dev/null +++ b/precompiles/balances-erc20/Cargo.toml @@ -0,0 +1,59 @@ +[package] +name = "pallet-evm-precompile-balances-erc20" +authors = { workspace = true } +description = "A Precompile to expose a Balances pallet through an ERC20-compliant interface." +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +paste = { workspace = true } +slices = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +libsecp256k1 = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = [ "std", "testing" ] } + +pallet-timestamp = { workspace = true, features = [ "std" ] } +scale-info = { workspace = true, features = [ "derive" ] } +sp-runtime = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-evm/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", +] diff --git a/precompiles/balances-erc20/ERC20.sol b/precompiles/balances-erc20/ERC20.sol new file mode 100644 index 00000000..1f5e3f5a --- /dev/null +++ b/precompiles/balances-erc20/ERC20.sol @@ -0,0 +1,116 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @title ERC20 interface +/// @dev see https://github.com/ethereum/EIPs/issues/20 +/// @dev copied from https://github.com/OpenZeppelin/openzeppelin-contracts +interface IERC20 { + /// @dev Returns the name of the token. + /// @custom:selector 06fdde03 + function name() external view returns (string memory); + + /// @dev Returns the symbol of the token. + /// @custom:selector 95d89b41 + function symbol() external view returns (string memory); + + /// @dev Returns the decimals places of the token. + /// @custom:selector 313ce567 + function decimals() external view returns (uint8); + + /// @dev Total number of tokens in existence + /// @custom:selector 18160ddd + function totalSupply() external view returns (uint256); + + /// @dev Gets the balance of the specified address. + /// @custom:selector 70a08231 + /// @param owner The address to query the balance of. + /// @return An uint256 representing the amount owned by the passed address. + function balanceOf(address owner) external view returns (uint256); + + /// @dev Function to check the amount of tokens that an owner allowed to a spender. + /// @custom:selector dd62ed3e + /// @param owner address The address which owns the funds. + /// @param spender address The address which will spend the funds. + /// @return A uint256 specifying the amount of tokens still available for the spender. + function allowance(address owner, address spender) + external + view + returns (uint256); + + /// @dev Transfer token for a specified address + /// @custom:selector a9059cbb + /// @param to The address to transfer to. + /// @param value The amount to be transferred. + /// @return true if the transfer was succesful, revert otherwise. + function transfer(address to, uint256 value) external returns (bool); + + /// @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender. + /// Beware that changing an allowance with this method brings the risk that someone may use both the old + /// and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this + /// race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards: + /// https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 + /// @custom:selector 095ea7b3 + /// @param spender The address which will spend the funds. + /// @param value The amount of tokens to be spent. + /// @return true, this cannot fail + function approve(address spender, uint256 value) external returns (bool); + + /// @dev Transfer tokens from one address to another + /// @custom:selector 23b872dd + /// @param from address The address which you want to send tokens from + /// @param to address The address which you want to transfer to + /// @param value uint256 the amount of tokens to be transferred + /// @return true if the transfer was succesful, revert otherwise. + function transferFrom( + address from, + address to, + uint256 value + ) external returns (bool); + + /// @dev Event emited when a transfer has been performed. + /// @custom:selector ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef + /// @param from address The address sending the tokens + /// @param to address The address receiving the tokens. + /// @param value uint256 The amount of tokens transfered. + event Transfer(address indexed from, address indexed to, uint256 value); + + /// @dev Event emited when an approval has been registered. + /// @custom:selector 8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925 + /// @param owner address Owner of the tokens. + /// @param spender address Allowed spender. + /// @param value uint256 Amount of tokens approved. + event Approval( + address indexed owner, + address indexed spender, + uint256 value + ); +} + +/// @title Native currency wrapper interface. +/// @dev Allow compatibility with dApps expecting this precompile to be +/// a WETH-like contract. +/// Moonbase address : 0x0000000000000000000000000000000000000802 +interface WrappedNativeCurrency { + /// @dev Provide compatibility for contracts that expect wETH design. + /// Returns funds to sender as this precompile tokens and the native tokens are the same. + /// @custom:selector d0e30db0 + function deposit() external payable; + + /// @dev Provide compatibility for contracts that expect wETH design. + /// Does nothing. + /// @custom:selector 2e1a7d4d + /// @param value uint256 The amount to withdraw/unwrap. + function withdraw(uint256 value) external; + + /// @dev Event emited when deposit() has been called. + /// @custom:selector e1fffcc4923d04b559f4d29a8bfc6cda04eb5b0d3c460751c2402c5c5cc9109c + /// @param owner address Owner of the tokens + /// @param value uint256 The amount of tokens "wrapped". + event Deposit(address indexed owner, uint256 value); + + /// @dev Event emited when withdraw(uint256) has been called. + /// @custom:selector 7fcf532c15f0a6db0bd6d0e038bea71d30d808c7d98cb3bf7268a95bf5081b65 + /// @param owner address Owner of the tokens + /// @param value uint256 The amount of tokens "unwrapped". + event Withdrawal(address indexed owner, uint256 value); +} diff --git a/precompiles/balances-erc20/Permit.sol b/precompiles/balances-erc20/Permit.sol new file mode 100644 index 00000000..fe781547 --- /dev/null +++ b/precompiles/balances-erc20/Permit.sol @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Extension of the ERC20 interface that allows users to +/// @dev Sign permit messages to interact with contracts without needing to +/// make a first approve transaction. +interface Permit { + /// @dev Consumes an approval permit. + /// Anyone can call this function for a permit. + /// @custom:selector d505accf + /// @param owner Owner of the tokens issuing the permit + /// @param spender Address whose allowance will be increased. + /// @param value Allowed value. + /// @param deadline Timestamp after which the permit will no longer be valid. + /// @param v V component of the signature. + /// @param r R component of the signature. + /// @param s S component of the signature. + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /// @dev Returns the current nonce for given owner. + /// A permit must have this nonce to be consumed, which will + /// increase the nonce by one. + /// @custom:selector 7ecebe00 + function nonces(address owner) external view returns (uint256); + + /// @dev Returns the EIP712 domain separator. It is used to avoid replay + /// attacks accross assets or other similar EIP712 message structures. + /// @custom:selector 3644e515 + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/precompiles/balances-erc20/src/eip2612.rs b/precompiles/balances-erc20/src/eip2612.rs new file mode 100644 index 00000000..ccc7ec45 --- /dev/null +++ b/precompiles/balances-erc20/src/eip2612.rs @@ -0,0 +1,181 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use super::*; +use frame_support::{ + ensure, + traits::{Get, Time}, +}; +use sp_core::H256; +use sp_io::hashing::keccak_256; +use sp_runtime::traits::UniqueSaturatedInto; +use sp_std::vec::Vec; + +/// EIP2612 permit typehash. +pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!( + "Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)" +); + +/// EIP2612 permit domain used to compute an individualized domain separator. +const PERMIT_DOMAIN: [u8; 32] = keccak256!( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +); + +pub struct Eip2612(PhantomData<(Runtime, Metadata, Instance)>); + +impl Eip2612 +where + Runtime: pallet_balances::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into, + Metadata: Erc20Metadata, + Instance: InstanceToPrefix + 'static, +{ + pub fn compute_domain_separator(address: H160) -> [u8; 32] { + let name: H256 = keccak_256(Metadata::name().as_bytes()).into(); + let version: H256 = keccak256!("1").into(); + let chain_id: U256 = Runtime::ChainId::get().into(); + + let domain_separator_inner = solidity::encode_arguments(( + H256::from(PERMIT_DOMAIN), + name, + version, + chain_id, + Address(address), + )); + + keccak_256(&domain_separator_inner).into() + } + + pub fn generate_permit( + address: H160, + owner: H160, + spender: H160, + value: U256, + nonce: U256, + deadline: U256, + ) -> [u8; 32] { + let domain_separator = Self::compute_domain_separator(address); + + let permit_content = solidity::encode_arguments(( + H256::from(PERMIT_TYPEHASH), + Address(owner), + Address(spender), + value, + nonce, + deadline, + )); + let permit_content = keccak_256(&permit_content); + + let mut pre_digest = Vec::with_capacity(2 + 32 + 32); + pre_digest.extend_from_slice(b"\x19\x01"); + pre_digest.extend_from_slice(&domain_separator); + pre_digest.extend_from_slice(&permit_content); + keccak_256(&pre_digest) + } + + // Translated from + // https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2ERC20.sol#L81 + pub(crate) fn permit( + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + // Blockchain time is in ms while Ethereum use second timestamps. + let timestamp: u128 = + ::Timestamp::now().unique_saturated_into(); + let timestamp: U256 = U256::from(timestamp / 1000); + + ensure!(deadline >= timestamp, revert("Permit expired")); + + let nonce = NoncesStorage::::get(owner); + + let permit = Self::generate_permit( + handle.context().address, + owner, + spender, + value, + nonce, + deadline, + ); + + let mut sig = [0u8; 65]; + sig[0..32].copy_from_slice(&r.as_bytes()); + sig[32..64].copy_from_slice(&s.as_bytes()); + sig[64] = v; + + let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit) + .map_err(|_| revert("Invalid permit"))?; + let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice())); + + ensure!( + signer != H160::zero() && signer == owner, + revert("Invalid permit") + ); + + NoncesStorage::::insert(owner, nonce + U256::one()); + + { + let amount = + Erc20BalancesPrecompile::::u256_to_amount(value) + .unwrap_or_else(|_| Bounded::max_value()); + + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + ApprovesStorage::::insert(owner, spender, amount); + } + + log3( + handle.context().address, + SELECTOR_LOG_APPROVAL, + owner, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(()) + } + + pub(crate) fn nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + + Ok(NoncesStorage::::get(owner)) + } + + pub(crate) fn domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult { + // ChainId + handle.record_db_read::(8)?; + + Ok(Self::compute_domain_separator(handle.context().address).into()) + } +} diff --git a/precompiles/balances-erc20/src/lib.rs b/precompiles/balances-erc20/src/lib.rs new file mode 100644 index 00000000..30e92f42 --- /dev/null +++ b/precompiles/balances-erc20/src/lib.rs @@ -0,0 +1,492 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Precompile to interact with pallet_balances instances using the ERC20 interface standard. + +#![cfg_attr(not(feature = "std"), no_std)] + +use fp_evm::PrecompileHandle; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + sp_runtime::traits::{Bounded, CheckedSub, Dispatchable, StaticLookup}, + storage::types::{StorageDoubleMap, StorageMap, ValueQuery}, + traits::StorageInstance, + Blake2_128Concat, +}; +use pallet_balances::pallet::{ + Instance1, Instance10, Instance11, Instance12, Instance13, Instance14, Instance15, Instance16, + Instance2, Instance3, Instance4, Instance5, Instance6, Instance7, Instance8, Instance9, +}; +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; +use sp_core::{H160, H256, U256}; +use sp_std::{ + convert::{TryFrom, TryInto}, + marker::PhantomData, +}; + +mod eip2612; +use eip2612::Eip2612; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +/// Solidity selector of the Transfer log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)"); + +/// Solidity selector of the Approval log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)"); + +/// Solidity selector of the Deposit log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_DEPOSIT: [u8; 32] = keccak256!("Deposit(address,uint256)"); + +/// Solidity selector of the Withdraw log, which is the Keccak of the Log signature. +pub const SELECTOR_LOG_WITHDRAWAL: [u8; 32] = keccak256!("Withdrawal(address,uint256)"); + +/// Associates pallet Instance to a prefix used for the Approves storage. +/// This trait is implemented for () and the 16 substrate Instance. +pub trait InstanceToPrefix { + /// Prefix used for the Approves storage. + type ApprovesPrefix: StorageInstance; + + /// Prefix used for the Approves storage. + type NoncesPrefix: StorageInstance; +} + +// We use a macro to implement the trait for () and the 16 substrate Instance. +macro_rules! impl_prefix { + ($instance:ident, $name:literal) => { + // Using `paste!` we generate a dedicated module to avoid collisions + // between each instance `Approves` struct. + paste::paste! { + mod [<_impl_prefix_ $instance:snake>] { + use super::*; + + pub struct Approves; + + impl StorageInstance for Approves { + const STORAGE_PREFIX: &'static str = "Approves"; + + fn pallet_prefix() -> &'static str { + $name + } + } + + pub struct Nonces; + + impl StorageInstance for Nonces { + const STORAGE_PREFIX: &'static str = "Nonces"; + + fn pallet_prefix() -> &'static str { + $name + } + } + + impl InstanceToPrefix for $instance { + type ApprovesPrefix = Approves; + type NoncesPrefix = Nonces; + } + } + } + }; +} + +// Since the macro expect a `ident` to be used with `paste!` we cannot provide `()` directly. +type Instance0 = (); + +impl_prefix!(Instance0, "Erc20Instance0Balances"); +impl_prefix!(Instance1, "Erc20Instance1Balances"); +impl_prefix!(Instance2, "Erc20Instance2Balances"); +impl_prefix!(Instance3, "Erc20Instance3Balances"); +impl_prefix!(Instance4, "Erc20Instance4Balances"); +impl_prefix!(Instance5, "Erc20Instance5Balances"); +impl_prefix!(Instance6, "Erc20Instance6Balances"); +impl_prefix!(Instance7, "Erc20Instance7Balances"); +impl_prefix!(Instance8, "Erc20Instance8Balances"); +impl_prefix!(Instance9, "Erc20Instance9Balances"); +impl_prefix!(Instance10, "Erc20Instance10Balances"); +impl_prefix!(Instance11, "Erc20Instance11Balances"); +impl_prefix!(Instance12, "Erc20Instance12Balances"); +impl_prefix!(Instance13, "Erc20Instance13Balances"); +impl_prefix!(Instance14, "Erc20Instance14Balances"); +impl_prefix!(Instance15, "Erc20Instance15Balances"); +impl_prefix!(Instance16, "Erc20Instance16Balances"); + +/// Alias for the Balance type for the provided Runtime and Instance. +pub type BalanceOf = + >::Balance; + +/// Storage type used to store approvals, since `pallet_balances` doesn't +/// handle this behavior. +/// (Owner => Allowed => Amount) +pub type ApprovesStorage = StorageDoubleMap< + ::ApprovesPrefix, + Blake2_128Concat, + ::AccountId, + Blake2_128Concat, + ::AccountId, + BalanceOf, +>; + +/// Storage type used to store EIP2612 nonces. +pub type NoncesStorage = StorageMap< + ::NoncesPrefix, + // Owner + Blake2_128Concat, + H160, + // Nonce + U256, + ValueQuery, +>; + +/// Metadata of an ERC20 token. +pub trait Erc20Metadata { + /// Returns the name of the token. + fn name() -> &'static str; + + /// Returns the symbol of the token. + fn symbol() -> &'static str; + + /// Returns the decimals places of the token. + fn decimals() -> u8; + + /// Must return `true` only if it represents the main native currency of + /// the network. It must be the currency used in `pallet_evm`. + fn is_native_currency() -> bool; +} + +/// Precompile exposing a pallet_balance as an ERC20. +/// Multiple precompiles can support instances of pallet_balance. +/// The precompile uses an additional storage to store approvals. +pub struct Erc20BalancesPrecompile( + PhantomData<(Runtime, Metadata, Instance)>, +); + +#[precompile_utils::precompile] +impl Erc20BalancesPrecompile +where + Runtime: pallet_balances::Config + pallet_evm::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into, + Metadata: Erc20Metadata, + Instance: InstanceToPrefix + 'static, +{ + #[precompile::public("totalSupply()")] + #[precompile::view] + fn total_supply(handle: &mut impl PrecompileHandle) -> EvmResult { + // TotalIssuance: Balance(16) + handle.record_db_read::(16)?; + + Ok(pallet_balances::Pallet::::total_issuance().into()) + } + + #[precompile::public("balanceOf(address)")] + #[precompile::view] + fn balance_of(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + // frame_system::Account: + // Blake2128(16) + AccountId(20) + AccountInfo ((4 * 4) + AccountData(16 * 4)) + handle.record_db_read::(116)?; + + let owner: H160 = owner.into(); + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + + Ok(pallet_balances::Pallet::::usable_balance(&owner).into()) + } + + #[precompile::public("allowance(address,address)")] + #[precompile::view] + fn allowance( + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + ) -> EvmResult { + // frame_system::ApprovesStorage: + // (2 * (Blake2128(16) + AccountId(20)) + Balanceof(16) + handle.record_db_read::(88)?; + + let owner: H160 = owner.into(); + let spender: H160 = spender.into(); + + let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + + Ok(ApprovesStorage::::get(owner, spender) + .unwrap_or_default() + .into()) + } + + #[precompile::public("approve(address,uint256)")] + fn approve( + handle: &mut impl PrecompileHandle, + spender: Address, + value: U256, + ) -> EvmResult { + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_log_costs_manual(3, 32)?; + + let spender: H160 = spender.into(); + + // Write into storage. + { + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender); + // Amount saturate if too high. + let value = Self::u256_to_amount(value).unwrap_or_else(|_| Bounded::max_value()); + + ApprovesStorage::::insert(caller, spender, value); + } + + log3( + handle.context().address, + SELECTOR_LOG_APPROVAL, + handle.context().caller, + spender, + solidity::encode_event_data(value), + ) + .record(handle)?; + + // Build output. + Ok(true) + } + + #[precompile::public("transfer(address,uint256)")] + fn transfer(handle: &mut impl PrecompileHandle, to: Address, value: U256) -> EvmResult { + handle.record_log_costs_manual(3, 32)?; + + let to: H160 = to.into(); + + // Build call with origin. + { + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let to = Runtime::AddressMapping::into_account_id(to); + let value = Self::u256_to_amount(value).in_field("value")?; + + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(origin).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(to), + value: value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + handle.context().caller, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + + #[precompile::public("transferFrom(address,address,uint256)")] + fn transfer_from( + handle: &mut impl PrecompileHandle, + from: Address, + to: Address, + value: U256, + ) -> EvmResult { + // frame_system::ApprovesStorage: + // (2 * (Blake2128(16) + AccountId(20)) + Balanceof(16) + handle.record_db_read::(88)?; + handle.record_cost(RuntimeHelper::::db_write_gas_cost())?; + handle.record_log_costs_manual(3, 32)?; + + let from: H160 = from.into(); + let to: H160 = to.into(); + + { + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from); + let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to); + let value = Self::u256_to_amount(value).in_field("value")?; + + // If caller is "from", it can spend as much as it wants. + if caller != from { + ApprovesStorage::::mutate(from.clone(), caller, |entry| { + // Get current allowed value, exit if None. + let allowed = entry.ok_or(revert("spender not allowed"))?; + + // Remove "value" from allowed, exit if underflow. + let allowed = allowed + .checked_sub(&value) + .ok_or_else(|| revert("trying to spend more than allowed"))?; + + // Update allowed value. + *entry = Some(allowed); + + EvmResult::Ok(()) + })?; + } + + // Build call with origin. Here origin is the "from"/owner field. + // Dispatch call (if enough gas). + RuntimeHelper::::try_dispatch( + handle, + Some(from).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(to), + value: value, + }, + )?; + } + + log3( + handle.context().address, + SELECTOR_LOG_TRANSFER, + from, + to, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(true) + } + + #[precompile::public("name()")] + #[precompile::view] + fn name(_handle: &mut impl PrecompileHandle) -> EvmResult { + Ok(Metadata::name().into()) + } + + #[precompile::public("symbol()")] + #[precompile::view] + fn symbol(_handle: &mut impl PrecompileHandle) -> EvmResult { + Ok(Metadata::symbol().into()) + } + + #[precompile::public("decimals()")] + #[precompile::view] + fn decimals(_handle: &mut impl PrecompileHandle) -> EvmResult { + Ok(Metadata::decimals()) + } + + #[precompile::public("deposit()")] + #[precompile::fallback] + #[precompile::payable] + fn deposit(handle: &mut impl PrecompileHandle) -> EvmResult { + // Deposit only makes sense for the native currency. + if !Metadata::is_native_currency() { + return Err(RevertReason::UnknownSelector.into()); + } + + let caller: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + let precompile = Runtime::AddressMapping::into_account_id(handle.context().address); + let amount = Self::u256_to_amount(handle.context().apparent_value)?; + + if amount.into() == U256::from(0u32) { + return Err(revert("deposited amount must be non-zero")); + } + + handle.record_log_costs_manual(2, 32)?; + + // Send back funds received by the precompile. + RuntimeHelper::::try_dispatch( + handle, + Some(precompile).into(), + pallet_balances::Call::::transfer_allow_death { + dest: Runtime::Lookup::unlookup(caller), + value: amount, + }, + )?; + + log2( + handle.context().address, + SELECTOR_LOG_DEPOSIT, + handle.context().caller, + solidity::encode_event_data(handle.context().apparent_value), + ) + .record(handle)?; + + Ok(()) + } + + #[precompile::public("withdraw(uint256)")] + fn withdraw(handle: &mut impl PrecompileHandle, value: U256) -> EvmResult { + // Withdraw only makes sense for the native currency. + if !Metadata::is_native_currency() { + return Err(RevertReason::UnknownSelector.into()); + } + + handle.record_log_costs_manual(2, 32)?; + + let account_amount: U256 = { + let owner: Runtime::AccountId = + Runtime::AddressMapping::into_account_id(handle.context().caller); + pallet_balances::Pallet::::usable_balance(&owner).into() + }; + + if value > account_amount { + return Err(revert("Trying to withdraw more than owned")); + } + + log2( + handle.context().address, + SELECTOR_LOG_WITHDRAWAL, + handle.context().caller, + solidity::encode_event_data(value), + ) + .record(handle)?; + + Ok(()) + } + + #[precompile::public("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")] + fn eip2612_permit( + handle: &mut impl PrecompileHandle, + owner: Address, + spender: Address, + value: U256, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + >::permit( + handle, owner, spender, value, deadline, v, r, s, + ) + } + + #[precompile::public("nonces(address)")] + #[precompile::view] + fn eip2612_nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + >::nonces(handle, owner) + } + + #[precompile::public("DOMAIN_SEPARATOR()")] + #[precompile::view] + fn eip2612_domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult { + >::domain_separator(handle) + } + + fn u256_to_amount(value: U256) -> MayRevert> { + value + .try_into() + .map_err(|_| RevertReason::value_is_too_large("balance type").into()) + } +} diff --git a/precompiles/balances-erc20/src/mock.rs b/precompiles/balances-erc20/src/mock.rs new file mode 100644 index 00000000..fe9c2c1c --- /dev/null +++ b/precompiles/balances-erc20/src/mock.rs @@ -0,0 +1,215 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Testing utilities. + +use super::*; + +use frame_support::{construct_runtime, parameter_types, traits::Everything, weights::Weight}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::{precompile_set::*, testing::MockAccount}; +use sp_core::{ConstU32, H256, U256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +pub type AccountId = MockAccount; +pub type Balance = u128; +pub type Block = frame_system::mocking::MockBlockU32; + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} + +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} + +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +pub type Precompiles = PrecompileSetBuilder< + R, + (PrecompileAt, Erc20BalancesPrecompile>,), +>; + +pub type PCall = Erc20BalancesPrecompileCall; + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + } +); + +/// ERC20 metadata for the native token. +pub struct NativeErc20Metadata; + +impl Erc20Metadata for NativeErc20Metadata { + /// Returns the name of the token. + fn name() -> &'static str { + "Mock token" + } + + /// Returns the symbol of the token. + fn symbol() -> &'static str { + "MOCK" + } + + /// Returns the decimals places of the token. + fn decimals() -> u8 { + 18 + } + + /// Must return `true` only if it represents the main native currency of + /// the network. It must be the currency used in `pallet_evm`. + fn is_native_currency() -> bool { + true + } +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} diff --git a/precompiles/balances-erc20/src/tests.rs b/precompiles/balances-erc20/src/tests.rs new file mode 100644 index 00000000..2d3b04e5 --- /dev/null +++ b/precompiles/balances-erc20/src/tests.rs @@ -0,0 +1,1313 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use std::str::from_utf8; + +use crate::{eip2612::Eip2612, mock::*, *}; + +use libsecp256k1::{sign, Message, SecretKey}; +use precompile_utils::testing::*; +use sha3::{Digest, Keccak256}; +use sp_core::{H256, U256}; + +// No test of invalid selectors since we have a fallback behavior (deposit). +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn selectors() { + assert!(PCall::balance_of_selectors().contains(&0x70a08231)); + assert!(PCall::total_supply_selectors().contains(&0x18160ddd)); + assert!(PCall::approve_selectors().contains(&0x095ea7b3)); + assert!(PCall::allowance_selectors().contains(&0xdd62ed3e)); + assert!(PCall::transfer_selectors().contains(&0xa9059cbb)); + assert!(PCall::transfer_from_selectors().contains(&0x23b872dd)); + assert!(PCall::name_selectors().contains(&0x06fdde03)); + assert!(PCall::symbol_selectors().contains(&0x95d89b41)); + assert!(PCall::deposit_selectors().contains(&0xd0e30db0)); + assert!(PCall::withdraw_selectors().contains(&0x2e1a7d4d)); + assert!(PCall::eip2612_nonces_selectors().contains(&0x7ecebe00)); + assert!(PCall::eip2612_permit_selectors().contains(&0xd505accf)); + assert!(PCall::eip2612_domain_separator_selectors().contains(&0x3644e515)); + + assert_eq!( + crate::SELECTOR_LOG_TRANSFER, + &Keccak256::digest(b"Transfer(address,address,uint256)")[..] + ); + + assert_eq!( + crate::SELECTOR_LOG_APPROVAL, + &Keccak256::digest(b"Approval(address,address,uint256)")[..] + ); + + assert_eq!( + crate::SELECTOR_LOG_DEPOSIT, + &Keccak256::digest(b"Deposit(address,uint256)")[..] + ); + + assert_eq!( + crate::SELECTOR_LOG_WITHDRAWAL, + &Keccak256::digest(b"Withdrawal(address,uint256)")[..] + ); +} + +#[test] +fn modifiers() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let mut tester = + PrecompilesModifierTester::new(precompiles(), CryptoAlith, Precompile1); + + tester.test_view_modifier(PCall::balance_of_selectors()); + tester.test_view_modifier(PCall::total_supply_selectors()); + tester.test_default_modifier(PCall::approve_selectors()); + tester.test_view_modifier(PCall::allowance_selectors()); + tester.test_default_modifier(PCall::transfer_selectors()); + tester.test_default_modifier(PCall::transfer_from_selectors()); + tester.test_view_modifier(PCall::name_selectors()); + tester.test_view_modifier(PCall::symbol_selectors()); + tester.test_view_modifier(PCall::decimals_selectors()); + tester.test_payable_modifier(PCall::deposit_selectors()); + tester.test_default_modifier(PCall::withdraw_selectors()); + tester.test_view_modifier(PCall::eip2612_nonces_selectors()); + tester.test_default_modifier(PCall::eip2612_permit_selectors()); + tester.test_view_modifier(PCall::eip2612_domain_separator_selectors()); + }); +} + +#[test] +fn get_total_supply() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::total_supply {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(3500u64)); + }); +} + +#[test] +fn get_balances_known_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000u64)); + }); +} + +#[test] +fn get_balances_unknown_user() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn approve() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .expect_cost(1756) + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(true); + }); +} + +#[test] +fn approve_saturating() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { + spender: Address(Bob.into()), + value: U256::MAX, + }, + ) + .expect_cost(1756u64) + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::MAX), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) + .expect_no_logs() + .execute_returns(U256::from(u128::MAX)); + }); +} + +#[test] +fn check_allowance_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u64)); + }); +} + +#[test] +fn check_allowance_not_existing() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u64)); + }); +} + +#[test] +fn transfer() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::transfer { + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(184118756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn transfer_not_enough_funds() { + ExtBuilder::default() + .with_balances(vec![ + (CryptoAlith.into(), 1000), + (CryptoBaltathar.into(), 1000), + ]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::transfer { + to: Address(Bob.into()), + value: 1400.into(), + }, + ) + .execute_reverts(|output| { + from_utf8(&output) + .unwrap() + .contains("Dispatched call failed with error: ") + && from_utf8(&output).unwrap().contains("FundsUnavailable") + }); + }); +} + +#[test] +fn transfer_from() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { + spender: Address(Bob.into()), + value: 500.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, + Precompile1, + PCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(184118756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(100u64)); + }); +} + +#[test] +fn transfer_from_above_allowance() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::approve { + spender: Address(Bob.into()), + value: 300.into(), + }, + ) + .execute_some(); + + precompiles() + .prepare_test( + Bob, // Bob is the one sending transferFrom! + Precompile1, + PCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .execute_reverts(|output| output == b"trying to spend more than allowed"); + }); +} + +#[test] +fn transfer_from_self() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test( + CryptoAlith, // CryptoAlith sending transferFrom herself, no need for allowance. + Precompile1, + PCall::transfer_from { + from: Address(CryptoAlith.into()), + to: Address(Bob.into()), + value: 400.into(), + }, + ) + .expect_cost(184118756) // 1 weight => 1 gas in mock + .expect_log(log3( + Precompile1, + SELECTOR_LOG_TRANSFER, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(400)), + )) + .execute_returns(true); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(600)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(400)); + }); +} + +#[test] +fn get_metadata_name() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::name {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("Mock token")); + }); +} + +#[test] +fn get_metadata_symbol() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::symbol {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(UnboundedBytes::from("MOCK")); + }); +} + +#[test] +fn get_metadata_decimals() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000), (Bob.into(), 2500)]) + .build() + .execute_with(|| { + precompiles() + .prepare_test(CryptoAlith, Precompile1, PCall::decimals {}) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(18u8); + }); +} + +fn deposit(data: Vec) { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Precompile1.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Deposit + // We need to call using EVM pallet so we can check the EVM correctly sends the amount + // to the precompile. + Evm::call( + RuntimeOrigin::root(), + CryptoAlith.into(), + Precompile1.into(), + data, + From::from(500), // amount sent + u64::MAX, // gas limit + 0u32.into(), // gas price + None, // max priority + None, // nonce + vec![], // access list + ) + .expect("it works"); + + assert_eq!( + events(), + vec![ + RuntimeEvent::System(frame_system::Event::NewAccount { + account: Precompile1.into() + }), + RuntimeEvent::Balances(pallet_balances::Event::Endowed { + account: Precompile1.into(), + free_balance: 500 + }), + // EVM make a transfer because some value is provided. + RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: CryptoAlith.into(), + to: Precompile1.into(), + amount: 500 + }), + // Precompile1 send it back since deposit should be a no-op. + RuntimeEvent::Balances(pallet_balances::Event::Transfer { + from: Precompile1.into(), + to: CryptoAlith.into(), + amount: 500 + }), + // Log is correctly emitted. + RuntimeEvent::Evm(pallet_evm::Event::Log { + log: log2( + Precompile1, + SELECTOR_LOG_DEPOSIT, + CryptoAlith, + solidity::encode_event_data(U256::from(500)), + ) + }), + RuntimeEvent::Evm(pallet_evm::Event::Executed { + address: Precompile1.into() + }), + ] + ); + + // Check precompile balance is still 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Precompile1.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn deposit_function() { + deposit(PCall::deposit {}.into()) +} + +#[test] +fn deposit_fallback() { + deposit(solidity::encode_with_selector(0x01234567u32, ())) +} + +#[test] +fn deposit_receive() { + deposit(vec![]) +} + +#[test] +fn deposit_zero() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Precompile1.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Deposit + // We need to call using EVM pallet so we can check the EVM correctly sends the amount + // to the precompile. + Evm::call( + RuntimeOrigin::root(), + CryptoAlith.into(), + Precompile1.into(), + PCall::deposit {}.into(), + From::from(0), // amount sent + u64::MAX, // gas limit + 0u32.into(), // gas price + None, // max priority + None, // nonce + vec![], // access list + ) + .expect("it works"); + + assert_eq!( + events(), + vec![RuntimeEvent::Evm(pallet_evm::Event::ExecutedFailed { + address: Precompile1.into() + }),] + ); + + // Check precompile balance is still 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Precompile1.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn withdraw() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Precompile1.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Withdraw + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::withdraw { value: 500.into() }, + ) + .expect_cost(1381) + .expect_log(log2( + Precompile1, + SELECTOR_LOG_WITHDRAWAL, + CryptoAlith, + solidity::encode_event_data(U256::from(500)), + )) + .execute_returns(()); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn withdraw_more_than_owned() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + // Check precompile balance is 0. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(Precompile1.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0)); + + // Withdraw + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::withdraw { value: 1001.into() }, + ) + .execute_reverts(|output| output == b"Trying to withdraw more than owned"); + + // Check CryptoAlith balance is still 1000. + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::balance_of { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1000)); + }); +} + +#[test] +fn permit_valid() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); // todo: proper timestamp + + let permit = Eip2612::::generate_permit( + Precompile1.into(), + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: rs.r.b32().into(), + s: rs.s.b32().into(), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(value)), + )) + .execute_returns(()); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(500u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(1u8)); + }); +} + +#[test] +fn permit_invalid_nonce() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + let permit = Eip2612::::generate_permit( + Precompile1.into(), + owner, + spender, + value, + 1u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: rs.r.b32().into(), + s: rs.s.b32().into(), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_signature() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 0u8.into(); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: 0, + r: H256::repeat_byte(0x11), + s: H256::repeat_byte(0x11), + }, + ) + .execute_reverts(|output| output == b"Invalid permit"); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +#[test] +fn permit_invalid_deadline() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + pallet_timestamp::Pallet::::set_timestamp(10_000); + + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 500u16.into(); + let deadline: U256 = 5u8.into(); // deadline < timestamp => expired + + let permit = Eip2612::::generate_permit( + Precompile1.into(), + owner, + spender, + value, + 0u8.into(), // nonce + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v.serialize(), + r: rs.r.b32().into(), + s: rs.s.b32().into(), + }, + ) + .execute_reverts(|output| output == b"Permit expired"); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::allowance { + owner: Address(CryptoAlith.into()), + spender: Address(Bob.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u16)); + + precompiles() + .prepare_test( + CryptoAlith, + Precompile1, + PCall::eip2612_nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + }); +} + +// This test checks the validity of a metamask signed message against the permit precompile +// The code used to generate the signature is the following. +// You will need to import ALICE_PRIV_KEY in metamask. +// If you put this code in the developer tools console, it will log the signature +/* +await window.ethereum.enable(); +const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); + +const value = 1000; + +const fromAddress = "0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac"; +const deadline = 1; +const nonce = 0; +const spender = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const from = accounts[0]; + +const createPermitMessageData = function () { + const message = { + owner: from, + spender: spender, + value: value, + nonce: nonce, + deadline: deadline, + }; + + const typedData = JSON.stringify({ + types: { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name: "Mock token", + version: "1", + chainId: 0, + verifyingContract: "0x0000000000000000000000000000000000000001", + }, + message: message, + }); + + return { + typedData, + message, + }; +}; + +const method = "eth_signTypedData_v4" +const messageData = createPermitMessageData(); +const params = [from, messageData.typedData]; + +web3.currentProvider.sendAsync( + { + method, + params, + from, + }, + function (err, result) { + if (err) return console.dir(err); + if (result.error) { + alert(result.error.message); + } + if (result.error) return console.error('ERROR', result); + console.log('TYPED SIGNED:' + JSON.stringify(result.result)); + + const recovered = sigUtil.recoverTypedSignature_v4({ + data: JSON.parse(msgParams), + sig: result.result, + }); + + if ( + ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from) + ) { + alert('Successfully recovered signer as ' + from); + } else { + alert( + 'Failed to verify signer when comparing ' + result + ' to ' + from + ); + } + } +); +*/ + +#[test] +fn permit_valid_with_metamask_signed_data() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let owner: H160 = CryptoAlith.into(); + let spender: H160 = Bob.into(); + let value: U256 = 1000u16.into(); + let deadline: U256 = 1u16.into(); // todo: proper timestamp + + let rsv = hex_literal::hex!( + "612960858951e133d05483804be5456a030be4ce6c000a855d865c0be75a8fc11d89ca96d5a153e8c + 7155ab1147f0f6d3326388b8d866c2406ce34567b7501a01b" + ) + .as_slice(); + let (r, sv) = rsv.split_at(32); + let (s, v) = sv.split_at(32); + let v_real = v[0]; + let r_real: [u8; 32] = r.try_into().unwrap(); + let s_real: [u8; 32] = s.try_into().unwrap(); + + precompiles() + .prepare_test( + Charlie, // can be anyone, + Precompile1, + PCall::eip2612_permit { + owner: Address(owner), + spender: Address(spender), + value, + deadline, + v: v_real, + r: r_real.into(), + s: s_real.into(), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_log(log3( + Precompile1, + SELECTOR_LOG_APPROVAL, + CryptoAlith, + Bob, + solidity::encode_event_data(U256::from(1000)), + )) + .execute_returns(()); + }); +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces( + &["ERC20.sol", "Permit.sol"], + PCall::supports_selector, + ) +} diff --git a/precompiles/batch/Batch.sol b/precompiles/batch/Batch.sol new file mode 100644 index 00000000..a5f9b133 --- /dev/null +++ b/precompiles/batch/Batch.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Batch precompile +/// @dev Allows to perform multiple calls throught one call to the precompile. +/// Can be used by EOA to do multiple calls in a single transaction. +interface Batch { + /// @dev Batch multiple calls into a single transaction. + /// All calls are performed from the address calling this precompile. + /// + /// In case of one subcall reverting following subcalls will still be attempted. + /// + /// @param to List of addresses to call. + /// @param value List of values for each subcall. If array is shorter than "to" then additional + /// calls will be performed with a value of 0. + /// @param callData Call data for each `to` address. If array is shorter than "to" then + /// additional calls will be performed with an empty call data. + /// @param gasLimit Gas limit for each `to` address. Use 0 to forward all the remaining gas. + /// If array is shorter than "to" then the remaining gas available will be used. + /// @custom:selector 79df4b9c + function batchSome( + address[] memory to, + uint256[] memory value, + bytes[] memory callData, + uint64[] memory gasLimit + ) external; + + /// @dev Batch multiple calls into a single transaction. + /// All calls are performed from the address calling this precompile. + /// + /// In case of one subcall reverting, no more subcalls will be executed but + /// the batch transaction will succeed. Use batchAll to revert on any subcall revert. + /// + /// @param to List of addresses to call. + /// @param value List of values for each subcall. If array is shorter than "to" then additional + /// calls will be performed with a value of 0. + /// @param callData Call data for each `to` address. If array is shorter than "to" then + /// additional calls will be performed with an empty call data. + /// @param gasLimit Gas limit for each `to` address. Use 0 to forward all the remaining gas. + /// If array is shorter than "to" then the remaining gas available will be used. + /// @custom:selector cf0491c7 + function batchSomeUntilFailure( + address[] memory to, + uint256[] memory value, + bytes[] memory callData, + uint64[] memory gasLimit + ) external; + + /// @dev Batch multiple calls into a single transaction. + /// All calls are performed from the address calling this precompile. + /// + /// In case of one subcall reverting, the entire batch will revert. + /// + /// @param to List of addresses to call. + /// @param value List of values for each subcall. If array is shorter than "to" then additional + /// calls will be performed with a value of 0. + /// @param callData Call data for each `to` address. If array is shorter than "to" then + /// additional calls will be performed with an empty call data. + /// @param gasLimit Gas limit for each `to` address. Use 0 to forward all the remaining gas. + /// If array is shorter than "to" then the remaining gas available will be used. + /// @custom:selector 96e292b8 + function batchAll( + address[] memory to, + uint256[] memory value, + bytes[] memory callData, + uint64[] memory gasLimit + ) external; + + /// Emitted when a subcall succeeds. + event SubcallSucceeded(uint256 index); + + /// Emitted when a subcall fails. + event SubcallFailed(uint256 index); +} diff --git a/precompiles/batch/Cargo.toml b/precompiles/batch/Cargo.toml new file mode 100644 index 00000000..ecfe6609 --- /dev/null +++ b/precompiles/batch/Cargo.toml @@ -0,0 +1,55 @@ +[package] +name = "pallet-evm-precompile-batch" +authors = { workspace = true } +description = "A Precompile to batch multiple calls." +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +paste = { workspace = true } +slices = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-std = { workspace = true } + +# Frontier +evm = { workspace = true, features = [ "with-codec" ] } +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +pallet-balances = { workspace = true, features = [ "insecure_zero_ed", "std" ] } +pallet-timestamp = { workspace = true, features = [ "std" ] } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len", "std" ] } +precompile-utils = { workspace = true, features = [ "std", "testing" ] } +scale-info = { workspace = true, features = [ "derive", "std" ] } +sp-runtime = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-evm/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", +] diff --git a/precompiles/batch/src/lib.rs b/precompiles/batch/src/lib.rs new file mode 100644 index 00000000..60e6d590 --- /dev/null +++ b/precompiles/batch/src/lib.rs @@ -0,0 +1,331 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Precompile to interact with pallet_balances instances using the ERC20 interface standard. + +#![cfg_attr(not(feature = "std"), no_std)] + +use evm::{ExitError, ExitReason}; +use fp_evm::{Context, Log, PrecompileFailure, PrecompileHandle, Transfer}; +use frame_support::traits::ConstU32; +use precompile_utils::{evm::costs::call_cost, prelude::*}; +use sp_core::{H160, U256}; +use sp_std::{iter::repeat, marker::PhantomData, vec, vec::Vec}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Mode { + BatchSome, // = "batchSome(address[],uint256[],bytes[],uint64[])", + BatchSomeUntilFailure, // = "batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])", + BatchAll, // = "batchAll(address[],uint256[],bytes[],uint64[])", +} + +pub const LOG_SUBCALL_SUCCEEDED: [u8; 32] = keccak256!("SubcallSucceeded(uint256)"); +pub const LOG_SUBCALL_FAILED: [u8; 32] = keccak256!("SubcallFailed(uint256)"); +pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16); +pub const ARRAY_LIMIT: u32 = 2u32.pow(9); + +type GetCallDataLimit = ConstU32; +type GetArrayLimit = ConstU32; + +pub fn log_subcall_succeeded(address: impl Into, index: usize) -> Log { + log1( + address, + LOG_SUBCALL_SUCCEEDED, + solidity::encode_event_data(U256::from(index)), + ) +} + +pub fn log_subcall_failed(address: impl Into, index: usize) -> Log { + log1( + address, + LOG_SUBCALL_FAILED, + solidity::encode_event_data(U256::from(index)), + ) +} + +/// Batch precompile. +#[derive(Debug, Clone)] +pub struct BatchPrecompile(PhantomData); + +// No funds are transfered to the precompile address. +// Transfers will directly be made on the behalf of the user by the precompile. +#[precompile_utils::precompile] +impl BatchPrecompile +where + Runtime: pallet_evm::Config, +{ + #[precompile::public("batchSome(address[],uint256[],bytes[],uint64[])")] + fn batch_some( + handle: &mut impl PrecompileHandle, + to: BoundedVec, + value: BoundedVec, + call_data: BoundedVec, GetArrayLimit>, + gas_limit: BoundedVec, + ) -> EvmResult { + Self::inner_batch(Mode::BatchSome, handle, to, value, call_data, gas_limit) + } + + #[precompile::public("batchSomeUntilFailure(address[],uint256[],bytes[],uint64[])")] + fn batch_some_until_failure( + handle: &mut impl PrecompileHandle, + to: BoundedVec, + value: BoundedVec, + call_data: BoundedVec, GetArrayLimit>, + gas_limit: BoundedVec, + ) -> EvmResult { + Self::inner_batch( + Mode::BatchSomeUntilFailure, + handle, + to, + value, + call_data, + gas_limit, + ) + } + + #[precompile::public("batchAll(address[],uint256[],bytes[],uint64[])")] + fn batch_all( + handle: &mut impl PrecompileHandle, + to: BoundedVec, + value: BoundedVec, + call_data: BoundedVec, GetArrayLimit>, + gas_limit: BoundedVec, + ) -> EvmResult { + Self::inner_batch(Mode::BatchAll, handle, to, value, call_data, gas_limit) + } + + fn inner_batch( + mode: Mode, + handle: &mut impl PrecompileHandle, + to: BoundedVec, + value: BoundedVec, + call_data: BoundedVec, GetArrayLimit>, + gas_limit: BoundedVec, + ) -> EvmResult { + let addresses = Vec::from(to).into_iter().enumerate(); + let values = Vec::from(value) + .into_iter() + .map(|x| Some(x)) + .chain(repeat(None)); + let calls_data = Vec::from(call_data) + .into_iter() + .map(|x| Some(x.into())) + .chain(repeat(None)); + let gas_limits = Vec::from(gas_limit).into_iter().map(|x| + // x = 0 => forward all remaining gas + if x == 0 { + None + } else { + Some(x) + } + ).chain(repeat(None)); + + // Cost of batch log. (doesn't change when index changes) + let log_cost = log_subcall_failed(handle.code_address(), 0) + .compute_cost() + .map_err(|_| revert("Failed to compute log cost"))?; + + for ((i, address), (value, (call_data, gas_limit))) in + addresses.zip(values.zip(calls_data.zip(gas_limits))) + { + let address = address.0; + let value = value.unwrap_or(U256::zero()); + let call_data = call_data.unwrap_or(vec![]); + + let sub_context = Context { + caller: handle.context().caller, + address: address.clone(), + apparent_value: value, + }; + + let transfer = if value.is_zero() { + None + } else { + Some(Transfer { + source: handle.context().caller, + target: address.clone(), + value, + }) + }; + + // We reserve enough gas to emit a final log and perform the subcall itself. + // If not enough gas we stop there according to Mode strategy. + let remaining_gas = handle.remaining_gas(); + + let forwarded_gas = match (remaining_gas.checked_sub(log_cost), mode) { + (Some(remaining), _) => remaining, + (None, Mode::BatchAll) => { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) + } + (None, _) => { + return Ok(()); + } + }; + + // Cost of the call itself that the batch precompile must pay. + let call_cost = call_cost(value, ::config()); + + let forwarded_gas = match forwarded_gas.checked_sub(call_cost) { + Some(remaining) => remaining, + None => { + let log = log_subcall_failed(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)?; + + match mode { + Mode::BatchAll => { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) + } + Mode::BatchSomeUntilFailure => return Ok(()), + Mode::BatchSome => continue, + } + } + }; + + // If there is a provided gas limit we ensure there is enough gas remaining. + let forwarded_gas = match gas_limit { + None => forwarded_gas, // provide all gas if no gas limit, + Some(limit) => { + if limit > forwarded_gas { + let log = log_subcall_failed(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)?; + + match mode { + Mode::BatchAll => { + return Err(PrecompileFailure::Error { + exit_status: ExitError::OutOfGas, + }) + } + Mode::BatchSomeUntilFailure => return Ok(()), + Mode::BatchSome => continue, + } + } + limit + } + }; + + let (reason, output) = handle.call( + address, + transfer, + call_data, + Some(forwarded_gas), + false, + &sub_context, + ); + + // Logs + // We reserved enough gas so this should not OOG. + match reason { + ExitReason::Revert(_) | ExitReason::Error(_) => { + let log = log_subcall_failed(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)? + } + ExitReason::Succeed(_) => { + let log = log_subcall_succeeded(handle.code_address(), i); + handle.record_log_costs(&[&log])?; + log.record(handle)? + } + _ => (), + } + + // How to proceed + match (mode, reason) { + // _: Fatal is always fatal + (_, ExitReason::Fatal(exit_status)) => { + return Err(PrecompileFailure::Fatal { exit_status }) + } + + // BatchAll : Reverts and errors are immediatly forwarded. + (Mode::BatchAll, ExitReason::Revert(exit_status)) => { + return Err(PrecompileFailure::Revert { + exit_status, + output, + }) + } + (Mode::BatchAll, ExitReason::Error(exit_status)) => { + return Err(PrecompileFailure::Error { exit_status }) + } + + // BatchSomeUntilFailure : Reverts and errors prevent subsequent subcalls to + // be executed but the precompile still succeed. + (Mode::BatchSomeUntilFailure, ExitReason::Revert(_) | ExitReason::Error(_)) => { + return Ok(()) + } + + // Success or ignored revert/error. + (_, _) => (), + } + } + + Ok(()) + } +} + +// The enum is generated by the macro above. +// We add this method to simplify writing tests generic over the mode. +impl BatchPrecompileCall +where + Runtime: pallet_evm::Config, +{ + pub fn batch_from_mode( + mode: Mode, + to: Vec
, + value: Vec, + call_data: Vec>, + gas_limit: Vec, + ) -> Self { + // Convert Vecs into their bounded versions. + // This is mainly a convenient function to write tests. + // Bounds are only checked when parsing from call data. + let to = to.into(); + let value = value.into(); + let call_data: Vec<_> = call_data.into_iter().map(|inner| inner.into()).collect(); + let call_data = call_data.into(); + let gas_limit = gas_limit.into(); + + match mode { + Mode::BatchSome => Self::batch_some { + to, + value, + call_data, + gas_limit, + }, + Mode::BatchSomeUntilFailure => Self::batch_some_until_failure { + to, + value, + call_data, + gas_limit, + }, + Mode::BatchAll => Self::batch_all { + to, + value, + call_data, + gas_limit, + }, + } + } +} diff --git a/precompiles/batch/src/mock.rs b/precompiles/batch/src/mock.rs new file mode 100644 index 00000000..f8821ca7 --- /dev/null +++ b/precompiles/batch/src/mock.rs @@ -0,0 +1,207 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Test utilities +use super::*; + +use frame_support::traits::Everything; +use frame_support::{construct_runtime, parameter_types, weights::Weight}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::{mock_account, precompile_set::*, testing::MockAccount}; +use sp_core::H256; +use sp_runtime::BuildStorage; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +pub type AccountId = MockAccount; +pub type Balance = u128; + +type Block = frame_system::mocking::MockBlockU32; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const MaximumBlockWeight: Weight = Weight::from_parts(1024, 1); + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileAt< + AddressU64<1>, + BatchPrecompile, + ( + SubcallWithMaxNesting<1>, + // Batch is the only precompile allowed to call Batch. + CallableByPrecompile>>, + ), + >, + RevertPrecompile>, + ), +>; + +pub type PCall = BatchPrecompileCall; + +mock_account!(Batch, |_| MockAccount::from_u64(1)); +mock_account!(Revert, |_| MockAccount::from_u64(2)); + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + pallet_evm::Pallet::::create_account( + Revert.into(), + hex_literal::hex!("1460006000fd").to_vec(), + ); + }); + ext + } +} + +pub fn balance(account: impl Into) -> Balance { + pallet_balances::Pallet::::usable_balance(account.into()) +} diff --git a/precompiles/batch/src/tests.rs b/precompiles/batch/src/tests.rs new file mode 100644 index 00000000..1baa23d8 --- /dev/null +++ b/precompiles/batch/src/tests.rs @@ -0,0 +1,1091 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use crate::mock::{ + balance, Batch, ExtBuilder, PCall, Precompiles, PrecompilesValue, Revert, Runtime, RuntimeCall, + RuntimeOrigin, +}; +use crate::{ + log_subcall_failed, log_subcall_succeeded, Mode, LOG_SUBCALL_FAILED, LOG_SUBCALL_SUCCEEDED, +}; +use fp_evm::ExitError; +use frame_support::assert_ok; +use pallet_evm::Call as EvmCall; +use precompile_utils::solidity::revert::revert_as_bytes; +use precompile_utils::{evm::costs::call_cost, prelude::*, testing::*}; +use sp_core::{H160, H256, U256}; +use sp_runtime::DispatchError; +use sp_runtime::{traits::Dispatchable, DispatchErrorWithPostInfo, ModuleError}; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +fn evm_call(from: impl Into, input: Vec) -> EvmCall { + EvmCall::call { + source: from.into(), + target: Batch.into(), + input, + value: U256::zero(), // No value sent in EVM + gas_limit: u64::max_value(), + max_fee_per_gas: 0.into(), + max_priority_fee_per_gas: Some(U256::zero()), + nonce: None, // Use the next nonce + access_list: Vec::new(), + } +} + +fn costs() -> (u64, u64) { + let return_log_cost = log_subcall_failed(Batch, 0).compute_cost().unwrap(); + let call_cost = + return_log_cost + call_cost(U256::one(), ::config()); + (return_log_cost, call_cost) +} + +#[test] +fn selectors() { + assert!(PCall::batch_some_selectors().contains(&0x79df4b9c)); + assert!(PCall::batch_some_until_failure_selectors().contains(&0xcf0491c7)); + assert!(PCall::batch_all_selectors().contains(&0x96e292b8)); + assert_eq!( + LOG_SUBCALL_FAILED, + hex_literal::hex!("dbc5d06f4f877f959b1ff12d2161cdd693fa8e442ee53f1790b2804b24881f05") + ); + assert_eq!( + LOG_SUBCALL_SUCCEEDED, + hex_literal::hex!("bf855484633929c3d6688eb3caf8eff910fb4bef030a8d7dbc9390d26759714d") + ); +} + +#[test] +fn modifiers() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + let mut tester = PrecompilesModifierTester::new(precompiles(), Alice, Batch); + + tester.test_default_modifier(PCall::batch_some_selectors()); + tester.test_default_modifier(PCall::batch_some_until_failure_selectors()); + tester.test_default_modifier(PCall::batch_all_selectors()); + }); +} + +#[test] +fn batch_some_empty() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + Alice, + Batch, + PCall::batch_some { + to: vec![].into(), + value: vec![].into(), + call_data: vec![].into(), + gas_limit: vec![].into(), + }, + ) + .with_subcall_handle(|Subcall { .. }| panic!("there should be no subcall")) + .execute_returns(()) + }) +} + +#[test] +fn batch_some_until_failure_empty() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + Alice, + Batch, + PCall::batch_some_until_failure { + to: vec![].into(), + value: vec![].into(), + call_data: vec![].into(), + gas_limit: vec![].into(), + }, + ) + .with_subcall_handle(|Subcall { .. }| panic!("there should be no subcall")) + .execute_returns(()) + }) +} + +#[test] +fn batch_all_empty() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + Alice, + Batch, + PCall::batch_all { + to: vec![].into(), + value: vec![].into(), + call_data: vec![].into(), + gas_limit: vec![].into(), + }, + ) + .with_subcall_handle(|Subcall { .. }| panic!("there should be no subcall")) + .execute_returns(()) + }) +} + +fn batch_returns( + precompiles: &Precompiles, + mode: Mode, +) -> PrecompilesTester> { + let mut counter = 0; + + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + Batch, + PCall::batch_from_mode( + mode, + vec![Address(Bob.into()), Address(Charlie.into())], + vec![U256::from(1u8), U256::from(2u8)], + vec![b"one".to_vec(), b"two".to_vec()], + vec![], + ), + ) + .with_target_gas(Some(100_000)) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called from the precompile caller. + assert_eq!(context.caller, Alice.into()); + assert_eq!(is_static, false); + + match address { + a if a == Bob.into() => { + assert_eq!(counter, 0, "this is the first call"); + counter += 1; + + assert_eq!( + target_gas, + Some(100_000 - total_call_cost), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 1u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 1u8.into()); + + assert_eq!(&input, b"one"); + + SubcallOutput { + cost: 13, + logs: vec![log1(Bob, H256::repeat_byte(0x11), vec![])], + ..SubcallOutput::succeed() + } + } + a if a == Charlie.into() => { + assert_eq!(counter, 1, "this is the second call"); + counter += 1; + + assert_eq!( + target_gas, + Some(100_000 - 13 - total_call_cost * 2), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Charlie.into()); + assert_eq!(transfer.value, 2u8.into()); + + assert_eq!(context.address, Charlie.into()); + assert_eq!(context.apparent_value, 2u8.into()); + + assert_eq!(&input, b"two"); + + SubcallOutput { + cost: 17, + logs: vec![log1(Charlie, H256::repeat_byte(0x22), vec![])], + ..SubcallOutput::succeed() + } + } + _ => panic!("unexpected subcall"), + } + }) + .expect_cost(13 + 17 + total_call_cost * 2) +} + +#[test] +fn batch_some_returns() { + ExtBuilder::default().build().execute_with(|| { + batch_returns(&precompiles(), Mode::BatchSome) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(Batch, 0)) + .expect_log(log1(Charlie, H256::repeat_byte(0x22), vec![])) + .expect_log(log_subcall_succeeded(Batch, 1)) + .execute_returns(()) + }) +} + +#[test] +fn batch_some_until_failure_returns() { + ExtBuilder::default().build().execute_with(|| { + batch_returns(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(Batch, 0)) + .expect_log(log1(Charlie, H256::repeat_byte(0x22), vec![])) + .expect_log(log_subcall_succeeded(Batch, 1)) + .execute_returns(()) + }) +} + +#[test] +fn batch_all_returns() { + ExtBuilder::default().build().execute_with(|| { + batch_returns(&precompiles(), Mode::BatchAll) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(Batch, 0)) + .expect_log(log1(Charlie, H256::repeat_byte(0x22), vec![])) + .expect_log(log_subcall_succeeded(Batch, 1)) + .execute_returns(()) + }) +} + +fn batch_out_of_gas( + precompiles: &Precompiles, + mode: Mode, +) -> PrecompilesTester> { + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + Batch, + PCall::batch_from_mode( + mode, + vec![Address(Bob.into())], + vec![U256::from(1u8)], + vec![b"one".to_vec()], + vec![], + ), + ) + .with_target_gas(Some(50_000)) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called from the precompile caller. + assert_eq!(context.caller, Alice.into()); + assert_eq!(is_static, false); + + match address { + a if a == Bob.into() => { + assert_eq!( + target_gas, + Some(50_000 - total_call_cost), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 1u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 1u8.into()); + + assert_eq!(&input, b"one"); + + SubcallOutput { + cost: 11_000, + ..SubcallOutput::out_of_gas() + } + } + _ => panic!("unexpected subcall"), + } + }) +} + +#[test] +fn batch_some_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_out_of_gas(&precompiles(), Mode::BatchSome) + .expect_log(log_subcall_failed(Batch, 0)) + .execute_returns(()) + }) +} + +#[test] +fn batch_some_until_failure_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_out_of_gas(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log_subcall_failed(Batch, 0)) + .execute_returns(()) + }) +} + +#[test] +fn batch_all_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_out_of_gas(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas) + }) +} + +fn batch_incomplete( + precompiles: &Precompiles, + mode: Mode, +) -> PrecompilesTester> { + let mut counter = 0; + + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + Batch, + PCall::batch_from_mode( + mode, + vec![ + Address(Bob.into()), + Address(Charlie.into()), + Address(Alice.into()), + ], + vec![U256::from(1u8), U256::from(2u8), U256::from(3u8)], + vec![b"one".to_vec()], + vec![], + ), + ) + .with_target_gas(Some(300_000)) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called from the precompile caller. + assert_eq!(context.caller, Alice.into()); + assert_eq!(is_static, false); + + match address { + a if a == Bob.into() => { + assert_eq!(counter, 0, "this is the first call"); + counter += 1; + + assert_eq!( + target_gas, + Some(300_000 - total_call_cost), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 1u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 1u8.into()); + + assert_eq!(&input, b"one"); + + SubcallOutput { + cost: 13, + logs: vec![log1(Bob, H256::repeat_byte(0x11), vec![])], + ..SubcallOutput::succeed() + } + } + a if a == Charlie.into() => { + assert_eq!(counter, 1, "this is the second call"); + counter += 1; + + assert_eq!( + target_gas, + Some(300_000 - 13 - total_call_cost * 2), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Charlie.into()); + assert_eq!(transfer.value, 2u8.into()); + + assert_eq!(context.address, Charlie.into()); + assert_eq!(context.apparent_value, 2u8.into()); + + assert_eq!(&input, b""); + + SubcallOutput { + output: revert_as_bytes("Revert message"), + cost: 17, + ..SubcallOutput::revert() + } + } + a if a == Alice.into() => { + assert_eq!(counter, 2, "this is the third call"); + counter += 1; + + assert_eq!( + target_gas, + Some(300_000 - 13 - 17 - total_call_cost * 3), + "batch forward all gas" + ); + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, Alice.into()); + assert_eq!(transfer.target, Alice.into()); + assert_eq!(transfer.value, 3u8.into()); + + assert_eq!(context.address, Alice.into()); + assert_eq!(context.apparent_value, 3u8.into()); + + assert_eq!(&input, b""); + + SubcallOutput { + cost: 19, + logs: vec![log1(Alice, H256::repeat_byte(0x33), vec![])], + ..SubcallOutput::succeed() + } + } + _ => panic!("unexpected subcall"), + } + }) +} + +#[test] +fn batch_some_incomplete() { + ExtBuilder::default().build().execute_with(|| { + let (_, total_call_cost) = costs(); + + batch_incomplete(&precompiles(), Mode::BatchSome) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(Batch, 0)) + .expect_log(log_subcall_failed(Batch, 1)) + .expect_log(log1(Alice, H256::repeat_byte(0x33), vec![])) + .expect_log(log_subcall_succeeded(Batch, 2)) + .expect_cost(13 + 17 + 19 + total_call_cost * 3) + .execute_returns(()) + }) +} + +#[test] +fn batch_some_until_failure_incomplete() { + ExtBuilder::default().build().execute_with(|| { + let (_, total_call_cost) = costs(); + + batch_incomplete(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .expect_log(log_subcall_succeeded(Batch, 0)) + .expect_log(log_subcall_failed(Batch, 1)) + .expect_cost(13 + 17 + total_call_cost * 2) + .execute_returns(()) + }) +} + +#[test] +fn batch_all_incomplete() { + ExtBuilder::default().build().execute_with(|| { + batch_incomplete(&precompiles(), Mode::BatchAll) + .execute_reverts(|output| output == b"Revert message") + }) +} + +fn batch_log_out_of_gas( + precompiles: &Precompiles, + mode: Mode, +) -> PrecompilesTester> { + let (log_cost, _) = costs(); + + precompiles + .prepare_test( + Alice, + Batch, + PCall::batch_from_mode( + mode, + vec![Address(Bob.into())], + vec![U256::from(1u8)], + vec![b"one".to_vec()], + vec![], + ), + ) + .with_target_gas(Some(log_cost - 1)) + .with_subcall_handle(move |_subcall| panic!("there shouldn't be any subcalls")) +} + +#[test] +fn batch_all_log_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_log_out_of_gas(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas); + }) +} + +#[test] +fn batch_some_log_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_log_out_of_gas(&precompiles(), Mode::BatchSome) + .expect_no_logs() + .execute_returns(()); + }) +} + +#[test] +fn batch_some_until_failure_log_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_log_out_of_gas(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_no_logs() + .execute_returns(()); + }) +} + +fn batch_call_out_of_gas( + precompiles: &Precompiles, + mode: Mode, +) -> PrecompilesTester> { + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + Batch, + PCall::batch_from_mode( + mode, + vec![Address(Bob.into())], + vec![U256::from(1u8)], + vec![b"one".to_vec()], + vec![], + ), + ) + .with_target_gas(Some(total_call_cost - 1)) + .with_subcall_handle(move |_subcall| panic!("there shouldn't be any subcalls")) +} + +#[test] +fn batch_all_call_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_call_out_of_gas(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas); + }) +} + +#[test] +fn batch_some_call_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_call_out_of_gas(&precompiles(), Mode::BatchSome) + .expect_log(log_subcall_failed(Batch, 0)) + .execute_returns(()); + }) +} + +#[test] +fn batch_some_until_failure_call_out_of_gas() { + ExtBuilder::default().build().execute_with(|| { + batch_call_out_of_gas(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log_subcall_failed(Batch, 0)) + .execute_returns(()); + }) +} + +fn batch_gas_limit( + precompiles: &Precompiles, + mode: Mode, +) -> PrecompilesTester> { + let (_, total_call_cost) = costs(); + + precompiles + .prepare_test( + Alice, + Batch, + PCall::batch_from_mode( + mode, + vec![Address(Bob.into())], + vec![U256::from(1u8)], + vec![b"one".to_vec()], + vec![50_000 - total_call_cost + 1], + ), + ) + .with_target_gas(Some(50_000)) + .with_subcall_handle(move |_subcall| panic!("there shouldn't be any subcalls")) +} + +#[test] +fn batch_all_gas_limit() { + ExtBuilder::default().build().execute_with(|| { + batch_gas_limit(&precompiles(), Mode::BatchAll).execute_error(ExitError::OutOfGas); + }) +} + +#[test] +fn batch_some_gas_limit() { + ExtBuilder::default().build().execute_with(|| { + let (return_log_cost, _) = costs(); + + batch_gas_limit(&precompiles(), Mode::BatchSome) + .expect_log(log_subcall_failed(Batch, 0)) + .expect_cost(return_log_cost) + .execute_returns(()); + }) +} + +#[test] +fn batch_some_until_failure_gas_limit() { + ExtBuilder::default().build().execute_with(|| { + batch_gas_limit(&precompiles(), Mode::BatchSomeUntilFailure) + .expect_log(log_subcall_failed(Batch, 0)) + .execute_returns(()); + }) +} + +#[test] +fn evm_batch_some_transfers_enough() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_some { + to: vec![Address(Bob.into()), Address(Charlie.into())].into(), + value: vec![U256::from(1_000u16), U256::from(2_000u16)].into(), + call_data: vec![].into(), + gas_limit: vec![].into(), + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + }) +} + +#[test] +fn evm_batch_some_until_failure_transfers_enough() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_some_until_failure { + to: vec![Address(Bob.into()), Address(Charlie.into())].into(), + value: vec![U256::from(1_000u16), U256::from(2_000u16)].into(), + call_data: vec![].into(), + gas_limit: vec![].into(), + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + }) +} + +#[test] +fn evm_batch_all_transfers_enough() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_all { + to: vec![Address(Bob.into()), Address(Charlie.into())].into(), + value: vec![U256::from(1_000u16), U256::from(2_000u16)].into(), + call_data: vec![].into(), + gas_limit: vec![].into(), + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Bob), 1_000); + assert_eq!(balance(Charlie), 2_000); + }) +} + +#[test] +fn evm_batch_some_transfers_too_much() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_some { + to: vec![ + Address(Bob.into()), + Address(Charlie.into()), + Address(David.into()), + ] + .into(), + value: vec![ + U256::from(9_000u16), + U256::from(2_000u16), + U256::from(500u16) + ] + .into(), + call_data: vec![].into(), + gas_limit: vec![].into() + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 500); // gasprice = 0 + assert_eq!(balance(Bob), 9_000); + assert_eq!(balance(Charlie), 0); + assert_eq!(balance(David), 500); + }) +} + +#[test] +fn evm_batch_some_until_failure_transfers_too_much() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_some_until_failure { + to: vec![ + Address(Bob.into()), + Address(Charlie.into()), + Address(David.into()), + ] + .into(), + value: vec![ + U256::from(9_000u16), + U256::from(2_000u16), + U256::from(500u16) + ] + .into(), + call_data: vec![].into(), + gas_limit: vec![].into() + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 1_000); // gasprice = 0 + assert_eq!(balance(Bob), 9_000); + assert_eq!(balance(Charlie), 0); + assert_eq!(balance(David), 0); + }) +} + +#[test] +fn evm_batch_all_transfers_too_much() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_all { + to: vec![ + Address(Bob.into()), + Address(Charlie.into()), + Address(David.into()), + ] + .into(), + value: vec![ + U256::from(9_000u16), + U256::from(2_000u16), + U256::from(500u16) + ] + .into(), + call_data: vec![].into(), + gas_limit: vec![].into() + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 10_000); // gasprice = 0 + assert_eq!(balance(Bob), 0); + assert_eq!(balance(Charlie), 0); + assert_eq!(balance(David), 0); + }) +} + +#[test] +fn evm_batch_some_contract_revert() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_some { + to: vec![ + Address(Bob.into()), + Address(Revert.into()), + Address(David.into()), + ] + .into(), + value: vec![ + U256::from(1_000u16), + U256::from(2_000), + U256::from(3_000u16) + ] + .into(), + call_data: vec![].into(), + gas_limit: vec![].into() + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 6_000); // gasprice = 0 + assert_eq!(balance(Bob), 1_000); + assert_eq!(balance(Revert), 0); + assert_eq!(balance(David), 3_000); + }) +} + +#[test] +fn evm_batch_some_until_failure_contract_revert() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_some_until_failure { + to: vec![ + Address(Bob.into()), + Address(Revert.into()), + Address(David.into()), + ] + .into(), + value: vec![ + U256::from(1_000u16), + U256::from(2_000), + U256::from(3_000u16) + ] + .into(), + call_data: vec![].into(), + gas_limit: vec![].into() + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 9_000); // gasprice = 0 + assert_eq!(balance(Bob), 1_000); + assert_eq!(balance(Revert), 0); + assert_eq!(balance(David), 0); + }) +} + +#[test] +fn evm_batch_all_contract_revert() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + assert_ok!(RuntimeCall::Evm(evm_call( + Alice, + PCall::batch_all { + to: vec![ + Address(Bob.into()), + Address(Revert.into()), + Address(David.into()), + ] + .into(), + value: vec![ + U256::from(1_000u16), + U256::from(2_000), + U256::from(3_000u16) + ] + .into(), + call_data: vec![].into(), + gas_limit: vec![].into() + } + .into() + )) + .dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 10_000); // gasprice = 0 + assert_eq!(balance(Bob), 0); + assert_eq!(balance(Revert), 0); + assert_eq!(balance(David), 0); + }) +} + +#[test] +fn evm_batch_recursion_under_limit() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + // Mock sets the recursion limit to 2, and we 2 nested batch. + // Thus it succeeds. + + let input = PCall::batch_all { + to: vec![Address(Batch.into())].into(), + value: vec![].into(), + gas_limit: vec![].into(), + call_data: vec![PCall::batch_all { + to: vec![Address(Bob.into())].into(), + value: vec![1000_u32.into()].into(), + gas_limit: vec![].into(), + call_data: vec![].into(), + } + .encode() + .into()] + .into(), + } + .into(); + + assert_ok!(RuntimeCall::Evm(evm_call(Alice, input)).dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 9_000); // gasprice = 0 + assert_eq!(balance(Bob), 1_000); + }) +} + +#[test] +fn evm_batch_recursion_over_limit() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + // Mock sets the recursion limit to 2, and we 3 nested batch. + // Thus it reverts. + + let input = PCall::batch_from_mode( + Mode::BatchAll, + vec![Address(Batch.into())], + vec![], + vec![PCall::batch_from_mode( + Mode::BatchAll, + vec![Address(Batch.into())], + vec![], + vec![PCall::batch_from_mode( + Mode::BatchAll, + vec![Address(Bob.into())], + vec![1000_u32.into()], + vec![], + vec![].into(), + ) + .into()], + vec![].into(), + ) + .into()], + vec![], + ) + .into(); + + assert_ok!(RuntimeCall::Evm(evm_call(Alice, input)).dispatch(RuntimeOrigin::root())); + + assert_eq!(balance(Alice), 10_000); // gasprice = 0 + assert_eq!(balance(Bob), 0); + }) +} + +#[test] +fn batch_not_callable_by_smart_contract() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + // "deploy" SC to alice address + let alice_h160: H160 = Alice.into(); + pallet_evm::AccountCodes::::insert(alice_h160, vec![10u8]); + + // succeeds if not called by SC, see `evm_batch_recursion_under_limit` + let input = PCall::batch_all { + to: vec![Address(Batch.into())].into(), + value: vec![].into(), + gas_limit: vec![].into(), + call_data: vec![PCall::batch_all { + to: vec![Address(Bob.into())].into(), + value: vec![1000_u32.into()].into(), + gas_limit: vec![].into(), + call_data: vec![].into(), + } + .encode() + .into()] + .into(), + } + .into(); + + match RuntimeCall::Evm(evm_call(Alice, input)).dispatch(RuntimeOrigin::root()) { + Err(DispatchErrorWithPostInfo { + error: + DispatchError::Module(ModuleError { + message: Some(err_msg), + .. + }), + .. + }) => assert_eq!("TransactionMustComeFromEOA", err_msg), + _ => panic!("expected error 'TransactionMustComeFromEOA'"), + } + }) +} + +#[test] +fn batch_is_not_callable_by_dummy_code() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 10_000)]) + .build() + .execute_with(|| { + // "deploy" dummy code to alice address + let alice_h160: H160 = Alice.into(); + pallet_evm::AccountCodes::::insert( + alice_h160, + [0x60, 0x00, 0x60, 0x00, 0xfd].to_vec(), + ); + + // succeeds if called by dummy code, see `evm_batch_recursion_under_limit` + let input = PCall::batch_all { + to: vec![Address(Batch.into())].into(), + value: vec![].into(), + gas_limit: vec![].into(), + call_data: vec![PCall::batch_all { + to: vec![Address(Bob.into())].into(), + value: vec![1000_u32.into()].into(), + gas_limit: vec![].into(), + call_data: vec![].into(), + } + .encode() + .into()] + .into(), + } + .into(); + + match RuntimeCall::Evm(evm_call(Alice, input)).dispatch(RuntimeOrigin::root()) { + Err(DispatchErrorWithPostInfo { + error: + DispatchError::Module(ModuleError { + message: Some(err_msg), + .. + }), + .. + }) => assert_eq!("TransactionMustComeFromEOA", err_msg), + _ => panic!("expected error 'TransactionMustComeFromEOA'"), + } + }) +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces(&["Batch.sol"], PCall::supports_selector) +} diff --git a/precompiles/call-permit/CallPermit.sol b/precompiles/call-permit/CallPermit.sol new file mode 100644 index 00000000..91713afc --- /dev/null +++ b/precompiles/call-permit/CallPermit.sol @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Call Permit Interface +/// @dev The interface aims to be a general-purpose tool to perform gas-less transactions. It uses the EIP-712 standard, +/// and signed messages can be dispatched by another network participant with a transaction +interface CallPermit { + /// @dev Dispatch a call on the behalf of an other user with a EIP712 permit. + /// Will revert if the permit is not valid or if the dispatched call reverts or errors (such as + /// out of gas). + /// If successful the EIP712 nonce is increased to prevent this permit to be replayed. + /// @param from Who made the permit and want its call to be dispatched on their behalf. + /// @param to Which address the call is made to. + /// @param value Value being transfered from the "from" account. + /// @param data Call data + /// @param gaslimit Gaslimit the dispatched call requires. + /// Providing it prevents the dispatcher to manipulate the gaslimit. + /// @param deadline Deadline in UNIX seconds after which the permit will no longer be valid. + /// @param v V part of the signature. + /// @param r R part of the signature. + /// @param s S part of the signature. + /// @return output Output of the call. + /// @custom:selector b5ea0966 + function dispatch( + address from, + address to, + uint256 value, + bytes memory data, + uint64 gaslimit, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external returns (bytes memory output); + + /// @dev Returns the current nonce for given owner. + /// A permit must have this nonce to be consumed, which will + /// increase the nonce by one. + /// @custom:selector 7ecebe00 + function nonces(address owner) external view returns (uint256); + + /// @dev Returns the EIP712 domain separator. It is used to avoid replay + /// attacks accross assets or other similar EIP712 message structures. + /// @custom:selector 3644e515 + function DOMAIN_SEPARATOR() external view returns (bytes32); +} diff --git a/precompiles/call-permit/Cargo.toml b/precompiles/call-permit/Cargo.toml new file mode 100644 index 00000000..eb3e09d3 --- /dev/null +++ b/precompiles/call-permit/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-evm-precompile-call-permit" +authors = { workspace = true } +description = "A Precompile to dispatch a call with a ERC712 permit." +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } +paste = { workspace = true } +slices = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-timestamp = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] } +sp-core = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +# Frontier +evm = { workspace = true, features = [ "with-codec" ] } +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +[dev-dependencies] +derive_more = { workspace = true } +hex-literal = { workspace = true } +libsecp256k1 = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +pallet-balances = { workspace = true, features = [ "insecure_zero_ed", "std" ] } +pallet-timestamp = { workspace = true, features = [ "std" ] } +precompile-utils = { workspace = true, features = [ "std", "testing" ] } +scale-info = { workspace = true, features = [ "derive", "std" ] } +sp-runtime = { workspace = true, features = [ "std" ] } + +[features] +default = [ "std" ] +std = [ + "fp-evm/std", + "frame-support/std", + "frame-system/std", + "pallet-evm/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", +] diff --git a/precompiles/call-permit/README.md b/precompiles/call-permit/README.md new file mode 100644 index 00000000..52d68fae --- /dev/null +++ b/precompiles/call-permit/README.md @@ -0,0 +1,101 @@ +# Call Permit Precompile + +This precompile aims to be a general-purpose tool to perform gas-less +transactions. + +It allows a user (we'll call her **Alice**) to sign a **call permit** with +MetaMask (using the EIP712 standard), which can then be dispatched by another +user (we'll call him **Bob**) with a transaction. + +**Bob** can make a transaction to the **Call Permit Precompile** with the call +data and **Alice**'s signature. If the permit and signature are valid, the +precompile will perform the call on the behalf of **Alice**, as if **Alice** +made a transaction herself. **Bob** is thus paying the transaction fees and +**Alice** can perform a call without having any native currency to pay for fees +(she'll still need to have some if the call includes a transfer). + +## How to sign the permit + +The following code is an exemple that is working in a Metamask-injected webpage. +**Bob** then need to make a transaction towards the precompile address with the same +data and **Alice**'s signature. + +```js +await window.ethereum.enable(); +const accounts = await window.ethereum.request({ + method: "eth_requestAccounts", +}); + +const from = accounts[0]; +const to = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const value = 42; +const data = "0xdeadbeef"; +const gaslimit = 100000; +const nonce = 0; +const deadline = 1000; + +const createPermitMessageData = function () { + const message = { + from: from, + to: to, + value: value, + data: data, + gaslimit: gaslimit, + nonce: nonce, + deadline: deadline, + }; + + const typedData = JSON.stringify({ + types: { + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ], + CallPermit: [ + { name: "from", type: "address" }, + { name: "to", type: "address" }, + { name: "value", type: "uint256" }, + { name: "data", type: "bytes" }, + { name: "gaslimit", type: "uint64" }, + { name: "nonce", type: "uint256" }, + { name: "deadline", type: "uint256" }, + ], + }, + primaryType: "CallPermit", + domain: { + name: "Call Permit Precompile", + version: "1", + chainId: 0, + verifyingContract: "0x000000000000000000000000000000000000080a", + }, + message: message, + }); + + return { + typedData, + message, + }; +}; + +const method = "eth_signTypedData_v4"; +const messageData = createPermitMessageData(); +const params = [from, messageData.typedData]; + +web3.currentProvider.sendAsync( + { + method, + params, + from, + }, + function (err, result) { + if (err) return console.dir(err); + if (result.error) { + alert(result.error.message); + return console.error("ERROR", result); + } + console.log("Signature:" + JSON.stringify(result.result)); + } +); +``` diff --git a/precompiles/call-permit/src/lib.rs b/precompiles/call-permit/src/lib.rs new file mode 100644 index 00000000..805933b6 --- /dev/null +++ b/precompiles/call-permit/src/lib.rs @@ -0,0 +1,265 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use core::marker::PhantomData; +use evm::ExitReason; +use fp_evm::{Context, ExitRevert, PrecompileFailure, PrecompileHandle, Transfer}; +use frame_support::{ + ensure, + storage::types::{StorageMap, ValueQuery}, + traits::{ConstU32, Get, StorageInstance, Time}, + Blake2_128Concat, +}; +use precompile_utils::{evm::costs::call_cost, prelude::*}; +use sp_core::{H160, H256, U256}; +use sp_io::hashing::keccak_256; +use sp_runtime::traits::UniqueSaturatedInto; +use sp_std::vec::Vec; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +/// Storage prefix for nonces. +pub struct Nonces; + +impl StorageInstance for Nonces { + const STORAGE_PREFIX: &'static str = "Nonces"; + + fn pallet_prefix() -> &'static str { + "PrecompileCallPermit" + } +} + +/// Storage type used to store EIP2612 nonces. +pub type NoncesStorage = StorageMap< + Nonces, + // From + Blake2_128Concat, + H160, + // Nonce + U256, + ValueQuery, +>; + +/// EIP712 permit typehash. +pub const PERMIT_TYPEHASH: [u8; 32] = keccak256!( + "CallPermit(address from,address to,uint256 value,bytes data,uint64 gaslimit\ +,uint256 nonce,uint256 deadline)" +); + +/// EIP712 permit domain used to compute an individualized domain separator. +const PERMIT_DOMAIN: [u8; 32] = keccak256!( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +); + +pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16); + +/// Precompile allowing to issue and dispatch call permits for gasless transactions. +/// A user can sign a permit for a call that can be dispatched and paid by another user or +/// smart contract. +pub struct CallPermitPrecompile(PhantomData); + +#[precompile_utils::precompile] +impl CallPermitPrecompile +where + Runtime: pallet_evm::Config, +{ + fn compute_domain_separator(address: H160) -> [u8; 32] { + let name: H256 = keccak_256(b"Call Permit Precompile").into(); + let version: H256 = keccak256!("1").into(); + let chain_id: U256 = Runtime::ChainId::get().into(); + + let domain_separator_inner = solidity::encode_arguments(( + H256::from(PERMIT_DOMAIN), + name, + version, + chain_id, + Address(address), + )); + + keccak_256(&domain_separator_inner).into() + } + + pub fn generate_permit( + address: H160, + from: H160, + to: H160, + value: U256, + data: Vec, + gaslimit: u64, + nonce: U256, + deadline: U256, + ) -> [u8; 32] { + let domain_separator = Self::compute_domain_separator(address); + + let permit_content = solidity::encode_arguments(( + H256::from(PERMIT_TYPEHASH), + Address(from), + Address(to), + value, + // bytes are encoded as the keccak_256 of the content + H256::from(keccak_256(&data)), + gaslimit, + nonce, + deadline, + )); + let permit_content = keccak_256(&permit_content); + let mut pre_digest = Vec::with_capacity(2 + 32 + 32); + pre_digest.extend_from_slice(b"\x19\x01"); + pre_digest.extend_from_slice(&domain_separator); + pre_digest.extend_from_slice(&permit_content); + keccak_256(&pre_digest) + } + + pub fn dispatch_inherent_cost() -> u64 { + 3_000 // cost of ECRecover precompile for reference + + RuntimeHelper::::db_write_gas_cost() // we write nonce + } + + #[precompile::public( + "dispatch(address,address,uint256,bytes,uint64,uint256,uint8,bytes32,bytes32)" + )] + fn dispatch( + handle: &mut impl PrecompileHandle, + from: Address, + to: Address, + value: U256, + data: BoundedBytes>, + gas_limit: u64, + deadline: U256, + v: u8, + r: H256, + s: H256, + ) -> EvmResult { + // Now: 8 + handle.record_db_read::(8)?; + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + handle.record_cost(Self::dispatch_inherent_cost())?; + + let from: H160 = from.into(); + let to: H160 = to.into(); + let data: Vec = data.into(); + + // ENSURE GASLIMIT IS SUFFICIENT + let call_cost = call_cost(value, ::config()); + + let total_cost = gas_limit + .checked_add(call_cost) + .ok_or_else(|| revert("Call require too much gas (uint64 overflow)"))?; + + if total_cost > handle.remaining_gas() { + return Err(revert("Gaslimit is too low to dispatch provided call")); + } + + // VERIFY PERMIT + + // Blockchain time is in ms while Ethereum use second timestamps. + let timestamp: u128 = + ::Timestamp::now().unique_saturated_into(); + let timestamp: U256 = U256::from(timestamp / 1000); + + ensure!(deadline >= timestamp, revert("Permit expired")); + + let nonce = NoncesStorage::get(from); + + let permit = Self::generate_permit( + handle.context().address, + from, + to, + value, + data.clone(), + gas_limit, + nonce, + deadline, + ); + + let mut sig = [0u8; 65]; + sig[0..32].copy_from_slice(&r.as_bytes()); + sig[32..64].copy_from_slice(&s.as_bytes()); + sig[64] = v; + + let signer = sp_io::crypto::secp256k1_ecdsa_recover(&sig, &permit) + .map_err(|_| revert("Invalid permit"))?; + let signer = H160::from(H256::from_slice(keccak_256(&signer).as_slice())); + + ensure!( + signer != H160::zero() && signer == from, + revert("Invalid permit") + ); + + NoncesStorage::insert(from, nonce + U256::one()); + + // DISPATCH CALL + let sub_context = Context { + caller: from, + address: to.clone(), + apparent_value: value, + }; + + let transfer = if value.is_zero() { + None + } else { + Some(Transfer { + source: from, + target: to.clone(), + value, + }) + }; + + let (reason, output) = + handle.call(to, transfer, data, Some(gas_limit), false, &sub_context); + match reason { + ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }), + ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }), + ExitReason::Revert(_) => Err(PrecompileFailure::Revert { + exit_status: ExitRevert::Reverted, + output, + }), + ExitReason::Succeed(_) => Ok(output.into()), + } + } + + #[precompile::public("nonces(address)")] + #[precompile::view] + fn nonces(handle: &mut impl PrecompileHandle, owner: Address) -> EvmResult { + // NoncesStorage: Blake2_128(16) + contract(20) + Blake2_128(16) + owner(20) + nonce(32) + handle.record_db_read::(104)?; + + let owner: H160 = owner.into(); + + let nonce = NoncesStorage::get(owner); + + Ok(nonce) + } + + #[precompile::public("DOMAIN_SEPARATOR()")] + #[precompile::view] + fn domain_separator(handle: &mut impl PrecompileHandle) -> EvmResult { + // ChainId + handle.record_db_read::(8)?; + + let domain_separator: H256 = + Self::compute_domain_separator(handle.context().address).into(); + + Ok(domain_separator) + } +} diff --git a/precompiles/call-permit/src/mock.rs b/precompiles/call-permit/src/mock.rs new file mode 100644 index 00000000..d7d900a9 --- /dev/null +++ b/precompiles/call-permit/src/mock.rs @@ -0,0 +1,193 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Test utilities +use super::*; + +use frame_support::traits::Everything; +use frame_support::{construct_runtime, pallet_prelude::*, parameter_types}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot}; +use precompile_utils::{mock_account, precompile_set::*, testing::MockAccount}; +use sp_core::H256; +use sp_runtime::BuildStorage; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +pub type AccountId = MockAccount; +pub type Balance = u128; + +type Block = frame_system::mocking::MockBlockU32; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const MaximumBlockWeight: Weight = Weight::from_parts(1024, 1); + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); + pub const SS58Prefix: u8 = 42; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = [u8; 4]; + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +mock_account!(CallPermit, |_| MockAccount::from_u64(1)); +mock_account!(Revert, |_| MockAccount::from_u64(2)); + +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileAt, CallPermitPrecompile, SubcallWithMaxNesting<0>>, + RevertPrecompile>, + ), +>; + +pub type PCall = CallPermitPrecompileCall; + +parameter_types! { + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub const SuicideQuickClearLimit: u32 = 0; +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = pallet_evm::FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesType = Precompiles; + type PrecompilesValue = PrecompilesValue; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = (); + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = (); + type SuicideQuickClearLimit = SuicideQuickClearLimit; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + System::set_block_number(1); + pallet_evm::Pallet::::create_account( + Revert.into(), + hex_literal::hex!("1460006000fd").to_vec(), + ); + }); + ext + } +} + +// pub fn balance(account: impl Into) -> Balance { +// pallet_balances::Pallet::::usable_balance(account.into()) +// } diff --git a/precompiles/call-permit/src/tests.rs b/precompiles/call-permit/src/tests.rs new file mode 100644 index 00000000..c720d193 --- /dev/null +++ b/precompiles/call-permit/src/tests.rs @@ -0,0 +1,676 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use crate::{ + mock::{CallPermit, ExtBuilder, PCall, Precompiles, PrecompilesValue, Runtime}, + CallPermitPrecompile, +}; +use libsecp256k1::{sign, Message, SecretKey}; +use precompile_utils::{ + evm::costs::call_cost, prelude::*, solidity::revert::revert_as_bytes, testing::*, +}; +use sp_core::{H160, H256, U256}; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +fn dispatch_cost() -> u64 { + CallPermitPrecompile::::dispatch_inherent_cost() +} + +#[test] +fn selectors() { + assert!(PCall::dispatch_selectors().contains(&0xb5ea0966)); + assert!(PCall::nonces_selectors().contains(&0x7ecebe00)); + assert!(PCall::domain_separator_selectors().contains(&0x3644e515)); +} + +#[test] +fn modifiers() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let mut tester = PrecompilesModifierTester::new(precompiles(), CryptoAlith, CallPermit); + + tester.test_default_modifier(PCall::dispatch_selectors()); + tester.test_view_modifier(PCall::nonces_selectors()); + tester.test_view_modifier(PCall::domain_separator_selectors()); + }); +} + +#[test] +fn valid_permit_returns() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let from: H160 = CryptoAlith.into(); + let to: H160 = Bob.into(); + let value: U256 = 42u8.into(); + let data: Vec = b"Test".to_vec(); + let gas_limit = 100_000u64; + let nonce: U256 = 0u8.into(); + let deadline: U256 = 1_000u32.into(); + let permit = CallPermitPrecompile::::generate_permit( + CallPermit.into(), + from, + to, + value, + data.clone(), + gas_limit, + nonce, + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + CallPermit, + PCall::nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + let call_cost = call_cost(value, ::config()); + + precompiles() + .prepare_test( + Charlie, // can be anyone + CallPermit, + PCall::dispatch { + from: Address(from), + to: Address(to), + value, + data: data.into(), + gas_limit, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called on the behalf of the permit maker. + assert_eq!(context.caller, CryptoAlith.into()); + assert_eq!(address, Bob.into()); + assert_eq!(is_static, false); + assert_eq!(target_gas, Some(100_000), "forward requested gas"); + + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, CryptoAlith.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 42u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 42u8.into()); + + assert_eq!(&input, b"Test"); + + SubcallOutput { + output: b"TEST".to_vec(), + cost: 13, + logs: vec![log1(Bob, H256::repeat_byte(0x11), vec![])], + ..SubcallOutput::succeed() + } + }) + .with_target_gas(Some(call_cost + 100_000 + dispatch_cost())) + .expect_cost(call_cost + 13 + dispatch_cost()) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .execute_returns(UnboundedBytes::from(b"TEST")); + }) +} + +#[test] +fn valid_permit_reverts() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let from: H160 = CryptoAlith.into(); + let to: H160 = Bob.into(); + let value: U256 = 42u8.into(); + let data: Vec = b"Test".to_vec(); + let gas_limit = 100_000u64; + let nonce: U256 = 0u8.into(); + let deadline: U256 = 1_000u32.into(); + + let permit = CallPermitPrecompile::::generate_permit( + CallPermit.into(), + from, + to, + value, + data.clone(), + gas_limit, + nonce, + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + CallPermit, + PCall::nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + let call_cost = call_cost(value, ::config()); + + precompiles() + .prepare_test( + Charlie, // can be anyone + CallPermit, + PCall::dispatch { + from: Address(from), + to: Address(to), + value, + data: data.into(), + gas_limit, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called on the behalf of the permit maker. + assert_eq!(context.caller, CryptoAlith.into()); + assert_eq!(address, Bob.into()); + assert_eq!(is_static, false); + assert_eq!(target_gas, Some(100_000), "forward requested gas"); + + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, CryptoAlith.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 42u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 42u8.into()); + + assert_eq!(&input, b"Test"); + + SubcallOutput { + output: revert_as_bytes("TEST"), + cost: 13, + ..SubcallOutput::revert() + } + }) + .with_target_gas(Some(call_cost + 100_000 + dispatch_cost())) + .expect_cost(call_cost + 13 + dispatch_cost()) + .expect_no_logs() + .execute_reverts(|x| x == b"TEST".to_vec()); + }) +} + +#[test] +fn invalid_permit_nonce() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let from: H160 = CryptoAlith.into(); + let to: H160 = Bob.into(); + let value: U256 = 42u8.into(); + let data: Vec = b"Test".to_vec(); + let gas_limit = 100_000u64; + let nonce: U256 = 1u8.into(); // WRONG NONCE + let deadline: U256 = 1_000u32.into(); + + let permit = CallPermitPrecompile::::generate_permit( + CallPermit.into(), + from, + to, + value, + data.clone(), + gas_limit, + nonce, + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + CallPermit, + PCall::nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + let call_cost = call_cost(value, ::config()); + + precompiles() + .prepare_test( + Charlie, // can be anyone + CallPermit, + PCall::dispatch { + from: Address(from), + to: Address(to), + value, + data: data.into(), + gas_limit, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .with_subcall_handle(move |_| panic!("should not perform subcall")) + .with_target_gas(Some(call_cost + 100_000 + dispatch_cost())) + .expect_cost(dispatch_cost()) + .execute_reverts(|x| x == b"Invalid permit"); + }) +} + +#[test] +fn invalid_permit_gas_limit_too_low() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let from: H160 = CryptoAlith.into(); + let to: H160 = Bob.into(); + let value: U256 = 42u8.into(); + let data: Vec = b"Test".to_vec(); + let gas_limit = 100_000u64; + let nonce: U256 = 0u8.into(); + let deadline: U256 = 1_000u32.into(); + + let permit = CallPermitPrecompile::::generate_permit( + CallPermit.into(), + from, + to, + value, + data.clone(), + gas_limit, + nonce, + deadline, + ); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + CallPermit, + PCall::nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + let call_cost = call_cost(value, ::config()); + + precompiles() + .prepare_test( + Charlie, // can be anyone + CallPermit, + PCall::dispatch { + from: Address(from), + to: Address(to), + value, + data: data.into(), + gas_limit, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .with_subcall_handle(move |_| panic!("should not perform subcall")) + .with_target_gas(Some(call_cost + 99_999 + dispatch_cost())) + .expect_cost(dispatch_cost()) + .execute_reverts(|x| x == b"Gaslimit is too low to dispatch provided call"); + }) +} + +#[test] +fn invalid_permit_gas_limit_overflow() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000)]) + .build() + .execute_with(|| { + let from: H160 = CryptoAlith.into(); + let to: H160 = Bob.into(); + let value: U256 = 42u8.into(); + let data: Vec = b"Test".to_vec(); + let gas_limit = u64::MAX; + let nonce: U256 = 0u8.into(); + let deadline: U256 = 1_000u32.into(); + + let permit = CallPermitPrecompile::::generate_permit( + CallPermit.into(), + from, + to, + value, + data.clone(), + gas_limit, + nonce, + deadline, + ); + + dbg!(H256::from(permit)); + + let secret_key = SecretKey::parse(&alith_secret_key()).unwrap(); + let message = Message::parse(&permit); + let (rs, v) = sign(&message, &secret_key); + + precompiles() + .prepare_test( + CryptoAlith, + CallPermit, + PCall::nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + precompiles() + .prepare_test( + Charlie, // can be anyone + CallPermit, + PCall::dispatch { + from: Address(from), + to: Address(to), + value, + data: data.into(), + gas_limit, + deadline, + v: v.serialize(), + r: H256::from(rs.r.b32()), + s: H256::from(rs.s.b32()), + }, + ) + .with_subcall_handle(move |_| panic!("should not perform subcall")) + .with_target_gas(Some(100_000 + dispatch_cost())) + .expect_cost(dispatch_cost()) + .execute_reverts(|x| x == b"Call require too much gas (uint64 overflow)"); + }) +} + +// // This test checks the validity of a metamask signed message against the permit precompile +// // The code used to generate the signature is the following. +// // You will need to import CryptoAlith_PRIV_KEY in metamask. +// // If you put this code in the developer tools console, it will log the signature + +// await window.ethereum.enable(); +// const accounts = await window.ethereum.request({ method: "eth_requestAccounts" }); + +// const from = accounts[0]; +// const to = "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +// const value = 42; +// const data = "0xdeadbeef"; +// const gaslimit = 100000; +// const nonce = 0; +// const deadline = 1000; + +// const createPermitMessageData = function () { +// const message = { +// from: from, +// to: to, +// value: value, +// data: data, +// gaslimit: gaslimit, +// nonce: nonce, +// deadline: deadline, +// }; + +// const typedData = JSON.stringify({ +// types: { +// EIP712Domain: [ +// { +// name: "name", +// type: "string", +// }, +// { +// name: "version", +// type: "string", +// }, +// { +// name: "chainId", +// type: "uint256", +// }, +// { +// name: "verifyingContract", +// type: "address", +// }, +// ], +// CallPermit: [ +// { +// name: "from", +// type: "address", +// }, +// { +// name: "to", +// type: "address", +// }, +// { +// name: "value", +// type: "uint256", +// }, +// { +// name: "data", +// type: "bytes", +// }, +// { +// name: "gaslimit", +// type: "uint64", +// }, +// { +// name: "nonce", +// type: "uint256", +// }, +// { +// name: "deadline", +// type: "uint256", +// }, +// ], +// }, +// primaryType: "CallPermit", +// domain: { +// name: "Call Permit CallPermit", +// version: "1", +// chainId: 0, +// verifyingContract: "0x0000000000000000000000000000000000000001", +// }, +// message: message, +// }); + +// return { +// typedData, +// message, +// }; +// }; + +// const method = "eth_signTypedData_v4" +// const messageData = createPermitMessageData(); +// const params = [from, messageData.typedData]; + +// web3.currentProvider.sendAsync( +// { +// method, +// params, +// from, +// }, +// function (err, result) { +// if (err) return console.dir(err); +// if (result.error) { +// alert(result.error.message); +// } +// if (result.error) return console.error('ERROR', result); +// console.log('TYPED SIGNED:' + JSON.stringify(result.result)); + +// const recovered = sigUtil.recoverTypedSignature_v4({ +// data: JSON.parse(msgParams), +// sig: result.result, +// }); + +// if ( +// ethUtil.toChecksumAddress(recovered) === ethUtil.toChecksumAddress(from) +// ) { +// alert('Successfully recovered signer as ' + from); +// } else { +// alert( +// 'Failed to verify signer when comparing ' + result + ' to ' + from +// ); +// } +// } +// ); +#[test] +fn valid_permit_returns_with_metamask_signed_data() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 2000)]) + .build() + .execute_with(|| { + let from: H160 = CryptoAlith.into(); + let to: H160 = Bob.into(); + let value: U256 = 42u8.into(); + let data: Vec = hex_literal::hex!("deadbeef").to_vec(); + let gas_limit = 100_000u64; + let deadline: U256 = 1_000u32.into(); + + // Made with MetaMask + let rsv = hex_literal::hex!( + "56b497d556cb1b57a16aac6e8d53f3cbf1108df467ffcb937a3744369a27478f608de05 + 34b8e0385e55ffd97cbafcfeac12ab52d0b74a2dea582bc8de46f257d1c" + ) + .as_slice(); + let (r, sv) = rsv.split_at(32); + let (s, v) = sv.split_at(32); + let v_real = v[0]; + let r_real: [u8; 32] = r.try_into().unwrap(); + let s_real: [u8; 32] = s.try_into().unwrap(); + + precompiles() + .prepare_test( + CryptoAlith, + CallPermit, + PCall::nonces { + owner: Address(CryptoAlith.into()), + }, + ) + .expect_cost(0) // TODO: Test db read/write costs + .expect_no_logs() + .execute_returns(U256::from(0u8)); + + let call_cost = call_cost(value, ::config()); + + precompiles() + .prepare_test( + Charlie, // can be anyone + CallPermit, + PCall::dispatch { + from: Address(from), + to: Address(to), + value, + data: data.clone().into(), + gas_limit, + deadline, + v: v_real, + r: r_real.into(), + s: s_real.into(), + }, + ) + .with_subcall_handle(move |subcall| { + let Subcall { + address, + transfer, + input, + target_gas, + is_static, + context, + } = subcall; + + // Called on the behalf of the permit maker. + assert_eq!(context.caller, CryptoAlith.into()); + assert_eq!(address, Bob.into()); + assert_eq!(is_static, false); + assert_eq!(target_gas, Some(100_000), "forward requested gas"); + + let transfer = transfer.expect("there is a transfer"); + assert_eq!(transfer.source, CryptoAlith.into()); + assert_eq!(transfer.target, Bob.into()); + assert_eq!(transfer.value, 42u8.into()); + + assert_eq!(context.address, Bob.into()); + assert_eq!(context.apparent_value, 42u8.into()); + + assert_eq!(&input, &data); + + SubcallOutput { + output: b"TEST".to_vec(), + cost: 13, + logs: vec![log1(Bob, H256::repeat_byte(0x11), vec![])], + ..SubcallOutput::succeed() + } + }) + .with_target_gas(Some(call_cost + 100_000 + dispatch_cost())) + .expect_cost(call_cost + 13 + dispatch_cost()) + .expect_log(log1(Bob, H256::repeat_byte(0x11), vec![])) + .execute_returns(UnboundedBytes::from(b"TEST")); + }) +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces(&["CallPermit.sol"], PCall::supports_selector) +} diff --git a/precompiles/xcm-utils/Cargo.toml b/precompiles/xcm-utils/Cargo.toml new file mode 100644 index 00000000..bf76d169 --- /dev/null +++ b/precompiles/xcm-utils/Cargo.toml @@ -0,0 +1,74 @@ +[package] +name = "pallet-evm-precompile-xcm-utils" +authors = { workspace = true } +description = "A Precompile to make xcm utilities accessible to pallet-evm" +edition = "2021" +version = "0.1.0" + +[dependencies] +num_enum = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = [ "codec-xcm" ] } +xcm-primitives = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +parity-scale-codec = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-weights = { workspace = true } + +# Frontier +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +# Polkadot +pallet-xcm = { workspace = true } +xcm = { workspace = true } +xcm-executor = { workspace = true } + +[dev-dependencies] +derive_more = { workspace = true } +serde = { workspace = true } +sha3 = { workspace = true } + +precompile-utils = { workspace = true, features = [ "testing", "codec-xcm" ] } + +# Substrate +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] } +scale-info = { workspace = true, features = [ "derive" ] } +sp-io = { workspace = true } +sp-runtime = { workspace = true } + +# Cumulus +cumulus-primitives-core = { workspace = true } + +# Polkadot +xcm-builder = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "precompile-utils/std", + "sp-core/std", + "sp-io/std", + "sp-std/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm-primitives/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] diff --git a/precompiles/xcm-utils/XcmUtils.sol b/precompiles/xcm-utils/XcmUtils.sol new file mode 100644 index 00000000..34e8c00a --- /dev/null +++ b/precompiles/xcm-utils/XcmUtils.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title Xcm Utils Interface +/// The interface through which solidity contracts will interact with xcm utils pallet +interface XcmUtils { + // A multilocation is defined by its number of parents and the encoded junctions (interior) + struct Multilocation { + uint8 parents; + bytes[] interior; + } + + /// Get retrieve the account associated to a given MultiLocation + /// @custom:selector 343b3e00 + /// @param multilocation The multilocation that we want to know to which account maps to + /// @return account The account the multilocation maps to in this chain + function multilocationToAddress(Multilocation memory multilocation) + external + view + returns (address account); + + /// Get the weight that a message will consume in our chain + /// @custom:selector 25d54154 + /// @param message scale encoded xcm mversioned xcm message + function weightMessage(bytes memory message) + external + view + returns (uint64 weight); + + /// Get units per second charged for a given multilocation + /// @custom:selector 3f0f65db + /// @param multilocation scale encoded xcm mversioned xcm message + function getUnitsPerSecond(Multilocation memory multilocation) + external + view + returns (uint256 unitsPerSecond); + + /// Execute custom xcm message + /// @dev This function CANNOT be called from a smart contract + /// @custom:selector 34334a02 + /// @param message The versioned message to be executed scale encoded + /// @param maxWeight The maximum weight to be consumed + function xcmExecute(bytes memory message, uint64 maxWeight) external; + + /// Send custom xcm message + /// @custom:selector 98600e64 + /// @param dest The destination chain to which send this message + /// @param message The versioned message to be sent scale-encoded + function xcmSend(Multilocation memory dest, bytes memory message) external; +} diff --git a/precompiles/xcm-utils/src/lib.rs b/precompiles/xcm-utils/src/lib.rs new file mode 100644 index 00000000..76cf0e55 --- /dev/null +++ b/precompiles/xcm-utils/src/lib.rs @@ -0,0 +1,259 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Precompile to xcm utils runtime methods via the EVM + +#![cfg_attr(not(feature = "std"), no_std)] + +use fp_evm::PrecompileHandle; +use frame_support::traits::ConstU32; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + traits::OriginTrait, +}; +use pallet_evm::AddressMapping; +use parity_scale_codec::{Decode, DecodeLimit, MaxEncodedLen}; +use precompile_utils::precompile_set::SelectorFilter; +use precompile_utils::prelude::*; +use sp_core::{H160, U256}; +use sp_runtime::traits::Dispatchable; +use sp_std::boxed::Box; +use sp_std::marker::PhantomData; +use sp_std::vec; +use sp_std::vec::Vec; +use sp_weights::Weight; +use xcm::{latest::prelude::*, VersionedXcm, MAX_XCM_DECODE_DEPTH}; +use xcm_executor::traits::ConvertOrigin; +use xcm_executor::traits::WeightBounds; +use xcm_executor::traits::WeightTrader; + +const DEFAULT_PROOF_SIZE: u64 = 256 * 1024; + +pub type XcmOriginOf = + <::RuntimeCall as Dispatchable>::RuntimeOrigin; +pub type XcmAccountIdOf = + <<::RuntimeCall as Dispatchable> + ::RuntimeOrigin as OriginTrait>::AccountId; + +pub type CallOf = ::RuntimeCall; +pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16); +type GetXcmSizeLimit = ConstU32; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[derive(Debug)] +pub struct AllExceptXcmExecute(PhantomData<(Runtime, XcmConfig)>); + +impl SelectorFilter for AllExceptXcmExecute +where + Runtime: pallet_evm::Config + frame_system::Config + pallet_xcm::Config, + XcmOriginOf: OriginTrait, + XcmAccountIdOf: Into, + XcmConfig: xcm_executor::Config, + ::RuntimeCall: + Dispatchable + Decode + GetDispatchInfo, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::RuntimeCall: From>, +{ + fn is_allowed(_caller: H160, selector: Option) -> bool { + match selector { + None => true, + Some(selector) => { + !XcmUtilsPrecompileCall::::xcm_execute_selectors() + .contains(&selector) + } + } + } + + fn description() -> String { + "Allowed for all callers for all selectors except 'execute'".into() + } +} + +/// A precompile to wrap the functionality from xcm-utils +pub struct XcmUtilsPrecompile(PhantomData<(Runtime, XcmConfig)>); + +#[precompile_utils::precompile] +impl XcmUtilsPrecompile +where + Runtime: pallet_evm::Config + frame_system::Config + pallet_xcm::Config, + XcmOriginOf: OriginTrait, + XcmAccountIdOf: Into, + XcmConfig: xcm_executor::Config, + ::RuntimeCall: + Dispatchable + Decode + GetDispatchInfo, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::RuntimeCall: From>, +{ + #[precompile::public("multilocationToAddress((uint8,bytes[]))")] + #[precompile::view] + fn multilocation_to_address( + handle: &mut impl PrecompileHandle, + multilocation: MultiLocation, + ) -> EvmResult
{ + // storage item: AssetTypeUnitsPerSecond + // max encoded len: hash (16) + Multilocation + u128 (16) + handle.record_db_read::(32 + MultiLocation::max_encoded_len())?; + + let origin = + XcmConfig::OriginConverter::convert_origin(multilocation, OriginKind::SovereignAccount) + .map_err(|_| { + RevertReason::custom("Failed multilocation conversion") + .in_field("multilocation") + })?; + + let account: H160 = origin + .into_signer() + .ok_or( + RevertReason::custom("Failed multilocation conversion").in_field("multilocation"), + )? + .into(); + Ok(Address(account)) + } + + #[precompile::public("getUnitsPerSecond((uint8,bytes[]))")] + #[precompile::view] + fn get_units_per_second( + handle: &mut impl PrecompileHandle, + multilocation: MultiLocation, + ) -> EvmResult { + // storage item: AssetTypeUnitsPerSecond + // max encoded len: hash (16) + Multilocation + u128 (16) + handle.record_db_read::(32 + MultiLocation::max_encoded_len())?; + + // We will construct an asset with the max amount, and check how much we + // get in return to substract + let multiasset: xcm::latest::MultiAsset = (multilocation.clone(), u128::MAX).into(); + let weight_per_second = 1_000_000_000_000u64; + + let mut trader = ::Trader::new(); + + let ctx = XcmContext { + origin: Some(multilocation), + message_id: XcmHash::default(), + topic: None, + }; + // buy_weight returns unused assets + let unused = trader + .buy_weight( + Weight::from_parts(weight_per_second, DEFAULT_PROOF_SIZE), + vec![multiasset.clone()].into(), + &ctx, + ) + .map_err(|_| { + RevertReason::custom("Asset not supported as fee payment").in_field("multilocation") + })?; + + // we just need to substract from u128::MAX the unused assets + if let Some(amount) = unused + .fungible + .get(&multiasset.id) + .map(|&value| u128::MAX.saturating_sub(value)) + { + Ok(amount.into()) + } else { + Err(revert( + "Weight was too expensive to be bought with this asset", + )) + } + } + + #[precompile::public("weightMessage(bytes)")] + #[precompile::view] + fn weight_message( + _handle: &mut impl PrecompileHandle, + message: BoundedBytes, + ) -> EvmResult { + let message: Vec = message.into(); + + let msg = + VersionedXcm::<::RuntimeCall>::decode_all_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut message.as_slice(), + ) + .map(Xcm::<::RuntimeCall>::try_from); + + let result = match msg { + Ok(Ok(mut x)) => { + XcmConfig::Weigher::weight(&mut x).map_err(|_| revert("failed weighting")) + } + _ => Err(RevertReason::custom("Failed decoding") + .in_field("message") + .into()), + }; + + Ok(result?.ref_time()) + } + + #[precompile::public("xcmExecute(bytes,uint64)")] + fn xcm_execute( + handle: &mut impl PrecompileHandle, + message: BoundedBytes, + weight: u64, + ) -> EvmResult { + let message: Vec = message.into(); + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let message: Vec<_> = message.to_vec(); + let xcm = xcm::VersionedXcm::>::decode_all_with_depth_limit( + xcm::MAX_XCM_DECODE_DEPTH, + &mut message.as_slice(), + ) + .map_err(|_e| RevertReason::custom("Failed xcm decoding").in_field("message"))?; + + let call = pallet_xcm::Call::::execute { + message: Box::new(xcm), + max_weight: Weight::from_parts(weight, DEFAULT_PROOF_SIZE), + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public("xcmSend((uint8,bytes[]),bytes)")] + fn xcm_send( + handle: &mut impl PrecompileHandle, + dest: MultiLocation, + message: BoundedBytes, + ) -> EvmResult { + let message: Vec = message.into(); + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + + let message: Vec<_> = message.to_vec(); + let xcm = xcm::VersionedXcm::<()>::decode_all_with_depth_limit( + xcm::MAX_XCM_DECODE_DEPTH, + &mut message.as_slice(), + ) + .map_err(|_e| RevertReason::custom("Failed xcm decoding").in_field("message"))?; + + let call = pallet_xcm::Call::::send { + dest: Box::new(dest.into()), + message: Box::new(xcm), + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } +} diff --git a/precompiles/xcm-utils/src/mock.rs b/precompiles/xcm-utils/src/mock.rs new file mode 100644 index 00000000..98c65bef --- /dev/null +++ b/precompiles/xcm-utils/src/mock.rs @@ -0,0 +1,483 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Test utilities +use super::*; +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU32, EnsureOrigin, Everything, Nothing, OriginTrait, PalletInfo as _}, + weights::{RuntimeDbWeight, Weight}, +}; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot, GasWeightMapping}; +use precompile_utils::{ + mock_account, + precompile_set::*, + testing::{AddressInPrefixedSet, MockAccount}, +}; +use sp_core::{H256, U256}; +use sp_io; +use sp_runtime::traits::{BlakeTwo256, IdentityLookup, TryConvert}; +use sp_runtime::BuildStorage; +use sp_std::borrow::Borrow; +use xcm::latest::Error as XcmError; +use xcm_builder::AllowUnpaidExecutionFrom; +use xcm_builder::FixedWeightBounds; +use xcm_builder::IsConcrete; +use xcm_builder::SovereignSignedViaLocation; +use xcm_executor::{ + traits::{ConvertLocation, TransactAsset, WeightTrader}, + Assets, +}; +use Junctions::Here; + +pub type AccountId = MockAccount; +pub type Balance = u128; + +type Block = frame_system::mocking::MockBlockU32; + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + PolkadotXcm: pallet_xcm, + } +); + +mock_account!(SelfReserveAccount, |_| MockAccount::from_u64(2)); +mock_account!(ParentAccount, |_| MockAccount::from_u64(3)); +// use simple encoding for parachain accounts. +mock_account!( + SiblingParachainAccount(u32), + |v: SiblingParachainAccount| { AddressInPrefixedSet(0xffffffff, v.0 as u128).into() } +); + +use frame_system::RawOrigin as SystemRawOrigin; +use xcm::latest::Junction; +pub struct MockAccountToAccountKey20(PhantomData<(Origin, AccountId)>); + +impl> TryConvert + for MockAccountToAccountKey20 +where + Origin::PalletsOrigin: From> + + TryInto, Error = Origin::PalletsOrigin>, +{ + fn try_convert(o: Origin) -> Result { + o.try_with_caller(|caller| match caller.try_into() { + Ok(SystemRawOrigin::Signed(who)) => { + let account_h160: H160 = who.into(); + Ok(Junction::AccountKey20 { + network: None, + key: account_h160.into(), + } + .into()) + } + Ok(other) => Err(other.into()), + Err(other) => Err(other), + }) + } +} + +pub struct MockParentMultilocationToAccountConverter; +impl ConvertLocation for MockParentMultilocationToAccountConverter { + fn convert_location(location: &MultiLocation) -> Option { + match location { + MultiLocation { + parents: 1, + interior: Here, + } => Some(ParentAccount.into()), + _ => None, + } + } +} + +pub struct MockParachainMultilocationToAccountConverter; +impl ConvertLocation for MockParachainMultilocationToAccountConverter { + fn convert_location(location: &MultiLocation) -> Option { + match location.borrow() { + MultiLocation { + parents: 1, + interior: Junctions::X1(Parachain(id)), + } => Some(SiblingParachainAccount(*id).into()), + _ => None, + } + } +} + +pub type LocationToAccountId = ( + MockParachainMultilocationToAccountConverter, + MockParentMultilocationToAccountConverter, + xcm_builder::AccountKey20Aliases, +); + +pub struct AccountIdToMultiLocation; +impl sp_runtime::traits::Convert for AccountIdToMultiLocation { + fn convert(account: AccountId) -> MultiLocation { + let as_h160: H160 = account.into(); + MultiLocation::new( + 0, + Junctions::X1(AccountKey20 { + network: None, + key: as_h160.as_fixed_bytes().clone(), + }), + ) + } +} + +parameter_types! { + pub ParachainId: cumulus_primitives_core::ParaId = 100.into(); + pub LocalNetworkId: Option = None; +} + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; + pub const MockDbWeight: RuntimeDbWeight = RuntimeDbWeight { + read: 1, + write: 5, + }; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = MockDbWeight; + type RuntimeOrigin = RuntimeOrigin; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} + +#[cfg(feature = "runtime-benchmarks")] +parameter_types! { + pub ReachableDest: Option = Some(Parent.into()); +} + +parameter_types! { + pub MatcherLocation: MultiLocation = MultiLocation::here(); +} +pub type LocalOriginToLocation = MockAccountToAccountKey20; +impl pallet_xcm::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmRouter = TestSendXcm; + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = frame_support::traits::Everything; + type XcmExecutor = xcm_executor::XcmExecutor; + // Do not allow teleports + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds; + type UniversalLocation = Ancestry; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + // We use a custom one to test runtime ugprades + type AdvertisedXcmVersion = (); + type Currency = Balances; + type CurrencyMatcher = IsConcrete; + type TrustedLockers = (); + type SovereignAccountOf = (); + type MaxLockers = ConstU32<8>; + type WeightInfo = pallet_xcm::TestWeightInfo; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type AdminOrigin = frame_system::EnsureRoot; + #[cfg(feature = "runtime-benchmarks")] + type ReachableDest = ReachableDest; +} +pub type Precompiles = PrecompileSetBuilder< + R, + ( + PrecompileAt< + AddressU64<1>, + XcmUtilsPrecompile, + CallableByContract>, + >, + ), +>; + +pub type PCall = XcmUtilsPrecompileCall; + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; +} + +/// A mapping function that converts Ethereum gas to Substrate weight +/// We are mocking this 1-1 to test db read charges too +pub struct MockGasWeightMapping; +impl GasWeightMapping for MockGasWeightMapping { + fn gas_to_weight(gas: u64, _without_base_weight: bool) -> Weight { + Weight::from_parts(gas, 1) + } + fn weight_to_gas(weight: Weight) -> u64 { + weight.ref_time().into() + } +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = MockGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesValue = PrecompilesValue; + type PrecompilesType = Precompiles; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} +pub type Barrier = AllowUnpaidExecutionFrom; + +pub struct ConvertOriginToLocal; +impl EnsureOrigin for ConvertOriginToLocal { + type Success = MultiLocation; + + fn try_origin(_: Origin) -> Result { + Ok(MultiLocation::here()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(Origin::root()) + } +} + +use sp_std::cell::RefCell; +use xcm::latest::opaque; +// Simulates sending a XCM message +thread_local! { + pub static SENT_XCM: RefCell> = RefCell::new(Vec::new()); +} +pub fn sent_xcm() -> Vec<(MultiLocation, opaque::Xcm)> { + SENT_XCM.with(|q| (*q.borrow()).clone()) +} +pub struct TestSendXcm; +impl SendXcm for TestSendXcm { + type Ticket = (); + + fn validate( + destination: &mut Option, + message: &mut Option, + ) -> SendResult { + SENT_XCM.with(|q| { + q.borrow_mut() + .push((destination.clone().unwrap(), message.clone().unwrap())) + }); + Ok(((), MultiAssets::new())) + } + + fn deliver(_: Self::Ticket) -> Result { + Ok(XcmHash::default()) + } +} + +pub struct DummyAssetTransactor; +impl TransactAsset for DummyAssetTransactor { + fn deposit_asset( + _what: &MultiAsset, + _who: &MultiLocation, + _context: Option<&XcmContext>, + ) -> XcmResult { + Ok(()) + } + + fn withdraw_asset( + _what: &MultiAsset, + _who: &MultiLocation, + _maybe_context: Option<&XcmContext>, + ) -> Result { + Ok(Assets::default()) + } +} + +pub struct DummyWeightTrader; +impl WeightTrader for DummyWeightTrader { + fn new() -> Self { + DummyWeightTrader + } + + fn buy_weight( + &mut self, + weight: Weight, + payment: Assets, + _context: &XcmContext, + ) -> Result { + let asset_to_charge: MultiAsset = + (MultiLocation::parent(), weight.ref_time() as u128).into(); + let unused = payment + .checked_sub(asset_to_charge) + .map_err(|_| XcmError::TooExpensive)?; + + Ok(unused) + } +} + +parameter_types! { + pub const BaseXcmWeight: Weight = Weight::from_parts(1000u64, 0u64); + pub const RelayNetwork: NetworkId = NetworkId::Polkadot; + + pub SelfLocation: MultiLocation = + MultiLocation::new(1, Junctions::X1(Parachain(ParachainId::get().into()))); + + pub SelfReserve: MultiLocation = MultiLocation::new( + 1, + Junctions::X2( + Parachain(ParachainId::get().into()), + PalletInstance(::PalletInfo::index::().unwrap() as u8) + )); + pub MaxInstructions: u32 = 100; + + pub UniversalLocation: InteriorMultiLocation = Here; + pub Ancestry: InteriorMultiLocation = + X2(GlobalConsensus(RelayNetwork::get()), Parachain(ParachainId::get().into()).into()); + + pub const MaxAssetsIntoHolding: u32 = 64; +} + +pub type XcmOriginToTransactDispatchOrigin = ( + // Sovereign account converter; this attempts to derive an `AccountId` from the origin location + // using `LocationToAccountId` and then turn that into the usual `Signed` origin. Useful for + // foreign chains who want to have a local sovereign account on this chain which they control. + SovereignSignedViaLocation, +); +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = TestSendXcm; + type AssetTransactor = DummyAssetTransactor; + type OriginConverter = XcmOriginToTransactDispatchOrigin; + type IsReserve = (); + type IsTeleporter = (); + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = DummyWeightTrader; + type ResponseHandler = (); + type SubscriptionService = (); + type AssetTrap = (); + type AssetClaims = (); + type CallDispatcher = RuntimeCall; + type AssetLocker = (); + type AssetExchanger = (); + type PalletInstancesInfo = (); + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type FeeManager = (); + type MessageExporter = (); + type UniversalAliases = Nothing; + type SafeCallFilter = Everything; + type Aliasers = Nothing; +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { balances: vec![] } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/precompiles/xcm-utils/src/tests.rs b/precompiles/xcm-utils/src/tests.rs new file mode 100644 index 00000000..51f275e1 --- /dev/null +++ b/precompiles/xcm-utils/src/tests.rs @@ -0,0 +1,263 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use crate::mock::{ + sent_xcm, AccountId, Balances, ExtBuilder, PCall, ParentAccount, Precompiles, PrecompilesValue, + Runtime, SiblingParachainAccount, System, +}; +use frame_support::{traits::PalletInfo, weights::Weight}; +use parity_scale_codec::Encode; +use precompile_utils::{prelude::*, testing::*}; +use sp_core::{H160, U256}; +use xcm::prelude::*; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn test_selector_enum() { + assert!(PCall::multilocation_to_address_selectors().contains(&0x343b3e00)); + assert!(PCall::weight_message_selectors().contains(&0x25d54154)); + assert!(PCall::get_units_per_second_selectors().contains(&0x3f0f65db)); +} + +#[test] +fn modifiers() { + ExtBuilder::default().build().execute_with(|| { + let mut tester = PrecompilesModifierTester::new(precompiles(), Alice, Precompile1); + + tester.test_view_modifier(PCall::multilocation_to_address_selectors()); + tester.test_view_modifier(PCall::weight_message_selectors()); + tester.test_view_modifier(PCall::get_units_per_second_selectors()); + }); +} + +#[test] +fn test_get_account_parent() { + ExtBuilder::default().build().execute_with(|| { + let input = PCall::multilocation_to_address { + multilocation: MultiLocation::parent(), + }; + + let expected_address: H160 = ParentAccount.into(); + + precompiles() + .prepare_test(Alice, Precompile1, input) + .expect_cost(1) + .expect_no_logs() + .execute_returns(Address(expected_address)); + }); +} + +#[test] +fn test_get_account_sibling() { + ExtBuilder::default().build().execute_with(|| { + let input = PCall::multilocation_to_address { + multilocation: MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::Parachain(2000u32)), + }, + }; + + let expected_address: H160 = SiblingParachainAccount(2000u32).into(); + + precompiles() + .prepare_test(Alice, Precompile1, input) + .expect_cost(1) + .expect_no_logs() + .execute_returns(Address(expected_address)); + }); +} + +#[test] +fn test_weight_message() { + ExtBuilder::default().build().execute_with(|| { + let message: Vec = xcm::VersionedXcm::<()>::V3(Xcm(vec![ClearOrigin])).encode(); + + let input = PCall::weight_message { + message: message.into(), + }; + + precompiles() + .prepare_test(Alice, Precompile1, input) + .expect_cost(0) + .expect_no_logs() + .execute_returns(1000u64); + }); +} + +#[test] +fn test_get_units_per_second() { + ExtBuilder::default().build().execute_with(|| { + let input = PCall::get_units_per_second { + multilocation: MultiLocation::parent(), + }; + + precompiles() + .prepare_test(Alice, Precompile1, input) + .expect_cost(1) + .expect_no_logs() + .execute_returns(U256::from(1_000_000_000_000u128)); + }); +} + +#[test] +fn test_executor_clear_origin() { + ExtBuilder::default().build().execute_with(|| { + let xcm_to_execute = VersionedXcm::<()>::V3(Xcm(vec![ClearOrigin])).encode(); + + let input = PCall::xcm_execute { + message: xcm_to_execute.into(), + weight: 10000u64, + }; + + precompiles() + .prepare_test(Alice, Precompile1, input) + .expect_cost(100001001) + .expect_no_logs() + .execute_returns(()); + }) +} + +#[test] +fn test_executor_send() { + ExtBuilder::default().build().execute_with(|| { + let withdrawn_asset: MultiAsset = (MultiLocation::parent(), 1u128).into(); + let xcm_to_execute = VersionedXcm::<()>::V3(Xcm(vec![ + WithdrawAsset(vec![withdrawn_asset].into()), + InitiateReserveWithdraw { + assets: MultiAssetFilter::Wild(All), + reserve: MultiLocation::parent(), + xcm: Xcm(vec![]), + }, + ])) + .encode(); + + let input = PCall::xcm_execute { + message: xcm_to_execute.into(), + weight: 10000u64, + }; + + precompiles() + .prepare_test(Alice, Precompile1, input) + .expect_cost(100002001) + .expect_no_logs() + .execute_returns(()); + + let sent_messages = sent_xcm(); + let (_, sent_message) = sent_messages.first().unwrap(); + // Lets make sure the message is as expected + assert!(sent_message.0.contains(&ClearOrigin)); + }); +} + +#[test] +fn test_executor_transact() { + ExtBuilder::default() + .with_balances(vec![(CryptoAlith.into(), 1000000000)]) + .build() + .execute_with(|| { + let mut encoded: Vec = Vec::new(); + let index = + ::PalletInfo::index::().unwrap() as u8; + + encoded.push(index); + + // Then call bytes + let mut call_bytes = pallet_balances::Call::::transfer_allow_death { + dest: CryptoBaltathar.into(), + value: 100u32.into(), + } + .encode(); + encoded.append(&mut call_bytes); + let xcm_to_execute = VersionedXcm::<()>::V3(Xcm(vec![Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_parts(1_000_000_000u64, 5206u64), + call: encoded.into(), + }])) + .encode(); + + let input = PCall::xcm_execute { + message: xcm_to_execute.into(), + weight: 2_000_000_000u64, + }; + + precompiles() + .prepare_test(CryptoAlith, Precompile1, input) + .expect_cost(1100001001) + .expect_no_logs() + .execute_returns(()); + + // Transact executed + let baltathar_account: AccountId = CryptoBaltathar.into(); + assert_eq!(System::account(baltathar_account).data.free, 100); + }); +} + +#[test] +fn test_send_clear_origin() { + ExtBuilder::default().build().execute_with(|| { + let xcm_to_send = VersionedXcm::<()>::V3(Xcm(vec![ClearOrigin])).encode(); + + let input = PCall::xcm_send { + dest: MultiLocation::parent(), + message: xcm_to_send.into(), + }; + + precompiles() + .prepare_test(CryptoAlith, Precompile1, input) + // Only the cost of TestWeightInfo + .expect_cost(100000000) + .expect_no_logs() + .execute_returns(()); + + let sent_messages = sent_xcm(); + let (_, sent_message) = sent_messages.first().unwrap(); + // Lets make sure the message is as expected + assert!(sent_message.0.contains(&ClearOrigin)); + }) +} + +#[test] +fn execute_fails_if_called_by_smart_contract() { + ExtBuilder::default() + .with_balances(vec![ + (CryptoAlith.into(), 1000), + (CryptoBaltathar.into(), 1000), + ]) + .build() + .execute_with(|| { + // Set code to Alice address as it if was a smart contract. + pallet_evm::AccountCodes::::insert(H160::from(Alice), vec![10u8]); + + let xcm_to_execute = VersionedXcm::<()>::V3(Xcm(vec![ClearOrigin])).encode(); + + let input = PCall::xcm_execute { + message: xcm_to_execute.into(), + weight: 10000u64, + }; + + PrecompilesValue::get() + .prepare_test(Alice, Precompile1, input) + .execute_reverts(|output| output == b"Function not callable by smart contracts"); + }) +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces(&["XcmUtils.sol"], PCall::supports_selector) +}