diff --git a/Cargo.lock b/Cargo.lock index b1b0783b2..62d42714d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8591,6 +8591,21 @@ dependencies = [ "staging-xcm-executor", ] +[[package]] +name = "pallet-xcm-teleport" +version = "1.6.0-d" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "parity-scale-codec", + "scale-info", + "sp-runtime 24.0.0 (git+https://github.com/pendulum-chain/polkadot-sdk?rev=22dd6dee5148a0879306337bd8619c16224cc07b)", + "sp-std 8.0.0 (git+https://github.com/pendulum-chain/polkadot-sdk?rev=22dd6dee5148a0879306337bd8619c16224cc07b)", + "staging-xcm", +] + [[package]] name = "parachain-staking" version = "1.6.0-d" @@ -8974,6 +8989,7 @@ dependencies = [ "pallet-utility", "pallet-vesting", "pallet-xcm", + "pallet-xcm-teleport", "parachain-staking", "parachains-common", "parity-scale-codec", @@ -11296,6 +11312,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "orml-asset-registry", "orml-traits", "orml-xcm-support", diff --git a/Cargo.toml b/Cargo.toml index f46c848fe..3557fcf41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "pallets/orml-currencies-allowance-extension", "pallets/orml-tokens-management-extension", "pallets/treasury-buyout-extension", + "pallets/xcm-teleport", "runtime/common", "runtime/amplitude", "runtime/foucoco", diff --git a/pallets/xcm-teleport/Cargo.toml b/pallets/xcm-teleport/Cargo.toml new file mode 100644 index 000000000..ddcddefec --- /dev/null +++ b/pallets/xcm-teleport/Cargo.toml @@ -0,0 +1,47 @@ +[package] +authors = ["Pendulum"] +description = "A pallet to teleport native PEN to AssetHub with correct XCM message ordering" +edition = "2021" +name = "pallet-xcm-teleport" +version = "1.6.0-d" + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true, features = ["derive"] } +scale-info = { workspace = true, features = ["derive"] } + +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +xcm = { workspace = true } + +# benchmarking +frame-benchmarking = { workspace = true, optional = true } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +std = [ + "frame-support/std", + "frame-system/std", + "log/std", + "parity-scale-codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "xcm/std", + "frame-benchmarking?/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/pallets/xcm-teleport/src/lib.rs b/pallets/xcm-teleport/src/lib.rs new file mode 100644 index 000000000..28f12cec0 --- /dev/null +++ b/pallets/xcm-teleport/src/lib.rs @@ -0,0 +1,286 @@ +//! # XCM Teleport Pallet +//! +//! A pallet that enables teleporting the native PEN token from Pendulum to AssetHub +//! with the correct XCM message ordering that passes AssetHub's barrier. +//! +//! ## Problem +//! +//! The standard `pallet_xcm::limitedTeleportAssets` uses `InitiateTeleport` which always +//! prepends `ReceiveTeleportedAsset` to the inner XCM. This produces a message ordering +//! that AssetHub's barrier rejects when DOT is needed for fees (PEN is not fee-payable on +//! AssetHub). +//! +//! ## Solution +//! +//! This pallet constructs the remote XCM message manually with the correct ordering: +//! ```text +//! WithdrawAsset(DOT) ← from Pendulum's sovereign account on AssetHub +//! BuyExecution(DOT) ← passes the barrier +//! ReceiveTeleportedAsset(PEN) ← mints PEN on AssetHub +//! ClearOrigin +//! DepositAsset(PEN, beneficiary) ← only PEN goes to the user +//! DepositAsset(remaining, sovereign_acct) ← leftover DOT returns to sovereign +//! ``` +//! +//! Locally, PEN is withdrawn from the sender's account and burned (removed from circulation). +//! The message is sent via `XcmRouter` from the **parachain origin** (no `DescendOrigin`), +//! so `WithdrawAsset(DOT)` correctly accesses the Pendulum sovereign account on AssetHub. +//! +//! ## Fee Protection +//! +//! Two layers of protection prevent users from draining the sovereign DOT balance: +//! +//! 1. **Max fee cap** (`MaxFeeAmount`): The `fee_amount` parameter is capped at a +//! configurable maximum. Any value above this is rejected. +//! +//! 2. **Split deposits**: PEN is deposited to the beneficiary, but leftover DOT (not +//! consumed by `BuyExecution`) is returned to the sovereign account — not the user. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +#[frame_support::pallet] +pub mod pallet { + use frame_support::{ + pallet_prelude::*, + traits::{Currency, ExistenceRequirement, WithdrawReasons}, + }; + use frame_system::pallet_prelude::*; + use xcm::v3::{ + prelude::*, Instruction, Junction, Junctions, MultiAsset, MultiAssetFilter, MultiAssets, + MultiLocation, SendXcm, WeightLimit, WildFungibility, WildMultiAsset, Xcm, + }; + + type BalanceOf = + <::Currency as Currency<::AccountId>>::Balance; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching runtime event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + /// The native currency (PEN / Balances pallet). + type Currency: Currency; + + /// The XCM router used to send messages to other chains. + type XcmRouter: SendXcm; + + /// The MultiLocation of the destination chain (AssetHub) relative to this chain. + /// For Pendulum → AssetHub: `(Parent, Parachain(1000))`. + #[pallet::constant] + type DestinationLocation: Get; + + /// The MultiLocation of the native token as seen from the destination chain. + /// For PEN on AssetHub: `(parents: 1, X2(Parachain(2094), PalletInstance(10)))`. + #[pallet::constant] + type NativeAssetOnDest: Get; + + /// The MultiLocation of the fee asset (DOT) as seen from the destination chain. + /// For DOT on AssetHub: `(parents: 1, Here)`. + #[pallet::constant] + type FeeAssetOnDest: Get; + + /// The MultiLocation of this chain's sovereign account on the destination, + /// used to return leftover fee assets after execution. + /// For Pendulum on AssetHub: `(parents: 0, X1(AccountId32 { network: None, id: sovereign_bytes }))`. + #[pallet::constant] + type SovereignAccountOnDest: Get; + + /// Maximum fee amount (in fee asset's smallest unit) that can be specified. + /// This prevents users from draining the sovereign account's fee asset balance. + #[pallet::constant] + type MaxFeeAmount: Get; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Native tokens were teleported to AssetHub. + NativeTeleportedToAssetHub { + /// The account that initiated the teleport. + sender: T::AccountId, + /// The beneficiary account on AssetHub. + beneficiary: T::AccountId, + /// The amount of native token teleported. + amount: BalanceOf, + /// The amount of DOT used for execution fees on AssetHub. + fee_amount: u128, + }, + } + + #[pallet::error] + pub enum Error { + /// Failed to send the XCM message to the destination chain. + XcmSendFailed, + /// The teleport amount must be greater than zero. + ZeroAmount, + /// The fee amount must be greater than zero. + ZeroFeeAmount, + /// The fee amount exceeds the maximum allowed. + FeeAmountTooHigh, + /// Failed to convert the amount to u128. + AmountConversionFailed, + } + + #[pallet::call] + impl Pallet + where + T::AccountId: Into<[u8; 32]>, + { + /// Teleport native tokens to AssetHub. + /// + /// This extrinsic: + /// 1. Burns `amount` of native tokens from the sender's account on this chain. + /// 2. Sends an XCM message to AssetHub that: + /// - Withdraws `fee_amount` DOT from this chain's sovereign account for fees. + /// - Mints `amount` native tokens on AssetHub via `ReceiveTeleportedAsset`. + /// - Deposits only the native tokens to the `beneficiary`. + /// - Returns any leftover DOT to the sovereign account. + /// + /// # Parameters + /// - `origin`: Must be a signed origin (the sender). + /// - `amount`: The amount of native tokens to teleport. + /// - `fee_amount`: The amount of DOT (in Plancks) to use for execution fees + /// on AssetHub. Must not exceed `MaxFeeAmount`. This DOT is withdrawn + /// from this chain's sovereign account on AssetHub. + /// - `beneficiary`: The destination AccountId32 on AssetHub. + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(200_000_000, 10_000))] + pub fn teleport_native_to_asset_hub( + origin: OriginFor, + amount: BalanceOf, + fee_amount: u128, + beneficiary: T::AccountId, + ) -> DispatchResult { + let sender = ensure_signed(origin)?; + + // Validate inputs + ensure!(amount > BalanceOf::::from(0u32), Error::::ZeroAmount); + ensure!(fee_amount > 0, Error::::ZeroFeeAmount); + ensure!( + fee_amount <= T::MaxFeeAmount::get(), + Error::::FeeAmountTooHigh + ); + + // Convert balance to u128 for XCM + let amount_u128: u128 = amount + .try_into() + .map_err(|_| Error::::AmountConversionFailed)?; + + // 1. Withdraw native tokens from the sender's account. + // We keep the imbalance and only burn it after successful XCM delivery. + // If validation or delivery fails locally, we refund the tokens back to the sender. + // Note: If the message is delivered but fails during execution on AssetHub, + // the tokens are still burned (remote execution failures cannot be detected here). + let imbalance = T::Currency::withdraw( + &sender, + amount, + WithdrawReasons::TRANSFER, + ExistenceRequirement::AllowDeath, + )?; + + // 2. Construct the remote XCM message for AssetHub. + let fee_asset_location = T::FeeAssetOnDest::get(); + let native_asset_on_dest = T::NativeAssetOnDest::get(); + let sovereign_on_dest = T::SovereignAccountOnDest::get(); + + let beneficiary_bytes: [u8; 32] = beneficiary.clone().into(); + let beneficiary_location = MultiLocation { + parents: 0, + interior: Junctions::X1(Junction::AccountId32 { + network: None, + id: beneficiary_bytes, + }), + }; + + let fee_multi_asset = MultiAsset { + id: AssetId::Concrete(fee_asset_location), + fun: Fungibility::Fungible(fee_amount), + }; + + let native_multi_asset = MultiAsset { + id: AssetId::Concrete(native_asset_on_dest.clone()), + fun: Fungibility::Fungible(amount_u128), + }; + + let message: Xcm<()> = Xcm(vec![ + // Withdraw fee asset (DOT) from this chain's sovereign account + Instruction::WithdrawAsset(MultiAssets::from(vec![fee_multi_asset.clone()])), + // Pay for execution with the fee asset — this passes the barrier + Instruction::BuyExecution { + fees: fee_multi_asset, + weight_limit: WeightLimit::Unlimited, + }, + // Mint the teleported native tokens on the destination + Instruction::ReceiveTeleportedAsset(MultiAssets::from(vec![native_multi_asset])), + // Remove origin to prevent further privileged operations + Instruction::ClearOrigin, + // Deposit ONLY the native token (PEN) to the beneficiary + Instruction::DepositAsset { + assets: MultiAssetFilter::Wild(WildMultiAsset::AllOf { + id: AssetId::Concrete(native_asset_on_dest), + fun: WildFungibility::Fungible, + }), + beneficiary: beneficiary_location, + }, + // Return any leftover fee asset (DOT) to the sovereign account + Instruction::DepositAsset { + assets: MultiAssetFilter::Wild(WildMultiAsset::All), + beneficiary: sovereign_on_dest, + }, + ]); + + // 3. Send the message to AssetHub via the XCM router. + // Since we call the router directly (not through pallet_xcm::send), + // no DescendOrigin is prepended. The message arrives from the + // parachain origin, so WithdrawAsset accesses the sovereign account. + let asset_hub = T::DestinationLocation::get(); + + log::info!( + target: "xcm-teleport", + "Teleporting native to AssetHub ({:?}): amount={}, fee_amount={}", + asset_hub, amount_u128, fee_amount, + ); + + let (ticket, _price) = match T::XcmRouter::validate(&mut Some(asset_hub), &mut Some(message)) { + Ok(result) => result, + Err(e) => { + log::error!( + target: "xcm-teleport", + "Failed to validate XCM message: {:?}", e + ); + // Refund the withdrawn tokens back to the sender + T::Currency::resolve_creating(&sender, imbalance); + return Err(Error::::XcmSendFailed.into()); + }, + }; + + if let Err(e) = T::XcmRouter::deliver(ticket) { + log::error!( + target: "xcm-teleport", + "Failed to deliver XCM message: {:?}", e + ); + // Refund the withdrawn tokens back to the sender + T::Currency::resolve_creating(&sender, imbalance); + return Err(Error::::XcmSendFailed.into()); + } + + // Drop the imbalance to burn the tokens (successful teleport) + drop(imbalance); + + // 4. Emit event + Self::deposit_event(Event::NativeTeleportedToAssetHub { + sender, + beneficiary, + amount, + fee_amount, + }); + + Ok(()) + } + } +} diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 175d285a5..a17730a4e 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -11,6 +11,7 @@ edition = "2021" targets = ["x86_64-unknown-linux-gnu"] [dependencies] +log = { workspace = true } paste.workspace = true parity-scale-codec = { workspace = true, features = ["derive"] } scale-info = { workspace = true, features = ["derive"] } @@ -45,6 +46,7 @@ default = [ ] std = [ + "log/std", "parity-scale-codec/std", "scale-info/std", "frame-benchmarking?/std", diff --git a/runtime/common/src/custom_transactor.rs b/runtime/common/src/custom_transactor.rs index efa7aa2eb..7f7b26c97 100644 --- a/runtime/common/src/custom_transactor.rs +++ b/runtime/common/src/custom_transactor.rs @@ -1,3 +1,4 @@ +use frame_support::traits::Contains; use sp_std::{marker::PhantomData, result}; use staging_xcm_executor::{traits::TransactAsset, Assets}; @@ -14,12 +15,25 @@ pub trait AutomationPalletConfig { fn callback(length: u8, data: [u8; 32], amount: u128) -> Result; } -pub struct CustomTransactorInterceptor( - PhantomData<(WrappedTransactor, AutomationPalletConfigT)>, -); +/// A wrapper around an inner `TransactAsset` that: +/// 1. Intercepts `deposit_asset` to optionally route to an automation pallet callback. +/// 2. Validates teleport destinations in `can_check_out` against `AllowedTeleportDest`. +/// +/// `AllowedTeleportDest` is a `Contains` filter that determines which +/// destinations are valid for teleporting assets out of this chain. If a destination +/// is not in the allowed set, `can_check_out` returns an error. +pub struct CustomTransactorInterceptor< + WrappedTransactor, + AutomationPalletConfigT, + AllowedTeleportDest, +>(PhantomData<(WrappedTransactor, AutomationPalletConfigT, AllowedTeleportDest)>); -impl - TransactAsset for CustomTransactorInterceptor +impl< + WrappedTransactor: TransactAsset, + AutomationPalletConfigT: AutomationPalletConfig, + AllowedTeleportDest: Contains, + > TransactAsset + for CustomTransactorInterceptor { fn deposit_asset( asset: &MultiAsset, @@ -57,4 +71,53 @@ impl result::Result { WrappedTransactor::transfer_asset(asset, from, to, _context) } + + fn can_check_out( + dest: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) -> Result { + // Only allow teleporting assets to destinations in the AllowedTeleportDest set. + // This prevents users from burning tokens by teleporting to chains that don't + // recognize this asset as teleportable. + if !AllowedTeleportDest::contains(dest) { + log::warn!( + target: "xcm::custom_transactor", + "Teleport check-out rejected: destination {:?} is not in the allowed set", + dest, + ); + return Err(XcmError::Unroutable); + } + Ok(()) + } + + fn check_out( + _dest: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) { + // No-op: the asset was already withdrawn from the sender's account via + // WithdrawAsset which correctly reduces total issuance via ORML's + // MultiCurrencyAdapter. No additional accounting needed here. + } + + fn can_check_in( + _origin: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) -> Result { + // Allow teleport check-in (receiving teleported assets). + // The origin is already validated by IsTeleporter (TrustedTeleporters) + // before this method is called. + Ok(()) + } + + fn check_in( + _origin: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) { + // No-op: the asset will be minted/deposited via deposit_asset which + // correctly increases total issuance. + } } diff --git a/runtime/pendulum/Cargo.toml b/runtime/pendulum/Cargo.toml index d783f9dac..4657b3d8e 100644 --- a/runtime/pendulum/Cargo.toml +++ b/runtime/pendulum/Cargo.toml @@ -120,6 +120,7 @@ dia-oracle-runtime-api = { workspace = true } # Pendulum Pallets vesting-manager = { path = "../../pallets/vesting-manager", default-features = false } +pallet-xcm-teleport = { path = "../../pallets/xcm-teleport", default-features = false } # Polkadot pallet-xcm = { workspace = true } @@ -255,6 +256,7 @@ std = [ "orml-currencies-allowance-extension/std", "parachain-staking/std", "vesting-manager/std", + "pallet-xcm-teleport/std", "price-chain-extension/std", "token-chain-extension/std", "treasury-buyout-extension/std", @@ -330,6 +332,7 @@ runtime-benchmarks = [ "staging-xcm-executor/runtime-benchmarks", "staking/runtime-benchmarks", "vesting-manager/runtime-benchmarks", + "pallet-xcm-teleport/runtime-benchmarks", ] try-runtime = [ @@ -386,6 +389,7 @@ try-runtime = [ "dia-oracle/try-runtime", "orml-currencies-allowance-extension/try-runtime", "vesting-manager/try-runtime", + "pallet-xcm-teleport/try-runtime", "bifrost-farming/try-runtime", "zenlink-protocol/try-runtime", "treasury-buyout-extension/try-runtime", diff --git a/runtime/pendulum/src/lib.rs b/runtime/pendulum/src/lib.rs index c7de018d8..237a6ccfe 100644 --- a/runtime/pendulum/src/lib.rs +++ b/runtime/pendulum/src/lib.rs @@ -371,6 +371,7 @@ impl Contains for BaseFilter { | RuntimeCall::ParachainInfo(_) | RuntimeCall::CumulusXcm(_) | RuntimeCall::VaultStaking(_) + | RuntimeCall::XcmTeleport(_) | RuntimeCall::MessageQueue(_) => true, // All pallets are allowed, but exhaustive match is defensive // in the case of adding new pallets. } @@ -1010,6 +1011,17 @@ impl vesting_manager::Config for Runtime { type VestingSchedule = Vesting; } +impl pallet_xcm_teleport::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type XcmRouter = xcm_config::XcmRouter; + type DestinationLocation = xcm_config::AssetHubLocation; + type NativeAssetOnDest = xcm_config::NativeAssetOnAssetHub; + type FeeAssetOnDest = xcm_config::DotOnAssetHub; + type SovereignAccountOnDest = xcm_config::SovereignAccountOnAssetHub; + type MaxFeeAmount = xcm_config::MaxDotFeeAmount; +} + const fn deposit(items: u32, bytes: u32) -> Balance { (items as Balance * UNIT + (bytes as Balance) * (5 * MILLIUNIT / 100)) / 10 } @@ -1584,6 +1596,8 @@ construct_runtime!( VestingManager: vesting_manager = 100, + XcmTeleport: pallet_xcm_teleport = 101, + MessageQueue: pallet_message_queue = 110, } ); diff --git a/runtime/pendulum/src/xcm_config.rs b/runtime/pendulum/src/xcm_config.rs index c79351700..72133db18 100644 --- a/runtime/pendulum/src/xcm_config.rs +++ b/runtime/pendulum/src/xcm_config.rs @@ -1,6 +1,7 @@ use core::marker::PhantomData; use cumulus_primitives_utility::XcmFeesTo32ByteAccount; +use frame_support::traits::{Contains, PalletInfoAccess}; use frame_support::{ match_types, parameter_types, traits::{ContainsPair, Everything, Nothing, ProcessMessageError}, @@ -14,7 +15,8 @@ use orml_traits::{ use orml_xcm_support::{DepositToAlternative, IsNativeConcrete, MultiCurrencyAdapter}; use pallet_xcm::XcmPassthrough; use polkadot_parachain::primitives::Sibling; -use sp_runtime::traits::Convert; +use sp_runtime::traits::{AccountIdConversion, Convert}; +use sp_std::vec::Vec; use staging_xcm_builder::{ AccountId32Aliases, AllowKnownQueryResponses, AllowSubscriptionsFrom, @@ -52,6 +54,43 @@ parameter_types! { pub CheckingAccount: AccountId = PolkadotXcm::check_account(); pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get()), Parachain(ParachainInfo::parachain_id().into())); + + /// Asset Hub + pub AssetHubLocation: MultiLocation = (Parent, Parachain(1000)).into(); + + // PEN (native) — local location + pub NativeTokenLocation: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1( + PalletInstance(::index() as u8) + ) + }; + + /// PEN location as seen from AssetHub (used for ReceiveTeleportedAsset on the remote side). + /// (parents: 1, X2(Parachain(self), PalletInstance(Balances_index))) + pub NativeAssetOnAssetHub: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::X2( + Parachain(ParachainInfo::parachain_id().into()), + PalletInstance(::index() as u8), + ) + }; + + /// DOT location as seen from AssetHub (the relay chain token). + pub const DotOnAssetHub: MultiLocation = MultiLocation { parents: 1, interior: Junctions::Here }; + + /// Pendulum's sovereign account on AssetHub, used for returning leftover DOT fees. + /// Computed from Sibling(para_id) using the standard AccountIdConversion. + pub SovereignAccountOnAssetHub: MultiLocation = { + let sovereign: AccountId = Sibling::from(ParachainInfo::parachain_id()).into_account_truncating(); + MultiLocation { + parents: 0, + interior: Junctions::X1(AccountId32 { network: None, id: sovereign.into() }), + } + }; + + /// Maximum amount of DOT (in Plancks) that can be used for fees per teleport. + pub const MaxDotFeeAmount: u128 = 10_000_000_000; // 1 DOT } /// Type for specifying how a `MultiLocation` can be converted into an `AccountId`. This is used @@ -263,8 +302,33 @@ impl AutomationPalletConfig for AutomationPalletConfigPendulum { } } -pub type LocalAssetTransactor = - CustomTransactorInterceptor; +/// Only allows teleporting assets to AssetHub. +pub struct AllowedTeleportDestinations; +impl Contains for AllowedTeleportDestinations { + fn contains(dest: &MultiLocation) -> bool { + *dest == AssetHubLocation::get() + } +} + +pub type LocalAssetTransactor = CustomTransactorInterceptor< + Transactor, + AutomationPalletConfigPendulum, + AllowedTeleportDestinations, +>; + +pub struct TrustedTeleporters; +impl ContainsPair for TrustedTeleporters { + fn contains(asset: &MultiAsset, origin: &MultiLocation) -> bool { + if let MultiAsset { id: Concrete(loc), fun: Fungible(_) } = asset { + if loc == &NativeTokenLocation::get() && origin == &AssetHubLocation::get() { + log::trace!(target: "xcm::TrustedTeleporters", "Allowing teleport of native asset from Asset Hub"); + return true; + } + } + + false + } +} pub struct XcmConfig; impl staging_xcm_executor::Config for XcmConfig { @@ -274,8 +338,8 @@ impl staging_xcm_executor::Config for XcmConfig { type AssetTransactor = LocalAssetTransactor; type OriginConverter = XcmOriginToTransactDispatchOrigin; type IsReserve = MultiNativeAsset; - // Teleporting is disabled. - type IsTeleporter = (); + // Teleporting is restricted to assets/origins defined in TrustedTeleporters. + type IsTeleporter = TrustedTeleporters; type UniversalLocation = UniversalLocation; type Barrier = Barrier; type Weigher = FixedWeightBounds; @@ -308,6 +372,34 @@ pub type XcmRouter = ( XcmpQueue, ); +pub struct OnlyTeleportNative; +impl Contains<(MultiLocation, Vec)> for OnlyTeleportNative { + fn contains(t: &(MultiLocation, Vec)) -> bool { + let native = NativeTokenLocation::get(); + let allowed_dest = AssetHubLocation::get(); + + // Only allow teleporting to AssetHub + if t.0 != allowed_dest { + log::warn!( + target: "xcm::OnlyTeleportNative", + "Teleport rejected: destination {:?} is not AssetHub", + t.0 + ); + return false; + } + + // Only allow teleporting PEN (native token) + t.1.iter().all(|asset| { + log::trace!(target: "xcm::OnlyTeleportNative", "Asset to be teleported: {:?}", asset); + if let MultiAsset { id: Concrete(location), fun: Fungible(_) } = asset { + *location == native + } else { + false + } + }) + } +} + impl pallet_xcm::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Currency = Balances; @@ -319,7 +411,7 @@ impl pallet_xcm::Config for Runtime { // ^ Disable dispatchable execute on the XCM pallet. // Needs to be `Everything` for local testing. type XcmExecutor = XcmExecutor; - type XcmTeleportFilter = Nothing; + type XcmTeleportFilter = OnlyTeleportNative; type XcmReserveTransferFilter = Everything; type Weigher = FixedWeightBounds; type UniversalLocation = UniversalLocation;