diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 08f259d7..7911f933 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -242,6 +242,21 @@ jobs: CARGO_INCREMENTAL: "0" SCCACHE_CACHE_SIZE: "100GB" steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@v1.3.0 + with: + # this might remove tools that are actually needed, + # if set to "true" but frees about 6 GB + tool-cache: false + + # all of these default to true, but feel free to set to + # "false" if necessary for your workflow + android: true + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: true - name: Checkout uses: actions/checkout@v3 with: diff --git a/Cargo.lock b/Cargo.lock index 8a31445f..7e671192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -735,6 +735,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" @@ -837,7 +849,7 @@ name = "binary-merkle-tree" version = "4.0.0-dev" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0#f60318f68687e601c47de5ad5ca88e2c3f8139a7" dependencies = [ - "hash-db", + "hash-db 0.16.0", "log", ] @@ -862,7 +874,7 @@ dependencies = [ "lazy_static", "lazycell", "peeking_take_while", - "prettyplease 0.2.6", + "prettyplease 0.2.15", "proc-macro2", "quote", "regex", @@ -1071,6 +1083,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" @@ -1169,6 +1192,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" @@ -2956,12 +2985,115 @@ 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.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89fb87a9e103f71b903b80b670200b54cc67a07578f070681f1fffb7396fb7" +dependencies = [ + "bytes", + "ethereum-types", + "hash-db 0.15.2", + "hash256-std-hasher", + "parity-scale-codec", + "rlp", + "scale-info", + "serde", + "sha3", + "triehash", +] + +[[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.39.1" +source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +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.39.0" +source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +dependencies = [ + "parity-scale-codec", + "primitive-types", + "scale-info", + "serde", +] + +[[package]] +name = "evm-gasometer" +version = "0.39.0" +source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +dependencies = [ + "environmental", + "evm-core", + "evm-runtime", + "primitive-types", +] + +[[package]] +name = "evm-runtime" +version = "0.39.0" +source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" +dependencies = [ + "auto_impl", + "environmental", + "evm-core", + "primitive-types", + "sha3", +] + [[package]] name = "exit-future" version = "0.2.0" @@ -3020,6 +3152,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" @@ -3214,6 +3352,41 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fp-account" +version = "1.0.0-dev" +source = "git+https://github.com/fgamundi/frontier?branch=polkadot-v1.1.0-xcm-codec#11372da1c3ac4e3298c412d5dcd0cc4c5ce0c407" +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/fgamundi/frontier?branch=polkadot-v1.1.0-xcm-codec#11372da1c3ac4e3298c412d5dcd0cc4c5ce0c407" +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" @@ -3818,7 +3991,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", @@ -3879,6 +4052,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + [[package]] name = "hash-db" version = "0.16.0" @@ -4221,6 +4400,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" @@ -5478,7 +5666,7 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "808b50db46293432a45e63bc15ea51e0ab4c0a1647b8eb114e31a3e698dd6fbe" dependencies = [ - "hash-db", + "hash-db 0.16.0", ] [[package]] @@ -6091,6 +6279,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683751d591e6d81200c39fb0d1032608b77724f34114db54f571ff1317b337c0" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c11e44798ad209ccdd91fc192f0526a369a01234f7373e1b141c96d7cee4f0e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.32", +] + [[package]] name = "number_prefix" version = "0.4.0" @@ -6579,6 +6788,148 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-evm" +version = "6.0.0-dev" +source = "git+https://github.com/fgamundi/frontier?branch=polkadot-v1.1.0-xcm-codec#11372da1c3ac4e3298c412d5dcd0cc4c5ce0c407" +dependencies = [ + "environmental", + "evm", + "fp-account", + "fp-evm", + "frame-benchmarking", + "frame-support", + "frame-system", + "hash-db 0.16.0", + "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", + "polkadot-parachain-primitives", + "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" @@ -8911,6 +9262,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/fgamundi/frontier?branch=polkadot-v1.1.0-xcm-codec#11372da1c3ac4e3298c412d5dcd0cc4c5ce0c407" +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/fgamundi/frontier?branch=polkadot-v1.1.0-xcm-codec#11372da1c3ac4e3298c412d5dcd0cc4c5ce0c407" +dependencies = [ + "case", + "num_enum", + "prettyplease 0.2.15", + "proc-macro2", + "quote", + "sp-core-hashing", + "syn 1.0.109", +] + [[package]] name = "predicates" version = "2.1.5" @@ -8965,9 +9360,9 @@ dependencies = [ [[package]] name = "prettyplease" -version = "0.2.6" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b69d39aab54d069e7f2fe8cb970493e7834601ca2d8c65fd7bbd183578080d1" +checksum = "ae005bd773ab59b4725093fd7df83fd7892f7d8eafb48dbd7de6e024e4215f9d" dependencies = [ "proc-macro2", "syn 2.0.32", @@ -8981,6 +9376,7 @@ checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" dependencies = [ "fixed-hash", "impl-codec", + "impl-rlp", "impl-serde", "scale-info", "uint", @@ -9534,6 +9930,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" @@ -10099,7 +10517,7 @@ name = "sc-client-db" version = "0.10.0-dev" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0#f60318f68687e601c47de5ad5ca88e2c3f8139a7" dependencies = [ - "hash-db", + "hash-db 0.16.0", "kvdb", "kvdb-memorydb", "kvdb-rocksdb", @@ -11477,6 +11895,26 @@ dependencies = [ "wide", ] +[[package]] +name = "similar" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aeaf503862c419d66959f5d7ca015337d864e9c49485d771b732e2a20453597" +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" @@ -11498,6 +11936,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" @@ -11697,7 +12147,7 @@ name = "sp-api" version = "4.0.0-dev" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0#f60318f68687e601c47de5ad5ca88e2c3f8139a7" dependencies = [ - "hash-db", + "hash-db 0.16.0", "log", "parity-scale-codec", "scale-info", @@ -11911,7 +12361,7 @@ dependencies = [ "dyn-clonable", "ed25519-zebra 3.1.0", "futures 0.3.28", - "hash-db", + "hash-db 0.16.0", "hash256-std-hasher", "impl-serde", "lazy_static", @@ -12236,7 +12686,7 @@ name = "sp-state-machine" version = "0.28.0" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0#f60318f68687e601c47de5ad5ca88e2c3f8139a7" dependencies = [ - "hash-db", + "hash-db 0.16.0", "log", "parity-scale-codec", "parking_lot 0.12.1", @@ -12349,7 +12799,7 @@ version = "22.0.0" source = "git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.1.0#f60318f68687e601c47de5ad5ca88e2c3f8139a7" dependencies = [ "ahash 0.8.3", - "hash-db", + "hash-db 0.16.0", "hashbrown 0.13.2", "lazy_static", "memory-db", @@ -13444,7 +13894,7 @@ version = "0.27.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "767abe6ffed88a1889671a102c2861ae742726f52e0a5a425b92c9fbfa7e9c85" dependencies = [ - "hash-db", + "hash-db 0.16.0", "hashbrown 0.13.2", "log", "rustc-hex", @@ -13457,7 +13907,17 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4ed310ef5ab98f5fa467900ed906cb9232dd5376597e00fd4cba2a449d06c0b" dependencies = [ - "hash-db", + "hash-db 0.16.0", +] + +[[package]] +name = "triehash" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" +dependencies = [ + "hash-db 0.15.2", + "rlp", ] [[package]] @@ -13645,6 +14105,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" diff --git a/Cargo.toml b/Cargo.toml index 0b8c99dc..669d1751 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,10 @@ members = [ "pallets/migrations", "pallets/maintenance-mode", "pallets/randomness", + "precompiles/balances-erc20", + "precompiles/batch", + "precompiles/call-permit", + "precompiles/xcm-utils", "primitives/nimbus-primitives", "template/node", "template/runtime", @@ -42,6 +46,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.1", default-features = false } +paste = "1.0.6" +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" } @@ -106,6 +115,7 @@ sp-consensus = { git = "https://github.com/paritytech/polkadot-sdk", branch = "r sp-storage = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } sp-timestamp = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } sp-trie = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +sp-weights = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } # Substrate (client) frame-benchmarking-cli = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } @@ -178,6 +188,9 @@ polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot-sd staging-xcm = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } staging-xcm-builder = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } staging-xcm-executor = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +xcm = { package = "staging-xcm", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +xcm-builder = { package = "staging-xcm-builder", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } +xcm-executor = { package = "staging-xcm-executor", git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0", default-features = false } # Polkadot (client) kusama-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } @@ -188,12 +201,19 @@ rococo-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = westend-runtime = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } xcm-simulator = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.1.0" } -# ORML (wasm) -orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "release-polkadot-v1.1.0", default-features = false } -orml-xcm-support = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "release-polkadot-v1.1.0", default-features = false } -orml-xtokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "release-polkadot-v1.1.0", default-features = false } +# Frontier (wasm) +fp-evm = { git = "https://github.com/fgamundi/frontier", branch="polkadot-v1.1.0-xcm-codec", default-features = false } +pallet-evm = { git = "https://github.com/fgamundi/frontier", branch="polkadot-v1.1.0-xcm-codec", default-features = false } +precompile-utils = { git = "https://github.com/fgamundi/frontier", branch="polkadot-v1.1.0-xcm-codec", default-features = false } + +# EVM +evm = { git = "https://github.com/rust-blockchain/evm", rev = "b7b82c7e1fc57b7449d6dfa6826600de37cc1e65", 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 } pallet-author-inherent = { path = "pallets/author-inherent", default-features = false } pallet-author-mapping = { path = "pallets/author-mapping", default-features = false } pallet-author-slot-filter = { path = "pallets/author-slot-filter", default-features = false } diff --git a/pallets/maintenance-mode/src/types.rs b/pallets/maintenance-mode/src/types.rs index 2a3b5238..0cf4c9cf 100644 --- a/pallets/maintenance-mode/src/types.rs +++ b/pallets/maintenance-mode/src/types.rs @@ -23,9 +23,9 @@ use frame_support::{ weights::Weight, }; use frame_system::pallet_prelude::BlockNumberFor as BlockNumberOf; -use sp_std::marker::PhantomData; #[cfg(feature = "try-runtime")] -use sp_std::vec::Vec; +use sp_runtime::TryRuntimeError; +use sp_std::marker::PhantomData; pub struct ExecutiveHooks(PhantomData); @@ -94,20 +94,11 @@ where } #[cfg(feature = "try-runtime")] - fn pre_upgrade() -> Result, sp_runtime::DispatchError> { - if Pallet::::maintenance_mode() { - T::MaintenanceExecutiveHooks::pre_upgrade() - } else { - T::NormalExecutiveHooks::pre_upgrade() - } - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(state: Vec) -> Result<(), sp_runtime::DispatchError> { + fn try_on_runtime_upgrade(checks: bool) -> Result { if Pallet::::maintenance_mode() { - T::MaintenanceExecutiveHooks::post_upgrade(state) + T::MaintenanceExecutiveHooks::try_on_runtime_upgrade(checks) } else { - T::NormalExecutiveHooks::post_upgrade(state) + T::NormalExecutiveHooks::try_on_runtime_upgrade(checks) } } } diff --git a/precompiles/balances-erc20/Cargo.toml b/precompiles/balances-erc20/Cargo.toml new file mode 100644 index 00000000..5c60ed98 --- /dev/null +++ b/precompiles/balances-erc20/Cargo.toml @@ -0,0 +1,58 @@ +[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-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..d23ff1a6 --- /dev/null +++ b/precompiles/balances-erc20/ERC20.sol @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The IERC20 contract's address. +address constant IERC20_ADDRESS = 0x0000000000000000000000000000000000000802; + +/// @dev The IERC20 contract's instance. +IERC20 constant IERC20_CONTRACT = IERC20(IERC20_ADDRESS); + +/// @title ERC20 interface +/// @dev see https://github.com/ethereum/EIPs/issues/20 +/// @dev copied from https://github.com/OpenZeppelin/openzeppelin-contracts +/// @custom:address 0x0000000000000000000000000000000000000802 +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..92801497 --- /dev/null +++ b/precompiles/balances-erc20/src/eip2612.rs @@ -0,0 +1,176 @@ +// 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}; +use sp_core::H256; +use sp_io::hashing::keccak_256; +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 + Metadata: Erc20Metadata, + Instance: InstanceToPrefix + 'static, + Runtime: pallet_balances::Config + pallet_evm::Config + pallet_timestamp::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into, + ::Moment: Into, +{ + 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(); + + // pallet_timestamp is in ms while Ethereum use second timestamps. + let timestamp: U256 = (pallet_timestamp::Pallet::::get()).into() / 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..da9ceba0 --- /dev/null +++ b/precompiles/balances-erc20/src/lib.rs @@ -0,0 +1,493 @@ +// 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 + Metadata: Erc20Metadata, + Instance: InstanceToPrefix + 'static, + Runtime: pallet_balances::Config + pallet_evm::Config + pallet_timestamp::Config, + Runtime::RuntimeCall: Dispatchable + GetDispatchInfo, + Runtime::RuntimeCall: From>, + ::RuntimeOrigin: From>, + BalanceOf: TryFrom + Into, + ::Moment: Into, +{ + #[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 { + 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 { + 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 { + 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..22d45352 --- /dev/null +++ b/precompiles/balances-erc20/src/mock.rs @@ -0,0 +1,213 @@ +// 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::{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 = (); +} + +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 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..e70cf9f8 --- /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 emited. + 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..d4069d1b --- /dev/null +++ b/precompiles/batch/Batch.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The Batch contract's address. +address constant BATCH_ADDRESS = 0x0000000000000000000000000000000000000808; + +/// @dev The Batch contract's instance. +Batch constant BATCH_CONTRACT = Batch(BATCH_ADDRESS); + +/// @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. +/// @custom:address 0x0000000000000000000000000000000000000808 +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..66ad37b7 --- /dev/null +++ b/precompiles/batch/src/mock.rs @@ -0,0 +1,205 @@ +// 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 = (); +} + +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 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..1cc48435 --- /dev/null +++ b/precompiles/call-permit/CallPermit.sol @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The CallPermit contract's address. +address constant CALL_PERMIT_ADDRESS = 0x000000000000000000000000000000000000080a; + +/// @dev The CallPermit contract's instance. +CallPermit constant CALL_PERMIT_CONTRACT = CallPermit(CALL_PERMIT_ADDRESS); + +/// @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 +/// @custom:address 0x000000000000000000000000000000000000080a +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..49719632 --- /dev/null +++ b/precompiles/call-permit/Cargo.toml @@ -0,0 +1,56 @@ +[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-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..9cfe94eb --- /dev/null +++ b/precompiles/call-permit/src/lib.rs @@ -0,0 +1,262 @@ +// 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}, + Blake2_128Concat, +}; +use precompile_utils::{evm::costs::call_cost, prelude::*}; +use sp_core::{H160, H256, U256}; +use sp_io::hashing::keccak_256; +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 + pallet_timestamp::Config, + ::Moment: Into, +{ + 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 + + // pallet_timestamp is in ms while Ethereum use second timestamps. + let timestamp: U256 = (pallet_timestamp::Pallet::::get()).into() / 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..2a9ea17d --- /dev/null +++ b/precompiles/call-permit/src/mock.rs @@ -0,0 +1,190 @@ +// 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 = (); +} + +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); +} + +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 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..174c967d --- /dev/null +++ b/precompiles/xcm-utils/Cargo.toml @@ -0,0 +1,80 @@ +[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 +polkadot-parachain-primitives = { workspace = true } +xcm-builder = { workspace = true } + +# ORML +# orml-traits = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "frame-support/std", + "frame-system/std", + # "orml-traits/std", + "pallet-balances/std", + "pallet-evm/std", + "pallet-timestamp/std", + "parity-scale-codec/std", + "polkadot-parachain-primitives/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..791525c2 --- /dev/null +++ b/precompiles/xcm-utils/XcmUtils.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @dev The XcmUtils contract's address. +address constant XCM_UTILS_ADDRESS = 0x000000000000000000000000000000000000080C; + +/// @dev The XcmUtils contract's instance. +XcmUtils constant XCM_UTILS_CONTRACT = XcmUtils(XCM_UTILS_ADDRESS); + +/// @author The Moonbeam Team +/// @title Xcm Utils Interface +/// The interface through which solidity contracts will interact with xcm utils pallet +/// @custom:address 0x000000000000000000000000000000000000080C +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..b4c3f7bb --- /dev/null +++ b/precompiles/xcm-utils/src/lib.rs @@ -0,0 +1,260 @@ +// 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)] + +extern crate alloc; + +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; + +// use xcm_primitives::DEFAULT_PROOF_SIZE; +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 SystemCallOf = ::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, + SystemCallOf: 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, + SystemCallOf: 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..b500b389 --- /dev/null +++ b/precompiles/xcm-utils/src/mock.rs @@ -0,0 +1,477 @@ +// 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 = (); +} + +#[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 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: &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..211d9504 --- /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 { + 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) +}