diff --git a/Cargo.dev.toml b/Cargo.dev.toml index a5511d950..b9a96ed22 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "asset-registry", "auction", "authority", "bencher", diff --git a/asset-registry/Cargo.toml b/asset-registry/Cargo.toml new file mode 100644 index 000000000..4f3f1661a --- /dev/null +++ b/asset-registry/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "orml-asset-registry" +description = "Registry for (foreign) assets" +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/asset-registry" +license = "Apache-2.0" +version = "0.4.1-dev" +authors = ["Interlay Ltd, etc"] +edition = "2021" + +[dependencies] +scale-info = { version = "2.0.1", default-features = false, features = ["derive"] } +serde = { version = "1.0.136", optional = true } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["max-encoded-len"] } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20", default-features = false } +orml-traits = { path = "../traits", version = "0.4.1-dev", default-features = false } +xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20", default-features = false } +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20", default-features = false } +xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20", default-features = false } + +[dev-dependencies] +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20", default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" } +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.20" } + +# cumulus +cumulus-primitives-core = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.20" } +cumulus-pallet-dmp-queue = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.20" } +cumulus-pallet-xcmp-queue = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.20" } +cumulus-pallet-xcm = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.20" } +parachain-info = { git = "https://github.com/paritytech/cumulus", branch = "polkadot-v0.9.20" } + +# polkadot +polkadot-parachain = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20" } +xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20" } +xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20" } +xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20" } +pallet-xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20" } +polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20" } +xcm-simulator = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.20"} + +orml-tokens = { path = "../tokens" } +orml-xtokens = { path = "../xtokens" } +orml-xcm = { path = "../xcm" } +orml-xcm-support = { path = "../xcm-support", default-features = false } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "orml-traits/std", + "xcm/std", + "xcm-builder/std", + "xcm-executor/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/asset-registry/README.md b/asset-registry/README.md new file mode 100644 index 000000000..2ad1175fd --- /dev/null +++ b/asset-registry/README.md @@ -0,0 +1,10 @@ +# Asset Registry Module + +## Overview + +This module provides functionality for storing asset metadata. For each asset, it stores the number of decimals, asset name, asset symbol, existential deposit and (optional) location. Additionally, it stores a value of a generic type that chains can use to store any other metadata that the parachain may need (such as the fee rate, for example). It is designed to be easy to integrate into xcm setups. Various default implementations are provided for this purpose. + +The pallet only contains two extrinsics, `register_asset` and `update_asset`: + +- `register_asset` creates a new asset +- `update_asset` modifies some (or all) of the fields of an existing asset diff --git a/asset-registry/src/impls.rs b/asset-registry/src/impls.rs new file mode 100644 index 000000000..cdc314434 --- /dev/null +++ b/asset-registry/src/impls.rs @@ -0,0 +1,171 @@ +use crate::{module::*, AssetMetadata}; +use frame_support::{log, pallet_prelude::*, weights::constants::WEIGHT_PER_SECOND}; +use orml_traits::{ + asset_registry::{AssetProcessor, FixedConversionRateProvider, WeightToFeeConverter}, + GetByKey, +}; +use sp_runtime::FixedPointNumber; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Bounded, CheckedAdd, One}, + ArithmeticError, FixedU128, +}; +use sp_std::prelude::*; +use xcm::v2::prelude::*; +use xcm_builder::TakeRevenue; +use xcm_executor::{traits::WeightTrader, Assets}; + +/// Alias for AssetMetadata to improve readability (and to placate clippy) +pub type DefaultAssetMetadata = AssetMetadata<::Balance, ::CustomMetadata>; + +/// An AssetProcessor that assigns a sequential ID +pub struct SequentialId(PhantomData); + +impl AssetProcessor> for SequentialId +where + T: Config, + T::AssetId: AtLeast32BitUnsigned, +{ + fn pre_register( + id: Option, + asset_metadata: DefaultAssetMetadata, + ) -> Result<(T::AssetId, DefaultAssetMetadata), DispatchError> { + let next_id = LastAssetId::::get() + .checked_add(&T::AssetId::one()) + .ok_or(ArithmeticError::Overflow)?; + + match id { + Some(explicit_id) if explicit_id != next_id => { + // we don't allow non-sequential ids + Err(Error::::InvalidAssetId.into()) + } + _ => { + LastAssetId::::put(&next_id); + Ok((next_id, asset_metadata)) + } + } + } +} + +/// A default implementation for WeightToFeeConverter that takes a fixed +/// conversion rate. +pub struct FixedRateAssetRegistryTrader(PhantomData

); +impl WeightToFeeConverter for FixedRateAssetRegistryTrader

{ + fn convert_weight_to_fee(location: &MultiLocation, weight: Weight) -> Option { + let fee_per_second = P::get_fee_per_second(location)?; + let weight_ratio = FixedU128::saturating_from_rational(weight as u128, WEIGHT_PER_SECOND as u128); + let amount = weight_ratio.saturating_mul_int(fee_per_second); + Some(amount) + } +} + +/// Helper struct for the AssetRegistryTrader that stores the data about +/// bought weight. +pub struct BoughtWeight { + weight: Weight, + asset_location: MultiLocation, + amount: u128, +} + +/// A WeightTrader implementation that tries to buy weight using a single +/// currency. It tries all assets in `payment` and uses the first asset that can +/// cover the weight. This asset is then "locked in" - later calls to +/// `buy_weight` in the same xcm message only try the same asset. +/// This is because only a single asset can be refunded due to the return type +/// of `refund_weight`. This implementation assumes that `WeightToFeeConverter` +/// implements a linear function, i.e. fee(x) + fee(y) = fee(x+y). +pub struct AssetRegistryTrader { + bought_weight: Option, + _phantom: PhantomData<(W, R)>, +} + +impl WeightTrader for AssetRegistryTrader { + fn new() -> Self { + Self { + bought_weight: None, + _phantom: Default::default(), + } + } + + fn buy_weight(&mut self, weight: Weight, payment: Assets) -> Result { + log::trace!( + target: "xcm::weight", + "AssetRegistryTrader::buy_weight weight: {:?}, payment: {:?}", + weight, payment, + ); + + for (asset, _) in payment.fungible.iter() { + if let AssetId::Concrete(ref location) = asset { + if matches!(self.bought_weight, Some(ref bought) if &bought.asset_location != location) { + // we already bought another asset - don't attempt to buy this one since + // we won't be able to refund it + continue; + } + + if let Some(fee_increase) = W::convert_weight_to_fee(location, weight) { + if fee_increase == 0 { + // if the fee is set very low it could lead to zero fees, in which case + // constructing the fee asset item to subtract from payment would fail. + // Therefore, provide early exit + return Ok(payment); + } + + if let Ok(unused) = payment.clone().checked_sub((asset.clone(), fee_increase).into()) { + let (existing_weight, existing_fee) = match self.bought_weight { + Some(ref x) => (x.weight, x.amount), + None => (0, 0), + }; + + self.bought_weight = Some(BoughtWeight { + amount: existing_fee.checked_add(fee_increase).ok_or(XcmError::Overflow)?, + weight: existing_weight.checked_add(weight).ok_or(XcmError::Overflow)?, + asset_location: location.clone(), + }); + return Ok(unused); + } + } + } + } + Err(XcmError::TooExpensive) + } + + fn refund_weight(&mut self, weight: Weight) -> Option { + log::trace!(target: "xcm::weight", "AssetRegistryTrader::refund_weight weight: {:?}", weight); + + match self.bought_weight { + Some(ref mut bought) => { + let new_weight = bought.weight.saturating_sub(weight); + let new_amount = W::convert_weight_to_fee(&bought.asset_location, new_weight)?; + let refunded_amount = bought.amount.saturating_sub(new_amount); + + bought.weight = new_weight; + bought.amount = new_amount; + + Some((AssetId::Concrete(bought.asset_location.clone()), refunded_amount).into()) + } + None => None, // nothing to refund + } + } +} + +impl Drop for AssetRegistryTrader { + fn drop(&mut self) { + if let Some(ref bought) = self.bought_weight { + R::take_revenue((AssetId::Concrete(bought.asset_location.clone()), bought.amount).into()); + } + } +} + +pub struct ExistentialDeposits(PhantomData); + +// Return Existential deposit of an asset. Implementing this trait allows the +// pallet to be used in the tokens::ExistentialDeposits config item +impl GetByKey for ExistentialDeposits { + fn get(k: &T::AssetId) -> T::Balance { + if let Some(metadata) = Pallet::::metadata(k) { + metadata.existential_deposit + } else { + // Asset does not exist - not supported + T::Balance::max_value() + } + } +} diff --git a/asset-registry/src/lib.rs b/asset-registry/src/lib.rs new file mode 100644 index 000000000..21961a25e --- /dev/null +++ b/asset-registry/src/lib.rs @@ -0,0 +1,323 @@ +#![cfg_attr(not(feature = "std"), no_std)] +// Older clippy versions give a false positive on the expansion of [pallet::call]. +// This is fixed in https://github.com/rust-lang/rust-clippy/issues/8321 +#![allow(clippy::large_enum_variant)] + +use frame_support::{pallet_prelude::*, traits::EnsureOrigin, transactional}; +use frame_system::pallet_prelude::*; +use orml_traits::asset_registry::AssetProcessor; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{AtLeast32BitUnsigned, Member}, + DispatchResult, +}; +use sp_std::prelude::*; +use xcm::{v2::prelude::*, VersionedMultiLocation}; + +pub use impls::*; +pub use module::*; +pub use weights::WeightInfo; + +mod impls; +mod mock; +mod tests; +mod weights; + +/// Data describing the asset properties. +#[derive(scale_info::TypeInfo, Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug)] +pub struct AssetMetadata { + pub decimals: u32, + pub name: Vec, + pub symbol: Vec, + pub existential_deposit: Balance, + pub location: Option, + pub additional: CustomMetadata, +} + +#[frame_support::pallet] +pub mod module { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From> + IsType<::Event>; + + /// Additional non-standard metadata to store for each asset + type CustomMetadata: Parameter + Member + TypeInfo; + + /// The type used as a unique asset id, + type AssetId: Parameter + Member + Default + TypeInfo; + + /// The origin that is allowed to manipulate metadata. + type AuthorityOrigin: EnsureOrigin<::Origin>; + + /// A filter ran upon metadata registration that assigns an is and + /// potentially modifies the supplied metadata. + type AssetProcessor: AssetProcessor>; + + /// The balance type. + type Balance: Parameter + Member + AtLeast32BitUnsigned + Default + Copy; + + /// Weight information for extrinsics in this module. + type WeightInfo: WeightInfo; + } + + #[pallet::error] + pub enum Error { + /// Asset was not found. + AssetNotFound, + /// The version of the `VersionedMultiLocation` value used is not able + /// to be interpreted. + BadVersion, + /// The asset id is invalid. + InvalidAssetId, + /// Another asset was already register with this location. + ConflictingLocation, + /// Another asset was already register with this asset id. + ConflictingAssetId, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + RegisteredAsset { + asset_id: T::AssetId, + metadata: AssetMetadata, + }, + UpdatedAsset { + asset_id: T::AssetId, + metadata: AssetMetadata, + }, + SetLocation { + asset_id: T::AssetId, + location: Box, + }, + } + + /// The metadata of an asset, indexed by asset id. + #[pallet::storage] + #[pallet::getter(fn metadata)] + pub type Metadata = + StorageMap<_, Twox64Concat, T::AssetId, AssetMetadata, OptionQuery>; + + /// Maps a multilocation to an asset id - useful when processing xcm + /// messages. + #[pallet::storage] + #[pallet::getter(fn location_to_asset_id)] + pub type LocationToAssetId = StorageMap<_, Twox64Concat, MultiLocation, T::AssetId, OptionQuery>; + + /// The last processed asset id - used when assigning a sequential id. + #[pallet::storage] + #[pallet::getter(fn last_asset_id)] + pub(crate) type LastAssetId = StorageValue<_, T::AssetId, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + _phantom: PhantomData, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { + _phantom: Default::default(), + } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) {} + } + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks for Pallet {} + + #[pallet::call] + impl Pallet { + #[pallet::weight(T::WeightInfo::register_asset())] + #[transactional] + pub fn register_asset( + origin: OriginFor, + metadata: AssetMetadata, + asset_id: Option, + ) -> DispatchResult { + T::AuthorityOrigin::ensure_origin(origin)?; + + Self::do_register_asset(metadata, asset_id) + } + + #[allow(clippy::too_many_arguments)] + #[pallet::weight(T::WeightInfo::update_asset())] + #[transactional] + pub fn update_asset( + origin: OriginFor, + asset_id: T::AssetId, + decimals: Option, + name: Option>, + symbol: Option>, + existential_deposit: Option, + location: Option>, + additional: Option, + ) -> DispatchResult { + T::AuthorityOrigin::ensure_origin(origin)?; + + Self::do_update_asset( + asset_id, + decimals, + name, + symbol, + existential_deposit, + location, + additional, + )?; + + Ok(()) + } + } +} + +impl Pallet { + /// Register a new asset + pub fn do_register_asset( + metadata: AssetMetadata, + asset_id: Option, + ) -> DispatchResult { + let (asset_id, metadata) = T::AssetProcessor::pre_register(asset_id, metadata)?; + + Self::do_register_asset_without_asset_processor(metadata.clone(), asset_id.clone())?; + + T::AssetProcessor::post_register(asset_id, metadata)?; + + Ok(()) + } + + /// Like do_register_asset, but without calling pre_register and + /// post_register hooks. + /// This function is useful in tests but it might also come in useful to + /// users. + pub fn do_register_asset_without_asset_processor( + metadata: AssetMetadata, + asset_id: T::AssetId, + ) -> DispatchResult { + Metadata::::try_mutate(&asset_id, |maybe_metadata| -> DispatchResult { + // make sure this asset id has not been registered yet + ensure!(maybe_metadata.is_none(), Error::::ConflictingAssetId); + + *maybe_metadata = Some(metadata.clone()); + + if let Some(ref location) = metadata.location { + Self::do_insert_location(asset_id.clone(), location.clone())?; + } + + Ok(()) + })?; + + Self::deposit_event(Event::::RegisteredAsset { asset_id, metadata }); + + Ok(()) + } + + pub fn do_update_asset( + asset_id: T::AssetId, + decimals: Option, + name: Option>, + symbol: Option>, + existential_deposit: Option, + location: Option>, + additional: Option, + ) -> DispatchResult { + Metadata::::try_mutate(&asset_id, |maybe_metadata| -> DispatchResult { + let metadata = maybe_metadata.as_mut().ok_or(Error::::AssetNotFound)?; + if let Some(decimals) = decimals { + metadata.decimals = decimals; + } + + if let Some(name) = name { + metadata.name = name; + } + + if let Some(symbol) = symbol { + metadata.symbol = symbol; + } + + if let Some(existential_deposit) = existential_deposit { + metadata.existential_deposit = existential_deposit; + } + + if let Some(location) = location { + Self::do_update_location(asset_id.clone(), metadata.location.clone(), location.clone())?; + metadata.location = location; + } + + if let Some(additional) = additional { + metadata.additional = additional; + } + + Self::deposit_event(Event::::UpdatedAsset { + asset_id: asset_id.clone(), + metadata: metadata.clone(), + }); + + Ok(()) + })?; + + Ok(()) + } + + pub fn fetch_metadata_by_location( + location: &MultiLocation, + ) -> Option> { + let asset_id = LocationToAssetId::::get(location)?; + Metadata::::get(asset_id) + } + + pub fn multilocation(asset_id: &T::AssetId) -> Result, DispatchError> { + Metadata::::get(asset_id) + .and_then(|metadata| { + metadata + .location + .map(|location| location.try_into().map_err(|()| Error::::BadVersion.into())) + }) + .transpose() + } + + /// update LocationToAssetId mapping if the location changed + fn do_update_location( + asset_id: T::AssetId, + old_location: Option, + new_location: Option, + ) -> DispatchResult { + // Update `LocationToAssetId` only if location changed + if new_location != old_location { + // remove the old location lookup if it exists + if let Some(ref old_location) = old_location { + let location: MultiLocation = old_location.clone().try_into().map_err(|()| Error::::BadVersion)?; + LocationToAssetId::::remove(location); + } + + // insert new location + if let Some(ref new_location) = new_location { + Self::do_insert_location(asset_id, new_location.clone())?; + } + } + + Ok(()) + } + + /// insert location into the LocationToAssetId map + fn do_insert_location(asset_id: T::AssetId, location: VersionedMultiLocation) -> DispatchResult { + // if the metadata contains a location, set the LocationToAssetId + let location: MultiLocation = location.try_into().map_err(|()| Error::::BadVersion)?; + LocationToAssetId::::try_mutate(&location, |maybe_asset_id| { + ensure!(maybe_asset_id.is_none(), Error::::ConflictingLocation); + *maybe_asset_id = Some(asset_id); + Ok(()) + }) + } +} diff --git a/asset-registry/src/mock/mod.rs b/asset-registry/src/mock/mod.rs new file mode 100644 index 000000000..bd190e7ab --- /dev/null +++ b/asset-registry/src/mock/mod.rs @@ -0,0 +1,196 @@ +#![cfg(test)] + +use super::*; + +use mock::para::AssetRegistry; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_io::TestExternalities; +use sp_runtime::{traits::Convert, AccountId32}; +use xcm_simulator::{decl_test_network, decl_test_parachain, decl_test_relay_chain}; + +pub mod para; +pub mod relay; + +pub const ALICE: AccountId32 = AccountId32::new([0u8; 32]); +pub const BOB: AccountId32 = AccountId32::new([1u8; 32]); +pub const CHARLIE: AccountId32 = AccountId32::new([2u8; 32]); + +#[derive(Encode, Decode, Eq, PartialEq, Copy, Clone, RuntimeDebug, PartialOrd, Ord, codec::MaxEncodedLen, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum CurrencyId { + /// Relay chain token. + R, + /// Parachain A token. + A, + /// Parachain A A1 token. + A1, + /// Parachain B token. + B, + /// Parachain B B1 token + B1, + /// Parachain B B2 token + B2, + /// Parachain D token + D, + /// Some asset from the asset registry + RegisteredAsset(u32), +} + +pub struct CurrencyIdConvert; +impl Convert> for CurrencyIdConvert { + fn convert(id: CurrencyId) -> Option { + match id { + CurrencyId::R => Some(Parent.into()), + CurrencyId::A => Some((Parent, Parachain(1), GeneralKey("A".into())).into()), + CurrencyId::A1 => Some((Parent, Parachain(1), GeneralKey("A1".into())).into()), + CurrencyId::B => Some((Parent, Parachain(2), GeneralKey("B".into())).into()), + CurrencyId::B1 => Some((Parent, Parachain(2), GeneralKey("B1".into())).into()), + CurrencyId::B2 => Some((Parent, Parachain(2), GeneralKey("B2".into())).into()), + CurrencyId::D => Some((Parent, Parachain(4), GeneralKey("D".into())).into()), + CurrencyId::RegisteredAsset(id) => AssetRegistry::multilocation(&id).unwrap_or_default(), + } + } +} +impl Convert> for CurrencyIdConvert { + fn convert(l: MultiLocation) -> Option { + let a: Vec = "A".into(); + let a1: Vec = "A1".into(); + let b: Vec = "B".into(); + let b1: Vec = "B1".into(); + let b2: Vec = "B2".into(); + let d: Vec = "D".into(); + if l == MultiLocation::parent() { + return Some(CurrencyId::R); + } + let currency_id = match l.clone() { + MultiLocation { parents, interior } if parents == 1 => match interior { + X2(Parachain(1), GeneralKey(k)) if k == a => Some(CurrencyId::A), + X2(Parachain(1), GeneralKey(k)) if k == a1 => Some(CurrencyId::A1), + X2(Parachain(2), GeneralKey(k)) if k == b => Some(CurrencyId::B), + X2(Parachain(2), GeneralKey(k)) if k == b1 => Some(CurrencyId::B1), + X2(Parachain(2), GeneralKey(k)) if k == b2 => Some(CurrencyId::B2), + X2(Parachain(4), GeneralKey(k)) if k == d => Some(CurrencyId::D), + _ => None, + }, + MultiLocation { parents, interior } if parents == 0 => match interior { + X1(GeneralKey(k)) if k == a => Some(CurrencyId::A), + X1(GeneralKey(k)) if k == b => Some(CurrencyId::B), + X1(GeneralKey(k)) if k == a1 => Some(CurrencyId::A1), + X1(GeneralKey(k)) if k == b1 => Some(CurrencyId::B1), + X1(GeneralKey(k)) if k == b2 => Some(CurrencyId::B2), + X1(GeneralKey(k)) if k == d => Some(CurrencyId::D), + _ => None, + }, + _ => None, + }; + currency_id.or_else(|| AssetRegistry::location_to_asset_id(&l).map(|id| CurrencyId::RegisteredAsset(id))) + } +} +impl Convert> for CurrencyIdConvert { + fn convert(a: MultiAsset) -> Option { + if let MultiAsset { + fun: Fungible(_), + id: Concrete(id), + } = a + { + Self::convert(id) + } else { + Option::None + } + } +} + +pub type Balance = u128; +pub type Amount = i128; + +decl_test_parachain! { + pub struct ParaA { + Runtime = para::Runtime, + XcmpMessageHandler = para::XcmpQueue, + DmpMessageHandler = para::DmpQueue, + new_ext = para_ext(1), + } +} + +decl_test_parachain! { + pub struct ParaB { + Runtime = para::Runtime, + XcmpMessageHandler = para::XcmpQueue, + DmpMessageHandler = para::DmpQueue, + new_ext = para_ext(2), + } +} + +decl_test_parachain! { + pub struct ParaC { + Runtime = para::Runtime, + XcmpMessageHandler = para::XcmpQueue, + DmpMessageHandler = para::DmpQueue, + new_ext = para_ext(3), + } +} + +decl_test_relay_chain! { + pub struct Relay { + Runtime = relay::Runtime, + XcmConfig = relay::XcmConfig, + new_ext = relay_ext(), + } +} + +decl_test_network! { + pub struct TestNet { + relay_chain = Relay, + parachains = vec![ + (1, ParaA), + (2, ParaB), + (3, ParaC), + ], + } +} + +pub type ParaTokens = orml_tokens::Pallet; +pub type ParaXTokens = orml_xtokens::Pallet; + +pub fn para_ext(para_id: u32) -> TestExternalities { + use para::{Runtime, System}; + + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let parachain_info_config = parachain_info::GenesisConfig { + parachain_id: para_id.into(), + }; + >::assimilate_storage(¶chain_info_config, &mut t) + .unwrap(); + + orml_tokens::GenesisConfig:: { + balances: vec![(ALICE, CurrencyId::R, 1_000)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn relay_ext() -> sp_io::TestExternalities { + use relay::{Runtime, System}; + + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(ALICE, 1_000)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} diff --git a/asset-registry/src/mock/para.rs b/asset-registry/src/mock/para.rs new file mode 100644 index 000000000..530ed9ee6 --- /dev/null +++ b/asset-registry/src/mock/para.rs @@ -0,0 +1,341 @@ +use super::{Amount, Balance, CurrencyId, CurrencyIdConvert, ParachainXcmRouter}; + +use crate as orml_asset_registry; + +use codec::{Decode, Encode}; +use cumulus_primitives_core::{ChannelStatus, GetChannelInfo, ParaId}; +use frame_support::{ + construct_runtime, match_type, parameter_types, + traits::{ConstU128, ConstU32, ConstU64, Everything, Nothing}, + weights::{constants::WEIGHT_PER_SECOND, Weight}, + PalletId, +}; +use frame_system::EnsureRoot; +use orml_asset_registry::{AssetRegistryTrader, FixedRateAssetRegistryTrader}; +use orml_traits::{ + location::{AbsoluteReserveProvider, RelativeReserveProvider}, + parameter_type_with_key, FixedConversionRateProvider, MultiCurrency, +}; +use orml_xcm_support::{IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset}; +use pallet_xcm::XcmPassthrough; +use polkadot_parachain::primitives::Sibling; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{AccountIdConversion, Convert, IdentityLookup}, + AccountId32, +}; +use xcm::latest::prelude::*; +use xcm_builder::{ + AccountId32Aliases, AllowTopLevelPaidExecutionFrom, EnsureXcmOrigin, FixedWeightBounds, LocationInverter, + ParentIsPreset, RelayChainAsNative, SiblingParachainAsNative, SiblingParachainConvertsVia, + SignedAccountId32AsNative, SignedToAccountId32, SovereignSignedViaLocation, TakeRevenue, TakeWeightCredit, +}; +use xcm_executor::{Config, XcmExecutor}; + +pub type AccountId = AccountId32; + +impl frame_system::Config for Runtime { + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = ConstU64<250>; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = ConstU32<50>; + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; +} + +use orml_asset_registry::impls::ExistentialDeposits as AssetRegistryExistentialDeposits; +parameter_type_with_key! { + pub ExistentialDeposits: |currency_id: CurrencyId| -> Balance { + match currency_id { + CurrencyId::RegisteredAsset(asset_id) => AssetRegistryExistentialDeposits::::get(asset_id), + _ => Default::default() + } + }; +} + +impl orml_tokens::Config for Runtime { + type Event = Event; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type OnDust = (); + type ReserveIdentifier = [u8; 8]; + type MaxReserves = (); + type MaxLocks = ConstU32<50>; + type DustRemovalWhitelist = Nothing; +} + +#[derive(scale_info::TypeInfo, Encode, Decode, Clone, Eq, PartialEq, Debug)] +pub struct CustomMetadata { + pub fee_per_second: u128, +} + +impl orml_asset_registry::Config for Runtime { + type Event = Event; + type Balance = Balance; + type AssetId = u32; + type AuthorityOrigin = EnsureRoot; + type CustomMetadata = CustomMetadata; + type AssetProcessor = orml_asset_registry::SequentialId; + type WeightInfo = (); +} + +parameter_types! { + pub const ReservedXcmpWeight: Weight = WEIGHT_PER_SECOND / 4; + pub const ReservedDmpWeight: Weight = WEIGHT_PER_SECOND / 4; +} + +impl parachain_info::Config for Runtime {} + +parameter_types! { + pub const RelayLocation: MultiLocation = MultiLocation::parent(); + pub const RelayNetwork: NetworkId = NetworkId::Kusama; + pub RelayChainOrigin: Origin = cumulus_pallet_xcm::Origin::Relay.into(); + pub Ancestry: MultiLocation = Parachain(ParachainInfo::parachain_id().into()).into(); +} + +pub type LocationToAccountId = ( + ParentIsPreset, + SiblingParachainConvertsVia, + AccountId32Aliases, +); + +pub type XcmOriginToCallOrigin = ( + SovereignSignedViaLocation, + RelayChainAsNative, + SiblingParachainAsNative, + SignedAccountId32AsNative, + XcmPassthrough, +); + +pub type LocalAssetTransactor = MultiCurrencyAdapter< + Tokens, + (), + IsNativeConcrete, + AccountId, + LocationToAccountId, + CurrencyId, + CurrencyIdConvert, + (), +>; + +pub type XcmRouter = ParachainXcmRouter; +pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom); + +parameter_types! { + pub TreasuryAccount: AccountId = PalletId(*b"Treasury").into_account(); +} + +pub struct ToTreasury; +impl TakeRevenue for ToTreasury { + fn take_revenue(revenue: MultiAsset) { + if let MultiAsset { + id: Concrete(location), + fun: Fungible(amount), + } = revenue + { + if let Some(currency_id) = CurrencyIdConvert::convert(location) { + let _ = Tokens::deposit(currency_id, &TreasuryAccount::get(), amount); + } + } + } +} + +pub type AssetRegistryWeightTrader = + (AssetRegistryTrader, ToTreasury>,); + +pub struct MyFixedConversionRateProvider; +impl FixedConversionRateProvider for MyFixedConversionRateProvider { + fn get_fee_per_second(location: &MultiLocation) -> Option { + let metadata = AssetRegistry::fetch_metadata_by_location(location)?; + Some(metadata.additional.fee_per_second) + } +} + +pub struct XcmConfig; +impl Config for XcmConfig { + type Call = Call; + type XcmSender = XcmRouter; + type AssetTransactor = LocalAssetTransactor; + type OriginConverter = XcmOriginToCallOrigin; + type IsReserve = MultiNativeAsset; + type IsTeleporter = (); + type LocationInverter = LocationInverter; + type Barrier = Barrier; + type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Trader = AssetRegistryWeightTrader; + type ResponseHandler = (); + type AssetTrap = PolkadotXcm; + type AssetClaims = PolkadotXcm; + type SubscriptionService = PolkadotXcm; +} + +pub struct ChannelInfo; +impl GetChannelInfo for ChannelInfo { + fn get_channel_status(_id: ParaId) -> ChannelStatus { + ChannelStatus::Ready(10, 10) + } + fn get_channel_max(_id: ParaId) -> Option { + Some(usize::max_value()) + } +} + +impl cumulus_pallet_xcmp_queue::Config for Runtime { + type Event = Event; + type XcmExecutor = XcmExecutor; + type ChannelInfo = ChannelInfo; + type VersionWrapper = (); + type ExecuteOverweightOrigin = EnsureRoot; + type ControllerOrigin = EnsureRoot; + type ControllerOriginConverter = XcmOriginToCallOrigin; + type WeightInfo = (); +} + +impl cumulus_pallet_dmp_queue::Config for Runtime { + type Event = Event; + type XcmExecutor = XcmExecutor; + type ExecuteOverweightOrigin = EnsureRoot; +} + +impl cumulus_pallet_xcm::Config for Runtime { + type Event = Event; + type XcmExecutor = XcmExecutor; +} + +pub type LocalOriginToLocation = SignedToAccountId32; + +impl pallet_xcm::Config for Runtime { + type Event = Event; + type SendXcmOrigin = EnsureXcmOrigin; + type XcmRouter = XcmRouter; + type ExecuteXcmOrigin = EnsureXcmOrigin; + type XcmExecuteFilter = Everything; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Nothing; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; +} + +pub struct AccountIdToMultiLocation; +impl Convert for AccountIdToMultiLocation { + fn convert(account: AccountId) -> MultiLocation { + X1(Junction::AccountId32 { + network: NetworkId::Any, + id: account.into(), + }) + .into() + } +} + +parameter_types! { + pub SelfLocation: MultiLocation = MultiLocation::here(); + pub const MaxAssetsForTransfer: usize = 3; +} + +match_type! { + pub type ParentOrParachains: impl Contains = { + MultiLocation { parents: 0, interior: X1(Junction::AccountId32 { .. }) } | + MultiLocation { parents: 1, interior: X1(Junction::AccountId32 { .. }) } | + MultiLocation { parents: 1, interior: X2(Parachain(1), Junction::AccountId32 { .. }) } | + MultiLocation { parents: 1, interior: X2(Parachain(2), Junction::AccountId32 { .. }) } | + MultiLocation { parents: 1, interior: X2(Parachain(3), Junction::AccountId32 { .. }) } | + MultiLocation { parents: 1, interior: X2(Parachain(4), Junction::AccountId32 { .. }) } | + MultiLocation { parents: 1, interior: X2(Parachain(100), Junction::AccountId32 { .. }) } + }; +} + +parameter_type_with_key! { + pub ParachainMinFee: |location: MultiLocation| -> u128 { + #[allow(clippy::match_ref_pats)] // false positive + match (location.parents, location.first_interior()) { + (1, Some(Parachain(2))) => 40, + _ => u128::MAX, + } + }; +} + +impl orml_xtokens::Config for Runtime { + type Event = Event; + type Balance = Balance; + type CurrencyId = CurrencyId; + type CurrencyIdConvert = CurrencyIdConvert; + type AccountIdToMultiLocation = AccountIdToMultiLocation; + type SelfLocation = SelfLocation; + type MultiLocationsFilter = ParentOrParachains; + type MinXcmFee = ParachainMinFee; + type XcmExecutor = XcmExecutor; + type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type BaseXcmWeight = ConstU64<100_000_000>; + type LocationInverter = LocationInverter; + type MaxAssetsForTransfer = MaxAssetsForTransfer; + type ReserveProvider = RelativeReserveProvider; +} + +impl orml_xcm::Config for Runtime { + type Event = Event; + type SovereignOrigin = EnsureRoot; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + + ParachainInfo: parachain_info::{Pallet, Storage, Config}, + XcmpQueue: cumulus_pallet_xcmp_queue::{Pallet, Call, Storage, Event}, + DmpQueue: cumulus_pallet_dmp_queue::{Pallet, Call, Storage, Event}, + CumulusXcm: cumulus_pallet_xcm::{Pallet, Event, Origin}, + + Tokens: orml_tokens::{Pallet, Storage, Event, Config}, + XTokens: orml_xtokens::{Pallet, Storage, Call, Event}, + AssetRegistry: orml_asset_registry::{Pallet, Storage, Call, Event}, + + PolkadotXcm: pallet_xcm::{Pallet, Call, Event, Origin}, + OrmlXcm: orml_xcm::{Pallet, Call, Event}, + } +); diff --git a/asset-registry/src/mock/relay.rs b/asset-registry/src/mock/relay.rs new file mode 100644 index 000000000..5f7f18bac --- /dev/null +++ b/asset-registry/src/mock/relay.rs @@ -0,0 +1,153 @@ +use cumulus_primitives_core::ParaId; +use frame_support::{ + construct_runtime, parameter_types, + traits::{ConstU128, ConstU32, ConstU64, Everything}, + weights::IdentityFee, +}; +use frame_system::EnsureRoot; +use polkadot_runtime_parachains::{configuration, origin, shared, ump}; +use sp_core::H256; +use sp_runtime::{testing::Header, traits::IdentityLookup, AccountId32}; +use xcm::latest::prelude::*; +use xcm_builder::{ + AccountId32Aliases, AllowTopLevelPaidExecutionFrom, ChildParachainAsNative, ChildParachainConvertsVia, + CurrencyAdapter as XcmCurrencyAdapter, FixedWeightBounds, IsConcrete, LocationInverter, SignedAccountId32AsNative, + SignedToAccountId32, SovereignSignedViaLocation, TakeWeightCredit, UsingComponents, +}; +use xcm_executor::{Config, XcmExecutor}; + +pub type AccountId = AccountId32; +pub type Balance = u128; + +impl frame_system::Config for Runtime { + type Origin = Origin; + type Call = Call; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = ::sp_runtime::traits::BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type Event = Event; + type BlockHashCount = ConstU64<250>; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = Everything; + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_balances::Config for Runtime { + type MaxLocks = ConstU32<50>; + type Balance = Balance; + type Event = Event; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; +} + +impl shared::Config for Runtime {} + +impl configuration::Config for Runtime { + type WeightInfo = configuration::TestWeightInfo; +} + +parameter_types! { + pub const KsmLocation: MultiLocation = Here.into(); + pub const KusamaNetwork: NetworkId = NetworkId::Kusama; + pub Ancestry: MultiLocation = Here.into(); +} + +pub type SovereignAccountOf = ( + ChildParachainConvertsVia, + AccountId32Aliases, +); + +pub type LocalAssetTransactor = + XcmCurrencyAdapter, SovereignAccountOf, AccountId, ()>; + +type LocalOriginConverter = ( + SovereignSignedViaLocation, + ChildParachainAsNative, + SignedAccountId32AsNative, +); + +pub type XcmRouter = super::RelayChainXcmRouter; +pub type Barrier = (TakeWeightCredit, AllowTopLevelPaidExecutionFrom); + +pub struct XcmConfig; +impl Config for XcmConfig { + type Call = Call; + type XcmSender = XcmRouter; + type AssetTransactor = LocalAssetTransactor; + type OriginConverter = LocalOriginConverter; + type IsReserve = (); + type IsTeleporter = (); + type LocationInverter = LocationInverter; + type Barrier = Barrier; + type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type Trader = UsingComponents, KsmLocation, AccountId, Balances, ()>; + type ResponseHandler = (); + type AssetTrap = (); + type AssetClaims = (); + type SubscriptionService = XcmPallet; +} + +pub type LocalOriginToLocation = SignedToAccountId32; + +impl pallet_xcm::Config for Runtime { + type Event = Event; + type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmRouter = XcmRouter; + // Anyone can execute XCM messages locally... + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = Everything; + type XcmExecutor = XcmExecutor; + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds, Call, ConstU32<100>>; + type LocationInverter = LocationInverter; + type Origin = Origin; + type Call = Call; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = pallet_xcm::CurrentXcmVersion; +} + +impl ump::Config for Runtime { + type Event = Event; + type UmpSink = ump::XcmSink, Runtime>; + type FirstMessageFactorPercent = ConstU64<100>; + type ExecuteOverweightOrigin = EnsureRoot; + type WeightInfo = polkadot_runtime_parachains::ump::TestWeightInfo; +} + +impl origin::Config for Runtime {} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Storage, Config, Event}, + Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, + ParasOrigin: origin::{Pallet, Origin}, + ParasUmp: ump::{Pallet, Call, Storage, Event}, + XcmPallet: pallet_xcm::{Pallet, Call, Storage, Event, Origin}, + } +); diff --git a/asset-registry/src/tests.rs b/asset-registry/src/tests.rs new file mode 100644 index 000000000..01a2b5622 --- /dev/null +++ b/asset-registry/src/tests.rs @@ -0,0 +1,463 @@ +#![cfg(test)] + +use super::*; +use crate as orml_asset_registry; +use crate::tests::para::{AssetRegistry, CustomMetadata, Origin, Tokens, TreasuryAccount}; +use frame_support::{assert_noop, assert_ok}; +use mock::*; +use orml_traits::MultiCurrency; +use polkadot_parachain::primitives::Sibling; +use sp_runtime::AccountId32; +use xcm_simulator::TestExt; + +fn treasury_account() -> AccountId32 { + TreasuryAccount::get() +} + +fn sibling_a_account() -> AccountId32 { + use sp_runtime::traits::AccountIdConversion; + Sibling::from(1).into_account() +} + +fn sibling_b_account() -> AccountId32 { + use sp_runtime::traits::AccountIdConversion; + Sibling::from(2).into_account() +} + +fn sibling_c_account() -> AccountId32 { + use sp_runtime::traits::AccountIdConversion; + Sibling::from(3).into_account() +} + +// Not used in any unit tests, but it's super helpful for debugging. Let's +// keep it here. +#[allow(dead_code)] +fn print_events(name: &'static str) { + println!("------ {:?} events -------", name); + frame_system::Pallet::::events() + .iter() + .for_each(|r| println!("> {:?}", r.event)); +} + +fn dummy_metadata() -> AssetMetadata<::Balance, CustomMetadata> { + AssetMetadata { + decimals: 12, + name: "para A native token".as_bytes().to_vec(), + symbol: "paraA".as_bytes().to_vec(), + existential_deposit: 0, + location: Some(MultiLocation::new(1, X2(Parachain(1), GeneralKey(vec![0]))).into()), + additional: CustomMetadata { + fee_per_second: 1_000_000_000_000, + }, + } +} + +#[test] +/// test that the asset registry can be used in xcm transfers +fn send_self_parachain_asset_to_sibling() { + TestNet::reset(); + + let mut metadata = dummy_metadata(); + + ParaB::execute_with(|| { + AssetRegistry::register_asset(Origin::root(), metadata.clone(), None).unwrap(); + }); + + ParaA::execute_with(|| { + metadata.location = Some(MultiLocation::new(0, X1(GeneralKey(vec![0]))).into()); + AssetRegistry::register_asset(Origin::root(), metadata, None).unwrap(); + + assert_ok!(ParaTokens::deposit(CurrencyId::RegisteredAsset(1), &ALICE, 1_000)); + + assert_ok!(ParaXTokens::transfer( + Some(ALICE).into(), + CurrencyId::RegisteredAsset(1), + 500, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + } + ) + ) + .into() + ), + 40, + )); + + assert_eq!(ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &ALICE), 500); + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &sibling_b_account()), + 500 + ); + }); + + ParaB::execute_with(|| { + assert_eq!(ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &BOB), 460); + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &treasury_account()), + 40 + ); + }); +} + +#[test] +/// test that the asset registry can be used in xcm transfers +fn send_sibling_asset_to_non_reserve_sibling() { + TestNet::reset(); + + // send from paraA send paraB's token to paraC + + ParaA::execute_with(|| { + AssetRegistry::register_asset( + Origin::root(), + AssetMetadata { + location: Some(MultiLocation::new(1, X2(Parachain(2), GeneralKey(vec![0]))).into()), + ..dummy_metadata() + }, + None, + ) + .unwrap(); + assert_ok!(ParaTokens::deposit(CurrencyId::RegisteredAsset(1), &ALICE, 1_000)); + }); + + ParaB::execute_with(|| { + AssetRegistry::register_asset( + Origin::root(), + AssetMetadata { + location: Some(MultiLocation::new(0, X1(GeneralKey(vec![0]))).into()), + ..dummy_metadata() + }, + None, + ) + .unwrap(); + assert_ok!(ParaTokens::deposit( + CurrencyId::RegisteredAsset(1), + &sibling_a_account(), + 1_000 + )); + }); + + ParaC::execute_with(|| { + AssetRegistry::register_asset( + Origin::root(), + AssetMetadata { + location: Some(MultiLocation::new(1, X2(Parachain(2), GeneralKey(vec![0]))).into()), + ..dummy_metadata() + }, + None, + ) + .unwrap(); + }); + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer( + Some(ALICE).into(), + CurrencyId::RegisteredAsset(1), + 500, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(3), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + } + ) + ) + .into() + ), + 40 + )); + assert_eq!(ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &ALICE), 500); + }); + + // check reserve accounts + ParaB::execute_with(|| { + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &sibling_a_account()), + 500 + ); + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &sibling_c_account()), + 460 + ); + }); + + ParaC::execute_with(|| { + assert_eq!(ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &BOB), 420); + }); +} + +#[test] +/// tests the SequentialId AssetProcessor +fn test_sequential_id_normal_behavior() { + TestNet::reset(); + + ParaA::execute_with(|| { + let metadata1 = dummy_metadata(); + + let metadata2 = AssetMetadata { + name: "para A native token 2".as_bytes().to_vec(), + symbol: "paraA2".as_bytes().to_vec(), + location: Some(MultiLocation::new(1, X2(Parachain(1), GeneralKey(vec![1]))).into()), + ..dummy_metadata() + }; + AssetRegistry::register_asset(Origin::root(), metadata1.clone(), None).unwrap(); + AssetRegistry::register_asset(Origin::root(), metadata2.clone(), None).unwrap(); + + assert_eq!(AssetRegistry::metadata(1).unwrap(), metadata1); + assert_eq!(AssetRegistry::metadata(2).unwrap(), metadata2); + }); +} + +#[test] +fn test_sequential_id_with_invalid_id_returns_error() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_ok!(AssetRegistry::register_asset(Origin::root(), dummy_metadata(), Some(1))); + assert_noop!( + AssetRegistry::register_asset(Origin::root(), dummy_metadata(), Some(1)), + Error::::InvalidAssetId + ); + }); +} + +#[test] +/// tests FixedRateAssetRegistryTrader +fn test_fixed_rate_asset_trader() { + TestNet::reset(); + + let metadata = dummy_metadata(); + + ParaB::execute_with(|| { + AssetRegistry::register_asset(Origin::root(), metadata.clone(), None).unwrap(); + }); + + ParaA::execute_with(|| { + let para_a_metadata = AssetMetadata { + location: Some(MultiLocation::new(0, X1(GeneralKey(vec![0]))).into()), + ..metadata.clone() + }; + AssetRegistry::register_asset(Origin::root(), para_a_metadata, None).unwrap(); + + assert_ok!(ParaTokens::deposit(CurrencyId::RegisteredAsset(1), &ALICE, 1_000)); + + assert_ok!(ParaXTokens::transfer( + Some(ALICE).into(), + CurrencyId::RegisteredAsset(1), + 500, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + } + ) + ) + .into() + ), + 40, + )); + }); + + let expected_fee = 40; + let expected_transfer_1_amount = 500 - expected_fee; + ParaB::execute_with(|| { + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &BOB), + expected_transfer_1_amount + ); + + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &treasury_account()), + expected_fee + ); + + // now double the fee rate + AssetRegistry::update_asset( + Origin::root(), + 1, + None, + None, + None, + None, + None, + Some(CustomMetadata { + fee_per_second: metadata.additional.fee_per_second * 2, + }), + ) + .unwrap(); + }); + + ParaA::execute_with(|| { + assert_ok!(ParaXTokens::transfer( + Some(ALICE).into(), + CurrencyId::RegisteredAsset(1), + 500, + Box::new( + MultiLocation::new( + 1, + X2( + Parachain(2), + Junction::AccountId32 { + network: NetworkId::Any, + id: BOB.into(), + } + ) + ) + .into() + ), + 40, + )); + }); + + // we doubled the fee rate, so subtract twice the original fee + let expected_transfer_2_amount = 500 - 2 * expected_fee; + + ParaB::execute_with(|| { + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &BOB), + expected_transfer_1_amount + expected_transfer_2_amount + ); + + assert_eq!( + ParaTokens::free_balance(CurrencyId::RegisteredAsset(1), &treasury_account()), + expected_fee * 3 // 1 for the first transfer, then twice for the second one + ); + }); +} + +#[test] +fn test_register_duplicate_location_returns_error() { + TestNet::reset(); + + ParaA::execute_with(|| { + let metadata = dummy_metadata(); + + assert_ok!(AssetRegistry::register_asset(Origin::root(), metadata.clone(), None)); + assert_noop!( + AssetRegistry::register_asset(Origin::root(), metadata.clone(), None), + Error::::ConflictingLocation + ); + }); +} + +#[test] +fn test_register_duplicate_asset_id_returns_error() { + TestNet::reset(); + + ParaA::execute_with(|| { + assert_ok!(AssetRegistry::register_asset(Origin::root(), dummy_metadata(), Some(1))); + assert_noop!( + AssetRegistry::do_register_asset_without_asset_processor(dummy_metadata(), 1), + Error::::ConflictingAssetId + ); + }); +} + +#[test] +fn test_update_metadata_works() { + TestNet::reset(); + + ParaA::execute_with(|| { + let old_metadata = dummy_metadata(); + assert_ok!(AssetRegistry::register_asset( + Origin::root(), + old_metadata.clone(), + None + )); + + let new_metadata = AssetMetadata { + decimals: 11, + name: "para A native token2".as_bytes().to_vec(), + symbol: "paraA2".as_bytes().to_vec(), + existential_deposit: 1, + location: Some(MultiLocation::new(1, X2(Parachain(1), GeneralKey(vec![1]))).into()), + additional: CustomMetadata { + fee_per_second: 2_000_000_000_000, + }, + }; + assert_ok!(AssetRegistry::update_asset( + Origin::root(), + 1, + Some(new_metadata.decimals), + Some(new_metadata.name.clone()), + Some(new_metadata.symbol.clone()), + Some(new_metadata.existential_deposit), + Some(new_metadata.location.clone()), + Some(new_metadata.additional.clone()) + )); + + let old_location: MultiLocation = old_metadata.location.clone().unwrap().try_into().unwrap(); + let new_location: MultiLocation = new_metadata.location.clone().unwrap().try_into().unwrap(); + + // check that the old location was removed and the new one added + assert_eq!(AssetRegistry::location_to_asset_id(old_location), None); + assert_eq!(AssetRegistry::location_to_asset_id(new_location), Some(1)); + + assert_eq!(AssetRegistry::metadata(1).unwrap(), new_metadata); + }); +} + +#[test] +fn test_update_metadata_fails_with_unknown_asset() { + TestNet::reset(); + + ParaA::execute_with(|| { + let old_metadata = dummy_metadata(); + assert_ok!(AssetRegistry::register_asset( + Origin::root(), + old_metadata.clone(), + None + )); + + assert_noop!( + AssetRegistry::update_asset(Origin::root(), 4, None, None, None, None, None, None,), + Error::::AssetNotFound + ); + }); +} + +#[test] +fn test_existential_deposits() { + TestNet::reset(); + + ParaA::execute_with(|| { + let metadata = AssetMetadata { + existential_deposit: 100, + ..dummy_metadata() + }; + assert_ok!(AssetRegistry::register_asset(Origin::root(), metadata, None)); + + assert_ok!(Tokens::set_balance( + Origin::root(), + ALICE, + CurrencyId::RegisteredAsset(1), + 1_000, + 0 + )); + + // transferring at existential_deposit succeeds + assert_ok!(Tokens::transfer( + Some(ALICE).into(), + BOB, + CurrencyId::RegisteredAsset(1), + 100 + )); + // transferring below existential_deposit fails + assert_noop!( + Tokens::transfer(Some(ALICE).into(), CHARLIE, CurrencyId::RegisteredAsset(1), 50), + orml_tokens::Error::::ExistentialDeposit + ); + }); +} diff --git a/asset-registry/src/weights.rs b/asset-registry/src/weights.rs new file mode 100644 index 000000000..9f4a3d596 --- /dev/null +++ b/asset-registry/src/weights.rs @@ -0,0 +1,29 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(clippy::unnecessary_cast)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +pub trait WeightInfo { + fn register_asset() -> Weight; + fn update_asset() -> Weight; + fn set_asset_location() -> Weight; +} + +/// Default weights. +impl WeightInfo for () { + fn register_asset() -> Weight { + 0 + } + fn update_asset() -> Weight { + 0 + } + fn set_asset_location() -> Weight { + 0 + } +} diff --git a/traits/src/asset_registry.rs b/traits/src/asset_registry.rs new file mode 100644 index 000000000..23b090e35 --- /dev/null +++ b/traits/src/asset_registry.rs @@ -0,0 +1,17 @@ +use frame_support::pallet_prelude::*; +use xcm::latest::prelude::*; + +pub trait WeightToFeeConverter { + fn convert_weight_to_fee(location: &MultiLocation, weight: Weight) -> Option; +} + +pub trait FixedConversionRateProvider { + fn get_fee_per_second(location: &MultiLocation) -> Option; +} + +pub trait AssetProcessor { + fn pre_register(id: Option, asset_metadata: Metadata) -> Result<(AssetId, Metadata), DispatchError>; + fn post_register(_id: AssetId, _asset_metadata: Metadata) -> Result<(), DispatchError> { + Ok(()) + } +} diff --git a/traits/src/lib.rs b/traits/src/lib.rs index f2642558b..f906aa498 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -11,6 +11,7 @@ use sp_std::{ #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; +pub use asset_registry::{FixedConversionRateProvider, WeightToFeeConverter}; pub use auction::{Auction, AuctionHandler, AuctionInfo, OnNewBidResult}; pub use currency::{ BalanceStatus, BasicCurrency, BasicCurrencyExtended, BasicLockableCurrency, BasicReservableCurrency, @@ -27,6 +28,7 @@ use scale_info::TypeInfo; pub use xcm_transfer::XcmTransfer; pub mod arithmetic; +pub mod asset_registry; pub mod auction; pub mod currency; pub mod data_provider;