#![cfg_attr(not(feature = "std"), no_std)]
pub use bp_xcm_bridge_hub_router::XcmChannelStatusProvider;
use codec::Encode;
use frame_support::traits::Get;
use sp_runtime::{FixedPointNumber, FixedU128, Saturating};
use sp_std::vec::Vec;
use xcm::prelude::*;
use xcm_builder::{ExporterFor, InspectMessageQueues, SovereignPaidRemoteExporter};
pub use pallet::*;
pub use weights::WeightInfo;
pub mod benchmarking;
pub mod weights;
mod mock;
pub const MINIMAL_DELIVERY_FEE_FACTOR: FixedU128 = FixedU128::from_u32(1);
const EXPONENTIAL_FEE_BASE: FixedU128 = FixedU128::from_rational(105, 100); const MESSAGE_SIZE_FEE_BASE: FixedU128 = FixedU128::from_rational(1, 1000); pub const HARD_MESSAGE_SIZE_LIMIT: u32 = 32 * 1024;
pub const LOG_TARGET: &str = "xcm::bridge-hub-router";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
#[pallet::config]
pub trait Config<I: 'static = ()>: frame_system::Config {
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: WeightInfo;
type UniversalLocation: Get<InteriorLocation>;
type SiblingBridgeHubLocation: Get<Location>;
type BridgedNetworkId: Get<Option<NetworkId>>;
type Bridges: ExporterFor;
type DestinationVersion: GetVersion;
type ToBridgeHubSender: SendXcm;
type LocalXcmChannelManager: XcmChannelStatusProvider;
type ByteFee: Get<u128>;
type FeeAsset: Get<AssetId>;
}
#[pallet::pallet]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
if T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get()) {
return T::WeightInfo::on_initialize_when_congested()
}
let mut delivery_fee_factor = Self::delivery_fee_factor();
if delivery_fee_factor == MINIMAL_DELIVERY_FEE_FACTOR {
return T::WeightInfo::on_initialize_when_congested()
}
let previous_factor = delivery_fee_factor;
delivery_fee_factor =
MINIMAL_DELIVERY_FEE_FACTOR.max(delivery_fee_factor / EXPONENTIAL_FEE_BASE);
log::info!(
target: LOG_TARGET,
"Bridge channel is uncongested. Decreased fee factor from {} to {}",
previous_factor,
delivery_fee_factor,
);
Self::deposit_event(Event::DeliveryFeeFactorDecreased {
new_value: delivery_fee_factor,
});
DeliveryFeeFactor::<T, I>::put(delivery_fee_factor);
T::WeightInfo::on_initialize_when_non_congested()
}
}
#[pallet::type_value]
pub fn InitialFactor() -> FixedU128 {
MINIMAL_DELIVERY_FEE_FACTOR
}
#[pallet::storage]
#[pallet::getter(fn delivery_fee_factor)]
pub type DeliveryFeeFactor<T: Config<I>, I: 'static = ()> =
StorageValue<_, FixedU128, ValueQuery, InitialFactor>;
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub(crate) fn on_message_sent_to_bridge(message_size: u32) {
if !T::LocalXcmChannelManager::is_congested(&T::SiblingBridgeHubLocation::get()) {
return
}
let message_size_factor = FixedU128::from_u32(message_size.saturating_div(1024))
.saturating_mul(MESSAGE_SIZE_FEE_BASE);
let total_factor = EXPONENTIAL_FEE_BASE.saturating_add(message_size_factor);
DeliveryFeeFactor::<T, I>::mutate(|f| {
let previous_factor = *f;
*f = f.saturating_mul(total_factor);
log::info!(
target: LOG_TARGET,
"Bridge channel is congested. Increased fee factor from {} to {}",
previous_factor,
f,
);
Self::deposit_event(Event::DeliveryFeeFactorIncreased { new_value: *f });
*f
});
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
DeliveryFeeFactorDecreased {
new_value: FixedU128,
},
DeliveryFeeFactorIncreased {
new_value: FixedU128,
},
}
}
type ViaBridgeHubExporter<T, I> = SovereignPaidRemoteExporter<
Pallet<T, I>,
<T as Config<I>>::ToBridgeHubSender,
<T as Config<I>>::UniversalLocation,
>;
impl<T: Config<I>, I: 'static> ExporterFor for Pallet<T, I> {
fn exporter_for(
network: &NetworkId,
remote_location: &InteriorLocation,
message: &Xcm<()>,
) -> Option<(Location, Option<Asset>)> {
log::trace!(
target: LOG_TARGET,
"exporter_for - network: {network:?}, remote_location: {remote_location:?}, msg: {message:?}",
);
if let Some(bridged_network) = T::BridgedNetworkId::get() {
if *network != bridged_network {
log::trace!(
target: LOG_TARGET,
"Router with bridged_network_id {bridged_network:?} does not support bridging to network {network:?}!",
);
return None
}
}
let (bridge_hub_location, maybe_payment) = match T::Bridges::exporter_for(
network,
remote_location,
message,
) {
Some((bridge_hub_location, maybe_payment))
if bridge_hub_location.eq(&T::SiblingBridgeHubLocation::get()) =>
(bridge_hub_location, maybe_payment),
_ => {
log::trace!(
target: LOG_TARGET,
"Router configured with bridged_network_id {:?} and sibling_bridge_hub_location: {:?} does not support bridging to network {:?} and remote_location {:?}!",
T::BridgedNetworkId::get(),
T::SiblingBridgeHubLocation::get(),
network,
remote_location,
);
return None
},
};
let base_fee = match maybe_payment {
Some(payment) => match payment {
Asset { fun: Fungible(amount), id } if id.eq(&T::FeeAsset::get()) => amount,
invalid_asset => {
log::error!(
target: LOG_TARGET,
"Router with bridged_network_id {:?} is configured for `T::FeeAsset` {:?} \
which is not compatible with {:?} for bridge_hub_location: {:?} for bridging to {:?}/{:?}!",
T::BridgedNetworkId::get(),
T::FeeAsset::get(),
invalid_asset,
bridge_hub_location,
network,
remote_location,
);
return None
},
},
None => 0,
};
let message_size = message.encoded_size();
let message_fee = (message_size as u128).saturating_mul(T::ByteFee::get());
let fee_sum = base_fee.saturating_add(message_fee);
let fee_factor = Self::delivery_fee_factor();
let fee = fee_factor.saturating_mul_int(fee_sum);
let fee = if fee > 0 { Some((T::FeeAsset::get(), fee).into()) } else { None };
log::info!(
target: LOG_TARGET,
"Going to send message to {:?} ({} bytes) over bridge. Computed bridge fee {:?} using fee factor {}",
(network, remote_location),
message_size,
fee,
fee_factor,
);
Some((bridge_hub_location, fee))
}
}
impl<T: Config<I>, I: 'static> SendXcm for Pallet<T, I> {
type Ticket = (u32, <T::ToBridgeHubSender as SendXcm>::Ticket);
fn validate(
dest: &mut Option<Location>,
xcm: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
log::trace!(target: LOG_TARGET, "validate - msg: {xcm:?}, destination: {dest:?}");
let xcm_to_dest_clone = xcm.clone();
let dest_clone = dest.clone();
match ViaBridgeHubExporter::<T, I>::validate(dest, xcm) {
Ok((ticket, cost)) => {
let xcm_to_dest_clone = xcm_to_dest_clone.ok_or(SendError::MissingArgument)?;
let dest_clone = dest_clone.ok_or(SendError::MissingArgument)?;
let message_size = xcm_to_dest_clone.encoded_size() as _;
if message_size > HARD_MESSAGE_SIZE_LIMIT {
return Err(SendError::ExceedsMaxMessageSize)
}
let destination_version = T::DestinationVersion::get_version_for(&dest_clone)
.ok_or(SendError::DestinationUnsupported)?;
let _ = VersionedXcm::from(xcm_to_dest_clone)
.into_version(destination_version)
.map_err(|()| SendError::DestinationUnsupported)?;
Ok(((message_size, ticket), cost))
},
Err(e) => {
log::trace!(target: LOG_TARGET, "validate - ViaBridgeHubExporter - error: {e:?}");
Err(e)
},
}
}
fn deliver(ticket: Self::Ticket) -> Result<XcmHash, SendError> {
let (message_size, ticket) = ticket;
let xcm_hash = ViaBridgeHubExporter::<T, I>::deliver(ticket)?;
Self::on_message_sent_to_bridge(message_size);
log::trace!(target: LOG_TARGET, "deliver - message sent, xcm_hash: {xcm_hash:?}");
Ok(xcm_hash)
}
}
impl<T: Config<I>, I: 'static> InspectMessageQueues for Pallet<T, I> {
fn clear_messages() {}
fn get_messages() -> Vec<(VersionedLocation, Vec<VersionedXcm<()>>)> {
Vec::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use frame_support::assert_ok;
use mock::*;
use frame_support::traits::Hooks;
use frame_system::{EventRecord, Phase};
use sp_runtime::traits::One;
#[test]
fn initial_fee_factor_is_one() {
run_test(|| {
assert_eq!(DeliveryFeeFactor::<TestRuntime, ()>::get(), MINIMAL_DELIVERY_FEE_FACTOR);
})
}
#[test]
fn fee_factor_is_not_decreased_from_on_initialize_when_xcm_channel_is_congested() {
run_test(|| {
DeliveryFeeFactor::<TestRuntime, ()>::put(FixedU128::from_rational(125, 100));
TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
let old_delivery_fee_factor = XcmBridgeHubRouter::delivery_fee_factor();
XcmBridgeHubRouter::on_initialize(One::one());
assert_eq!(XcmBridgeHubRouter::delivery_fee_factor(), old_delivery_fee_factor);
assert_eq!(System::events(), vec![]);
})
}
#[test]
fn fee_factor_is_decreased_from_on_initialize_when_xcm_channel_is_uncongested() {
run_test(|| {
let initial_fee_factor = FixedU128::from_rational(125, 100);
DeliveryFeeFactor::<TestRuntime, ()>::put(initial_fee_factor);
while XcmBridgeHubRouter::delivery_fee_factor() > MINIMAL_DELIVERY_FEE_FACTOR {
XcmBridgeHubRouter::on_initialize(One::one());
}
XcmBridgeHubRouter::on_initialize(One::one());
assert_eq!(XcmBridgeHubRouter::delivery_fee_factor(), MINIMAL_DELIVERY_FEE_FACTOR);
let first_system_event = System::events().first().cloned();
assert_eq!(
first_system_event,
Some(EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmBridgeHubRouter(Event::DeliveryFeeFactorDecreased {
new_value: initial_fee_factor / EXPONENTIAL_FEE_BASE,
}),
topics: vec![],
})
);
})
}
#[test]
fn not_applicable_if_destination_is_within_other_network() {
run_test(|| {
let dest = Location::new(2, [GlobalConsensus(ByGenesis([0; 32])), Parachain(1000)]);
let xcm: Xcm<()> = vec![ClearOrigin].into();
let mut xcm_wrapper = Some(xcm.clone());
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
Err(SendError::NotApplicable),
);
assert_eq!(Some(xcm.clone()), xcm_wrapper);
assert_eq!(send_xcm::<XcmBridgeHubRouter>(dest, xcm,), Err(SendError::NotApplicable),);
});
}
#[test]
fn exceeds_max_message_size_if_size_is_above_hard_limit() {
run_test(|| {
let dest =
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]);
let xcm: Xcm<()> = vec![ClearOrigin; HARD_MESSAGE_SIZE_LIMIT as usize].into();
assert_ok!(ViaBridgeHubExporter::<TestRuntime, ()>::validate(
&mut Some(dest.clone()),
&mut Some(xcm.clone())
));
let mut xcm_wrapper = Some(xcm.clone());
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
Err(SendError::ExceedsMaxMessageSize),
);
assert!(xcm_wrapper.is_none());
assert_eq!(
send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
Err(SendError::ExceedsMaxMessageSize),
);
});
}
#[test]
fn destination_unsupported_if_wrap_version_fails() {
run_test(|| {
let dest = UnknownXcmVersionForRoutableLocation::get();
let xcm: Xcm<()> = vec![ClearOrigin].into();
assert_ok!(ViaBridgeHubExporter::<TestRuntime, ()>::validate(
&mut Some(dest.clone()),
&mut Some(xcm.clone())
));
let mut xcm_wrapper = Some(xcm.clone());
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut xcm_wrapper),
Err(SendError::DestinationUnsupported),
);
assert!(xcm_wrapper.is_none());
assert_eq!(
send_xcm::<XcmBridgeHubRouter>(dest, xcm,),
Err(SendError::DestinationUnsupported),
);
});
}
#[test]
fn returns_proper_delivery_price() {
run_test(|| {
let dest = Location::new(2, [GlobalConsensus(BridgedNetworkId::get())]);
let xcm: Xcm<()> = vec![ClearOrigin].into();
let msg_size = xcm.encoded_size();
let expected_fee = BASE_FEE + BYTE_FEE * (msg_size as u128) + HRMP_FEE;
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest.clone()), &mut Some(xcm.clone()))
.unwrap()
.1
.get(0),
Some(&(BridgeFeeAsset::get(), expected_fee).into()),
);
let factor = FixedU128::from_rational(125, 100);
DeliveryFeeFactor::<TestRuntime, ()>::put(factor);
let expected_fee =
(FixedU128::saturating_from_integer(BASE_FEE + BYTE_FEE * (msg_size as u128)) *
factor)
.into_inner() / FixedU128::DIV +
HRMP_FEE;
assert_eq!(
XcmBridgeHubRouter::validate(&mut Some(dest), &mut Some(xcm)).unwrap().1.get(0),
Some(&(BridgeFeeAsset::get(), expected_fee).into()),
);
});
}
#[test]
fn sent_message_doesnt_increase_factor_if_queue_is_uncongested() {
run_test(|| {
let old_delivery_fee_factor = XcmBridgeHubRouter::delivery_fee_factor();
assert_eq!(
send_xcm::<XcmBridgeHubRouter>(
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
vec![ClearOrigin].into(),
)
.map(drop),
Ok(()),
);
assert!(TestToBridgeHubSender::is_message_sent());
assert_eq!(old_delivery_fee_factor, XcmBridgeHubRouter::delivery_fee_factor());
assert_eq!(System::events(), vec![]);
});
}
#[test]
fn sent_message_increases_factor_if_xcm_channel_is_congested() {
run_test(|| {
TestLocalXcmChannelManager::make_congested(&SiblingBridgeHubLocation::get());
let old_delivery_fee_factor = XcmBridgeHubRouter::delivery_fee_factor();
assert_ok!(send_xcm::<XcmBridgeHubRouter>(
Location::new(2, [GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)]),
vec![ClearOrigin].into(),
)
.map(drop));
assert!(TestToBridgeHubSender::is_message_sent());
assert!(old_delivery_fee_factor < XcmBridgeHubRouter::delivery_fee_factor());
let first_system_event = System::events().first().cloned();
assert!(matches!(
first_system_event,
Some(EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmBridgeHubRouter(
Event::DeliveryFeeFactorIncreased { .. }
),
..
})
));
});
}
#[test]
fn get_messages_does_not_return_anything() {
run_test(|| {
assert_ok!(send_xcm::<XcmBridgeHubRouter>(
(Parent, Parent, GlobalConsensus(BridgedNetworkId::get()), Parachain(1000)).into(),
vec![ClearOrigin].into()
));
assert_eq!(XcmBridgeHubRouter::get_messages(), vec![]);
});
}
}