referrerpolicy=no-referrer-when-downgrade

pallet_xcm_bridge_hub_router/
lib.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Parity Bridges Common.
3
4// Parity Bridges Common is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Parity Bridges Common is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Parity Bridges Common.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Pallet that may be used instead of `SovereignPaidRemoteExporter` or `UnpaidRemoteExporter`
18//! in the XCM router configuration. The main thing that the pallet offers is the dynamic
19//! message fee, that is computed based on the bridge queues state. It starts exponentially
20//! increasing if the queue between this chain and the sibling/child bridge hub is congested.
21//!
22//! The pallet is configurable to use either paid or unpaid execution on the bridge hub
23//! via the [`Config::UnpaidExport`] associated type. It will use `SovereignPaidRemoteExporter`
24//! for sovereign-paid bridging or `UnpaidRemoteExporter` for unpaid bridging (e.g. between
25//! system parachains where the bridge hub waives fees).
26//!
27//! All other bridge hub queues offer some backpressure mechanisms. So if at least one
28//! of all queues is congested, it will eventually lead to the growth of the queue at
29//! this chain.
30//!
31//! **A note on terminology**: when we mention the bridge hub here, we mean the chain that
32//! has the messages pallet deployed (`pallet-bridge-grandpa`, `pallet-bridge-messages`,
33//! `pallet-xcm-bridge-hub`, ...). It may be the system bridge hub parachain or any other
34//! chain.
35
36#![cfg_attr(not(feature = "std"), no_std)]
37
38use bp_xcm_bridge_hub_router::MINIMAL_DELIVERY_FEE_FACTOR;
39pub use bp_xcm_bridge_hub_router::{BridgeState, XcmChannelStatusProvider};
40use codec::Encode;
41use frame_support::traits::Get;
42use polkadot_runtime_parachains::FeeTracker;
43use sp_core::H256;
44use sp_runtime::{FixedPointNumber, FixedU128};
45use sp_std::vec::Vec;
46use xcm::prelude::*;
47use xcm_builder::{
48	ExporterFor, InspectMessageQueues, SovereignPaidRemoteExporter, UnpaidRemoteExporter,
49};
50
51pub use pallet::*;
52pub use weights::WeightInfo;
53
54pub mod benchmarking;
55pub mod weights;
56
57mod mock;
58
59/// Maximal size of the XCM message that may be sent over bridge.
60///
61/// This should be less than the maximal size, allowed by the messages pallet, because
62/// the message itself is wrapped in other structs and is double encoded.
63pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024;
64
65/// The target that will be used when publishing logs related to this pallet.
66///
67/// This doesn't match the pattern used by other bridge pallets (`runtime::bridge-*`). But this
68/// pallet has significant differences with those pallets. The main one is that is intended to
69/// be deployed at sending chains. Other bridge pallets are likely to be deployed at the separate
70/// bridge hub parachain.
71pub const LOG_TARGET: &str = "xcm::bridge-hub-router";
72
73#[frame_support::pallet]
74pub mod pallet {
75	use super::*;
76	use frame_support::pallet_prelude::*;
77	use frame_system::pallet_prelude::*;
78
79	#[pallet::config]
80	pub trait Config<I: 'static = ()>: frame_system::Config {
81		/// The overarching event type.
82		#[allow(deprecated)]
83		type RuntimeEvent: From<Event<Self, I>>
84			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
85		/// Benchmarks results from runtime we're plugged into.
86		type WeightInfo: WeightInfo;
87
88		/// Universal location of this runtime.
89		type UniversalLocation: Get<InteriorLocation>;
90		/// Relative location of the supported sibling bridge hub.
91		type SiblingBridgeHubLocation: Get<Location>;
92		/// The bridged network that this config is for if specified.
93		/// Also used for filtering `Bridges` by `BridgedNetworkId`.
94		/// If not specified, allows all networks pass through.
95		type BridgedNetworkId: Get<Option<NetworkId>>;
96		/// Configuration for supported **bridged networks/locations** with **bridge location** and
97		/// **possible fee**. Allows to externalize better control over allowed **bridged
98		/// networks/locations**.
99		type Bridges: ExporterFor;
100		/// Checks the XCM version for the destination.
101		type DestinationVersion: GetVersion;
102
103		/// Origin of the sibling bridge hub that is allowed to report bridge status.
104		type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
105		/// Actual message sender (`HRMP` or `DMP`) to the sibling bridge hub location.
106		type ToBridgeHubSender: SendXcm;
107		/// Local XCM channel manager.
108		type LocalXcmChannelManager: XcmChannelStatusProvider;
109
110		/// Whether to use unpaid execution when sending export messages to the bridge hub.
111		/// Set to `ConstBool<true>` for system parachains where the bridge hub waives fees,
112		/// `ConstBool<false>` for sovereign-paid bridging.
113		type UnpaidExport: Get<bool>;
114
115		/// Additional fee that is paid for every byte of the outbound message.
116		type ByteFee: Get<u128>;
117		/// Asset that is used to paid bridge fee.
118		type FeeAsset: Get<AssetId>;
119	}
120
121	#[pallet::pallet]
122	pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
123
124	#[pallet::hooks]
125	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
126		fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
127			// if XCM channel is still congested, we don't change anything
128			if T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get()) {
129				return T::WeightInfo::on_initialize_when_congested();
130			}
131
132			// if bridge has reported congestion, we don't change anything
133			let mut bridge = Self::bridge();
134			if bridge.is_congested {
135				return T::WeightInfo::on_initialize_when_congested();
136			}
137
138			let previous_factor = Self::get_fee_factor(());
139			// if we can't decrease the delivery fee factor anymore, we don't change anything
140			if !Self::do_decrease_fee_factor(&mut bridge.delivery_fee_factor) {
141				return T::WeightInfo::on_initialize_when_congested();
142			}
143
144			tracing::info!(
145				target: LOG_TARGET,
146				from=%previous_factor,
147				to=%bridge.delivery_fee_factor,
148				"Bridge channel is uncongested. Decreased fee factor"
149			);
150			Self::deposit_event(Event::DeliveryFeeFactorDecreased {
151				new_value: bridge.delivery_fee_factor,
152			});
153
154			Bridge::<T, I>::put(bridge);
155
156			T::WeightInfo::on_initialize_when_non_congested()
157		}
158	}
159
160	#[pallet::call]
161	impl<T: Config<I>, I: 'static> Pallet<T, I> {
162		/// Notification about congested bridge queue.
163		#[pallet::call_index(0)]
164		#[pallet::weight(T::WeightInfo::report_bridge_status())]
165		pub fn report_bridge_status(
166			origin: OriginFor<T>,
167			// this argument is not currently used, but to ease future migration, we'll keep it
168			// here
169			bridge_id: H256,
170			is_congested: bool,
171		) -> DispatchResult {
172			T::BridgeHubOrigin::ensure_origin(origin)?;
173
174			tracing::info!(
175				target: LOG_TARGET,
176				from=?bridge_id,
177				congested=%is_congested,
178				"Received bridge status"
179			);
180
181			Bridge::<T, I>::mutate(|bridge| {
182				bridge.is_congested = is_congested;
183			});
184			Ok(())
185		}
186	}
187
188	/// Bridge that we are using.
189	///
190	/// **bridges-v1** assumptions: all outbound messages through this router are using single lane
191	/// and to single remote consensus. If there is some other remote consensus that uses the same
192	/// bridge hub, the separate pallet instance shall be used, In `v2` we'll have all required
193	/// primitives (lane-id aka bridge-id, derived from XCM locations) to support multiple  bridges
194	/// by the same pallet instance.
195	#[pallet::storage]
196	pub type Bridge<T: Config<I>, I: 'static = ()> = StorageValue<_, BridgeState, ValueQuery>;
197
198	impl<T: Config<I>, I: 'static> Pallet<T, I> {
199		/// Bridge that we are using.
200		pub fn bridge() -> BridgeState {
201			Bridge::<T, I>::get()
202		}
203
204		/// Called when new message is sent (queued to local outbound XCM queue) over the bridge.
205		pub(crate) fn on_message_sent_to_bridge(message_size: u32) {
206			tracing::trace!(
207				target: LOG_TARGET,
208				?message_size, "on_message_sent_to_bridge"
209			);
210			let _ = Bridge::<T, I>::try_mutate(|bridge| {
211				let is_channel_with_bridge_hub_congested =
212					T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get());
213				let is_bridge_congested = bridge.is_congested;
214
215				// if outbound queue is not congested AND bridge has not reported congestion, do
216				// nothing
217				if !is_channel_with_bridge_hub_congested && !is_bridge_congested {
218					return Err(());
219				}
220
221				let previous_factor = Self::get_fee_factor(());
222				// ok - we need to increase the fee factor, let's do that
223				<Self as FeeTracker>::do_increase_fee_factor(
224					&mut bridge.delivery_fee_factor,
225					message_size as u128,
226				);
227
228				tracing::info!(
229					target: LOG_TARGET,
230					from=%previous_factor,
231					to=%bridge.delivery_fee_factor,
232					"Bridge channel is congested. Increased fee factor"
233				);
234				Self::deposit_event(Event::DeliveryFeeFactorIncreased {
235					new_value: bridge.delivery_fee_factor,
236				});
237				Ok(())
238			});
239		}
240	}
241
242	#[pallet::event]
243	#[pallet::generate_deposit(pub(super) fn deposit_event)]
244	pub enum Event<T: Config<I>, I: 'static = ()> {
245		/// Delivery fee factor has been decreased.
246		DeliveryFeeFactorDecreased {
247			/// New value of the `DeliveryFeeFactor`.
248			new_value: FixedU128,
249		},
250		/// Delivery fee factor has been increased.
251		DeliveryFeeFactorIncreased {
252			/// New value of the `DeliveryFeeFactor`.
253			new_value: FixedU128,
254		},
255	}
256}
257
258// This pallet acts as the `ExporterFor` for the inner exporter to compute
259// message fee using fee factor.
260impl<T: Config<I>, I: 'static> ExporterFor for Pallet<T, I> {
261	fn exporter_for(
262		network: &NetworkId,
263		remote_location: &InteriorLocation,
264		message: &Xcm<()>,
265	) -> Option<(Location, Option<Asset>)> {
266		tracing::trace!(
267			target: LOG_TARGET,
268			?network, ?remote_location, msg=?message, "exporter_for"
269		);
270		// ensure that the message is sent to the expected bridged network (if specified).
271		if let Some(bridged_network) = T::BridgedNetworkId::get() {
272			if *network != bridged_network {
273				tracing::trace!(
274					target: LOG_TARGET,
275					bridged_network_id=?bridged_network, ?network, "Router does not support bridging!"
276				);
277				return None;
278			}
279		}
280
281		// ensure that the message is sent to the expected bridged network and location.
282		let (bridge_hub_location, maybe_payment) =
283			match T::Bridges::exporter_for(network, remote_location, message) {
284				Some((bridge_hub_location, maybe_payment))
285					if bridge_hub_location.eq(&T::SiblingBridgeHubLocation::get()) =>
286				{
287					(bridge_hub_location, maybe_payment)
288				},
289				_ => {
290					tracing::trace!(
291						target: LOG_TARGET,
292						bridged_network_id=?T::BridgedNetworkId::get(),
293						sibling_bridge_hub_location=?T::SiblingBridgeHubLocation::get(),
294						?network,
295						?remote_location,
296						"Router configured does not support bridging!"
297					);
298					return None;
299				},
300			};
301
302		// take `base_fee` from `T::Brides`, but it has to be the same `T::FeeAsset`
303		let base_fee = match maybe_payment {
304			Some(payment) => match payment {
305				Asset { fun: Fungible(amount), id } if id.eq(&T::FeeAsset::get()) => amount,
306				invalid_asset => {
307					tracing::error!(
308						target: LOG_TARGET,
309						bridged_network_id=?T::BridgedNetworkId::get(),
310						fee_asset=?T::FeeAsset::get(),
311						with=?invalid_asset,
312						?bridge_hub_location,
313						?network,
314						?remote_location,
315						"Router is configured for `T::FeeAsset` which is not compatible for bridging!"
316					);
317					return None;
318				},
319			},
320			None => 0,
321		};
322
323		// compute fee amount. Keep in mind that this is only the bridge fee. The fee for sending
324		// message from this chain to child/sibling bridge hub is determined by the
325		// `Config::ToBridgeHubSender`
326		let message_size = message.encoded_size();
327		let message_fee = (message_size as u128).saturating_mul(T::ByteFee::get());
328		let fee_sum = base_fee.saturating_add(message_fee);
329		let fee_factor = Self::get_fee_factor(());
330		let fee = fee_factor.saturating_mul_int(fee_sum);
331
332		let fee = if fee > 0 { Some((T::FeeAsset::get(), fee).into()) } else { None };
333
334		tracing::info!(
335			target: LOG_TARGET,
336			to=?(network, remote_location),
337			bridge_fee=?fee,
338			%fee_factor,
339			"Going to send message ({message_size} bytes) over bridge."
340		);
341
342		Some((bridge_hub_location, fee))
343	}
344}
345
346// This pallet acts as the `SendXcm` to the sibling/child bridge hub instead of regular
347// XCMP/DMP transport. This allows injecting dynamic message fees into XCM programs that
348// are going to the bridged network.
349impl<T: Config<I>, I: 'static> SendXcm for Pallet<T, I> {
350	type Ticket = (u32, <T::ToBridgeHubSender as SendXcm>::Ticket);
351
352	fn validate(
353		dest: &mut Option<Location>,
354		xcm: &mut Option<Xcm<()>>,
355	) -> SendResult<Self::Ticket> {
356		tracing::trace!(target: LOG_TARGET, msg=?xcm, destination=?dest, "validate");
357
358		// In case of success, the inner exporter can modify XCM instructions and consume
359		// `dest` / `xcm`, so we retain the clone of original message and the destination for later
360		// `DestinationVersion` validation.
361		let xcm_to_dest_clone = xcm.clone();
362		let dest_clone = dest.clone();
363
364		// First, use the inner exporter to validate the destination to determine if it is even
365		// routable. If it is not, return an error. If it is, then the XCM is extended with
366		// instructions to pay the message fee at the sibling/child bridge hub. The cost will
367		// include both the cost of (1) delivery to the sibling bridge hub and (2) delivery
368		// to the bridged bridge hub (returned by `Self::exporter_for`).
369		let exporter_result = if T::UnpaidExport::get() {
370			UnpaidRemoteExporter::<
371				Pallet<T, I>,
372				T::ToBridgeHubSender,
373				T::UniversalLocation,
374			>::validate(dest, xcm)
375		} else {
376			SovereignPaidRemoteExporter::<
377				Pallet<T, I>,
378				T::ToBridgeHubSender,
379				T::UniversalLocation,
380			>::validate(dest, xcm)
381		};
382		match exporter_result {
383			Ok((ticket, cost)) => {
384				// If the ticket is ok, it means we are routing with this router, so we need to
385				// apply more validations to the cloned `dest` and `xcm`, which are required here.
386				let xcm_to_dest_clone = xcm_to_dest_clone.ok_or(SendError::MissingArgument)?;
387				let dest_clone = dest_clone.ok_or(SendError::MissingArgument)?;
388
389				// We won't have access to `dest` and `xcm` in the `deliver` method, so we need to
390				// precompute everything required here. However, `dest` and `xcm` were consumed by
391				// the inner exporter, so we need to use their clones.
392				let message_size = xcm_to_dest_clone.encoded_size() as _;
393
394				// The bridge doesn't support oversized or overweight messages. Therefore, it's
395				// better to drop such messages here rather than at the bridge hub. Let's check the
396				// message size."
397				if message_size > HARD_MESSAGE_SIZE_LIMIT {
398					return Err(SendError::ExceedsMaxMessageSize);
399				}
400
401				// We need to ensure that the known `dest`'s XCM version can comprehend the current
402				// `xcm` program. This may seem like an additional, unnecessary check, but it is
403				// not. A similar check is probably performed by the inner exporter, which
404				// attempts to send a versioned message to the sibling bridge hub. However, the
405				// local bridge hub may have a higher XCM version than the remote `dest`. Once
406				// again, it is better to discard such messages here than at the bridge hub (e.g.,
407				// to avoid losing funds).
408				let destination_version = T::DestinationVersion::get_version_for(&dest_clone)
409					.ok_or(SendError::DestinationUnsupported)?;
410				VersionedXcm::from(xcm_to_dest_clone)
411					.into_version(destination_version)
412					.map_err(|()| SendError::DestinationUnsupported)?;
413
414				Ok(((message_size, ticket), cost))
415			},
416			Err(e) => {
417				tracing::trace!(target: LOG_TARGET, error=?e, "validate - inner exporter");
418				Err(e)
419			},
420		}
421	}
422
423	fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
424		// use router to enqueue message to the sibling/child bridge hub. This also should handle
425		// payment for passing through this queue.
426		let (message_size, ticket) = ticket;
427		let xcm_hash = T::ToBridgeHubSender::deliver(ticket)?;
428
429		// increase delivery fee factor if required
430		Self::on_message_sent_to_bridge(message_size);
431
432		tracing::trace!(target: LOG_TARGET, ?xcm_hash, "deliver - message sent");
433		Ok(xcm_hash)
434	}
435}
436
437impl<T: Config<I>, I: 'static> InspectMessageQueues for Pallet<T, I> {
438	fn clear_messages() {}
439
440	/// This router needs to implement `InspectMessageQueues` but doesn't have to
441	/// return any messages, since it just reuses the `XcmpQueue` router.
442	fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
443		Vec::new()
444	}
445}
446
447impl<T: Config<I>, I: 'static> FeeTracker for Pallet<T, I> {
448	type Id = ();
449
450	const MIN_FEE_FACTOR: FixedU128 = MINIMAL_DELIVERY_FEE_FACTOR;
451
452	fn get_fee_factor(_id: Self::Id) -> FixedU128 {
453		Self::bridge().delivery_fee_factor
454	}
455
456	fn set_fee_factor(_id: Self::Id, val: FixedU128) {
457		let mut bridge = Self::bridge();
458		bridge.delivery_fee_factor = val;
459		Bridge::<T, I>::put(bridge);
460	}
461}
462
463#[cfg(test)]
464mod tests {
465	use super::*;
466	use frame_support::assert_ok;
467	use mock::*;
468
469	use frame_support::traits::Hooks;
470	use frame_system::{EventRecord, Phase};
471	use sp_runtime::traits::One;
472
473	fn congested_bridge(delivery_fee_factor: FixedU128) -> BridgeState {
474		BridgeState { is_congested: true, delivery_fee_factor }
475	}
476
477	fn uncongested_bridge(delivery_fee_factor: FixedU128) -> BridgeState {
478		BridgeState { is_congested: false, delivery_fee_factor }
479	}
480
481	#[test]
482	fn initial_fee_factor_is_one() {
483		run_test(|| {
484			assert_eq!(
485				Bridge::<TestRuntime, ()>::get(),
486				uncongested_bridge(Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR),
487			);
488		})
489	}
490
491	#[test]
492	fn fee_factor_is_not_decreased_from_on_initialize_when_xcm_channel_is_congested() {
493		run_test(|| {
494			Bridge::<TestRuntime, ()>::put(uncongested_bridge(FixedU128::from_rational(125, 100)));
495			TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
496
497			// it should not decrease, because queue is congested
498			let old_delivery = XcmBridgeHubRouter::bridge();
499			XcmBridgeHubRouter::on_initialize(One::one());
500			assert_eq!(XcmBridgeHubRouter::bridge(), old_delivery);
501			assert_eq!(System::events(), vec![]);
502		})
503	}
504
505	#[test]
506	fn fee_factor_is_not_decreased_from_on_initialize_when_bridge_has_reported_congestion() {
507		run_test(|| {
508			Bridge::<TestRuntime, ()>::put(congested_bridge(FixedU128::from_rational(125, 100)));
509
510			// it should not decrease, because bridge congested
511			let old_bridge = XcmBridgeHubRouter::bridge();
512			XcmBridgeHubRouter::on_initialize(One::one());
513			assert_eq!(XcmBridgeHubRouter::bridge(), old_bridge);
514			assert_eq!(System::events(), vec![]);
515		})
516	}
517
518	#[test]
519	fn fee_factor_is_decreased_from_on_initialize_when_xcm_channel_is_uncongested() {
520		run_test(|| {
521			let initial_fee_factor = FixedU128::from_rational(125, 100);
522			Bridge::<TestRuntime, ()>::put(uncongested_bridge(initial_fee_factor));
523
524			// it should eventually decrease to one
525			while XcmBridgeHubRouter::bridge().delivery_fee_factor >
526				Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR
527			{
528				XcmBridgeHubRouter::on_initialize(One::one());
529			}
530
531			// verify that it doesn't decrease anymore
532			XcmBridgeHubRouter::on_initialize(One::one());
533			assert_eq!(
534				XcmBridgeHubRouter::bridge(),
535				uncongested_bridge(Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR)
536			);
537
538			// check emitted event
539			let first_system_event = System::events().first().cloned();
540			assert_eq!(
541				first_system_event,
542				Some(EventRecord {
543					phase: Phase::Initialization,
544					event: RuntimeEvent::XcmBridgeHubRouter(Event::DeliveryFeeFactorDecreased {
545						new_value: initial_fee_factor / XcmBridgeHubRouter::EXPONENTIAL_FEE_BASE,
546					}),
547					topics: vec![],
548				})
549			);
550		})
551	}
552
553	#[test]
554	fn not_applicable_if_destination_is_within_other_network() {
555		run_test(|| {
556			// unroutable dest
557			let dest = Location::new(2, [GlobalConsensus(ByGenesis([0; 32])), Parachain(1000)]);
558			let xcm: Xcm<()> = vec![ClearOrigin].into();
559
560			// check that router does not consume when `NotApplicable`
561			let mut xcm_wrapper = Some(xcm.clone());
562			assert_eq!(
563				XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
564				Err(SendError::NotApplicable),
565			);
566			// XCM is NOT consumed and untouched
567			assert_eq!(Some(xcm.clone()), xcm_wrapper);
568
569			// check the full `send_xcm`
570			assert_eq!(send_xcm::<XcmBridgeHubRouter>(dest, xcm,), Err(SendError::NotApplicable),);
571		});
572	}
573
574	#[test]
575	fn exceeds_max_message_size_if_size_is_above_hard_limit() {
576		run_test(|| {
577			// routable dest with XCM version
578			let dest =
579				Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]);
580			// oversized XCM
581			let xcm: Xcm<()> = vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into();
582
583			// dest is routable with the inner router
584			assert_ok!(SovereignPaidRemoteExporter::<
585				Pallet<TestRuntime, ()>,
586				TestToBridgeHubSender,
587				UniversalLocation,
588			>::validate(&mut Some(dest.clone()), &mut Some(xcm.clone())));
589
590			// check for oversized message
591			let mut xcm_wrapper = Some(xcm.clone());
592			assert_eq!(
593				XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
594				Err(SendError::ExceedsMaxMessageSize),
595			);
596			// XCM is consumed by the inner router
597			assert!(xcm_wrapper.is_none());
598
599			// check the full `send_xcm`
600			assert_eq!(
601				send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
602				Err(SendError::ExceedsMaxMessageSize),
603			);
604		});
605	}
606
607	#[test]
608	fn destination_unsupported_if_wrap_version_fails() {
609		run_test(|| {
610			// routable dest but we don't know XCM version
611			let dest = UnknownXcmVersionForRoutableLocation::get();
612			let xcm: Xcm<()> = vec![ClearOrigin].into();
613
614			// dest is routable with the inner router
615			assert_ok!(SovereignPaidRemoteExporter::<
616				Pallet<TestRuntime, ()>,
617				TestToBridgeHubSender,
618				UniversalLocation,
619			>::validate(&mut Some(dest.clone()), &mut Some(xcm.clone())));
620
621			// check that it does not pass XCM version check
622			let mut xcm_wrapper = Some(xcm.clone());
623			assert_eq!(
624				XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
625				Err(SendError::DestinationUnsupported),
626			);
627			// XCM is consumed by the inner router
628			assert!(xcm_wrapper.is_none());
629
630			// check the full `send_xcm`
631			assert_eq!(
632				send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
633				Err(SendError::DestinationUnsupported),
634			);
635		});
636	}
637
638	#[test]
639	fn returns_proper_delivery_price() {
640		run_test(|| {
641			let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]);
642			let xcm: Xcm<()> = vec![ClearOrigin].into();
643			let msg_size = xcm.encoded_size();
644
645			// initially the base fee is used: `BASE_FEE + BYTE_FEE * msg_size + HRMP_FEE`
646			let expected_fee = BASE_FEE + BYTE_FEE * (msg_size as u128) + HRMP_FEE;
647			assert_eq!(
648				XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut Some(xcm.clone()))
649					.unwrap()
650					.1
651					.get(0),
652				Some(&(BridgeFeeAsset::get(), expected_fee).into()),
653			);
654
655			// but when factor is larger than one, it increases the fee, so it becomes:
656			// `(BASE_FEE + BYTE_FEE * msg_size) * F + HRMP_FEE`
657			let factor = FixedU128::from_rational(125, 100);
658			Bridge::<TestRuntime, ()>::put(uncongested_bridge(factor));
659			let expected_fee =
660				(FixedU128::saturating_from_integer(BASE_FEE + BYTE_FEE * (msg_size as u128)) *
661					factor)
662					.into_inner() / FixedU128::DIV +
663					HRMP_FEE;
664			assert_eq!(
665				XcmBridgeHubRouter::validate(&mut Some(dest), &mut Some(xcm)).unwrap().1.get(0),
666				Some(&(BridgeFeeAsset::get(), expected_fee).into()),
667			);
668		});
669	}
670
671	#[test]
672	fn sent_message_doesnt_increase_factor_if_queue_is_uncongested() {
673		run_test(|| {
674			let old_bridge = XcmBridgeHubRouter::bridge();
675			assert_eq!(
676				send_xcm::<XcmBridgeHubRouter>(
677					Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
678					vec![ClearOrigin].into(),
679				)
680				.map(drop),
681				Ok(()),
682			);
683
684			assert!(TestToBridgeHubSender::is_message_sent());
685			assert_eq!(old_bridge, XcmBridgeHubRouter::bridge());
686
687			assert_eq!(System::events(), vec![]);
688		});
689	}
690
691	#[test]
692	fn sent_message_increases_factor_if_xcm_channel_is_congested() {
693		run_test(|| {
694			TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
695
696			let old_bridge = XcmBridgeHubRouter::bridge();
697			assert_ok!(send_xcm::<XcmBridgeHubRouter>(
698				Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
699				vec![ClearOrigin].into(),
700			)
701			.map(drop));
702
703			assert!(TestToBridgeHubSender::is_message_sent());
704			assert!(
705				old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
706			);
707
708			// check emitted event
709			let first_system_event = System::events().first().cloned();
710			assert!(matches!(
711				first_system_event,
712				Some(EventRecord {
713					phase: Phase::Initialization,
714					event: RuntimeEvent::XcmBridgeHubRouter(
715						Event::DeliveryFeeFactorIncreased { .. }
716					),
717					..
718				})
719			));
720		});
721	}
722
723	#[test]
724	fn sent_message_increases_factor_if_bridge_has_reported_congestion() {
725		run_test(|| {
726			Bridge::<TestRuntime, ()>::put(congested_bridge(
727				Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR,
728			));
729
730			let old_bridge = XcmBridgeHubRouter::bridge();
731			assert_ok!(send_xcm::<XcmBridgeHubRouter>(
732				Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
733				vec![ClearOrigin].into(),
734			)
735			.map(drop));
736
737			assert!(TestToBridgeHubSender::is_message_sent());
738			assert!(
739				old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
740			);
741
742			// check emitted event
743			let first_system_event = System::events().first().cloned();
744			assert!(matches!(
745				first_system_event,
746				Some(EventRecord {
747					phase: Phase::Initialization,
748					event: RuntimeEvent::XcmBridgeHubRouter(
749						Event::DeliveryFeeFactorIncreased { .. }
750					),
751					..
752				})
753			));
754		});
755	}
756
757	#[test]
758	fn get_messages_does_not_return_anything() {
759		run_test(|| {
760			assert_ok!(send_xcm::<XcmBridgeHubRouter>(
761				(Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(),
762				vec![ClearOrigin].into()
763			));
764			assert_eq!(XcmBridgeHubRouter::get_messages(), vec![]);
765		});
766	}
767}