diff --git a/Cargo.lock b/Cargo.lock index dad578ba0c1b..3a5a84a672bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2736,6 +2736,7 @@ dependencies = [ "cumulus-pallet-xcmp-queue 0.7.0", "emulated-integration-tests-common", "frame-support 28.0.0", + "hex", "hex-literal", "log", "pallet-asset-conversion 10.0.0", @@ -2747,6 +2748,7 @@ dependencies = [ "pallet-xcm-bridge-hub 0.2.0", "parachains-common 7.0.0", "parity-scale-codec", + "penpal-emulated-chain", "rococo-westend-system-emulated-network", "scale-info", "snowbridge-core 0.2.0", @@ -2829,9 +2831,11 @@ dependencies = [ "serde_json", "snowbridge-beacon-primitives 0.2.0", "snowbridge-core 0.2.0", + "snowbridge-inbound-queue-v2-runtime-api", "snowbridge-outbound-queue-runtime-api 0.2.0", "snowbridge-pallet-ethereum-client 0.2.0", "snowbridge-pallet-inbound-queue 0.2.0", + "snowbridge-pallet-inbound-queue-v2", "snowbridge-pallet-outbound-queue 0.2.0", "snowbridge-pallet-system 0.2.0", "snowbridge-router-primitives 0.9.0", @@ -24733,6 +24737,7 @@ dependencies = [ "frame-system 28.0.0", "hex", "hex-literal", + "log", "parity-scale-codec", "polkadot-parachain-primitives 6.0.0", "scale-info", @@ -24815,6 +24820,18 @@ dependencies = [ "sp-std 14.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "snowbridge-inbound-queue-v2-runtime-api" +version = "0.2.0" +dependencies = [ + "frame-support 28.0.0", + "snowbridge-core 0.2.0", + "snowbridge-router-primitives 0.9.0", + "sp-api 26.0.0", + "sp-runtime 31.0.1", + "staging-xcm 7.0.0", +] + [[package]] name = "snowbridge-milagro-bls" version = "1.5.4" @@ -25040,6 +25057,48 @@ dependencies = [ "sp-std 14.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "snowbridge-pallet-inbound-queue-fixtures-v2" +version = "0.10.0" +dependencies = [ + "hex-literal", + "snowbridge-beacon-primitives 0.2.0", + "snowbridge-core 0.2.0", + "sp-core 28.0.0", + "sp-std 14.0.0", +] + +[[package]] +name = "snowbridge-pallet-inbound-queue-v2" +version = "0.2.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "frame-benchmarking 28.0.0", + "frame-support 28.0.0", + "frame-system 28.0.0", + "hex", + "hex-literal", + "log", + "pallet-balances 28.0.0", + "parity-scale-codec", + "scale-info", + "serde", + "snowbridge-beacon-primitives 0.2.0", + "snowbridge-core 0.2.0", + "snowbridge-pallet-ethereum-client 0.2.0", + "snowbridge-pallet-inbound-queue-fixtures-v2", + "snowbridge-router-primitives 0.9.0", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-keyring 31.0.0", + "sp-runtime 31.0.1", + "sp-std 14.0.0", + "staging-xcm 7.0.0", + "staging-xcm-builder 7.0.0", + "staging-xcm-executor 7.0.0", +] + [[package]] name = "snowbridge-pallet-outbound-queue" version = "0.2.0" @@ -25137,7 +25196,9 @@ dependencies = [ name = "snowbridge-router-primitives" version = "0.9.0" dependencies = [ + "alloy-sol-types", "frame-support 28.0.0", + "frame-system 28.0.0", "hex-literal", "log", "parity-scale-codec", @@ -25148,6 +25209,7 @@ dependencies = [ "sp-runtime 31.0.1", "sp-std 14.0.0", "staging-xcm 7.0.0", + "staging-xcm-builder 7.0.0", "staging-xcm-executor 7.0.0", ] diff --git a/Cargo.toml b/Cargo.toml index 383fc46c4e76..8e2e7a78b31e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,9 @@ members = [ "bridges/snowbridge/pallets/ethereum-client/fixtures", "bridges/snowbridge/pallets/inbound-queue", "bridges/snowbridge/pallets/inbound-queue/fixtures", + "bridges/snowbridge/pallets/inbound-queue-v2", + "bridges/snowbridge/pallets/inbound-queue-v2/fixtures", + "bridges/snowbridge/pallets/inbound-queue-v2/runtime-api", "bridges/snowbridge/pallets/outbound-queue", "bridges/snowbridge/pallets/outbound-queue/merkle-tree", "bridges/snowbridge/pallets/outbound-queue/runtime-api", @@ -1228,6 +1231,9 @@ snowbridge-pallet-ethereum-client = { path = "bridges/snowbridge/pallets/ethereu snowbridge-pallet-ethereum-client-fixtures = { path = "bridges/snowbridge/pallets/ethereum-client/fixtures", default-features = false } snowbridge-pallet-inbound-queue = { path = "bridges/snowbridge/pallets/inbound-queue", default-features = false } snowbridge-pallet-inbound-queue-fixtures = { path = "bridges/snowbridge/pallets/inbound-queue/fixtures", default-features = false } +snowbridge-pallet-inbound-queue-fixtures-v2 = { path = "bridges/snowbridge/pallets/inbound-queue-v2/fixtures", default-features = false } +snowbridge-pallet-inbound-queue-v2 = { path = "bridges/snowbridge/pallets/inbound-queue-v2", default-features = false } +snowbridge-inbound-queue-v2-runtime-api = { path = "bridges/snowbridge/pallets/inbound-queue-v2/runtime-api", default-features = false } snowbridge-pallet-outbound-queue = { path = "bridges/snowbridge/pallets/outbound-queue", default-features = false } snowbridge-pallet-system = { path = "bridges/snowbridge/pallets/system", default-features = false } snowbridge-router-primitives = { path = "bridges/snowbridge/primitives/router", default-features = false } diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml new file mode 100644 index 000000000000..ecebc677e997 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/Cargo.toml @@ -0,0 +1,96 @@ +[package] +name = "snowbridge-pallet-inbound-queue-v2" +description = "Snowbridge Inbound Queue Pallet V2" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +serde = { optional = true, workspace = true, default-features = true } +codec = { features = ["derive"], workspace = true } +scale-info = { features = ["derive"], workspace = true } +hex-literal = { optional = true, workspace = true, default-features = true } +log = { workspace = true } +alloy-primitives = { features = ["rlp"], workspace = true } +alloy-sol-types = { workspace = true } + +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-balances = { workspace = true } +sp-core = { workspace = true } +sp-std = { workspace = true } +sp-io = { workspace = true } +sp-runtime = { workspace = true } + +xcm = { workspace = true } +xcm-executor = { workspace = true } +xcm-builder = { workspace = true } + +snowbridge-core = { workspace = true } +snowbridge-router-primitives = { workspace = true } +snowbridge-beacon-primitives = { workspace = true } +snowbridge-pallet-inbound-queue-fixtures-v2 = { optional = true, workspace = true } + +[dev-dependencies] +frame-benchmarking = { workspace = true, default-features = true } +sp-keyring = { workspace = true, default-features = true } +snowbridge-pallet-ethereum-client = { workspace = true, default-features = true } +hex-literal = { workspace = true, default-features = true } +hex = { workspace = true, default-features = true } + +[features] +default = ["std"] +std = [ + "alloy-primitives/std", + "alloy-sol-types/std", + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-balances/std", + "scale-info/std", + "serde", + "snowbridge-beacon-primitives/std", + "snowbridge-core/std", + "snowbridge-pallet-inbound-queue-fixtures-v2?/std", + "snowbridge-router-primitives/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "xcm-executor/std", + "xcm-builder/std", + "xcm/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "hex-literal", + "pallet-balances/runtime-benchmarks", + "snowbridge-core/runtime-benchmarks", + "snowbridge-pallet-ethereum-client/runtime-benchmarks", + "snowbridge-pallet-inbound-queue-fixtures-v2/runtime-benchmarks", + "snowbridge-router-primitives/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "xcm-executor/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-balances/try-runtime", + "snowbridge-pallet-ethereum-client/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/README.md b/bridges/snowbridge/pallets/inbound-queue-v2/README.md new file mode 100644 index 000000000000..cc2f7c636e68 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/README.md @@ -0,0 +1,3 @@ +# Ethereum Inbound Queue + +Reads messages from Ethereum and sends it to intended destination on Polkadot, using XCM. diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml new file mode 100644 index 000000000000..05a4a473a28a --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "snowbridge-pallet-inbound-queue-fixtures-v2" +description = "Snowbridge Inbound Queue Test Fixtures V2" +version = "0.10.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +hex-literal = { workspace = true, default-features = true } +sp-core = { workspace = true } +sp-std = { workspace = true } +snowbridge-core = { workspace = true } +snowbridge-beacon-primitives = { workspace = true } + +[features] +default = ["std"] +std = [ + "snowbridge-beacon-primitives/std", + "snowbridge-core/std", + "sp-core/std", + "sp-std/std", +] +runtime-benchmarks = [ + "snowbridge-core/runtime-benchmarks", +] diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs new file mode 100644 index 000000000000..00adcdfa186a --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/lib.rs @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +#![cfg_attr(not(feature = "std"), no_std)] + +pub mod register_token; +pub mod send_token; +pub mod send_token_to_penpal; diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/register_token.rs b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/register_token.rs new file mode 100644 index 000000000000..5ab12490d040 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/register_token.rs @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +// Generated, do not edit! +// See ethereum client README.md for instructions to generate + +use hex_literal::hex; +use snowbridge_beacon_primitives::{ + types::deneb, AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader, +}; +use snowbridge_core::inbound::{InboundQueueFixture, Log, Message, Proof}; +use sp_core::U256; +use sp_std::vec; + +pub fn make_register_token_message() -> InboundQueueFixture { + InboundQueueFixture { + message: Message { + event_log: Log { + address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(), + }, + proof: Proof { + receipt_proof: (vec![ + hex!("dccdfceea05036f7b61dcdabadc937945d31e68a8d3dfd4dc85684457988c284").to_vec(), + hex!("4a98e45a319168b0fc6005ce6b744ee9bf54338e2c0784b976a8578d241ced0f").to_vec(), + ], vec![ + hex!("f851a09c01dd6d2d8de951c45af23d3ad00829ce021c04d6c8acbe1612d456ee320d4980808080808080a04a98e45a319168b0fc6005ce6b744ee9bf54338e2c0784b976a8578d241ced0f8080808080808080").to_vec(), + hex!("f9028c30b9028802f90284018301d205b9010000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000080000000000000000000000000000004000000000080000000000000000000000000000000000010100000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000040004000000000000002000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000200000000000010f90179f85894eda338e4dc46038493b885327842fd3e301cab39e1a0f78bb28d4b1d7da699e5c0bc2be29c2b04b5aab6aacf6298fe5304f9db9c6d7ea000000000000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7df9011c94eda338e4dc46038493b885327842fd3e301cab39f863a07153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84fa0c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539a05f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0b8a000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e00a736aa00000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").to_vec(), + ]), + execution_proof: ExecutionProof { + header: BeaconHeader { + slot: 393, + proposer_index: 4, + parent_root: hex!("6545b47a614a1dd4cad042a0cdbbf5be347e8ffcdc02c6c64540d5153acebeef").into(), + state_root: hex!("b62ac34a8cb82497be9542fe2114410c9f6021855b766015406101a1f3d86434").into(), + body_root: hex!("04005fe231e11a5b7b1580cb73b177ae8b338bedd745497e6bb7122126a806db").into(), + }, + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + hex!("6545b47a614a1dd4cad042a0cdbbf5be347e8ffcdc02c6c64540d5153acebeef").into(), + hex!("fa84cc88ca53a72181599ff4eb07d8b444bce023fe2347c3b4f51004c43439d3").into(), + hex!("cadc8ae211c6f2221c9138e829249adf902419c78eb4727a150baa4d9a02cc9d").into(), + hex!("33a89962df08a35c52bd7e1d887cd71fa7803e68787d05c714036f6edf75947c").into(), + hex!("2c9760fce5c2829ef3f25595a703c21eb22d0186ce223295556ed5da663a82cf").into(), + hex!("e1aa87654db79c8a0ecd6c89726bb662fcb1684badaef5cd5256f479e3c622e1").into(), + hex!("aa70d5f314e4a1fbb9c362f3db79b21bf68b328887248651fbd29fc501d0ca97").into(), + hex!("160b6c235b3a1ed4ef5f80b03ee1c76f7bf3f591c92fca9d8663e9221b9f9f0f").into(), + hex!("f68d7dcd6a07a18e9de7b5d2aa1980eb962e11d7dcb584c96e81a7635c8d2535").into(), + hex!("1d5f912dfd6697110dd1ecb5cb8e77952eef57d85deb373572572df62bb157fc").into(), + hex!("ffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b").into(), + hex!("6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220").into(), + hex!("b7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f").into(), + ], + finalized_block_root: hex!("751414cd97c0624f922b3e80285e9f776b08fa22fd5f87391f2ed7ef571a8d46").into(), + }), + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("8092290aa21b7751576440f77edd02a94058429ce50e63a92d620951fb25eda2").into(), + fee_recipient: hex!("0000000000000000000000000000000000000000").into(), + state_root: hex!("96a83e9ddf745346fafcb0b03d57314623df669ed543c110662b21302a0fae8b").into(), + receipts_root: hex!("dccdfceea05036f7b61dcdabadc937945d31e68a8d3dfd4dc85684457988c284").into(), + logs_bloom: hex!("00000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000080000000400000000000000000000004000000000080000000000000000000000000000000000010100000000000000000000000000000000020000000000000000000000000000000000080000000000000000000000000000040004000000000000002002002000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000080000000000000000000000000000000000100000000000000000200000200000010").into(), + prev_randao: hex!("62e309d4f5119d1f5c783abc20fc1a549efbab546d8d0b25ff1cfd58be524e67").into(), + block_number: 393, + gas_limit: 54492273, + gas_used: 199644, + timestamp: 1710552813, + extra_data: hex!("d983010d0b846765746888676f312e32312e368664617277696e").into(), + base_fee_per_gas: U256::from(7u64), + block_hash: hex!("6a9810efb9581d30c1a5c9074f27c68ea779a8c1ae31c213241df16225f4e131").into(), + transactions_root: hex!("2cfa6ed7327e8807c7973516c5c32a68ef2459e586e8067e113d081c3bd8c07d").into(), + withdrawals_root: hex!("792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535").into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + execution_branch: vec![ + hex!("a6833fa629f3286b6916c6e50b8bf089fc9126bee6f64d0413b4e59c1265834d").into(), + hex!("b46f0c01805fe212e15907981b757e6c496b0cb06664224655613dcec82505bb").into(), + hex!("db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").into(), + hex!("d3af7c05c516726be7505239e0b9c7cb53d24abce6b91cdb3b3995f0164a75da").into(), + ], + } + }, + }, + finalized_header: BeaconHeader { + slot: 864, + proposer_index: 4, + parent_root: hex!("614e7672f991ac268cd841055973f55e1e42228831a211adef207bb7329be614").into(), + state_root: hex!("5fa8dfca3d760e4242ab46d529144627aa85348a19173b6e081172c701197a4a").into(), + body_root: hex!("0f34c083b1803666bb1ac5e73fa71582731a2cf37d279ff0a3b0cad5a2ff371e").into(), + }, + block_roots_root: hex!("b9aab9c388c4e4fcd899b71f62c498fc73406e38e8eb14aa440e9affa06f2a10").into(), + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/send_token.rs b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/send_token.rs new file mode 100644 index 000000000000..52da807efd31 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/send_token.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +// Generated, do not edit! +// See ethereum client README.md for instructions to generate + +use hex_literal::hex; +use snowbridge_beacon_primitives::{ + types::deneb, AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader, +}; +use snowbridge_core::inbound::{InboundQueueFixture, Log, Message, Proof}; +use sp_core::U256; +use sp_std::vec; + +pub fn make_send_token_message() -> InboundQueueFixture { + InboundQueueFixture { + message: Message { + event_log: Log { + address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + hex!("c8eaf22f2cb07bac4679df0a660e7115ed87fcfd4e32ac269f6540265bbbd26f").into(), + ], + data: hex!("00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005f00a736aa00000000000187d1f7fdfee7f651fabc8bfcb6e086c278b77a7d008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000064a7b3b6e00d000000000000000000e40b5402000000000000000000000000").into(), + }, + proof: Proof { + receipt_proof: (vec![ + hex!("f9d844c5b79638609ba385b910fec3b5d891c9d7b189f135f0432f33473de915").to_vec(), + ], vec![ + hex!("f90451822080b9044b02f90447018301bcb6b9010000800000000000000000000020000000000000000000004000000000000000000400000000000000000000001000000010000000000000000000000008000000200000000000000001000008000000000000000000000000000000008000080000000000200000000000000000000000000100000000000000000011000000000000020200000000000000000000000000003000000040080008000000000000000000040044000021000000002000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000200800000000000f9033cf89b9487d1f7fdfee7f651fabc8bfcb6e086c278b77a7df863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000090a987b944cb1dcce5564e5fdecd7a54d3de27fea000000000000000000000000057a2d4ff0c3866d96556884bf09fecdd7ccd530ca00000000000000000000000000000000000000000000000000de0b6b3a7640000f9015d94eda338e4dc46038493b885327842fd3e301cab39f884a024c5d2de620c6e25186ae16f6919eba93b6e2c1a33857cc419d9f3a00d6967e9a000000000000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7da000000000000000000000000090a987b944cb1dcce5564e5fdecd7a54d3de27fea000000000000000000000000000000000000000000000000000000000000003e8b8c000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000208eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48f9013c94eda338e4dc46038493b885327842fd3e301cab39f863a07153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84fa0c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539a0c8eaf22f2cb07bac4679df0a660e7115ed87fcfd4e32ac269f6540265bbbd26fb8c000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005f00a736aa00000000000187d1f7fdfee7f651fabc8bfcb6e086c278b77a7d008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000064a7b3b6e00d000000000000000000e40b5402000000000000000000000000").to_vec(), + ]), + execution_proof: ExecutionProof { + header: BeaconHeader { + slot: 2321, + proposer_index: 5, + parent_root: hex!("2add14727840d3a5ea061e14baa47030bb81380a65999200d119e73b86411d20").into(), + state_root: hex!("d962981467920bb2b7efa4a7a1baf64745582c3250857f49a957c5dae9a0da39").into(), + body_root: hex!("18e3f7f51a350f371ad35d166f2683b42af51d1836b295e4093be08acb0dcb7a").into(), + }, + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + hex!("2add14727840d3a5ea061e14baa47030bb81380a65999200d119e73b86411d20").into(), + hex!("48b2e2f5256906a564e5058698f70e3406765fefd6a2edc064bb5fb88aa2ed0a").into(), + hex!("e5ed7c704e845418219b2fda42cd2f3438ffbe4c4b320935ae49439c6189f7a7").into(), + hex!("4a7ce24526b3f571548ad69679e4e260653a1b3b911a344e7f988f25a5c917a7").into(), + hex!("46fc859727ab0d0e8c344011f7d7a4426ccb537bb51363397e56cc7153f56391").into(), + hex!("f496b6f85a7c6c28a9048f2153550a7c5bcb4b23844ed3b87f6baa646124d8a3").into(), + hex!("7318644e474beb46e595a1875acc7444b937f5208065241911d2a71ac50c2de3").into(), + hex!("5cf48519e518ac64286aef5391319782dd38831d5dcc960578a6b9746d5f8cee").into(), + hex!("efb3e50fa39ca9fe7f76adbfa36fa8451ec2fd5d07b22aaf822137c04cf95a76").into(), + hex!("2206cd50750355ffaef4a67634c21168f2b564c58ffd04f33b0dc7af7dab3291").into(), + hex!("1a4014f6c4fcce9949fba74cb0f9e88df086706f9e05560cc9f0926f8c90e373").into(), + hex!("2df7cc0bcf3060be4132c63da7599c2600d9bbadf37ab001f15629bc2255698e").into(), + hex!("b7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f").into(), + ], + finalized_block_root: hex!("f869dd1c9598043008a3ac2a5d91b3d6c7b0bb3295b3843bc84c083d70b0e604").into(), + }), + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("5d7859883dde1eba6c98b20eac18426134b25da2a89e5e360f3343b15e0e0a31").into(), + fee_recipient: hex!("0000000000000000000000000000000000000000").into(), + state_root: hex!("f8fbebed4c84d46231bd293bb9fbc9340d5c28c284d99fdaddb77238b8960ae2").into(), + receipts_root: hex!("f9d844c5b79638609ba385b910fec3b5d891c9d7b189f135f0432f33473de915").into(), + logs_bloom: hex!("00800000000000000000000020000000000000000000004000000000000000000400000000000000000000001000000010000000000000000000000008000000200000000000000001000008000000000000000000000000000000008000080000000000200000000000000000000000000100000000000000000011000000000000020200000000000000000000000000003000000040080008000000000000000000040044000021000000002000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000200800000000000").into(), + prev_randao: hex!("15533eeb366c6386bea5aeb8f425871928348c092209e4377f2418a6dedd7fd0").into(), + block_number: 2321, + gas_limit: 30000000, + gas_used: 113846, + timestamp: 1710554741, + extra_data: hex!("d983010d0b846765746888676f312e32312e368664617277696e").into(), + base_fee_per_gas: U256::from(7u64), + block_hash: hex!("585a07122a30339b03b6481eae67c2d3de2b6b64f9f426230986519bf0f1bdfe").into(), + transactions_root: hex!("09cd60ee2207d804397c81f7b7e1e5d3307712b136e5376623a80317a4bdcd7a").into(), + withdrawals_root: hex!("792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535").into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + execution_branch: vec![ + hex!("9d419471a9a4719b40e7607781fbe32d9a7766b79805505c78c0c58133496ba2").into(), + hex!("b46f0c01805fe212e15907981b757e6c496b0cb06664224655613dcec82505bb").into(), + hex!("db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").into(), + hex!("bee375b8f1bbe4cd0e783c78026c1829ae72741c2dead5cab05d6834c5e5df65").into(), + ], + } + }, + }, + finalized_header: BeaconHeader { + slot: 4032, + proposer_index: 5, + parent_root: hex!("180aaaec59d38c3860e8af203f01f41c9bc41665f4d17916567c80f6cd23e8a2").into(), + state_root: hex!("3341790429ed3bf894cafa3004351d0b99e08baf6c38eb2a54d58e69fd2d19c6").into(), + body_root: hex!("a221e0c695ac7b7d04ce39b28b954d8a682ecd57961d81b44783527c6295f455").into(), + }, + block_roots_root: hex!("5744385ef06f82e67606f49aa29cd162f2e837a68fb7bd82f1fc6155d9f8640f").into(), + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/send_token_to_penpal.rs b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/send_token_to_penpal.rs new file mode 100644 index 000000000000..4b4e78b63513 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/fixtures/src/send_token_to_penpal.rs @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +// Generated, do not edit! +// See ethereum client README.md for instructions to generate + +use hex_literal::hex; +use snowbridge_beacon_primitives::{ + types::deneb, AncestryProof, BeaconHeader, ExecutionProof, VersionedExecutionPayloadHeader, +}; +use snowbridge_core::inbound::{InboundQueueFixture, Log, Message, Proof}; +use sp_core::U256; +use sp_std::vec; + +pub fn make_send_token_to_penpal_message() -> InboundQueueFixture { + InboundQueueFixture { + message: Message { + event_log: Log { + address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + hex!("be323bced46a1a49c8da2ab62ad5e974fd50f1dabaeed70b23ca5bcf14bfe4aa").into(), + ], + data: hex!("00000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000007300a736aa00000000000187d1f7fdfee7f651fabc8bfcb6e086c278b77a7d01d00700001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c00286bee000000000000000000000000000064a7b3b6e00d000000000000000000e40b5402000000000000000000000000000000000000000000000000").into(), + }, + proof: Proof { + receipt_proof: (vec![ + hex!("106f1eaeac04e469da0020ad5c8a72af66323638bd3f561a3c8236063202c120").to_vec(), + ], vec![ + hex!("f90471822080b9046b02f904670183017d9cb9010000800000000000008000000000000000000000000000004000000000000000000400000000004000000000001000000010000000000000000000001008000000000000000000000001000008000040000000000000000000000000008000080000000000200000000000000000000000000100000000000000000010000000000000020000000000000000000000000000003000000000080018000000000000000000040004000021000000002000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000200820000000000f9035cf89b9487d1f7fdfee7f651fabc8bfcb6e086c278b77a7df863a0ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa000000000000000000000000090a987b944cb1dcce5564e5fdecd7a54d3de27fea000000000000000000000000057a2d4ff0c3866d96556884bf09fecdd7ccd530ca00000000000000000000000000000000000000000000000000de0b6b3a7640000f9015d94eda338e4dc46038493b885327842fd3e301cab39f884a024c5d2de620c6e25186ae16f6919eba93b6e2c1a33857cc419d9f3a00d6967e9a000000000000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7da000000000000000000000000090a987b944cb1dcce5564e5fdecd7a54d3de27fea000000000000000000000000000000000000000000000000000000000000007d0b8c000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000201cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07cf9015c94eda338e4dc46038493b885327842fd3e301cab39f863a07153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84fa0c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539a0be323bced46a1a49c8da2ab62ad5e974fd50f1dabaeed70b23ca5bcf14bfe4aab8e000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000007300a736aa00000000000187d1f7fdfee7f651fabc8bfcb6e086c278b77a7d01d00700001cbd2d43530a44705ad088af313e18f80b53ef16b36177cd4b77b846f2a5f07c00286bee000000000000000000000000000064a7b3b6e00d000000000000000000e40b5402000000000000000000000000000000000000000000000000").to_vec(), + ]), + execution_proof: ExecutionProof { + header: BeaconHeader { + slot: 4235, + proposer_index: 4, + parent_root: hex!("1b31e6264c19bcad120e434e0aede892e7d7c8ed80ab505cb593d9a4a16bc566").into(), + state_root: hex!("725f51771a0ecf72c647a283ab814ca088f998eb8c203181496b0b8e01f624fa").into(), + body_root: hex!("6f1c326d192e7e97e21e27b16fd7f000b8fa09b435ff028849927e382302b0ce").into(), + }, + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + hex!("1b31e6264c19bcad120e434e0aede892e7d7c8ed80ab505cb593d9a4a16bc566").into(), + hex!("335eb186c077fa7053ec96dcc5d34502c997713d2d5bc4eb74842118d8cd5a64").into(), + hex!("326607faf2a7dfc9cfc4b6895f8f3d92a659552deb2c8fd1e892ec00c86c734c").into(), + hex!("4e20002125d7b6504df7c774f3f48e018e1e6762d03489149670a8335bba1425").into(), + hex!("e76af5cd61aade5aec8282b6f1df9046efa756b0466bba5e49032410f7739a1b").into(), + hex!("ee4dcd9527712116380cddafd120484a3bedf867225bbb86850b84decf6da730").into(), + hex!("e4687a07421d3150439a2cd2f09f3b468145d75b359a2e5fa88dfbec51725b15").into(), + hex!("38eaa78978e95759aa9b6f8504a8dbe36151f20ae41907e6a1ea165700ceefcd").into(), + hex!("1c1b071ec6f13e15c47d07d1bfbcc9135d6a6c819e68e7e6078a2007418c1a23").into(), + hex!("0b3ad7ad193c691c8c4ba1606ad2a90482cd1d033c7db58cfe739d0e20431e9e").into(), + hex!("ffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b").into(), + hex!("6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220").into(), + hex!("b2ffec5f2c14640305dd941330f09216c53b99d198e93735a400a6d3a4de191f").into(), + ], + finalized_block_root: hex!("08be7a59e947f08cd95c4ef470758730bf9e3b0db0824cb663ea541c39b0e65c").into(), + }), + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("5d1186ae041f58785edb2f01248e95832f2e5e5d6c4eb8f7ff2f58980bfc2de9").into(), + fee_recipient: hex!("0000000000000000000000000000000000000000").into(), + state_root: hex!("2a66114d20e93082c8e9b47c8d401a937013487d757c9c2f3123cf43dc1f656d").into(), + receipts_root: hex!("106f1eaeac04e469da0020ad5c8a72af66323638bd3f561a3c8236063202c120").into(), + logs_bloom: hex!("00800000000000008000000000000000000000000000004000000000000000000400000000004000000000001000000010000000000000000000001008000000000000000000000001000008000040000000000000000000000000008000080000000000200000000000000000000000000100000000000000000010000000000000020000000000000000000000000000003000000000080018000000000000000000040004000021000000002000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000200820000000000").into(), + prev_randao: hex!("92e063c7e369b74149fdd1d7132ed2f635a19b9d8bff57637b8ee4736576426e").into(), + block_number: 4235, + gas_limit: 30000000, + gas_used: 97692, + timestamp: 1710556655, + extra_data: hex!("d983010d0b846765746888676f312e32312e368664617277696e").into(), + base_fee_per_gas: U256::from(7u64), + block_hash: hex!("ce24fe3047aa20a8f222cd1d04567c12b39455400d681141962c2130e690953f").into(), + transactions_root: hex!("0c8388731de94771777c60d452077065354d90d6e5088db61fc6a134684195cc").into(), + withdrawals_root: hex!("792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535").into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + execution_branch: vec![ + hex!("99d397fa180078e66cd3a3b77bcb07553052f4e21d447167f3a406f663b14e6a").into(), + hex!("b46f0c01805fe212e15907981b757e6c496b0cb06664224655613dcec82505bb").into(), + hex!("db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").into(), + hex!("53ddf17147819c1abb918178b0230d965d1bc2c0d389f45e91e54cb1d2d468aa").into(), + ], + } + }, + }, + finalized_header: BeaconHeader { + slot: 4672, + proposer_index: 4, + parent_root: hex!("951233bf9f4bddfb2fa8f54e3bd0c7883779ef850e13e076baae3130dd7732db").into(), + state_root: hex!("4d303003b8cb097cbcc14b0f551ee70dac42de2c1cc2f4acfca7058ca9713291").into(), + body_root: hex!("664d13952b6f369bf4cf3af74d067ec33616eb57ed3a8a403fd5bae4fbf737dd").into(), + }, + block_roots_root: hex!("af71048297c070e6539cf3b9b90ae07d86d363454606bc239734629e6b49b983").into(), + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/Cargo.toml new file mode 100644 index 000000000000..c9c38a44dd54 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "snowbridge-inbound-queue-v2-runtime-api" +description = "Snowbridge Inbound Queue V2 Runtime API" +version = "0.2.0" +authors = ["Snowfork "] +edition.workspace = true +repository.workspace = true +license = "Apache-2.0" +categories = ["cryptography::cryptocurrencies"] + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +frame-support = { workspace = true, default-features = false } +sp-api = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } +snowbridge-core = { workspace = true, default-features = false } +snowbridge-router-primitives = { workspace = true, default-features = false } +xcm = { workspace = true, default-features = false } + +[features] +default = ["std"] +std = [ + "frame-support/std", + "snowbridge-core/std", + "snowbridge-router-primitives/std", + "sp-runtime/std", + "sp-api/std", + "xcm/std", +] diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/README.md b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/README.md new file mode 100644 index 000000000000..89b6b0e157c5 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/README.md @@ -0,0 +1,3 @@ +# Ethereum Inbound Queue V2 Runtime API + +Provides an API to dry-run inbound messages to get the XCM (and its execution cost) that will be executed on AssetHub. diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/src/lib.rs new file mode 100644 index 000000000000..d899f7477b45 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/runtime-api/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::tokens::Balance as BalanceT; +use snowbridge_router_primitives::inbound::v2::Message; +use xcm::latest::Xcm; +use sp_runtime::DispatchError; + +sp_api::decl_runtime_apis! { + pub trait InboundQueueApiV2 where Balance: BalanceT + { + /// Dry runs the provided message on AH to provide the XCM payload and execution cost. + fn dry_run(message: Message) -> Result<(Xcm<()>, Balance), DispatchError>; + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/api.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/api.rs new file mode 100644 index 000000000000..8efc6eb2a280 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/api.rs @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Implements the dry-run API. + +use crate::{weights::WeightInfo, Config, Error, Junction::AccountId32, Location}; +use frame_support::weights::WeightToFee; +use snowbridge_router_primitives::inbound::v2::{ConvertMessage, Message}; +use sp_core::H256; +use sp_runtime::DispatchError; +use xcm::latest::Xcm; + +pub fn dry_run(message: Message) -> Result<(Xcm<()>, T::Balance), DispatchError> +where + T: Config, +{ + // Convert message to XCM + let dummy_origin = Location::new(0, AccountId32 { id: H256::zero().into(), network: None }); + let (xcm, _) = T::MessageConverter::convert(message, dummy_origin) + .map_err(|e| Error::::ConvertMessage(e))?; + + // Calculate fee. Consists of the cost of the "submit" extrinsic as well as the XCM execution + // prologue fee (static XCM part of the message that is execution on AH). + let weight_fee = T::WeightToFee::weight_to_fee(&T::WeightInfo::submit()); + let fee: u128 = weight_fee.try_into().map_err(|_| Error::::InvalidFee)?; + + Ok((xcm, fee.into())) +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs new file mode 100644 index 000000000000..4c5df07b27ac --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/benchmarking.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use super::*; + +use crate::Pallet as InboundQueue; +use frame_benchmarking::v2::*; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use snowbridge_pallet_inbound_queue_fixtures_v2::register_token::make_register_token_message; + +#[benchmarks] +mod benchmarks { + use super::*; + + #[benchmark] + fn submit() -> Result<(), BenchmarkError> { + let caller: T::AccountId = whitelisted_caller(); + + let create_message = make_register_token_message(); + + T::Helper::initialize_storage( + create_message.finalized_header, + create_message.block_roots_root, + ); + + #[block] + { + assert_ok!(InboundQueue::::submit( + RawOrigin::Signed(caller.clone()).into(), + create_message.message, + )); + } + + Ok(()) + } + + impl_benchmark_test_suite!(InboundQueue, crate::mock::new_tester(), crate::mock::Test); +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs new file mode 100644 index 000000000000..8c9b137c64ba --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/envelope.rs @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use snowbridge_core::inbound::Log; + +use sp_core::{RuntimeDebug, H160}; +use sp_std::prelude::*; + +use alloy_primitives::B256; +use alloy_sol_types::{sol, SolEvent}; + +sol! { + event OutboundMessageAccepted(uint64 indexed nonce, uint128 fee, bytes payload); +} + +/// An inbound message that has had its outer envelope decoded. +#[derive(Clone, RuntimeDebug)] +pub struct Envelope { + /// The address of the outbound queue on Ethereum that emitted this message as an event log + pub gateway: H160, + /// A nonce for enforcing replay protection and ordering. + pub nonce: u64, + /// Total fee paid in Ether on Ethereum, should cover all the cost + pub fee: u128, + /// The inner payload generated from the source application. + pub payload: Vec, +} + +#[derive(Copy, Clone, RuntimeDebug)] +pub struct EnvelopeDecodeError; + +impl TryFrom<&Log> for Envelope { + type Error = EnvelopeDecodeError; + + fn try_from(log: &Log) -> Result { + let topics: Vec = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect(); + + let event = OutboundMessageAccepted::decode_log(topics, &log.data, true) + .map_err(|_| EnvelopeDecodeError)?; + + Ok(Self { + gateway: log.address, + nonce: event.nonce, + fee: event.fee, + payload: event.payload, + }) + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs new file mode 100644 index 000000000000..7320bf9188a6 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/lib.rs @@ -0,0 +1,286 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Inbound Queue +//! +//! # Overview +//! +//! Receives messages emitted by the Gateway contract on Ethereum, whereupon they are verified, +//! translated to XCM, and finally sent to their final destination parachain. +//! +//! The message relayers are rewarded using native currency from the sovereign account of the +//! destination parachain. +//! +//! # Extrinsics +//! +//! ## Governance +//! +//! * [`Call::set_operating_mode`]: Set the operating mode of the pallet. Can be used to disable +//! processing of inbound messages. +//! +//! ## Message Submission +//! +//! * [`Call::submit`]: Submit a message for verification and dispatch the final destination +//! parachain. +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; +pub mod api; +mod envelope; + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; +mod types; + +pub mod weights; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod test; + +use codec::{Decode, DecodeAll, Encode}; +use envelope::Envelope; +use frame_support::{ + traits::{ + fungible::{Inspect, Mutate}, + tokens::Balance, + }, + weights::WeightToFee, + PalletError, +}; +use frame_system::{ensure_signed, pallet_prelude::*}; +use scale_info::TypeInfo; +use snowbridge_core::{ + inbound::{Message, VerificationError, Verifier}, + sparse_bitmap::SparseBitmap, + BasicOperatingMode, +}; +use snowbridge_router_primitives::inbound::v2::{ + ConvertMessage, ConvertMessageError, Message as MessageV2, +}; +use sp_core::H160; +use sp_std::vec; +use types::Nonce; +pub use weights::WeightInfo; +use xcm::prelude::{send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, *}; + +#[cfg(feature = "runtime-benchmarks")] +use snowbridge_beacon_primitives::BeaconHeader; + +pub use pallet::*; + +pub const LOG_TARGET: &str = "snowbridge-inbound-queue:v2"; + +pub type AccountIdOf = ::AccountId; +type BalanceOf = + <::Token as Inspect<::AccountId>>::Balance; +#[frame_support::pallet] +pub mod pallet { + use super::*; + + use frame_support::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[cfg(feature = "runtime-benchmarks")] + pub trait BenchmarkHelper { + fn initialize_storage(beacon_header: BeaconHeader, block_roots_root: H256); + } + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// The verifier for inbound messages from Ethereum. + type Verifier: Verifier; + /// XCM message sender. + type XcmSender: SendXcm; + /// Address of the Gateway contract. + #[pallet::constant] + type GatewayAddress: Get; + type WeightInfo: WeightInfo; + /// Convert a weight value into deductible balance type. + type WeightToFee: WeightToFee>; + /// AssetHub parachain ID. + type AssetHubParaId: Get; + /// Convert a command from Ethereum to an XCM message. + type MessageConverter: ConvertMessage; + /// Used to burn fees from the origin account (the relayer), which will be teleported to AH. + type Token: Mutate + Inspect; + /// Used for the dry run API implementation. + type Balance: Balance + From; + #[cfg(feature = "runtime-benchmarks")] + type Helper: BenchmarkHelper; + } + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A message was received from Ethereum + MessageReceived { + /// The message nonce + nonce: u64, + /// ID of the XCM message which was forwarded to the final destination parachain + message_id: [u8; 32], + }, + /// Set OperatingMode + OperatingModeChanged { mode: BasicOperatingMode }, + } + + #[pallet::error] + pub enum Error { + /// Message came from an invalid outbound channel on the Ethereum side. + InvalidGateway, + /// Account could not be converted to bytes + InvalidAccount, + /// Message has an invalid envelope. + InvalidEnvelope, + /// Message has an unexpected nonce. + InvalidNonce, + /// Fee provided is invalid. + InvalidFee, + /// Message has an invalid payload. + InvalidPayload, + /// Message channel is invalid + InvalidChannel, + /// The max nonce for the type has been reached + MaxNonceReached, + /// Cannot convert location + InvalidAccountConversion, + /// Pallet is halted + Halted, + /// Message verification error, + Verification(VerificationError), + /// XCMP send failure + Send(SendError), + /// Message conversion error + ConvertMessage(ConvertMessageError), + } + + #[derive(Clone, Encode, Decode, Eq, PartialEq, Debug, TypeInfo, PalletError)] + pub enum SendError { + NotApplicable, + NotRoutable, + Transport, + DestinationUnsupported, + ExceedsMaxMessageSize, + MissingArgument, + Fees, + } + + impl From for Error { + fn from(e: XcmpSendError) -> Self { + match e { + XcmpSendError::NotApplicable => Error::::Send(SendError::NotApplicable), + XcmpSendError::Unroutable => Error::::Send(SendError::NotRoutable), + XcmpSendError::Transport(_) => Error::::Send(SendError::Transport), + XcmpSendError::DestinationUnsupported => + Error::::Send(SendError::DestinationUnsupported), + XcmpSendError::ExceedsMaxMessageSize => + Error::::Send(SendError::ExceedsMaxMessageSize), + XcmpSendError::MissingArgument => Error::::Send(SendError::MissingArgument), + XcmpSendError::Fees => Error::::Send(SendError::Fees), + } + } + } + + /// The nonce of the message been processed or not + #[pallet::storage] + pub type NonceBitmap = StorageMap<_, Twox64Concat, u128, u128, ValueQuery>; + + /// The current operating mode of the pallet. + #[pallet::storage] + #[pallet::getter(fn operating_mode)] + pub type OperatingMode = StorageValue<_, BasicOperatingMode, ValueQuery>; + + #[pallet::call] + impl Pallet { + /// Submit an inbound message originating from the Gateway contract on Ethereum + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::submit())] + pub fn submit(origin: OriginFor, message: Message) -> DispatchResult { + let who = ensure_signed(origin.clone())?; + ensure!(!Self::operating_mode().is_halted(), Error::::Halted); + + // submit message to verifier for verification + T::Verifier::verify(&message.event_log, &message.proof) + .map_err(|e| Error::::Verification(e))?; + + // Decode event log into an Envelope + let envelope = + Envelope::try_from(&message.event_log).map_err(|_| Error::::InvalidEnvelope)?; + + // Verify that the message was submitted from the known Gateway contract + ensure!(T::GatewayAddress::get() == envelope.gateway, Error::::InvalidGateway); + + // Verify the message has not been processed + ensure!(!Nonce::::get(envelope.nonce.into()), Error::::InvalidNonce); + + // Decode payload into `MessageV2` + let message = MessageV2::decode_all(&mut envelope.payload.as_ref()) + .map_err(|_| Error::::InvalidPayload)?; + + let origin_account_location = Self::account_to_location(who)?; + + let (xcm, _relayer_reward) = + Self::do_convert(message, origin_account_location.clone())?; + + // Todo: Deposit fee(in Ether) to RewardLeger which should cover all of: + // T::RewardLeger::deposit(who, relayer_reward.into())?; + // a. The submit extrinsic cost on BH + // b. The delivery cost to AH + // c. The execution cost on AH + // d. The execution cost on destination chain(if any) + // e. The reward + + // Attempt to forward XCM to AH + + // Set nonce flag to true + Nonce::::set(envelope.nonce.into()); + + let message_id = Self::send_xcm(xcm, T::AssetHubParaId::get())?; + Self::deposit_event(Event::MessageReceived { nonce: envelope.nonce, message_id }); + + Ok(()) + } + + /// Halt or resume all pallet operations. May only be called by root. + #[pallet::call_index(1)] + #[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))] + pub fn set_operating_mode( + origin: OriginFor, + mode: BasicOperatingMode, + ) -> DispatchResult { + ensure_root(origin)?; + OperatingMode::::set(mode); + Self::deposit_event(Event::OperatingModeChanged { mode }); + Ok(()) + } + } + + impl Pallet { + pub fn account_to_location(account: AccountIdOf) -> Result> { + let account_bytes: [u8; 32] = + account.encode().try_into().map_err(|_| Error::::InvalidAccount)?; + Ok(Location::new(0, [AccountId32 { network: None, id: account_bytes }])) + } + + pub fn send_xcm(xcm: Xcm<()>, dest_para_id: u32) -> Result> { + let dest = Location::new(1, [Parachain(dest_para_id)]); + let (message_id, _) = send_xcm::(dest, xcm).map_err(Error::::from)?; + Ok(message_id) + } + + pub fn do_convert( + message: MessageV2, + origin_account_location: Location, + ) -> Result<(Xcm<()>, u128), Error> { + Ok(T::MessageConverter::convert(message, origin_account_location) + .map_err(|e| Error::::ConvertMessage(e))?) + } + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs new file mode 100644 index 000000000000..e603ab8e270a --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/mock.rs @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use super::*; + +use crate::{self as inbound_queue_v2}; +use frame_support::{derive_impl, parameter_types, traits::ConstU32, weights::IdentityFee}; +use hex_literal::hex; +use snowbridge_beacon_primitives::{ + types::deneb, BeaconHeader, ExecutionProof, Fork, ForkVersions, VersionedExecutionPayloadHeader, +}; +use snowbridge_core::{ + inbound::{Log, Proof, VerificationError}, + TokenId, +}; +use snowbridge_router_primitives::inbound::v2::MessageToXcm; +use sp_core::H160; +use sp_runtime::{ + traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify}, + BuildStorage, MultiSignature, +}; +use sp_std::{convert::From, default::Default}; +use xcm::{latest::SendXcm, opaque::latest::WESTEND_GENESIS_HASH, prelude::*}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system::{Pallet, Call, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + EthereumBeaconClient: snowbridge_pallet_ethereum_client::{Pallet, Call, Storage, Event}, + InboundQueue: inbound_queue_v2::{Pallet, Call, Storage, Event}, + } +); + +pub type Signature = MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; + +type Balance = u128; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountId = AccountId; + type Lookup = IdentityLookup; + type AccountData = pallet_balances::AccountData; + type Block = Block; +} + +parameter_types! { + pub const ExistentialDeposit: u128 = 1; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type Balance = Balance; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; +} + +parameter_types! { + pub const ChainForkVersions: ForkVersions = ForkVersions{ + genesis: Fork { + version: [0, 0, 0, 1], // 0x00000001 + epoch: 0, + }, + altair: Fork { + version: [1, 0, 0, 1], // 0x01000001 + epoch: 0, + }, + bellatrix: Fork { + version: [2, 0, 0, 1], // 0x02000001 + epoch: 0, + }, + capella: Fork { + version: [3, 0, 0, 1], // 0x03000001 + epoch: 0, + }, + deneb: Fork { + version: [4, 0, 0, 1], // 0x04000001 + epoch: 4294967295, + } + }; +} + +impl snowbridge_pallet_ethereum_client::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ForkVersions = ChainForkVersions; + type FreeHeadersInterval = ConstU32<32>; + type WeightInfo = (); +} + +// Mock verifier +pub struct MockVerifier; + +impl Verifier for MockVerifier { + fn verify(_: &Log, _: &Proof) -> Result<(), VerificationError> { + Ok(()) + } +} + +const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"]; + +#[cfg(feature = "runtime-benchmarks")] +impl BenchmarkHelper for Test { + // not implemented since the MockVerifier is used for tests + fn initialize_storage(_: BeaconHeader, _: H256) {} +} + +// Mock XCM sender that always succeeds +pub struct MockXcmSender; +impl SendXcm for MockXcmSender { + type Ticket = Xcm<()>; + + fn validate( + dest: &mut Option, + xcm: &mut Option>, + ) -> SendResult { + if let Some(location) = dest { + match location.unpack() { + (_, [Parachain(1001)]) => return Err(XcmpSendError::NotApplicable), + _ => Ok((xcm.clone().unwrap(), Assets::default())), + } + } else { + Ok((xcm.clone().unwrap(), Assets::default())) + } + } + + fn deliver(xcm: Self::Ticket) -> core::result::Result { + let hash = xcm.using_encoded(sp_io::hashing::blake2_256); + Ok(hash) + } +} + +pub struct MockTokenIdConvert; +impl MaybeEquivalence for MockTokenIdConvert { + fn convert(_id: &TokenId) -> Option { + Some(Location::parent()) + } + fn convert_back(_loc: &Location) -> Option { + None + } +} + +parameter_types! { + pub const EthereumNetwork: xcm::v5::NetworkId = xcm::v5::NetworkId::Ethereum { chain_id: 11155111 }; + pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); + pub const InboundQueuePalletInstance: u8 = 84; + pub AssetHubLocation: InteriorLocation = Parachain(1000).into(); + pub UniversalLocation: InteriorLocation = + [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)), Parachain(1002)].into(); + pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)),Parachain(1000)]); +} + +impl inbound_queue_v2::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Verifier = MockVerifier; + type XcmSender = MockXcmSender; + type WeightInfo = (); + type WeightToFee = IdentityFee; + type GatewayAddress = GatewayAddress; + type AssetHubParaId = ConstU32<1000>; + type MessageConverter = MessageToXcm< + EthereumNetwork, + InboundQueuePalletInstance, + MockTokenIdConvert, + GatewayAddress, + UniversalLocation, + AssetHubFromEthereum, + >; + type Token = Balances; + type Balance = u128; + #[cfg(feature = "runtime-benchmarks")] + type Helper = Test; +} + +pub fn setup() { + System::set_block_number(1); +} + +pub fn new_tester() -> sp_io::TestExternalities { + let storage = frame_system::GenesisConfig::::default().build_storage().unwrap(); + let mut ext: sp_io::TestExternalities = storage.into(); + ext.execute_with(setup); + ext +} + +// Generated from smoketests: +// cd smoketests +// ./make-bindings +// cargo test --test register_token -- --nocapture +pub fn mock_event_log() -> Log { + Log { + // gateway address + address: hex!("eda338e4dc46038493b885327842fd3e301cab39").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + // channel id + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + // message id + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + // Nonce + Payload + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000002e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d00e40b54020000000000000000000000000000000000000000000000000000000000").into(), + } +} + +pub fn mock_event_log_invalid_gateway() -> Log { + Log { + // gateway address + address: H160::zero(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + // channel id + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + // message id + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + // Nonce + Payload + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000001e000f000000000000000087d1f7fdfee7f651fabc8bfcb6e086c278b77a7d0000").into(), + } +} + +pub fn mock_execution_proof() -> ExecutionProof { + ExecutionProof { + header: BeaconHeader::default(), + ancestry_proof: None, + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: Default::default(), + fee_recipient: Default::default(), + state_root: Default::default(), + receipts_root: Default::default(), + logs_bloom: vec![], + prev_randao: Default::default(), + block_number: 0, + gas_limit: 0, + gas_used: 0, + timestamp: 0, + extra_data: vec![], + base_fee_per_gas: Default::default(), + block_hash: Default::default(), + transactions_root: Default::default(), + withdrawals_root: Default::default(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + execution_branch: vec![], + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs new file mode 100644 index 000000000000..db321576917e --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/test.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use super::*; + +use frame_support::{assert_noop, assert_ok}; +use snowbridge_core::inbound::Proof; +use sp_keyring::AccountKeyring as Keyring; +use sp_runtime::DispatchError; + +use crate::{mock::*, Error}; + +#[test] +fn test_submit_with_invalid_gateway() { + new_tester().execute_with(|| { + let relayer: AccountId = Keyring::Bob.into(); + let origin = RuntimeOrigin::signed(relayer); + + // Submit message + let message = Message { + event_log: mock_event_log_invalid_gateway(), + proof: Proof { + receipt_proof: Default::default(), + execution_proof: mock_execution_proof(), + }, + }; + assert_noop!( + InboundQueue::submit(origin.clone(), message.clone()), + Error::::InvalidGateway + ); + }); +} + +#[test] +fn test_set_operating_mode() { + new_tester().execute_with(|| { + let relayer: AccountId = Keyring::Bob.into(); + let origin = RuntimeOrigin::signed(relayer); + let message = Message { + event_log: mock_event_log(), + proof: Proof { + receipt_proof: Default::default(), + execution_proof: mock_execution_proof(), + }, + }; + + assert_ok!(InboundQueue::set_operating_mode( + RuntimeOrigin::root(), + snowbridge_core::BasicOperatingMode::Halted + )); + + assert_noop!(InboundQueue::submit(origin, message), Error::::Halted); + }); +} + +#[test] +fn test_set_operating_mode_root_only() { + new_tester().execute_with(|| { + assert_noop!( + InboundQueue::set_operating_mode( + RuntimeOrigin::signed(Keyring::Bob.into()), + snowbridge_core::BasicOperatingMode::Halted + ), + DispatchError::BadOrigin + ); + }); +} diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/types.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/types.rs new file mode 100644 index 000000000000..150f6028b129 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/types.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use snowbridge_core::sparse_bitmap::SparseBitmapImpl; + +pub type Nonce = SparseBitmapImpl>; diff --git a/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs b/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs new file mode 100644 index 000000000000..c2c665f40d9e --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue-v2/src/weights.rs @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Autogenerated weights for `snowbridge_inbound_queue` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-07-14, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `macbook pro 14 m2`, CPU: `m2-arm64` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-rococo-dev"), DB CACHE: 1024 + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for ethereum_beacon_client. +pub trait WeightInfo { + fn submit() -> Weight; +} + +// For backwards compatibility and tests +impl WeightInfo for () { + fn submit() -> Weight { + Weight::from_parts(70_000_000, 0) + .saturating_add(Weight::from_parts(0, 3601)) + .saturating_add(RocksDbWeight::get().reads(2)) + .saturating_add(RocksDbWeight::get().writes(2)) + } +} diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs index 423b92b9fae0..5814886fe355 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs @@ -61,7 +61,7 @@ use snowbridge_core::{ sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters, StaticLookup, }; -use snowbridge_router_primitives::inbound::{ +use snowbridge_router_primitives::inbound::v1::{ ConvertMessage, ConvertMessageError, VersionedMessage, }; use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError}; diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs index 675d4b691593..82862616466d 100644 --- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs +++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs @@ -12,7 +12,7 @@ use snowbridge_core::{ inbound::{Log, Proof, VerificationError}, meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup, TokenId, }; -use snowbridge_router_primitives::inbound::MessageToXcm; +use snowbridge_router_primitives::inbound::v1::MessageToXcm; use sp_core::{H160, H256}; use sp_runtime::{ traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify}, diff --git a/bridges/snowbridge/primitives/core/Cargo.toml b/bridges/snowbridge/primitives/core/Cargo.toml index fa37c795b2d1..4f5935a9fa43 100644 --- a/bridges/snowbridge/primitives/core/Cargo.toml +++ b/bridges/snowbridge/primitives/core/Cargo.toml @@ -16,6 +16,7 @@ serde = { optional = true, features = ["alloc", "derive"], workspace = true } codec = { workspace = true } scale-info = { features = ["derive"], workspace = true } hex-literal = { workspace = true, default-features = true } +log = { workspace = true } polkadot-parachain-primitives = { workspace = true } xcm = { workspace = true } @@ -33,9 +34,10 @@ snowbridge-beacon-primitives = { workspace = true } ethabi = { workspace = true } +xcm-executor = { workspace = true } + [dev-dependencies] hex = { workspace = true, default-features = true } -xcm-executor = { workspace = true, default-features = true } [features] default = ["std"] @@ -54,6 +56,7 @@ std = [ "sp-runtime/std", "sp-std/std", "xcm-builder/std", + "xcm-executor/std", "xcm/std", ] serde = ["dep:serde", "scale-info/serde"] diff --git a/bridges/snowbridge/primitives/core/src/fees.rs b/bridges/snowbridge/primitives/core/src/fees.rs new file mode 100644 index 000000000000..a9ae0407fbfe --- /dev/null +++ b/bridges/snowbridge/primitives/core/src/fees.rs @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use log; +use sp_runtime::{DispatchResult, SaturatedConversion, Saturating, TokenError}; +use xcm::opaque::latest::{Location, XcmContext}; +use xcm_executor::traits::TransactAsset; +const LOG_TARGET: &str = "xcm_fees"; + +/// Burns the fees embedded in the XCM for teleports. +pub fn burn_fees(dest: Location, fee: Balance) -> DispatchResult +where + AssetTransactor: TransactAsset, + Balance: Saturating + TryInto + Copy, +{ + let dummy_context = XcmContext { origin: None, message_id: Default::default(), topic: None }; + let fees = (Location::parent(), fee.saturated_into::()).into(); + + // Check if the asset can be checked out + AssetTransactor::can_check_out(&dest, &fees, &dummy_context).map_err(|error| { + log::error!( + target: LOG_TARGET, + "XCM asset check out failed with error {:?}", + error + ); + TokenError::FundsUnavailable + })?; + + // Check out the asset + AssetTransactor::check_out(&dest, &fees, &dummy_context); + + // Withdraw the asset and handle potential errors + AssetTransactor::withdraw_asset(&fees, &dest, None).map_err(|error| { + log::error!( + target: LOG_TARGET, + "XCM asset withdraw failed with error {:?}", + error + ); + TokenError::FundsUnavailable + })?; + + Ok(()) +} diff --git a/bridges/snowbridge/primitives/core/src/lib.rs b/bridges/snowbridge/primitives/core/src/lib.rs index 7ad129a52542..558ac43d0d31 100644 --- a/bridges/snowbridge/primitives/core/src/lib.rs +++ b/bridges/snowbridge/primitives/core/src/lib.rs @@ -8,12 +8,14 @@ #[cfg(test)] mod tests; +pub mod fees; pub mod inbound; pub mod location; pub mod operating_mode; pub mod outbound; pub mod pricing; pub mod ringbuffer; +pub mod sparse_bitmap; pub use location::{AgentId, AgentIdOf, TokenId, TokenIdOf}; pub use polkadot_parachain_primitives::primitives::{ diff --git a/bridges/snowbridge/primitives/core/src/sparse_bitmap.rs b/bridges/snowbridge/primitives/core/src/sparse_bitmap.rs new file mode 100644 index 000000000000..810c4747c382 --- /dev/null +++ b/bridges/snowbridge/primitives/core/src/sparse_bitmap.rs @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use frame_support::storage::StorageMap; +use sp_std::marker::PhantomData; + +/// Sparse bitmap implementation. +pub trait SparseBitmap +where + BitMap: StorageMap, +{ + fn get(index: u128) -> bool; + fn set(index: u128); +} + +pub struct SparseBitmapImpl(PhantomData); + +impl SparseBitmap for SparseBitmapImpl +where + BitMap: StorageMap, +{ + fn get(index: u128) -> bool { + // Calculate bucket and mask + let bucket = index >> 7; // Divide by 2^7 (128 bits) + let mask = 1u128 << (index & 127); // Mask for the bit in the bucket + + // Retrieve bucket and check bit + let bucket_value = BitMap::get(bucket); + bucket_value & mask != 0 + } + + fn set(index: u128) { + // Calculate bucket and mask + let bucket = index >> 7; // Divide by 2^7 (128 bits) + let mask = 1u128 << (index & 127); // Mask for the bit in the bucket + + // Mutate the storage to set the bit + BitMap::mutate(bucket, |value| { + *value |= mask; // Set the bit in the bucket + }); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use frame_support::{ + storage::{generator::StorageMap as StorageMapHelper, storage_prefix}, + Twox64Concat, + }; + use sp_io::TestExternalities; + pub struct MockStorageMap; + + impl StorageMapHelper for MockStorageMap { + type Query = u128; + type Hasher = Twox64Concat; + fn pallet_prefix() -> &'static [u8] { + b"MyModule" + } + + fn storage_prefix() -> &'static [u8] { + b"MyStorageMap" + } + + fn prefix_hash() -> [u8; 32] { + storage_prefix(Self::pallet_prefix(), Self::storage_prefix()) + } + + fn from_optional_value_to_query(v: Option) -> Self::Query { + v.unwrap_or_default() + } + + fn from_query_to_optional_value(v: Self::Query) -> Option { + Some(v) + } + } + + type TestSparseBitmap = SparseBitmapImpl; + + #[test] + fn test_sparse_bitmap_set_and_get() { + TestExternalities::default().execute_with(|| { + let index = 300; + let bucket = index >> 7; + let mask = 1u128 << (index & 127); + + // Test initial state + assert_eq!(MockStorageMap::get(bucket), 0); + assert!(!TestSparseBitmap::get(index)); + + // Set the bit + TestSparseBitmap::set(index); + + // Test after setting + assert_eq!(MockStorageMap::get(bucket), mask); + assert!(TestSparseBitmap::get(index)); + }); + } + + #[test] + fn test_sparse_bitmap_multiple_sets() { + TestExternalities::default().execute_with(|| { + let index1 = 300; + let index2 = 305; // Same bucket, different bit + let bucket = index1 >> 7; + + let mask1 = 1u128 << (index1 & 127); + let mask2 = 1u128 << (index2 & 127); + + // Test initial state + assert_eq!(MockStorageMap::get(bucket), 0); + assert!(!TestSparseBitmap::get(index1)); + assert!(!TestSparseBitmap::get(index2)); + + // Set the first bit + TestSparseBitmap::set(index1); + + // Test after first set + assert_eq!(MockStorageMap::get(bucket), mask1); + assert!(TestSparseBitmap::get(index1)); + assert!(!TestSparseBitmap::get(index2)); + + // Set the second bit + TestSparseBitmap::set(index2); + + // Test after second set + assert_eq!(MockStorageMap::get(bucket), mask1 | mask2); // Bucket should contain both masks + assert!(TestSparseBitmap::get(index1)); + assert!(TestSparseBitmap::get(index2)); + }) + } + + #[test] + fn test_sparse_bitmap_different_buckets() { + TestExternalities::default().execute_with(|| { + let index1 = 300; // Bucket 1 + let index2 = 300 + (1 << 7); // Bucket 2 (128 bits apart) + + let bucket1 = index1 >> 7; + let bucket2 = index2 >> 7; + + let mask1 = 1u128 << (index1 & 127); + let mask2 = 1u128 << (index2 & 127); + + // Test initial state + assert_eq!(MockStorageMap::get(bucket1), 0); + assert_eq!(MockStorageMap::get(bucket2), 0); + + // Set bits in different buckets + TestSparseBitmap::set(index1); + TestSparseBitmap::set(index2); + + // Test after setting + assert_eq!(MockStorageMap::get(bucket1), mask1); // Bucket 1 should contain mask1 + assert_eq!(MockStorageMap::get(bucket2), mask2); // Bucket 2 should contain mask2 + + assert!(TestSparseBitmap::get(index1)); + assert!(TestSparseBitmap::get(index2)); + }) + } +} diff --git a/bridges/snowbridge/primitives/router/Cargo.toml b/bridges/snowbridge/primitives/router/Cargo.toml index ee8d481cec12..77c8f3c1e9e2 100644 --- a/bridges/snowbridge/primitives/router/Cargo.toml +++ b/bridges/snowbridge/primitives/router/Cargo.toml @@ -15,8 +15,10 @@ workspace = true codec = { workspace = true } scale-info = { features = ["derive"], workspace = true } log = { workspace = true } +alloy-sol-types = { workspace = true } frame-support = { workspace = true } +frame-system = { workspace = true } sp-core = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } @@ -24,6 +26,7 @@ sp-std = { workspace = true } xcm = { workspace = true } xcm-executor = { workspace = true } +xcm-builder = { workspace = true } snowbridge-core = { workspace = true } @@ -34,8 +37,10 @@ hex-literal = { workspace = true, default-features = true } [features] default = ["std"] std = [ + "alloy-sol-types/std", "codec/std", "frame-support/std", + "frame-system/std", "log/std", "scale-info/std", "snowbridge-core/std", @@ -43,12 +48,15 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm-builder/std", "xcm-executor/std", "xcm/std", ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", "snowbridge-core/runtime-benchmarks", "sp-runtime/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", "xcm-executor/runtime-benchmarks", ] diff --git a/bridges/snowbridge/primitives/router/src/inbound/mod.rs b/bridges/snowbridge/primitives/router/src/inbound/mod.rs index 54a578b988a4..fc0a32163b49 100644 --- a/bridges/snowbridge/primitives/router/src/inbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/inbound/mod.rs @@ -1,459 +1,16 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork -//! Converts messages from Ethereum to XCM messages - -#[cfg(test)] -mod tests; - -use codec::{Decode, Encode}; -use core::marker::PhantomData; -use frame_support::{traits::tokens::Balance as BalanceT, PalletError}; -use scale_info::TypeInfo; -use snowbridge_core::TokenId; -use sp_core::{Get, RuntimeDebug, H160, H256}; -use sp_io::hashing::blake2_256; -use sp_runtime::{traits::MaybeEquivalence, MultiAddress}; -use sp_std::prelude::*; -use xcm::prelude::{Junction::AccountKey20, *}; +// SPDX-FileCopyrightText: 2021-2022 Parity Technologies (UK) Ltd. + +pub mod payload; +pub mod v1; +pub mod v2; +use codec::Encode; +use sp_core::blake2_256; +use sp_std::marker::PhantomData; +use xcm::prelude::{AccountKey20, Ethereum, GlobalConsensus, Location}; use xcm_executor::traits::ConvertLocation; -const MINIMUM_DEPOSIT: u128 = 1; - -/// Messages from Ethereum are versioned. This is because in future, -/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly. -/// Instead having BridgeHub transcode the messages into XCM. -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum VersionedMessage { - V1(MessageV1), -} - -/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are -/// self-contained, in that they can be transcoded using only information in the message. -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub struct MessageV1 { - /// EIP-155 chain id of the origin Ethereum network - pub chain_id: u64, - /// The command originating from the Gateway contract - pub command: Command, -} - -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum Command { - /// Register a wrapped token on the AssetHub `ForeignAssets` pallet - RegisterToken { - /// The address of the ERC20 token to be bridged over to AssetHub - token: H160, - /// XCM execution fee on AssetHub - fee: u128, - }, - /// Send Ethereum token to AssetHub or another parachain - SendToken { - /// The address of the ERC20 token to be bridged over to AssetHub - token: H160, - /// The destination for the transfer - destination: Destination, - /// Amount to transfer - amount: u128, - /// XCM execution fee on AssetHub - fee: u128, - }, - /// Send Polkadot token back to the original parachain - SendNativeToken { - /// The Id of the token - token_id: TokenId, - /// The destination for the transfer - destination: Destination, - /// Amount to transfer - amount: u128, - /// XCM execution fee on AssetHub - fee: u128, - }, -} - -/// Destination for bridged tokens -#[derive(Clone, Encode, Decode, RuntimeDebug)] -pub enum Destination { - /// The funds will be deposited into account `id` on AssetHub - AccountId32 { id: [u8; 32] }, - /// The funds will deposited into the sovereign account of destination parachain `para_id` on - /// AssetHub, Account `id` on the destination parachain will receive the funds via a - /// reserve-backed transfer. See - ForeignAccountId32 { - para_id: u32, - id: [u8; 32], - /// XCM execution fee on final destination - fee: u128, - }, - /// The funds will deposited into the sovereign account of destination parachain `para_id` on - /// AssetHub, Account `id` on the destination parachain will receive the funds via a - /// reserve-backed transfer. See - ForeignAccountId20 { - para_id: u32, - id: [u8; 20], - /// XCM execution fee on final destination - fee: u128, - }, -} - -pub struct MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, -> where - CreateAssetCall: Get, - CreateAssetDeposit: Get, - Balance: BalanceT, - ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, -{ - _phantom: PhantomData<( - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - )>, -} - -/// Reason why a message conversion failed. -#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)] -pub enum ConvertMessageError { - /// The message version is not supported for conversion. - UnsupportedVersion, - InvalidDestination, - InvalidToken, - /// The fee asset is not supported for conversion. - UnsupportedFeeAsset, - CannotReanchor, -} - -/// convert the inbound message to xcm which will be forwarded to the destination chain -pub trait ConvertMessage { - type Balance: BalanceT + From; - type AccountId; - /// Converts a versioned message into an XCM message and an optional topicID - fn convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; -} - -pub type CallIndex = [u8; 2]; - -impl< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > ConvertMessage - for MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > -where - CreateAssetCall: Get, - CreateAssetDeposit: Get, - InboundQueuePalletInstance: Get, - Balance: BalanceT + From, - AccountId: Into<[u8; 32]>, - ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, -{ - type Balance = Balance; - type AccountId = AccountId; - - fn convert( - message_id: H256, - message: VersionedMessage, - ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { - use Command::*; - use VersionedMessage::*; - match message { - V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) => - Ok(Self::convert_register_token(message_id, chain_id, token, fee)), - V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) => - Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)), - V1(MessageV1 { - chain_id, - command: SendNativeToken { token_id, destination, amount, fee }, - }) => Self::convert_send_native_token( - message_id, - chain_id, - token_id, - destination, - amount, - fee, - ), - } - } -} - -impl< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > - MessageToXcm< - CreateAssetCall, - CreateAssetDeposit, - InboundQueuePalletInstance, - AccountId, - Balance, - ConvertAssetId, - EthereumUniversalLocation, - GlobalAssetHubLocation, - > -where - CreateAssetCall: Get, - CreateAssetDeposit: Get, - InboundQueuePalletInstance: Get, - Balance: BalanceT + From, - AccountId: Into<[u8; 32]>, - ConvertAssetId: MaybeEquivalence, - EthereumUniversalLocation: Get, - GlobalAssetHubLocation: Get, -{ - fn convert_register_token( - message_id: H256, - chain_id: u64, - token: H160, - fee: u128, - ) -> (Xcm<()>, Balance) { - let network = Ethereum { chain_id }; - let xcm_fee: Asset = (Location::parent(), fee).into(); - let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into(); - - let total_amount = fee + CreateAssetDeposit::get(); - let total: Asset = (Location::parent(), total_amount).into(); - - let bridge_location = Location::new(2, GlobalConsensus(network)); - - let owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&chain_id); - let asset_id = Self::convert_token_address(network, token); - let create_call_index: [u8; 2] = CreateAssetCall::get(); - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); - - let xcm: Xcm<()> = vec![ - // Teleport required fees. - ReceiveTeleportedAsset(total.into()), - // Pay for execution. - BuyExecution { fees: xcm_fee, weight_limit: Unlimited }, - // Fund the snowbridge sovereign with the required deposit for creation. - DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() }, - // This `SetAppendix` ensures that `xcm_fee` not spent by `Transact` will be - // deposited to snowbridge sovereign, instead of being trapped, regardless of - // `Transact` success or not. - SetAppendix(Xcm(vec![ - RefundSurplus, - DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location }, - ])), - // Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`. - DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), - // Change origin to the bridge. - UniversalOrigin(GlobalConsensus(network)), - // Call create_asset on foreign assets pallet. - Transact { - origin_kind: OriginKind::Xcm, - fallback_max_weight: None, - call: ( - create_call_index, - asset_id, - MultiAddress::<[u8; 32], ()>::Id(owner), - MINIMUM_DEPOSIT, - ) - .encode() - .into(), - }, - // Forward message id to Asset Hub - SetTopic(message_id.into()), - // Once the program ends here, appendix program will run, which will deposit any - // leftover fee to snowbridge sovereign. - ] - .into(); - - (xcm, total_amount.into()) - } - - fn convert_send_token( - message_id: H256, - chain_id: u64, - token: H160, - destination: Destination, - amount: u128, - asset_hub_fee: u128, - ) -> (Xcm<()>, Balance) { - let network = Ethereum { chain_id }; - let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); - let asset: Asset = (Self::convert_token_address(network, token), amount).into(); - - let (dest_para_id, beneficiary, dest_para_fee) = match destination { - // Final destination is a 32-byte account on AssetHub - Destination::AccountId32 { id } => - (None, Location::new(0, [AccountId32 { network: None, id }]), 0), - // Final destination is a 32-byte account on a sibling of AssetHub - Destination::ForeignAccountId32 { para_id, id, fee } => ( - Some(para_id), - Location::new(0, [AccountId32 { network: None, id }]), - // Total fee needs to cover execution on AssetHub and Sibling - fee, - ), - // Final destination is a 20-byte account on a sibling of AssetHub - Destination::ForeignAccountId20 { para_id, id, fee } => ( - Some(para_id), - Location::new(0, [AccountKey20 { network: None, key: id }]), - // Total fee needs to cover execution on AssetHub and Sibling - fee, - ), - }; - - let total_fees = asset_hub_fee.saturating_add(dest_para_fee); - let total_fee_asset: Asset = (Location::parent(), total_fees).into(); - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); - - let mut instructions = vec![ - ReceiveTeleportedAsset(total_fee_asset.into()), - BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, - DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), - UniversalOrigin(GlobalConsensus(network)), - ReserveAssetDeposited(asset.clone().into()), - ClearOrigin, - ]; - - match dest_para_id { - Some(dest_para_id) => { - let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into(); - let bridge_location = Location::new(2, GlobalConsensus(network)); - - instructions.extend(vec![ - // After program finishes deposit any leftover assets to the snowbridge - // sovereign. - SetAppendix(Xcm(vec![DepositAsset { - assets: Wild(AllCounted(2)), - beneficiary: bridge_location, - }])), - // Perform a deposit reserve to send to destination chain. - DepositReserveAsset { - assets: Definite(vec![dest_para_fee_asset.clone(), asset].into()), - dest: Location::new(1, [Parachain(dest_para_id)]), - xcm: vec![ - // Buy execution on target. - BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited }, - // Deposit assets to beneficiary. - DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, - // Forward message id to destination parachain. - SetTopic(message_id.into()), - ] - .into(), - }, - ]); - }, - None => { - instructions.extend(vec![ - // Deposit both asset and fees to beneficiary so the fees will not get - // trapped. Another benefit is when fees left more than ED on AssetHub could be - // used to create the beneficiary account in case it does not exist. - DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, - ]); - }, - } - - // Forward message id to Asset Hub. - instructions.push(SetTopic(message_id.into())); - - // The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since - // they are teleported within `instructions`). - (instructions.into(), total_fees.into()) - } - - // Convert ERC20 token address to a location that can be understood by Assets Hub. - fn convert_token_address(network: NetworkId, token: H160) -> Location { - Location::new( - 2, - [GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }], - ) - } - - /// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign - /// account of the Gateway contract and either deposits those assets into a recipient account or - /// forwards the assets to another parachain. - fn convert_send_native_token( - message_id: H256, - chain_id: u64, - token_id: TokenId, - destination: Destination, - amount: u128, - asset_hub_fee: u128, - ) -> Result<(Xcm<()>, Balance), ConvertMessageError> { - let network = Ethereum { chain_id }; - let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); - - let beneficiary = match destination { - // Final destination is a 32-byte account on AssetHub - Destination::AccountId32 { id } => - Ok(Location::new(0, [AccountId32 { network: None, id }])), - // Forwarding to a destination parachain is not allowed for PNA and is validated on the - // Ethereum side. https://github.com/Snowfork/snowbridge/blob/e87ddb2215b513455c844463a25323bb9c01ff36/contracts/src/Assets.sol#L216-L224 - _ => Err(ConvertMessageError::InvalidDestination), - }?; - - let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); - - let asset_loc = - ConvertAssetId::convert(&token_id).ok_or(ConvertMessageError::InvalidToken)?; - - let mut reanchored_asset_loc = asset_loc.clone(); - reanchored_asset_loc - .reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get()) - .map_err(|_| ConvertMessageError::CannotReanchor)?; - - let asset: Asset = (reanchored_asset_loc, amount).into(); - - let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); - - let instructions = vec![ - ReceiveTeleportedAsset(total_fee_asset.clone().into()), - BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, - DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), - UniversalOrigin(GlobalConsensus(network)), - WithdrawAsset(asset.clone().into()), - // Deposit both asset and fees to beneficiary so the fees will not get - // trapped. Another benefit is when fees left more than ED on AssetHub could be - // used to create the beneficiary account in case it does not exist. - DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, - SetTopic(message_id.into()), - ]; - - // `total_fees` to burn on this chain when sending `instructions` to run on AH (which also - // teleport fees) - Ok((instructions.into(), asset_hub_fee.into())) - } -} - pub struct EthereumLocationsConverterFor(PhantomData); impl ConvertLocation for EthereumLocationsConverterFor where @@ -478,3 +35,79 @@ impl EthereumLocationsConverterFor { (b"ethereum-chain", chain_id, key).using_encoded(blake2_256) } } + +pub type CallIndex = [u8; 2]; + +#[cfg(test)] +mod tests { + use crate::inbound::{CallIndex, EthereumLocationsConverterFor}; + use frame_support::{assert_ok, parameter_types}; + use hex_literal::hex; + use xcm::prelude::*; + use xcm_executor::traits::ConvertLocation; + + const NETWORK: NetworkId = Ethereum { chain_id: 11155111 }; + + parameter_types! { + pub EthereumNetwork: NetworkId = NETWORK; + + pub const CreateAssetCall: CallIndex = [1, 1]; + pub const CreateAssetExecutionFee: u128 = 123; + pub const CreateAssetDeposit: u128 = 891; + pub const SendTokenExecutionFee: u128 = 592; + } + + #[test] + fn test_contract_location_with_network_converts_successfully() { + let expected_account: [u8; 32] = + hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d"); + let contract_location = Location::new(2, [GlobalConsensus(NETWORK)]); + + let account = + EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location) + .unwrap(); + + assert_eq!(account, expected_account); + } + + #[test] + fn test_contract_location_with_incorrect_location_fails_convert() { + let contract_location = Location::new(2, [GlobalConsensus(Polkadot), Parachain(1000)]); + + assert_eq!( + EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location), + None, + ); + } + + #[test] + fn test_reanchor_all_assets() { + let ethereum_context: InteriorLocation = [GlobalConsensus(Ethereum { chain_id: 1 })].into(); + let ethereum = Location::new(2, ethereum_context.clone()); + let ah_context: InteriorLocation = [GlobalConsensus(Polkadot), Parachain(1000)].into(); + let global_ah = Location::new(1, ah_context.clone()); + let assets = vec![ + // DOT + Location::new(1, []), + // GLMR (Some Polkadot parachain currency) + Location::new(1, [Parachain(2004)]), + // AH asset + Location::new(0, [PalletInstance(50), GeneralIndex(42)]), + // KSM + Location::new(2, [GlobalConsensus(Kusama)]), + // KAR (Some Kusama parachain currency) + Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]), + ]; + for asset in assets.iter() { + // reanchor logic in pallet_xcm on AH + let mut reanchored_asset = asset.clone(); + assert_ok!(reanchored_asset.reanchor(ðereum, &ah_context)); + // reanchor back to original location in context of Ethereum + let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone(); + assert_ok!( + reanchored_asset_with_ethereum_context.reanchor(&global_ah, ðereum_context) + ); + assert_eq!(reanchored_asset_with_ethereum_context, asset.clone()); + } + } +} diff --git a/bridges/snowbridge/primitives/router/src/inbound/payload.rs b/bridges/snowbridge/primitives/router/src/inbound/payload.rs new file mode 100644 index 000000000000..3caa5641427f --- /dev/null +++ b/bridges/snowbridge/primitives/router/src/inbound/payload.rs @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +use crate::inbound::v2::{ + Asset::{ForeignTokenERC20, NativeTokenERC20}, + Message, +}; +use alloy_sol_types::{sol, SolType}; +use sp_core::{RuntimeDebug, H160, H256}; + +sol! { + struct AsNativeTokenERC20 { + address token_id; + uint128 value; + } +} + +sol! { + struct AsForeignTokenERC20 { + bytes32 token_id; + uint128 value; + } +} + +sol! { + struct EthereumAsset { + uint8 kind; + bytes data; + } +} + +sol! { + struct Payload { + address origin; + EthereumAsset[] assets; + bytes xcm; + bytes claimer; + uint128 value; + uint128 executionFee; + uint128 relayerFee; + } +} + +#[derive(Copy, Clone, RuntimeDebug)] +pub struct PayloadDecodeError; +impl TryFrom<&[u8]> for Message { + type Error = PayloadDecodeError; + + fn try_from(encoded_payload: &[u8]) -> Result { + let decoded_payload = + Payload::abi_decode(&encoded_payload, true).map_err(|_| PayloadDecodeError)?; + + let mut substrate_assets = vec![]; + + for asset in decoded_payload.assets { + match asset.kind { + 0 => { + let native_data = AsNativeTokenERC20::abi_decode(&asset.data, true) + .map_err(|_| PayloadDecodeError)?; + substrate_assets.push(NativeTokenERC20 { + token_id: H160::from(native_data.token_id.as_ref()), + value: native_data.value, + }); + }, + 1 => { + let foreign_data = AsForeignTokenERC20::abi_decode(&asset.data, true) + .map_err(|_| PayloadDecodeError)?; + substrate_assets.push(ForeignTokenERC20 { + token_id: H256::from(foreign_data.token_id.as_ref()), + value: foreign_data.value, + }); + }, + _ => return Err(PayloadDecodeError), + } + } + + let mut claimer = None; + if decoded_payload.claimer.len() > 0 { + claimer = Some(decoded_payload.claimer); + } + + Ok(Self { + origin: H160::from(decoded_payload.origin.as_ref()), + assets: substrate_assets, + xcm: decoded_payload.xcm, + claimer, + value: decoded_payload.value, + execution_fee: decoded_payload.executionFee, + relayer_fee: decoded_payload.relayerFee, + }) + } +} diff --git a/bridges/snowbridge/primitives/router/src/inbound/v1.rs b/bridges/snowbridge/primitives/router/src/inbound/v1.rs new file mode 100644 index 000000000000..b78c9ca78b43 --- /dev/null +++ b/bridges/snowbridge/primitives/router/src/inbound/v1.rs @@ -0,0 +1,523 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Converts messages from Ethereum to XCM messages + +use crate::inbound::{CallIndex, EthereumLocationsConverterFor}; +use codec::{Decode, Encode}; +use core::marker::PhantomData; +use frame_support::{traits::tokens::Balance as BalanceT, PalletError}; +use scale_info::TypeInfo; +use snowbridge_core::TokenId; +use sp_core::{Get, RuntimeDebug, H160, H256}; +use sp_runtime::{traits::MaybeEquivalence, MultiAddress}; +use sp_std::prelude::*; +use xcm::prelude::{Junction::AccountKey20, *}; + +const MINIMUM_DEPOSIT: u128 = 1; + +/// Messages from Ethereum are versioned. This is because in future, +/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly. +/// Instead having BridgeHub transcode the messages into XCM. +#[derive(Clone, Encode, Decode, RuntimeDebug)] +pub enum VersionedMessage { + V1(MessageV1), +} + +/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are +/// self-contained, in that they can be transcoded using only information in the message. +#[derive(Clone, Encode, Decode, RuntimeDebug)] +pub struct MessageV1 { + /// EIP-155 chain id of the origin Ethereum network + pub chain_id: u64, + /// The command originating from the Gateway contract + pub command: Command, +} + +#[derive(Clone, Encode, Decode, RuntimeDebug)] +pub enum Command { + /// Register a wrapped token on the AssetHub `ForeignAssets` pallet + RegisterToken { + /// The address of the ERC20 token to be bridged over to AssetHub + token: H160, + /// XCM execution fee on AssetHub + fee: u128, + }, + /// Send Ethereum token to AssetHub or another parachain + SendToken { + /// The address of the ERC20 token to be bridged over to AssetHub + token: H160, + /// The destination for the transfer + destination: Destination, + /// Amount to transfer + amount: u128, + /// XCM execution fee on AssetHub + fee: u128, + }, + /// Send Polkadot token back to the original parachain + SendNativeToken { + /// The Id of the token + token_id: TokenId, + /// The destination for the transfer + destination: Destination, + /// Amount to transfer + amount: u128, + /// XCM execution fee on AssetHub + fee: u128, + }, +} + +/// Destination for bridged tokens +#[derive(Clone, Encode, Decode, RuntimeDebug)] +pub enum Destination { + /// The funds will be deposited into account `id` on AssetHub + AccountId32 { id: [u8; 32] }, + /// The funds will deposited into the sovereign account of destination parachain `para_id` on + /// AssetHub, Account `id` on the destination parachain will receive the funds via a + /// reserve-backed transfer. See + ForeignAccountId32 { + para_id: u32, + id: [u8; 32], + /// XCM execution fee on final destination + fee: u128, + }, + /// The funds will deposited into the sovereign account of destination parachain `para_id` on + /// AssetHub, Account `id` on the destination parachain will receive the funds via a + /// reserve-backed transfer. See + ForeignAccountId20 { + para_id: u32, + id: [u8; 20], + /// XCM execution fee on final destination + fee: u128, + }, +} + +pub struct MessageToXcm< + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + ConvertAssetId, + EthereumUniversalLocation, + GlobalAssetHubLocation, +> where + CreateAssetCall: Get, + CreateAssetDeposit: Get, + Balance: BalanceT, + ConvertAssetId: MaybeEquivalence, + EthereumUniversalLocation: Get, + GlobalAssetHubLocation: Get, +{ + _phantom: PhantomData<( + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + ConvertAssetId, + EthereumUniversalLocation, + GlobalAssetHubLocation, + )>, +} + +/// Reason why a message conversion failed. +#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)] +pub enum ConvertMessageError { + /// The message version is not supported for conversion. + UnsupportedVersion, + InvalidDestination, + InvalidToken, + /// The fee asset is not supported for conversion. + UnsupportedFeeAsset, + CannotReanchor, +} + +/// convert the inbound message to xcm which will be forwarded to the destination chain +pub trait ConvertMessage { + type Balance: BalanceT + From; + type AccountId; + /// Converts a versioned message into an XCM message and an optional topicID + fn convert( + message_id: H256, + message: VersionedMessage, + ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>; +} + +impl< + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + ConvertAssetId, + EthereumUniversalLocation, + GlobalAssetHubLocation, + > ConvertMessage + for MessageToXcm< + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + ConvertAssetId, + EthereumUniversalLocation, + GlobalAssetHubLocation, + > +where + CreateAssetCall: Get, + CreateAssetDeposit: Get, + InboundQueuePalletInstance: Get, + Balance: BalanceT + From, + AccountId: Into<[u8; 32]>, + ConvertAssetId: MaybeEquivalence, + EthereumUniversalLocation: Get, + GlobalAssetHubLocation: Get, +{ + type Balance = Balance; + type AccountId = AccountId; + + fn convert( + message_id: H256, + message: VersionedMessage, + ) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> { + use Command::*; + use VersionedMessage::*; + match message { + V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) => + Ok(Self::convert_register_token(message_id, chain_id, token, fee)), + V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) => + Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)), + V1(MessageV1 { + chain_id, + command: SendNativeToken { token_id, destination, amount, fee }, + }) => Self::convert_send_native_token( + message_id, + chain_id, + token_id, + destination, + amount, + fee, + ), + } + } +} + +impl< + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + ConvertAssetId, + EthereumUniversalLocation, + GlobalAssetHubLocation, + > + MessageToXcm< + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + ConvertAssetId, + EthereumUniversalLocation, + GlobalAssetHubLocation, + > +where + CreateAssetCall: Get, + CreateAssetDeposit: Get, + InboundQueuePalletInstance: Get, + Balance: BalanceT + From, + AccountId: Into<[u8; 32]>, + ConvertAssetId: MaybeEquivalence, + EthereumUniversalLocation: Get, + GlobalAssetHubLocation: Get, +{ + fn convert_register_token( + message_id: H256, + chain_id: u64, + token: H160, + fee: u128, + ) -> (Xcm<()>, Balance) { + let network = Ethereum { chain_id }; + let xcm_fee: Asset = (Location::parent(), fee).into(); + let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into(); + + let total_amount = fee + CreateAssetDeposit::get(); + let total: Asset = (Location::parent(), total_amount).into(); + + let bridge_location = Location::new(2, GlobalConsensus(network)); + + let owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&chain_id); + let asset_id = Self::convert_token_address(network, token); + let create_call_index: [u8; 2] = CreateAssetCall::get(); + let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); + + let xcm: Xcm<()> = vec![ + // Teleport required fees. + ReceiveTeleportedAsset(total.into()), + // Pay for execution. + BuyExecution { fees: xcm_fee, weight_limit: Unlimited }, + // Fund the snowbridge sovereign with the required deposit for creation. + DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location.clone() }, + // This `SetAppendix` ensures that `xcm_fee` not spent by `Transact` will be + // deposited to snowbridge sovereign, instead of being trapped, regardless of + // `Transact` success or not. + SetAppendix(Xcm(vec![ + RefundSurplus, + DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location }, + ])), + // Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`. + DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), + // Change origin to the bridge. + UniversalOrigin(GlobalConsensus(network)), + // Call create_asset on foreign assets pallet. + Transact { + origin_kind: OriginKind::Xcm, + fallback_max_weight: None, + call: ( + create_call_index, + asset_id, + MultiAddress::<[u8; 32], ()>::Id(owner), + MINIMUM_DEPOSIT, + ) + .encode() + .into(), + }, + // Forward message id to Asset Hub + SetTopic(message_id.into()), + // Once the program ends here, appendix program will run, which will deposit any + // leftover fee to snowbridge sovereign. + ] + .into(); + + (xcm, total_amount.into()) + } + + fn convert_send_token( + message_id: H256, + chain_id: u64, + token: H160, + destination: Destination, + amount: u128, + asset_hub_fee: u128, + ) -> (Xcm<()>, Balance) { + let network = Ethereum { chain_id }; + let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); + let asset: Asset = (Self::convert_token_address(network, token), amount).into(); + + let (dest_para_id, beneficiary, dest_para_fee) = match destination { + // Final destination is a 32-byte account on AssetHub + Destination::AccountId32 { id } => + (None, Location::new(0, [AccountId32 { network: None, id }]), 0), + // Final destination is a 32-byte account on a sibling of AssetHub + Destination::ForeignAccountId32 { para_id, id, fee } => ( + Some(para_id), + Location::new(0, [AccountId32 { network: None, id }]), + // Total fee needs to cover execution on AssetHub and Sibling + fee, + ), + // Final destination is a 20-byte account on a sibling of AssetHub + Destination::ForeignAccountId20 { para_id, id, fee } => ( + Some(para_id), + Location::new(0, [AccountKey20 { network: None, key: id }]), + // Total fee needs to cover execution on AssetHub and Sibling + fee, + ), + }; + + let total_fees = asset_hub_fee.saturating_add(dest_para_fee); + let total_fee_asset: Asset = (Location::parent(), total_fees).into(); + let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); + + let mut instructions = vec![ + ReceiveTeleportedAsset(total_fee_asset.into()), + BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, + DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), + UniversalOrigin(GlobalConsensus(network)), + ReserveAssetDeposited(asset.clone().into()), + ClearOrigin, + ]; + + match dest_para_id { + Some(dest_para_id) => { + let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into(); + let bridge_location = Location::new(2, GlobalConsensus(network)); + + instructions.extend(vec![ + // After program finishes deposit any leftover assets to the snowbridge + // sovereign. + SetAppendix(Xcm(vec![DepositAsset { + assets: Wild(AllCounted(2)), + beneficiary: bridge_location, + }])), + // Perform a deposit reserve to send to destination chain. + DepositReserveAsset { + assets: Definite(vec![dest_para_fee_asset.clone(), asset].into()), + dest: Location::new(1, [Parachain(dest_para_id)]), + xcm: vec![ + // Buy execution on target. + BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited }, + // Deposit assets to beneficiary. + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + // Forward message id to destination parachain. + SetTopic(message_id.into()), + ] + .into(), + }, + ]); + }, + None => { + instructions.extend(vec![ + // Deposit both asset and fees to beneficiary so the fees will not get + // trapped. Another benefit is when fees left more than ED on AssetHub could be + // used to create the beneficiary account in case it does not exist. + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + ]); + }, + } + + // Forward message id to Asset Hub. + instructions.push(SetTopic(message_id.into())); + + // The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since + // they are teleported within `instructions`). + (instructions.into(), total_fees.into()) + } + + // Convert ERC20 token address to a location that can be understood by Assets Hub. + fn convert_token_address(network: NetworkId, token: H160) -> Location { + Location::new( + 2, + [GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }], + ) + } + + /// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign + /// account of the Gateway contract and either deposits those assets into a recipient account or + /// forwards the assets to another parachain. + fn convert_send_native_token( + message_id: H256, + chain_id: u64, + token_id: TokenId, + destination: Destination, + amount: u128, + asset_hub_fee: u128, + ) -> Result<(Xcm<()>, Balance), ConvertMessageError> { + let network = Ethereum { chain_id }; + let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); + + let beneficiary = match destination { + // Final destination is a 32-byte account on AssetHub + Destination::AccountId32 { id } => + Ok(Location::new(0, [AccountId32 { network: None, id }])), + // Forwarding to a destination parachain is not allowed for PNA and is validated on the + // Ethereum side. https://github.com/Snowfork/snowbridge/blob/e87ddb2215b513455c844463a25323bb9c01ff36/contracts/src/Assets.sol#L216-L224 + _ => Err(ConvertMessageError::InvalidDestination), + }?; + + let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into(); + + let asset_loc = + ConvertAssetId::convert(&token_id).ok_or(ConvertMessageError::InvalidToken)?; + + let mut reanchored_asset_loc = asset_loc.clone(); + reanchored_asset_loc + .reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get()) + .map_err(|_| ConvertMessageError::CannotReanchor)?; + + let asset: Asset = (reanchored_asset_loc, amount).into(); + + let inbound_queue_pallet_index = InboundQueuePalletInstance::get(); + + let instructions = vec![ + ReceiveTeleportedAsset(total_fee_asset.clone().into()), + BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited }, + DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()), + UniversalOrigin(GlobalConsensus(network)), + WithdrawAsset(asset.clone().into()), + // Deposit both asset and fees to beneficiary so the fees will not get + // trapped. Another benefit is when fees left more than ED on AssetHub could be + // used to create the beneficiary account in case it does not exist. + DepositAsset { assets: Wild(AllCounted(2)), beneficiary }, + SetTopic(message_id.into()), + ]; + + // `total_fees` to burn on this chain when sending `instructions` to run on AH (which also + // teleport fees) + Ok((instructions.into(), asset_hub_fee.into())) + } +} + +#[cfg(test)] +mod tests { + use crate::inbound::{CallIndex, EthereumLocationsConverterFor}; + use frame_support::{assert_ok, parameter_types}; + use hex_literal::hex; + use xcm::prelude::*; + use xcm_executor::traits::ConvertLocation; + + const NETWORK: NetworkId = Ethereum { chain_id: 11155111 }; + + parameter_types! { + pub EthereumNetwork: NetworkId = NETWORK; + + pub const CreateAssetCall: CallIndex = [1, 1]; + pub const CreateAssetExecutionFee: u128 = 123; + pub const CreateAssetDeposit: u128 = 891; + pub const SendTokenExecutionFee: u128 = 592; + } + + #[test] + fn test_contract_location_with_network_converts_successfully() { + let expected_account: [u8; 32] = + hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d"); + let contract_location = Location::new(2, [GlobalConsensus(NETWORK)]); + + let account = + EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location) + .unwrap(); + + assert_eq!(account, expected_account); + } + + #[test] + fn test_contract_location_with_incorrect_location_fails_convert() { + let contract_location = Location::new(2, [GlobalConsensus(Polkadot), Parachain(1000)]); + + assert_eq!( + EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location), + None, + ); + } + + #[test] + fn test_reanchor_all_assets() { + let ethereum_context: InteriorLocation = [GlobalConsensus(Ethereum { chain_id: 1 })].into(); + let ethereum = Location::new(2, ethereum_context.clone()); + let ah_context: InteriorLocation = [GlobalConsensus(Polkadot), Parachain(1000)].into(); + let global_ah = Location::new(1, ah_context.clone()); + let assets = vec![ + // DOT + Location::new(1, []), + // GLMR (Some Polkadot parachain currency) + Location::new(1, [Parachain(2004)]), + // AH asset + Location::new(0, [PalletInstance(50), GeneralIndex(42)]), + // KSM + Location::new(2, [GlobalConsensus(Kusama)]), + // KAR (Some Kusama parachain currency) + Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]), + ]; + for asset in assets.iter() { + // reanchor logic in pallet_xcm on AH + let mut reanchored_asset = asset.clone(); + assert_ok!(reanchored_asset.reanchor(ðereum, &ah_context)); + // reanchor back to original location in context of Ethereum + let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone(); + assert_ok!( + reanchored_asset_with_ethereum_context.reanchor(&global_ah, ðereum_context) + ); + assert_eq!(reanchored_asset_with_ethereum_context, asset.clone()); + } + } +} diff --git a/bridges/snowbridge/primitives/router/src/inbound/v2.rs b/bridges/snowbridge/primitives/router/src/inbound/v2.rs new file mode 100644 index 000000000000..eb5cdcece065 --- /dev/null +++ b/bridges/snowbridge/primitives/router/src/inbound/v2.rs @@ -0,0 +1,633 @@ +// SPDX-License-Identifier: Apache-2.0 +// SPDX-FileCopyrightText: 2023 Snowfork +//! Converts messages from Ethereum to XCM messages + +use codec::{Decode, DecodeLimit, Encode}; +use core::marker::PhantomData; +use frame_support::PalletError; +use scale_info::TypeInfo; +use snowbridge_core::TokenId; +use sp_core::{Get, RuntimeDebug, H160, H256}; +use sp_runtime::traits::MaybeEquivalence; +use sp_std::prelude::*; +use xcm::{ + prelude::{Asset as XcmAsset, Junction::AccountKey20, *}, + MAX_XCM_DECODE_DEPTH, +}; + +const LOG_TARGET: &str = "snowbridge-router-primitives"; + +/// The ethereum side sends messages which are transcoded into XCM on BH. These messages are +/// self-contained, in that they can be transcoded using only information in the message. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub struct Message { + /// The origin address + pub origin: H160, + /// The assets + pub assets: Vec, + /// The command originating from the Gateway contract + pub xcm: Vec, + /// The claimer in the case that funds get trapped. + pub claimer: Option>, + /// The full value of the assets. + pub value: u128, + /// Fee in eth to cover the xcm execution on AH. + pub execution_fee: u128, + /// Relayer reward in eth. Needs to cover all costs of sending a message. + pub relayer_fee: u128, +} + +/// An asset that will be transacted on AH. The asset will be reserved/withdrawn and placed into +/// the holding register. The user needs to provide additional xcm to deposit the asset +/// in a beneficiary account. +#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +pub enum Asset { + NativeTokenERC20 { + /// The native token ID + token_id: H160, + /// The monetary value of the asset + value: u128, + }, + ForeignTokenERC20 { + /// The foreign token ID + token_id: H256, + /// The monetary value of the asset + value: u128, + }, +} + +/// Reason why a message conversion failed. +#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug, PartialEq)] +pub enum ConvertMessageError { + /// Invalid foreign ERC-20 token ID + InvalidAsset, + /// Cannot reachor a foreign ERC-20 asset location. + CannotReanchor, +} + +pub trait ConvertMessage { + fn convert( + message: Message, + origin_account: Location, + ) -> Result<(Xcm<()>, u128), ConvertMessageError>; +} + +pub struct MessageToXcm< + EthereumNetwork, + InboundQueuePalletInstance, + ConvertAssetId, + GatewayProxyAddress, + EthereumUniversalLocation, + GlobalAssetHubLocation, +> where + EthereumNetwork: Get, + InboundQueuePalletInstance: Get, + ConvertAssetId: MaybeEquivalence, + GatewayProxyAddress: Get, + EthereumUniversalLocation: Get, + GlobalAssetHubLocation: Get, +{ + _phantom: PhantomData<( + EthereumNetwork, + InboundQueuePalletInstance, + ConvertAssetId, + GatewayProxyAddress, + EthereumUniversalLocation, + GlobalAssetHubLocation, + )>, +} + +impl< + EthereumNetwork, + InboundQueuePalletInstance, + ConvertAssetId, + GatewayProxyAddress, + EthereumUniversalLocation, + GlobalAssetHubLocation, + > ConvertMessage + for MessageToXcm< + EthereumNetwork, + InboundQueuePalletInstance, + ConvertAssetId, + GatewayProxyAddress, + EthereumUniversalLocation, + GlobalAssetHubLocation, + > +where + EthereumNetwork: Get, + InboundQueuePalletInstance: Get, + ConvertAssetId: MaybeEquivalence, + GatewayProxyAddress: Get, + EthereumUniversalLocation: Get, + GlobalAssetHubLocation: Get, +{ + fn convert( + message: Message, + origin_account_location: Location, + ) -> Result<(Xcm<()>, u128), ConvertMessageError> { + let mut message_xcm: Xcm<()> = Xcm::new(); + if message.xcm.len() > 0 { + // Allow xcm decode failure so that assets can be trapped on AH instead of this + // message failing but funds are already locked on Ethereum. + if let Ok(versioned_xcm) = VersionedXcm::<()>::decode_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut message.xcm.as_ref(), + ) { + if let Ok(decoded_xcm) = versioned_xcm.try_into() { + message_xcm = decoded_xcm; + } + } + } + + log::debug!(target: LOG_TARGET,"xcm decoded as {:?}", message_xcm); + + let network = EthereumNetwork::get(); + + // use eth as asset + let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); + let fee: XcmAsset = (fee_asset.clone(), message.execution_fee).into(); + let eth: XcmAsset = + (fee_asset.clone(), message.execution_fee.saturating_add(message.value)).into(); + let mut instructions = vec![ + DescendOrigin(PalletInstance(InboundQueuePalletInstance::get()).into()), + UniversalOrigin(GlobalConsensus(network)), + ReserveAssetDeposited(eth.into()), + PayFees { asset: fee }, + ]; + let mut reserve_assets = vec![]; + let mut withdraw_assets = vec![]; + + let mut refund_surplus_to = origin_account_location; + + if let Some(claimer) = message.claimer { + // If the claimer can be decoded, add it to the message. If the claimer decoding fails, + // do not add it to the message, because it will cause the xcm to fail. + if let Ok(claimer) = Junction::decode(&mut claimer.as_ref()) { + let claimer_location: Location = Location::new(0, [claimer.into()]); + refund_surplus_to = claimer_location.clone(); + instructions.push(SetAssetClaimer { location: claimer_location }); + } + } + + for asset in &message.assets { + match asset { + Asset::NativeTokenERC20 { token_id, value } => { + let token_location: Location = Location::new( + 2, + [ + GlobalConsensus(EthereumNetwork::get()), + AccountKey20 { network: None, key: (*token_id).into() }, + ], + ); + let asset: XcmAsset = (token_location, *value).into(); + reserve_assets.push(asset); + }, + Asset::ForeignTokenERC20 { token_id, value } => { + let asset_loc = ConvertAssetId::convert(&token_id) + .ok_or(ConvertMessageError::InvalidAsset)?; + let mut reanchored_asset_loc = asset_loc.clone(); + reanchored_asset_loc + .reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get()) + .map_err(|_| ConvertMessageError::CannotReanchor)?; + let asset: XcmAsset = (reanchored_asset_loc, *value).into(); + withdraw_assets.push(asset); + }, + } + } + + if reserve_assets.len() > 0 { + instructions.push(ReserveAssetDeposited(reserve_assets.into())); + } + if withdraw_assets.len() > 0 { + instructions.push(WithdrawAsset(withdraw_assets.into())); + } + + // If the message origin is not the gateway proxy contract, set the origin to + // the original sender on Ethereum. Important to be before the arbitrary XCM that is + // appended to the message on the next line. + if message.origin != GatewayProxyAddress::get() { + instructions.push(DescendOrigin( + AccountKey20 { key: message.origin.into(), network: None }.into(), + )); + } + + // Add the XCM sent in the message to the end of the xcm instruction + instructions.extend(message_xcm.0); + + let appendix = vec![ + RefundSurplus, + // Refund excess fees to the claimer, if present, otherwise the relayer + DepositAsset { + assets: Wild(AllOf { id: AssetId(fee_asset.into()), fun: WildFungible }), + beneficiary: refund_surplus_to, + }, + ]; + + instructions.extend(appendix); + + Ok((instructions.into(), message.relayer_fee)) + } +} + +#[cfg(test)] +mod tests { + use crate::inbound::v2::{ + Asset::{ForeignTokenERC20, NativeTokenERC20}, + ConvertMessage, ConvertMessageError, Message, MessageToXcm, XcmAsset, + }; + use codec::Encode; + use frame_support::{assert_err, assert_ok, parameter_types}; + use hex_literal::hex; + use snowbridge_core::TokenId; + use sp_core::{H160, H256}; + use sp_runtime::traits::MaybeEquivalence; + use xcm::{opaque::latest::WESTEND_GENESIS_HASH, prelude::*}; + const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"]; + parameter_types! { + pub const EthereumNetwork: xcm::v5::NetworkId = xcm::v5::NetworkId::Ethereum { chain_id: 11155111 }; + pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS); + pub const InboundQueuePalletInstance: u8 = 84; + pub AssetHubLocation: InteriorLocation = Parachain(1000).into(); + pub UniversalLocation: InteriorLocation = + [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)), Parachain(1002)].into(); + pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)),Parachain(1000)]); + } + + pub struct MockTokenIdConvert; + impl MaybeEquivalence for MockTokenIdConvert { + fn convert(_id: &TokenId) -> Option { + Some(Location::parent()) + } + fn convert_back(_loc: &Location) -> Option { + None + } + } + + pub struct MockFailedTokenConvert; + impl MaybeEquivalence for MockFailedTokenConvert { + fn convert(_id: &TokenId) -> Option { + None + } + fn convert_back(_loc: &Location) -> Option { + None + } + } + + #[test] + fn test_successful_message() { + let origin_account = + Location::new(0, [AccountId32 { network: None, id: H256::random().into() }]); + let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); + let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); + let foreign_token_id: H256 = + hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into(); + let beneficiary = + hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); + let message_id: H256 = + hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into(); + let token_value = 3_000_000_000_000u128; + let assets = vec![ + NativeTokenERC20 { token_id: native_token_id, value: token_value }, + ForeignTokenERC20 { token_id: foreign_token_id, value: token_value }, + ]; + let instructions = vec![ + DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }, + SetTopic(message_id.into()), + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_xcm = VersionedXcm::V5(xcm); + let claimer_account = AccountId32 { network: None, id: H256::random().into() }; + let claimer: Option> = Some(claimer_account.clone().encode()); + let value = 6_000_000_000_000u128; + let execution_fee = 1_000_000_000_000u128; + let relayer_fee = 5_000_000_000_000u128; + + let message = Message { + origin: origin.clone(), + assets, + xcm: versioned_xcm.encode(), + claimer, + value, + execution_fee, + relayer_fee, + }; + + let result = MessageToXcm::< + EthereumNetwork, + InboundQueuePalletInstance, + MockTokenIdConvert, + GatewayAddress, + UniversalLocation, + AssetHubFromEthereum, + >::convert(message, origin_account); + + assert_ok!(result.clone()); + + let (xcm, _) = result.unwrap(); + + let mut instructions = xcm.into_iter(); + + let mut asset_claimer_found = false; + let mut pay_fees_found = false; + let mut descend_origin_found = 0; + let mut reserve_deposited_found = 0; + let mut withdraw_assets_found = 0; + while let Some(instruction) = instructions.next() { + if let SetAssetClaimer { ref location } = instruction { + assert_eq!(Location::new(0, [claimer_account]), location.clone()); + asset_claimer_found = true; + } + if let DescendOrigin(ref location) = instruction { + descend_origin_found = descend_origin_found + 1; + // The second DescendOrigin should be the message.origin (sender) + if descend_origin_found == 2 { + let junctions: Junctions = + AccountKey20 { key: origin.into(), network: None }.into(); + assert_eq!(junctions, location.clone()); + } + } + if let PayFees { ref asset } = instruction { + let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); + assert_eq!(asset.id, AssetId(fee_asset)); + assert_eq!(asset.fun, Fungible(execution_fee)); + pay_fees_found = true; + } + if let ReserveAssetDeposited(ref reserve_assets) = instruction { + reserve_deposited_found = reserve_deposited_found + 1; + if reserve_deposited_found == 1 { + let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); + let fee: XcmAsset = (fee_asset, execution_fee + value).into(); + let fee_assets: Assets = fee.into(); + assert_eq!(fee_assets, reserve_assets.clone()); + } + if reserve_deposited_found == 2 { + let token_asset = Location::new( + 2, + [ + GlobalConsensus(EthereumNetwork::get()), + AccountKey20 { network: None, key: native_token_id.into() }, + ], + ); + let token: XcmAsset = (token_asset, token_value).into(); + let token_assets: Assets = token.into(); + assert_eq!(token_assets, reserve_assets.clone()); + } + } + if let WithdrawAsset(ref withdraw_assets) = instruction { + withdraw_assets_found = withdraw_assets_found + 1; + let token_asset = Location::new(2, Here); + let token: XcmAsset = (token_asset, token_value).into(); + let token_assets: Assets = token.into(); + assert_eq!(token_assets, withdraw_assets.clone()); + } + } + // SetAssetClaimer must be in the message. + assert!(asset_claimer_found); + // PayFees must be in the message. + assert!(pay_fees_found); + // The first DescendOrigin to descend into the InboundV2 pallet index and the DescendOrigin + // into the message.origin + assert!(descend_origin_found == 2); + // Expecting two ReserveAssetDeposited instructions, one for the fee and one for the token + // being transferred. + assert!(reserve_deposited_found == 2); + // Expecting one WithdrawAsset for the foreign ERC-20 + assert!(withdraw_assets_found == 1); + } + + #[test] + fn test_message_with_gateway_origin_does_not_descend_origin_into_sender() { + let origin_account = + Location::new(0, [AccountId32 { network: None, id: H256::random().into() }]); + let origin: H160 = GatewayAddress::get(); + let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into(); + let foreign_token_id: H256 = + hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into(); + let beneficiary = + hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); + let message_id: H256 = + hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into(); + let token_value = 3_000_000_000_000u128; + let assets = vec![ + NativeTokenERC20 { token_id: native_token_id, value: token_value }, + ForeignTokenERC20 { token_id: foreign_token_id, value: token_value }, + ]; + let instructions = vec![ + DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }, + SetTopic(message_id.into()), + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_xcm = VersionedXcm::V5(xcm); + let claimer_account = AccountId32 { network: None, id: H256::random().into() }; + let claimer: Option> = Some(claimer_account.clone().encode()); + let value = 6_000_000_000_000u128; + let execution_fee = 1_000_000_000_000u128; + let relayer_fee = 5_000_000_000_000u128; + + let message = Message { + origin: origin.clone(), + assets, + xcm: versioned_xcm.encode(), + claimer, + value, + execution_fee, + relayer_fee, + }; + + let result = MessageToXcm::< + EthereumNetwork, + InboundQueuePalletInstance, + MockTokenIdConvert, + GatewayAddress, + UniversalLocation, + AssetHubFromEthereum, + >::convert(message, origin_account); + + assert_ok!(result.clone()); + + let (xcm, _) = result.unwrap(); + + let mut instructions = xcm.into_iter(); + let mut commands_found = 0; + while let Some(instruction) = instructions.next() { + if let DescendOrigin(ref _location) = instruction { + commands_found = commands_found + 1; + } + } + // There should only be 1 DescendOrigin in the message. + assert!(commands_found == 1); + } + + #[test] + fn test_invalid_foreign_erc20() { + let origin_account = + Location::new(0, [AccountId32 { network: None, id: H256::random().into() }]); + let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); + let token_id: H256 = + hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into(); + let beneficiary = + hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); + let message_id: H256 = + hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into(); + let token_value = 3_000_000_000_000u128; + let assets = vec![ForeignTokenERC20 { token_id, value: token_value }]; + let instructions = vec![ + DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }, + SetTopic(message_id.into()), + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_xcm = VersionedXcm::V5(xcm); + let claimer_account = AccountId32 { network: None, id: H256::random().into() }; + let claimer: Option> = Some(claimer_account.clone().encode()); + let value = 6_000_000_000_000u128; + let execution_fee = 1_000_000_000_000u128; + let relayer_fee = 5_000_000_000_000u128; + + let message = Message { + origin, + assets, + xcm: versioned_xcm.encode(), + claimer, + value, + execution_fee, + relayer_fee, + }; + + let result = MessageToXcm::< + EthereumNetwork, + InboundQueuePalletInstance, + MockFailedTokenConvert, + GatewayAddress, + UniversalLocation, + AssetHubFromEthereum, + >::convert(message, origin_account); + + assert_err!(result.clone(), ConvertMessageError::InvalidAsset); + } + + #[test] + fn test_invalid_claimer() { + let origin_account = + Location::new(0, [AccountId32 { network: None, id: H256::random().into() }]); + let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); + let token_id: H256 = + hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into(); + let beneficiary = + hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into(); + let message_id: H256 = + hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into(); + let token_value = 3_000_000_000_000u128; + let assets = vec![ForeignTokenERC20 { token_id, value: token_value }]; + let instructions = vec![ + DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }, + SetTopic(message_id.into()), + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_xcm = VersionedXcm::V5(xcm); + // Invalid claimer location, cannot be decoded into a Junction + let claimer: Option> = + Some(hex!("43581a7d43757158624921ab0e9e112a1d7da93cbe64782d563e8e1144a06c3c").to_vec()); + let value = 6_000_000_000_000u128; + let execution_fee = 1_000_000_000_000u128; + let relayer_fee = 5_000_000_000_000u128; + + let message = Message { + origin, + assets, + xcm: versioned_xcm.encode(), + claimer, + value, + execution_fee, + relayer_fee, + }; + + let result = MessageToXcm::< + EthereumNetwork, + InboundQueuePalletInstance, + MockTokenIdConvert, + GatewayAddress, + UniversalLocation, + AssetHubFromEthereum, + >::convert(message, origin_account.clone()); + + // Invalid claimer does not break the message conversion + assert_ok!(result.clone()); + + let (xcm, _) = result.unwrap(); + + let mut result_instructions = xcm.clone().into_iter(); + + let mut found = false; + while let Some(instruction) = result_instructions.next() { + if let SetAssetClaimer { .. } = instruction { + found = true; + break; + } + } + // SetAssetClaimer should not be in the message. + assert!(!found); + + // Find the last two instructions to check the appendix is correct. + let mut second_last = None; + let mut last = None; + + for instruction in xcm.into_iter() { + second_last = last; + last = Some(instruction); + } + + // Check if both instructions are found + assert!(last.is_some()); + assert!(second_last.is_some()); + + let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]); + assert_eq!( + last, + Some(DepositAsset { + assets: Wild(AllOf { id: AssetId(fee_asset), fun: WildFungibility::Fungible }), + // beneficiary is the relayer + beneficiary: origin_account + }) + ); + } + + #[test] + fn test_invalid_xcm() { + let origin_account = + Location::new(0, [AccountId32 { network: None, id: H256::random().into() }]); + let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into(); + let token_id: H256 = + hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into(); + let token_value = 3_000_000_000_000u128; + let assets = vec![ForeignTokenERC20 { token_id, value: token_value }]; + // invalid xcm + let versioned_xcm = hex!("8b69c7e376e28114618e829a7ec7").to_vec(); + let claimer_account = AccountId32 { network: None, id: H256::random().into() }; + let claimer: Option> = Some(claimer_account.clone().encode()); + let value = 6_000_000_000_000u128; + let execution_fee = 1_000_000_000_000u128; + let relayer_fee = 5_000_000_000_000u128; + + let message = Message { + origin, + assets, + xcm: versioned_xcm, + claimer: Some(claimer.encode()), + value, + execution_fee, + relayer_fee, + }; + + let result = MessageToXcm::< + EthereumNetwork, + InboundQueuePalletInstance, + MockTokenIdConvert, + GatewayAddress, + UniversalLocation, + AssetHubFromEthereum, + >::convert(message, origin_account.clone()); + + // Invalid xcm does not break the message, allowing funds to be trapped on AH. + assert_ok!(result.clone()); + } +} diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs index c72d5045ddc0..8331138f777b 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/tests/snowbridge.rs @@ -25,7 +25,10 @@ use snowbridge_pallet_inbound_queue_fixtures::{ }; use snowbridge_pallet_system; use snowbridge_router_primitives::inbound::{ - Command, Destination, EthereumLocationsConverterFor, MessageV1, VersionedMessage, + EthereumLocationsConverterFor +}; +use snowbridge_router_primitives::inbound::v1::{ + Command, Destination, MessageV1, VersionedMessage, }; use sp_core::H256; use sp_runtime::{DispatchError::Token, TokenError::FundsUnavailable}; diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml index b87f25ac0f01..ec518175fc61 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/Cargo.toml @@ -12,6 +12,7 @@ workspace = true [dependencies] hex-literal = { workspace = true, default-features = true } +hex = { workspace = true, default-features = true } codec = { workspace = true } log = { workspace = true } scale-info = { workspace = true } @@ -43,6 +44,7 @@ rococo-westend-system-emulated-network = { workspace = true } testnet-parachains-constants = { features = ["rococo", "westend"], workspace = true, default-features = true } asset-hub-westend-runtime = { workspace = true } bridge-hub-westend-runtime = { workspace = true } +penpal-emulated-chain = { workspace = true } # Snowbridge snowbridge-core = { workspace = true } diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/mod.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/mod.rs index 6c1cdb98e8b2..cd826e3bfb29 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/mod.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/mod.rs @@ -20,6 +20,7 @@ mod claim_assets; mod register_bridged_assets; mod send_xcm; mod snowbridge; +mod snowbridge_v2; mod teleport; mod transact; diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs index ffa60a4f52e7..c5926913bd70 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge.rs @@ -19,10 +19,12 @@ use codec::{Decode, Encode}; use emulated_integration_tests_common::RESERVABLE_ASSET_ID; use frame_support::pallet_prelude::TypeInfo; use hex_literal::hex; +use penpal_emulated_chain::PARA_ID_B; use rococo_westend_system_emulated_network::asset_hub_westend_emulated_chain::genesis::AssetHubWestendAssetOwner; use snowbridge_core::{outbound::OperatingMode, AssetMetadata, TokenIdOf}; use snowbridge_router_primitives::inbound::{ - Command, Destination, EthereumLocationsConverterFor, MessageV1, VersionedMessage, + v1::{Command, Destination, MessageV1, VersionedMessage}, + EthereumLocationsConverterFor, }; use sp_core::H256; use testnet_parachains_constants::westend::snowbridge::EthereumNetwork; @@ -286,6 +288,123 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { }); } +/// Tests sending a token to a 3rd party parachain, called PenPal. The token reserve is +/// still located on AssetHub. +#[test] +fn send_token_from_ethereum_to_penpal() { + let asset_hub_sovereign = BridgeHubWestend::sovereign_account_id_of(Location::new( + 1, + [Parachain(AssetHubWestend::para_id().into())], + )); + // Fund AssetHub sovereign account so it can pay execution fees for the asset transfer + BridgeHubWestend::fund_accounts(vec![(asset_hub_sovereign.clone(), INITIAL_FUND)]); + // Fund PenPal receiver (covering ED) + PenpalB::fund_accounts(vec![(PenpalBReceiver::get(), INITIAL_FUND)]); + + PenpalB::execute_with(|| { + assert_ok!(::System::set_storage( + ::RuntimeOrigin::root(), + vec![( + PenpalCustomizableAssetFromSystemAssetHub::key().to_vec(), + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]).encode(), + )], + )); + }); + + let ethereum_network_v5: NetworkId = EthereumNetwork::get().into(); + + // The Weth asset location, identified by the contract address on Ethereum + let weth_asset_location: Location = + (Parent, Parent, ethereum_network_v5, AccountKey20 { network: None, key: WETH }).into(); + + let origin_location = (Parent, Parent, ethereum_network_v5).into(); + + // Fund ethereum sovereign on AssetHub + let ethereum_sovereign: AccountId = + EthereumLocationsConverterFor::::convert_location(&origin_location).unwrap(); + AssetHubWestend::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]); + + // Create asset on the Penpal parachain. + PenpalB::execute_with(|| { + assert_ok!(::ForeignAssets::force_create( + ::RuntimeOrigin::root(), + weth_asset_location.clone(), + asset_hub_sovereign.clone().into(), + false, + 1000, + )); + + assert!(::ForeignAssets::asset_exists( + weth_asset_location.clone() + )); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + weth_asset_location.clone().try_into().unwrap(), + asset_hub_sovereign.into(), + false, + 1, + )); + + assert!(::ForeignAssets::asset_exists( + weth_asset_location.clone().try_into().unwrap(), + )); + }); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let message = VersionedMessage::V1(MessageV1 { + chain_id: CHAIN_ID, + command: Command::SendToken { + token: WETH.into(), + destination: Destination::ForeignAccountId32 { + para_id: PARA_ID_B, + id: PenpalBReceiver::get().into(), + fee: 100_000_000_000u128, + }, + amount: TOKEN_AMOUNT, + fee: XCM_FEE, + }, + }); + let (xcm, _) = EthereumInboundQueue::do_convert([0; 32].into(), message).unwrap(); + let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + // Check that the send token message was sent using xcm + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) =>{},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on AssetHub + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on PenPal + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + ] + ); + }); +} + #[test] fn transfer_relay_token() { let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of( diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs new file mode 100644 index 000000000000..6fb1f0354bce --- /dev/null +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-westend/src/tests/snowbridge_v2.rs @@ -0,0 +1,1018 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +use crate::imports::*; +use asset_hub_westend_runtime::ForeignAssets; +use bridge_hub_westend_runtime::{ + bridge_to_ethereum_config::{CreateAssetCall, CreateAssetDeposit, EthereumGatewayAddress}, + EthereumInboundQueueV2, +}; +use codec::Encode; +use emulated_integration_tests_common::RESERVABLE_ASSET_ID; +use hex_literal::hex; +use penpal_emulated_chain::PARA_ID_B; +use snowbridge_core::{AssetMetadata, TokenIdOf}; +use snowbridge_router_primitives::inbound::{ + v2::{ + Asset::{ForeignTokenERC20, NativeTokenERC20}, + Message, + }, + EthereumLocationsConverterFor, +}; +use sp_core::{H160, H256}; +use sp_runtime::MultiAddress; +use xcm_executor::traits::ConvertLocation; +const TOKEN_AMOUNT: u128 = 100_000_000_000; + +/// Calculates the XCM prologue fee for sending an XCM to AH. +const INITIAL_FUND: u128 = 5_000_000_000_000; +use testnet_parachains_constants::westend::snowbridge::EthereumNetwork; +const WETH: [u8; 20] = hex!("fff9976782d46cc05630d1f6ebab18b2324d6b14"); +/// An ERC-20 token to be registered and sent. +const TOKEN_ID: [u8; 20] = hex!("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"); +const CHAIN_ID: u64 = 11155111u64; + +pub fn eth_location() -> Location { + Location::new(2, [GlobalConsensus(EthereumNetwork::get().into())]) +} + +pub fn weth_location() -> Location { + erc20_token_location(WETH.into()) +} + +pub fn erc20_token_location(token_id: H160) -> Location { + Location::new( + 2, + [ + GlobalConsensus(EthereumNetwork::get().into()), + AccountKey20 { network: None, key: token_id.into() }, + ], + ) +} + +#[test] +fn register_token_v2() { + let relayer = BridgeHubWestendSender::get(); + let receiver = AssetHubWestendReceiver::get(); + BridgeHubWestend::fund_accounts(vec![(relayer.clone(), INITIAL_FUND)]); + + register_foreign_asset(eth_location()); + + set_up_eth_and_dot_pool(eth_location()); + + let claimer = AccountId32 { network: None, id: receiver.clone().into() }; + let claimer_bytes = claimer.encode(); + + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let bridge_owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&CHAIN_ID); + + let token: H160 = TOKEN_ID.into(); + let asset_id = erc20_token_location(token.into()); + + let dot_asset = Location::new(1, Here); + let dot_fee: xcm::prelude::Asset = (dot_asset, CreateAssetDeposit::get()).into(); + + let eth_asset_value = 9_000_000_000_000u128; + let asset_deposit: xcm::prelude::Asset = (eth_location(), eth_asset_value).into(); + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + // Exchange eth for dot to pay the asset creation deposit + ExchangeAsset { + give: asset_deposit.clone().into(), + want: dot_fee.clone().into(), + maximal: false, + }, + // Deposit the dot deposit into the bridge sovereign account (where the asset creation + // fee will be deducted from) + DepositAsset { assets: dot_fee.into(), beneficiary: bridge_owner.into() }, + // Call to create the asset. + Transact { + origin_kind: OriginKind::Xcm, + fallback_max_weight: None, + call: ( + CreateAssetCall::get(), + asset_id, + MultiAddress::<[u8; 32], ()>::Id(bridge_owner.into()), + 1u128, + ) + .encode() + .into(), + }, + ExpectTransactStatus(MaybeErrorCode::Success), + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets: vec![], + xcm: versioned_message_xcm.encode(), + claimer: Some(claimer_bytes), + // Used to pay the asset creation deposit. + value: 9_000_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { .. }) => {},] + ); + }); +} + +#[test] +fn send_token_v2() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer_acc_id_bytes: [u8; 32] = claimer_acc_id.into(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + register_foreign_asset(eth_location()); + register_foreign_asset(token_location.clone()); + + let token_transfer_value = 2_000_000_000_000u128; + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: token.into(), value: token_transfer_value }, + ]; + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![DepositAsset { + assets: Wild(AllOf { + id: AssetId(token_location.clone()), + fun: WildFungibility::Fungible, + }), + beneficiary, + }]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: versioned_message_xcm.encode(), + claimer: Some(claimer_bytes), + value: 1_500_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the token was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + + // Claimer received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), AccountId::from(claimer_acc_id_bytes)) > 0); + }); +} + +#[test] +fn send_weth_v2() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer_acc_id_bytes: [u8; 32] = claimer_acc_id.into(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + register_foreign_asset(eth_location()); + register_foreign_asset(weth_location()); + + let token_transfer_value = 2_000_000_000_000u128; + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: WETH.into(), value: token_transfer_value }, + ]; + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![DepositAsset { + assets: Wild(AllOf { + id: AssetId(weth_location().clone()), + fun: WildFungibility::Fungible, + }), + beneficiary, + }]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: versioned_message_xcm.encode(), + claimer: Some(claimer_bytes), + value: 3_500_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the weth was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(weth_location(), AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + + // Claimer received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), AccountId::from(claimer_acc_id_bytes)) > 0); + }); +} + +#[test] +fn register_and_send_multiple_tokens_v2() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let bridge_owner = EthereumLocationsConverterFor::<[u8; 32]>::from_chain_id(&CHAIN_ID); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.clone().into() }); + + // To satisfy ED + AssetHubWestend::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + 3_000_000_000_000, + )]); + + let claimer_acc_id = H256::random(); + let claimer_acc_id_bytes: [u8; 32] = claimer_acc_id.into(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + register_foreign_asset(eth_location()); + register_foreign_asset(weth_location()); + + set_up_eth_and_dot_pool(eth_location()); + + let token_transfer_value = 2_000_000_000_000u128; + let weth_transfer_value = 2_500_000_000_000u128; + + let dot_asset = Location::new(1, Here); + let dot_fee: xcm::prelude::Asset = (dot_asset, CreateAssetDeposit::get()).into(); + + // Used to pay the asset creation deposit. + let eth_asset_value = 9_000_000_000_000u128; + let asset_deposit: xcm::prelude::Asset = (eth_location(), eth_asset_value).into(); + + let assets = vec![ + NativeTokenERC20 { token_id: WETH.into(), value: 2_800_000_000_000u128 }, + NativeTokenERC20 { token_id: token.into(), value: token_transfer_value }, + ]; + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + ExchangeAsset { + give: asset_deposit.clone().into(), + want: dot_fee.clone().into(), + maximal: false, + }, + DepositAsset { assets: dot_fee.into(), beneficiary: bridge_owner.into() }, + // register new token + Transact { + origin_kind: OriginKind::Xcm, + fallback_max_weight: None, + call: ( + CreateAssetCall::get(), + token_location.clone(), + MultiAddress::<[u8; 32], ()>::Id(bridge_owner.into()), + 1u128, + ) + .encode() + .into(), + }, + ExpectTransactStatus(MaybeErrorCode::Success), + // deposit new token to beneficiary + DepositAsset { + assets: Wild(AllOf { + id: AssetId(token_location.clone()), + fun: WildFungibility::Fungible, + }), + beneficiary: beneficiary.clone(), + }, + // deposit weth to beneficiary + DepositAsset { + assets: Wild(AllOf { + id: AssetId(weth_location()), + fun: WildFungibility::Fungible, + }), + beneficiary: beneficiary.clone(), + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: versioned_message_xcm.encode(), + claimer: Some(claimer_bytes), + value: 3_500_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // The token was created + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { .. }) => {},] + ); + + // Check that the token was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + + // Beneficiary received the weth transfer value + assert!( + ForeignAssets::balance(weth_location(), AccountId::from(beneficiary_acc_bytes)) > + weth_transfer_value + ); + + // Claimer received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), AccountId::from(claimer_acc_id_bytes)) > 0); + }); +} + +#[test] +fn send_token_to_penpal_v2() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let token: H160 = TOKEN_ID.into(); + let token_location = erc20_token_location(token); + + let beneficiary_acc_id: H256 = H256::random(); + let beneficiary_acc_bytes: [u8; 32] = beneficiary_acc_id.into(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: beneficiary_acc_id.into() }); + + let claimer_acc_id = H256::random(); + let claimer = AccountId32 { network: None, id: claimer_acc_id.into() }; + let claimer_bytes = claimer.encode(); + + // To pay fees on Penpal. + let eth_fee_penpal: xcm::prelude::Asset = (eth_location(), 3_000_000_000_000u128).into(); + + register_foreign_asset(eth_location()); + register_foreign_asset(token_location.clone()); + + // To satisfy ED + PenpalB::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + 3_000_000_000_000, + )]); + + let penpal_location = BridgeHubWestend::sibling_location_of(PenpalB::para_id()); + let penpal_sovereign = BridgeHubWestend::sovereign_account_id_of(penpal_location); + PenpalB::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + // Register token on Penpal + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + token_location.clone().try_into().unwrap(), + penpal_sovereign.clone().into(), + true, + 1000, + )); + + assert!(::ForeignAssets::asset_exists( + token_location.clone().try_into().unwrap(), + )); + + // Register eth on Penpal + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + eth_location().try_into().unwrap(), + penpal_sovereign.clone().into(), + true, + 1000, + )); + + assert!(::ForeignAssets::asset_exists( + eth_location().try_into().unwrap(), + )); + + assert_ok!(::System::set_storage( + ::RuntimeOrigin::root(), + vec![( + PenpalCustomizableAssetFromSystemAssetHub::key().to_vec(), + Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]).encode(), + )], + )); + }); + + set_up_eth_and_dot_pool(eth_location()); + set_up_eth_and_dot_pool_on_penpal(eth_location()); + + let token_transfer_value = 2_000_000_000_000u128; + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: token.into(), value: token_transfer_value }, + ]; + + let token_asset: xcm::prelude::Asset = (token_location.clone(), token_transfer_value).into(); + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![ + // Send message to Penpal + DepositReserveAsset { + // Send the token plus some eth for execution fees + assets: Definite(vec![eth_fee_penpal.clone(), token_asset].into()), + // Penpal + dest: Location::new(1, [Parachain(PARA_ID_B)]), + xcm: vec![ + // Pay fees on Penpal. + PayFees { asset: eth_fee_penpal }, + // Deposit assets to beneficiary. + DepositAsset { + assets: Wild(AllOf { + id: AssetId(token_location.clone()), + fun: WildFungibility::Fungible, + }), + beneficiary: beneficiary.clone(), + }, + SetTopic(H256::random().into()), + ] + .into(), + }, + ]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: versioned_message_xcm.encode(), + claimer: Some(claimer_bytes), + value: 3_500_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the assets were issued on AssetHub + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {}, + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the token was received and issued as a foreign asset on PenpalB + assert_expected_events!( + PenpalB, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(token_location, AccountId::from(beneficiary_acc_bytes)), + token_transfer_value + ); + }); +} + +#[test] +fn send_foreign_erc20_token_back_to_polkadot() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let claimer = AccountId32 { network: None, id: H256::random().into() }; + let claimer_bytes = claimer.encode(); + let beneficiary = + Location::new(0, AccountId32 { network: None, id: AssetHubWestendReceiver::get().into() }); + + let asset_id: Location = + [PalletInstance(ASSETS_PALLET_ID), GeneralIndex(RESERVABLE_ASSET_ID.into())].into(); + + register_foreign_asset(eth_location()); + + let asset_id_in_bh: Location = Location::new( + 1, + [ + Parachain(AssetHubWestend::para_id().into()), + PalletInstance(ASSETS_PALLET_ID), + GeneralIndex(RESERVABLE_ASSET_ID.into()), + ], + ); + + let asset_id_after_reanchored = Location::new( + 1, + [ + GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)), + Parachain(AssetHubWestend::para_id().into()), + ], + ) + .appended_with(asset_id.clone().interior) + .unwrap(); + + let ethereum_destination = Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]); + + // Register token + BridgeHubWestend::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::EthereumSystem::register_token( + RuntimeOrigin::root(), + Box::new(VersionedLocation::from(asset_id_in_bh.clone())), + AssetMetadata { + name: "ah_asset".as_bytes().to_vec().try_into().unwrap(), + symbol: "ah_asset".as_bytes().to_vec().try_into().unwrap(), + decimals: 12, + }, + )); + }); + + let ethereum_sovereign: AccountId = + EthereumLocationsConverterFor::<[u8; 32]>::convert_location(ðereum_destination) + .unwrap() + .into(); + AssetHubWestend::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]); + + // Mint the asset into the bridge sovereign account, to mimic locked funds + AssetHubWestend::mint_asset( + ::RuntimeOrigin::signed(AssetHubWestendAssetOwner::get()), + RESERVABLE_ASSET_ID, + ethereum_sovereign.clone(), + TOKEN_AMOUNT, + ); + + let token_id = TokenIdOf::convert_location(&asset_id_after_reanchored).unwrap(); + + let assets = vec![ + // the token being transferred + ForeignTokenERC20 { token_id: token_id.into(), value: TOKEN_AMOUNT }, + ]; + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![DepositAsset { assets: Wild(AllCounted(2)), beneficiary }]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: versioned_message_xcm.encode(), + claimer: Some(claimer_bytes), + value: 1_500_000_000_000u128, + execution_fee: 3_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::Assets(pallet_assets::Event::Burned{..}) => {},] + ); + + let events = AssetHubWestend::events(); + + // Check that the native token burnt from some reserved account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Assets(pallet_assets::Event::Burned { owner, .. }) + if *owner == ethereum_sovereign.clone(), + )), + "token burnt from Ethereum sovereign account." + ); + + // Check that the token was minted to beneficiary + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Assets(pallet_assets::Event::Issued { owner, .. }) + if *owner == AssetHubWestendReceiver::get() + )), + "Token minted to beneficiary." + ); + }); +} + +#[test] +fn invalid_xcm_traps_funds_on_ah() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let token: H160 = TOKEN_ID.into(); + let claimer = AccountId32 { network: None, id: H256::random().into() }; + let claimer_bytes = claimer.encode(); + let beneficiary_acc_bytes: [u8; 32] = H256::random().into(); + + AssetHubWestend::fund_accounts(vec![( + sp_runtime::AccountId32::from(beneficiary_acc_bytes), + 3_000_000_000_000, + )]); + + register_foreign_asset(eth_location()); + + set_up_eth_and_dot_pool(eth_location()); + + let assets = vec![ + // to transfer assets + NativeTokenERC20 { token_id: WETH.into(), value: 2_800_000_000_000u128 }, + // the token being transferred + NativeTokenERC20 { token_id: token.into(), value: 2_000_000_000_000u128 }, + ]; + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // invalid xcm + let instructions = hex!("02806c072d50e2c7cd6821d1f084cbb4"); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: instructions.to_vec(), + claimer: Some(claimer_bytes), + value: 1_500_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Assets are trapped + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::PolkadotXcm(pallet_xcm::Event::AssetsTrapped { .. }) => {},] + ); + }); +} + +#[test] +fn invalid_claimer_does_not_fail_the_message() { + let relayer = BridgeHubWestendSender::get(); + let relayer_location = + Location::new(0, AccountId32 { network: None, id: relayer.clone().into() }); + + let beneficiary_acc: [u8; 32] = H256::random().into(); + let beneficiary = Location::new(0, AccountId32 { network: None, id: beneficiary_acc.into() }); + + register_foreign_asset(eth_location()); + register_foreign_asset(weth_location()); + + let token_transfer_value = 2_000_000_000_000u128; + + let assets = vec![ + // the token being transferred + NativeTokenERC20 { token_id: WETH.into(), value: token_transfer_value }, + ]; + + BridgeHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + let instructions = vec![DepositAsset { + assets: Wild(AllOf { + id: AssetId(weth_location().clone()), + fun: WildFungibility::Fungible, + }), + beneficiary, + }]; + let xcm: Xcm<()> = instructions.into(); + let versioned_message_xcm = VersionedXcm::V5(xcm); + let origin = EthereumGatewayAddress::get(); + + let message = Message { + origin, + assets, + xcm: versioned_message_xcm.encode(), + // Set an invalid claimer + claimer: Some(hex!("2b7ce7bc7e87e4d6619da21487c7a53f").to_vec()), + value: 1_500_000_000_000u128, + execution_fee: 1_500_000_000_000u128, + relayer_fee: 1_500_000_000_000u128, + }; + + let (xcm, _) = EthereumInboundQueueV2::do_convert(message, relayer_location).unwrap(); + let _ = EthereumInboundQueueV2::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap(); + + assert_expected_events!( + BridgeHubWestend, + vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},] + ); + }); + + // Message still processes successfully + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + // Check that the token was received and issued as a foreign asset on AssetHub + assert_expected_events!( + AssetHubWestend, + vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},] + ); + + // Beneficiary received the token transfer value + assert_eq!( + ForeignAssets::balance(weth_location(), AccountId::from(beneficiary_acc)), + token_transfer_value + ); + + // Relayer (instead of claimer) received eth refund for fees paid + assert!(ForeignAssets::balance(eth_location(), AccountId::from(relayer)) > 0); + }); +} + +pub fn register_foreign_asset(token_location: Location) { + let assethub_location = BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id()); + let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of(assethub_location); + AssetHubWestend::execute_with(|| { + type RuntimeOrigin = ::RuntimeOrigin; + + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + token_location.clone().try_into().unwrap(), + assethub_sovereign.clone().into(), + true, + 1000, + )); + + assert!(::ForeignAssets::asset_exists( + token_location.clone().try_into().unwrap(), + )); + }); +} + +pub(crate) fn set_up_eth_and_dot_pool(asset: v5::Location) { + let wnd: v5::Location = v5::Parent.into(); + let assethub_location = BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id()); + let owner = AssetHubWestendSender::get(); + let bh_sovereign = BridgeHubWestend::sovereign_account_id_of(assethub_location); + + AssetHubWestend::fund_accounts(vec![(owner.clone(), 3_000_000_000_000)]); + + AssetHubWestend::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let signed_owner = ::RuntimeOrigin::signed(owner.clone()); + let signed_bh_sovereign = + ::RuntimeOrigin::signed(bh_sovereign.clone()); + + assert_ok!(::ForeignAssets::mint( + signed_bh_sovereign.clone(), + asset.clone().into(), + bh_sovereign.clone().into(), + 3_500_000_000_000, + )); + + assert_ok!(::ForeignAssets::transfer( + signed_bh_sovereign.clone(), + asset.clone().into(), + owner.clone().into(), + 3_000_000_000_000, + )); + + assert_ok!(::AssetConversion::create_pool( + signed_owner.clone(), + Box::new(wnd.clone()), + Box::new(asset.clone()), + )); + + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + signed_owner.clone(), + Box::new(wnd), + Box::new(asset), + 1_000_000_000_000, + 2_000_000_000_000, + 1, + 1, + owner.into() + )); + + assert_expected_events!( + AssetHubWestend, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {..}) => {}, + ] + ); + }); +} + +pub(crate) fn set_up_eth_and_dot_pool_on_penpal(asset: v5::Location) { + let wnd: v5::Location = v5::Parent.into(); + let penpal_location = BridgeHubWestend::sibling_location_of(PenpalB::para_id()); + let owner = PenpalBSender::get(); + let bh_sovereign = BridgeHubWestend::sovereign_account_id_of(penpal_location); + + PenpalB::fund_accounts(vec![(owner.clone(), 3_000_000_000_000)]); + + PenpalB::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + + let signed_owner = ::RuntimeOrigin::signed(owner.clone()); + let signed_bh_sovereign = ::RuntimeOrigin::signed(bh_sovereign.clone()); + + assert_ok!(::ForeignAssets::mint( + signed_bh_sovereign.clone(), + asset.clone().into(), + bh_sovereign.clone().into(), + 3_500_000_000_000, + )); + + assert_ok!(::ForeignAssets::transfer( + signed_bh_sovereign.clone(), + asset.clone().into(), + owner.clone().into(), + 3_000_000_000_000, + )); + + assert_ok!(::AssetConversion::create_pool( + signed_owner.clone(), + Box::new(wnd.clone()), + Box::new(asset.clone()), + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::PoolCreated { .. }) => {}, + ] + ); + + assert_ok!(::AssetConversion::add_liquidity( + signed_owner.clone(), + Box::new(wnd), + Box::new(asset), + 1_000_000_000_000, + 2_000_000_000_000, + 1, + 1, + owner.into() + )); + + assert_expected_events!( + PenpalB, + vec![ + RuntimeEvent::AssetConversion(pallet_asset_conversion::Event::LiquidityAdded {..}) => {}, + ] + ); + }); +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs index ff99f1242b22..3782fececa1b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/mod.rs @@ -123,7 +123,7 @@ impl XcmWeightInfo for AssetHubWestendXcmWeight { assets.weigh_assets(XcmFungibleWeight::::deposit_reserve_asset()) } fn exchange_asset(_give: &AssetFilter, _receive: &Assets, _maximal: &bool) -> Weight { - Weight::MAX + XcmFungibleWeight::::exchange_asset() } fn initiate_reserve_withdraw( assets: &AssetFilter, diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs index 97e59c24dd89..52c941f69cd2 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/xcm/pallet_xcm_benchmarks_fungible.rs @@ -220,4 +220,14 @@ impl WeightInfo { .saturating_add(T::DbWeight::get().reads(9)) .saturating_add(T::DbWeight::get().writes(4)) } + + pub fn exchange_asset() -> Weight { + // Proof Size summary in bytes: + // Measured: `159` + // Estimated: `6196` + // Minimum execution time: 87_253_000 picoseconds. + Weight::from_parts(88_932_000, 6196) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(4)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs index b4e938f1f8b5..8045a4c9255f 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/xcm_config.rs @@ -649,7 +649,7 @@ pub mod bridging { use assets_common::matching::FromNetwork; use sp_std::collections::btree_set::BTreeSet; use testnet_parachains_constants::westend::snowbridge::{ - EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX, + EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX_V1, INBOUND_QUEUE_PALLET_INDEX_V2 }; parameter_types! { @@ -659,11 +659,18 @@ pub mod bridging { /// Polkadot uses 10 decimals, Kusama,Rococo,Westend 12 decimals. pub const DefaultBridgeHubEthereumBaseFee: Balance = 2_750_872_500_000; pub storage BridgeHubEthereumBaseFee: Balance = DefaultBridgeHubEthereumBaseFee::get(); - pub SiblingBridgeHubWithEthereumInboundQueueInstance: Location = Location::new( + pub SiblingBridgeHubWithEthereumInboundQueueV1Instance: Location = Location::new( 1, [ Parachain(SiblingBridgeHubParaId::get()), - PalletInstance(INBOUND_QUEUE_PALLET_INDEX) + PalletInstance(INBOUND_QUEUE_PALLET_INDEX_V1) + ] + ); + pub SiblingBridgeHubWithEthereumInboundQueueV2Instance: Location = Location::new( + 1, + [ + Parachain(SiblingBridgeHubParaId::get()), + PalletInstance(INBOUND_QUEUE_PALLET_INDEX_V2) ] ); @@ -684,7 +691,8 @@ pub mod bridging { /// Universal aliases pub UniversalAliases: BTreeSet<(Location, Junction)> = BTreeSet::from_iter( sp_std::vec![ - (SiblingBridgeHubWithEthereumInboundQueueInstance::get(), GlobalConsensus(EthereumNetwork::get().into())), + (SiblingBridgeHubWithEthereumInboundQueueV1Instance::get(), GlobalConsensus(EthereumNetwork::get().into())), + (SiblingBridgeHubWithEthereumInboundQueueV2Instance::get(), GlobalConsensus(EthereumNetwork::get().into())), ] ); diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs index be7005b5379a..16fcaeab5bad 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/bridge_to_ethereum_config.rs @@ -24,7 +24,7 @@ use crate::{ use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards}; -use snowbridge_router_primitives::{inbound::MessageToXcm, outbound::EthereumBlobExporter}; +use snowbridge_router_primitives::{inbound::v1::MessageToXcm, outbound::EthereumBlobExporter}; use sp_core::H160; use testnet_parachains_constants::rococo::{ currency::*, diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml index 91900c830ba6..d287b4de89d4 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/Cargo.toml @@ -112,12 +112,13 @@ snowbridge-system-runtime-api = { workspace = true } snowbridge-core = { workspace = true } snowbridge-pallet-ethereum-client = { workspace = true } snowbridge-pallet-inbound-queue = { workspace = true } +snowbridge-pallet-inbound-queue-v2 = { workspace = true } +snowbridge-inbound-queue-v2-runtime-api = { workspace = true } snowbridge-pallet-outbound-queue = { workspace = true } snowbridge-outbound-queue-runtime-api = { workspace = true } snowbridge-router-primitives = { workspace = true } snowbridge-runtime-common = { workspace = true } - [dev-dependencies] bridge-hub-test-utils = { workspace = true, default-features = true } bridge-runtime-common = { features = ["integrity-test"], workspace = true, default-features = true } @@ -189,6 +190,7 @@ std = [ "snowbridge-beacon-primitives/std", "snowbridge-core/std", "snowbridge-outbound-queue-runtime-api/std", + "snowbridge-inbound-queue-v2-runtime-api/std", "snowbridge-pallet-ethereum-client/std", "snowbridge-pallet-inbound-queue/std", "snowbridge-pallet-outbound-queue/std", diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs index 94921fd8af9a..2de53d503118 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/bridge_to_ethereum_config.rs @@ -25,12 +25,12 @@ use crate::{ use parachains_common::{AccountId, Balance}; use snowbridge_beacon_primitives::{Fork, ForkVersions}; use snowbridge_core::{gwei, meth, AllowSiblingsOnly, PricingParameters, Rewards}; -use snowbridge_router_primitives::{inbound::MessageToXcm, outbound::EthereumBlobExporter}; +use snowbridge_router_primitives::outbound::EthereumBlobExporter; use sp_core::H160; use testnet_parachains_constants::westend::{ currency::*, fee::WeightToFee, - snowbridge::{EthereumLocation, EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX}, + snowbridge::{EthereumLocation, EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX_V1, INBOUND_QUEUE_PALLET_INDEX_V2}, }; use crate::xcm_config::RelayNetwork; @@ -71,7 +71,9 @@ parameter_types! { }; pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(RelayNetwork::get()),Parachain(westend_runtime_constants::system_parachain::ASSET_HUB_ID)]); pub EthereumUniversalLocation: InteriorLocation = [GlobalConsensus(EthereumNetwork::get())].into(); + pub WethAddress: H160 = H160(hex_literal::hex!("fff9976782d46cc05630d1f6ebab18b2324d6b14")); } + impl snowbridge_pallet_inbound_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Verifier = snowbridge_pallet_ethereum_client::Pallet; @@ -84,10 +86,10 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type GatewayAddress = EthereumGatewayAddress; #[cfg(feature = "runtime-benchmarks")] type Helper = Runtime; - type MessageConverter = MessageToXcm< + type MessageConverter = snowbridge_router_primitives::inbound::v1::MessageToXcm< CreateAssetCall, CreateAssetDeposit, - ConstU8, + ConstU8, AccountId, Balance, EthereumSystem, @@ -102,6 +104,31 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime { type AssetTransactor = ::AssetTransactor; } +impl snowbridge_pallet_inbound_queue_v2::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Verifier = snowbridge_pallet_ethereum_client::Pallet; + #[cfg(not(feature = "runtime-benchmarks"))] + type XcmSender = XcmRouter; + #[cfg(feature = "runtime-benchmarks")] + type XcmSender = DoNothingRouter; + type GatewayAddress = EthereumGatewayAddress; + #[cfg(feature = "runtime-benchmarks")] + type Helper = Runtime; + type WeightInfo = crate::weights::snowbridge_pallet_inbound_queue_v2::WeightInfo; + type WeightToFee = WeightToFee; + type AssetHubParaId = ConstU32<1000>; + type Token = Balances; + type Balance = Balance; + type MessageConverter = snowbridge_router_primitives::inbound::v2::MessageToXcm< + EthereumNetwork, + ConstU8, + EthereumSystem, + EthereumGatewayAddress, + EthereumUniversalLocation, + AssetHubFromEthereum, + >; +} + impl snowbridge_pallet_outbound_queue::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Hashing = Keccak256; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs index ae3dbfa06cba..b653e3990137 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/lib.rs @@ -50,7 +50,9 @@ use sp_runtime::{ transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, }; - +use frame_support::traits::Contains; +use snowbridge_router_primitives::inbound::v2::Message; +use sp_runtime::DispatchError; #[cfg(feature = "std")] use sp_version::NativeVersion; use sp_version::RuntimeVersion; @@ -265,6 +267,22 @@ parameter_types! { } // Configure FRAME pallets to include in runtime. +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &RuntimeCall) -> bool { + // Disallow these Snowbridge system calls. + if matches!( + call, + RuntimeCall::EthereumSystem(snowbridge_pallet_system::Call::create_agent { .. }) + ) || matches!( + call, + RuntimeCall::EthereumSystem(snowbridge_pallet_system::Call::create_channel { .. }) + ) { + return false + } + return true + } +} #[derive_impl(frame_system::config_preludes::ParaChainDefaultConfig)] impl frame_system::Config for Runtime { @@ -297,6 +315,7 @@ impl frame_system::Config for Runtime { /// The action to take on a Runtime Upgrade type OnSetCode = cumulus_pallet_parachain_system::ParachainSetCode; type MaxConsumers = frame_support::traits::ConstU32<16>; + type BaseCallFilter = BaseFilter; } impl pallet_timestamp::Config for Runtime { @@ -564,6 +583,7 @@ construct_runtime!( EthereumOutboundQueue: snowbridge_pallet_outbound_queue = 81, EthereumBeaconClient: snowbridge_pallet_ethereum_client = 82, EthereumSystem: snowbridge_pallet_system = 83, + EthereumInboundQueueV2: snowbridge_pallet_inbound_queue_v2 = 84, // Message Queue. Importantly, is registered last so that messages are processed after // the `on_initialize` hooks of bridging pallets. @@ -622,6 +642,7 @@ mod benches { [snowbridge_pallet_outbound_queue, EthereumOutboundQueue] [snowbridge_pallet_system, EthereumSystem] [snowbridge_pallet_ethereum_client, EthereumBeaconClient] + [snowbridge_pallet_inbound_queue_v2, EthereumInboundQueueV2] ); } @@ -901,6 +922,12 @@ impl_runtime_apis! { } } + impl snowbridge_inbound_queue_v2_runtime_api::InboundQueueApiV2 for Runtime { + fn dry_run(message: Message) -> Result<(Xcm<()>, Balance), DispatchError> { + snowbridge_pallet_inbound_queue_v2::api::dry_run::(message) + } + } + impl snowbridge_system_runtime_api::ControlApi for Runtime { fn agent_id(location: VersionedLocation) -> Option { snowbridge_pallet_system::api::agent_id::(location) diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/mod.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/mod.rs index c1c5c337aca8..ee8ad5f31794 100644 --- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/mod.rs +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/mod.rs @@ -47,6 +47,7 @@ pub mod xcm; pub mod snowbridge_pallet_ethereum_client; pub mod snowbridge_pallet_inbound_queue; +pub mod snowbridge_pallet_inbound_queue_v2; pub mod snowbridge_pallet_outbound_queue; pub mod snowbridge_pallet_system; diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs new file mode 100644 index 000000000000..8cfa14981b3a --- /dev/null +++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_inbound_queue_v2.rs @@ -0,0 +1,69 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `snowbridge_pallet_inbound_queue` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev +//! DATE: 2023-09-06, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `macbook pro 14 m2`, CPU: `m2-arm64` +//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("bridge-hub-rococo-dev"), DB CACHE: 1024 + +// Executed Command: +// target/release/polkadot-parachain +// benchmark +// pallet +// --chain=bridge-hub-rococo-dev +// --pallet=snowbridge_inbound_queue +// --extrinsic=* +// --execution=wasm +// --wasm-execution=compiled +// --steps +// 50 +// --repeat +// 20 +// --output +// ./parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_inbound_queue.rs + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `snowbridge_pallet_inbound_queue_v2`. +pub struct WeightInfo(PhantomData); +impl snowbridge_pallet_inbound_queue_v2::WeightInfo for WeightInfo { + /// Storage: EthereumInboundQueue PalletOperatingMode (r:1 w:0) + /// Proof: EthereumInboundQueue PalletOperatingMode (max_values: Some(1), max_size: Some(1), added: 496, mode: MaxEncodedLen) + /// Storage: EthereumBeaconClient ExecutionHeaders (r:1 w:0) + /// Proof: EthereumBeaconClient ExecutionHeaders (max_values: None, max_size: Some(136), added: 2611, mode: MaxEncodedLen) + /// Storage: EthereumInboundQueue Nonce (r:1 w:1) + /// Proof: EthereumInboundQueue Nonce (max_values: None, max_size: Some(20), added: 2495, mode: MaxEncodedLen) + /// Storage: System Account (r:1 w:1) + /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + fn submit() -> Weight { + // Proof Size summary in bytes: + // Measured: `800` + // Estimated: `7200` + // Minimum execution time: 200_000_000 picoseconds. + Weight::from_parts(200_000_000, 0) + .saturating_add(Weight::from_parts(0, 7200)) + .saturating_add(T::DbWeight::get().reads(9)) + .saturating_add(T::DbWeight::get().writes(6)) + } +} diff --git a/cumulus/parachains/runtimes/constants/src/westend.rs b/cumulus/parachains/runtimes/constants/src/westend.rs index 8c4c0c594359..b880e229bc1f 100644 --- a/cumulus/parachains/runtimes/constants/src/westend.rs +++ b/cumulus/parachains/runtimes/constants/src/westend.rs @@ -174,7 +174,8 @@ pub mod snowbridge { use xcm::prelude::{Location, NetworkId}; /// The pallet index of the Ethereum inbound queue pallet in the bridge hub runtime. - pub const INBOUND_QUEUE_PALLET_INDEX: u8 = 80; + pub const INBOUND_QUEUE_PALLET_INDEX_V1: u8 = 80; + pub const INBOUND_QUEUE_PALLET_INDEX_V2: u8 = 84; parameter_types! { /// Network and location for the Ethereum chain. On Westend, the Ethereum chain bridged diff --git a/prdoc/pr_6697.prdoc b/prdoc/pr_6697.prdoc new file mode 100644 index 000000000000..cf2c5cbe845d --- /dev/null +++ b/prdoc/pr_6697.prdoc @@ -0,0 +1,22 @@ +title: 'Snowbridge Unordered Message Delivery - Inbound Queue' +doc: +- audience: Node Dev + description: |- + New pallets for unordered message delivery for Snowbridge, specifically the Inbound Queue part. No breaking changes + are made in this PR, only new functionality added. + +crates: +- name: snowbridge-pallet-inbound-queue-v2 + bump: minor +- name: snowbridge-inbound-queue-v2-runtime-api + bump: minor +- name: snowbridge-pallet-inbound-queue-fixtures-v2 + bump: minor +- name: snowbridge-core + bump: major +- name: snowbridge-router-primitives + bump: major +- name: bridge-hub-westend-integration-tests + bump: major +- name: bridge-hub-westend-runtime + bump: major