feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! XCM Execution weights for invoking the backend implementation
|
||||
|
||||
use frame_support::weights::{constants::RocksDbWeight, Weight};
|
||||
|
||||
/// XCM Execution weights for invoking the backend implementation
|
||||
pub trait BackendWeightInfo {
|
||||
/// Execution weight for remote xcm that dispatches `EthereumSystemCall::RegisterToken`
|
||||
/// using `Transact`.
|
||||
fn transact_register_token() -> Weight;
|
||||
fn transact_add_tip() -> Weight;
|
||||
fn do_process_message() -> Weight;
|
||||
fn commit_single() -> Weight;
|
||||
fn submit_delivery_receipt() -> Weight;
|
||||
}
|
||||
|
||||
impl BackendWeightInfo for () {
|
||||
fn transact_register_token() -> Weight {
|
||||
Weight::from_parts(100_000_000, 10000)
|
||||
}
|
||||
fn transact_add_tip() -> Weight {
|
||||
Weight::from_parts(100_000_000, 10000)
|
||||
}
|
||||
fn do_process_message() -> Weight {
|
||||
Weight::from_parts(39_000_000, 3485)
|
||||
.saturating_add(RocksDbWeight::get().reads(4_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
fn commit_single() -> Weight {
|
||||
Weight::from_parts(9_000_000, 1586)
|
||||
.saturating_add(RocksDbWeight::get().reads(2_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(1_u64))
|
||||
}
|
||||
|
||||
fn submit_delivery_receipt() -> Weight {
|
||||
Weight::from_parts(70_000_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3601))
|
||||
.saturating_add(RocksDbWeight::get().reads(2))
|
||||
.saturating_add(RocksDbWeight::get().writes(2))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Benchmarking setup for pallet-template
|
||||
use super::*;
|
||||
#[allow(unused)]
|
||||
use crate::Pallet as SnowbridgeControlFrontend;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_system::RawOrigin;
|
||||
use xcm::prelude::{Location, *};
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
#[benchmarks(where <T as frame_system::Config>::AccountId: Into<Location>)]
|
||||
mod benchmarks {
|
||||
use super::*;
|
||||
#[benchmark]
|
||||
fn register_token() -> Result<(), BenchmarkError> {
|
||||
let origin_location = Location::new(1, [Teyrchain(2000)]);
|
||||
let origin = T::Helper::make_xcm_origin(origin_location.clone());
|
||||
|
||||
let asset_location: Location = Location::new(1, [Teyrchain(2000), GeneralIndex(1)]);
|
||||
let asset_id = Box::new(VersionedLocation::from(asset_location.clone()));
|
||||
T::Helper::initialize_storage(asset_location, origin_location.clone());
|
||||
|
||||
let ether = T::EthereumLocation::get();
|
||||
let asset_owner = T::AccountIdConverter::convert_location(&origin_location).unwrap();
|
||||
T::Helper::setup_pools(asset_owner, ether.clone());
|
||||
|
||||
let asset_metadata = AssetMetadata {
|
||||
name: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
symbol: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
decimals: 12,
|
||||
};
|
||||
|
||||
let fee_asset = Asset::from((Location::parent(), 1_000_000u128));
|
||||
|
||||
#[extrinsic_call]
|
||||
_(origin as T::RuntimeOrigin, asset_id, asset_metadata, fee_asset);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn add_tip() -> Result<(), BenchmarkError> {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
|
||||
let ether = T::EthereumLocation::get();
|
||||
T::Helper::setup_pools(caller.clone(), ether.clone());
|
||||
|
||||
let message_id = MessageId::Inbound(1);
|
||||
let dot = Location::new(1, Here);
|
||||
let asset = Asset::from((dot, 1_000_000_00u128));
|
||||
|
||||
#[extrinsic_call]
|
||||
_(RawOrigin::Signed(caller.clone()), message_id, asset);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(
|
||||
SnowbridgeControlFrontend,
|
||||
crate::mock::new_test_ext(),
|
||||
crate::mock::Test
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,432 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//!
|
||||
//! System frontend pallet that acts as the user-facing control-plane for Snowbridge.
|
||||
//!
|
||||
//! Some operations are delegated to a backend pallet installed on a remote teyrchain.
|
||||
//!
|
||||
//! # Extrinsics
|
||||
//!
|
||||
//! * [`Call::register_token`]: Register Pezkuwi native asset as a wrapped ERC20 token on Ethereum.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
mod benchmarking;
|
||||
|
||||
pub mod weights;
|
||||
pub use weights::*;
|
||||
|
||||
pub mod backend_weights;
|
||||
pub use backend_weights::*;
|
||||
|
||||
use frame_support::{pallet_prelude::*, traits::EnsureOriginWithArg};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use pallet_asset_conversion::Swap;
|
||||
use snowbridge_core::{
|
||||
burn_for_teleport, operating_mode::ExportPausedQuery, reward::MessageId, AssetMetadata,
|
||||
BasicOperatingMode as OperatingMode,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
use xcm::{
|
||||
latest::{validate_send, XcmHash},
|
||||
prelude::*,
|
||||
};
|
||||
use xcm_executor::traits::{FeeManager, FeeReason, TransactAsset};
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use frame_support::traits::OriginTrait;
|
||||
|
||||
pub use pallet::*;
|
||||
pub type AccountIdOf<T> = <T as frame_system::Config>::AccountId;
|
||||
|
||||
pub const LOG_TARGET: &str = "snowbridge-system-frontend";
|
||||
|
||||
/// Call indices within BridgeHub runtime for dispatchables within `snowbridge-pallet-system-v2`
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)]
|
||||
pub enum BridgeHubRuntime<T: frame_system::Config> {
|
||||
#[codec(index = 90)]
|
||||
EthereumSystem(EthereumSystemCall<T>),
|
||||
}
|
||||
|
||||
/// Call indices for dispatchables within `snowbridge-pallet-system-v2`
|
||||
#[derive(Encode, Decode, Debug, PartialEq, Clone, TypeInfo)]
|
||||
pub enum EthereumSystemCall<T: frame_system::Config> {
|
||||
#[codec(index = 2)]
|
||||
RegisterToken {
|
||||
sender: Box<VersionedLocation>,
|
||||
asset_id: Box<VersionedLocation>,
|
||||
metadata: AssetMetadata,
|
||||
amount: u128,
|
||||
},
|
||||
#[codec(index = 3)]
|
||||
AddTip { sender: AccountIdOf<T>, message_id: MessageId, amount: u128 },
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
pub trait BenchmarkHelper<O, AccountId>
|
||||
where
|
||||
O: OriginTrait,
|
||||
{
|
||||
fn make_xcm_origin(location: Location) -> O;
|
||||
fn initialize_storage(asset_location: Location, asset_owner: Location);
|
||||
fn setup_pools(caller: AccountId, asset: Location);
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
#[pallet::pallet]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Origin check for XCM locations that can register token
|
||||
type RegisterTokenOrigin: EnsureOriginWithArg<
|
||||
Self::RuntimeOrigin,
|
||||
Location,
|
||||
Success = Location,
|
||||
>;
|
||||
|
||||
/// XCM message sender
|
||||
type XcmSender: SendXcm;
|
||||
|
||||
/// To withdraw and deposit an asset.
|
||||
type AssetTransactor: TransactAsset;
|
||||
|
||||
/// To charge XCM delivery fees
|
||||
type XcmExecutor: ExecuteXcm<Self::RuntimeCall> + FeeManager;
|
||||
|
||||
/// Fee asset for the execution cost on ethereum
|
||||
type EthereumLocation: Get<Location>;
|
||||
/// To swap the provided tip asset for
|
||||
type Swap: Swap<Self::AccountId, AssetKind = Location, Balance = u128>;
|
||||
|
||||
/// Location of bridge hub
|
||||
type BridgeHubLocation: Get<Location>;
|
||||
|
||||
/// Universal location of this runtime.
|
||||
type UniversalLocation: Get<InteriorLocation>;
|
||||
|
||||
/// InteriorLocation of this pallet.
|
||||
type PalletLocation: Get<InteriorLocation>;
|
||||
|
||||
type AccountIdConverter: ConvertLocation<Self::AccountId>;
|
||||
|
||||
/// Weights for dispatching XCM to backend implementation of `register_token`
|
||||
type BackendWeightInfo: BackendWeightInfo;
|
||||
|
||||
/// Weights for pallet dispatchables
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// A set of helper functions for benchmarking.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper: BenchmarkHelper<Self::RuntimeOrigin, Self::AccountId>;
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// An XCM was sent
|
||||
MessageSent {
|
||||
origin: Location,
|
||||
destination: Location,
|
||||
message: Xcm<()>,
|
||||
message_id: XcmHash,
|
||||
},
|
||||
/// Set OperatingMode
|
||||
ExportOperatingModeChanged { mode: OperatingMode },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// Convert versioned location failure
|
||||
UnsupportedLocationVersion,
|
||||
/// Check location failure, should start from the dispatch origin as owner
|
||||
InvalidAssetOwner,
|
||||
/// Send xcm message failure
|
||||
SendFailure,
|
||||
/// Withdraw fee asset failure
|
||||
FeesNotMet,
|
||||
/// Convert to reanchored location failure
|
||||
LocationConversionFailed,
|
||||
/// Message export is halted
|
||||
Halted,
|
||||
/// The desired destination was unreachable, generally because there is a no way of routing
|
||||
/// to it.
|
||||
Unreachable,
|
||||
/// The asset provided for the tip is unsupported.
|
||||
UnsupportedAsset,
|
||||
/// Unable to withdraw asset.
|
||||
WithdrawError,
|
||||
/// Account could not be converted to a location.
|
||||
InvalidAccount,
|
||||
/// Provided tip asset could not be swapped for ether.
|
||||
SwapError,
|
||||
/// Ether could not be burned.
|
||||
BurnError,
|
||||
/// The tip provided is zero.
|
||||
TipAmountZero,
|
||||
}
|
||||
|
||||
impl<T: Config> From<SendError> for Error<T> {
|
||||
fn from(e: SendError) -> Self {
|
||||
match e {
|
||||
SendError::Fees => Error::<T>::FeesNotMet,
|
||||
SendError::NotApplicable => Error::<T>::Unreachable,
|
||||
_ => Error::<T>::SendFailure,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The current operating mode for exporting to Ethereum.
|
||||
#[pallet::storage]
|
||||
#[pallet::getter(fn export_operating_mode)]
|
||||
pub type ExportOperatingMode<T: Config> = StorageValue<_, OperatingMode, ValueQuery>;
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T>
|
||||
where
|
||||
<T as frame_system::Config>::AccountId: Into<Location>,
|
||||
{
|
||||
/// Set the operating mode for exporting messages to Ethereum.
|
||||
#[pallet::call_index(0)]
|
||||
#[pallet::weight((T::DbWeight::get().reads_writes(1, 1), DispatchClass::Operational))]
|
||||
pub fn set_operating_mode(origin: OriginFor<T>, mode: OperatingMode) -> DispatchResult {
|
||||
ensure_root(origin)?;
|
||||
ExportOperatingMode::<T>::put(mode);
|
||||
Self::deposit_event(Event::ExportOperatingModeChanged { mode });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initiates the registration for a Pezkuwi-native token as a wrapped ERC20 token on
|
||||
/// Ethereum.
|
||||
/// - `asset_id`: Location of the asset
|
||||
/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
|
||||
///
|
||||
/// All origins are allowed, however `asset_id` must be a location nested within the origin
|
||||
/// consensus system.
|
||||
#[pallet::call_index(1)]
|
||||
#[pallet::weight(
|
||||
T::WeightInfo::register_token()
|
||||
.saturating_add(T::BackendWeightInfo::transact_register_token())
|
||||
.saturating_add(T::BackendWeightInfo::do_process_message())
|
||||
.saturating_add(T::BackendWeightInfo::commit_single())
|
||||
.saturating_add(T::BackendWeightInfo::submit_delivery_receipt())
|
||||
)]
|
||||
pub fn register_token(
|
||||
origin: OriginFor<T>,
|
||||
asset_id: Box<VersionedLocation>,
|
||||
metadata: AssetMetadata,
|
||||
fee_asset: Asset,
|
||||
) -> DispatchResult {
|
||||
ensure!(!Self::export_operating_mode().is_halted(), Error::<T>::Halted);
|
||||
|
||||
let asset_location: Location =
|
||||
(*asset_id).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;
|
||||
let origin_location = T::RegisterTokenOrigin::ensure_origin(origin, &asset_location)?;
|
||||
|
||||
let ether_gained = if origin_location.is_here() {
|
||||
// Root origin/location does not pay any fees/tip.
|
||||
0
|
||||
} else {
|
||||
Self::swap_fee_asset_and_burn(origin_location.clone(), fee_asset)?
|
||||
};
|
||||
|
||||
let call = Self::build_register_token_call(
|
||||
origin_location.clone(),
|
||||
asset_location,
|
||||
metadata,
|
||||
ether_gained,
|
||||
)?;
|
||||
|
||||
Self::send_transact_call(origin_location, call)
|
||||
}
|
||||
|
||||
/// Add an additional relayer tip for a committed message identified by `message_id`.
|
||||
/// The tip asset will be swapped for ether.
|
||||
#[pallet::call_index(2)]
|
||||
#[pallet::weight(
|
||||
T::WeightInfo::add_tip()
|
||||
.saturating_add(T::BackendWeightInfo::transact_add_tip())
|
||||
)]
|
||||
pub fn add_tip(origin: OriginFor<T>, message_id: MessageId, asset: Asset) -> DispatchResult
|
||||
where
|
||||
<T as frame_system::Config>::AccountId: Into<Location>,
|
||||
{
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let ether_gained = Self::swap_fee_asset_and_burn(who.clone().into(), asset)?;
|
||||
|
||||
// Send the tip details to BH to be allocated to the reward in the Inbound/Outbound
|
||||
// pallet
|
||||
let call = Self::build_add_tip_call(who.clone(), message_id.clone(), ether_gained);
|
||||
Self::send_transact_call(who.into(), call)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
fn send_xcm(origin: Location, dest: Location, xcm: Xcm<()>) -> Result<XcmHash, SendError> {
|
||||
let is_waived =
|
||||
<T::XcmExecutor as FeeManager>::is_waived(Some(&origin), FeeReason::ChargeFees);
|
||||
let (ticket, price) = validate_send::<T::XcmSender>(dest, xcm.clone())?;
|
||||
if !is_waived {
|
||||
T::XcmExecutor::charge_fees(origin, price).map_err(|_| SendError::Fees)?;
|
||||
}
|
||||
T::XcmSender::deliver(ticket)
|
||||
}
|
||||
|
||||
/// Swaps a specified tip asset to Ether and then burns the resulting ether for
|
||||
/// teleportation. Returns the amount of Ether gained if successful, or a DispatchError if
|
||||
/// any step fails.
|
||||
fn swap_and_burn(
|
||||
origin: Location,
|
||||
tip_asset_location: Location,
|
||||
ether_location: Location,
|
||||
tip_amount: u128,
|
||||
) -> Result<u128, DispatchError> {
|
||||
// Swap tip asset to ether
|
||||
let swap_path = vec![tip_asset_location.clone(), ether_location.clone()];
|
||||
let who = T::AccountIdConverter::convert_location(&origin)
|
||||
.ok_or(Error::<T>::LocationConversionFailed)?;
|
||||
|
||||
let ether_gained = T::Swap::swap_exact_tokens_for_tokens(
|
||||
who.clone(),
|
||||
swap_path,
|
||||
tip_amount,
|
||||
None, // No minimum amount required
|
||||
who,
|
||||
true,
|
||||
)?;
|
||||
|
||||
// Burn the ether
|
||||
let ether_asset = Asset::from((ether_location.clone(), ether_gained));
|
||||
|
||||
burn_for_teleport::<T::AssetTransactor>(&origin, ðer_asset)
|
||||
.map_err(|_| Error::<T>::BurnError)?;
|
||||
|
||||
Ok(ether_gained)
|
||||
}
|
||||
|
||||
// Build the call to dispatch the `EthereumSystem::register_token` extrinsic on BH
|
||||
fn build_register_token_call(
|
||||
sender: Location,
|
||||
asset: Location,
|
||||
metadata: AssetMetadata,
|
||||
amount: u128,
|
||||
) -> Result<BridgeHubRuntime<T>, Error<T>> {
|
||||
// reanchor locations relative to BH
|
||||
let sender = Self::reanchored(sender)?;
|
||||
let asset = Self::reanchored(asset)?;
|
||||
|
||||
let call = BridgeHubRuntime::EthereumSystem(EthereumSystemCall::RegisterToken {
|
||||
sender: Box::new(VersionedLocation::from(sender)),
|
||||
asset_id: Box::new(VersionedLocation::from(asset)),
|
||||
metadata,
|
||||
amount,
|
||||
});
|
||||
|
||||
Ok(call)
|
||||
}
|
||||
|
||||
// Build the call to dispatch the `EthereumSystem::add_tip` extrinsic on BH
|
||||
fn build_add_tip_call(
|
||||
sender: AccountIdOf<T>,
|
||||
message_id: MessageId,
|
||||
amount: u128,
|
||||
) -> BridgeHubRuntime<T> {
|
||||
BridgeHubRuntime::EthereumSystem(EthereumSystemCall::AddTip {
|
||||
sender,
|
||||
message_id,
|
||||
amount,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_remote_xcm(call: &impl Encode) -> Xcm<()> {
|
||||
Xcm(vec![
|
||||
DescendOrigin(T::PalletLocation::get()),
|
||||
UnpaidExecution { weight_limit: Unlimited, check_origin: None },
|
||||
Transact {
|
||||
origin_kind: OriginKind::Xcm,
|
||||
call: call.encode().into(),
|
||||
fallback_max_weight: None,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/// Reanchors `location` relative to BridgeHub.
|
||||
fn reanchored(location: Location) -> Result<Location, Error<T>> {
|
||||
location
|
||||
.reanchored(&T::BridgeHubLocation::get(), &T::UniversalLocation::get())
|
||||
.map_err(|_| Error::<T>::LocationConversionFailed)
|
||||
}
|
||||
|
||||
fn swap_fee_asset_and_burn(
|
||||
origin: Location,
|
||||
fee_asset: Asset,
|
||||
) -> Result<u128, DispatchError> {
|
||||
let ether_location = T::EthereumLocation::get();
|
||||
let (fee_asset_location, fee_amount) = match fee_asset {
|
||||
Asset { id: AssetId(ref loc), fun: Fungible(amount) } => (loc, amount),
|
||||
_ => {
|
||||
tracing::debug!(target: LOG_TARGET, ?fee_asset, "error matching fee asset");
|
||||
return Err(Error::<T>::UnsupportedAsset.into());
|
||||
},
|
||||
};
|
||||
if fee_amount == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
let ether_gained = if *fee_asset_location != ether_location {
|
||||
Self::swap_and_burn(
|
||||
origin.clone(),
|
||||
fee_asset_location.clone(),
|
||||
ether_location,
|
||||
fee_amount,
|
||||
)
|
||||
.inspect_err(|&e| {
|
||||
tracing::debug!(target: LOG_TARGET, ?e, "error swapping asset");
|
||||
})?
|
||||
} else {
|
||||
burn_for_teleport::<T::AssetTransactor>(&origin, &fee_asset)
|
||||
.map_err(|_| Error::<T>::BurnError)?;
|
||||
fee_amount
|
||||
};
|
||||
Ok(ether_gained)
|
||||
}
|
||||
|
||||
fn send_transact_call(
|
||||
origin_location: Location,
|
||||
call: BridgeHubRuntime<T>,
|
||||
) -> DispatchResult {
|
||||
let dest = T::BridgeHubLocation::get();
|
||||
let remote_xcm = Self::build_remote_xcm(&call);
|
||||
let message_id = Self::send_xcm(origin_location, dest.clone(), remote_xcm.clone())
|
||||
.map_err(|error| Error::<T>::from(error))?;
|
||||
|
||||
Self::deposit_event(Event::<T>::MessageSent {
|
||||
origin: T::PalletLocation::get().into(),
|
||||
destination: dest,
|
||||
message: remote_xcm,
|
||||
message_id,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> ExportPausedQuery for Pallet<T> {
|
||||
fn is_paused() -> bool {
|
||||
Self::export_operating_mode().is_halted()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate as snowbridge_system_frontend;
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
use crate::BenchmarkHelper;
|
||||
use frame_support::{
|
||||
derive_impl, parameter_types,
|
||||
traits::{AsEnsureOriginWithArg, Everything},
|
||||
};
|
||||
use snowbridge_core::ParaId;
|
||||
use snowbridge_test_utils::mock_swap_executor::SwapExecutor;
|
||||
pub use snowbridge_test_utils::{mock_origin::pallet_xcm_origin, mock_xcm::*};
|
||||
use sp_core::H256;
|
||||
use sp_runtime::{
|
||||
traits::{AccountIdConversion, BlakeTwo256, IdentityLookup},
|
||||
AccountId32, BuildStorage,
|
||||
};
|
||||
use xcm::prelude::*;
|
||||
|
||||
type Block = frame_system::mocking::MockBlock<Test>;
|
||||
pub type AccountId = AccountId32;
|
||||
|
||||
// Configure a mock runtime to test the pallet.
|
||||
frame_support::construct_runtime!(
|
||||
pub enum Test
|
||||
{
|
||||
System: frame_system,
|
||||
XcmOrigin: pallet_xcm_origin::{Pallet, Origin},
|
||||
EthereumSystemFrontend: snowbridge_system_frontend,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive_impl(frame_system::config_preludes::TestDefaultConfig)]
|
||||
impl frame_system::Config for Test {
|
||||
type BaseCallFilter = frame_support::traits::Everything;
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type RuntimeTask = RuntimeTask;
|
||||
type Hash = H256;
|
||||
type Hashing = BlakeTwo256;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type PalletInfo = PalletInfo;
|
||||
type AccountData = pallet_balances::AccountData<u128>;
|
||||
type Nonce = u64;
|
||||
type Block = Block;
|
||||
}
|
||||
|
||||
impl pallet_xcm_origin::Config for Test {
|
||||
type RuntimeOrigin = RuntimeOrigin;
|
||||
}
|
||||
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
impl BenchmarkHelper<RuntimeOrigin, AccountId> for () {
|
||||
fn make_xcm_origin(location: Location) -> RuntimeOrigin {
|
||||
RuntimeOrigin::from(pallet_xcm_origin::Origin(location))
|
||||
}
|
||||
|
||||
fn initialize_storage(_: Location, _: Location) {}
|
||||
|
||||
fn setup_pools(_: AccountId, _: Location) {}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub storage Ether: Location = Location::new(
|
||||
2,
|
||||
[
|
||||
GlobalConsensus(Ethereum { chain_id: 11155111 }),
|
||||
],
|
||||
);
|
||||
pub storage DeliveryFee: Asset = (Location::parent(), 80_000_000_000u128).into();
|
||||
pub BridgeHubLocation: Location = Location::new(1, [Teyrchain(1002)]);
|
||||
pub UniversalLocation: InteriorLocation =
|
||||
[GlobalConsensus(Pezkuwi), Teyrchain(1000)].into();
|
||||
pub PalletLocation: InteriorLocation = [PalletInstance(36)].into();
|
||||
}
|
||||
|
||||
pub struct AccountIdConverter;
|
||||
impl xcm_executor::traits::ConvertLocation<AccountId> for AccountIdConverter {
|
||||
fn convert_location(ml: &Location) -> Option<AccountId> {
|
||||
match ml.unpack() {
|
||||
(0, [Junction::AccountId32 { id, .. }]) =>
|
||||
Some(<AccountId as codec::Decode>::decode(&mut &*id.to_vec()).unwrap()),
|
||||
(1, [Teyrchain(id)]) => Some(ParaId::from(*id).into_account_truncating()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl crate::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type RegisterTokenOrigin = AsEnsureOriginWithArg<pallet_xcm_origin::EnsureXcm<Everything>>;
|
||||
type XcmSender = MockXcmSender;
|
||||
type AssetTransactor = SuccessfulTransactor;
|
||||
type EthereumLocation = Ether;
|
||||
type XcmExecutor = MockXcmExecutor;
|
||||
type BridgeHubLocation = BridgeHubLocation;
|
||||
type UniversalLocation = UniversalLocation;
|
||||
type PalletLocation = PalletLocation;
|
||||
type BackendWeightInfo = ();
|
||||
type Swap = SwapExecutor;
|
||||
type WeightInfo = ();
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
type Helper = ();
|
||||
type AccountIdConverter = AccountIdConverter;
|
||||
}
|
||||
|
||||
// Build genesis storage according to the mock runtime.
|
||||
pub fn new_test_ext() -> sp_io::TestExternalities {
|
||||
let storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
|
||||
let mut ext: sp_io::TestExternalities = storage.into();
|
||||
ext.execute_with(|| {
|
||||
System::set_block_number(1);
|
||||
});
|
||||
ext
|
||||
}
|
||||
|
||||
pub fn make_xcm_origin(location: Location) -> RuntimeOrigin {
|
||||
pallet_xcm_origin::Origin(location).into()
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
use crate::{mock::*, DispatchError::Other, Error};
|
||||
use frame_support::{assert_err, assert_noop, assert_ok};
|
||||
use frame_system::RawOrigin;
|
||||
use snowbridge_core::{reward::MessageId, AssetMetadata, BasicOperatingMode};
|
||||
use snowbridge_test_utils::mock_swap_executor::TRIGGER_SWAP_ERROR_AMOUNT;
|
||||
use sp_keyring::sr25519::Keyring;
|
||||
use xcm::{
|
||||
latest::{Assets, Error as XcmError, Location},
|
||||
opaque::latest::{Asset, AssetId, AssetInstance, Fungibility},
|
||||
prelude::{GeneralIndex, SendError, Teyrchain},
|
||||
VersionedLocation,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn register_token() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let origin_location = Location::new(1, [Teyrchain(2000)]);
|
||||
let origin = make_xcm_origin(origin_location.clone());
|
||||
let asset_location: Location = Location::new(1, [Teyrchain(2000), GeneralIndex(1)]);
|
||||
let asset_id = Box::new(VersionedLocation::from(asset_location));
|
||||
let asset_metadata = AssetMetadata {
|
||||
name: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
symbol: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
decimals: 12,
|
||||
};
|
||||
|
||||
let ether_location = Ether::get();
|
||||
let fee_amount = 1000;
|
||||
let asset = Asset::from((ether_location.clone(), fee_amount));
|
||||
|
||||
assert_ok!(EthereumSystemFrontend::register_token(
|
||||
origin.clone(),
|
||||
asset_id.clone(),
|
||||
asset_metadata.clone(),
|
||||
asset.clone(),
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_token_fails_delivery_fees_not_met() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let origin_location = Location::new(1, [Teyrchain(2000)]);
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
let asset_location: Location = Location::new(1, [Teyrchain(2000), GeneralIndex(1)]);
|
||||
let asset_id = Box::new(VersionedLocation::from(asset_location));
|
||||
let asset_metadata = AssetMetadata {
|
||||
name: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
symbol: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
decimals: 12,
|
||||
};
|
||||
let ether_location = Ether::get();
|
||||
let fee_amount = 1000;
|
||||
let asset = Asset::from((ether_location.clone(), fee_amount));
|
||||
|
||||
set_charge_fees_override(|_, _| Err(XcmError::FeesNotMet));
|
||||
|
||||
assert_err!(
|
||||
EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset.clone()),
|
||||
Error::<Test>::FeesNotMet,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_token_fails_unroutable() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let origin_location = Location::new(1, [Teyrchain(2000)]);
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
let asset_location: Location = Location::new(1, [Teyrchain(2000), GeneralIndex(1)]);
|
||||
let asset_id = Box::new(VersionedLocation::from(asset_location));
|
||||
let asset_metadata = AssetMetadata {
|
||||
name: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
symbol: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
decimals: 12,
|
||||
};
|
||||
let ether_location = Ether::get();
|
||||
let fee_amount = 1000;
|
||||
let asset = Asset::from((ether_location.clone(), fee_amount));
|
||||
|
||||
// Send XCM with overrides for `SendXcm` behavior to return `Unroutable` error on
|
||||
// validate
|
||||
set_sender_override(
|
||||
|_, _| Err(SendError::Unroutable),
|
||||
|_| Err(SendError::Transport("not allowed to call here")),
|
||||
);
|
||||
assert_err!(
|
||||
EthereumSystemFrontend::register_token(
|
||||
origin.clone(),
|
||||
asset_id.clone(),
|
||||
asset_metadata.clone(),
|
||||
asset.clone(),
|
||||
),
|
||||
Error::<Test>::SendFailure
|
||||
);
|
||||
|
||||
// Send XCM with overrides for `SendXcm` behavior to return `Unroutable` error on
|
||||
// deliver
|
||||
set_sender_override(
|
||||
|_, y| Ok((y.take().unwrap(), Assets::default())),
|
||||
|_| Err(SendError::Unroutable),
|
||||
);
|
||||
|
||||
assert_err!(
|
||||
EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset.clone()),
|
||||
Error::<Test>::SendFailure
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_switch_operating_mode() {
|
||||
new_test_ext().execute_with(|| {
|
||||
assert_ok!(EthereumSystemFrontend::set_operating_mode(
|
||||
RawOrigin::Root.into(),
|
||||
BasicOperatingMode::Halted,
|
||||
));
|
||||
let origin_location = Location::new(1, [Teyrchain(2000)]);
|
||||
let origin = make_xcm_origin(origin_location);
|
||||
let asset_location: Location = Location::new(1, [Teyrchain(2000), GeneralIndex(1)]);
|
||||
let asset_id = Box::new(VersionedLocation::from(asset_location));
|
||||
let asset_metadata = AssetMetadata {
|
||||
name: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
symbol: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
decimals: 12,
|
||||
};
|
||||
let ether_location = Ether::get();
|
||||
let fee_amount = 1000;
|
||||
let asset = Asset::from((ether_location.clone(), fee_amount));
|
||||
assert_noop!(
|
||||
EthereumSystemFrontend::register_token(
|
||||
origin.clone(),
|
||||
asset_id.clone(),
|
||||
asset_metadata.clone(),
|
||||
asset.clone(),
|
||||
),
|
||||
crate::Error::<Test>::Halted
|
||||
);
|
||||
assert_ok!(EthereumSystemFrontend::set_operating_mode(
|
||||
RawOrigin::Root.into(),
|
||||
BasicOperatingMode::Normal,
|
||||
));
|
||||
assert_ok!(EthereumSystemFrontend::register_token(origin, asset_id, asset_metadata, asset));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_tip_ether_asset_succeeds() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let who: AccountId = Keyring::Alice.into();
|
||||
let message_id = MessageId::Inbound(1);
|
||||
let ether_location = Ether::get();
|
||||
let tip_amount = 1000;
|
||||
let asset = Asset::from((ether_location.clone(), tip_amount));
|
||||
|
||||
assert_ok!(EthereumSystemFrontend::add_tip(
|
||||
RuntimeOrigin::signed(who.clone()),
|
||||
message_id.clone(),
|
||||
asset.clone()
|
||||
));
|
||||
|
||||
let events = System::events();
|
||||
let event_record = events.last().expect("Expected at least one event").event.clone();
|
||||
|
||||
if !matches!(
|
||||
event_record,
|
||||
RuntimeEvent::EthereumSystemFrontend(crate::Event::MessageSent { .. })
|
||||
) {
|
||||
panic!("Expected MessageSent event, got: {:?}", event_record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_tip_non_ether_asset_succeeds() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let who: AccountId = Keyring::Alice.into();
|
||||
let message_id = MessageId::Outbound(2);
|
||||
let non_ether_location = Location::new(1, [Teyrchain(3000)]);
|
||||
let tip_amount = 2000;
|
||||
let asset = Asset::from((non_ether_location.clone(), tip_amount));
|
||||
|
||||
assert_ok!(EthereumSystemFrontend::add_tip(
|
||||
RuntimeOrigin::signed(who.clone()),
|
||||
message_id.clone(),
|
||||
asset.clone()
|
||||
));
|
||||
|
||||
let events = System::events();
|
||||
let event_record = events.last().expect("Expected at least one event").event.clone();
|
||||
|
||||
if !matches!(
|
||||
event_record,
|
||||
RuntimeEvent::EthereumSystemFrontend(crate::Event::MessageSent { .. })
|
||||
) {
|
||||
panic!("Expected MessageSent event, got: {:?}", event_record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_tip_unsupported_asset_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let who: AccountId = Keyring::Alice.into();
|
||||
let message_id = MessageId::Inbound(1);
|
||||
let asset = Asset {
|
||||
id: AssetId(Location::new(1, [Teyrchain(4000)])),
|
||||
fun: Fungibility::NonFungible(AssetInstance::Array4([0u8; 4])),
|
||||
};
|
||||
assert_noop!(
|
||||
EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset),
|
||||
Error::<Test>::UnsupportedAsset
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_tip_send_xcm_failure() {
|
||||
new_test_ext().execute_with(|| {
|
||||
set_sender_override(
|
||||
|_, _| Ok((Default::default(), Default::default())),
|
||||
|_| Err(SendError::Unroutable),
|
||||
);
|
||||
let who: AccountId = Keyring::Alice.into();
|
||||
let message_id = MessageId::Outbound(4);
|
||||
let ether_location = Ether::get();
|
||||
let tip_amount = 3000;
|
||||
let asset = Asset::from((ether_location.clone(), tip_amount));
|
||||
assert_noop!(
|
||||
EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset),
|
||||
Error::<Test>::SendFailure
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn add_tip_origin_not_signed_fails() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let message_id = MessageId::Inbound(5);
|
||||
let ether_location = Ether::get();
|
||||
let tip_amount = 1500;
|
||||
let asset = Asset::from((ether_location, tip_amount));
|
||||
assert_noop!(
|
||||
EthereumSystemFrontend::add_tip(RuntimeOrigin::root(), message_id, asset),
|
||||
sp_runtime::DispatchError::BadOrigin
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tip_fails_due_to_swap_error() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let who: AccountId = Keyring::Alice.into();
|
||||
let message_id = MessageId::Inbound(6);
|
||||
let non_ether_location = Location::new(1, [Teyrchain(3000)]);
|
||||
// Use the special amount 12345 that will trigger a swap error in mock_swap_executor
|
||||
let tip_amount = TRIGGER_SWAP_ERROR_AMOUNT;
|
||||
let asset = Asset::from((non_ether_location.clone(), tip_amount));
|
||||
|
||||
assert_noop!(
|
||||
EthereumSystemFrontend::add_tip(RuntimeOrigin::signed(who), message_id, asset),
|
||||
Other("Swap failed for test")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn register_token_with_non_ether_fee_asset_succeeds() {
|
||||
new_test_ext().execute_with(|| {
|
||||
let origin_location = Location::new(1, [Teyrchain(2000)]);
|
||||
let origin = make_xcm_origin(origin_location.clone());
|
||||
let asset_location: Location = Location::new(1, [Teyrchain(2000), GeneralIndex(1)]);
|
||||
let asset_id = Box::new(VersionedLocation::from(asset_location));
|
||||
let asset_metadata = AssetMetadata {
|
||||
name: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
symbol: "pal".as_bytes().to_vec().try_into().unwrap(),
|
||||
decimals: 12,
|
||||
};
|
||||
|
||||
let fee_amount = 1000;
|
||||
let fee_asset = Asset::from((Location::parent(), fee_amount));
|
||||
|
||||
assert_ok!(EthereumSystemFrontend::register_token(
|
||||
origin.clone(),
|
||||
asset_id.clone(),
|
||||
asset_metadata.clone(),
|
||||
fee_asset.clone(),
|
||||
));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
//! Autogenerated weights for `snowbridge_system`
|
||||
//!
|
||||
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev
|
||||
//! DATE: 2023-10-09, STEPS: `2`, REPEAT: `1`, LOW RANGE: `[]`, HIGH RANGE: `[]`
|
||||
//! WORST CASE MAP SIZE: `1000000`
|
||||
//! HOSTNAME: `crake.local`, CPU: `<UNKNOWN>`
|
||||
//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("bridge-hub-pezkuwichain-dev")`, DB CACHE: `1024`
|
||||
|
||||
// Executed Command:
|
||||
// target/release/pezkuwi-teyrchain
|
||||
// benchmark
|
||||
// pallet
|
||||
// --chain
|
||||
// bridge-hub-pezkuwichain-dev
|
||||
// --pallet=snowbridge_system
|
||||
// --extrinsic=*
|
||||
// --execution=wasm
|
||||
// --wasm-execution=compiled
|
||||
// --template
|
||||
// ../teyrchain/templates/module-weight-template.hbs
|
||||
// --output
|
||||
// ../teyrchain/pallets/control/src/weights.rs
|
||||
|
||||
#![cfg_attr(rustfmt, rustfmt_skip)]
|
||||
#![allow(unused_parens)]
|
||||
#![allow(unused_imports)]
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
|
||||
use core::marker::PhantomData;
|
||||
|
||||
/// Weight functions needed for `snowbridge_system`.
|
||||
pub trait WeightInfo {
|
||||
fn register_token() -> Weight;
|
||||
fn add_tip() -> Weight;
|
||||
}
|
||||
|
||||
// For backwards compatibility and tests.
|
||||
impl WeightInfo for () {
|
||||
fn register_token() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `256`
|
||||
// Estimated: `6044`
|
||||
// Minimum execution time: 45_000_000 picoseconds.
|
||||
Weight::from_parts(45_000_000, 6044)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
|
||||
fn add_tip() -> Weight {
|
||||
// Proof Size summary in bytes:
|
||||
// Measured: `256`
|
||||
// Estimated: `6044`
|
||||
// Minimum execution time: 45_000_000 picoseconds.
|
||||
Weight::from_parts(45_000_000, 6044)
|
||||
.saturating_add(RocksDbWeight::get().reads(5_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(3_u64))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user