1#![cfg_attr(not(feature = "std"), no_std)]
32
33use bp_xcm_bridge_hub_router::MINIMAL_DELIVERY_FEE_FACTOR;
34pub use bp_xcm_bridge_hub_router::{BridgeState, XcmChannelStatusProvider};
35use codec::Encode;
36use frame_support::traits::Get;
37use polkadot_runtime_parachains::FeeTracker;
38use sp_core::H256;
39use sp_runtime::{FixedPointNumber, FixedU128};
40use sp_std::vec::Vec;
41use xcm::prelude::*;
42use xcm_builder::{ExporterFor, InspectMessageQueues, SovereignPaidRemoteExporter};
43
44pub use pallet::*;
45pub use weights::WeightInfo;
46
47pub mod benchmarking;
48pub mod weights;
49
50mod mock;
51
52pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024;
57
58pub const LOG_TARGET: &str = "xcm::bridge-hub-router";
65
66#[frame_support::pallet]
67pub mod pallet {
68 use super::*;
69 use frame_support::pallet_prelude::*;
70 use frame_system::pallet_prelude::*;
71
72 #[pallet::config]
73 pub trait Config<I: 'static = ()>: frame_system::Config {
74 #[allow(deprecated)]
76 type RuntimeEvent: From<Event<Self, I>>
77 + IsType<<Self as frame_system::Config>::RuntimeEvent>;
78 type WeightInfo: WeightInfo;
80
81 type UniversalLocation: Get<InteriorLocation>;
83 type SiblingBridgeHubLocation: Get<Location>;
85 type BridgedNetworkId: Get<Option<NetworkId>>;
89 type Bridges: ExporterFor;
93 type DestinationVersion: GetVersion;
95
96 type BridgeHubOrigin: EnsureOrigin<Self::RuntimeOrigin>;
98 type ToBridgeHubSender: SendXcm;
100 type LocalXcmChannelManager: XcmChannelStatusProvider;
102
103 type ByteFee: Get<u128>;
105 type FeeAsset: Get<AssetId>;
107 }
108
109 #[pallet::pallet]
110 pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
111
112 #[pallet::hooks]
113 impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
114 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
115 if T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get()) {
117 return T::WeightInfo::on_initialize_when_congested();
118 }
119
120 let mut bridge = Self::bridge();
122 if bridge.is_congested {
123 return T::WeightInfo::on_initialize_when_congested();
124 }
125
126 let previous_factor = Self::get_fee_factor(());
127 if !Self::do_decrease_fee_factor(&mut bridge.delivery_fee_factor) {
129 return T::WeightInfo::on_initialize_when_congested();
130 }
131
132 tracing::info!(
133 target: LOG_TARGET,
134 from=%previous_factor,
135 to=%bridge.delivery_fee_factor,
136 "Bridge channel is uncongested. Decreased fee factor"
137 );
138 Self::deposit_event(Event::DeliveryFeeFactorDecreased {
139 new_value: bridge.delivery_fee_factor,
140 });
141
142 Bridge::<T, I>::put(bridge);
143
144 T::WeightInfo::on_initialize_when_non_congested()
145 }
146 }
147
148 #[pallet::call]
149 impl<T: Config<I>, I: 'static> Pallet<T, I> {
150 #[pallet::call_index(0)]
152 #[pallet::weight(T::WeightInfo::report_bridge_status())]
153 pub fn report_bridge_status(
154 origin: OriginFor<T>,
155 bridge_id: H256,
158 is_congested: bool,
159 ) -> DispatchResult {
160 T::BridgeHubOrigin::ensure_origin(origin)?;
161
162 tracing::info!(
163 target: LOG_TARGET,
164 from=?bridge_id,
165 congested=%is_congested,
166 "Received bridge status"
167 );
168
169 Bridge::<T, I>::mutate(|bridge| {
170 bridge.is_congested = is_congested;
171 });
172 Ok(())
173 }
174 }
175
176 #[pallet::storage]
184 pub type Bridge<T: Config<I>, I: 'static = ()> = StorageValue<_, BridgeState, ValueQuery>;
185
186 impl<T: Config<I>, I: 'static> Pallet<T, I> {
187 pub fn bridge() -> BridgeState {
189 Bridge::<T, I>::get()
190 }
191
192 pub(crate) fn on_message_sent_to_bridge(message_size: u32) {
194 tracing::trace!(
195 target: LOG_TARGET,
196 ?message_size, "on_message_sent_to_bridge"
197 );
198 let _ = Bridge::<T, I>::try_mutate(|bridge| {
199 let is_channel_with_bridge_hub_congested =
200 T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get());
201 let is_bridge_congested = bridge.is_congested;
202
203 if !is_channel_with_bridge_hub_congested && !is_bridge_congested {
206 return Err(());
207 }
208
209 let previous_factor = Self::get_fee_factor(());
210 <Self as FeeTracker>::do_increase_fee_factor(
212 &mut bridge.delivery_fee_factor,
213 message_size as u128,
214 );
215
216 tracing::info!(
217 target: LOG_TARGET,
218 from=%previous_factor,
219 to=%bridge.delivery_fee_factor,
220 "Bridge channel is congested. Increased fee factor"
221 );
222 Self::deposit_event(Event::DeliveryFeeFactorIncreased {
223 new_value: bridge.delivery_fee_factor,
224 });
225 Ok(())
226 });
227 }
228 }
229
230 #[pallet::event]
231 #[pallet::generate_deposit(pub(super) fn deposit_event)]
232 pub enum Event<T: Config<I>, I: 'static = ()> {
233 DeliveryFeeFactorDecreased {
235 new_value: FixedU128,
237 },
238 DeliveryFeeFactorIncreased {
240 new_value: FixedU128,
242 },
243 }
244}
245
246type ViaBridgeHubExporter<T, I> = SovereignPaidRemoteExporter<
249 Pallet<T, I>,
250 <T as Config<I>>::ToBridgeHubSender,
251 <T as Config<I>>::UniversalLocation,
252>;
253
254impl<T: Config<I>, I: 'static> ExporterFor for Pallet<T, I> {
257 fn exporter_for(
258 network: &NetworkId,
259 remote_location: &InteriorLocation,
260 message: &Xcm<()>,
261 ) -> Option<(Location, Option<Asset>)> {
262 tracing::trace!(
263 target: LOG_TARGET,
264 ?network, ?remote_location, msg=?message, "exporter_for"
265 );
266 if let Some(bridged_network) = T::BridgedNetworkId::get() {
268 if *network != bridged_network {
269 tracing::trace!(
270 target: LOG_TARGET,
271 bridged_network_id=?bridged_network, ?network, "Router does not support bridging!"
272 );
273 return None;
274 }
275 }
276
277 let (bridge_hub_location, maybe_payment) =
279 match T::Bridges::exporter_for(network, remote_location, message) {
280 Some((bridge_hub_location, maybe_payment))
281 if bridge_hub_location.eq(&T::SiblingBridgeHubLocation::get()) =>
282 (bridge_hub_location, maybe_payment),
283 _ => {
284 tracing::trace!(
285 target: LOG_TARGET,
286 bridged_network_id=?T::BridgedNetworkId::get(),
287 sibling_bridge_hub_location=?T::SiblingBridgeHubLocation::get(),
288 ?network,
289 ?remote_location,
290 "Router configured does not support bridging!"
291 );
292 return None;
293 },
294 };
295
296 let base_fee = match maybe_payment {
298 Some(payment) => match payment {
299 Asset { fun: Fungible(amount), id } if id.eq(&T::FeeAsset::get()) => amount,
300 invalid_asset => {
301 tracing::error!(
302 target: LOG_TARGET,
303 bridged_network_id=?T::BridgedNetworkId::get(),
304 fee_asset=?T::FeeAsset::get(),
305 with=?invalid_asset,
306 ?bridge_hub_location,
307 ?network,
308 ?remote_location,
309 "Router is configured for `T::FeeAsset` which is not compatible for bridging!"
310 );
311 return None;
312 },
313 },
314 None => 0,
315 };
316
317 let message_size = message.encoded_size();
321 let message_fee = (message_size as u128).saturating_mul(T::ByteFee::get());
322 let fee_sum = base_fee.saturating_add(message_fee);
323 let fee_factor = Self::get_fee_factor(());
324 let fee = fee_factor.saturating_mul_int(fee_sum);
325
326 let fee = if fee > 0 { Some((T::FeeAsset::get(), fee).into()) } else { None };
327
328 tracing::info!(
329 target: LOG_TARGET,
330 to=?(network, remote_location),
331 bridge_fee=?fee,
332 %fee_factor,
333 "Going to send message ({message_size} bytes) over bridge."
334 );
335
336 Some((bridge_hub_location, fee))
337 }
338}
339
340impl<T: Config<I>, I: 'static> SendXcm for Pallet<T, I> {
344 type Ticket = (u32, <T::ToBridgeHubSender as SendXcm>::Ticket);
345
346 fn validate(
347 dest: &mut Option<Location>,
348 xcm: &mut Option<Xcm<()>>,
349 ) -> SendResult<Self::Ticket> {
350 tracing::trace!(target: LOG_TARGET, msg=?xcm, destination=?dest, "validate");
351
352 let xcm_to_dest_clone = xcm.clone();
356 let dest_clone = dest.clone();
357
358 match ViaBridgeHubExporter::<T, I>::validate(dest, xcm) {
365 Ok((ticket, cost)) => {
366 let xcm_to_dest_clone = xcm_to_dest_clone.ok_or(SendError::MissingArgument)?;
369 let dest_clone = dest_clone.ok_or(SendError::MissingArgument)?;
370
371 let message_size = xcm_to_dest_clone.encoded_size() as _;
375
376 if message_size > HARD_MESSAGE_SIZE_LIMIT {
380 return Err(SendError::ExceedsMaxMessageSize);
381 }
382
383 let destination_version = T::DestinationVersion::get_version_for(&dest_clone)
391 .ok_or(SendError::DestinationUnsupported)?;
392 VersionedXcm::from(xcm_to_dest_clone)
393 .into_version(destination_version)
394 .map_err(|()| SendError::DestinationUnsupported)?;
395
396 Ok(((message_size, ticket), cost))
397 },
398 Err(e) => {
399 tracing::trace!(target: LOG_TARGET, error=?e, "validate - ViaBridgeHubExporter");
400 Err(e)
401 },
402 }
403 }
404
405 fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
406 let (message_size, ticket) = ticket;
409 let xcm_hash = ViaBridgeHubExporter::<T, I>::deliver(ticket)?;
410
411 Self::on_message_sent_to_bridge(message_size);
413
414 tracing::trace!(target: LOG_TARGET, ?xcm_hash, "deliver - message sent");
415 Ok(xcm_hash)
416 }
417}
418
419impl<T: Config<I>, I: 'static> InspectMessageQueues for Pallet<T, I> {
420 fn clear_messages() {}
421
422 fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
425 Vec::new()
426 }
427}
428
429impl<T: Config<I>, I: 'static> FeeTracker for Pallet<T, I> {
430 type Id = ();
431
432 const MIN_FEE_FACTOR: FixedU128 = MINIMAL_DELIVERY_FEE_FACTOR;
433
434 fn get_fee_factor(_id: Self::Id) -> FixedU128 {
435 Self::bridge().delivery_fee_factor
436 }
437
438 fn set_fee_factor(_id: Self::Id, val: FixedU128) {
439 let mut bridge = Self::bridge();
440 bridge.delivery_fee_factor = val;
441 Bridge::<T, I>::put(bridge);
442 }
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use frame_support::assert_ok;
449 use mock::*;
450
451 use frame_support::traits::Hooks;
452 use frame_system::{EventRecord, Phase};
453 use sp_runtime::traits::One;
454
455 fn congested_bridge(delivery_fee_factor: FixedU128) -> BridgeState {
456 BridgeState { is_congested: true, delivery_fee_factor }
457 }
458
459 fn uncongested_bridge(delivery_fee_factor: FixedU128) -> BridgeState {
460 BridgeState { is_congested: false, delivery_fee_factor }
461 }
462
463 #[test]
464 fn initial_fee_factor_is_one() {
465 run_test(|| {
466 assert_eq!(
467 Bridge::<TestRuntime, ()>::get(),
468 uncongested_bridge(Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR),
469 );
470 })
471 }
472
473 #[test]
474 fn fee_factor_is_not_decreased_from_on_initialize_when_xcm_channel_is_congested() {
475 run_test(|| {
476 Bridge::<TestRuntime, ()>::put(uncongested_bridge(FixedU128::from_rational(125, 100)));
477 TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
478
479 let old_delivery = XcmBridgeHubRouter::bridge();
481 XcmBridgeHubRouter::on_initialize(One::one());
482 assert_eq!(XcmBridgeHubRouter::bridge(), old_delivery);
483 assert_eq!(System::events(), vec![]);
484 })
485 }
486
487 #[test]
488 fn fee_factor_is_not_decreased_from_on_initialize_when_bridge_has_reported_congestion() {
489 run_test(|| {
490 Bridge::<TestRuntime, ()>::put(congested_bridge(FixedU128::from_rational(125, 100)));
491
492 let old_bridge = XcmBridgeHubRouter::bridge();
494 XcmBridgeHubRouter::on_initialize(One::one());
495 assert_eq!(XcmBridgeHubRouter::bridge(), old_bridge);
496 assert_eq!(System::events(), vec![]);
497 })
498 }
499
500 #[test]
501 fn fee_factor_is_decreased_from_on_initialize_when_xcm_channel_is_uncongested() {
502 run_test(|| {
503 let initial_fee_factor = FixedU128::from_rational(125, 100);
504 Bridge::<TestRuntime, ()>::put(uncongested_bridge(initial_fee_factor));
505
506 while XcmBridgeHubRouter::bridge().delivery_fee_factor >
508 Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR
509 {
510 XcmBridgeHubRouter::on_initialize(One::one());
511 }
512
513 XcmBridgeHubRouter::on_initialize(One::one());
515 assert_eq!(
516 XcmBridgeHubRouter::bridge(),
517 uncongested_bridge(Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR)
518 );
519
520 let first_system_event = System::events().first().cloned();
522 assert_eq!(
523 first_system_event,
524 Some(EventRecord {
525 phase: Phase::Initialization,
526 event: RuntimeEvent::XcmBridgeHubRouter(Event::DeliveryFeeFactorDecreased {
527 new_value: initial_fee_factor / XcmBridgeHubRouter::EXPONENTIAL_FEE_BASE,
528 }),
529 topics: vec![],
530 })
531 );
532 })
533 }
534
535 #[test]
536 fn not_applicable_if_destination_is_within_other_network() {
537 run_test(|| {
538 let dest = Location::new(2, [GlobalConsensus(ByGenesis([0; 32])), Parachain(1000)]);
540 let xcm: Xcm<()> = vec![ClearOrigin].into();
541
542 let mut xcm_wrapper = Some(xcm.clone());
544 assert_eq!(
545 XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
546 Err(SendError::NotApplicable),
547 );
548 assert_eq!(Some(xcm.clone()), xcm_wrapper);
550
551 assert_eq!(send_xcm::<XcmBridgeHubRouter>(dest, xcm,), Err(SendError::NotApplicable),);
553 });
554 }
555
556 #[test]
557 fn exceeds_max_message_size_if_size_is_above_hard_limit() {
558 run_test(|| {
559 let dest =
561 Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]);
562 let xcm: Xcm<()> = vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into();
564
565 assert_ok!(ViaBridgeHubExporter::<TestRuntime, ()>::validate(
567 &mut Some(dest.clone()),
568 &mut Some(xcm.clone())
569 ));
570
571 let mut xcm_wrapper = Some(xcm.clone());
573 assert_eq!(
574 XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
575 Err(SendError::ExceedsMaxMessageSize),
576 );
577 assert!(xcm_wrapper.is_none());
579
580 assert_eq!(
582 send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
583 Err(SendError::ExceedsMaxMessageSize),
584 );
585 });
586 }
587
588 #[test]
589 fn destination_unsupported_if_wrap_version_fails() {
590 run_test(|| {
591 let dest = UnknownXcmVersionForRoutableLocation::get();
593 let xcm: Xcm<()> = vec![ClearOrigin].into();
594
595 assert_ok!(ViaBridgeHubExporter::<TestRuntime, ()>::validate(
597 &mut Some(dest.clone()),
598 &mut Some(xcm.clone())
599 ));
600
601 let mut xcm_wrapper = Some(xcm.clone());
603 assert_eq!(
604 XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
605 Err(SendError::DestinationUnsupported),
606 );
607 assert!(xcm_wrapper.is_none());
609
610 assert_eq!(
612 send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
613 Err(SendError::DestinationUnsupported),
614 );
615 });
616 }
617
618 #[test]
619 fn returns_proper_delivery_price() {
620 run_test(|| {
621 let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]);
622 let xcm: Xcm<()> = vec![ClearOrigin].into();
623 let msg_size = xcm.encoded_size();
624
625 let expected_fee = BASE_FEE + BYTE_FEE * (msg_size as u128) + HRMP_FEE;
627 assert_eq!(
628 XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut Some(xcm.clone()))
629 .unwrap()
630 .1
631 .get(0),
632 Some(&(BridgeFeeAsset::get(), expected_fee).into()),
633 );
634
635 let factor = FixedU128::from_rational(125, 100);
638 Bridge::<TestRuntime, ()>::put(uncongested_bridge(factor));
639 let expected_fee =
640 (FixedU128::saturating_from_integer(BASE_FEE + BYTE_FEE * (msg_size as u128)) *
641 factor)
642 .into_inner() / FixedU128::DIV +
643 HRMP_FEE;
644 assert_eq!(
645 XcmBridgeHubRouter::validate(&mut Some(dest), &mut Some(xcm)).unwrap().1.get(0),
646 Some(&(BridgeFeeAsset::get(), expected_fee).into()),
647 );
648 });
649 }
650
651 #[test]
652 fn sent_message_doesnt_increase_factor_if_queue_is_uncongested() {
653 run_test(|| {
654 let old_bridge = XcmBridgeHubRouter::bridge();
655 assert_eq!(
656 send_xcm::<XcmBridgeHubRouter>(
657 Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
658 vec![ClearOrigin].into(),
659 )
660 .map(drop),
661 Ok(()),
662 );
663
664 assert!(TestToBridgeHubSender::is_message_sent());
665 assert_eq!(old_bridge, XcmBridgeHubRouter::bridge());
666
667 assert_eq!(System::events(), vec![]);
668 });
669 }
670
671 #[test]
672 fn sent_message_increases_factor_if_xcm_channel_is_congested() {
673 run_test(|| {
674 TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
675
676 let old_bridge = XcmBridgeHubRouter::bridge();
677 assert_ok!(send_xcm::<XcmBridgeHubRouter>(
678 Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
679 vec![ClearOrigin].into(),
680 )
681 .map(drop));
682
683 assert!(TestToBridgeHubSender::is_message_sent());
684 assert!(
685 old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
686 );
687
688 let first_system_event = System::events().first().cloned();
690 assert!(matches!(
691 first_system_event,
692 Some(EventRecord {
693 phase: Phase::Initialization,
694 event: RuntimeEvent::XcmBridgeHubRouter(
695 Event::DeliveryFeeFactorIncreased { .. }
696 ),
697 ..
698 })
699 ));
700 });
701 }
702
703 #[test]
704 fn sent_message_increases_factor_if_bridge_has_reported_congestion() {
705 run_test(|| {
706 Bridge::<TestRuntime, ()>::put(congested_bridge(
707 Pallet::<TestRuntime, ()>::MIN_FEE_FACTOR,
708 ));
709
710 let old_bridge = XcmBridgeHubRouter::bridge();
711 assert_ok!(send_xcm::<XcmBridgeHubRouter>(
712 Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
713 vec![ClearOrigin].into(),
714 )
715 .map(drop));
716
717 assert!(TestToBridgeHubSender::is_message_sent());
718 assert!(
719 old_bridge.delivery_fee_factor < XcmBridgeHubRouter::bridge().delivery_fee_factor
720 );
721
722 let first_system_event = System::events().first().cloned();
724 assert!(matches!(
725 first_system_event,
726 Some(EventRecord {
727 phase: Phase::Initialization,
728 event: RuntimeEvent::XcmBridgeHubRouter(
729 Event::DeliveryFeeFactorIncreased { .. }
730 ),
731 ..
732 })
733 ));
734 });
735 }
736
737 #[test]
738 fn get_messages_does_not_return_anything() {
739 run_test(|| {
740 assert_ok!(send_xcm::<XcmBridgeHubRouter>(
741 (Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(),
742 vec![ClearOrigin].into()
743 ));
744 assert_eq!(XcmBridgeHubRouter::get_messages(), vec![]);
745 });
746 }
747}