diff --git a/Cargo.dev.toml b/Cargo.dev.toml index a38c164db..bb6b15656 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -16,6 +16,7 @@ members = [ "nft", "xtokens", "xcm-support", + "unknown-tokens", ] resolver = "2" diff --git a/unknown-tokens/Cargo.toml b/unknown-tokens/Cargo.toml new file mode 100644 index 000000000..3518ee05d --- /dev/null +++ b/unknown-tokens/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "orml-unknown-tokens" +description = "Unknown tokens module that implements `UnknownAsset` trait." +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/unknown-tokens" +license = "Apache-2.0" +version = "0.4.1-dev" +authors = ["Acala Developers"] +edition = "2018" + +[dependencies] +serde = { version = "1.0.124", optional = true } +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1", default-features = false } + +xcm = { git = "https://github.com/paritytech/polkadot", branch = "rococo-v1", default-features = false } + +orml-xcm-support = { path = "../xcm-support", default-features = false } + +[dev-dependencies] +sp-io = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "rococo-v1" } + +[features] +default = ["std"] +std = [ + "serde", + "codec/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "xcm/std", + "orml-xcm-support/std", +] diff --git a/unknown-tokens/src/lib.rs b/unknown-tokens/src/lib.rs new file mode 100644 index 000000000..f162c91bb --- /dev/null +++ b/unknown-tokens/src/lib.rs @@ -0,0 +1,127 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] + +use frame_support::pallet_prelude::*; +use sp_std::vec::Vec; +use xcm::v0::{MultiAsset, MultiLocation}; + +use orml_xcm_support::UnknownAsset; + +pub use module::*; + +mod mock; +mod tests; + +#[frame_support::pallet] +pub mod module { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + type Event: From + IsType<::Event>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// Deposit success. [asset, to] + Deposited(MultiAsset, MultiLocation), + /// Deposit failed. [asset, to, error] + DepositFailed(MultiAsset, MultiLocation, DispatchError), + /// Withdraw success. [asset, from] + Withdrawn(MultiAsset, MultiLocation), + /// Withdraw failed. [asset, from, error] + WithdrawFailed(MultiAsset, MultiLocation, DispatchError), + } + + #[pallet::error] + pub enum Error { + /// The balance is too low. + BalanceTooLow, + /// The operation will cause balance to overflow. + BalanceOverflow, + /// Unhandled asset. + UnhandledAsset, + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks for Pallet {} + + /// Concrete fungible balances under a given location and a concrete + /// fungible id. + /// + /// double_map: who, asset_id => u128 + #[pallet::storage] + #[pallet::getter(fn concrete_fungible_balances)] + pub(crate) type ConcreteFungibleBalances = + StorageDoubleMap<_, Blake2_128Concat, MultiLocation, Blake2_128Concat, MultiLocation, u128, ValueQuery>; + + /// Abstract fungible balances under a given location and a abstract + /// fungible id. + /// + /// double_map: who, asset_id => u128 + #[pallet::storage] + #[pallet::getter(fn abstract_fungible_balances)] + pub(crate) type AbstractFungibleBalances = + StorageDoubleMap<_, Blake2_128Concat, MultiLocation, Blake2_128Concat, Vec, u128, ValueQuery>; + + #[pallet::call] + impl Pallet {} +} + +impl UnknownAsset for Pallet { + fn deposit(asset: &MultiAsset, to: &MultiLocation) -> DispatchResult { + let result = match asset { + MultiAsset::ConcreteFungible { id, amount } => { + ConcreteFungibleBalances::::try_mutate(to, id, |b| -> DispatchResult { + *b = b.checked_add(*amount).ok_or(Error::::BalanceOverflow)?; + Ok(()) + }) + } + MultiAsset::AbstractFungible { id, amount } => { + AbstractFungibleBalances::::try_mutate(to, id, |b| -> DispatchResult { + *b = b.checked_add(*amount).ok_or(Error::::BalanceOverflow)?; + Ok(()) + }) + } + _ => Err(Error::::UnhandledAsset.into()), + }; + + if let Err(err) = result { + Self::deposit_event(Event::DepositFailed(asset.clone(), to.clone(), err)); + } else { + Self::deposit_event(Event::Deposited(asset.clone(), to.clone())); + } + + result + } + + fn withdraw(asset: &MultiAsset, from: &MultiLocation) -> DispatchResult { + let result = match asset { + MultiAsset::ConcreteFungible { id, amount } => { + ConcreteFungibleBalances::::try_mutate(from, id, |b| -> DispatchResult { + *b = b.checked_sub(*amount).ok_or(Error::::BalanceTooLow)?; + Ok(()) + }) + } + MultiAsset::AbstractFungible { id, amount } => { + AbstractFungibleBalances::::try_mutate(from, id, |b| -> DispatchResult { + *b = b.checked_sub(*amount).ok_or(Error::::BalanceTooLow)?; + Ok(()) + }) + } + _ => Err(Error::::UnhandledAsset.into()), + }; + + if let Err(err) = result { + Self::deposit_event(Event::WithdrawFailed(asset.clone(), from.clone(), err)); + } else { + Self::deposit_event(Event::Withdrawn(asset.clone(), from.clone())); + } + + result + } +} diff --git a/unknown-tokens/src/mock.rs b/unknown-tokens/src/mock.rs new file mode 100644 index 000000000..ee1a68842 --- /dev/null +++ b/unknown-tokens/src/mock.rs @@ -0,0 +1,73 @@ +//! Mocks for the unknown pallet. + +#![cfg(test)] + +use super::*; +use crate as unknown_tokens; + +use frame_support::{construct_runtime, parameter_types}; +use sp_core::H256; +use sp_runtime::{testing::Header, traits::IdentityLookup, AccountId32}; + +pub type AccountId = AccountId32; + +parameter_types! { + pub const BlockHashCount: u64 = 250; +} + +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 = BlockHashCount; + type BlockWeights = (); + type BlockLength = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type DbWeight = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); + type SS58Prefix = (); +} + +impl Config for Runtime { + type Event = Event; +} + +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::{Module, Call, Storage, Config, Event}, + UnknownTokens: unknown_tokens::{Module, Storage, Event}, + } +); + +pub struct ExtBuilder; + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/unknown-tokens/src/tests.rs b/unknown-tokens/src/tests.rs new file mode 100644 index 000000000..395f803c7 --- /dev/null +++ b/unknown-tokens/src/tests.rs @@ -0,0 +1,206 @@ +//! Unit tests for unknown tokens pallet. + +#![cfg(test)] + +use super::*; +use mock::{Event, *}; + +use codec::{Decode, Encode}; + +use frame_support::{assert_err, assert_ok}; +use xcm::v0::Junction; + +const MOCK_RECIPIENT: MultiLocation = MultiLocation::X1(Junction::Parent); +const MOCK_CONCRETE_FUNGIBLE_ID: MultiLocation = MultiLocation::X1(Junction::Parent); + +fn mock_abstract_fungible_id() -> Vec { + vec![1] +} + +fn concrete_fungible(amount: u128) -> MultiAsset { + MultiAsset::ConcreteFungible { + id: MOCK_CONCRETE_FUNGIBLE_ID, + amount, + } +} + +fn abstract_fungible(amount: u128) -> MultiAsset { + MultiAsset::AbstractFungible { + id: mock_abstract_fungible_id(), + amount, + } +} + +// `message` field of `DispatchError` would be gone after inserted into system +// pallet storage. +fn convert_err(err: DispatchError) -> DispatchError { + DispatchError::decode(&mut &err.encode()[..]).expect("encode then decode cannot fail") +} + +#[test] +fn deposit_concrete_fungible_asset_works() { + ExtBuilder.build().execute_with(|| { + let asset = concrete_fungible(3); + assert_ok!(UnknownTokens::deposit(&asset, &MOCK_RECIPIENT)); + assert_eq!( + UnknownTokens::concrete_fungible_balances(&MOCK_RECIPIENT, &MOCK_CONCRETE_FUNGIBLE_ID), + 3 + ); + + let deposited_event = Event::unknown_tokens(crate::Event::Deposited(asset, MOCK_RECIPIENT)); + assert!(System::events().iter().any(|record| record.event == deposited_event)); + + // overflow case + let max_asset = concrete_fungible(u128::max_value()); + assert_err!( + UnknownTokens::deposit(&max_asset, &MOCK_RECIPIENT), + Error::::BalanceOverflow + ); + + let deposit_failed_event = Event::unknown_tokens(crate::Event::DepositFailed( + max_asset, + MOCK_RECIPIENT, + convert_err(Error::::BalanceOverflow.into()), + )); + assert!(System::events() + .iter() + .any(|record| record.event == deposit_failed_event)); + }); +} + +#[test] +fn deposit_abstract_fungible_asset() { + ExtBuilder.build().execute_with(|| { + let asset = abstract_fungible(3); + assert_ok!(UnknownTokens::deposit(&asset, &MOCK_RECIPIENT)); + assert_eq!( + UnknownTokens::abstract_fungible_balances(&MOCK_RECIPIENT, &mock_abstract_fungible_id()), + 3 + ); + + let deposited_event = Event::unknown_tokens(crate::Event::Deposited(asset, MOCK_RECIPIENT)); + assert!(System::events().iter().any(|record| record.event == deposited_event)); + + // overflow case + let max_asset = abstract_fungible(u128::max_value()); + assert_err!( + UnknownTokens::deposit(&max_asset, &MOCK_RECIPIENT), + Error::::BalanceOverflow + ); + assert_eq!( + UnknownTokens::abstract_fungible_balances(&MOCK_RECIPIENT, &mock_abstract_fungible_id()), + 3 + ); + + let deposit_failed_event = Event::unknown_tokens(crate::Event::DepositFailed( + max_asset, + MOCK_RECIPIENT, + convert_err(Error::::BalanceOverflow.into()), + )); + assert!(System::events() + .iter() + .any(|record| record.event == deposit_failed_event)); + }); +} + +#[test] +fn deposit_unhandled_asset_should_fail() { + ExtBuilder.build().execute_with(|| { + assert_err!( + UnknownTokens::deposit(&MultiAsset::All, &MOCK_RECIPIENT), + Error::::UnhandledAsset + ); + + let deposit_failed_event = Event::unknown_tokens(crate::Event::DepositFailed( + MultiAsset::All, + MOCK_RECIPIENT, + convert_err(Error::::UnhandledAsset.into()), + )); + assert!(System::events() + .iter() + .any(|record| record.event == deposit_failed_event)); + }); +} + +#[test] +fn withdraw_concrete_fungible_asset_works() { + ExtBuilder.build().execute_with(|| { + ConcreteFungibleBalances::::insert(&MOCK_RECIPIENT, &MOCK_CONCRETE_FUNGIBLE_ID, 3); + + let asset = concrete_fungible(3); + assert_ok!(UnknownTokens::withdraw(&asset, &MOCK_RECIPIENT)); + assert_eq!( + UnknownTokens::concrete_fungible_balances(&MOCK_RECIPIENT, &MOCK_CONCRETE_FUNGIBLE_ID), + 0 + ); + + let withdrawn_event = Event::unknown_tokens(crate::Event::Withdrawn(asset.clone(), MOCK_RECIPIENT)); + assert!(System::events().iter().any(|record| record.event == withdrawn_event)); + + // balance too low case + assert_err!( + UnknownTokens::withdraw(&asset, &MOCK_RECIPIENT), + Error::::BalanceTooLow + ); + + let withdraw_failed_event = Event::unknown_tokens(crate::Event::WithdrawFailed( + asset, + MOCK_RECIPIENT, + convert_err(Error::::BalanceTooLow.into()), + )); + assert!(System::events() + .iter() + .any(|record| record.event == withdraw_failed_event)); + }); +} + +#[test] +fn withdraw_abstract_fungible_asset_works() { + ExtBuilder.build().execute_with(|| { + AbstractFungibleBalances::::insert(&MOCK_RECIPIENT, &mock_abstract_fungible_id(), 3); + + let asset = abstract_fungible(3); + assert_ok!(UnknownTokens::withdraw(&asset, &MOCK_RECIPIENT)); + assert_eq!( + UnknownTokens::abstract_fungible_balances(&MOCK_RECIPIENT, &mock_abstract_fungible_id()), + 0 + ); + + let withdrawn_event = Event::unknown_tokens(crate::Event::Withdrawn(asset.clone(), MOCK_RECIPIENT)); + assert!(System::events().iter().any(|record| record.event == withdrawn_event)); + + // balance too low case + assert_err!( + UnknownTokens::withdraw(&asset, &MOCK_RECIPIENT), + Error::::BalanceTooLow + ); + + let withdraw_failed_event = Event::unknown_tokens(crate::Event::WithdrawFailed( + asset, + MOCK_RECIPIENT, + convert_err(Error::::BalanceTooLow.into()), + )); + assert!(System::events() + .iter() + .any(|record| record.event == withdraw_failed_event)); + }); +} + +#[test] +fn withdraw_unhandled_asset_should_fail() { + ExtBuilder.build().execute_with(|| { + assert_err!( + UnknownTokens::withdraw(&MultiAsset::All, &MOCK_RECIPIENT), + Error::::UnhandledAsset + ); + + let withdraw_failed_event = Event::unknown_tokens(crate::Event::WithdrawFailed( + MultiAsset::All, + MOCK_RECIPIENT, + convert_err(Error::::UnhandledAsset.into()), + )); + assert!(System::events() + .iter() + .any(|record| record.event == withdraw_failed_event)); + }); +} diff --git a/xcm-support/src/lib.rs b/xcm-support/src/lib.rs index e95d3c2f7..5b419a71d 100644 --- a/xcm-support/src/lib.rs +++ b/xcm-support/src/lib.rs @@ -7,6 +7,7 @@ //! modules. #![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::unused_unit)] use frame_support::{ dispatch::{DispatchError, DispatchResult}, @@ -27,7 +28,6 @@ pub use currency_adapter::MultiCurrencyAdapter; mod currency_adapter; -#[cfg(test)] mod tests; /// The XCM handler to execute XCM locally. diff --git a/xcm-support/src/tests.rs b/xcm-support/src/tests.rs index eb2bb777f..b0348befd 100644 --- a/xcm-support/src/tests.rs +++ b/xcm-support/src/tests.rs @@ -1,5 +1,7 @@ //! Unit tests for xcm-support implementations. +#![cfg(test)] + use super::*; use frame_support::parameter_types;