From e5de270cd0118b5744b42104118dbcf50d87fddc Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Tue, 24 Feb 2026 20:18:07 +0000 Subject: [PATCH 01/10] Adjust xcm_config.rs for Pendulum and allow teleport of PEN to assethub --- runtime/pendulum/src/xcm_config.rs | 48 ++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/runtime/pendulum/src/xcm_config.rs b/runtime/pendulum/src/xcm_config.rs index c79351700..a3a1afc51 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}, @@ -15,6 +16,7 @@ use orml_xcm_support::{DepositToAlternative, IsNativeConcrete, MultiCurrencyAdap use pallet_xcm::XcmPassthrough; use polkadot_parachain::primitives::Sibling; use sp_runtime::traits::Convert; +use sp_std::vec::Vec; use staging_xcm_builder::{ AccountId32Aliases, AllowKnownQueryResponses, AllowSubscriptionsFrom, @@ -52,6 +54,17 @@ 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) + pub NativeTokenLocation: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1( + PalletInstance(::index() as u8) + ) + }; } /// Type for specifying how a `MultiLocation` can be converted into an `AccountId`. This is used @@ -118,7 +131,7 @@ pub type XcmOriginToTransactDispatchOrigin = ( parameter_types! { // One XCM operation is 1_000_000_000 weight - almost certainly a conservative estimate. - pub UnitWeightCost: XCMWeight = XCMWeight::from_parts(1_000_000_000, 0); + pub UnitWeightCost: XCMWeight = XCMWeight::from_parts(1_000_000_000, 1024); pub const MaxInstructions: u32 = 100; pub SelfLocation: MultiLocation = MultiLocation::here(); pub const BaseXcmWeight: XCMWeight = XCMWeight::from_parts(150_000_000, 0); @@ -266,6 +279,20 @@ impl AutomationPalletConfig for AutomationPalletConfigPendulum { pub type LocalAssetTransactor = CustomTransactorInterceptor; +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 { type RuntimeCall = RuntimeCall; @@ -275,7 +302,7 @@ impl staging_xcm_executor::Config for XcmConfig { type OriginConverter = XcmOriginToTransactDispatchOrigin; type IsReserve = MultiNativeAsset; // Teleporting is disabled. - type IsTeleporter = (); + type IsTeleporter = TrustedTeleporters; type UniversalLocation = UniversalLocation; type Barrier = Barrier; type Weigher = FixedWeightBounds; @@ -308,6 +335,21 @@ pub type XcmRouter = ( XcmpQueue, ); +pub struct OnlyTeleportNative; +impl Contains<(MultiLocation, Vec)> for OnlyTeleportNative { + fn contains(t: &(MultiLocation, Vec)) -> bool { + let native = NativeTokenLocation::get(); + 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 +361,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; From 8929cfacebcb740ee18dedb421a3870921930384 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 26 Feb 2026 16:37:11 +0000 Subject: [PATCH 02/10] Implement xcm-teleport pallet --- pallets/xcm-teleport/Cargo.toml | 50 +++++++ pallets/xcm-teleport/src/lib.rs | 238 ++++++++++++++++++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 pallets/xcm-teleport/Cargo.toml create mode 100644 pallets/xcm-teleport/src/lib.rs diff --git a/pallets/xcm-teleport/Cargo.toml b/pallets/xcm-teleport/Cargo.toml new file mode 100644 index 000000000..c5b4ad131 --- /dev/null +++ b/pallets/xcm-teleport/Cargo.toml @@ -0,0 +1,50 @@ +[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 } +staging-xcm-executor = { 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", + "staging-xcm-executor/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", + "staging-xcm-executor/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..9bf7b2f35 --- /dev/null +++ b/pallets/xcm-teleport/src/lib.rs @@ -0,0 +1,238 @@ +//! # 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(All, beneficiary) +//! ``` +//! +//! 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. + +#![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 sp_std::vec; + use xcm::v3::{ + prelude::*, Instruction, Junction, Junctions, MultiAsset, MultiAssetFilter, MultiAssets, + MultiLocation, SendXcm, WeightLimit, 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; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Native tokens were teleported to the destination chain. + TeleportedNativeTo { + /// The account that initiated the teleport. + sender: T::AccountId, + /// The beneficiary account on the destination chain. + beneficiary: T::AccountId, + /// The amount of native token teleported. + amount: BalanceOf, + /// The amount of fee asset (DOT) used for execution on the destination. + 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, + /// Failed to convert the amount to u128. + AmountConversionFailed, + } + + #[pallet::call] + impl Pallet + where + T::AccountId: Into<[u8; 32]>, + { + /// Teleport native tokens to the destination chain (AssetHub). + /// + /// This extrinsic: + /// 1. Burns `amount` of native tokens from the sender's account. + /// 2. Sends an XCM message to the destination that: + /// - Withdraws `fee_amount` of the fee asset (DOT) from this chain's + /// sovereign account for execution fees. + /// - Mints `amount` native tokens on the destination via `ReceiveTeleportedAsset`. + /// - Deposits all assets to the `beneficiary`. + /// + /// # Parameters + /// - `origin`: Must be a signed origin (the sender). + /// - `amount`: The amount of native tokens to teleport. + /// - `fee_amount`: The amount of the fee asset (DOT) to use for execution fees + /// on the destination. This is withdrawn from this chain's sovereign account. + /// - `beneficiary`: The destination AccountId32 on the destination chain. + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(200_000_000, 10_000))] + pub fn teleport_native_to_dest( + 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); + + // Convert balance to u128 for XCM + let amount_u128: u128 = amount + .try_into() + .map_err(|_| Error::::AmountConversionFailed)?; + + // 1. Withdraw and burn native tokens from the sender's account. + // Dropping the NegativeImbalance burns the tokens (reduces total issuance). + let _imbalance = T::Currency::withdraw( + &sender, + amount, + WithdrawReasons::TRANSFER, + ExistenceRequirement::AllowDeath, + )?; + // _imbalance is dropped here → tokens are burned + + // 2. Construct the remote XCM message for the destination chain. + let fee_asset_location = T::FeeAssetOnDest::get(); + let native_asset_on_dest = T::NativeAssetOnDest::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), + 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 everything (native token + leftover DOT) to the beneficiary + Instruction::DepositAsset { + assets: MultiAssetFilter::Wild(WildMultiAsset::All), + beneficiary: beneficiary_location, + }, + ]); + + // 3. Send the message to the destination 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 dest = T::DestinationLocation::get(); + + log::info!( + target: "xcm-teleport", + "Sending teleport message to {:?}: amount={}, fee_amount={}", + dest, amount_u128, fee_amount, + ); + + let (ticket, _price) = T::XcmRouter::validate(&mut Some(dest), &mut Some(message)) + .map_err(|e| { + log::error!( + target: "xcm-teleport", + "Failed to validate XCM message: {:?}", e + ); + Error::::XcmSendFailed + })?; + + T::XcmRouter::deliver(ticket).map_err(|e| { + log::error!( + target: "xcm-teleport", + "Failed to deliver XCM message: {:?}", e + ); + Error::::XcmSendFailed + })?; + + // 4. Emit event + Self::deposit_event(Event::TeleportedNativeTo { + sender, + beneficiary, + amount, + fee_amount, + }); + + Ok(()) + } + } +} From ba88493ba868b44635c13682b368ef2296ed8804 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 26 Feb 2026 16:38:06 +0000 Subject: [PATCH 03/10] Add xcm-teleport pallet to pendulum runtime --- Cargo.lock | 17 +++++++++++++++++ Cargo.toml | 1 + runtime/pendulum/Cargo.toml | 4 ++++ runtime/pendulum/src/lib.rs | 12 ++++++++++++ runtime/pendulum/src/xcm_config.rs | 15 ++++++++++++++- 5 files changed, 48 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b1b0783b2..7aeacf497 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8591,6 +8591,22 @@ 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", + "staging-xcm-executor", +] + [[package]] name = "parachain-staking" version = "1.6.0-d" @@ -8974,6 +8990,7 @@ dependencies = [ "pallet-utility", "pallet-vesting", "pallet-xcm", + "pallet-xcm-teleport", "parachain-staking", "parachains-common", "parity-scale-codec", 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/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..8d0ae0f54 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,15 @@ 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; +} + const fn deposit(items: u32, bytes: u32) -> Balance { (items as Balance * UNIT + (bytes as Balance) * (5 * MILLIUNIT / 100)) / 10 } @@ -1584,6 +1594,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 a3a1afc51..e238a1a39 100644 --- a/runtime/pendulum/src/xcm_config.rs +++ b/runtime/pendulum/src/xcm_config.rs @@ -58,13 +58,26 @@ parameter_types! { /// Asset Hub pub AssetHubLocation: MultiLocation = (Parent, Parachain(1000)).into(); - // PEN (native) + // 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 }; } /// Type for specifying how a `MultiLocation` can be converted into an `AccountId`. This is used From 16998b50cf6f205da83f5e2ba2a447eec3ba84a9 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 26 Feb 2026 16:44:39 +0000 Subject: [PATCH 04/10] Adjust xcm-teleport pallet to deposit leftover DOT back into sovereign account --- pallets/xcm-teleport/src/lib.rs | 53 ++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/pallets/xcm-teleport/src/lib.rs b/pallets/xcm-teleport/src/lib.rs index 9bf7b2f35..24d67d218 100644 --- a/pallets/xcm-teleport/src/lib.rs +++ b/pallets/xcm-teleport/src/lib.rs @@ -18,12 +18,23 @@ //! BuyExecution(DOT) ← passes the barrier //! ReceiveTeleportedAsset(PEN) ← mints PEN on AssetHub //! ClearOrigin -//! DepositAsset(All, beneficiary) +//! 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)] @@ -39,7 +50,7 @@ pub mod pallet { use sp_std::vec; use xcm::v3::{ prelude::*, Instruction, Junction, Junctions, MultiAsset, MultiAssetFilter, MultiAssets, - MultiLocation, SendXcm, WeightLimit, WildMultiAsset, Xcm, + MultiLocation, SendXcm, WeightLimit, WildFungibility, WildMultiAsset, Xcm, }; type BalanceOf = @@ -73,6 +84,17 @@ pub mod pallet { /// 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] @@ -99,6 +121,8 @@ pub mod pallet { 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, } @@ -116,13 +140,15 @@ pub mod pallet { /// - Withdraws `fee_amount` of the fee asset (DOT) from this chain's /// sovereign account for execution fees. /// - Mints `amount` native tokens on the destination via `ReceiveTeleportedAsset`. - /// - Deposits all assets to the `beneficiary`. + /// - Deposits only the native tokens to the `beneficiary`. + /// - Returns any leftover fee asset (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 the fee asset (DOT) to use for execution fees - /// on the destination. This is withdrawn from this chain's sovereign account. + /// on the destination. Must not exceed `MaxFeeAmount`. This DOT is withdrawn + /// from this chain's sovereign account on the destination. /// - `beneficiary`: The destination AccountId32 on the destination chain. #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(200_000_000, 10_000))] @@ -137,6 +163,10 @@ pub mod pallet { // 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 @@ -156,6 +186,7 @@ pub mod pallet { // 2. Construct the remote XCM message for the destination chain. 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 { @@ -172,7 +203,7 @@ pub mod pallet { }; let native_multi_asset = MultiAsset { - id: AssetId::Concrete(native_asset_on_dest), + id: AssetId::Concrete(native_asset_on_dest.clone()), fun: Fungibility::Fungible(amount_u128), }; @@ -188,11 +219,19 @@ pub mod pallet { Instruction::ReceiveTeleportedAsset(MultiAssets::from(vec![native_multi_asset])), // Remove origin to prevent further privileged operations Instruction::ClearOrigin, - // Deposit everything (native token + leftover DOT) to the beneficiary + // Deposit ONLY the native token (PEN) to the beneficiary Instruction::DepositAsset { - assets: MultiAssetFilter::Wild(WildMultiAsset::All), + 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 the destination via the XCM router. From acbcf82123ea83600f7833b83a13091d757dc4b3 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 26 Feb 2026 16:49:52 +0000 Subject: [PATCH 05/10] Adjust config in pendulum runtime --- runtime/pendulum/src/lib.rs | 2 ++ runtime/pendulum/src/xcm_config.rs | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/runtime/pendulum/src/lib.rs b/runtime/pendulum/src/lib.rs index 8d0ae0f54..237a6ccfe 100644 --- a/runtime/pendulum/src/lib.rs +++ b/runtime/pendulum/src/lib.rs @@ -1018,6 +1018,8 @@ impl pallet_xcm_teleport::Config for Runtime { 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 { diff --git a/runtime/pendulum/src/xcm_config.rs b/runtime/pendulum/src/xcm_config.rs index e238a1a39..f08dfd8da 100644 --- a/runtime/pendulum/src/xcm_config.rs +++ b/runtime/pendulum/src/xcm_config.rs @@ -15,7 +15,7 @@ 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::{ @@ -78,6 +78,19 @@ parameter_types! { /// 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 From 267bf397e6269b49ce6446dcbf62987220e2b3c1 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 26 Feb 2026 17:46:24 +0000 Subject: [PATCH 06/10] Implement teleport check-in and check-out logic in custom transactor --- runtime/common/src/custom_transactor.rs | 39 +++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/runtime/common/src/custom_transactor.rs b/runtime/common/src/custom_transactor.rs index efa7aa2eb..6bc90f585 100644 --- a/runtime/common/src/custom_transactor.rs +++ b/runtime/common/src/custom_transactor.rs @@ -57,4 +57,43 @@ impl result::Result { WrappedTransactor::transfer_asset(asset, from, to, _context) } + + fn can_check_out( + _dest: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) -> Result { + // Allow teleport check-out. The asset has already been withdrawn from the + // sender's account via WithdrawAsset and is in the holding register. + // We simply permit the teleport-out here. + Ok(()) + } + + fn check_out( + _dest: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) { + // No-op: the asset was already withdrawn from the sender's account. + // In a teleport, the local side just needs to ensure the asset is + // removed from circulation, which WithdrawAsset + not depositing + // back effectively does (the asset is burned from holding). + } + + fn can_check_in( + _origin: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) -> Result { + // Allow teleport check-in (receiving teleported assets). + Ok(()) + } + + fn check_in( + _origin: &MultiLocation, + _what: &MultiAsset, + _context: &XcmContext, + ) { + // No-op: the asset will be deposited via deposit_asset. + } } From c1ae66a021d71bf7cea2388665db479f5ae032c2 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Thu, 26 Feb 2026 18:44:21 +0000 Subject: [PATCH 07/10] Implement teleport destination validation in custom transactor --- Cargo.lock | 1 + runtime/common/Cargo.toml | 2 + runtime/common/src/custom_transactor.rs | 52 ++++++++++++++++++------- runtime/pendulum/src/xcm_config.rs | 23 ++++++++++- 4 files changed, 63 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7aeacf497..3e9b3f309 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11313,6 +11313,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "log", "orml-asset-registry", "orml-traits", "orml-xcm-support", 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 6bc90f585..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, @@ -59,13 +73,21 @@ impl Result { - // Allow teleport check-out. The asset has already been withdrawn from the - // sender's account via WithdrawAsset and is in the holding register. - // We simply permit the teleport-out here. + // 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(()) } @@ -74,10 +96,9 @@ impl Result { // Allow teleport check-in (receiving teleported assets). + // The origin is already validated by IsTeleporter (TrustedTeleporters) + // before this method is called. Ok(()) } @@ -94,6 +117,7 @@ impl for AllowedTeleportDestinations { + fn contains(dest: &MultiLocation) -> bool { + *dest == AssetHubLocation::get() + } +} + pub type LocalAssetTransactor = - CustomTransactorInterceptor; + CustomTransactorInterceptor; pub struct TrustedTeleporters; impl ContainsPair for TrustedTeleporters { @@ -365,6 +373,19 @@ 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 { From b5461856761e15f2ad3741c44b8603fba47f216e Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 27 Feb 2026 08:59:42 +0000 Subject: [PATCH 08/10] Rename variables --- pallets/xcm-teleport/src/lib.rs | 45 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/pallets/xcm-teleport/src/lib.rs b/pallets/xcm-teleport/src/lib.rs index 24d67d218..1545398cf 100644 --- a/pallets/xcm-teleport/src/lib.rs +++ b/pallets/xcm-teleport/src/lib.rs @@ -100,15 +100,15 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// Native tokens were teleported to the destination chain. - TeleportedNativeTo { + /// Native tokens were teleported to AssetHub. + NativeTeleportedToAssetHub { /// The account that initiated the teleport. sender: T::AccountId, - /// The beneficiary account on the destination chain. + /// The beneficiary account on AssetHub. beneficiary: T::AccountId, /// The amount of native token teleported. amount: BalanceOf, - /// The amount of fee asset (DOT) used for execution on the destination. + /// The amount of DOT used for execution fees on AssetHub. fee_amount: u128, }, } @@ -132,27 +132,26 @@ pub mod pallet { where T::AccountId: Into<[u8; 32]>, { - /// Teleport native tokens to the destination chain (AssetHub). + /// Teleport native tokens to AssetHub. /// /// This extrinsic: - /// 1. Burns `amount` of native tokens from the sender's account. - /// 2. Sends an XCM message to the destination that: - /// - Withdraws `fee_amount` of the fee asset (DOT) from this chain's - /// sovereign account for execution fees. - /// - Mints `amount` native tokens on the destination via `ReceiveTeleportedAsset`. + /// 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 fee asset (DOT) to the sovereign account. + /// - 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 the fee asset (DOT) to use for execution fees - /// on the destination. Must not exceed `MaxFeeAmount`. This DOT is withdrawn - /// from this chain's sovereign account on the destination. - /// - `beneficiary`: The destination AccountId32 on the destination chain. + /// - `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_dest( + pub fn teleport_native_to_asset_hub( origin: OriginFor, amount: BalanceOf, fee_amount: u128, @@ -183,7 +182,7 @@ pub mod pallet { )?; // _imbalance is dropped here → tokens are burned - // 2. Construct the remote XCM message for the destination chain. + // 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(); @@ -234,19 +233,19 @@ pub mod pallet { }, ]); - // 3. Send the message to the destination via the XCM router. + // 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 dest = T::DestinationLocation::get(); + let asset_hub = T::DestinationLocation::get(); log::info!( target: "xcm-teleport", - "Sending teleport message to {:?}: amount={}, fee_amount={}", - dest, amount_u128, fee_amount, + "Teleporting native to AssetHub ({:?}): amount={}, fee_amount={}", + asset_hub, amount_u128, fee_amount, ); - let (ticket, _price) = T::XcmRouter::validate(&mut Some(dest), &mut Some(message)) + let (ticket, _price) = T::XcmRouter::validate(&mut Some(asset_hub), &mut Some(message)) .map_err(|e| { log::error!( target: "xcm-teleport", @@ -264,7 +263,7 @@ pub mod pallet { })?; // 4. Emit event - Self::deposit_event(Event::TeleportedNativeTo { + Self::deposit_event(Event::NativeTeleportedToAssetHub { sender, beneficiary, amount, From 7bac43233b9f2343114db36d9034a43ddfab4ecb Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:51:05 +0100 Subject: [PATCH 09/10] Address review comments: remove unused deps, fix imbalance handling, update comments (#554) * Initial plan * Address review comments: remove unused deps, fix comment, handle imbalance properly Co-authored-by: ebma <6690623+ebma@users.noreply.github.com> * Clarify comment about refund limitations Co-authored-by: ebma <6690623+ebma@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ebma <6690623+ebma@users.noreply.github.com> --- Cargo.lock | 1 - pallets/xcm-teleport/Cargo.toml | 3 --- pallets/xcm-teleport/src/lib.rs | 34 +++++++++++++++++++----------- runtime/pendulum/src/xcm_config.rs | 2 +- 4 files changed, 23 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3e9b3f309..62d42714d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8604,7 +8604,6 @@ dependencies = [ "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", - "staging-xcm-executor", ] [[package]] diff --git a/pallets/xcm-teleport/Cargo.toml b/pallets/xcm-teleport/Cargo.toml index c5b4ad131..ddcddefec 100644 --- a/pallets/xcm-teleport/Cargo.toml +++ b/pallets/xcm-teleport/Cargo.toml @@ -16,7 +16,6 @@ sp-runtime = { workspace = true } sp-std = { workspace = true } xcm = { workspace = true } -staging-xcm-executor = { workspace = true } # benchmarking frame-benchmarking = { workspace = true, optional = true } @@ -29,7 +28,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", - "staging-xcm-executor/runtime-benchmarks", ] std = [ "frame-support/std", @@ -40,7 +38,6 @@ std = [ "sp-runtime/std", "sp-std/std", "xcm/std", - "staging-xcm-executor/std", "frame-benchmarking?/std", ] try-runtime = [ diff --git a/pallets/xcm-teleport/src/lib.rs b/pallets/xcm-teleport/src/lib.rs index 1545398cf..28f12cec0 100644 --- a/pallets/xcm-teleport/src/lib.rs +++ b/pallets/xcm-teleport/src/lib.rs @@ -47,7 +47,6 @@ pub mod pallet { traits::{Currency, ExistenceRequirement, WithdrawReasons}, }; use frame_system::pallet_prelude::*; - use sp_std::vec; use xcm::v3::{ prelude::*, Instruction, Junction, Junctions, MultiAsset, MultiAssetFilter, MultiAssets, MultiLocation, SendXcm, WeightLimit, WildFungibility, WildMultiAsset, Xcm, @@ -172,15 +171,17 @@ pub mod pallet { .try_into() .map_err(|_| Error::::AmountConversionFailed)?; - // 1. Withdraw and burn native tokens from the sender's account. - // Dropping the NegativeImbalance burns the tokens (reduces total issuance). - let _imbalance = T::Currency::withdraw( + // 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, )?; - // _imbalance is dropped here → tokens are burned // 2. Construct the remote XCM message for AssetHub. let fee_asset_location = T::FeeAssetOnDest::get(); @@ -245,22 +246,31 @@ pub mod pallet { asset_hub, amount_u128, fee_amount, ); - let (ticket, _price) = T::XcmRouter::validate(&mut Some(asset_hub), &mut Some(message)) - .map_err(|e| { + 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 ); - Error::::XcmSendFailed - })?; + // Refund the withdrawn tokens back to the sender + T::Currency::resolve_creating(&sender, imbalance); + return Err(Error::::XcmSendFailed.into()); + }, + }; - T::XcmRouter::deliver(ticket).map_err(|e| { + if let Err(e) = T::XcmRouter::deliver(ticket) { log::error!( target: "xcm-teleport", "Failed to deliver XCM message: {:?}", e ); - Error::::XcmSendFailed - })?; + // 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 { diff --git a/runtime/pendulum/src/xcm_config.rs b/runtime/pendulum/src/xcm_config.rs index 93693a974..c323fa10d 100644 --- a/runtime/pendulum/src/xcm_config.rs +++ b/runtime/pendulum/src/xcm_config.rs @@ -335,7 +335,7 @@ impl staging_xcm_executor::Config for XcmConfig { type AssetTransactor = LocalAssetTransactor; type OriginConverter = XcmOriginToTransactDispatchOrigin; type IsReserve = MultiNativeAsset; - // Teleporting is disabled. + // Teleporting is restricted to assets/origins defined in TrustedTeleporters. type IsTeleporter = TrustedTeleporters; type UniversalLocation = UniversalLocation; type Barrier = Barrier; From c2c126f718ce21d7a2c5c611b69ff30aea214104 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Fri, 27 Feb 2026 09:52:09 +0000 Subject: [PATCH 10/10] Revert UnitWeightCost change --- runtime/pendulum/src/xcm_config.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/runtime/pendulum/src/xcm_config.rs b/runtime/pendulum/src/xcm_config.rs index c323fa10d..72133db18 100644 --- a/runtime/pendulum/src/xcm_config.rs +++ b/runtime/pendulum/src/xcm_config.rs @@ -157,7 +157,7 @@ pub type XcmOriginToTransactDispatchOrigin = ( parameter_types! { // One XCM operation is 1_000_000_000 weight - almost certainly a conservative estimate. - pub UnitWeightCost: XCMWeight = XCMWeight::from_parts(1_000_000_000, 1024); + pub UnitWeightCost: XCMWeight = XCMWeight::from_parts(1_000_000_000, 0); pub const MaxInstructions: u32 = 100; pub SelfLocation: MultiLocation = MultiLocation::here(); pub const BaseXcmWeight: XCMWeight = XCMWeight::from_parts(150_000_000, 0); @@ -310,8 +310,11 @@ impl Contains for AllowedTeleportDestinations { } } -pub type LocalAssetTransactor = - CustomTransactorInterceptor; +pub type LocalAssetTransactor = CustomTransactorInterceptor< + Transactor, + AutomationPalletConfigPendulum, + AllowedTeleportDestinations, +>; pub struct TrustedTeleporters; impl ContainsPair for TrustedTeleporters {