1#![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
59pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024;
64
65pub 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 #[allow(deprecated)]
83 type RuntimeEvent: From<Event<Self, I>>
84 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
85 type WeightInfo: WeightInfo;
87
88 type UniversalLocation: Get<InteriorLocation>;
90 type SiblingBridgeHubLocation: Get<Location>;
92 type BridgedNetworkId: Get<Option<NetworkId>>;
96 type Bridges: ExporterFor;
100 type DestinationVersion: GetVersion;
102
103 type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
105 type ToBridgeHubSender: SendXcm;
107 type LocalXcmChannelManager: XcmChannelStatusProvider;
109
110 type UnpaidExport: Get<bool>;
114
115 type ByteFee: Get<u128>;
117 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 T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get()) {
129 return T::WeightInfo::on_initialize_when_congested();
130 }
131
132 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 !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 #[pallet::call_index(0)]
164 #[pallet::weight(T::WeightInfo::report_bridge_status())]
165 pub fn report_bridge_status(
166 origin: OriginFor<T>,
167 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 #[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 pub fn bridge() -> BridgeState {
201 Bridge::<T, I>::get()
202 }
203
204 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 !is_channel_with_bridge_hub_congested && !is_bridge_congested {
218 return Err(());
219 }
220
221 let previous_factor = Self::get_fee_factor(());
222 <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 DeliveryFeeFactorDecreased {
247 new_value: FixedU128,
249 },
250 DeliveryFeeFactorIncreased {
252 new_value: FixedU128,
254 },
255 }
256}
257
258impl<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 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 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 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 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
346impl<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 let xcm_to_dest_clone = xcm.clone();
362 let dest_clone = dest.clone();
363
364 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 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 let message_size = xcm_to_dest_clone.encoded_size() as _;
393
394 if message_size > HARD_MESSAGE_SIZE_LIMIT {
398 return Err(SendError::ExceedsMaxMessageSize);
399 }
400
401 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 let (message_size, ticket) = ticket;
427 let xcm_hash = T::ToBridgeHubSender::deliver(ticket)?;
428
429 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 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 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 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 while XcmBridgeHubRouter::bridge().delivery_fee_factor >
526 Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR
527 {
528 XcmBridgeHubRouter::on_initialize(One::one());
529 }
530
531 XcmBridgeHubRouter::on_initialize(One::one());
533 assert_eq!(
534 XcmBridgeHubRouter::bridge(),
535 uncongested_bridge(Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR)
536 );
537
538 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 let dest = Location::new(2, [GlobalConsensus(ByGenesis([0; 32])), Parachain(1000)]);
558 let xcm: Xcm<()> = vec![ClearOrigin].into();
559
560 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 assert_eq!(Some(xcm.clone()), xcm_wrapper);
568
569 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 let dest =
579 Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]);
580 let xcm: Xcm<()> = vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into();
582
583 assert_ok!(SovereignPaidRemoteExporter::<
585 Pallet<TestRuntime, ()>,
586 TestToBridgeHubSender,
587 UniversalLocation,
588 >::validate(&mut Some(dest.clone()), &mut Some(xcm.clone())));
589
590 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 assert!(xcm_wrapper.is_none());
598
599 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 let dest = UnknownXcmVersionForRoutableLocation::get();
612 let xcm: Xcm<()> = vec![ClearOrigin].into();
613
614 assert_ok!(SovereignPaidRemoteExporter::<
616 Pallet<TestRuntime, ()>,
617 TestToBridgeHubSender,
618 UniversalLocation,
619 >::validate(&mut Some(dest.clone()), &mut Some(xcm.clone())));
620
621 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 assert!(xcm_wrapper.is_none());
629
630 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 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 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 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 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}