diff --git a/Cargo.lock b/Cargo.lock index 6151ed33c5b6..24d8ba60c752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1008,7 +1008,7 @@ dependencies = [ "rococo-runtime-constants 7.0.0", "scale-info", "serde_json", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "sp-api 26.0.0", "sp-block-builder 26.0.0", "sp-consensus-aura 0.32.0", @@ -1145,7 +1145,7 @@ dependencies = [ "primitive-types 0.13.1", "scale-info", "serde_json", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "sp-api 26.0.0", "sp-block-builder 26.0.0", "sp-consensus-aura 0.32.0", @@ -2629,10 +2629,10 @@ dependencies = [ "rococo-westend-system-emulated-network", "scale-info", "snowbridge-core 0.2.0", - "snowbridge-pallet-inbound-queue-fixtures 0.10.0", + "snowbridge-pallet-inbound-queue-fixtures 0.11.0", "snowbridge-pallet-outbound-queue 0.2.0", "snowbridge-pallet-system 0.2.0", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "sp-core 28.0.0", "sp-runtime 31.0.1", "staging-xcm 7.0.0", @@ -2716,7 +2716,7 @@ dependencies = [ "snowbridge-pallet-inbound-queue 0.2.0", "snowbridge-pallet-outbound-queue 0.2.0", "snowbridge-pallet-system 0.2.0", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "snowbridge-runtime-common 0.2.0", "snowbridge-runtime-test-common 0.2.0", "snowbridge-system-runtime-api 0.2.0", @@ -2870,10 +2870,10 @@ dependencies = [ "scale-info", "snowbridge-core 0.2.0", "snowbridge-pallet-inbound-queue 0.2.0", - "snowbridge-pallet-inbound-queue-fixtures 0.10.0", + "snowbridge-pallet-inbound-queue-fixtures 0.11.0", "snowbridge-pallet-outbound-queue 0.2.0", "snowbridge-pallet-system 0.2.0", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "sp-core 28.0.0", "sp-runtime 31.0.1", "staging-xcm 7.0.0", @@ -2954,7 +2954,7 @@ dependencies = [ "snowbridge-pallet-inbound-queue 0.2.0", "snowbridge-pallet-outbound-queue 0.2.0", "snowbridge-pallet-system 0.2.0", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "snowbridge-runtime-common 0.2.0", "snowbridge-runtime-test-common 0.2.0", "snowbridge-system-runtime-api 0.2.0", @@ -16686,7 +16686,7 @@ dependencies = [ "primitive-types 0.12.2", "scale-info", "smallvec", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "sp-api 26.0.0", "sp-block-builder 26.0.0", "sp-consensus-aura 0.32.0", @@ -18910,10 +18910,10 @@ dependencies = [ "snowbridge-pallet-ethereum-client 0.2.0", "snowbridge-pallet-ethereum-client-fixtures 0.9.0", "snowbridge-pallet-inbound-queue 0.2.0", - "snowbridge-pallet-inbound-queue-fixtures 0.10.0", + "snowbridge-pallet-inbound-queue-fixtures 0.11.0", "snowbridge-pallet-outbound-queue 0.2.0", "snowbridge-pallet-system 0.2.0", - "snowbridge-router-primitives 0.9.0", + "snowbridge-router-primitives 0.10.0", "snowbridge-runtime-common 0.2.0", "snowbridge-runtime-test-common 0.2.0", "snowbridge-system-runtime-api 0.2.0", @@ -25141,8 +25141,8 @@ dependencies = [ "snowbridge-beacon-primitives 0.2.0", "snowbridge-core 0.2.0", "snowbridge-pallet-ethereum-client 0.2.0", - "snowbridge-pallet-inbound-queue-fixtures 0.10.0", - "snowbridge-router-primitives 0.9.0", + "snowbridge-pallet-inbound-queue-fixtures 0.11.0", + "snowbridge-router-primitives 0.10.0", "sp-core 28.0.0", "sp-io 30.0.0", "sp-keyring 31.0.0", @@ -25182,7 +25182,7 @@ dependencies = [ [[package]] name = "snowbridge-pallet-inbound-queue-fixtures" -version = "0.10.0" +version = "0.11.0" dependencies = [ "hex-literal", "snowbridge-beacon-primitives 0.2.0", @@ -25299,7 +25299,7 @@ dependencies = [ [[package]] name = "snowbridge-router-primitives" -version = "0.9.0" +version = "0.10.0" dependencies = [ "frame-support 28.0.0", "hex-literal", diff --git a/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml b/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml index c698dbbf1003..b3954bfcdde1 100644 --- a/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml +++ b/bridges/snowbridge/pallets/inbound-queue/fixtures/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "snowbridge-pallet-inbound-queue-fixtures" description = "Snowbridge Inbound Queue Test Fixtures" -version = "0.10.0" +version = "0.11.0" authors = ["Snowfork "] edition.workspace = true repository.workspace = true diff --git a/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs index 00adcdfa186a..cb4232376c6f 100644 --- a/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs +++ b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/lib.rs @@ -3,5 +3,6 @@ #![cfg_attr(not(feature = "std"), no_std)] pub mod register_token; +pub mod send_native_eth; pub mod send_token; pub mod send_token_to_penpal; diff --git a/bridges/snowbridge/pallets/inbound-queue/fixtures/src/send_native_eth.rs b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/send_native_eth.rs new file mode 100755 index 000000000000..d3e8d76e6b39 --- /dev/null +++ b/bridges/snowbridge/pallets/inbound-queue/fixtures/src/send_native_eth.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_native_eth_message() -> InboundQueueFixture { + InboundQueueFixture { + message: Message { + event_log: Log { + address: hex!("87d1f7fdfee7f651fabc8bfcb6e086c278b77a7d").into(), + topics: vec![ + hex!("7153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84f").into(), + hex!("c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539").into(), + hex!("5f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0").into(), + ], + data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005f00a736aa0000000000010000000000000000000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000064a7b3b6e00d000000000000000000e8764817000000000000000000000000").into(), + }, + proof: Proof { + receipt_proof: (vec![ + hex!("17cd4d05dde30703008a4f213205923630cff8e6bc9d5d95a52716bfb5551fd7").to_vec(), + ], vec![ + hex!("f903b4822080b903ae02f903aa018301a7fcb9010000000000000000000000000020000000000000000000004000000000000000000400000000000000000000001000000000000000000000000000000000000000000000000000000001080000000000000000000000000000000000000000080000000000020000000000000000000800010100000000000000000000000000000000000200000000000000000000000000001000000040080008000000000000000000040000000021000000002000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000200000000000000f9029ff9015d9487d1f7fdfee7f651fabc8bfcb6e086c278b77a7df884a024c5d2de620c6e25186ae16f6919eba93b6e2c1a33857cc419d9f3a00d6967e9a00000000000000000000000000000000000000000000000000000000000000000a000000000000000000000000090a987b944cb1dcce5564e5fdecd7a54d3de27fea000000000000000000000000000000000000000000000000000000000000003e8b8c000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000208eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48f9013c9487d1f7fdfee7f651fabc8bfcb6e086c278b77a7df863a07153f9357c8ea496bba60bf82e67143e27b64462b49041f8e689e1b05728f84fa0c173fac324158e77fb5840738a1a541f633cbec8884c6a601c567d2b376a0539a05f7060e971b0dc81e63f0aa41831091847d97c1a4693ac450cc128c7214e65e0b8c000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000005f00a736aa0000000000010000000000000000000000000000000000000000008eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48000064a7b3b6e00d000000000000000000e8764817000000000000000000000000").to_vec(), + ]), + execution_proof: ExecutionProof { + header: BeaconHeader { + slot: 246, + proposer_index: 7, + parent_root: hex!("4faaac5d2fa0b8884fe1175c7cac1c92aac9eba5a20b4302edb98a56428c5974").into(), + state_root: hex!("882c13f1d56df781e3444a78cae565bfa1c89822c86cdb0daea71f5351231580").into(), + body_root: hex!("c47eb72204b1ca567396dacef8b0214027eb7f0789330b55166085d1f9cb4c65").into(), + }, + ancestry_proof: Some(AncestryProof { + header_branch: vec![ + hex!("38e2454bc93c4cfafcea772b8531e4802bbd2561366620699096dd4e591bc488").into(), + hex!("3d7389fb144ccaeca8b8e1667ce1d1538dfceb50bf1e49c4b368a223f051fda3").into(), + hex!("0d49c9c24137ad4d86ebca2f36a159573a68b5d5d60e317776c77cc8b6093034").into(), + hex!("0fadc6735bcdc2793a5039a806fbf39984c39374ed4d272c1147e1c23df88983").into(), + hex!("3a058ad4b169eebb4c754c8488d41e56a7a0e5f8b55b5ec67452a8d326585c69").into(), + hex!("de200426caa9bc03f8e0033b4ef4df1db6501924b5c10fb7867e76db942b903c").into(), + hex!("48b578632bc40eebb517501f179ffdd06d762c03e9383df16fc651eeddd18806").into(), + hex!("98d9d6904b2a6a285db4c4ae59a07100cd38ec4d9fb7a16a10fe83ec99e6ba1d").into(), + hex!("1b2bbae6e684864b714654a60778664e63ba6c3c9bed8074ec1a0380fe5042e6").into(), + hex!("eb907a888eadf5a7e2bd0a3a5a9369e409c7aa688bd4cde758d5b608c6c82785").into(), + hex!("ffff0ad7e659772f9534c195c815efc4014ef1e1daed4404c06385d11192e92b").into(), + hex!("6cf04127db05441cd833107a52be852868890e4317e6a02ab47683aa75964220").into(), + hex!("b7d05f875f140027ef5118a2247bbb84ce8f2f0f1123623085daf7960c329f5f").into(), + ], + finalized_block_root: hex!("440615588532ce496a93d189cb0ef1df7cf67d529faee0fd03213ce26ea115e5").into(), + }), + execution_header: VersionedExecutionPayloadHeader::Deneb(deneb::ExecutionPayloadHeader { + parent_hash: hex!("a8c89213b7d7d2ac76462d89e6a7384374db905b657ad803d3c86f88f86c39df").into(), + fee_recipient: hex!("0000000000000000000000000000000000000000").into(), + state_root: hex!("a1e8175213a6a43da17fae65109245867cbc60e3ada16b8ac28c6b208761c772").into(), + receipts_root: hex!("17cd4d05dde30703008a4f213205923630cff8e6bc9d5d95a52716bfb5551fd7").into(), + logs_bloom: hex!("00000000000000000000000020000000000000000000004000000000000000000400000000000000000000001000000000000000000000000000000000000000000000000000000001080000000000000000000000000000000000000000080000000000020000000000000000000800010100000000000000000000000000000000000200000000000000000000000000001000000040080008000000000000000000040000000021000000002000002000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000220000000000000000000000000000000000000000000000000000200000000000000").into(), + prev_randao: hex!("b9b26dc14ea8c57d069fde0c94ad31c2558365c3986a0c06558470f8c02e62ce").into(), + block_number: 246, + gas_limit: 62908420, + gas_used: 108540, + timestamp: 1734718384, + extra_data: hex!("d983010e08846765746888676f312e32322e358664617277696e").into(), + base_fee_per_gas: U256::from(7u64), + block_hash: hex!("878195e2ea83c74d475363d03d41a7fbfc4026d6e5bcffb713928253984a64a7").into(), + transactions_root: hex!("909139b3137666b4551b629ce6d9fb7e5e6f6def8a48d078448ec6600fe63c7f").into(), + withdrawals_root: hex!("792930bbd5baac43bcc798ee49aa8185ef76bb3b44ba62b91d86ae569e4bb535").into(), + blob_gas_used: 0, + excess_blob_gas: 0, + }), + execution_branch: vec![ + hex!("5d78e26ea639df17c2194ff925f782b9522009d58cfc60e3d34ba79a19f8faf1").into(), + hex!("b46f0c01805fe212e15907981b757e6c496b0cb06664224655613dcec82505bb").into(), + hex!("db56114e00fdd4c1f85c892bf35ac9a89289aaecb1ebd0a96cde606a748b5d71").into(), + hex!("3d84b2809a36450186e5169995a5e3cab55d751aee90fd8456b33d871ccaa463").into(), + ], + } + }, + }, + finalized_header: BeaconHeader { + slot: 608, + proposer_index: 3, + parent_root: hex!("f10c2349530dbd339a72886270e2e304bb68155af68c918c850acd9ab341350f").into(), + state_root: hex!("6df0ef4cbb4986a84ff0763727402b88636e6b5535022cd3ad6967b8dd799402").into(), + body_root: hex!("f66fc1c022f07f91c777ad5c464625fc0b43d3e7a45650567dce60011210f574").into(), + }, + block_roots_root: hex!("1c0dbf54db070770f5e573b72afe0aac2b0e3cf312107d1cd73bf64d7a2ed90c").into(), + } +} diff --git a/bridges/snowbridge/primitives/router/Cargo.toml b/bridges/snowbridge/primitives/router/Cargo.toml index e44cca077ef3..c26e5541d1cf 100644 --- a/bridges/snowbridge/primitives/router/Cargo.toml +++ b/bridges/snowbridge/primitives/router/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "snowbridge-router-primitives" description = "Snowbridge Router Primitives" -version = "0.9.0" +version = "0.10.0" authors = ["Snowfork "] edition.workspace = true repository.workspace = true diff --git a/bridges/snowbridge/primitives/router/src/inbound/mock.rs b/bridges/snowbridge/primitives/router/src/inbound/mock.rs new file mode 100644 index 000000000000..537853b324f6 --- /dev/null +++ b/bridges/snowbridge/primitives/router/src/inbound/mock.rs @@ -0,0 +1,48 @@ +use crate::inbound::{MessageToXcm, TokenId}; +use frame_support::parameter_types; +use sp_runtime::{ + traits::{IdentifyAccount, MaybeEquivalence, Verify}, + MultiSignature, +}; +use xcm::{latest::WESTEND_GENESIS_HASH, prelude::*}; + +pub const CHAIN_ID: u64 = 11155111; +pub const NETWORK: NetworkId = Ethereum { chain_id: CHAIN_ID }; + +parameter_types! { + pub EthereumNetwork: NetworkId = NETWORK; + + pub const CreateAssetCall: [u8;2] = [53, 0]; + pub const CreateAssetExecutionFee: u128 = 2_000_000_000; + pub const CreateAssetDeposit: u128 = 100_000_000_000; + pub const SendTokenExecutionFee: u128 = 1_000_000_000; + pub const InboundQueuePalletInstance: u8 = 80; + pub UniversalLocation: InteriorLocation = + [GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)), Parachain(1002)].into(); + pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(ByGenesis(WESTEND_GENESIS_HASH)),Parachain(1000)]); +} + +type Signature = MultiSignature; +type AccountId = <::Signer as IdentifyAccount>::AccountId; +type Balance = u128; + +pub(crate) struct MockTokenIdConvert; +impl MaybeEquivalence for MockTokenIdConvert { + fn convert(_id: &TokenId) -> Option { + Some(Location::parent()) + } + fn convert_back(_loc: &Location) -> Option { + None + } +} + +pub(crate) type MessageConverter = MessageToXcm< + CreateAssetCall, + CreateAssetDeposit, + InboundQueuePalletInstance, + AccountId, + Balance, + MockTokenIdConvert, + UniversalLocation, + AssetHubFromEthereum, +>; diff --git a/bridges/snowbridge/primitives/router/src/inbound/mod.rs b/bridges/snowbridge/primitives/router/src/inbound/mod.rs index bc5d401cd4f7..bd0fbfa2178c 100644 --- a/bridges/snowbridge/primitives/router/src/inbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/inbound/mod.rs @@ -2,6 +2,8 @@ // SPDX-FileCopyrightText: 2023 Snowfork //! Converts messages from Ethereum to XCM messages +#[cfg(test)] +mod mock; #[cfg(test)] mod tests; @@ -394,10 +396,14 @@ where // 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() }], - ) + if token == H160([0; 20]) { + Location::new(2, [GlobalConsensus(network)]) + } else { + Location::new( + 2, + [GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }], + ) + } } /// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign diff --git a/bridges/snowbridge/primitives/router/src/inbound/tests.rs b/bridges/snowbridge/primitives/router/src/inbound/tests.rs index 786aa594f653..11d7928602c6 100644 --- a/bridges/snowbridge/primitives/router/src/inbound/tests.rs +++ b/bridges/snowbridge/primitives/router/src/inbound/tests.rs @@ -1,21 +1,12 @@ use super::EthereumLocationsConverterFor; -use crate::inbound::CallIndex; -use frame_support::{assert_ok, parameter_types}; +use crate::inbound::{ + mock::*, Command, ConvertMessage, Destination, MessageV1, VersionedMessage, H160, +}; +use frame_support::assert_ok; 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_ethereum_network_converts_successfully() { let expected_account: [u8; 32] = @@ -81,3 +72,74 @@ fn test_reanchor_all_assets() { assert_eq!(reanchored_asset_with_ethereum_context, asset.clone()); } } + +#[test] +fn test_convert_send_token_with_weth() { + const WETH: H160 = H160([0xff; 20]); + const AMOUNT: u128 = 1_000_000; + const FEE: u128 = 1_000; + const ACCOUNT_ID: [u8; 32] = [0xBA; 32]; + const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 { + chain_id: CHAIN_ID, + command: Command::SendToken { + token: WETH, + destination: Destination::AccountId32 { id: ACCOUNT_ID }, + amount: AMOUNT, + fee: FEE, + }, + }); + let result = MessageConverter::convert([1; 32].into(), MESSAGE); + assert_ok!(&result); + let (xcm, fee) = result.unwrap(); + assert_eq!(FEE, fee); + + let expected_assets = ReserveAssetDeposited( + vec![Asset { + id: AssetId(Location { + parents: 2, + interior: Junctions::X2( + [GlobalConsensus(NETWORK), AccountKey20 { network: None, key: WETH.into() }] + .into(), + ), + }), + fun: Fungible(AMOUNT), + }] + .into(), + ); + let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..))); + assert_eq!(actual_assets, Some(expected_assets)) +} + +#[test] +fn test_convert_send_token_with_eth() { + const ETH: H160 = H160([0x00; 20]); + const AMOUNT: u128 = 1_000_000; + const FEE: u128 = 1_000; + const ACCOUNT_ID: [u8; 32] = [0xBA; 32]; + const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 { + chain_id: CHAIN_ID, + command: Command::SendToken { + token: ETH, + destination: Destination::AccountId32 { id: ACCOUNT_ID }, + amount: AMOUNT, + fee: FEE, + }, + }); + let result = MessageConverter::convert([1; 32].into(), MESSAGE); + assert_ok!(&result); + let (xcm, fee) = result.unwrap(); + assert_eq!(FEE, fee); + + let expected_assets = ReserveAssetDeposited( + vec![Asset { + id: AssetId(Location { + parents: 2, + interior: Junctions::X1([GlobalConsensus(NETWORK)].into()), + }), + fun: Fungible(AMOUNT), + }] + .into(), + ); + let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..))); + assert_eq!(actual_assets, Some(expected_assets)) +} diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs index 3b5dbdb77c89..03a16eb35e79 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/mod.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs @@ -291,6 +291,7 @@ where match inner_location.unpack() { (0, [AccountKey20 { network, key }]) if self.network_matches(network) => Some((H160(*key), *amount)), + (0, []) => Some((H160([0; 20]), *amount)), _ => None, }, _ => None, diff --git a/bridges/snowbridge/primitives/router/src/outbound/tests.rs b/bridges/snowbridge/primitives/router/src/outbound/tests.rs index 44f81ce31b3a..2a60f9f3e0ea 100644 --- a/bridges/snowbridge/primitives/router/src/outbound/tests.rs +++ b/bridges/snowbridge/primitives/router/src/outbound/tests.rs @@ -515,6 +515,46 @@ fn xcm_converter_convert_with_wildcard_all_asset_filter_succeeds() { assert_eq!(result, Ok((expected_payload, [0; 32]))); } +#[test] +fn xcm_converter_convert_with_native_eth_succeeds() { + let network = BridgedNetwork::get(); + + let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000"); + + // The asset is `{ parents: 0, interior: X1(Here) }` relative to ethereum. + let assets: Assets = vec![Asset { id: AssetId([].into()), fun: Fungible(1000) }].into(); + let filter: AssetFilter = Wild(All); + + let message: Xcm<()> = vec![ + WithdrawAsset(assets.clone()), + ClearOrigin, + BuyExecution { fees: assets.get(0).unwrap().clone(), weight_limit: Unlimited }, + DepositAsset { + assets: filter, + beneficiary: AccountKey20 { network: None, key: beneficiary_address }.into(), + }, + SetTopic([0; 32]), + ] + .into(); + + let mut converter = + XcmConverter::::new(&message, network, Default::default()); + + // The token address that is expected to be sent should be + // `0x0000000000000000000000000000000000000000`. The solidity will + // interpret this as a transfer of ETH. + let expected_payload = Command::AgentExecute { + agent_id: Default::default(), + command: AgentExecuteCommand::TransferToken { + token: H160([0; 20]), + recipient: beneficiary_address.into(), + amount: 1000, + }, + }; + let result = converter.convert(); + assert_eq!(result, Ok((expected_payload, [0; 32]))); +} + #[test] fn xcm_converter_convert_with_fees_less_than_reserve_yields_success() { let network = BridgedNetwork::get(); diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-rococo/src/lib.rs index 5ef0993f70a1..43398eb8bd48 100644 --- a/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-rococo/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-rococo/src/lib.rs @@ -16,7 +16,8 @@ pub mod genesis; pub use bridge_hub_rococo_runtime::{ - xcm_config::XcmConfig as BridgeHubRococoXcmConfig, EthereumBeaconClient, EthereumInboundQueue, + self as bridge_hub_rococo_runtime, xcm_config::XcmConfig as BridgeHubRococoXcmConfig, + EthereumBeaconClient, EthereumInboundQueue, ExistentialDeposit as BridgeHubRococoExistentialDeposit, RuntimeOrigin as BridgeHubRococoRuntimeOrigin, }; diff --git a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs index 54bc395c86f0..f84d42cb29f8 100644 --- a/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs +++ b/cumulus/parachains/integration-tests/emulated/tests/bridges/bridge-hub-rococo/src/lib.rs @@ -50,6 +50,7 @@ mod imports { AssetHubWestendParaPallet as AssetHubWestendPallet, }, bridge_hub_rococo_emulated_chain::{ + bridge_hub_rococo_runtime::bridge_to_ethereum_config::EthereumGatewayAddress, genesis::ED as BRIDGE_HUB_ROCOCO_ED, BridgeHubRococoExistentialDeposit, BridgeHubRococoParaPallet as BridgeHubRococoPallet, BridgeHubRococoRuntimeOrigin, BridgeHubRococoXcmConfig, EthereumBeaconClient, EthereumInboundQueue, 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..0fe549f46b6d 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 @@ -20,8 +20,8 @@ use hex_literal::hex; use rococo_westend_system_emulated_network::BridgeHubRococoParaSender as BridgeHubRococoSender; use snowbridge_core::{inbound::InboundQueueFixture, outbound::OperatingMode}; use snowbridge_pallet_inbound_queue_fixtures::{ - register_token::make_register_token_message, send_token::make_send_token_message, - send_token_to_penpal::make_send_token_to_penpal_message, + register_token::make_register_token_message, send_native_eth::make_send_native_eth_message, + send_token::make_send_token_message, send_token_to_penpal::make_send_token_to_penpal_message, }; use snowbridge_pallet_system; use snowbridge_router_primitives::inbound::{ @@ -238,7 +238,7 @@ fn register_weth_token_from_ethereum_to_asset_hub() { /// Tests the registering of a token as an asset on AssetHub, and then subsequently sending /// a token from Ethereum to AssetHub. #[test] -fn send_token_from_ethereum_to_asset_hub() { +fn send_weth_from_ethereum_to_asset_hub() { BridgeHubRococo::fund_para_sovereign(AssetHubRococo::para_id().into(), INITIAL_FUND); // Fund ethereum sovereign on AssetHub @@ -278,7 +278,7 @@ fn send_token_from_ethereum_to_asset_hub() { /// 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() { +fn send_weth_from_ethereum_to_penpal() { let asset_hub_sovereign = BridgeHubRococo::sovereign_account_id_of(Location::new( 1, [Parachain(AssetHubRococo::para_id().into())], @@ -515,6 +515,176 @@ fn send_weth_asset_from_asset_hub_to_ethereum() { }); } +/// Tests the full cycle of eth transfers: +/// - sending a token to AssetHub +/// - returning the token to Ethereum +#[test] +fn send_eth_asset_from_asset_hub_to_ethereum_and_back() { + let ethereum_network: NetworkId = EthereumNetwork::get().into(); + let origin_location = (Parent, Parent, ethereum_network).into(); + + use ahr_xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee; + let assethub_location = BridgeHubRococo::sibling_location_of(AssetHubRococo::para_id()); + let assethub_sovereign = BridgeHubRococo::sovereign_account_id_of(assethub_location); + let ethereum_sovereign: AccountId = + EthereumLocationsConverterFor::::convert_location(&origin_location).unwrap(); + + AssetHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + BridgeHubRococo::force_default_xcm_version(Some(XCM_VERSION)); + AssetHubRococo::force_xcm_version(origin_location.clone(), XCM_VERSION); + + BridgeHubRococo::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]); + AssetHubRococo::fund_accounts(vec![ + (AssetHubRococoReceiver::get(), INITIAL_FUND), + (ethereum_sovereign.clone(), INITIAL_FUND), + ]); + + // Register ETH + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + assert_ok!(::ForeignAssets::force_create( + RuntimeOrigin::root(), + origin_location.clone(), + ethereum_sovereign.into(), + true, + 1000, + )); + + assert_expected_events!( + AssetHubRococo, + vec![ + RuntimeEvent::ForeignAssets(pallet_assets::Event::ForceCreated { .. }) => {}, + ] + ); + }); + const ETH_AMOUNT: u128 = 1_000_000_000_000_000_000; + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + // Set the gateway. This is needed because new fixtures use a different gateway address. + assert_ok!(::System::set_storage( + RuntimeOrigin::root(), + vec![( + EthereumGatewayAddress::key().to_vec(), + sp_core::H160(hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d")).encode(), + )], + )); + + // Construct SendToken message and sent to inbound queue + assert_ok!(send_inbound_message(make_send_native_eth_message())); + + // Check that the send token message was sent using xcm + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {}, + ] + ); + }); + + AssetHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + type RuntimeOrigin = ::RuntimeOrigin; + + let _issued_event = RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { + asset_id: origin_location.clone(), + owner: AssetHubRococoReceiver::get().into(), + amount: ETH_AMOUNT, + }); + // Check that AssetHub has issued the foreign asset + assert_expected_events!( + AssetHubRococo, + vec![ + _issued_event => {}, + ] + ); + let assets = + vec![Asset { id: AssetId(origin_location.clone()), fun: Fungible(ETH_AMOUNT) }]; + let multi_assets = VersionedAssets::from(Assets::from(assets)); + + let destination = origin_location.clone().into(); + + let beneficiary = VersionedLocation::from(Location::new( + 0, + [AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }], + )); + + let free_balance_before = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Send the Weth back to Ethereum + ::PolkadotXcm::limited_reserve_transfer_assets( + RuntimeOrigin::signed(AssetHubRococoReceiver::get()), + Box::new(destination), + Box::new(beneficiary), + Box::new(multi_assets), + 0, + Unlimited, + ) + .unwrap(); + + let _burned_event = RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { + asset_id: origin_location.clone(), + owner: AssetHubRococoReceiver::get().into(), + balance: ETH_AMOUNT, + }); + // Check that AssetHub has issued the foreign asset + let _destination = origin_location.clone(); + assert_expected_events!( + AssetHubRococo, + vec![ + _burned_event => {}, + RuntimeEvent::PolkadotXcm(pallet_xcm::Event::Sent { + destination: _destination, .. + }) => {}, + ] + ); + + let free_balance_after = ::Balances::free_balance( + AssetHubRococoReceiver::get(), + ); + // Assert at least DefaultBridgeHubEthereumBaseFee charged from the sender + let free_balance_diff = free_balance_before - free_balance_after; + assert!(free_balance_diff > DefaultBridgeHubEthereumBaseFee::get()); + }); + + BridgeHubRococo::execute_with(|| { + type RuntimeEvent = ::RuntimeEvent; + // Check that the transfer token back to Ethereum message was queue in the Ethereum + // Outbound Queue + assert_expected_events!( + BridgeHubRococo, + vec![ + RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageAccepted {..}) => {}, + RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued {..}) => {}, + ] + ); + + let events = BridgeHubRococo::events(); + // Check that the local fee was credited to the Snowbridge sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) + if *who == TREASURY_ACCOUNT.into() && *amount == 16903333 + )), + "Snowbridge sovereign takes local fee." + ); + // Check that the remote fee was credited to the AssetHub sovereign account + assert!( + events.iter().any(|event| matches!( + event, + RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount }) + if *who == assethub_sovereign && *amount == 2680000000000, + )), + "AssetHub sovereign takes remote fee." + ); + }); +} + #[test] fn send_token_from_ethereum_to_asset_hub_fail_for_insufficient_fund() { // Insufficient fund @@ -565,7 +735,7 @@ fn register_weth_token_in_asset_hub_fail_for_insufficient_fee() { }); } -fn send_token_from_ethereum_to_asset_hub_with_fee(account_id: [u8; 32], fee: u128) { +fn send_weth_from_ethereum_to_asset_hub_with_fee(account_id: [u8; 32], fee: u128) { let ethereum_network_v5: NetworkId = EthereumNetwork::get().into(); let weth_asset_location: Location = Location::new(2, [ethereum_network_v5.into(), AccountKey20 { network: None, key: WETH }]); @@ -623,8 +793,8 @@ fn send_token_from_ethereum_to_asset_hub_with_fee(account_id: [u8; 32], fee: u12 } #[test] -fn send_token_from_ethereum_to_existent_account_on_asset_hub() { - send_token_from_ethereum_to_asset_hub_with_fee(AssetHubRococoSender::get().into(), XCM_FEE); +fn send_weth_from_ethereum_to_existent_account_on_asset_hub() { + send_weth_from_ethereum_to_asset_hub_with_fee(AssetHubRococoSender::get().into(), XCM_FEE); AssetHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; @@ -640,8 +810,8 @@ fn send_token_from_ethereum_to_existent_account_on_asset_hub() { } #[test] -fn send_token_from_ethereum_to_non_existent_account_on_asset_hub() { - send_token_from_ethereum_to_asset_hub_with_fee([1; 32], XCM_FEE); +fn send_weth_from_ethereum_to_non_existent_account_on_asset_hub() { + send_weth_from_ethereum_to_asset_hub_with_fee([1; 32], XCM_FEE); AssetHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; @@ -657,8 +827,8 @@ fn send_token_from_ethereum_to_non_existent_account_on_asset_hub() { } #[test] -fn send_token_from_ethereum_to_non_existent_account_on_asset_hub_with_insufficient_fee() { - send_token_from_ethereum_to_asset_hub_with_fee([1; 32], INSUFFICIENT_XCM_FEE); +fn send_weth_from_ethereum_to_non_existent_account_on_asset_hub_with_insufficient_fee() { + send_weth_from_ethereum_to_asset_hub_with_fee([1; 32], INSUFFICIENT_XCM_FEE); AssetHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; @@ -675,10 +845,10 @@ fn send_token_from_ethereum_to_non_existent_account_on_asset_hub_with_insufficie } #[test] -fn send_token_from_ethereum_to_non_existent_account_on_asset_hub_with_sufficient_fee_but_do_not_satisfy_ed( +fn send_weth_from_ethereum_to_non_existent_account_on_asset_hub_with_sufficient_fee_but_do_not_satisfy_ed( ) { // On AH the xcm fee is 26_789_690 and the ED is 3_300_000 - send_token_from_ethereum_to_asset_hub_with_fee([1; 32], 30_000_000); + send_weth_from_ethereum_to_asset_hub_with_fee([1; 32], 30_000_000); AssetHubRococo::execute_with(|| { type RuntimeEvent = ::RuntimeEvent; diff --git a/prdoc/pr_6855.prdoc b/prdoc/pr_6855.prdoc new file mode 100644 index 000000000000..a665115ce6c7 --- /dev/null +++ b/prdoc/pr_6855.prdoc @@ -0,0 +1,16 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Snowbridge - Support bridging native ETH + +doc: + - audience: Runtime User + description: + Support Native ETH as an asset type instead of only supporting WETH. WETH is still supported, but adds + support for ETH in the inbound and outbound routers. + +crates: + - name: snowbridge-router-primitives + bump: minor + - name: snowbridge-pallet-inbound-queue-fixtures + bump: minor