#![warn(missing_docs)]
#![cfg_attr(not(feature = "std"), no_std)]
use bp_messages::{LaneState, MessageNonce};
use bp_runtime::{AccountIdOf, BalanceOf, RangeInclusiveExt};
pub use bp_xcm_bridge_hub::{Bridge, BridgeId, BridgeState, LocalXcmChannelManager};
use bp_xcm_bridge_hub::{BridgeLocations, BridgeLocationsError};
use frame_support::{traits::fungible::MutateHold, DefaultNoBound};
use frame_system::Config as SystemConfig;
use pallet_bridge_messages::{Config as BridgeMessagesConfig, LanesManagerError};
use sp_runtime::traits::Zero;
use sp_std::{boxed::Box, vec::Vec};
use xcm::prelude::*;
use xcm_builder::DispatchBlob;
use xcm_executor::traits::ConvertLocation;
pub use bp_xcm_bridge_hub::XcmAsPlainPayload;
pub use dispatcher::XcmBlobMessageDispatchResult;
pub use exporter::PalletAsHaulBlobExporter;
pub use pallet::*;
mod dispatcher;
mod exporter;
pub mod migration;
mod mock;
pub const LOG_TARGET: &str = "runtime::bridge-xcm";
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::{
pallet_prelude::*,
traits::{tokens::Precision, Contains},
};
use frame_system::pallet_prelude::{BlockNumberFor, *};
#[pallet::composite_enum]
pub enum HoldReason<I: 'static = ()> {
#[codec(index = 0)]
BridgeDeposit,
}
#[pallet::config]
#[pallet::disable_frame_system_supertrait_check]
pub trait Config<I: 'static = ()>:
BridgeMessagesConfig<Self::BridgeMessagesPalletInstance>
{
type RuntimeEvent: From<Event<Self, I>>
+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
type UniversalLocation: Get<InteriorLocation>;
#[pallet::constant]
type BridgedNetwork: Get<Location>;
type BridgeMessagesPalletInstance: 'static;
type MessageExportPrice: Get<Assets>;
type DestinationVersion: GetVersion;
type ForceOrigin: EnsureOrigin<<Self as SystemConfig>::RuntimeOrigin>;
type OpenBridgeOrigin: EnsureOrigin<
<Self as SystemConfig>::RuntimeOrigin,
Success = Location,
>;
type BridgeOriginAccountIdConverter: ConvertLocation<AccountIdOf<ThisChainOf<Self, I>>>;
#[pallet::constant]
type BridgeDeposit: Get<BalanceOf<ThisChainOf<Self, I>>>;
type Currency: MutateHold<
AccountIdOf<ThisChainOf<Self, I>>,
Balance = BalanceOf<ThisChainOf<Self, I>>,
Reason = Self::RuntimeHoldReason,
>;
type RuntimeHoldReason: From<HoldReason<I>>;
type AllowWithoutBridgeDeposit: Contains<Location>;
type LocalXcmChannelManager: LocalXcmChannelManager;
type BlobDispatcher: DispatchBlob;
}
pub type BridgeOf<T, I> = Bridge<ThisChainOf<T, I>, LaneIdOf<T, I>>;
pub type ThisChainOf<T, I> =
pallet_bridge_messages::ThisChainOf<T, <T as Config<I>>::BridgeMessagesPalletInstance>;
pub type LaneIdOf<T, I> =
<T as BridgeMessagesConfig<<T as Config<I>>::BridgeMessagesPalletInstance>>::LaneId;
pub type LanesManagerOf<T, I> =
pallet_bridge_messages::LanesManager<T, <T as Config<I>>::BridgeMessagesPalletInstance>;
#[pallet::pallet]
#[pallet::storage_version(migration::STORAGE_VERSION)]
pub struct Pallet<T, I = ()>(PhantomData<(T, I)>);
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
fn integrity_test() {
assert!(
Self::bridged_network_id().is_ok(),
"Configured `T::BridgedNetwork`: {:?} does not contain `GlobalConsensus` junction with `NetworkId`",
T::BridgedNetwork::get()
)
}
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
Self::do_try_state()
}
}
#[pallet::call]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::call_index(0)]
#[pallet::weight(Weight::zero())] pub fn open_bridge(
origin: OriginFor<T>,
bridge_destination_universal_location: Box<VersionedInteriorLocation>,
) -> DispatchResult {
let xcm_version = bridge_destination_universal_location.identify_version();
let locations =
Self::bridge_locations_from_origin(origin, bridge_destination_universal_location)?;
let lane_id = locations.calculate_lane_id(xcm_version).map_err(|e| {
log::trace!(
target: LOG_TARGET,
"calculate_lane_id error: {e:?}",
);
Error::<T, I>::BridgeLocations(e)
})?;
Self::do_open_bridge(locations, lane_id, true)
}
#[pallet::call_index(1)]
#[pallet::weight(Weight::zero())] pub fn close_bridge(
origin: OriginFor<T>,
bridge_destination_universal_location: Box<VersionedInteriorLocation>,
may_prune_messages: MessageNonce,
) -> DispatchResult {
let locations =
Self::bridge_locations_from_origin(origin, bridge_destination_universal_location)?;
let bridge =
Bridges::<T, I>::try_mutate_exists(locations.bridge_id(), |bridge| match bridge {
Some(bridge) => {
bridge.state = BridgeState::Closed;
Ok(bridge.clone())
},
None => Err(Error::<T, I>::UnknownBridge),
})?;
let lanes_manager = LanesManagerOf::<T, I>::new();
let mut inbound_lane = lanes_manager
.any_state_inbound_lane(bridge.lane_id)
.map_err(Error::<T, I>::LanesManager)?;
let mut outbound_lane = lanes_manager
.any_state_outbound_lane(bridge.lane_id)
.map_err(Error::<T, I>::LanesManager)?;
let mut pruned_messages = 0;
for _ in outbound_lane.queued_messages() {
if pruned_messages == may_prune_messages {
break
}
outbound_lane.remove_oldest_unpruned_message();
pruned_messages += 1;
}
if !outbound_lane.queued_messages().is_empty() {
inbound_lane.set_state(LaneState::Closed);
outbound_lane.set_state(LaneState::Closed);
let enqueued_messages = outbound_lane.queued_messages().saturating_len();
log::trace!(
target: LOG_TARGET,
"Bridge {:?} between {:?} and {:?} is closing lane_id: {:?}. {} messages remaining",
locations.bridge_id(),
locations.bridge_origin_universal_location(),
locations.bridge_destination_universal_location(),
bridge.lane_id,
enqueued_messages,
);
Self::deposit_event(Event::<T, I>::ClosingBridge {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
pruned_messages,
enqueued_messages,
});
return Ok(())
}
inbound_lane.purge();
outbound_lane.purge();
Bridges::<T, I>::remove(locations.bridge_id());
LaneToBridge::<T, I>::remove(bridge.lane_id);
let released_deposit = T::Currency::release(
&HoldReason::BridgeDeposit.into(),
&bridge.bridge_owner_account,
bridge.deposit,
Precision::BestEffort,
)
.inspect_err(|e| {
log::error!(
target: LOG_TARGET,
"Failed to unreserve during the bridge {:?} closure with error: {e:?}",
locations.bridge_id(),
);
})
.ok()
.unwrap_or(BalanceOf::<ThisChainOf<T, I>>::zero());
log::trace!(
target: LOG_TARGET,
"Bridge {:?} between {:?} and {:?} has closed lane_id: {:?}, the bridge deposit {released_deposit:?} was returned",
locations.bridge_id(),
bridge.lane_id,
locations.bridge_origin_universal_location(),
locations.bridge_destination_universal_location(),
);
Self::deposit_event(Event::<T, I>::BridgePruned {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
bridge_deposit: released_deposit,
pruned_messages,
});
Ok(())
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn do_open_bridge(
locations: Box<BridgeLocations>,
lane_id: T::LaneId,
create_lanes: bool,
) -> Result<(), DispatchError> {
let bridge_owner_account = T::BridgeOriginAccountIdConverter::convert_location(
locations.bridge_origin_relative_location(),
)
.ok_or(Error::<T, I>::InvalidBridgeOriginAccount)?;
let deposit = if T::AllowWithoutBridgeDeposit::contains(
locations.bridge_origin_relative_location(),
) {
BalanceOf::<ThisChainOf<T, I>>::zero()
} else {
let deposit = T::BridgeDeposit::get();
T::Currency::hold(
&HoldReason::BridgeDeposit.into(),
&bridge_owner_account,
deposit,
)
.map_err(|e| {
log::error!(
target: LOG_TARGET,
"Failed to hold bridge deposit: {deposit:?} \
from bridge_owner_account: {bridge_owner_account:?} derived from \
bridge_origin_relative_location: {:?} with error: {e:?}",
locations.bridge_origin_relative_location(),
);
Error::<T, I>::FailedToReserveBridgeDeposit
})?;
deposit
};
Bridges::<T, I>::try_mutate(locations.bridge_id(), |bridge| match bridge {
Some(_) => Err(Error::<T, I>::BridgeAlreadyExists),
None => {
*bridge = Some(BridgeOf::<T, I> {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account,
deposit,
lane_id,
});
Ok(())
},
})?;
LaneToBridge::<T, I>::try_mutate(lane_id, |bridge| match bridge {
Some(_) => Err(Error::<T, I>::BridgeAlreadyExists),
None => {
*bridge = Some(*locations.bridge_id());
Ok(())
},
})?;
if create_lanes {
let lanes_manager = LanesManagerOf::<T, I>::new();
lanes_manager
.create_inbound_lane(lane_id)
.map_err(Error::<T, I>::LanesManager)?;
lanes_manager
.create_outbound_lane(lane_id)
.map_err(Error::<T, I>::LanesManager)?;
}
log::trace!(
target: LOG_TARGET,
"Bridge {:?} between {:?} and {:?} has been opened using lane_id: {lane_id:?}",
locations.bridge_id(),
locations.bridge_origin_universal_location(),
locations.bridge_destination_universal_location(),
);
Self::deposit_event(Event::<T, I>::BridgeOpened {
bridge_id: *locations.bridge_id(),
bridge_deposit: deposit,
local_endpoint: Box::new(locations.bridge_origin_universal_location().clone()),
remote_endpoint: Box::new(
locations.bridge_destination_universal_location().clone(),
),
lane_id: lane_id.into(),
});
Ok(())
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn bridge_locations_from_origin(
origin: OriginFor<T>,
bridge_destination_universal_location: Box<VersionedInteriorLocation>,
) -> Result<Box<BridgeLocations>, sp_runtime::DispatchError> {
Self::bridge_locations(
T::OpenBridgeOrigin::ensure_origin(origin)?,
(*bridge_destination_universal_location)
.try_into()
.map_err(|_| Error::<T, I>::UnsupportedXcmVersion)?,
)
}
pub fn bridge_locations(
bridge_origin_relative_location: Location,
bridge_destination_universal_location: InteriorLocation,
) -> Result<Box<BridgeLocations>, sp_runtime::DispatchError> {
BridgeLocations::bridge_locations(
T::UniversalLocation::get(),
bridge_origin_relative_location,
bridge_destination_universal_location,
Self::bridged_network_id()?,
)
.map_err(|e| {
log::trace!(
target: LOG_TARGET,
"bridge_locations error: {e:?}",
);
Error::<T, I>::BridgeLocations(e).into()
})
}
pub fn bridge(bridge_id: &BridgeId) -> Option<BridgeOf<T, I>> {
Bridges::<T, I>::get(bridge_id)
}
pub fn bridge_by_lane_id(lane_id: &T::LaneId) -> Option<(BridgeId, BridgeOf<T, I>)> {
LaneToBridge::<T, I>::get(lane_id)
.and_then(|bridge_id| Self::bridge(&bridge_id).map(|bridge| (bridge_id, bridge)))
}
}
impl<T: Config<I>, I: 'static> Pallet<T, I> {
fn bridged_network_id() -> Result<NetworkId, sp_runtime::DispatchError> {
match T::BridgedNetwork::get().take_first_interior() {
Some(GlobalConsensus(network)) => Ok(network),
_ => Err(Error::<T, I>::BridgeLocations(
BridgeLocationsError::InvalidBridgeDestination,
)
.into()),
}
}
}
#[cfg(any(test, feature = "try-runtime", feature = "std"))]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
pub fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> {
use sp_std::collections::btree_set::BTreeSet;
let mut lanes = BTreeSet::new();
for (bridge_id, bridge) in Bridges::<T, I>::iter() {
lanes.insert(Self::do_try_state_for_bridge(bridge_id, bridge)?);
}
ensure!(
lanes.len() == Bridges::<T, I>::iter().count(),
"Invalid `Bridges` configuration, probably two bridges handle the same laneId!"
);
ensure!(
lanes.len() == LaneToBridge::<T, I>::iter().count(),
"Invalid `LaneToBridge` configuration, probably missing or not removed laneId!"
);
Self::do_try_state_for_messages()
}
pub fn do_try_state_for_bridge(
bridge_id: BridgeId,
bridge: BridgeOf<T, I>,
) -> Result<T::LaneId, sp_runtime::TryRuntimeError> {
log::info!(target: LOG_TARGET, "Checking `do_try_state_for_bridge` for bridge_id: {bridge_id:?} and bridge: {bridge:?}");
ensure!(
Some(bridge_id) == LaneToBridge::<T, I>::get(bridge.lane_id),
"Found `LaneToBridge` inconsistency for bridge_id - missing mapping!"
);
let lanes_manager = LanesManagerOf::<T, I>::new();
ensure!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).is_ok(),
"Inbound lane not found!",
);
ensure!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).is_ok(),
"Outbound lane not found!",
);
let bridge_origin_relative_location_as_latest: &Location =
bridge.bridge_origin_relative_location.try_as().map_err(|_| {
"`bridge.bridge_origin_relative_location` cannot be converted to the `latest` XCM, needs migration!"
})?;
let bridge_origin_universal_location_as_latest: &InteriorLocation = bridge.bridge_origin_universal_location
.try_as()
.map_err(|_| "`bridge.bridge_origin_universal_location` cannot be converted to the `latest` XCM, needs migration!")?;
let bridge_destination_universal_location_as_latest: &InteriorLocation = bridge.bridge_destination_universal_location
.try_as()
.map_err(|_| "`bridge.bridge_destination_universal_location` cannot be converted to the `latest` XCM, needs migration!")?;
ensure!(
bridge_id == BridgeId::new(bridge_origin_universal_location_as_latest, bridge_destination_universal_location_as_latest),
"`bridge_id` is different than calculated from `bridge_origin_universal_location_as_latest` and `bridge_destination_universal_location_as_latest`, needs migration!"
);
ensure!(
T::BridgeOriginAccountIdConverter::convert_location(bridge_origin_relative_location_as_latest) == Some(bridge.bridge_owner_account),
"`bridge.bridge_owner_account` is different than calculated from `bridge.bridge_origin_relative_location`, needs migration!"
);
Ok(bridge.lane_id)
}
pub fn do_try_state_for_messages() -> Result<(), sp_runtime::TryRuntimeError> {
for lane_id in pallet_bridge_messages::InboundLanes::<T, T::BridgeMessagesPalletInstance>::iter_keys() {
log::info!(target: LOG_TARGET, "Checking `do_try_state_for_messages` for `InboundLanes`'s lane_id: {lane_id:?}...");
ensure!(
LaneToBridge::<T, I>::get(lane_id).is_some(),
"Found `LaneToBridge` inconsistency for `InboundLanes`'s lane_id - missing mapping!"
);
}
for lane_id in pallet_bridge_messages::OutboundLanes::<T, T::BridgeMessagesPalletInstance>::iter_keys() {
log::info!(target: LOG_TARGET, "Checking `do_try_state_for_messages` for `OutboundLanes`'s lane_id: {lane_id:?}...");
ensure!(
LaneToBridge::<T, I>::get(lane_id).is_some(),
"Found `LaneToBridge` inconsistency for `OutboundLanes`'s lane_id - missing mapping!"
);
}
Ok(())
}
}
#[pallet::storage]
pub type Bridges<T: Config<I>, I: 'static = ()> =
StorageMap<_, Identity, BridgeId, BridgeOf<T, I>>;
#[pallet::storage]
pub type LaneToBridge<T: Config<I>, I: 'static = ()> =
StorageMap<_, Identity, T::LaneId, BridgeId>;
#[pallet::genesis_config]
#[derive(DefaultNoBound)]
pub struct GenesisConfig<T: Config<I>, I: 'static = ()> {
pub opened_bridges: Vec<(Location, InteriorLocation, Option<T::LaneId>)>,
#[serde(skip)]
pub _phantom: sp_std::marker::PhantomData<(T, I)>,
}
#[pallet::genesis_build]
impl<T: Config<I>, I: 'static> BuildGenesisConfig for GenesisConfig<T, I>
where
T: frame_system::Config<AccountId = AccountIdOf<ThisChainOf<T, I>>>,
{
fn build(&self) {
for (
bridge_origin_relative_location,
bridge_destination_universal_location,
maybe_lane_id,
) in &self.opened_bridges
{
let locations = Pallet::<T, I>::bridge_locations(
bridge_origin_relative_location.clone(),
bridge_destination_universal_location.clone().into(),
)
.expect("Invalid genesis configuration");
let lane_id = match maybe_lane_id {
Some(lane_id) => *lane_id,
None =>
locations.calculate_lane_id(xcm::latest::VERSION).expect("Valid locations"),
};
Pallet::<T, I>::do_open_bridge(locations, lane_id, true)
.expect("Valid opened bridge!");
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config<I>, I: 'static = ()> {
BridgeOpened {
bridge_id: BridgeId,
bridge_deposit: BalanceOf<ThisChainOf<T, I>>,
local_endpoint: Box<InteriorLocation>,
remote_endpoint: Box<InteriorLocation>,
lane_id: T::LaneId,
},
ClosingBridge {
bridge_id: BridgeId,
lane_id: T::LaneId,
pruned_messages: MessageNonce,
enqueued_messages: MessageNonce,
},
BridgePruned {
bridge_id: BridgeId,
lane_id: T::LaneId,
bridge_deposit: BalanceOf<ThisChainOf<T, I>>,
pruned_messages: MessageNonce,
},
}
#[pallet::error]
pub enum Error<T, I = ()> {
BridgeLocations(BridgeLocationsError),
InvalidBridgeOriginAccount,
BridgeAlreadyExists,
TooManyBridgesForLocalOrigin,
BridgeAlreadyClosed,
LanesManager(LanesManagerError),
UnknownBridge,
FailedToReserveBridgeDeposit,
UnsupportedXcmVersion,
}
}
#[cfg(test)]
mod tests {
use super::*;
use bp_messages::LaneIdType;
use mock::*;
use frame_support::{assert_err, assert_noop, assert_ok, traits::fungible::Mutate, BoundedVec};
use frame_system::{EventRecord, Phase};
use sp_runtime::TryRuntimeError;
fn fund_origin_sovereign_account(locations: &BridgeLocations, balance: Balance) -> AccountId {
let bridge_owner_account =
LocationToAccountId::convert_location(locations.bridge_origin_relative_location())
.unwrap();
assert_ok!(Balances::mint_into(&bridge_owner_account, balance));
bridge_owner_account
}
fn mock_open_bridge_from_with(
origin: RuntimeOrigin,
deposit: Balance,
with: InteriorLocation,
) -> (BridgeOf<TestRuntime, ()>, BridgeLocations) {
let locations =
XcmOverBridge::bridge_locations_from_origin(origin, Box::new(with.into())).unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
let bridge_owner_account =
fund_origin_sovereign_account(&locations, deposit + ExistentialDeposit::get());
Balances::hold(&HoldReason::BridgeDeposit.into(), &bridge_owner_account, deposit).unwrap();
let bridge = Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account,
deposit,
lane_id,
};
Bridges::<TestRuntime, ()>::insert(locations.bridge_id(), bridge.clone());
LaneToBridge::<TestRuntime, ()>::insert(bridge.lane_id, locations.bridge_id());
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
lanes_manager.create_inbound_lane(bridge.lane_id).unwrap();
lanes_manager.create_outbound_lane(bridge.lane_id).unwrap();
assert_ok!(XcmOverBridge::do_try_state());
(bridge, *locations)
}
fn mock_open_bridge_from(
origin: RuntimeOrigin,
deposit: Balance,
) -> (BridgeOf<TestRuntime, ()>, BridgeLocations) {
mock_open_bridge_from_with(origin, deposit, bridged_asset_hub_universal_location())
}
fn enqueue_message(lane: TestLaneIdType) {
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
lanes_manager
.active_outbound_lane(lane)
.unwrap()
.send_message(BoundedVec::try_from(vec![42]).expect("We craft valid messages"));
}
#[test]
fn open_bridge_fails_if_origin_is_not_allowed() {
run_test(|| {
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::disallowed_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
),
sp_runtime::DispatchError::BadOrigin,
);
})
}
#[test]
fn open_bridge_fails_if_origin_is_not_relative() {
run_test(|| {
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::parent_relay_chain_universal_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::BridgeLocations(
BridgeLocationsError::InvalidBridgeOrigin
),
);
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::sibling_parachain_universal_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::BridgeLocations(
BridgeLocationsError::InvalidBridgeOrigin
),
);
})
}
#[test]
fn open_bridge_fails_if_destination_is_not_remote() {
run_test(|| {
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::parent_relay_chain_origin(),
Box::new(
[GlobalConsensus(RelayNetwork::get()), Parachain(BRIDGED_ASSET_HUB_ID)]
.into()
),
),
Error::<TestRuntime, ()>::BridgeLocations(BridgeLocationsError::DestinationIsLocal),
);
});
}
#[test]
fn open_bridge_fails_if_outside_of_bridged_consensus() {
run_test(|| {
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::parent_relay_chain_origin(),
Box::new(
[
GlobalConsensus(NonBridgedRelayNetwork::get()),
Parachain(BRIDGED_ASSET_HUB_ID)
]
.into()
),
),
Error::<TestRuntime, ()>::BridgeLocations(
BridgeLocationsError::UnreachableDestination
),
);
});
}
#[test]
fn open_bridge_fails_if_origin_has_no_sovereign_account() {
run_test(|| {
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::origin_without_sovereign_account(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::InvalidBridgeOriginAccount,
);
});
}
#[test]
fn open_bridge_fails_if_origin_sovereign_account_has_no_enough_funds() {
run_test(|| {
assert_noop!(
XcmOverBridge::open_bridge(
OpenBridgeOrigin::sibling_parachain_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::FailedToReserveBridgeDeposit,
);
});
}
#[test]
fn open_bridge_fails_if_it_already_exists() {
run_test(|| {
let origin = OpenBridgeOrigin::parent_relay_chain_origin();
let locations = XcmOverBridge::bridge_locations_from_origin(
origin.clone(),
Box::new(bridged_asset_hub_universal_location().into()),
)
.unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
fund_origin_sovereign_account(
&locations,
BridgeDeposit::get() + ExistentialDeposit::get(),
);
Bridges::<TestRuntime, ()>::insert(
locations.bridge_id(),
Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into(),
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: [0u8; 32].into(),
deposit: 0,
lane_id,
},
);
assert_noop!(
XcmOverBridge::open_bridge(
origin,
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::BridgeAlreadyExists,
);
})
}
#[test]
fn open_bridge_fails_if_its_lanes_already_exists() {
run_test(|| {
let origin = OpenBridgeOrigin::parent_relay_chain_origin();
let locations = XcmOverBridge::bridge_locations_from_origin(
origin.clone(),
Box::new(bridged_asset_hub_universal_location().into()),
)
.unwrap();
let lane_id = locations.calculate_lane_id(xcm::latest::VERSION).unwrap();
fund_origin_sovereign_account(
&locations,
BridgeDeposit::get() + ExistentialDeposit::get(),
);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
lanes_manager.create_inbound_lane(lane_id).unwrap();
assert_noop!(
XcmOverBridge::open_bridge(
origin.clone(),
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::LanesManager(LanesManagerError::InboundLaneAlreadyExists),
);
lanes_manager.active_inbound_lane(lane_id).unwrap().purge();
lanes_manager.create_outbound_lane(lane_id).unwrap();
assert_noop!(
XcmOverBridge::open_bridge(
origin,
Box::new(bridged_asset_hub_universal_location().into()),
),
Error::<TestRuntime, ()>::LanesManager(
LanesManagerError::OutboundLaneAlreadyExists
),
);
})
}
#[test]
fn open_bridge_works() {
run_test(|| {
let origins = [
(OpenBridgeOrigin::parent_relay_chain_origin(), 0),
(OpenBridgeOrigin::sibling_parachain_origin(), BridgeDeposit::get()),
];
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
let existential_deposit = ExistentialDeposit::get();
for (origin, expected_deposit) in origins {
System::set_block_number(1);
System::reset_events();
let xcm_version = xcm::latest::VERSION;
let locations = XcmOverBridge::bridge_locations_from_origin(
origin.clone(),
Box::new(
VersionedInteriorLocation::from(bridged_asset_hub_universal_location())
.into_version(xcm_version)
.expect("valid conversion"),
),
)
.unwrap();
let lane_id = locations.calculate_lane_id(xcm_version).unwrap();
assert_eq!(Bridges::<TestRuntime, ()>::get(locations.bridge_id()), None);
assert_eq!(
lanes_manager.active_inbound_lane(lane_id).map(drop),
Err(LanesManagerError::UnknownInboundLane)
);
assert_eq!(
lanes_manager.active_outbound_lane(lane_id).map(drop),
Err(LanesManagerError::UnknownOutboundLane)
);
assert_eq!(LaneToBridge::<TestRuntime, ()>::get(lane_id), None);
let bridge_owner_account = fund_origin_sovereign_account(
&locations,
expected_deposit + existential_deposit,
);
assert_eq!(
Balances::free_balance(&bridge_owner_account),
expected_deposit + existential_deposit
);
assert_eq!(Balances::reserved_balance(&bridge_owner_account), 0);
assert_ok!(XcmOverBridge::open_bridge(
origin,
Box::new(locations.bridge_destination_universal_location().clone().into()),
));
assert_eq!(
Bridges::<TestRuntime, ()>::get(locations.bridge_id()),
Some(Bridge {
bridge_origin_relative_location: Box::new(
locations.bridge_origin_relative_location().clone().into()
),
bridge_origin_universal_location: Box::new(
locations.bridge_origin_universal_location().clone().into(),
),
bridge_destination_universal_location: Box::new(
locations.bridge_destination_universal_location().clone().into(),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: expected_deposit,
lane_id
}),
);
assert_eq!(
lanes_manager.active_inbound_lane(lane_id).map(|l| l.state()),
Ok(LaneState::Opened)
);
assert_eq!(
lanes_manager.active_outbound_lane(lane_id).map(|l| l.state()),
Ok(LaneState::Opened)
);
assert_eq!(
LaneToBridge::<TestRuntime, ()>::get(lane_id),
Some(*locations.bridge_id())
);
assert_eq!(Balances::free_balance(&bridge_owner_account), existential_deposit);
assert_eq!(Balances::reserved_balance(&bridge_owner_account), expected_deposit);
assert_eq!(
System::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmOverBridge(Event::BridgeOpened {
bridge_id: *locations.bridge_id(),
bridge_deposit: expected_deposit,
local_endpoint: Box::new(
locations.bridge_origin_universal_location().clone()
),
remote_endpoint: Box::new(
locations.bridge_destination_universal_location().clone()
),
lane_id: lane_id.into()
}),
topics: vec![],
}),
);
assert_ok!(XcmOverBridge::do_try_state());
}
});
}
#[test]
fn close_bridge_fails_if_origin_is_not_allowed() {
run_test(|| {
assert_noop!(
XcmOverBridge::close_bridge(
OpenBridgeOrigin::disallowed_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
0,
),
sp_runtime::DispatchError::BadOrigin,
);
})
}
#[test]
fn close_bridge_fails_if_origin_is_not_relative() {
run_test(|| {
assert_noop!(
XcmOverBridge::close_bridge(
OpenBridgeOrigin::parent_relay_chain_universal_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
0,
),
Error::<TestRuntime, ()>::BridgeLocations(
BridgeLocationsError::InvalidBridgeOrigin
),
);
assert_noop!(
XcmOverBridge::close_bridge(
OpenBridgeOrigin::sibling_parachain_universal_origin(),
Box::new(bridged_asset_hub_universal_location().into()),
0,
),
Error::<TestRuntime, ()>::BridgeLocations(
BridgeLocationsError::InvalidBridgeOrigin
),
);
})
}
#[test]
fn close_bridge_fails_if_its_lanes_are_unknown() {
run_test(|| {
let origin = OpenBridgeOrigin::parent_relay_chain_origin();
let (bridge, locations) = mock_open_bridge_from(origin.clone(), 0);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().purge();
assert_noop!(
XcmOverBridge::close_bridge(
origin.clone(),
Box::new(locations.bridge_destination_universal_location().clone().into()),
0,
),
Error::<TestRuntime, ()>::LanesManager(LanesManagerError::UnknownInboundLane),
);
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().purge();
let (_, locations) = mock_open_bridge_from(origin.clone(), 0);
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().purge();
assert_noop!(
XcmOverBridge::close_bridge(
origin,
Box::new(locations.bridge_destination_universal_location().clone().into()),
0,
),
Error::<TestRuntime, ()>::LanesManager(LanesManagerError::UnknownOutboundLane),
);
});
}
#[test]
fn close_bridge_works() {
run_test(|| {
let origin = OpenBridgeOrigin::parent_relay_chain_origin();
let expected_deposit = BridgeDeposit::get();
let (bridge, locations) = mock_open_bridge_from(origin.clone(), expected_deposit);
System::set_block_number(1);
let free_balance = Balances::free_balance(&bridge.bridge_owner_account);
let reserved_balance = Balances::reserved_balance(&bridge.bridge_owner_account);
for _ in 0..32 {
enqueue_message(bridge.lane_id);
}
assert_ok!(XcmOverBridge::close_bridge(
origin.clone(),
Box::new(locations.bridge_destination_universal_location().clone().into()),
16,
),);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
assert_eq!(
Bridges::<TestRuntime, ()>::get(locations.bridge_id()).map(|b| b.state),
Some(BridgeState::Closed)
);
assert_eq!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager
.any_state_outbound_lane(bridge.lane_id)
.unwrap()
.queued_messages()
.checked_len(),
Some(16)
);
assert_eq!(
LaneToBridge::<TestRuntime, ()>::get(bridge.lane_id),
Some(*locations.bridge_id())
);
assert_eq!(Balances::free_balance(&bridge.bridge_owner_account), free_balance);
assert_eq!(Balances::reserved_balance(&bridge.bridge_owner_account), reserved_balance);
assert_eq!(
System::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmOverBridge(Event::ClosingBridge {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
pruned_messages: 16,
enqueued_messages: 16,
}),
topics: vec![],
}),
);
assert_ok!(XcmOverBridge::close_bridge(
origin.clone(),
Box::new(locations.bridge_destination_universal_location().clone().into()),
8,
),);
assert_eq!(
Bridges::<TestRuntime, ()>::get(locations.bridge_id()).map(|b| b.state),
Some(BridgeState::Closed)
);
assert_eq!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).unwrap().state(),
LaneState::Closed
);
assert_eq!(
lanes_manager
.any_state_outbound_lane(bridge.lane_id)
.unwrap()
.queued_messages()
.checked_len(),
Some(8)
);
assert_eq!(
LaneToBridge::<TestRuntime, ()>::get(bridge.lane_id),
Some(*locations.bridge_id())
);
assert_eq!(Balances::free_balance(&bridge.bridge_owner_account), free_balance);
assert_eq!(Balances::reserved_balance(&bridge.bridge_owner_account), reserved_balance);
assert_eq!(
System::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmOverBridge(Event::ClosingBridge {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
pruned_messages: 8,
enqueued_messages: 8,
}),
topics: vec![],
}),
);
assert_ok!(XcmOverBridge::close_bridge(
origin,
Box::new(locations.bridge_destination_universal_location().clone().into()),
9,
),);
assert_eq!(
Bridges::<TestRuntime, ()>::get(locations.bridge_id()).map(|b| b.state),
None
);
assert_eq!(
lanes_manager.any_state_inbound_lane(bridge.lane_id).map(drop),
Err(LanesManagerError::UnknownInboundLane)
);
assert_eq!(
lanes_manager.any_state_outbound_lane(bridge.lane_id).map(drop),
Err(LanesManagerError::UnknownOutboundLane)
);
assert_eq!(LaneToBridge::<TestRuntime, ()>::get(bridge.lane_id), None);
assert_eq!(
Balances::free_balance(&bridge.bridge_owner_account),
free_balance + reserved_balance
);
assert_eq!(Balances::reserved_balance(&bridge.bridge_owner_account), 0);
assert_eq!(
System::events().last(),
Some(&EventRecord {
phase: Phase::Initialization,
event: RuntimeEvent::XcmOverBridge(Event::BridgePruned {
bridge_id: *locations.bridge_id(),
lane_id: bridge.lane_id.into(),
bridge_deposit: expected_deposit,
pruned_messages: 8,
}),
topics: vec![],
}),
);
});
}
#[test]
fn do_try_state_works() {
let bridge_origin_relative_location = SiblingLocation::get();
let bridge_origin_universal_location = SiblingUniversalLocation::get();
let bridge_destination_universal_location = BridgedUniversalDestination::get();
let bridge_owner_account =
LocationToAccountId::convert_location(&bridge_origin_relative_location)
.expect("valid accountId");
let bridge_owner_account_mismatch =
LocationToAccountId::convert_location(&Location::parent()).expect("valid accountId");
let bridge_id = BridgeId::new(
&bridge_origin_universal_location,
&bridge_destination_universal_location,
);
let bridge_id_mismatch = BridgeId::new(&InteriorLocation::Here, &InteriorLocation::Here);
let lane_id = TestLaneIdType::try_new(1, 2).unwrap();
let lane_id_mismatch = TestLaneIdType::try_new(3, 4).unwrap();
let test_bridge_state =
|id,
bridge,
(lane_id, bridge_id),
(inbound_lane_id, outbound_lane_id),
expected_error: Option<TryRuntimeError>| {
Bridges::<TestRuntime, ()>::insert(id, bridge);
LaneToBridge::<TestRuntime, ()>::insert(lane_id, bridge_id);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
lanes_manager.create_inbound_lane(inbound_lane_id).unwrap();
lanes_manager.create_outbound_lane(outbound_lane_id).unwrap();
let result = XcmOverBridge::do_try_state();
if let Some(e) = expected_error {
assert_err!(result, e);
} else {
assert_ok!(result);
}
};
let cleanup = |bridge_id, lane_ids| {
Bridges::<TestRuntime, ()>::remove(bridge_id);
for lane_id in lane_ids {
LaneToBridge::<TestRuntime, ()>::remove(lane_id);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
if let Ok(lane) = lanes_manager.any_state_inbound_lane(lane_id) {
lane.purge();
}
if let Ok(lane) = lanes_manager.any_state_outbound_lane(lane_id) {
lane.purge();
}
}
assert_ok!(XcmOverBridge::do_try_state());
};
run_test(|| {
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id),
None,
);
cleanup(bridge_id, vec![lane_id]);
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id_mismatch),
(lane_id, lane_id),
Some(TryRuntimeError::Other(
"Found `LaneToBridge` inconsistency for bridge_id - missing mapping!",
)),
);
cleanup(bridge_id, vec![lane_id]);
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
)),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account_mismatch.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id),
Some(TryRuntimeError::Other("`bridge.bridge_owner_account` is different than calculated from `bridge.bridge_origin_relative_location`, needs migration!")),
);
cleanup(bridge_id, vec![lane_id]);
test_bridge_state(
bridge_id_mismatch,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
)),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account_mismatch.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id_mismatch),
(lane_id, lane_id),
Some(TryRuntimeError::Other("`bridge_id` is different than calculated from `bridge_origin_universal_location_as_latest` and `bridge_destination_universal_location_as_latest`, needs migration!")),
);
cleanup(bridge_id_mismatch, vec![lane_id]);
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id_mismatch, lane_id),
Some(TryRuntimeError::Other("Inbound lane not found!")),
);
cleanup(bridge_id, vec![lane_id, lane_id_mismatch]);
test_bridge_state(
bridge_id,
Bridge {
bridge_origin_relative_location: Box::new(VersionedLocation::from(
bridge_origin_relative_location.clone(),
)),
bridge_origin_universal_location: Box::new(VersionedInteriorLocation::from(
bridge_origin_universal_location.clone(),
)),
bridge_destination_universal_location: Box::new(
VersionedInteriorLocation::from(
bridge_destination_universal_location.clone(),
),
),
state: BridgeState::Opened,
bridge_owner_account: bridge_owner_account.clone(),
deposit: Zero::zero(),
lane_id,
},
(lane_id, bridge_id),
(lane_id, lane_id_mismatch),
Some(TryRuntimeError::Other("Outbound lane not found!")),
);
cleanup(bridge_id, vec![lane_id, lane_id_mismatch]);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
assert!(lanes_manager.create_inbound_lane(lane_id).is_ok());
assert_err!(XcmOverBridge::do_try_state(), TryRuntimeError::Other("Found `LaneToBridge` inconsistency for `InboundLanes`'s lane_id - missing mapping!"));
cleanup(bridge_id, vec![lane_id]);
let lanes_manager = LanesManagerOf::<TestRuntime, ()>::new();
assert!(lanes_manager.create_outbound_lane(lane_id).is_ok());
assert_err!(XcmOverBridge::do_try_state(), TryRuntimeError::Other("Found `LaneToBridge` inconsistency for `OutboundLanes`'s lane_id - missing mapping!"));
cleanup(bridge_id, vec![lane_id]);
});
}
#[test]
fn ensure_encoding_compatibility() {
use codec::Encode;
let bridge_destination_universal_location = BridgedUniversalDestination::get();
let may_prune_messages = 13;
assert_eq!(
bp_xcm_bridge_hub::XcmBridgeHubCall::open_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
)
}
.encode(),
Call::<TestRuntime, ()>::open_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
)
}
.encode()
);
assert_eq!(
bp_xcm_bridge_hub::XcmBridgeHubCall::close_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
),
may_prune_messages,
}
.encode(),
Call::<TestRuntime, ()>::close_bridge {
bridge_destination_universal_location: Box::new(
bridge_destination_universal_location.clone().into()
),
may_prune_messages,
}
.encode()
);
}
}