diff --git a/Cargo.lock b/Cargo.lock
index ed8d87361a1ac9d1f0d9bd24f12dd9624b35d6d2..2549bfbf8e3ae9fbf56beb8201f134a647faa2b4 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -19675,6 +19675,7 @@ dependencies = [
  "sp-std 14.0.0",
  "staging-xcm",
  "staging-xcm-builder",
+ "staging-xcm-executor",
 ]
 
 [[package]]
diff --git a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs
index 4a1486204eb08a43846bafaacfa05c465d8dc5fd..423b92b9fae04dceca93820f0a2f11a866498576 100644
--- a/bridges/snowbridge/pallets/inbound-queue/src/lib.rs
+++ b/bridges/snowbridge/pallets/inbound-queue/src/lib.rs
@@ -48,12 +48,11 @@ use frame_support::{
 };
 use frame_system::ensure_signed;
 use scale_info::TypeInfo;
-use sp_core::{H160, H256};
+use sp_core::H160;
 use sp_runtime::traits::Zero;
 use sp_std::vec;
 use xcm::prelude::{
-	send_xcm, Instruction::SetTopic, Junction::*, Location, SendError as XcmpSendError, SendXcm,
-	Xcm, XcmContext, XcmHash,
+	send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm, XcmContext, XcmHash,
 };
 use xcm_executor::traits::TransactAsset;
 
@@ -62,9 +61,8 @@ use snowbridge_core::{
 	sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters,
 	StaticLookup,
 };
-use snowbridge_router_primitives::{
-	inbound,
-	inbound::{ConvertMessage, ConvertMessageError},
+use snowbridge_router_primitives::inbound::{
+	ConvertMessage, ConvertMessageError, VersionedMessage,
 };
 use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError};
 
@@ -86,6 +84,7 @@ pub mod pallet {
 
 	use frame_support::pallet_prelude::*;
 	use frame_system::pallet_prelude::*;
+	use sp_core::H256;
 
 	#[pallet::pallet]
 	pub struct Pallet<T>(_);
@@ -276,12 +275,12 @@ pub mod pallet {
 				T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?;
 			}
 
+			// Decode payload into `VersionedMessage`
+			let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref())
+				.map_err(|_| Error::<T>::InvalidPayload)?;
+
 			// Decode message into XCM
-			let (xcm, fee) =
-				match inbound::VersionedMessage::decode_all(&mut envelope.payload.as_ref()) {
-					Ok(message) => Self::do_convert(envelope.message_id, message)?,
-					Err(_) => return Err(Error::<T>::InvalidPayload.into()),
-				};
+			let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?;
 
 			log::info!(
 				target: LOG_TARGET,
@@ -323,12 +322,10 @@ pub mod pallet {
 	impl<T: Config> Pallet<T> {
 		pub fn do_convert(
 			message_id: H256,
-			message: inbound::VersionedMessage,
+			message: VersionedMessage,
 		) -> Result<(Xcm<()>, BalanceOf<T>), Error<T>> {
-			let (mut xcm, fee) =
-				T::MessageConverter::convert(message).map_err(|e| Error::<T>::ConvertMessage(e))?;
-			// Append the message id as an XCM topic
-			xcm.inner_mut().extend(vec![SetTopic(message_id.into())]);
+			let (xcm, fee) = T::MessageConverter::convert(message_id, message)
+				.map_err(|e| Error::<T>::ConvertMessage(e))?;
 			Ok((xcm, fee))
 		}
 
diff --git a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs
index 871df6d1e51b333c71167295f0519a9c4e146b7b..3e67d5ab738b1a1a492667ceb955c5508292dac3 100644
--- a/bridges/snowbridge/pallets/inbound-queue/src/mock.rs
+++ b/bridges/snowbridge/pallets/inbound-queue/src/mock.rs
@@ -10,12 +10,12 @@ use snowbridge_beacon_primitives::{
 use snowbridge_core::{
 	gwei,
 	inbound::{Log, Proof, VerificationError},
-	meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup,
+	meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup, TokenId,
 };
 use snowbridge_router_primitives::inbound::MessageToXcm;
 use sp_core::{H160, H256};
 use sp_runtime::{
-	traits::{IdentifyAccount, IdentityLookup, Verify},
+	traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify},
 	BuildStorage, FixedU128, MultiSignature,
 };
 use sp_std::{convert::From, default::Default};
@@ -112,6 +112,9 @@ parameter_types! {
 	pub const SendTokenExecutionFee: u128 = 1_000_000_000;
 	pub const InitialFund: u128 = 1_000_000_000_000;
 	pub const InboundQueuePalletInstance: u8 = 80;
+	pub UniversalLocation: InteriorLocation =
+		[GlobalConsensus(Westend), Parachain(1002)].into();
+	pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Westend),Parachain(1000)]);
 }
 
 #[cfg(feature = "runtime-benchmarks")]
@@ -205,6 +208,16 @@ impl TransactAsset for SuccessfulTransactor {
 	}
 }
 
+pub struct MockTokenIdConvert;
+impl MaybeEquivalence<TokenId, Location> for MockTokenIdConvert {
+	fn convert(_id: &TokenId) -> Option<Location> {
+		Some(Location::parent())
+	}
+	fn convert_back(_loc: &Location) -> Option<TokenId> {
+		None
+	}
+}
+
 impl inbound_queue::Config for Test {
 	type RuntimeEvent = RuntimeEvent;
 	type Verifier = MockVerifier;
@@ -218,6 +231,9 @@ impl inbound_queue::Config for Test {
 		InboundQueuePalletInstance,
 		AccountId,
 		Balance,
+		MockTokenIdConvert,
+		UniversalLocation,
+		AssetHubFromEthereum,
 	>;
 	type PricingParameters = Parameters;
 	type ChannelLookup = MockChannelLookup;
diff --git a/bridges/snowbridge/pallets/outbound-queue/src/mock.rs b/bridges/snowbridge/pallets/outbound-queue/src/mock.rs
index d65a96e2702dfbc69e52a913964de230bcdeadd0..0b34893333e4b33be3be949a032521ac3f240a8f 100644
--- a/bridges/snowbridge/pallets/outbound-queue/src/mock.rs
+++ b/bridges/snowbridge/pallets/outbound-queue/src/mock.rs
@@ -164,13 +164,11 @@ pub fn mock_message(sibling_para_id: u32) -> Message {
 	Message {
 		id: None,
 		channel_id: ParaId::from(sibling_para_id).into(),
-		command: Command::AgentExecute {
+		command: Command::TransferNativeToken {
 			agent_id: Default::default(),
-			command: AgentExecuteCommand::TransferToken {
-				token: Default::default(),
-				recipient: Default::default(),
-				amount: 0,
-			},
+			token: Default::default(),
+			recipient: Default::default(),
+			amount: 0,
 		},
 	}
 }
diff --git a/bridges/snowbridge/pallets/system/src/benchmarking.rs b/bridges/snowbridge/pallets/system/src/benchmarking.rs
index ef908ad6a3f9dee2f269333a79b8d4afa5e6b7ca..20798b7c349389815b96eecc3d3f9887337f5fce 100644
--- a/bridges/snowbridge/pallets/system/src/benchmarking.rs
+++ b/bridges/snowbridge/pallets/system/src/benchmarking.rs
@@ -159,6 +159,29 @@ mod benchmarks {
 		Ok(())
 	}
 
+	#[benchmark]
+	fn register_token() -> Result<(), BenchmarkError> {
+		let caller: T::AccountId = whitelisted_caller();
+
+		let amount: BalanceOf<T> =
+			(10_000_000_000_000_u128).saturated_into::<u128>().saturated_into();
+
+		T::Token::mint_into(&caller, amount)?;
+
+		let relay_token_asset_id: Location = Location::parent();
+		let asset = Box::new(VersionedLocation::V4(relay_token_asset_id));
+		let asset_metadata = AssetMetadata {
+			name: "wnd".as_bytes().to_vec().try_into().unwrap(),
+			symbol: "wnd".as_bytes().to_vec().try_into().unwrap(),
+			decimals: 12,
+		};
+
+		#[extrinsic_call]
+		_(RawOrigin::Root, asset, asset_metadata);
+
+		Ok(())
+	}
+
 	impl_benchmark_test_suite!(
 		SnowbridgeControl,
 		crate::mock::new_test_ext(true),
diff --git a/bridges/snowbridge/pallets/system/src/lib.rs b/bridges/snowbridge/pallets/system/src/lib.rs
index 39c73e3630e7b425e46252943a44d10bfe1cebc7..1e8a788b7a5a8979e66f9e0003a1fc169f6c398d 100644
--- a/bridges/snowbridge/pallets/system/src/lib.rs
+++ b/bridges/snowbridge/pallets/system/src/lib.rs
@@ -35,8 +35,14 @@
 //!
 //! Typically, Polkadot governance will use the `force_transfer_native_from_agent` and
 //! `force_update_channel` and extrinsics to manage agents and channels for system parachains.
+//!
+//! ## Polkadot-native tokens on Ethereum
+//!
+//! Tokens deposited on AssetHub pallet can be bridged to Ethereum as wrapped ERC20 tokens. As a
+//! prerequisite, the token should be registered first.
+//!
+//! * [`Call::register_token`]: Register a token location as a wrapped ERC20 contract on Ethereum.
 #![cfg_attr(not(feature = "std"), no_std)]
-
 #[cfg(test)]
 mod mock;
 
@@ -63,13 +69,16 @@ use frame_system::pallet_prelude::*;
 use snowbridge_core::{
 	meth,
 	outbound::{Command, Initializer, Message, OperatingMode, SendError, SendMessage},
-	sibling_sovereign_account, AgentId, Channel, ChannelId, ParaId,
-	PricingParameters as PricingParametersRecord, PRIMARY_GOVERNANCE_CHANNEL,
+	sibling_sovereign_account, AgentId, AssetMetadata, Channel, ChannelId, ParaId,
+	PricingParameters as PricingParametersRecord, TokenId, TokenIdOf, PRIMARY_GOVERNANCE_CHANNEL,
 	SECONDARY_GOVERNANCE_CHANNEL,
 };
 use sp_core::{RuntimeDebug, H160, H256};
 use sp_io::hashing::blake2_256;
-use sp_runtime::{traits::BadOrigin, DispatchError, SaturatedConversion};
+use sp_runtime::{
+	traits::{BadOrigin, MaybeEquivalence},
+	DispatchError, SaturatedConversion,
+};
 use sp_std::prelude::*;
 use xcm::prelude::*;
 use xcm_executor::traits::ConvertLocation;
@@ -99,7 +108,7 @@ where
 }
 
 /// Hash the location to produce an agent id
-fn agent_id_of<T: Config>(location: &Location) -> Result<H256, DispatchError> {
+pub fn agent_id_of<T: Config>(location: &Location) -> Result<H256, DispatchError> {
 	T::AgentIdOf::convert_location(location).ok_or(Error::<T>::LocationConversionFailed.into())
 }
 
@@ -127,6 +136,7 @@ where
 
 #[frame_support::pallet]
 pub mod pallet {
+	use frame_support::dispatch::PostDispatchInfo;
 	use snowbridge_core::StaticLookup;
 	use sp_core::U256;
 
@@ -164,6 +174,12 @@ pub mod pallet {
 
 		type WeightInfo: WeightInfo;
 
+		/// This chain's Universal Location.
+		type UniversalLocation: Get<InteriorLocation>;
+
+		// The bridges configured Ethereum location
+		type EthereumLocation: Get<Location>;
+
 		#[cfg(feature = "runtime-benchmarks")]
 		type Helper: BenchmarkHelper<Self::RuntimeOrigin>;
 	}
@@ -211,6 +227,13 @@ pub mod pallet {
 		PricingParametersChanged {
 			params: PricingParametersOf<T>,
 		},
+		/// Register Polkadot-native token as a wrapped ERC20 token on Ethereum
+		RegisterToken {
+			/// Location of Polkadot-native token
+			location: VersionedLocation,
+			/// ID of Polkadot-native token on Ethereum
+			foreign_token_id: H256,
+		},
 	}
 
 	#[pallet::error]
@@ -243,6 +266,16 @@ pub mod pallet {
 	pub type PricingParameters<T: Config> =
 		StorageValue<_, PricingParametersOf<T>, ValueQuery, T::DefaultPricingParameters>;
 
+	/// Lookup table for foreign token ID to native location relative to ethereum
+	#[pallet::storage]
+	pub type ForeignToNativeId<T: Config> =
+		StorageMap<_, Blake2_128Concat, TokenId, xcm::v4::Location, OptionQuery>;
+
+	/// Lookup table for native location relative to ethereum to foreign token ID
+	#[pallet::storage]
+	pub type NativeToForeignId<T: Config> =
+		StorageMap<_, Blake2_128Concat, xcm::v4::Location, TokenId, OptionQuery>;
+
 	#[pallet::genesis_config]
 	#[derive(frame_support::DefaultNoBound)]
 	pub struct GenesisConfig<T: Config> {
@@ -574,6 +607,34 @@ pub mod pallet {
 			});
 			Ok(())
 		}
+
+		/// Registers a Polkadot-native token as a wrapped ERC20 token on Ethereum.
+		/// Privileged. Can only be called by root.
+		///
+		/// Fee required: No
+		///
+		/// - `origin`: Must be root
+		/// - `location`: Location of the asset (relative to this chain)
+		/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
+		#[pallet::call_index(10)]
+		#[pallet::weight(T::WeightInfo::register_token())]
+		pub fn register_token(
+			origin: OriginFor<T>,
+			location: Box<VersionedLocation>,
+			metadata: AssetMetadata,
+		) -> DispatchResultWithPostInfo {
+			ensure_root(origin)?;
+
+			let location: Location =
+				(*location).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
+
+			Self::do_register_token(&location, metadata, PaysFee::<T>::No)?;
+
+			Ok(PostDispatchInfo {
+				actual_weight: Some(T::WeightInfo::register_token()),
+				pays_fee: Pays::No,
+			})
+		}
 	}
 
 	impl<T: Config> Pallet<T> {
@@ -663,6 +724,42 @@ pub mod pallet {
 			let secondary_exists = Channels::<T>::contains_key(SECONDARY_GOVERNANCE_CHANNEL);
 			primary_exists && secondary_exists
 		}
+
+		pub(crate) fn do_register_token(
+			location: &Location,
+			metadata: AssetMetadata,
+			pays_fee: PaysFee<T>,
+		) -> Result<(), DispatchError> {
+			let ethereum_location = T::EthereumLocation::get();
+			// reanchor to Ethereum context
+			let location = location
+				.clone()
+				.reanchored(&ethereum_location, &T::UniversalLocation::get())
+				.map_err(|_| Error::<T>::LocationConversionFailed)?;
+
+			let token_id = TokenIdOf::convert_location(&location)
+				.ok_or(Error::<T>::LocationConversionFailed)?;
+
+			if !ForeignToNativeId::<T>::contains_key(token_id) {
+				NativeToForeignId::<T>::insert(location.clone(), token_id);
+				ForeignToNativeId::<T>::insert(token_id, location.clone());
+			}
+
+			let command = Command::RegisterForeignToken {
+				token_id,
+				name: metadata.name.into_inner(),
+				symbol: metadata.symbol.into_inner(),
+				decimals: metadata.decimals,
+			};
+			Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;
+
+			Self::deposit_event(Event::<T>::RegisterToken {
+				location: location.clone().into(),
+				foreign_token_id: token_id,
+			});
+
+			Ok(())
+		}
 	}
 
 	impl<T: Config> StaticLookup for Pallet<T> {
@@ -684,4 +781,13 @@ pub mod pallet {
 			PricingParameters::<T>::get()
 		}
 	}
+
+	impl<T: Config> MaybeEquivalence<TokenId, Location> for Pallet<T> {
+		fn convert(foreign_id: &TokenId) -> Option<Location> {
+			ForeignToNativeId::<T>::get(foreign_id)
+		}
+		fn convert_back(location: &Location) -> Option<TokenId> {
+			NativeToForeignId::<T>::get(location)
+		}
+	}
 }
diff --git a/bridges/snowbridge/pallets/system/src/mock.rs b/bridges/snowbridge/pallets/system/src/mock.rs
index 98bd3da9ab27c974a1a85abbb208064e41f89991..47b089866a53fda4a689efb5e37b8e66f5df433d 100644
--- a/bridges/snowbridge/pallets/system/src/mock.rs
+++ b/bridges/snowbridge/pallets/system/src/mock.rs
@@ -166,10 +166,12 @@ impl snowbridge_pallet_outbound_queue::Config for Test {
 parameter_types! {
 	pub const SS58Prefix: u8 = 42;
 	pub const AnyNetwork: Option<NetworkId> = None;
-	pub const RelayNetwork: Option<NetworkId> = Some(NetworkId::Kusama);
+	pub const RelayNetwork: Option<NetworkId> = Some(NetworkId::Polkadot);
 	pub const RelayLocation: Location = Location::parent();
 	pub UniversalLocation: InteriorLocation =
 		[GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(1013)].into();
+	pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
+	pub EthereumDestination: Location = Location::new(2,[GlobalConsensus(EthereumNetwork::get())]);
 }
 
 pub const DOT: u128 = 10_000_000_000;
@@ -177,8 +179,8 @@ pub const DOT: u128 = 10_000_000_000;
 parameter_types! {
 	pub TreasuryAccount: AccountId = PalletId(*b"py/trsry").into_account_truncating();
 	pub Fee: u64 = 1000;
-	pub const RococoNetwork: NetworkId = NetworkId::Rococo;
 	pub const InitialFunding: u128 = 1_000_000_000_000;
+	pub BridgeHubParaId: ParaId = ParaId::new(1002);
 	pub AssetHubParaId: ParaId = ParaId::new(1000);
 	pub TestParaId: u32 = 2000;
 	pub Parameters: PricingParameters<u128> = PricingParameters {
@@ -188,7 +190,6 @@ parameter_types! {
 		multiplier: FixedU128::from_rational(4, 3)
 	};
 	pub const InboundDeliveryCost: u128 = 1_000_000_000;
-
 }
 
 #[cfg(feature = "runtime-benchmarks")]
@@ -208,6 +209,8 @@ impl crate::Config for Test {
 	type DefaultPricingParameters = Parameters;
 	type WeightInfo = ();
 	type InboundDeliveryCost = InboundDeliveryCost;
+	type UniversalLocation = UniversalLocation;
+	type EthereumLocation = EthereumDestination;
 	#[cfg(feature = "runtime-benchmarks")]
 	type Helper = ();
 }
diff --git a/bridges/snowbridge/pallets/system/src/tests.rs b/bridges/snowbridge/pallets/system/src/tests.rs
index 09f24195a30ad8ee13f4587c54102bc24eed4134..d0286e04abdfca3375d1d71446f62408e4e94464 100644
--- a/bridges/snowbridge/pallets/system/src/tests.rs
+++ b/bridges/snowbridge/pallets/system/src/tests.rs
@@ -248,7 +248,7 @@ fn create_channel() {
 		let _ = Balances::mint_into(&sovereign_account, 10000);
 
 		assert_ok!(EthereumSystem::create_agent(origin.clone()));
-		assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
+		assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal,));
 	});
 }
 
@@ -264,10 +264,10 @@ fn create_channel_fail_already_exists() {
 		let _ = Balances::mint_into(&sovereign_account, 10000);
 
 		assert_ok!(EthereumSystem::create_agent(origin.clone()));
-		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
+		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal,));
 
 		assert_noop!(
-			EthereumSystem::create_channel(origin, OperatingMode::Normal),
+			EthereumSystem::create_channel(origin, OperatingMode::Normal,),
 			Error::<Test>::ChannelAlreadyCreated
 		);
 	});
@@ -334,10 +334,10 @@ fn update_channel() {
 		// First create the channel
 		let _ = Balances::mint_into(&sovereign_account, 10000);
 		assert_ok!(EthereumSystem::create_agent(origin.clone()));
-		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
+		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal,));
 
 		// Now try to update it
-		assert_ok!(EthereumSystem::update_channel(origin, OperatingMode::Normal));
+		assert_ok!(EthereumSystem::update_channel(origin, OperatingMode::Normal,));
 
 		System::assert_last_event(RuntimeEvent::EthereumSystem(crate::Event::UpdateChannel {
 			channel_id: ParaId::from(2000).into(),
@@ -383,12 +383,12 @@ fn update_channel_bad_origin() {
 
 		// Signed origin not allowed
 		assert_noop!(
-			EthereumSystem::update_channel(RuntimeOrigin::signed([14; 32].into()), mode),
+			EthereumSystem::update_channel(RuntimeOrigin::signed([14; 32].into()), mode,),
 			BadOrigin
 		);
 
 		// None origin not allowed
-		assert_noop!(EthereumSystem::update_channel(RuntimeOrigin::none(), mode), BadOrigin);
+		assert_noop!(EthereumSystem::update_channel(RuntimeOrigin::none(), mode,), BadOrigin);
 	});
 }
 
@@ -400,7 +400,7 @@ fn update_channel_fails_not_exist() {
 
 		// Now try to update it
 		assert_noop!(
-			EthereumSystem::update_channel(origin, OperatingMode::Normal),
+			EthereumSystem::update_channel(origin, OperatingMode::Normal,),
 			Error::<Test>::NoChannel
 		);
 	});
@@ -419,7 +419,7 @@ fn force_update_channel() {
 		// First create the channel
 		let _ = Balances::mint_into(&sovereign_account, 10000);
 		assert_ok!(EthereumSystem::create_agent(origin.clone()));
-		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
+		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal,));
 
 		// Now try to force update it
 		let force_origin = RuntimeOrigin::root();
@@ -463,7 +463,7 @@ fn transfer_native_from_agent() {
 
 		// First create the agent and channel
 		assert_ok!(EthereumSystem::create_agent(origin.clone()));
-		assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal));
+		assert_ok!(EthereumSystem::create_channel(origin, OperatingMode::Normal,));
 
 		let origin = make_xcm_origin(origin_location.clone());
 		assert_ok!(EthereumSystem::transfer_native_from_agent(origin, recipient, amount),);
@@ -584,7 +584,7 @@ fn charge_fee_for_transfer_native_from_agent() {
 
 		// create_agent & create_channel first
 		assert_ok!(EthereumSystem::create_agent(origin.clone()));
-		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal));
+		assert_ok!(EthereumSystem::create_channel(origin.clone(), OperatingMode::Normal,));
 
 		// assert sovereign_balance decreased by only the base_fee
 		let sovereign_balance_before = Balances::balance(&sovereign_account);
@@ -631,3 +631,116 @@ fn no_genesis_build_is_uninitialized() {
 		assert!(!EthereumSystem::is_initialized(), "Ethereum initialized.");
 	});
 }
+
+#[test]
+fn register_token_with_signed_yields_bad_origin() {
+	new_test_ext(true).execute_with(|| {
+		let origin = RuntimeOrigin::signed([14; 32].into());
+		let location = Location::new(1, [Parachain(2000)]);
+		let versioned_location: Box<VersionedLocation> = Box::new(location.clone().into());
+		assert_noop!(
+			EthereumSystem::register_token(origin, versioned_location, Default::default()),
+			BadOrigin
+		);
+	});
+}
+
+pub struct RegisterTokenTestCase {
+	/// Input: Location of Polkadot-native token relative to BH
+	pub native: Location,
+	/// Output: Reanchored, canonicalized location
+	pub reanchored: Location,
+	/// Output: Stable hash of reanchored location
+	pub foreign: TokenId,
+}
+
+#[test]
+fn register_all_tokens_succeeds() {
+	let test_cases = vec![
+		// DOT
+		RegisterTokenTestCase {
+			native: Location::parent(),
+			reanchored: Location::new(1, GlobalConsensus(Polkadot)),
+			foreign: hex!("4e241583d94b5d48a27a22064cd49b2ed6f5231d2d950e432f9b7c2e0ade52b2")
+				.into(),
+		},
+		// GLMR (Some Polkadot parachain currency)
+		RegisterTokenTestCase {
+			native: Location::new(1, [Parachain(2004)]),
+			reanchored: Location::new(1, [GlobalConsensus(Polkadot), Parachain(2004)]),
+			foreign: hex!("34c08fc90409b6924f0e8eabb7c2aaa0c749e23e31adad9f6d217b577737fafb")
+				.into(),
+		},
+		// USDT
+		RegisterTokenTestCase {
+			native: Location::new(1, [Parachain(1000), PalletInstance(50), GeneralIndex(1984)]),
+			reanchored: Location::new(
+				1,
+				[
+					GlobalConsensus(Polkadot),
+					Parachain(1000),
+					PalletInstance(50),
+					GeneralIndex(1984),
+				],
+			),
+			foreign: hex!("14b0579be12d7d7f9971f1d4b41f0e88384b9b74799b0150d4aa6cd01afb4444")
+				.into(),
+		},
+		// KSM
+		RegisterTokenTestCase {
+			native: Location::new(2, [GlobalConsensus(Kusama)]),
+			reanchored: Location::new(1, [GlobalConsensus(Kusama)]),
+			foreign: hex!("03b6054d0c576dd8391e34e1609cf398f68050c23009d19ce93c000922bcd852")
+				.into(),
+		},
+		// KAR (Some Kusama parachain currency)
+		RegisterTokenTestCase {
+			native: Location::new(2, [GlobalConsensus(Kusama), Parachain(2000)]),
+			reanchored: Location::new(1, [GlobalConsensus(Kusama), Parachain(2000)]),
+			foreign: hex!("d3e39ad6ea4cee68c9741181e94098823b2ea34a467577d0875c036f0fce5be0")
+				.into(),
+		},
+	];
+	for tc in test_cases.iter() {
+		new_test_ext(true).execute_with(|| {
+			let origin = RuntimeOrigin::root();
+			let versioned_location: VersionedLocation = tc.native.clone().into();
+
+			assert_ok!(EthereumSystem::register_token(
+				origin,
+				Box::new(versioned_location),
+				Default::default()
+			));
+
+			assert_eq!(NativeToForeignId::<Test>::get(tc.reanchored.clone()), Some(tc.foreign));
+			assert_eq!(ForeignToNativeId::<Test>::get(tc.foreign), Some(tc.reanchored.clone()));
+
+			System::assert_last_event(RuntimeEvent::EthereumSystem(Event::<Test>::RegisterToken {
+				location: tc.reanchored.clone().into(),
+				foreign_token_id: tc.foreign,
+			}));
+		});
+	}
+}
+
+#[test]
+fn register_ethereum_native_token_fails() {
+	new_test_ext(true).execute_with(|| {
+		let origin = RuntimeOrigin::root();
+		let location = Location::new(
+			2,
+			[
+				GlobalConsensus(Ethereum { chain_id: 11155111 }),
+				AccountKey20 {
+					network: None,
+					key: hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d"),
+				},
+			],
+		);
+		let versioned_location: Box<VersionedLocation> = Box::new(location.clone().into());
+		assert_noop!(
+			EthereumSystem::register_token(origin, versioned_location, Default::default()),
+			Error::<Test>::LocationConversionFailed
+		);
+	});
+}
diff --git a/bridges/snowbridge/pallets/system/src/weights.rs b/bridges/snowbridge/pallets/system/src/weights.rs
index 6e532a0d8a8c19339cc39574aeb09668468f34e9..3513097f8b554ac40a1c3780fb7da59c65dcfd20 100644
--- a/bridges/snowbridge/pallets/system/src/weights.rs
+++ b/bridges/snowbridge/pallets/system/src/weights.rs
@@ -42,6 +42,7 @@ pub trait WeightInfo {
 	fn force_transfer_native_from_agent() -> Weight;
 	fn set_token_transfer_fees() -> Weight;
 	fn set_pricing_parameters() -> Weight;
+	fn register_token() -> Weight;
 }
 
 // For backwards compatibility and tests.
@@ -246,4 +247,14 @@ impl WeightInfo for () {
 			.saturating_add(RocksDbWeight::get().reads(4_u64))
 			.saturating_add(RocksDbWeight::get().writes(3_u64))
 	}
+
+	fn register_token() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `256`
+		//  Estimated: `6044`
+		// Minimum execution time: 45_000_000 picoseconds.
+		Weight::from_parts(45_000_000, 6044)
+			.saturating_add(RocksDbWeight::get().reads(5_u64))
+			.saturating_add(RocksDbWeight::get().writes(3_u64))
+	}
 }
diff --git a/bridges/snowbridge/primitives/core/Cargo.toml b/bridges/snowbridge/primitives/core/Cargo.toml
index f9bee1ff4959ae56f73b50d4c91c2ea2e63bb0a6..fa37c795b2d1e0871109cb393d6ef546e282cc99 100644
--- a/bridges/snowbridge/primitives/core/Cargo.toml
+++ b/bridges/snowbridge/primitives/core/Cargo.toml
@@ -35,6 +35,7 @@ ethabi = { workspace = true }
 
 [dev-dependencies]
 hex = { workspace = true, default-features = true }
+xcm-executor = { workspace = true, default-features = true }
 
 [features]
 default = ["std"]
@@ -62,4 +63,5 @@ runtime-benchmarks = [
 	"polkadot-parachain-primitives/runtime-benchmarks",
 	"sp-runtime/runtime-benchmarks",
 	"xcm-builder/runtime-benchmarks",
+	"xcm-executor/runtime-benchmarks",
 ]
diff --git a/bridges/snowbridge/primitives/core/src/lib.rs b/bridges/snowbridge/primitives/core/src/lib.rs
index ed1af4225d24bb9ae8d08f7adda2925e7d5029d3..7ad129a52542ed32f96e4533e7cf962039d7224d 100644
--- a/bridges/snowbridge/primitives/core/src/lib.rs
+++ b/bridges/snowbridge/primitives/core/src/lib.rs
@@ -9,11 +9,13 @@
 mod tests;
 
 pub mod inbound;
+pub mod location;
 pub mod operating_mode;
 pub mod outbound;
 pub mod pricing;
 pub mod ringbuffer;
 
+pub use location::{AgentId, AgentIdOf, TokenId, TokenIdOf};
 pub use polkadot_parachain_primitives::primitives::{
 	Id as ParaId, IsSystem, Sibling as SiblingParaId,
 };
@@ -21,18 +23,16 @@ pub use ringbuffer::{RingBufferMap, RingBufferMapImpl};
 pub use sp_core::U256;
 
 use codec::{Decode, Encode, MaxEncodedLen};
-use frame_support::traits::Contains;
+use frame_support::{traits::Contains, BoundedVec};
 use hex_literal::hex;
 use scale_info::TypeInfo;
-use sp_core::H256;
+use sp_core::{ConstU32, H256};
 use sp_io::hashing::keccak_256;
 use sp_runtime::{traits::AccountIdConversion, RuntimeDebug};
 use sp_std::prelude::*;
 use xcm::prelude::{Junction::Parachain, Location};
-use xcm_builder::{DescribeAllTerminal, DescribeFamily, DescribeLocation, HashedDescription};
 
 /// The ID of an agent contract
-pub type AgentId = H256;
 pub use operating_mode::BasicOperatingMode;
 
 pub use pricing::{PricingParameters, Rewards};
@@ -151,16 +151,24 @@ pub const PRIMARY_GOVERNANCE_CHANNEL: ChannelId =
 pub const SECONDARY_GOVERNANCE_CHANNEL: ChannelId =
 	ChannelId::new(hex!("0000000000000000000000000000000000000000000000000000000000000002"));
 
-pub struct DescribeHere;
-impl DescribeLocation for DescribeHere {
-	fn describe_location(l: &Location) -> Option<Vec<u8>> {
-		match l.unpack() {
-			(0, []) => Some(Vec::<u8>::new().encode()),
-			_ => None,
+/// Metadata to include in the instantiated ERC20 token contract
+#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)]
+pub struct AssetMetadata {
+	pub name: BoundedVec<u8, ConstU32<METADATA_FIELD_MAX_LEN>>,
+	pub symbol: BoundedVec<u8, ConstU32<METADATA_FIELD_MAX_LEN>>,
+	pub decimals: u8,
+}
+
+#[cfg(any(test, feature = "std", feature = "runtime-benchmarks"))]
+impl Default for AssetMetadata {
+	fn default() -> Self {
+		AssetMetadata {
+			name: BoundedVec::truncate_from(vec![]),
+			symbol: BoundedVec::truncate_from(vec![]),
+			decimals: 0,
 		}
 	}
 }
 
-/// Creates an AgentId from a Location. An AgentId is a unique mapping to a Agent contract on
-/// Ethereum which acts as the sovereign account for the Location.
-pub type AgentIdOf = HashedDescription<H256, (DescribeHere, DescribeFamily<DescribeAllTerminal>)>;
+/// Maximum length of a string field in ERC20 token metada
+const METADATA_FIELD_MAX_LEN: u32 = 32;
diff --git a/bridges/snowbridge/primitives/core/src/location.rs b/bridges/snowbridge/primitives/core/src/location.rs
new file mode 100644
index 0000000000000000000000000000000000000000..aad1c9ece05c3f7097a08ee243bad0ccec60010d
--- /dev/null
+++ b/bridges/snowbridge/primitives/core/src/location.rs
@@ -0,0 +1,205 @@
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
+//! # Location
+//!
+//! Location helpers for dealing with Tokens and Agents
+
+pub use polkadot_parachain_primitives::primitives::{
+	Id as ParaId, IsSystem, Sibling as SiblingParaId,
+};
+pub use sp_core::U256;
+
+use codec::Encode;
+use sp_core::H256;
+use sp_std::prelude::*;
+use xcm::prelude::{
+	AccountId32, AccountKey20, GeneralIndex, GeneralKey, GlobalConsensus, Location, PalletInstance,
+};
+use xcm_builder::{
+	DescribeAllTerminal, DescribeFamily, DescribeLocation, DescribeTerminus, HashedDescription,
+};
+
+pub type AgentId = H256;
+
+/// Creates an AgentId from a Location. An AgentId is a unique mapping to a Agent contract on
+/// Ethereum which acts as the sovereign account for the Location.
+#[allow(deprecated)]
+pub type AgentIdOf =
+	HashedDescription<AgentId, (DescribeHere, DescribeFamily<DescribeAllTerminal>)>;
+
+pub type TokenId = H256;
+
+/// Convert a token location (relative to Ethereum) to a stable ID that can be used on the Ethereum
+/// side
+pub type TokenIdOf = HashedDescription<
+	TokenId,
+	DescribeGlobalPrefix<(DescribeTerminus, DescribeFamily<DescribeTokenTerminal>)>,
+>;
+
+/// This looks like DescribeTerminus that was added to xcm-builder. However this does an extra
+/// `encode` to the Vector producing a different output to DescribeTerminus. `DescribeHere`
+/// should NOT be used for new code. This is left here for backwards compatibility of channels and
+/// agents.
+#[deprecated(note = "Use DescribeTerminus from xcm-builder instead.")]
+pub struct DescribeHere;
+#[allow(deprecated)]
+impl DescribeLocation for DescribeHere {
+	fn describe_location(l: &Location) -> Option<Vec<u8>> {
+		match l.unpack() {
+			(0, []) => Some(Vec::<u8>::new().encode()),
+			_ => None,
+		}
+	}
+}
+pub struct DescribeGlobalPrefix<DescribeInterior>(sp_std::marker::PhantomData<DescribeInterior>);
+impl<Suffix: DescribeLocation> DescribeLocation for DescribeGlobalPrefix<Suffix> {
+	fn describe_location(l: &Location) -> Option<Vec<u8>> {
+		match (l.parent_count(), l.first_interior()) {
+			(1, Some(GlobalConsensus(network))) => {
+				let mut tail = l.clone().split_first_interior().0;
+				tail.dec_parent();
+				let interior = Suffix::describe_location(&tail)?;
+				Some((b"GlobalConsensus", network, interior).encode())
+			},
+			_ => None,
+		}
+	}
+}
+
+pub struct DescribeTokenTerminal;
+impl DescribeLocation for DescribeTokenTerminal {
+	fn describe_location(l: &Location) -> Option<Vec<u8>> {
+		match l.unpack().1 {
+			[] => Some(Vec::<u8>::new().encode()),
+			[GeneralIndex(index)] => Some((b"GeneralIndex", *index).encode()),
+			[GeneralKey { data, .. }] => Some((b"GeneralKey", *data).encode()),
+			[AccountKey20 { key, .. }] => Some((b"AccountKey20", *key).encode()),
+			[AccountId32 { id, .. }] => Some((b"AccountId32", *id).encode()),
+
+			// Pallet
+			[PalletInstance(instance)] => Some((b"PalletInstance", *instance).encode()),
+			[PalletInstance(instance), GeneralIndex(index)] =>
+				Some((b"PalletInstance", *instance, b"GeneralIndex", *index).encode()),
+			[PalletInstance(instance), GeneralKey { data, .. }] =>
+				Some((b"PalletInstance", *instance, b"GeneralKey", *data).encode()),
+
+			[PalletInstance(instance), AccountKey20 { key, .. }] =>
+				Some((b"PalletInstance", *instance, b"AccountKey20", *key).encode()),
+			[PalletInstance(instance), AccountId32 { id, .. }] =>
+				Some((b"PalletInstance", *instance, b"AccountId32", *id).encode()),
+
+			// Reject all other locations
+			_ => None,
+		}
+	}
+}
+
+#[cfg(test)]
+mod tests {
+	use crate::TokenIdOf;
+	use xcm::prelude::{
+		GeneralIndex, GeneralKey, GlobalConsensus, Junction::*, Location, NetworkId::*,
+		PalletInstance, Parachain,
+	};
+	use xcm_executor::traits::ConvertLocation;
+
+	#[test]
+	fn test_token_of_id() {
+		let token_locations = [
+			// Relay Chain cases
+			// Relay Chain relative to Ethereum
+			Location::new(1, [GlobalConsensus(Westend)]),
+			// Parachain cases
+			// Parachain relative to Ethereum
+			Location::new(1, [GlobalConsensus(Westend), Parachain(2000)]),
+			// Parachain general index
+			Location::new(1, [GlobalConsensus(Westend), Parachain(2000), GeneralIndex(1)]),
+			// Parachain general key
+			Location::new(
+				1,
+				[
+					GlobalConsensus(Westend),
+					Parachain(2000),
+					GeneralKey { length: 32, data: [0; 32] },
+				],
+			),
+			// Parachain account key 20
+			Location::new(
+				1,
+				[
+					GlobalConsensus(Westend),
+					Parachain(2000),
+					AccountKey20 { network: None, key: [0; 20] },
+				],
+			),
+			// Parachain account id 32
+			Location::new(
+				1,
+				[
+					GlobalConsensus(Westend),
+					Parachain(2000),
+					AccountId32 { network: None, id: [0; 32] },
+				],
+			),
+			// Parchain Pallet instance cases
+			// Parachain pallet instance
+			Location::new(1, [GlobalConsensus(Westend), Parachain(2000), PalletInstance(8)]),
+			// Parachain Pallet general index
+			Location::new(
+				1,
+				[GlobalConsensus(Westend), Parachain(2000), PalletInstance(8), GeneralIndex(1)],
+			),
+			// Parachain Pallet general key
+			Location::new(
+				1,
+				[
+					GlobalConsensus(Westend),
+					Parachain(2000),
+					PalletInstance(8),
+					GeneralKey { length: 32, data: [0; 32] },
+				],
+			),
+			// Parachain Pallet account key 20
+			Location::new(
+				1,
+				[
+					GlobalConsensus(Westend),
+					Parachain(2000),
+					PalletInstance(8),
+					AccountKey20 { network: None, key: [0; 20] },
+				],
+			),
+			// Parachain Pallet account id 32
+			Location::new(
+				1,
+				[
+					GlobalConsensus(Westend),
+					Parachain(2000),
+					PalletInstance(8),
+					AccountId32 { network: None, id: [0; 32] },
+				],
+			),
+		];
+
+		for token in token_locations {
+			assert!(
+				TokenIdOf::convert_location(&token).is_some(),
+				"Valid token = {token:?} yeilds no TokenId."
+			);
+		}
+
+		let non_token_locations = [
+			// Relative location for a token should fail.
+			Location::new(1, []),
+			// Relative location for a token should fail.
+			Location::new(1, [Parachain(1000)]),
+		];
+
+		for token in non_token_locations {
+			assert!(
+				TokenIdOf::convert_location(&token).is_none(),
+				"Invalid token = {token:?} yeilds a TokenId."
+			);
+		}
+	}
+}
diff --git a/bridges/snowbridge/primitives/core/src/outbound.rs b/bridges/snowbridge/primitives/core/src/outbound.rs
index 0ba0fdb61089e9bd77f162a87bcf7f78842bae5c..77770761822a8fe1a88d39f304dac9124ba9be1a 100644
--- a/bridges/snowbridge/primitives/core/src/outbound.rs
+++ b/bridges/snowbridge/primitives/core/src/outbound.rs
@@ -139,6 +139,37 @@ mod v1 {
 			// Fee multiplier
 			multiplier: UD60x18,
 		},
+		/// Transfer ERC20 tokens
+		TransferNativeToken {
+			/// ID of the agent
+			agent_id: H256,
+			/// Address of the ERC20 token
+			token: H160,
+			/// The recipient of the tokens
+			recipient: H160,
+			/// The amount of tokens to transfer
+			amount: u128,
+		},
+		/// Register foreign token from Polkadot
+		RegisterForeignToken {
+			/// ID for the token
+			token_id: H256,
+			/// Name of the token
+			name: Vec<u8>,
+			/// Short symbol for the token
+			symbol: Vec<u8>,
+			/// Number of decimal places
+			decimals: u8,
+		},
+		/// Mint foreign token from Polkadot
+		MintForeignToken {
+			/// ID for the token
+			token_id: H256,
+			/// The recipient of the newly minted tokens
+			recipient: H160,
+			/// The amount of tokens to mint
+			amount: u128,
+		},
 	}
 
 	impl Command {
@@ -154,6 +185,9 @@ mod v1 {
 				Command::TransferNativeFromAgent { .. } => 6,
 				Command::SetTokenTransferFees { .. } => 7,
 				Command::SetPricingParameters { .. } => 8,
+				Command::TransferNativeToken { .. } => 9,
+				Command::RegisterForeignToken { .. } => 10,
+				Command::MintForeignToken { .. } => 11,
 			}
 		}
 
@@ -211,6 +245,26 @@ mod v1 {
 						Token::Uint(U256::from(*delivery_cost)),
 						Token::Uint(multiplier.clone().into_inner()),
 					])]),
+				Command::TransferNativeToken { agent_id, token, recipient, amount } =>
+					ethabi::encode(&[Token::Tuple(vec![
+						Token::FixedBytes(agent_id.as_bytes().to_owned()),
+						Token::Address(*token),
+						Token::Address(*recipient),
+						Token::Uint(U256::from(*amount)),
+					])]),
+				Command::RegisterForeignToken { token_id, name, symbol, decimals } =>
+					ethabi::encode(&[Token::Tuple(vec![
+						Token::FixedBytes(token_id.as_bytes().to_owned()),
+						Token::String(name.to_owned()),
+						Token::String(symbol.to_owned()),
+						Token::Uint(U256::from(*decimals)),
+					])]),
+				Command::MintForeignToken { token_id, recipient, amount } =>
+					ethabi::encode(&[Token::Tuple(vec![
+						Token::FixedBytes(token_id.as_bytes().to_owned()),
+						Token::Address(*recipient),
+						Token::Uint(U256::from(*amount)),
+					])]),
 			}
 		}
 	}
@@ -403,6 +457,9 @@ impl GasMeter for ConstantGasMeter {
 			},
 			Command::SetTokenTransferFees { .. } => 60_000,
 			Command::SetPricingParameters { .. } => 60_000,
+			Command::TransferNativeToken { .. } => 100_000,
+			Command::RegisterForeignToken { .. } => 1_200_000,
+			Command::MintForeignToken { .. } => 100_000,
 		}
 	}
 }
diff --git a/bridges/snowbridge/primitives/router/src/inbound/mod.rs b/bridges/snowbridge/primitives/router/src/inbound/mod.rs
index 54e47a7a8b6af0a0dce1d54d52e3e0799e6f6e84..5cff8413af66b23fe55fc6c68f8b936768289ce9 100644
--- a/bridges/snowbridge/primitives/router/src/inbound/mod.rs
+++ b/bridges/snowbridge/primitives/router/src/inbound/mod.rs
@@ -9,9 +9,10 @@ use codec::{Decode, Encode};
 use core::marker::PhantomData;
 use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError};
 use scale_info::TypeInfo;
-use sp_core::{Get, RuntimeDebug, H160};
+use snowbridge_core::TokenId;
+use sp_core::{Get, RuntimeDebug, H160, H256};
 use sp_io::hashing::blake2_256;
-use sp_runtime::MultiAddress;
+use sp_runtime::{traits::MaybeEquivalence, MultiAddress};
 use sp_std::prelude::*;
 use xcm::prelude::{Junction::AccountKey20, *};
 use xcm_executor::traits::ConvertLocation;
@@ -45,7 +46,7 @@ pub enum Command {
 		/// XCM execution fee on AssetHub
 		fee: u128,
 	},
-	/// Send a token to AssetHub or another parachain
+	/// Send Ethereum token to AssetHub or another parachain
 	SendToken {
 		/// The address of the ERC20 token to be bridged over to AssetHub
 		token: H160,
@@ -56,6 +57,17 @@ pub enum Command {
 		/// 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
@@ -89,10 +101,16 @@ pub struct MessageToXcm<
 	InboundQueuePalletInstance,
 	AccountId,
 	Balance,
+	ConvertAssetId,
+	EthereumUniversalLocation,
+	GlobalAssetHubLocation,
 > where
 	CreateAssetCall: Get<CallIndex>,
 	CreateAssetDeposit: Get<u128>,
 	Balance: BalanceT,
+	ConvertAssetId: MaybeEquivalence<TokenId, Location>,
+	EthereumUniversalLocation: Get<InteriorLocation>,
+	GlobalAssetHubLocation: Get<Location>,
 {
 	_phantom: PhantomData<(
 		CreateAssetCall,
@@ -100,6 +118,9 @@ pub struct MessageToXcm<
 		InboundQueuePalletInstance,
 		AccountId,
 		Balance,
+		ConvertAssetId,
+		EthereumUniversalLocation,
+		GlobalAssetHubLocation,
 	)>,
 }
 
@@ -108,6 +129,11 @@ pub struct MessageToXcm<
 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
@@ -115,51 +141,107 @@ pub trait ConvertMessage {
 	type Balance: BalanceT + From<u128>;
 	type AccountId;
 	/// Converts a versioned message into an XCM message and an optional topicID
-	fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
+	fn convert(
+		message_id: H256,
+		message: VersionedMessage,
+	) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
 }
 
 pub type CallIndex = [u8; 2];
 
-impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
-	ConvertMessage
+impl<
+		CreateAssetCall,
+		CreateAssetDeposit,
+		InboundQueuePalletInstance,
+		AccountId,
+		Balance,
+		ConvertAssetId,
+		EthereumUniversalLocation,
+		GlobalAssetHubLocation,
+	> ConvertMessage
 	for MessageToXcm<
 		CreateAssetCall,
 		CreateAssetDeposit,
 		InboundQueuePalletInstance,
 		AccountId,
 		Balance,
+		ConvertAssetId,
+		EthereumUniversalLocation,
+		GlobalAssetHubLocation,
 	> where
 	CreateAssetCall: Get<CallIndex>,
 	CreateAssetDeposit: Get<u128>,
 	InboundQueuePalletInstance: Get<u8>,
 	Balance: BalanceT + From<u128>,
 	AccountId: Into<[u8; 32]>,
+	ConvertAssetId: MaybeEquivalence<TokenId, Location>,
+	EthereumUniversalLocation: Get<InteriorLocation>,
+	GlobalAssetHubLocation: Get<Location>,
 {
 	type Balance = Balance;
 	type AccountId = AccountId;
 
-	fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
+	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(chain_id, 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(chain_id, 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>
-	MessageToXcm<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
-where
+impl<
+		CreateAssetCall,
+		CreateAssetDeposit,
+		InboundQueuePalletInstance,
+		AccountId,
+		Balance,
+		ConvertAssetId,
+		EthereumUniversalLocation,
+		GlobalAssetHubLocation,
+	>
+	MessageToXcm<
+		CreateAssetCall,
+		CreateAssetDeposit,
+		InboundQueuePalletInstance,
+		AccountId,
+		Balance,
+		ConvertAssetId,
+		EthereumUniversalLocation,
+		GlobalAssetHubLocation,
+	> where
 	CreateAssetCall: Get<CallIndex>,
 	CreateAssetDeposit: Get<u128>,
 	InboundQueuePalletInstance: Get<u8>,
 	Balance: BalanceT + From<u128>,
 	AccountId: Into<[u8; 32]>,
+	ConvertAssetId: MaybeEquivalence<TokenId, Location>,
+	EthereumUniversalLocation: Get<InteriorLocation>,
+	GlobalAssetHubLocation: Get<Location>,
 {
-	fn convert_register_token(chain_id: u64, token: H160, fee: u128) -> (Xcm<()>, Balance) {
+	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();
@@ -202,6 +284,8 @@ where
 			// Clear the origin so that remaining assets in holding
 			// are claimable by the physical origin (BridgeHub)
 			ClearOrigin,
+			// Forward message id to Asset Hub
+			SetTopic(message_id.into()),
 		]
 		.into();
 
@@ -209,6 +293,7 @@ where
 	}
 
 	fn convert_send_token(
+		message_id: H256,
 		chain_id: u64,
 		token: H160,
 		destination: Destination,
@@ -266,6 +351,8 @@ where
 							BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
 							// Deposit asset to beneficiary.
 							DepositAsset { assets: Definite(asset.into()), beneficiary },
+							// Forward message id to destination parachain.
+							SetTopic(message_id.into()),
 						]
 						.into(),
 					},
@@ -281,6 +368,9 @@ where
 			},
 		}
 
+		// Forward message id to Asset Hub.
+		instructions.push(SetTopic(message_id.into()));
+
 		(instructions.into(), total_fees.into())
 	}
 
@@ -291,6 +381,59 @@ where
 			[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 }])),
+			_ => 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 GlobalConsensusEthereumConvertsFor<AccountId>(PhantomData<AccountId>);
diff --git a/bridges/snowbridge/primitives/router/src/inbound/tests.rs b/bridges/snowbridge/primitives/router/src/inbound/tests.rs
index 75670b05c1004d65f16e20848c05ca9b28c38452..e0e90e516be166b0dadd06b158c035e1f12e9d30 100644
--- a/bridges/snowbridge/primitives/router/src/inbound/tests.rs
+++ b/bridges/snowbridge/primitives/router/src/inbound/tests.rs
@@ -1,6 +1,6 @@
 use super::GlobalConsensusEthereumConvertsFor;
 use crate::inbound::CallIndex;
-use frame_support::parameter_types;
+use frame_support::{assert_ok, parameter_types};
 use hex_literal::hex;
 use xcm::prelude::*;
 use xcm_executor::traits::ConvertLocation;
@@ -38,3 +38,32 @@ fn test_contract_location_with_incorrect_location_fails_convert() {
 		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(&ethereum, &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, &ethereum_context));
+		assert_eq!(reanchored_asset_with_ethereum_context, asset.clone());
+	}
+}
diff --git a/bridges/snowbridge/primitives/router/src/outbound/mod.rs b/bridges/snowbridge/primitives/router/src/outbound/mod.rs
index ddc36ce8cb61b370e862de1784a4f6b9fc6f16b0..d3b6c116dd7a3420c730f77614de61f355b64319 100644
--- a/bridges/snowbridge/primitives/router/src/outbound/mod.rs
+++ b/bridges/snowbridge/primitives/router/src/outbound/mod.rs
@@ -12,9 +12,10 @@ use codec::{Decode, Encode};
 use frame_support::{ensure, traits::Get};
 use snowbridge_core::{
 	outbound::{AgentExecuteCommand, Command, Message, SendMessage},
-	ChannelId, ParaId,
+	AgentId, ChannelId, ParaId, TokenId, TokenIdOf,
 };
 use sp_core::{H160, H256};
+use sp_runtime::traits::MaybeEquivalence;
 use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
 use xcm::prelude::*;
 use xcm_executor::traits::{ConvertLocation, ExportXcm};
@@ -24,15 +25,31 @@ pub struct EthereumBlobExporter<
 	EthereumNetwork,
 	OutboundQueue,
 	AgentHashedDescription,
->(PhantomData<(UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription)>);
-
-impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription> ExportXcm
-	for EthereumBlobExporter<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription>
-where
+	ConvertAssetId,
+>(
+	PhantomData<(
+		UniversalLocation,
+		EthereumNetwork,
+		OutboundQueue,
+		AgentHashedDescription,
+		ConvertAssetId,
+	)>,
+);
+
+impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription, ConvertAssetId>
+	ExportXcm
+	for EthereumBlobExporter<
+		UniversalLocation,
+		EthereumNetwork,
+		OutboundQueue,
+		AgentHashedDescription,
+		ConvertAssetId,
+	> where
 	UniversalLocation: Get<InteriorLocation>,
 	EthereumNetwork: Get<NetworkId>,
 	OutboundQueue: SendMessage<Balance = u128>,
 	AgentHashedDescription: ConvertLocation<H256>,
+	ConvertAssetId: MaybeEquivalence<TokenId, Location>,
 {
 	type Ticket = (Vec<u8>, XcmHash);
 
@@ -87,13 +104,8 @@ where
 			SendError::MissingArgument
 		})?;
 
-		let mut converter = XcmConverter::new(&message, &expected_network);
-		let (agent_execute_command, message_id) = converter.convert().map_err(|err|{
-			log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
-			SendError::Unroutable
-		})?;
-
 		let source_location = Location::new(1, local_sub.clone());
+
 		let agent_id = match AgentHashedDescription::convert_location(&source_location) {
 			Some(id) => id,
 			None => {
@@ -102,13 +114,16 @@ where
 			},
 		};
 
+		let mut converter =
+			XcmConverter::<ConvertAssetId, ()>::new(&message, expected_network, agent_id);
+		let (command, message_id) = converter.convert().map_err(|err|{
+			log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
+			SendError::Unroutable
+		})?;
+
 		let channel_id: ChannelId = ParaId::from(para_id).into();
 
-		let outbound_message = Message {
-			id: Some(message_id.into()),
-			channel_id,
-			command: Command::AgentExecute { agent_id, command: agent_execute_command },
-		};
+		let outbound_message = Message { id: Some(message_id.into()), channel_id, command };
 
 		// validate the message
 		let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
@@ -154,6 +169,9 @@ enum XcmConverterError {
 	AssetResolutionFailed,
 	InvalidFeeAsset,
 	SetTopicExpected,
+	ReserveAssetDepositedExpected,
+	InvalidAsset,
+	UnexpectedInstruction,
 }
 
 macro_rules! match_expression {
@@ -165,18 +183,33 @@ macro_rules! match_expression {
 	};
 }
 
-struct XcmConverter<'a, Call> {
+struct XcmConverter<'a, ConvertAssetId, Call> {
 	iter: Peekable<Iter<'a, Instruction<Call>>>,
-	ethereum_network: &'a NetworkId,
+	ethereum_network: NetworkId,
+	agent_id: AgentId,
+	_marker: PhantomData<ConvertAssetId>,
 }
-impl<'a, Call> XcmConverter<'a, Call> {
-	fn new(message: &'a Xcm<Call>, ethereum_network: &'a NetworkId) -> Self {
-		Self { iter: message.inner().iter().peekable(), ethereum_network }
+impl<'a, ConvertAssetId, Call> XcmConverter<'a, ConvertAssetId, Call>
+where
+	ConvertAssetId: MaybeEquivalence<TokenId, Location>,
+{
+	fn new(message: &'a Xcm<Call>, ethereum_network: NetworkId, agent_id: AgentId) -> Self {
+		Self {
+			iter: message.inner().iter().peekable(),
+			ethereum_network,
+			agent_id,
+			_marker: Default::default(),
+		}
 	}
 
-	fn convert(&mut self) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
-		// Get withdraw/deposit and make native tokens create message.
-		let result = self.native_tokens_unlock_message()?;
+	fn convert(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
+		let result = match self.peek() {
+			Ok(ReserveAssetDeposited { .. }) => self.send_native_tokens_message(),
+			// Get withdraw/deposit and make native tokens create message.
+			Ok(WithdrawAsset { .. }) => self.send_tokens_message(),
+			Err(e) => Err(e),
+			_ => return Err(XcmConverterError::UnexpectedInstruction),
+		}?;
 
 		// All xcm instructions must be consumed before exit.
 		if self.next().is_ok() {
@@ -186,9 +219,7 @@ impl<'a, Call> XcmConverter<'a, Call> {
 		Ok(result)
 	}
 
-	fn native_tokens_unlock_message(
-		&mut self,
-	) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
+	fn send_tokens_message(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
 		use XcmConverterError::*;
 
 		// Get the reserve assets from WithdrawAsset.
@@ -262,7 +293,13 @@ impl<'a, Call> XcmConverter<'a, Call> {
 		// Check if there is a SetTopic and skip over it if found.
 		let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
 
-		Ok((AgentExecuteCommand::TransferToken { token, recipient, amount }, *topic_id))
+		Ok((
+			Command::AgentExecute {
+				agent_id: self.agent_id,
+				command: AgentExecuteCommand::TransferToken { token, recipient, amount },
+			},
+			*topic_id,
+		))
 	}
 
 	fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
@@ -275,9 +312,95 @@ impl<'a, Call> XcmConverter<'a, Call> {
 
 	fn network_matches(&self, network: &Option<NetworkId>) -> bool {
 		if let Some(network) = network {
-			network == self.ethereum_network
+			*network == self.ethereum_network
 		} else {
 			true
 		}
 	}
+
+	/// Convert the xcm for Polkadot-native token from AH into the Command
+	/// To match transfers of Polkadot-native tokens, we expect an input of the form:
+	/// # ReserveAssetDeposited
+	/// # ClearOrigin
+	/// # BuyExecution
+	/// # DepositAsset
+	/// # SetTopic
+	fn send_native_tokens_message(&mut self) -> Result<(Command, [u8; 32]), XcmConverterError> {
+		use XcmConverterError::*;
+
+		// Get the reserve assets.
+		let reserve_assets =
+			match_expression!(self.next()?, ReserveAssetDeposited(reserve_assets), reserve_assets)
+				.ok_or(ReserveAssetDepositedExpected)?;
+
+		// Check if clear origin exists and skip over it.
+		if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
+			let _ = self.next();
+		}
+
+		// Get the fee asset item from BuyExecution or continue parsing.
+		let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
+		if fee_asset.is_some() {
+			let _ = self.next();
+		}
+
+		let (deposit_assets, beneficiary) = match_expression!(
+			self.next()?,
+			DepositAsset { assets, beneficiary },
+			(assets, beneficiary)
+		)
+		.ok_or(DepositAssetExpected)?;
+
+		// assert that the beneficiary is AccountKey20.
+		let recipient = match_expression!(
+			beneficiary.unpack(),
+			(0, [AccountKey20 { network, key }])
+				if self.network_matches(network),
+			H160(*key)
+		)
+		.ok_or(BeneficiaryResolutionFailed)?;
+
+		// Make sure there are reserved assets.
+		if reserve_assets.len() == 0 {
+			return Err(NoReserveAssets)
+		}
+
+		// Check the the deposit asset filter matches what was reserved.
+		if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
+			return Err(FilterDoesNotConsumeAllAssets)
+		}
+
+		// We only support a single asset at a time.
+		ensure!(reserve_assets.len() == 1, TooManyAssets);
+		let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;
+
+		// If there was a fee specified verify it.
+		if let Some(fee_asset) = fee_asset {
+			// The fee asset must be the same as the reserve asset.
+			if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
+				return Err(InvalidFeeAsset)
+			}
+		}
+
+		let (asset_id, amount) = match reserve_asset {
+			Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
+				Some((inner_location.clone(), *amount)),
+			_ => None,
+		}
+		.ok_or(AssetResolutionFailed)?;
+
+		// transfer amount must be greater than 0.
+		ensure!(amount > 0, ZeroAssetTransfer);
+
+		let token_id = TokenIdOf::convert_location(&asset_id).ok_or(InvalidAsset)?;
+
+		let expected_asset_id = ConvertAssetId::convert(&token_id).ok_or(InvalidAsset)?;
+
+		ensure!(asset_id == expected_asset_id, InvalidAsset);
+
+		// Check if there is a SetTopic and skip over it if found.
+		let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
+
+		Ok((Command::MintForeignToken { token_id, recipient, amount }, *topic_id))
+	}
 }
diff --git a/bridges/snowbridge/primitives/router/src/outbound/tests.rs b/bridges/snowbridge/primitives/router/src/outbound/tests.rs
index 111243bb45a7ee8e9e06438e1d4eaffb16573d7c..6e4fd594634039163806c5dda22598461f17d9c0 100644
--- a/bridges/snowbridge/primitives/router/src/outbound/tests.rs
+++ b/bridges/snowbridge/primitives/router/src/outbound/tests.rs
@@ -4,7 +4,8 @@ use snowbridge_core::{
 	outbound::{Fee, SendError, SendMessageFeeProvider},
 	AgentIdOf,
 };
-use xcm::v3::prelude::SendError as XcmSendError;
+use sp_std::default::Default;
+use xcm::prelude::SendError as XcmSendError;
 
 use super::*;
 
@@ -57,6 +58,16 @@ impl SendMessageFeeProvider for MockErrOutboundQueue {
 	}
 }
 
+pub struct MockTokenIdConvert;
+impl MaybeEquivalence<TokenId, Location> for MockTokenIdConvert {
+	fn convert(_id: &TokenId) -> Option<Location> {
+		Some(Location::new(1, [GlobalConsensus(Westend)]))
+	}
+	fn convert_back(_loc: &Location) -> Option<TokenId> {
+		None
+	}
+}
+
 #[test]
 fn exporter_validate_with_unknown_network_yields_not_applicable() {
 	let network = Ethereum { chain_id: 1337 };
@@ -65,14 +76,14 @@ fn exporter_validate_with_unknown_network_yields_not_applicable() {
 	let mut destination: Option<InteriorLocation> = None;
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::NotApplicable));
 }
 
@@ -84,14 +95,14 @@ fn exporter_validate_with_invalid_destination_yields_missing_argument() {
 	let mut destination: Option<InteriorLocation> = None;
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::MissingArgument));
 }
 
@@ -106,14 +117,14 @@ fn exporter_validate_with_x8_destination_yields_not_applicable() {
 	);
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::NotApplicable));
 }
 
@@ -125,14 +136,14 @@ fn exporter_validate_without_universal_source_yields_missing_argument() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::MissingArgument));
 }
 
@@ -144,14 +155,14 @@ fn exporter_validate_without_global_universal_location_yields_unroutable() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::Unroutable));
 }
 
@@ -163,14 +174,14 @@ fn exporter_validate_without_global_bridge_location_yields_not_applicable() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::NotApplicable));
 }
 
@@ -183,14 +194,14 @@ fn exporter_validate_with_remote_universal_source_yields_not_applicable() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::NotApplicable));
 }
 
@@ -202,14 +213,14 @@ fn exporter_validate_without_para_id_in_source_yields_missing_argument() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::MissingArgument));
 }
 
@@ -222,14 +233,14 @@ fn exporter_validate_complex_para_id_in_source_yields_missing_argument() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::MissingArgument));
 }
 
@@ -242,14 +253,14 @@ fn exporter_validate_without_xcm_message_yields_missing_argument() {
 	let mut destination: Option<InteriorLocation> = Here.into();
 	let mut message: Option<Xcm<()>> = None;
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 	assert_eq!(result, Err(XcmSendError::MissingArgument));
 }
 
@@ -289,14 +300,14 @@ fn exporter_validate_with_max_target_fee_yields_unroutable() {
 		.into(),
 	);
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 
 	assert_eq!(result, Err(XcmSendError::Unroutable));
 }
@@ -316,14 +327,14 @@ fn exporter_validate_with_unparsable_xcm_yields_unroutable() {
 	let mut message: Option<Xcm<()>> =
 		Some(vec![WithdrawAsset(fees), BuyExecution { fees: fee, weight_limit: Unlimited }].into());
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 
 	assert_eq!(result, Err(XcmSendError::Unroutable));
 }
@@ -362,14 +373,14 @@ fn exporter_validate_xcm_success_case_1() {
 		.into(),
 	);
 
-	let result = EthereumBlobExporter::<
-		UniversalLocation,
-		BridgedNetwork,
-		MockOkOutboundQueue,
-		AgentIdOf,
-	>::validate(
-		network, channel, &mut universal_source, &mut destination, &mut message
-	);
+	let result =
+		EthereumBlobExporter::<
+			UniversalLocation,
+			BridgedNetwork,
+			MockOkOutboundQueue,
+			AgentIdOf,
+			MockTokenIdConvert,
+		>::validate(network, channel, &mut universal_source, &mut destination, &mut message);
 
 	assert!(result.is_ok());
 }
@@ -381,6 +392,7 @@ fn exporter_deliver_with_submit_failure_yields_unroutable() {
 		BridgedNetwork,
 		MockErrOutboundQueue,
 		AgentIdOf,
+		MockTokenIdConvert,
 	>::deliver((hex!("deadbeef").to_vec(), XcmHash::default()));
 	assert_eq!(result, Err(XcmSendError::Transport("other transport error")))
 }
@@ -410,11 +422,15 @@ fn xcm_converter_convert_success() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
-	let expected_payload = AgentExecuteCommand::TransferToken {
-		token: token_address.into(),
-		recipient: beneficiary_address.into(),
-		amount: 1000,
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
+	let expected_payload = Command::AgentExecute {
+		agent_id: Default::default(),
+		command: AgentExecuteCommand::TransferToken {
+			token: token_address.into(),
+			recipient: beneficiary_address.into(),
+			amount: 1000,
+		},
 	};
 	let result = converter.convert();
 	assert_eq!(result, Ok((expected_payload, [0; 32])));
@@ -443,11 +459,15 @@ fn xcm_converter_convert_without_buy_execution_yields_success() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
-	let expected_payload = AgentExecuteCommand::TransferToken {
-		token: token_address.into(),
-		recipient: beneficiary_address.into(),
-		amount: 1000,
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
+	let expected_payload = Command::AgentExecute {
+		agent_id: Default::default(),
+		command: AgentExecuteCommand::TransferToken {
+			token: token_address.into(),
+			recipient: beneficiary_address.into(),
+			amount: 1000,
+		},
 	};
 	let result = converter.convert();
 	assert_eq!(result, Ok((expected_payload, [0; 32])));
@@ -478,11 +498,15 @@ fn xcm_converter_convert_with_wildcard_all_asset_filter_succeeds() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
-	let expected_payload = AgentExecuteCommand::TransferToken {
-		token: token_address.into(),
-		recipient: beneficiary_address.into(),
-		amount: 1000,
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
+	let expected_payload = Command::AgentExecute {
+		agent_id: Default::default(),
+		command: AgentExecuteCommand::TransferToken {
+			token: token_address.into(),
+			recipient: beneficiary_address.into(),
+			amount: 1000,
+		},
 	};
 	let result = converter.convert();
 	assert_eq!(result, Ok((expected_payload, [0; 32])));
@@ -513,11 +537,15 @@ fn xcm_converter_convert_with_fees_less_than_reserve_yields_success() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
-	let expected_payload = AgentExecuteCommand::TransferToken {
-		token: token_address.into(),
-		recipient: beneficiary_address.into(),
-		amount: 1000,
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
+	let expected_payload = Command::AgentExecute {
+		agent_id: Default::default(),
+		command: AgentExecuteCommand::TransferToken {
+			token: token_address.into(),
+			recipient: beneficiary_address.into(),
+			amount: 1000,
+		},
 	};
 	let result = converter.convert();
 	assert_eq!(result, Ok((expected_payload, [0; 32])));
@@ -547,7 +575,8 @@ fn xcm_converter_convert_without_set_topic_yields_set_topic_expected() {
 		ClearTopic,
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::SetTopicExpected));
 }
@@ -564,7 +593,8 @@ fn xcm_converter_convert_with_partial_message_yields_unexpected_end_of_xcm() {
 	.into();
 	let message: Xcm<()> = vec![WithdrawAsset(assets)].into();
 
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm));
 }
@@ -595,7 +625,8 @@ fn xcm_converter_with_different_fee_asset_fails() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset));
 }
@@ -625,7 +656,8 @@ fn xcm_converter_with_fees_greater_than_reserve_fails() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::InvalidFeeAsset));
 }
@@ -636,7 +668,8 @@ fn xcm_converter_convert_with_empty_xcm_yields_unexpected_end_of_xcm() {
 
 	let message: Xcm<()> = vec![].into();
 
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::UnexpectedEndOfXcm));
@@ -668,7 +701,8 @@ fn xcm_converter_convert_with_extra_instructions_yields_end_of_xcm_message_expec
 		ClearError,
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::EndOfXcmMessageExpected));
@@ -698,10 +732,11 @@ fn xcm_converter_convert_without_withdraw_asset_yields_withdraw_expected() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
-	assert_eq!(result.err(), Some(XcmConverterError::WithdrawAssetExpected));
+	assert_eq!(result.err(), Some(XcmConverterError::UnexpectedInstruction));
 }
 
 #[test]
@@ -723,7 +758,8 @@ fn xcm_converter_convert_without_withdraw_asset_yields_deposit_expected() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::DepositAssetExpected));
@@ -756,7 +792,8 @@ fn xcm_converter_convert_without_assets_yields_no_reserve_assets() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::NoReserveAssets));
@@ -794,7 +831,8 @@ fn xcm_converter_convert_with_two_assets_yields_too_many_assets() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::TooManyAssets));
@@ -825,7 +863,8 @@ fn xcm_converter_convert_without_consuming_filter_yields_filter_does_not_consume
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::FilterDoesNotConsumeAllAssets));
@@ -856,7 +895,8 @@ fn xcm_converter_convert_with_zero_amount_asset_yields_zero_asset_transfer() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::ZeroAssetTransfer));
@@ -886,7 +926,8 @@ fn xcm_converter_convert_non_ethereum_asset_yields_asset_resolution_failed() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed));
@@ -919,7 +960,8 @@ fn xcm_converter_convert_non_ethereum_chain_asset_yields_asset_resolution_failed
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed));
@@ -952,7 +994,8 @@ fn xcm_converter_convert_non_ethereum_chain_yields_asset_resolution_failed() {
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::AssetResolutionFailed));
@@ -989,7 +1032,8 @@ fn xcm_converter_convert_with_non_ethereum_beneficiary_yields_beneficiary_resolu
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed));
@@ -1025,7 +1069,8 @@ fn xcm_converter_convert_with_non_ethereum_chain_beneficiary_yields_beneficiary_
 		SetTopic([0; 32]),
 	]
 	.into();
-	let mut converter = XcmConverter::new(&message, &network);
+	let mut converter =
+		XcmConverter::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
 
 	let result = converter.convert();
 	assert_eq!(result.err(), Some(XcmConverterError::BeneficiaryResolutionFailed));
@@ -1056,3 +1101,65 @@ fn test_describe_here() {
 		hex!("03170a2e7597b7b7e3d84c05391d139a62b157e78786d8c082f29dcf4c111314").into()
 	)
 }
+
+#[test]
+fn xcm_converter_transfer_native_token_success() {
+	let network = BridgedNetwork::get();
+
+	let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+	let amount = 1000000;
+	let asset_location = Location::new(1, [GlobalConsensus(Westend)]);
+	let token_id = TokenIdOf::convert_location(&asset_location).unwrap();
+
+	let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(amount) }].into();
+	let filter: AssetFilter = assets.clone().into();
+
+	let message: Xcm<()> = vec![
+		ReserveAssetDeposited(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::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
+	let expected_payload =
+		Command::MintForeignToken { recipient: beneficiary_address.into(), amount, token_id };
+	let result = converter.convert();
+	assert_eq!(result, Ok((expected_payload, [0; 32])));
+}
+
+#[test]
+fn xcm_converter_transfer_native_token_with_invalid_location_will_fail() {
+	let network = BridgedNetwork::get();
+
+	let beneficiary_address: [u8; 20] = hex!("2000000000000000000000000000000000000000");
+
+	let amount = 1000000;
+	// Invalid asset location from a different consensus
+	let asset_location = Location { parents: 2, interior: [GlobalConsensus(Rococo)].into() };
+
+	let assets: Assets = vec![Asset { id: AssetId(asset_location), fun: Fungible(amount) }].into();
+	let filter: AssetFilter = assets.clone().into();
+
+	let message: Xcm<()> = vec![
+		ReserveAssetDeposited(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::<MockTokenIdConvert, ()>::new(&message, network, Default::default());
+	let result = converter.convert();
+	assert_eq!(result.err(), Some(XcmConverterError::InvalidAsset));
+}
diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs
index 2876474e094c18ca77300a49457d8168b958def0..a9cfcda0dacdd3afa0cadc19f80e2805e245f172 100644
--- a/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs
+++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/assets/asset-hub-westend/src/genesis.rs
@@ -20,6 +20,7 @@ use sp_core::{sr25519, storage::Storage};
 // Cumulus
 use emulated_integration_tests_common::{
 	accounts, build_genesis_storage, collators, get_account_id_from_seed,
+	PenpalBSiblingSovereignAccount, PenpalBTeleportableAssetLocation,
 	PenpalSiblingSovereignAccount, PenpalTeleportableAssetLocation, RESERVABLE_ASSET_ID,
 	SAFE_XCM_VERSION, USDT_ID,
 };
@@ -81,6 +82,13 @@ pub fn genesis() -> Storage {
 					false,
 					ED,
 				),
+				// PenpalB's teleportable asset representation
+				(
+					PenpalBTeleportableAssetLocation::get(),
+					PenpalBSiblingSovereignAccount::get(),
+					false,
+					ED,
+				),
 			],
 			..Default::default()
 		},
diff --git a/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs b/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs
index feb59c411c8df488c748fe79e3fd485757fd0de0..e7a28ebf4a4690dcb0930e371433e8e35fd6fc8d 100644
--- a/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/chains/parachains/bridges/bridge-hub-westend/src/lib.rs
@@ -46,6 +46,7 @@ decl_test_parachains! {
 		pallets = {
 			PolkadotXcm: bridge_hub_westend_runtime::PolkadotXcm,
 			Balances: bridge_hub_westend_runtime::Balances,
+			EthereumSystem: bridge_hub_westend_runtime::EthereumSystem,
 		}
 	},
 }
diff --git a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs
index 30e66ced1fb08f640d912a1c08c3354090b67db4..c6b8889730e51da2c5ec6891d7005c0dbfe3d31e 100644
--- a/cumulus/parachains/integration-tests/emulated/common/src/lib.rs
+++ b/cumulus/parachains/integration-tests/emulated/common/src/lib.rs
@@ -60,6 +60,7 @@ pub const TELEPORTABLE_ASSET_ID: u32 = 2;
 pub const USDT_ID: u32 = 1984;
 
 pub const PENPAL_ID: u32 = 2000;
+pub const PENPAL_B_ID: u32 = 2001;
 pub const ASSETS_PALLET_ID: u8 = 50;
 
 parameter_types! {
@@ -71,6 +72,14 @@ parameter_types! {
 			]
 		);
 	pub PenpalSiblingSovereignAccount: AccountId = Sibling::from(PENPAL_ID).into_account_truncating();
+	pub PenpalBTeleportableAssetLocation: xcm::v4::Location
+		= xcm::v4::Location::new(1, [
+				xcm::v4::Junction::Parachain(PENPAL_B_ID),
+				xcm::v4::Junction::PalletInstance(ASSETS_PALLET_ID),
+				xcm::v4::Junction::GeneralIndex(TELEPORTABLE_ASSET_ID.into()),
+			]
+		);
+	pub PenpalBSiblingSovereignAccount: AccountId = Sibling::from(PENPAL_B_ID).into_account_truncating();
 }
 
 /// Helper function to generate a crypto pair from seed
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 30cc4de3905eaf9beb1986357d13fec736fd4138..699641d3328fc416ae6ef0c550eb1961084c50e1 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
@@ -18,9 +18,10 @@ use crate::imports::*;
 mod asset_transfers;
 mod claim_assets;
 mod send_xcm;
-mod snowbridge;
 mod teleport;
 
+mod snowbridge;
+
 pub(crate) fn asset_hub_rococo_location() -> Location {
 	Location::new(2, [GlobalConsensus(Rococo), Parachain(AssetHubRococo::para_id().into())])
 }
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 b4db9b365f390c820b83ca069912a5fa09e2a069..4e9dd5a77dd7b14ffb6ce24754ceb957ef14ea56 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
@@ -16,20 +16,24 @@ use crate::imports::*;
 use asset_hub_westend_runtime::xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee;
 use bridge_hub_westend_runtime::EthereumInboundQueue;
 use codec::{Decode, Encode};
+use emulated_integration_tests_common::RESERVABLE_ASSET_ID;
 use frame_support::pallet_prelude::TypeInfo;
 use hex_literal::hex;
-use snowbridge_core::outbound::OperatingMode;
+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, ConvertMessage, Destination, MessageV1, VersionedMessage,
+	Command, Destination, GlobalConsensusEthereumConvertsFor, MessageV1, VersionedMessage,
 };
+use sp_core::H256;
 use testnet_parachains_constants::westend::snowbridge::EthereumNetwork;
+use xcm_executor::traits::ConvertLocation;
 
-const INITIAL_FUND: u128 = 5_000_000_000_000_000_000;
+const INITIAL_FUND: u128 = 5_000_000_000_000;
 pub const CHAIN_ID: u64 = 11155111;
 pub const WETH: [u8; 20] = hex!("87d1f7fdfEe7f651FaBc8bFCB6E086C278b77A7d");
 const ETHEREUM_DESTINATION_ADDRESS: [u8; 20] = hex!("44a57ee2f2FCcb85FDa2B0B18EBD0D8D2333700e");
 const XCM_FEE: u128 = 100_000_000_000;
-const WETH_AMOUNT: u128 = 1_000_000_000;
+const TOKEN_AMOUNT: u128 = 100_000_000_000;
 
 #[derive(Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)]
 pub enum ControlCall {
@@ -55,20 +59,16 @@ fn register_weth_token_from_ethereum_to_asset_hub() {
 	BridgeHubWestend::execute_with(|| {
 		type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
 
-		type Converter = <bridge_hub_westend_runtime::Runtime as snowbridge_pallet_inbound_queue::Config>::MessageConverter;
-
 		let message = VersionedMessage::V1(MessageV1 {
 			chain_id: CHAIN_ID,
 			command: Command::RegisterToken { token: WETH.into(), fee: XCM_FEE },
 		});
-		let (xcm, _) = Converter::convert(message).unwrap();
-		let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubRococo::para_id().into()).unwrap();
+		let (xcm, _) = EthereumInboundQueue::do_convert([0; 32].into(), message).unwrap();
+		let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap();
 
 		assert_expected_events!(
 			BridgeHubWestend,
-			vec![
-				RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},
-			]
+			vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},]
 		);
 	});
 
@@ -77,9 +77,7 @@ fn register_weth_token_from_ethereum_to_asset_hub() {
 
 		assert_expected_events!(
 			AssetHubWestend,
-			vec![
-				RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { .. }) => {},
-			]
+			vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Created { .. }) => {},]
 		);
 	});
 }
@@ -120,26 +118,22 @@ fn send_token_from_ethereum_to_asset_hub() {
 	BridgeHubWestend::execute_with(|| {
 		type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
 
-		type Converter = <bridge_hub_westend_runtime::Runtime as snowbridge_pallet_inbound_queue::Config>::MessageConverter;
-
 		let message = VersionedMessage::V1(MessageV1 {
 			chain_id: CHAIN_ID,
 			command: Command::SendToken {
 				token: WETH.into(),
 				destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
-				amount: WETH_AMOUNT,
+				amount: TOKEN_AMOUNT,
 				fee: XCM_FEE,
 			},
 		});
-		let (xcm, _) = Converter::convert(message).unwrap();
+		let (xcm, _) = EthereumInboundQueue::do_convert([0; 32].into(), message).unwrap();
 		let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap();
 
 		// Check that the message was sent
 		assert_expected_events!(
 			BridgeHubWestend,
-			vec![
-				RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},
-			]
+			vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},]
 		);
 	});
 
@@ -149,9 +143,7 @@ fn send_token_from_ethereum_to_asset_hub() {
 		// 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 { .. }) => {},
-			]
+			vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},]
 		);
 	});
 }
@@ -167,13 +159,6 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 	let weth_asset_location: Location =
 		(Parent, Parent, EthereumNetwork::get(), AccountKey20 { network: None, key: WETH }).into();
 
-	AssetHubWestend::force_default_xcm_version(Some(XCM_VERSION));
-	BridgeHubWestend::force_default_xcm_version(Some(XCM_VERSION));
-	AssetHubWestend::force_xcm_version(
-		Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]),
-		XCM_VERSION,
-	);
-
 	BridgeHubWestend::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]);
 
 	AssetHubWestend::execute_with(|| {
@@ -194,26 +179,23 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 
 	BridgeHubWestend::execute_with(|| {
 		type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
-		type Converter = <bridge_hub_westend_runtime::Runtime as
-	snowbridge_pallet_inbound_queue::Config>::MessageConverter;
 
 		let message = VersionedMessage::V1(MessageV1 {
 			chain_id: CHAIN_ID,
 			command: Command::SendToken {
 				token: WETH.into(),
 				destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
-				amount: WETH_AMOUNT,
+				amount: TOKEN_AMOUNT,
 				fee: XCM_FEE,
 			},
 		});
-		let (xcm, _) = Converter::convert(message).unwrap();
+		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 { .. }) =>{},]
+			vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) =>{},]
 		);
 	});
 
@@ -224,9 +206,7 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 		// Check that AssetHub has issued the foreign asset
 		assert_expected_events!(
 			AssetHubWestend,
-			vec![
-				RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},
-			]
+			vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},]
 		);
 		let assets = vec![Asset {
 			id: AssetId(Location::new(
@@ -236,9 +216,9 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 					AccountKey20 { network: None, key: WETH },
 				],
 			)),
-			fun: Fungible(WETH_AMOUNT),
+			fun: Fungible(TOKEN_AMOUNT),
 		}];
-		let multi_assets = VersionedAssets::V4(Assets::from(assets));
+		let versioned_assets = VersionedAssets::V4(Assets::from(assets));
 
 		let destination = VersionedLocation::V4(Location::new(
 			2,
@@ -259,7 +239,7 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 			RuntimeOrigin::signed(AssetHubWestendReceiver::get()),
 			Box::new(destination),
 			Box::new(beneficiary),
-			Box::new(multi_assets),
+			Box::new(versioned_assets),
 			0,
 			Unlimited,
 		)
@@ -279,10 +259,7 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 		// Outbound Queue
 		assert_expected_events!(
 			BridgeHubWestend,
-			vec![
-
-	RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued
-	{..}) => {},             ]
+			vec![RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued{ .. }) => {},]
 		);
 		let events = BridgeHubWestend::events();
 		// Check that the local fee was credited to the Snowbridge sovereign account
@@ -305,3 +282,316 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
 		);
 	});
 }
+
+#[test]
+fn transfer_relay_token() {
+	let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of(
+		BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id()),
+	);
+	BridgeHubWestend::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]);
+
+	let asset_id: Location = Location { parents: 1, interior: [].into() };
+	let expected_asset_id: Location =
+		Location { parents: 1, interior: [GlobalConsensus(Westend)].into() };
+
+	let expected_token_id = TokenIdOf::convert_location(&expected_asset_id).unwrap();
+
+	let ethereum_sovereign: AccountId =
+		GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&Location::new(
+			2,
+			[GlobalConsensus(EthereumNetwork::get())],
+		))
+		.unwrap()
+		.into();
+
+	// Register token
+	BridgeHubWestend::execute_with(|| {
+		type RuntimeOrigin = <BridgeHubWestend as Chain>::RuntimeOrigin;
+		type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
+
+		assert_ok!(<BridgeHubWestend as BridgeHubWestendPallet>::Balances::force_set_balance(
+			RuntimeOrigin::root(),
+			sp_runtime::MultiAddress::Id(BridgeHubWestendSender::get()),
+			INITIAL_FUND * 10,
+		));
+
+		assert_ok!(<BridgeHubWestend as BridgeHubWestendPallet>::EthereumSystem::register_token(
+			RuntimeOrigin::root(),
+			Box::new(VersionedLocation::V4(asset_id.clone())),
+			AssetMetadata {
+				name: "wnd".as_bytes().to_vec().try_into().unwrap(),
+				symbol: "wnd".as_bytes().to_vec().try_into().unwrap(),
+				decimals: 12,
+			},
+		));
+		// Check that a message was sent to Ethereum to create the agent
+		assert_expected_events!(
+			BridgeHubWestend,
+			vec![RuntimeEvent::EthereumSystem(snowbridge_pallet_system::Event::RegisterToken { .. }) => {},]
+		);
+	});
+
+	// Send token to Ethereum
+	AssetHubWestend::execute_with(|| {
+		type RuntimeOrigin = <AssetHubWestend as Chain>::RuntimeOrigin;
+		type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
+
+		let assets = vec![Asset { id: AssetId(Location::parent()), fun: Fungible(TOKEN_AMOUNT) }];
+		let versioned_assets = VersionedAssets::V4(Assets::from(assets));
+
+		let destination = VersionedLocation::V4(Location::new(
+			2,
+			[GlobalConsensus(Ethereum { chain_id: CHAIN_ID })],
+		));
+
+		let beneficiary = VersionedLocation::V4(Location::new(
+			0,
+			[AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }],
+		));
+
+		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::limited_reserve_transfer_assets(
+			RuntimeOrigin::signed(AssetHubWestendSender::get()),
+			Box::new(destination),
+			Box::new(beneficiary),
+			Box::new(versioned_assets),
+			0,
+			Unlimited,
+		));
+
+		let events = AssetHubWestend::events();
+		// Check that the native asset transferred to some reserved account(sovereign of Ethereum)
+		assert!(
+			events.iter().any(|event| matches!(
+				event,
+				RuntimeEvent::Balances(pallet_balances::Event::Transfer { amount, to, ..})
+					if *amount == TOKEN_AMOUNT && *to == ethereum_sovereign.clone(),
+			)),
+			"native token reserved to Ethereum sovereign account."
+		);
+	});
+
+	// Send token back from ethereum
+	BridgeHubWestend::execute_with(|| {
+		type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
+
+		// Check that the transfer token back to Ethereum message was queue in the Ethereum
+		// Outbound Queue
+		assert_expected_events!(
+			BridgeHubWestend,
+			vec![RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued{ .. }) => {},]
+		);
+
+		// Send relay token back to AH
+		let message_id: H256 = [0; 32].into();
+		let message = VersionedMessage::V1(MessageV1 {
+			chain_id: CHAIN_ID,
+			command: Command::SendNativeToken {
+				token_id: expected_token_id,
+				destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
+				amount: TOKEN_AMOUNT,
+				fee: XCM_FEE,
+			},
+		});
+		// Convert the message to XCM
+		let (xcm, _) = EthereumInboundQueue::do_convert(message_id, message).unwrap();
+		// Send the XCM
+		let _ = EthereumInboundQueue::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 = <AssetHubWestend as Chain>::RuntimeEvent;
+
+		assert_expected_events!(
+			AssetHubWestend,
+			vec![RuntimeEvent::Balances(pallet_balances::Event::Burned{ .. }) => {},]
+		);
+
+		let events = AssetHubWestend::events();
+
+		// Check that the native token burnt from some reserved account
+		assert!(
+			events.iter().any(|event| matches!(
+				event,
+				RuntimeEvent::Balances(pallet_balances::Event::Burned { who, ..})
+					if *who == ethereum_sovereign.clone(),
+			)),
+			"native token burnt from Ethereum sovereign account."
+		);
+
+		// Check that the token was minted to beneficiary
+		assert!(
+			events.iter().any(|event| matches!(
+				event,
+				RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount })
+					if *amount >= TOKEN_AMOUNT && *who == AssetHubWestendReceiver::get()
+			)),
+			"Token minted to beneficiary."
+		);
+	});
+}
+
+#[test]
+fn transfer_ah_token() {
+	let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of(
+		BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id()),
+	);
+	BridgeHubWestend::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]);
+
+	let ethereum_destination = Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]);
+
+	let ethereum_sovereign: AccountId =
+		GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&ethereum_destination)
+			.unwrap()
+			.into();
+	AssetHubWestend::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]);
+
+	let asset_id: Location =
+		[PalletInstance(ASSETS_PALLET_ID), GeneralIndex(RESERVABLE_ASSET_ID.into())].into();
+
+	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(Westend), Parachain(AssetHubWestend::para_id().into())])
+			.appended_with(asset_id.clone().interior)
+			.unwrap();
+
+	let token_id = TokenIdOf::convert_location(&asset_id_after_reanchored).unwrap();
+
+	// Register token
+	BridgeHubWestend::execute_with(|| {
+		type RuntimeOrigin = <BridgeHubWestend as Chain>::RuntimeOrigin;
+
+		assert_ok!(<BridgeHubWestend as BridgeHubWestendPallet>::EthereumSystem::register_token(
+			RuntimeOrigin::root(),
+			Box::new(VersionedLocation::V4(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,
+			},
+		));
+	});
+
+	// Mint some token
+	AssetHubWestend::mint_asset(
+		<AssetHubWestend as Chain>::RuntimeOrigin::signed(AssetHubWestendAssetOwner::get()),
+		RESERVABLE_ASSET_ID,
+		AssetHubWestendSender::get(),
+		TOKEN_AMOUNT,
+	);
+
+	// Send token to Ethereum
+	AssetHubWestend::execute_with(|| {
+		type RuntimeOrigin = <AssetHubWestend as Chain>::RuntimeOrigin;
+		type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;
+
+		// Send partial of the token, will fail if send all
+		let assets =
+			vec![Asset { id: AssetId(asset_id.clone()), fun: Fungible(TOKEN_AMOUNT / 10) }];
+		let versioned_assets = VersionedAssets::V4(Assets::from(assets));
+
+		let beneficiary = VersionedLocation::V4(Location::new(
+			0,
+			[AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }],
+		));
+
+		assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::limited_reserve_transfer_assets(
+			RuntimeOrigin::signed(AssetHubWestendSender::get()),
+			Box::new(VersionedLocation::from(ethereum_destination)),
+			Box::new(beneficiary),
+			Box::new(versioned_assets),
+			0,
+			Unlimited,
+		));
+
+		assert_expected_events!(
+			AssetHubWestend,
+			vec![RuntimeEvent::Assets(pallet_assets::Event::Transferred{ .. }) => {},]
+		);
+
+		let events = AssetHubWestend::events();
+		// Check that the native asset transferred to some reserved account(sovereign of Ethereum)
+		assert!(
+			events.iter().any(|event| matches!(
+				event,
+				RuntimeEvent::Assets(pallet_assets::Event::Transferred { asset_id, to, ..})
+					if *asset_id == RESERVABLE_ASSET_ID && *to == ethereum_sovereign.clone()
+			)),
+			"native token reserved to Ethereum sovereign account."
+		);
+	});
+
+	// Send token back from Ethereum
+	BridgeHubWestend::execute_with(|| {
+		type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
+
+		// Check that the transfer token back to Ethereum message was queue in the Ethereum
+		// Outbound Queue
+		assert_expected_events!(
+			BridgeHubWestend,
+			vec![RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued{ .. }) => {},]
+		);
+
+		let message = VersionedMessage::V1(MessageV1 {
+			chain_id: CHAIN_ID,
+			command: Command::SendNativeToken {
+				token_id,
+				destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
+				amount: TOKEN_AMOUNT / 10,
+				fee: XCM_FEE,
+			},
+		});
+		// Convert the message to XCM
+		let (xcm, _) = EthereumInboundQueue::do_convert([0; 32].into(), message).unwrap();
+		// Send the XCM
+		let _ = EthereumInboundQueue::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 = <AssetHubWestend as Chain>::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."
+		);
+	});
+}
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 6c0486c62fa658e6020616a566be1d4ad5b87f1d..be7005b5379a1fa1732cc5b4ae18842c8a9d229a 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
@@ -29,9 +29,10 @@ use sp_core::H160;
 use testnet_parachains_constants::rococo::{
 	currency::*,
 	fee::WeightToFee,
-	snowbridge::{EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX},
+	snowbridge::{EthereumLocation, EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX},
 };
 
+use crate::xcm_config::RelayNetwork;
 #[cfg(feature = "runtime-benchmarks")]
 use benchmark_helpers::DoNothingRouter;
 use frame_support::{parameter_types, weights::ConstantMultiplier};
@@ -40,8 +41,7 @@ use sp_runtime::{
 	traits::{ConstU32, ConstU8, Keccak256},
 	FixedU128,
 };
-
-pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32;
+use xcm::prelude::{GlobalConsensus, InteriorLocation, Location, Parachain};
 
 /// Exports message to the Ethereum Gateway contract.
 pub type SnowbridgeExporter = EthereumBlobExporter<
@@ -49,6 +49,7 @@ pub type SnowbridgeExporter = EthereumBlobExporter<
 	EthereumNetwork,
 	snowbridge_pallet_outbound_queue::Pallet<Runtime>,
 	snowbridge_core::AgentIdOf,
+	EthereumSystem,
 >;
 
 // Ethereum Bridge
@@ -65,6 +66,8 @@ parameter_types! {
 		rewards: Rewards { local: 1 * UNITS, remote: meth(1) },
 		multiplier: FixedU128::from_rational(1, 1),
 	};
+	pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(RelayNetwork::get()),Parachain(rococo_runtime_constants::system_parachain::ASSET_HUB_ID)]);
+	pub EthereumUniversalLocation: InteriorLocation = [GlobalConsensus(EthereumNetwork::get())].into();
 }
 
 impl snowbridge_pallet_inbound_queue::Config for Runtime {
@@ -85,6 +88,9 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime {
 		ConstU8<INBOUND_QUEUE_PALLET_INDEX>,
 		AccountId,
 		Balance,
+		EthereumSystem,
+		EthereumUniversalLocation,
+		AssetHubFromEthereum,
 	>;
 	type WeightToFee = WeightToFee;
 	type LengthToFee = ConstantMultiplier<Balance, TransactionByteFee>;
@@ -161,6 +167,8 @@ parameter_types! {
 	};
 }
 
+pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32;
+
 impl snowbridge_pallet_ethereum_client::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type ForkVersions = ChainForkVersions;
@@ -181,6 +189,8 @@ impl snowbridge_pallet_system::Config for Runtime {
 	type Helper = ();
 	type DefaultPricingParameters = Parameters;
 	type InboundDeliveryCost = EthereumInboundQueue;
+	type UniversalLocation = UniversalLocation;
+	type EthereumLocation = EthereumLocation;
 }
 
 #[cfg(feature = "runtime-benchmarks")]
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs
index c6c188e323af84d11ba396cb9ab4e97983bac33c..3831111f0977dd33b784a5ba9f4bf2686528f292 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-rococo/src/weights/snowbridge_pallet_system.rs
@@ -253,4 +253,14 @@ impl<T: frame_system::Config> snowbridge_pallet_system::WeightInfo for WeightInf
 			.saturating_add(T::DbWeight::get().reads(4_u64))
 			.saturating_add(T::DbWeight::get().writes(3_u64))
 	}
+
+	fn register_token() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `256`
+		//  Estimated: `6044`
+		// Minimum execution time: 45_000_000 picoseconds.
+		Weight::from_parts(45_000_000, 6044)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(3_u64))
+	}
 }
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 47b6006ed6c1172d2b1bbbb67d73beeb97be65c0..dbca4166a13513b23e09d01798cec73aceb1097f 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
@@ -30,9 +30,10 @@ use sp_core::H160;
 use testnet_parachains_constants::westend::{
 	currency::*,
 	fee::WeightToFee,
-	snowbridge::{EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX},
+	snowbridge::{EthereumLocation, EthereumNetwork, INBOUND_QUEUE_PALLET_INDEX},
 };
 
+use crate::xcm_config::RelayNetwork;
 #[cfg(feature = "runtime-benchmarks")]
 use benchmark_helpers::DoNothingRouter;
 use frame_support::{parameter_types, weights::ConstantMultiplier};
@@ -41,6 +42,7 @@ use sp_runtime::{
 	traits::{ConstU32, ConstU8, Keccak256},
 	FixedU128,
 };
+use xcm::prelude::{GlobalConsensus, InteriorLocation, Location, Parachain};
 
 pub const SLOTS_PER_EPOCH: u32 = snowbridge_pallet_ethereum_client::config::SLOTS_PER_EPOCH as u32;
 
@@ -50,6 +52,7 @@ pub type SnowbridgeExporter = EthereumBlobExporter<
 	EthereumNetwork,
 	snowbridge_pallet_outbound_queue::Pallet<Runtime>,
 	snowbridge_core::AgentIdOf,
+	EthereumSystem,
 >;
 
 // Ethereum Bridge
@@ -66,8 +69,9 @@ parameter_types! {
 		rewards: Rewards { local: 1 * UNITS, remote: meth(1) },
 		multiplier: FixedU128::from_rational(1, 1),
 	};
+	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();
 }
-
 impl snowbridge_pallet_inbound_queue::Config for Runtime {
 	type RuntimeEvent = RuntimeEvent;
 	type Verifier = snowbridge_pallet_ethereum_client::Pallet<Runtime>;
@@ -86,6 +90,9 @@ impl snowbridge_pallet_inbound_queue::Config for Runtime {
 		ConstU8<INBOUND_QUEUE_PALLET_INDEX>,
 		AccountId,
 		Balance,
+		EthereumSystem,
+		EthereumUniversalLocation,
+		AssetHubFromEthereum,
 	>;
 	type WeightToFee = WeightToFee;
 	type LengthToFee = ConstantMultiplier<Balance, TransactionByteFee>;
@@ -181,6 +188,8 @@ impl snowbridge_pallet_system::Config for Runtime {
 	type Helper = ();
 	type DefaultPricingParameters = Parameters;
 	type InboundDeliveryCost = EthereumInboundQueue;
+	type UniversalLocation = UniversalLocation;
+	type EthereumLocation = EthereumLocation;
 }
 
 #[cfg(feature = "runtime-benchmarks")]
diff --git a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs
index c6c188e323af84d11ba396cb9ab4e97983bac33c..3831111f0977dd33b784a5ba9f4bf2686528f292 100644
--- a/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs
+++ b/cumulus/parachains/runtimes/bridge-hubs/bridge-hub-westend/src/weights/snowbridge_pallet_system.rs
@@ -253,4 +253,14 @@ impl<T: frame_system::Config> snowbridge_pallet_system::WeightInfo for WeightInf
 			.saturating_add(T::DbWeight::get().reads(4_u64))
 			.saturating_add(T::DbWeight::get().writes(3_u64))
 	}
+
+	fn register_token() -> Weight {
+		// Proof Size summary in bytes:
+		//  Measured:  `256`
+		//  Estimated: `6044`
+		// Minimum execution time: 45_000_000 picoseconds.
+		Weight::from_parts(45_000_000, 6044)
+			.saturating_add(T::DbWeight::get().reads(5_u64))
+			.saturating_add(T::DbWeight::get().writes(3_u64))
+	}
 }
diff --git a/cumulus/parachains/runtimes/constants/src/rococo.rs b/cumulus/parachains/runtimes/constants/src/rococo.rs
index 56f4868371c117ed49312fda3d7653e6fd060c0f..be4b5c9711ccb0f176f58af771a5226be777456c 100644
--- a/cumulus/parachains/runtimes/constants/src/rococo.rs
+++ b/cumulus/parachains/runtimes/constants/src/rococo.rs
@@ -148,7 +148,7 @@ pub mod time {
 
 pub mod snowbridge {
 	use frame_support::parameter_types;
-	use xcm::opaque::lts::NetworkId;
+	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;
@@ -159,6 +159,7 @@ pub mod snowbridge {
 		/// <https://chainlist.org/chain/11155111>
 		/// <https://ethereum.org/en/developers/docs/apis/json-rpc/#net_version>
 		pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
+		pub EthereumLocation: Location = Location::new(2, EthereumNetwork::get());
 	}
 }
 
diff --git a/cumulus/parachains/runtimes/constants/src/westend.rs b/cumulus/parachains/runtimes/constants/src/westend.rs
index fec66cec2eb6a8df284a50b95c6b71c6a0d81c17..47ba8f7e97ae38762865680399aedebc565afc35 100644
--- a/cumulus/parachains/runtimes/constants/src/westend.rs
+++ b/cumulus/parachains/runtimes/constants/src/westend.rs
@@ -171,7 +171,7 @@ pub mod time {
 
 pub mod snowbridge {
 	use frame_support::parameter_types;
-	use xcm::opaque::lts::NetworkId;
+	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;
@@ -182,5 +182,6 @@ pub mod snowbridge {
 		/// <https://chainlist.org/chain/11155111>
 		/// <https://ethereum.org/en/developers/docs/apis/json-rpc/#net_version>
 		pub EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 11155111 };
+		pub EthereumLocation: Location = Location::new(2, EthereumNetwork::get());
 	}
 }
diff --git a/prdoc/pr_5546.prdoc b/prdoc/pr_5546.prdoc
new file mode 100644
index 0000000000000000000000000000000000000000..95f02dbe13b25c5e66149a0b7bbe99bdea7f4302
--- /dev/null
+++ b/prdoc/pr_5546.prdoc
@@ -0,0 +1,36 @@
+title: "Transfer polkadot native assets to Ethereum through snowbridge"
+
+doc:
+  - audience: Runtime Dev
+    description: |
+      Transfer polkadot native asset to Ethereum through snowbridge.
+
+crates:
+  - name: snowbridge-pallet-inbound-queue
+    bump: patch
+  - name: snowbridge-pallet-inbound-queue
+    bump: patch
+  - name: snowbridge-pallet-outbound-queue
+    bump: patch
+  - name: snowbridge-pallet-system
+    bump: minor
+    validate: false
+  - name: snowbridge-core
+    bump: minor
+    validate: false
+  - name: snowbridge-router-primitives
+    bump: minor
+    validate: false
+  - name: bridge-hub-westend-runtime
+    bump: patch
+  - name: bridge-hub-rococo-runtime
+    bump: patch
+  - name: bridge-hub-westend-emulated-chain
+    bump: patch
+  - name: bridge-hub-westend-integration-tests
+    bump: minor
+  - name: asset-hub-westend-emulated-chain
+    bump: patch
+  - name: emulated-integration-tests-common
+    bump: patch
+