diff --git a/Cargo.dev.toml b/Cargo.dev.toml index 728aec898..89f6f765f 100644 --- a/Cargo.dev.toml +++ b/Cargo.dev.toml @@ -6,6 +6,7 @@ members = [ "benchmarking", "build-script-utils", "currencies", + "delay-tasks", "gradually-update", "nft", "oracle", diff --git a/asset-registry/src/mock/para.rs b/asset-registry/src/mock/para.rs index cd428225c..69c4c5f5e 100644 --- a/asset-registry/src/mock/para.rs +++ b/asset-registry/src/mock/para.rs @@ -327,6 +327,8 @@ impl orml_xtokens::Config for Runtime { type ReserveProvider = RelativeReserveProvider; type RateLimiter = (); type RateLimiterId = (); + type Task = (); + type DelayTasks = orml_xtokens::DisabledDelayTask; } impl orml_xcm::Config for Runtime { diff --git a/delay-tasks/Cargo.toml b/delay-tasks/Cargo.toml new file mode 100644 index 000000000..fdb5f9424 --- /dev/null +++ b/delay-tasks/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "orml-delay-tasks" +description = "Scheduler delay task and execute." +repository = "https://github.com/open-web3-stack/open-runtime-module-library/tree/master/auction" +license = "Apache-2.0" +version = "0.7.0" +authors = ["Acala Developers"] +edition = "2021" + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +scale-info = { workspace = true } +serde = { workspace = true, optional = true } + +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +xcm = { workspace = true } + +orml-traits = { path = "../traits", version = "0.7.0", default-features = false } +orml-xtokens = { path = "../xtokens", version = "0.7.0", default-features = false } + +[dev-dependencies] +sp-core = { workspace = true, features = ["std"] } +sp-io = { workspace = true, features = ["std"] } +pallet-balances = { workspace = true, features = ["std"] } +pallet-preimage = { workspace = true, features = ["std"] } +pallet-scheduler = { workspace = true, features = ["std"] } +orml-tokens = { path = "../tokens", version = "0.7.0", features = ["std"] } + +cumulus-pallet-xcm = { workspace = true, features = ["std"] } +orml-xcm-mock-message-queue = { path = "../xcm-mock-message-queue" } +orml-xcm = { path = "../xcm" } +orml-xcm-support = { path = "../xcm-support" } +pallet-xcm = { workspace = true, features = ["std"] } +xcm-builder = { workspace = true, features = ["std"] } +xcm-executor = { workspace = true, features = ["std"] } +polkadot-parachain-primitives = { workspace = true, features = ["std"] } +polkadot-runtime-parachains = { workspace = true, features = ["std"] } +polkadot-runtime-common = { workspace = true, features = ["std"] } +xcm-simulator = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "frame-support/std", + "frame-system/std", + "orml-traits/std", + "orml-xtokens/std", + "parity-scale-codec/std", + "scale-info/std", + "serde", + "sp-runtime/std", + "sp-std/std", + "xcm/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/delay-tasks/src/lib.rs b/delay-tasks/src/lib.rs new file mode 100644 index 000000000..7990bf05b --- /dev/null +++ b/delay-tasks/src/lib.rs @@ -0,0 +1,335 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + pallet_prelude::*, + traits::{ + schedule::{v1::Named as ScheduleNamed, DispatchTime}, + OriginTrait, + }, + weights::Weight, +}; +use frame_system::pallet_prelude::*; +use orml_traits::{ + task::{DelayTaskHooks, DelayTasksManager, DispatchableTask}, + MultiCurrency, NamedMultiReservableCurrency, +}; +use orml_xtokens::XtokensTask; +use parity_scale_codec::FullCodec; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{CheckedAdd, Convert, Zero}, + ArithmeticError, +}; +use sp_std::fmt::Debug; +use sp_std::marker::PhantomData; +use xcm::v4::prelude::*; + +pub use module::*; + +mod mock; +mod tests; + +pub const DELAY_TASK_ID: [u8; 8] = *b"orml/dts"; + +/// A delayed origin. Can only be dispatched via `dispatch_as` with a delay. +#[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo, MaxEncodedLen)] +pub struct DelayedExecuteOrigin; + +pub struct EnsureDelayed; +impl> + From> EnsureOrigin for EnsureDelayed { + type Success = (); + fn try_origin(o: O) -> Result { + o.into().map(|_| ()) + } + + #[cfg(feature = "runtime-benchmarks")] + fn try_successful_origin() -> Result { + Ok(O::from(DelayedExecuteOrigin)) + } +} + +#[frame_support::pallet] +pub mod module { + use super::*; + + type Nonce = u64; + + /// Origin for the delay tasks module. + #[pallet::origin] + pub type Origin = DelayedExecuteOrigin; + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + + type RuntimeCall: Parameter + From>; + + /// The outer origin type. + type RuntimeOrigin: From + + From<::RuntimeOrigin> + + OriginTrait; + + /// The caller origin, overarching type of all pallets origins. + type PalletsOrigin: Parameter + Into<::RuntimeOrigin>; + + type DelayOrigin: EnsureOrigin<::RuntimeOrigin>; + + type GovernanceOrigin: EnsureOrigin<::RuntimeOrigin>; + + type Task: DispatchableTask + FullCodec + Debug + Clone + PartialEq + TypeInfo; + + /// The Scheduler. + type Scheduler: ScheduleNamed, ::RuntimeCall, Self::PalletsOrigin>; + + type DelayTaskHooks: DelayTaskHooks; + + /// Convert `Location` to `CurrencyId`. + type CurrencyIdConvert: Convert< + Location, + Option<>::CurrencyId>, + >; + + type Currency: NamedMultiReservableCurrency; + + type ReserveId: Get<>::ReserveIdentifier>; + } + + #[pallet::error] + pub enum Error { + InvalidDelayBlock, + InvalidId, + FailedToSchedule, + AssetIndexNonExistent, + AssetConvertFailed, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + DelayedTaskAdded { + id: Nonce, + task: T::Task, + execute_block: BlockNumberFor, + }, + DelayedTaskExecuted { + id: Nonce, + result: DispatchResult, + }, + DelayedTaskReDelayed { + id: Nonce, + execute_block: BlockNumberFor, + }, + DelayedTaskCanceled { + id: Nonce, + }, + DelayedTaskStuck { + id: Nonce, + error: DispatchError, + }, + } + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::hooks] + impl Hooks> for Pallet {} + + #[pallet::storage] + #[pallet::getter(fn next_delayed_task_id)] + pub type NextDelayedTaskId = StorageValue<_, Nonce, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn delayed_tasks)] + pub type DelayedTasks = StorageMap<_, Twox64Concat, Nonce, (T::Task, BlockNumberFor), OptionQuery>; + + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight(Weight::zero())] + pub fn delayed_execute(origin: OriginFor, id: Nonce) -> DispatchResult { + T::DelayOrigin::ensure_origin(origin)?; + + let (delayed_task, _) = DelayedTasks::::get(id).ok_or(Error::::InvalidId)?; + + // pre delayed execute + if let Err(error) = T::DelayTaskHooks::pre_delayed_execute(&delayed_task) { + Self::deposit_event(Event::::DelayedTaskStuck { id, error }); + } else { + let execute_result = delayed_task.dispatch(Weight::zero()); + + DelayedTasks::::remove(id); + Self::deposit_event(Event::::DelayedTaskExecuted { + id, + result: execute_result.result, + }); + } + + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight(Weight::zero())] + pub fn reschedule_delay_task( + origin: OriginFor, + id: Nonce, + when: DispatchTime>, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + DelayedTasks::::try_mutate_exists(id, |maybe_task| -> DispatchResult { + let (_, execute_block) = maybe_task.as_mut().ok_or(Error::::InvalidId)?; + + let now = frame_system::Pallet::::block_number(); + let new_execute_block = match when { + DispatchTime::At(x) => x, + DispatchTime::After(x) => x.checked_add(&now).ok_or(ArithmeticError::Overflow)?, + }; + ensure!(new_execute_block > now, Error::::InvalidDelayBlock); + + T::Scheduler::reschedule_named((&DELAY_TASK_ID, id).encode(), DispatchTime::At(new_execute_block)) + .map_err(|_| Error::::FailedToSchedule)?; + + *execute_block = new_execute_block; + + Self::deposit_event(Event::::DelayedTaskReDelayed { + id, + execute_block: new_execute_block, + }); + Ok(()) + })?; + + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight(Weight::zero())] + pub fn cancel_delayed_task(origin: OriginFor, id: Nonce, skip_pre_cancel: bool) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + let (task, execute_block) = DelayedTasks::::take(id).ok_or(Error::::InvalidId)?; + + if !skip_pre_cancel { + T::DelayTaskHooks::pre_cancel(&task)?; + } + + if frame_system::Pallet::::block_number() < execute_block { + // if now < execute_block, need cancel scheduler + T::Scheduler::cancel_named((&DELAY_TASK_ID, id).encode()).map_err(|_| Error::::FailedToSchedule)?; + } + + Self::deposit_event(Event::::DelayedTaskCanceled { id }); + Ok(()) + } + } + + impl Pallet { + /// Retrieves the next delayed task ID from storage, and increment it by + /// one. + fn get_next_delayed_task_id() -> Result { + NextDelayedTaskId::::mutate(|current| -> Result { + let id = *current; + + *current = current.checked_add(1).ok_or(ArithmeticError::Overflow)?; + Ok(id) + }) + } + } + + impl DelayTasksManager> for Pallet { + fn add_delay_task(task: T::Task, delay_blocks: BlockNumberFor) -> DispatchResult { + ensure!(!delay_blocks.is_zero(), Error::::InvalidDelayBlock); + let execute_block = frame_system::Pallet::::block_number() + .checked_add(&delay_blocks) + .ok_or(ArithmeticError::Overflow)?; + + // pre schedule delay task + T::DelayTaskHooks::pre_delay(&task)?; + + let id = Self::get_next_delayed_task_id()?; + let delayed_origin: ::RuntimeOrigin = From::from(DelayedExecuteOrigin); + let pallets_origin = delayed_origin.caller().clone(); + + T::Scheduler::schedule_named( + (&DELAY_TASK_ID, id).encode(), + DispatchTime::At(execute_block), + None, + Zero::zero(), + pallets_origin, + ::RuntimeCall::from(Call::::delayed_execute { id }), + ) + .map_err(|_| Error::::FailedToSchedule)?; + + DelayedTasks::::insert(id, (&task, execute_block)); + + Self::deposit_event(Event::::DelayedTaskAdded { + id, + task, + execute_block, + }); + Ok(()) + } + } + + pub struct DelayedXtokensTaskHooks(PhantomData); + impl DelayTaskHooks> for DelayedXtokensTaskHooks + where + ::Currency: MultiCurrency< + T::AccountId, + CurrencyId = ::CurrencyId, + Balance = ::Balance, + >, + { + fn pre_delay(task: &XtokensTask) -> DispatchResult { + match task { + XtokensTask::::TransferAssets { who, assets, .. } => { + let asset_len = assets.len(); + for i in 0..asset_len { + let asset = assets.get(i).ok_or(Error::::AssetIndexNonExistent)?; + let currency_id: >::CurrencyId = + ::CurrencyIdConvert::convert(asset.id.0.clone()) + .ok_or(Error::::AssetConvertFailed)?; + let amount: T::Balance = match asset.fun { + Fungibility::Fungible(amount) => { + amount.try_into().map_err(|_| Error::::AssetConvertFailed)? + } + Fungibility::NonFungible(_) => return Err(Error::::AssetConvertFailed.into()), + }; + + T::Currency::reserve_named(&T::ReserveId::get(), currency_id, who, amount)?; + } + } + } + + Ok(()) + } + + fn pre_delayed_execute(task: &XtokensTask) -> DispatchResult { + match task { + XtokensTask::::TransferAssets { who, assets, .. } => { + let asset_len = assets.len(); + for i in 0..asset_len { + let asset = assets.get(i).ok_or(Error::::AssetIndexNonExistent)?; + let currency_id: >::CurrencyId = + ::CurrencyIdConvert::convert(asset.id.0.clone()) + .ok_or(Error::::AssetConvertFailed)?; + let amount: T::Balance = match asset.fun { + Fungibility::Fungible(amount) => { + amount.try_into().map_err(|_| Error::::AssetConvertFailed)? + } + Fungibility::NonFungible(_) => return Err(Error::::AssetConvertFailed.into()), + }; + + T::Currency::unreserve_named(&T::ReserveId::get(), currency_id, who, amount); + } + } + } + + Ok(()) + } + + fn pre_cancel(task: &XtokensTask) -> DispatchResult { + Self::pre_delayed_execute(task) + } + } +} diff --git a/delay-tasks/src/mock.rs b/delay-tasks/src/mock.rs new file mode 100644 index 000000000..7edd1e1f2 --- /dev/null +++ b/delay-tasks/src/mock.rs @@ -0,0 +1,453 @@ +//! Mocks for the delay tasks module. + +#![cfg(test)] + +use super::*; +use frame_support::{ + construct_runtime, derive_impl, parameter_types, + traits::{EqualPrivilegeOnly, Everything}, +}; +use frame_system::EnsureRoot; +use orml_traits::{ + define_combined_task_and_bind_delay_hooks, location::AbsoluteReserveProvider, parameter_type_with_key, + task::TaskResult, +}; +use serde::{Deserialize, Serialize}; +use sp_runtime::{traits::IdentityLookup, AccountId32, BuildStorage, DispatchError}; +use sp_std::cell::RefCell; + +use crate as delay_tasks; + +pub type AccountId = AccountId32; +pub type Amount = i128; +pub type Balance = u128; +pub type ReserveIdentifier = [u8; 8]; + +pub const ALICE: AccountId32 = AccountId32::new([0u8; 32]); +pub const BOB: AccountId32 = AccountId32::new([1u8; 32]); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for Runtime { + type Nonce = u64; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Block = Block; +} + +impl pallet_preimage::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); + type Currency = (); + type ManagerOrigin = EnsureRoot; + type Consideration = (); +} + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(2_000_000_000_000, 0).set_proof_size(u64::MAX)); + pub MaximumSchedulerWeight: Weight = BlockWeights::get().max_block; +} + +impl pallet_scheduler::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type PalletsOrigin = OriginCaller; + type RuntimeCall = RuntimeCall; + type MaximumWeight = MaximumSchedulerWeight; + type ScheduleOrigin = EnsureRoot; + type MaxScheduledPerBlock = ConstU32<10>; + type WeightInfo = (); + type OriginPrivilegeCmp = EqualPrivilegeOnly; + type Preimages = Preimage; +} + +#[derive( + Encode, + Decode, + Eq, + PartialEq, + Copy, + Clone, + RuntimeDebug, + PartialOrd, + Ord, + parity_scale_codec::MaxEncodedLen, + TypeInfo, +)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum CurrencyId { + /// Relay chain token. + R, + /// Parachain A token. + A, + /// Parachain B token. + B, +} + +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), + Junction::from(BoundedVec::try_from(b"A".to_vec()).unwrap()), + ) + .into(), + ), + CurrencyId::B => Some( + ( + Parent, + Parachain(2), + Junction::from(BoundedVec::try_from(b"B".to_vec()).unwrap()), + ) + .into(), + ), + } + } +} +impl Convert> for CurrencyIdConvert { + fn convert(l: Location) -> Option { + let mut a: Vec = "A".into(); + a.resize(32, 0); + let mut b: Vec = "B".into(); + b.resize(32, 0); + + if l == Location::parent() { + return Some(CurrencyId::R); + } + match l.unpack() { + (parents, interior) if parents == 1 => match interior { + [Parachain(1), GeneralKey { data, .. }] if data.to_vec() == a => Some(CurrencyId::A), + [Parachain(2), GeneralKey { data, .. }] if data.to_vec() == b => Some(CurrencyId::B), + _ => None, + }, + (parents, interior) if parents == 0 => match interior { + [GeneralKey { data, .. }] if data.to_vec() == a => Some(CurrencyId::A), + [GeneralKey { data, .. }] if data.to_vec() == b => Some(CurrencyId::B), + _ => None, + }, + _ => None, + } + } +} +impl Convert> for CurrencyIdConvert { + fn convert(a: Asset) -> Option { + if let Asset { + fun: Fungible(_), + id: AssetId(id), + } = a + { + Self::convert(id) + } else { + Option::None + } + } +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: CurrencyId| -> Balance { + Default::default() + }; +} + +impl orml_tokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type Amount = Amount; + type CurrencyId = CurrencyId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = ConstU32<50>; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = ReserveIdentifier; + type DustRemovalWhitelist = Everything; +} + +parameter_types! { + pub SelfLocation: Location = Location::new(1, [Parachain(2000)]); +} + +pub struct AccountIdToLocation; +impl Convert for AccountIdToLocation { + fn convert(account: AccountId) -> Location { + [Junction::AccountId32 { + network: None, + id: account.into(), + }] + .into() + } +} + +parameter_type_with_key! { + pub ParachainMinFee: |location: Location| -> Option { + #[allow(clippy::match_ref_pats)] // false positive + match (location.parents, location.first_interior()) { + (1, Some(Parachain(3))) => Some(100), + _ => None, + } + }; +} + +pub enum Weightless {} +impl PreparedMessage for Weightless { + fn weight_of(&self) -> Weight { + unreachable!() + } +} + +pub struct MockExec; +impl ExecuteXcm for MockExec { + type Prepared = Weightless; + + fn prepare(_message: Xcm) -> Result> { + unreachable!() + } + + fn execute(_origin: impl Into, _pre: Weightless, _hash: &mut XcmHash, _weight_credit: Weight) -> Outcome { + unreachable!() + } + + fn charge_fees(_location: impl Into, _fees: Assets) -> XcmResult { + Err(XcmError::Unimplemented) + } +} + +parameter_types! { + pub UniversalLocation: InteriorLocation = Here; + pub const UnitWeightCost: Weight = Weight::from_parts(10, 10); + pub const BaseXcmWeight: Weight = Weight::from_parts(100_000_000, 100_000_000); + pub const MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; + pub const MaxAssetsForTransfer: usize = 2; +} + +impl orml_xtokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type CurrencyId = CurrencyId; + type CurrencyIdConvert = CurrencyIdConvert; + type AccountIdToLocation = AccountIdToLocation; + type SelfLocation = SelfLocation; + type XcmExecutor = MockExec; + type Weigher = xcm_builder::FixedWeightBounds; + type BaseXcmWeight = BaseXcmWeight; + type UniversalLocation = UniversalLocation; + type MaxAssetsForTransfer = MaxAssetsForTransfer; + type MinXcmFee = ParachainMinFee; + type LocationsFilter = Everything; + type ReserveProvider = AbsoluteReserveProvider; + type RateLimiter = (); + type RateLimiterId = (); + type Task = MockTaskType; + type DelayTasks = DelayTasks; +} + +thread_local! { + pub static DISPATCH_SUCCEEDED: RefCell = RefCell::new(0); + pub static DISPATCH_FAILED: RefCell = RefCell::new(0); + pub static PRE_DELAY_SUCCEEDED: RefCell = RefCell::new(0); + pub static PRE_DELAYED_EXECUTE_SUCCEEDED: RefCell = RefCell::new(0); + pub static PRE_CANCEL_SUCCEEDED: RefCell = RefCell::new(0); +} + +#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] +pub struct SuccessTask; +impl DispatchableTask for SuccessTask { + fn dispatch(self, _weight: Weight) -> TaskResult { + DISPATCH_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + + TaskResult { + result: Ok(()), + used_weight: Weight::zero(), + finished: true, + } + } +} +pub struct SuccessTaskHook; +impl DelayTaskHooks for SuccessTaskHook { + fn pre_delay(_: &SuccessTask) -> DispatchResult { + PRE_DELAY_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_delayed_execute(_: &SuccessTask) -> DispatchResult { + PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_cancel(_: &SuccessTask) -> DispatchResult { + PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } +} + +#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] +pub struct FailDispatchTask; +impl DispatchableTask for FailDispatchTask { + fn dispatch(self, _weight: Weight) -> TaskResult { + DISPATCH_FAILED.with(|v| *v.borrow_mut() += 1); + + TaskResult { + result: Err(DispatchError::Other("dispatch failed")), + used_weight: Weight::zero(), + finished: true, + } + } +} +pub struct FailDispatchTaskHook; +impl DelayTaskHooks for FailDispatchTaskHook { + fn pre_delay(_: &FailDispatchTask) -> DispatchResult { + PRE_DELAY_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_delayed_execute(_: &FailDispatchTask) -> DispatchResult { + PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_cancel(_: &FailDispatchTask) -> DispatchResult { + PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } +} + +#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] +pub struct FailPreDelayTask; +impl DispatchableTask for FailPreDelayTask { + fn dispatch(self, _weight: Weight) -> TaskResult { + unimplemented!() + } +} +pub struct FailPreDelayTaskHook; +impl DelayTaskHooks for FailPreDelayTaskHook { + fn pre_delay(_: &FailPreDelayTask) -> DispatchResult { + Err(DispatchError::Other("pre_delay failed")) + } + fn pre_delayed_execute(_: &FailPreDelayTask) -> DispatchResult { + unimplemented!() + } + fn pre_cancel(_: &FailPreDelayTask) -> DispatchResult { + unimplemented!() + } +} + +#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] +pub struct FailPreDelayedExecuteTask; +impl DispatchableTask for FailPreDelayedExecuteTask { + fn dispatch(self, _weight: Weight) -> TaskResult { + unimplemented!() + } +} +pub struct FailPreDelayedExecuteTaskHook; +impl DelayTaskHooks for FailPreDelayedExecuteTaskHook { + fn pre_delay(_: &FailPreDelayedExecuteTask) -> DispatchResult { + PRE_DELAY_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_delayed_execute(_: &FailPreDelayedExecuteTask) -> DispatchResult { + Err(DispatchError::Other("pre_delayed_execute failed")) + } + fn pre_cancel(_: &FailPreDelayedExecuteTask) -> DispatchResult { + PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } +} + +#[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] +pub struct FailPreCancelTask; +impl DispatchableTask for FailPreCancelTask { + fn dispatch(self, _weight: Weight) -> TaskResult { + DISPATCH_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + + TaskResult { + result: Ok(()), + used_weight: Weight::zero(), + finished: true, + } + } +} +pub struct FailPreCancelTaskHook; +impl DelayTaskHooks for FailPreCancelTaskHook { + fn pre_delay(_: &FailPreCancelTask) -> DispatchResult { + PRE_DELAY_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_delayed_execute(_: &FailPreCancelTask) -> DispatchResult { + PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow_mut() += 1); + Ok(()) + } + fn pre_cancel(_: &FailPreCancelTask) -> DispatchResult { + Err(DispatchError::Other("pre_cancel failed")) + } +} + +define_combined_task_and_bind_delay_hooks! { + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub enum MockTaskType { + Success(SuccessTask, SuccessTaskHook), + FailDispatch(FailDispatchTask, FailDispatchTaskHook), + FailPreDelay(FailPreDelayTask, FailPreDelayTaskHook), + FailPreDelayedExecute(FailPreDelayedExecuteTask, FailPreDelayedExecuteTaskHook), + FailPreCancel(FailPreCancelTask, FailPreCancelTaskHook), + Xtokens(XtokensTask, DelayedXtokensTaskHooks), + } +} + +parameter_types! { + pub ReserveId: ReserveIdentifier = [1u8;8]; +} + +impl delay_tasks::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type RuntimeOrigin = RuntimeOrigin; + type PalletsOrigin = OriginCaller; + type DelayOrigin = EnsureDelayed; + type GovernanceOrigin = EnsureRoot; + type Task = MockTaskType; + type Scheduler = Scheduler; + type DelayTaskHooks = MockTaskType; + type CurrencyIdConvert = CurrencyIdConvert; + type Currency = Tokens; + type ReserveId = ReserveId; +} + +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub enum Runtime { + System: frame_system, + DelayTasks: delay_tasks, + Scheduler: pallet_scheduler, + Preimage: pallet_preimage, + Tokens: orml_tokens, + XTokens: orml_xtokens, + } +); + +pub struct ExtBuilder; + +impl Default for ExtBuilder { + fn default() -> Self { + ExtBuilder + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + t.into() + } +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + Scheduler::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + Scheduler::on_initialize(System::block_number()); + } +} diff --git a/delay-tasks/src/tests.rs b/delay-tasks/src/tests.rs new file mode 100644 index 000000000..0c720c497 --- /dev/null +++ b/delay-tasks/src/tests.rs @@ -0,0 +1,434 @@ +//! Unit tests for the delay tasks. +#![cfg(test)] + +use super::*; +use frame_support::{assert_noop, assert_ok}; +use mock::*; +use sp_io::hashing::blake2_256; +use sp_runtime::traits::{BadOrigin, Bounded}; + +#[test] +fn add_delay_task_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + assert_noop!( + DelayTasks::add_delay_task(MockTaskType::Success(SuccessTask), 0), + Error::::InvalidDelayBlock + ); + + assert_noop!( + DelayTasks::add_delay_task(MockTaskType::FailPreDelay(FailPreDelayTask), 0), + Error::::InvalidDelayBlock + ); + + assert_eq!(PRE_DELAY_SUCCEEDED.with(|v| *v.borrow()), 0); + assert_eq!(DelayTasks::next_delayed_task_id(), 0); + assert_eq!(DelayTasks::delayed_tasks(0), None); + assert_ok!(DelayTasks::add_delay_task(MockTaskType::Success(SuccessTask), 10)); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Scheduled { when: 11, index: 0 }, + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskAdded { + id: 0, + task: MockTaskType::Success(SuccessTask), + execute_block: 11, + })); + + assert_eq!(PRE_DELAY_SUCCEEDED.with(|v| *v.borrow()), 1); + assert_eq!(DelayTasks::next_delayed_task_id(), 1); + assert_eq!( + DelayTasks::delayed_tasks(0), + Some((MockTaskType::Success(SuccessTask), 11)) + ); + }); +} + +#[test] +fn reschedule_delay_task_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_ok!(DelayTasks::add_delay_task(MockTaskType::Success(SuccessTask), 100)); + assert_ok!(DelayTasks::add_delay_task( + MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), + 9 + )); + assert_eq!( + DelayTasks::delayed_tasks(0), + Some((MockTaskType::Success(SuccessTask), 101)) + ); + assert_eq!( + DelayTasks::delayed_tasks(1), + Some((MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), 10)) + ); + + assert_noop!( + DelayTasks::reschedule_delay_task(RuntimeOrigin::signed(ALICE), 0, DispatchTime::At(10)), + BadOrigin + ); + + assert_noop!( + DelayTasks::reschedule_delay_task(RuntimeOrigin::root(), 2, DispatchTime::At(10)), + Error::::InvalidId + ); + + assert_noop!( + DelayTasks::reschedule_delay_task(RuntimeOrigin::root(), 0, DispatchTime::After(Bounded::max_value())), + ArithmeticError::Overflow + ); + + assert_ok!(DelayTasks::reschedule_delay_task( + RuntimeOrigin::root(), + 0, + DispatchTime::At(10) + )); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Canceled { when: 101, index: 0 }, + )); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Scheduled { when: 10, index: 0 }, + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskReDelayed { + id: 0, + execute_block: 10, + })); + + assert_eq!( + DelayTasks::delayed_tasks(0), + Some((MockTaskType::Success(SuccessTask), 10)) + ); + + run_to_block(10); + assert_eq!(DelayTasks::delayed_tasks(0), None); + + // scheduler dispatched delayed_execute call for task#1, + // but task#1 stuck for failed at pre_delayed_execute + assert_eq!( + DelayTasks::delayed_tasks(1), + Some((MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), 10)) + ); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Dispatched { + task: (10, 0), + id: Some(blake2_256(&(&DELAY_TASK_ID, 1u64).encode())), + result: Ok(()), + }, + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskStuck { + id: 1, + error: DispatchError::Other(""), + })); + + // cannot rescheduler stucked task + assert_noop!( + DelayTasks::reschedule_delay_task(RuntimeOrigin::root(), 1, DispatchTime::At(100)), + Error::::FailedToSchedule + ); + }); +} + +#[test] +fn cancel_delayed_task_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + assert_ok!(DelayTasks::add_delay_task(MockTaskType::Success(SuccessTask), 100)); + assert_ok!(DelayTasks::add_delay_task( + MockTaskType::FailPreCancel(FailPreCancelTask), + 100 + )); + assert_ok!(DelayTasks::add_delay_task( + MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), + 100 + )); + assert_eq!( + DelayTasks::delayed_tasks(0), + Some((MockTaskType::Success(SuccessTask), 101)) + ); + assert_eq!( + DelayTasks::delayed_tasks(1), + Some((MockTaskType::FailPreCancel(FailPreCancelTask), 101)) + ); + assert_eq!( + DelayTasks::delayed_tasks(2), + Some((MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), 101)) + ); + + assert_noop!( + DelayTasks::cancel_delayed_task(RuntimeOrigin::signed(ALICE), 0, false), + BadOrigin + ); + + assert_noop!( + DelayTasks::cancel_delayed_task(RuntimeOrigin::root(), 3, false), + Error::::InvalidId + ); + + assert_eq!(PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow()), 0); + assert_ok!(DelayTasks::cancel_delayed_task(RuntimeOrigin::root(), 0, false)); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Canceled { when: 101, index: 0 }, + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskCanceled { id: 0 })); + assert_eq!(DelayTasks::delayed_tasks(0), None); + assert_eq!(PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow()), 1); + + // failed cancel for failed on pre_cancel + assert_noop!( + DelayTasks::cancel_delayed_task(RuntimeOrigin::root(), 1, false), + DispatchError::Other("pre_cancel failed"), + ); + + // cancel by skip pre_cancel + assert_ok!(DelayTasks::cancel_delayed_task(RuntimeOrigin::root(), 1, true)); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Canceled { when: 101, index: 1 }, + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskCanceled { id: 1 })); + assert_eq!(DelayTasks::delayed_tasks(1), None); + assert_eq!(PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow()), 1); // skip pre_cancel + + run_to_block(101); + + // scheduler dispatched delayed_execute call for task#2, + // but task#2 stuck for failed at pre_delayed_execute + assert_eq!( + DelayTasks::delayed_tasks(2), + Some((MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), 101)) + ); + System::assert_has_event(mock::RuntimeEvent::Scheduler( + pallet_scheduler::Event::::Dispatched { + task: (101, 2), + id: Some(blake2_256(&(&DELAY_TASK_ID, 2u64).encode())), + result: Ok(()), + }, + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskStuck { + id: 2, + error: DispatchError::Other(""), + })); + + // cancel stuck task#2 + assert_ok!(DelayTasks::cancel_delayed_task(RuntimeOrigin::root(), 2, false)); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskCanceled { id: 2 })); + assert_eq!(DelayTasks::delayed_tasks(2), None); + assert_eq!(PRE_CANCEL_SUCCEEDED.with(|v| *v.borrow()), 2); + }); +} + +#[test] +fn do_delayed_execute_work() { + ExtBuilder::default().build().execute_with(|| { + System::set_block_number(1); + + assert_noop!(DelayTasks::delayed_execute(RuntimeOrigin::root(), 0), BadOrigin); + assert_noop!(DelayTasks::delayed_execute(RuntimeOrigin::signed(ALICE), 0), BadOrigin); + + assert_noop!( + DelayTasks::delayed_execute(RuntimeOrigin::from(DelayedExecuteOrigin), 0), + Error::::InvalidId + ); + + assert_ok!(DelayTasks::add_delay_task(MockTaskType::Success(SuccessTask), 100)); + assert_ok!(DelayTasks::add_delay_task( + MockTaskType::FailDispatch(FailDispatchTask), + 100 + )); + assert_ok!(DelayTasks::add_delay_task( + MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), + 100 + )); + + assert_eq!( + DelayTasks::delayed_tasks(0), + Some((MockTaskType::Success(SuccessTask), 101)) + ); + assert_eq!( + DelayTasks::delayed_tasks(1), + Some((MockTaskType::FailDispatch(FailDispatchTask), 101)) + ); + assert_eq!( + DelayTasks::delayed_tasks(2), + Some((MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), 101)) + ); + + assert_eq!(DISPATCH_SUCCEEDED.with(|v| *v.borrow()), 0); + assert_eq!(DISPATCH_FAILED.with(|v| *v.borrow()), 0); + assert_eq!(PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow()), 0); + + // delayed task executed, and succeeded + assert_ok!(DelayTasks::delayed_execute( + RuntimeOrigin::from(DelayedExecuteOrigin), + 0 + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskExecuted { + id: 0, + result: Ok(()), + })); + assert_eq!(DelayTasks::delayed_tasks(0), None); + assert_eq!(DISPATCH_SUCCEEDED.with(|v| *v.borrow()), 1); + assert_eq!(DISPATCH_FAILED.with(|v| *v.borrow()), 0); + assert_eq!(PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow()), 1); + + // delayed task executed, and failed + assert_ok!(DelayTasks::delayed_execute( + RuntimeOrigin::from(DelayedExecuteOrigin), + 1 + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskExecuted { + id: 1, + result: Err(DispatchError::Other("")), + })); + assert_eq!(DelayTasks::delayed_tasks(1), None); + assert_eq!(DISPATCH_SUCCEEDED.with(|v| *v.borrow()), 1); + assert_eq!(DISPATCH_FAILED.with(|v| *v.borrow()), 1); + assert_eq!(PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow()), 2); + + // delayed task stuck for failed pre_delayed_execute + assert_ok!(DelayTasks::delayed_execute( + RuntimeOrigin::from(DelayedExecuteOrigin), + 2 + )); + System::assert_has_event(mock::RuntimeEvent::DelayTasks(Event::DelayedTaskStuck { + id: 2, + error: DispatchError::Other(""), + })); + assert_eq!( + DelayTasks::delayed_tasks(2), + Some((MockTaskType::FailPreDelayedExecute(FailPreDelayedExecuteTask), 101)) + ); + assert_eq!(DISPATCH_SUCCEEDED.with(|v| *v.borrow()), 1); + assert_eq!(DISPATCH_FAILED.with(|v| *v.borrow()), 1); + assert_eq!(PRE_DELAYED_EXECUTE_SUCCEEDED.with(|v| *v.borrow()), 2); + }); +} + +#[test] +fn delayed_xtokens_task_hooks_work() { + ExtBuilder::default().build().execute_with(|| { + let assets: Assets = Assets::from(vec![ + (Location::parent(), 1000).into(), + ( + ( + Parent, + Parachain(1), + Junction::from(BoundedVec::try_from(b"A".to_vec()).unwrap()), + ), + 2000, + ) + .into(), + ( + ( + Parent, + Parachain(2), + Junction::from(BoundedVec::try_from(b"B".to_vec()).unwrap()), + ), + 3000, + ) + .into(), + ]); + let fee: Asset = (Location::parent(), 1000).into(); + let dest: Location = ( + Parent, + Parachain(2), + Junction::AccountId32 { + network: None, + id: BOB.into(), + }, + ) + .into(); + let task = XtokensTask::::TransferAssets { + who: ALICE, + assets, + fee, + dest, + dest_weight_limit: WeightLimit::Unlimited, + }; + + assert_ok!(Tokens::deposit(CurrencyId::R, &ALICE, 3000)); + assert_ok!(Tokens::deposit(CurrencyId::A, &ALICE, 3000)); + assert_ok!(Tokens::deposit(CurrencyId::B, &ALICE, 3000)); + assert_eq!(Tokens::free_balance(CurrencyId::R, &ALICE), 3000); + assert_eq!(Tokens::free_balance(CurrencyId::A, &ALICE), 3000); + assert_eq!(Tokens::free_balance(CurrencyId::B, &ALICE), 3000); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::R, &ALICE), + 0 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::A, &ALICE), + 0 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::B, &ALICE), + 0 + ); + + assert_ok!(DelayedXtokensTaskHooks::::pre_delay(&task)); + assert_eq!(Tokens::free_balance(CurrencyId::R, &ALICE), 2000); + assert_eq!(Tokens::free_balance(CurrencyId::A, &ALICE), 1000); + assert_eq!(Tokens::free_balance(CurrencyId::B, &ALICE), 0); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::R, &ALICE), + 1000 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::A, &ALICE), + 2000 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::B, &ALICE), + 3000 + ); + + assert_ok!(DelayedXtokensTaskHooks::::pre_delayed_execute(&task)); + assert_eq!(Tokens::free_balance(CurrencyId::R, &ALICE), 3000); + assert_eq!(Tokens::free_balance(CurrencyId::A, &ALICE), 3000); + assert_eq!(Tokens::free_balance(CurrencyId::B, &ALICE), 3000); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::R, &ALICE), + 0 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::A, &ALICE), + 0 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::B, &ALICE), + 0 + ); + + assert_ok!(DelayedXtokensTaskHooks::::pre_delay(&task)); + assert_eq!(Tokens::free_balance(CurrencyId::R, &ALICE), 2000); + assert_eq!(Tokens::free_balance(CurrencyId::A, &ALICE), 1000); + assert_eq!(Tokens::free_balance(CurrencyId::B, &ALICE), 0); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::R, &ALICE), + 1000 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::A, &ALICE), + 2000 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::B, &ALICE), + 3000 + ); + + assert_ok!(DelayedXtokensTaskHooks::::pre_cancel(&task)); + assert_eq!(Tokens::free_balance(CurrencyId::R, &ALICE), 3000); + assert_eq!(Tokens::free_balance(CurrencyId::A, &ALICE), 3000); + assert_eq!(Tokens::free_balance(CurrencyId::B, &ALICE), 3000); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::R, &ALICE), + 0 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::A, &ALICE), + 0 + ); + assert_eq!( + Tokens::reserved_balance_named(&ReserveId::get(), CurrencyId::B, &ALICE), + 0 + ); + }); +} diff --git a/rate-limit/src/lib.rs b/rate-limit/src/lib.rs index 50d49dcbe..33b2656b4 100644 --- a/rate-limit/src/lib.rs +++ b/rate-limit/src/lib.rs @@ -112,6 +112,8 @@ pub mod module { /// Exceed the allowed maximum number of KeyFilter configured to a /// RateLimiterId. MaxFilterExceeded, + /// Delay block must not be zero. + ZeroDelayBlock, } #[pallet::event] @@ -129,6 +131,10 @@ pub mod module { WhitelistFilterRemoved { rate_limiter_id: T::RateLimiterId }, /// The whitelist of bypass rate limit has been reset. WhitelistFilterReset { rate_limiter_id: T::RateLimiterId }, + AllowDelayBlockChanged { + rate_limiter_id: T::RateLimiterId, + update: Option>, + }, } /// The rate limit rule for specific RateLimiterId and encoded key. @@ -156,6 +162,12 @@ pub mod module { pub type LimitWhitelist = StorageMap<_, Twox64Concat, T::RateLimiterId, OrderedSet, ValueQuery>; + /// Allow delay (not consume quota) when limit check for specific + /// RateLimiterId + #[pallet::storage] + #[pallet::getter(fn allow_delay)] + pub type AllowDelay = StorageMap<_, Twox64Concat, T::RateLimiterId, BlockNumberFor, OptionQuery>; + #[pallet::pallet] #[pallet::without_storage_info] pub struct Pallet(_); @@ -317,6 +329,37 @@ pub mod module { Self::deposit_event(Event::WhitelistFilterReset { rate_limiter_id }); Ok(()) } + + /// Allow delayed when limit check. + /// + /// Requires `GovernanceOrigin` + /// + /// Parameters: + /// - `rate_limiter_id`: rate limiter id. + /// - `allow_delayed_block`: allow delay when limit check. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::reset_whitelist())] + #[transactional] + pub fn allow_delay_block( + origin: OriginFor, + rate_limiter_id: T::RateLimiterId, + allow_delayed_block: Option>, + ) -> DispatchResult { + T::GovernanceOrigin::ensure_origin(origin)?; + + if let Some(dely_block) = allow_delayed_block { + ensure!(!dely_block.is_zero(), Error::::ZeroDelayBlock); + AllowDelay::::insert(rate_limiter_id, dely_block); + } else { + AllowDelay::::remove(rate_limiter_id); + } + + Self::deposit_event(Event::AllowDelayBlockChanged { + rate_limiter_id, + update: allow_delayed_block, + }); + Ok(()) + } } impl Pallet { @@ -381,7 +424,7 @@ pub mod module { } } - impl RateLimiter for Pallet { + impl RateLimiter> for Pallet { type RateLimiterId = T::RateLimiterId; fn is_whitelist(limiter_id: Self::RateLimiterId, key: impl Encode) -> bool { @@ -410,31 +453,33 @@ pub mod module { false } - fn can_consume(limiter_id: Self::RateLimiterId, key: impl Encode, value: u128) -> Result<(), RateLimiterError> { + fn can_consume( + limiter_id: Self::RateLimiterId, + key: impl Encode, + value: u128, + ) -> Result<(), RateLimiterError>> { let encoded_key: Vec = key.encode(); - let allowed = match RateLimitRules::::get(limiter_id, &encoded_key) { + match RateLimitRules::::get(limiter_id, &encoded_key) { Some(rate_limit_rule @ RateLimitRule::PerPeriod { .. }) | Some(rate_limit_rule @ RateLimitRule::TokenBucket { .. }) => { let remainer_quota = Self::access_remainer_quota_after_update(rate_limit_rule, &limiter_id, &encoded_key); - value <= remainer_quota - } - Some(RateLimitRule::Unlimited) => true, - Some(RateLimitRule::NotAllowed) => { - // always return false, even if the value is zero. - false - } - None => { - // if doesn't rate limit rule, always return true. - true - } - }; - - ensure!(allowed, RateLimiterError::ExceedLimit); + if value > remainer_quota { + if let Some(delay_block) = AllowDelay::::get(limiter_id) { + return Err(RateLimiterError::Delay { duration: delay_block }); + } else { + return Err(RateLimiterError::Deny); + } + } - Ok(()) + Ok(()) + } + Some(RateLimitRule::Unlimited) => Ok(()), + Some(RateLimitRule::NotAllowed) => Err(RateLimiterError::Deny), + None => Ok(()), + } } fn consume(limiter_id: Self::RateLimiterId, key: impl Encode, value: u128) { diff --git a/rate-limit/src/tests.rs b/rate-limit/src/tests.rs index 4a4817b55..08eb9d041 100644 --- a/rate-limit/src/tests.rs +++ b/rate-limit/src/tests.rs @@ -392,6 +392,37 @@ fn reset_whitelist_work() { }); } +#[test] +fn allow_delay_block_work() { + ExtBuilder::default().build().execute_with(|| { + assert_noop!( + RateLimit::allow_delay_block(RuntimeOrigin::signed(ALICE), 0, Some(1),), + BadOrigin + ); + + // delay block is zero + assert_noop!( + RateLimit::allow_delay_block(RuntimeOrigin::root(), 0, Some(0),), + Error::::ZeroDelayBlock + ); + + assert_eq!(RateLimit::allow_delay(0), None); + assert_ok!(RateLimit::allow_delay_block(RuntimeOrigin::root(), 0, Some(100),)); + System::assert_last_event(RuntimeEvent::RateLimit(crate::Event::AllowDelayBlockChanged { + rate_limiter_id: 0, + update: Some(100), + })); + assert_eq!(RateLimit::allow_delay(0), Some(100)); + + assert_ok!(RateLimit::allow_delay_block(RuntimeOrigin::root(), 0, None,)); + System::assert_last_event(RuntimeEvent::RateLimit(crate::Event::AllowDelayBlockChanged { + rate_limiter_id: 0, + update: None, + })); + assert_eq!(RateLimit::allow_delay(0), None); + }); +} + #[test] fn is_whitelist_work() { ExtBuilder::default().build().execute_with(|| { @@ -799,12 +830,12 @@ fn can_consume_work() { assert_eq!(RateLimit::rate_limit_quota(0, DOT.encode()), (0, 0)); assert_ok!(RateLimit::can_consume(0, DOT, 0)); - assert_eq!(RateLimit::can_consume(0, DOT, 500), Err(RateLimiterError::ExceedLimit),); + assert_eq!(RateLimit::can_consume(0, DOT, 500), Err(RateLimiterError::Deny),); assert_eq!(RateLimit::rate_limit_quota(0, DOT.encode()), (0, 0)); assert_eq!(RateLimit::rate_limit_quota(1, DOT.encode()), (0, 0)); assert_ok!(RateLimit::can_consume(1, DOT, 0)); - assert_eq!(RateLimit::can_consume(1, DOT, 501), Err(RateLimiterError::ExceedLimit),); + assert_eq!(RateLimit::can_consume(1, DOT, 501), Err(RateLimiterError::Deny),); assert_eq!(RateLimit::rate_limit_quota(1, DOT.encode()), (0, 0)); System::set_block_number(100); @@ -813,20 +844,29 @@ fn can_consume_work() { assert_eq!(RateLimit::rate_limit_quota(0, DOT.encode()), (0, 0)); assert_ok!(RateLimit::can_consume(0, DOT, 500)); assert_eq!(RateLimit::rate_limit_quota(0, DOT.encode()), (100, 500)); - assert_eq!(RateLimit::can_consume(0, DOT, 501), Err(RateLimiterError::ExceedLimit),); + assert_eq!(RateLimit::can_consume(0, DOT, 501), Err(RateLimiterError::Deny),); assert_eq!(RateLimit::rate_limit_quota(0, DOT.encode()), (100, 500)); assert_eq!(RateLimit::rate_limit_quota(1, DOT.encode()), (0, 0)); assert_ok!(RateLimit::can_consume(1, DOT, 501)); assert_eq!(RateLimit::rate_limit_quota(1, DOT.encode()), (100, 1000)); - assert_eq!(RateLimit::can_consume(1, DOT, 1001), Err(RateLimiterError::ExceedLimit),); + assert_eq!(RateLimit::can_consume(1, DOT, 1001), Err(RateLimiterError::Deny),); assert_eq!(RateLimit::rate_limit_quota(1, DOT.encode()), (100, 1000)); + // if config allow delay for rate limit id 0, return RateLimiterError::Delay + assert_ok!(RateLimit::allow_delay_block(RuntimeOrigin::root(), 0, Some(10000),)); + assert_eq!(RateLimit::rate_limit_quota(0, DOT.encode()), (100, 500)); + assert_ok!(RateLimit::can_consume(0, DOT, 500)); + assert_eq!( + RateLimit::can_consume(0, DOT, 501), + Err(RateLimiterError::Delay { duration: 10000 }) + ); + // NotAllowed always return error, even if value is 0 RateLimitQuota::::mutate(0, BTC.encode(), |(_, remainer_quota)| *remainer_quota = 10000); assert_eq!(RateLimit::rate_limit_quota(0, BTC.encode()), (0, 10000)); - assert_eq!(RateLimit::can_consume(0, BTC, 0), Err(RateLimiterError::ExceedLimit),); - assert_eq!(RateLimit::can_consume(0, BTC, 100), Err(RateLimiterError::ExceedLimit),); + assert_eq!(RateLimit::can_consume(0, BTC, 0), Err(RateLimiterError::Deny),); + assert_eq!(RateLimit::can_consume(0, BTC, 100), Err(RateLimiterError::Deny),); // Unlimited always return true assert_eq!(RateLimit::rate_limit_quota(1, BTC.encode()), (0, 0)); diff --git a/traits/src/lib.rs b/traits/src/lib.rs index c59e7340b..63c3453ae 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -40,6 +40,7 @@ pub mod parameters; pub mod price; pub mod rate_limit; pub mod rewards; +pub mod task; pub mod xcm_transfer; /// New data handler diff --git a/traits/src/rate_limit.rs b/traits/src/rate_limit.rs index 2abac8363..8e896fbb7 100644 --- a/traits/src/rate_limit.rs +++ b/traits/src/rate_limit.rs @@ -3,12 +3,13 @@ use parity_scale_codec::Encode; use sp_runtime::{traits::Member, RuntimeDebug}; #[derive(PartialEq, Eq, RuntimeDebug)] -pub enum RateLimiterError { - ExceedLimit, +pub enum RateLimiterError { + Deny, + Delay { duration: Duration }, } /// Rate Limiter -pub trait RateLimiter { +pub trait RateLimiter { /// The type for the rate limiter. type RateLimiterId: Parameter + Member + Copy; @@ -22,7 +23,7 @@ pub trait RateLimiter { limiter_id: Self::RateLimiterId, limit_key: impl Encode, value: u128, - ) -> Result<(), RateLimiterError>; + ) -> Result<(), RateLimiterError>; /// The handler function to consume quota. fn consume(limiter_id: Self::RateLimiterId, limit_key: impl Encode, value: u128); @@ -33,29 +34,30 @@ pub trait RateLimiter { limit_key: impl Encode + Clone, value: u128, whitelist_check: Option, - ) -> Result<(), RateLimiterError> { + ) -> Result<(), RateLimiterError> { let need_consume = match whitelist_check { Some(whitelist_key) => !Self::is_whitelist(limiter_id, whitelist_key), None => true, }; if need_consume { - Self::can_consume(limiter_id, limit_key.clone(), value)?; - Self::consume(limiter_id, limit_key, value); + Self::can_consume(limiter_id, limit_key.clone(), value).map(|_| { + Self::consume(limiter_id, limit_key, value); + })?; } Ok(()) } } -impl RateLimiter for () { +impl RateLimiter for () { type RateLimiterId = (); fn is_whitelist(_: Self::RateLimiterId, _: impl Encode) -> bool { true } - fn can_consume(_: Self::RateLimiterId, _: impl Encode, _: u128) -> Result<(), RateLimiterError> { + fn can_consume(_: Self::RateLimiterId, _: impl Encode, _: u128) -> Result<(), RateLimiterError> { Ok(()) } diff --git a/traits/src/task.rs b/traits/src/task.rs new file mode 100644 index 000000000..0cbba710e --- /dev/null +++ b/traits/src/task.rs @@ -0,0 +1,147 @@ +use frame_support::weights::Weight; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_runtime::DispatchResult; +use sp_runtime::RuntimeDebug; + +#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct TaskResult { + pub result: DispatchResult, + pub used_weight: Weight, + pub finished: bool, +} + +/// Dispatchable tasks +pub trait DispatchableTask { + fn dispatch(self, weight: Weight) -> TaskResult; +} + +#[cfg(feature = "std")] +impl DispatchableTask for () { + fn dispatch(self, _weight: Weight) -> TaskResult { + unimplemented!() + } +} + +pub trait DelayTasksManager { + fn add_delay_task(task: Task, delay_blocks: BlockNumber) -> DispatchResult; +} + +pub trait DelayTaskHooks { + fn pre_delay(task: &Task) -> DispatchResult; + fn pre_delayed_execute(task: &Task) -> DispatchResult; + fn pre_cancel(task: &Task) -> DispatchResult; +} + +impl DelayTaskHooks for () { + fn pre_delay(_: &Task) -> DispatchResult { + Ok(()) + } + fn pre_delayed_execute(_: &Task) -> DispatchResult { + Ok(()) + } + fn pre_cancel(_: &Task) -> DispatchResult { + Ok(()) + } +} + +#[macro_export] +macro_rules! define_combined_task { + ( + $(#[$meta:meta])* + $vis:vis enum $combined_name:ident { + $( + $task:ident ( $vtask:ident $(<$($generic:tt),*>)? ) + ),+ $(,)? + } + ) => { + $(#[$meta])* + $vis enum $combined_name { + $( + $task($vtask $(<$($generic),*>)?), + )* + } + + impl DispatchableTask for $combined_name { + fn dispatch(self, weight: Weight) -> TaskResult { + match self { + $( + $combined_name::$task(t) => t.dispatch(weight), + )* + } + } + } + + $( + impl From<$vtask $(<$($generic),*>)?> for $combined_name { + fn from(t: $vtask $(<$($generic),*>)?) -> Self{ + $combined_name::$task(t) + } + } + )* + }; +} + +#[macro_export] +macro_rules! define_combined_task_and_bind_delay_hooks { + ( + $(#[$meta:meta])* + $vis:vis enum $combined_name:ident { + $( + $task:ident ( $vtask:ident $(<$($generic:tt),*>)? , $hook:ty ) + ),+ $(,)? + } + ) => { + $(#[$meta])* + $vis enum $combined_name { + $( + $task($vtask $(<$($generic),*>)?), + )* + } + + impl DispatchableTask for $combined_name { + fn dispatch(self, weight: Weight) -> TaskResult { + match self { + $( + $combined_name::$task(t) => t.dispatch(weight), + )* + } + } + } + + impl DelayTaskHooks<$combined_name> for $combined_name { + fn pre_delay(task: &$combined_name) -> DispatchResult { + match task { + $( + $combined_name::$task(t) => <$hook>::pre_delay(t), + )* + } + } + fn pre_delayed_execute(task: &$combined_name) -> DispatchResult { + match task { + $( + $combined_name::$task(t) => <$hook>::pre_delayed_execute(t), + )* + } + } + fn pre_cancel(task: &$combined_name) -> DispatchResult { + match task { + $( + $combined_name::$task(t) => <$hook>::pre_cancel(t), + )* + } + } + } + + $( + impl From<$vtask $(<$($generic),*>)?> for $combined_name { + fn from(t: $vtask $(<$($generic),*>)?) -> Self{ + $combined_name::$task(t) + } + } + )* + }; +} diff --git a/xtokens/src/lib.rs b/xtokens/src/lib.rs index 38aa0a0f8..565d94b2b 100644 --- a/xtokens/src/lib.rs +++ b/xtokens/src/lib.rs @@ -36,15 +36,17 @@ use frame_support::{ pallet_prelude::*, require_transactional, traits::{Contains, Get}, - Parameter, + transactional, Parameter, }; use frame_system::{ensure_signed, pallet_prelude::*}; use sp_runtime::{ traits::{AtLeast32BitUnsigned, Bounded, Convert, MaybeSerializeDeserialize, Member, Zero}, DispatchError, }; -use sp_std::{prelude::*, result::Result}; +use sp_std::{fmt::Debug, prelude::*, result::Result}; +use parity_scale_codec::{Decode, Encode, FullCodec}; +use scale_info::TypeInfo; use xcm::{ v4::{prelude::*, Weight}, VersionedAsset, VersionedAssets, VersionedLocation, @@ -54,8 +56,9 @@ use xcm_executor::traits::WeightBounds; pub use module::*; use orml_traits::{ location::{Parse, Reserve}, + task::{DelayTasksManager, DispatchableTask, TaskResult}, xcm_transfer::{Transferred, XtokensWeightInfo}, - GetByKey, RateLimiter, XcmTransfer, + GetByKey, RateLimiter, RateLimiterError, XcmTransfer, }; mod mock; @@ -75,6 +78,55 @@ use TransferKind::*; pub mod module { use super::*; + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub enum XtokensTask { + TransferAssets { + who: T::AccountId, + assets: Assets, + fee: Asset, + dest: Location, + dest_weight_limit: WeightLimit, + }, + } + + #[cfg(feature = "std")] + impl From> for () { + fn from(_task: XtokensTask) -> Self { + unimplemented!() + } + } + + impl DispatchableTask for XtokensTask { + fn dispatch(self, weight: Weight) -> TaskResult { + match self { + XtokensTask::TransferAssets { + who, + assets, + fee, + dest, + dest_weight_limit, + } => { + // execute `do_transfer_assets` without delay check + let result = + Pallet::::do_transfer_assets(who, assets, fee, dest, dest_weight_limit, false).map(|_| ()); + + TaskResult { + result, + used_weight: weight, // TODO: update + finished: true, + } + } + } + } + } + + pub struct DisabledDelayTask(sp_std::marker::PhantomData); + impl DelayTasksManager> for DisabledDelayTask { + fn add_delay_task(_task: T::Task, _delay_blocks: BlockNumberFor) -> DispatchResult { + Err(Error::::RateLimiterDeny.into()) + } + } + #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; @@ -92,7 +144,8 @@ pub mod module { type CurrencyId: Parameter + Member + Clone; /// Convert `T::CurrencyId` to `Location`. - type CurrencyIdConvert: Convert>; + type CurrencyIdConvert: Convert> + + Convert>; /// Convert `T::AccountId` to `Location`. type AccountIdToLocation: Convert; @@ -132,11 +185,16 @@ pub mod module { type ReserveProvider: Reserve; /// The rate limiter used to limit the cross-chain transfer asset. - type RateLimiter: RateLimiter; + type RateLimiter: RateLimiter>; /// The id of the RateLimiter. #[pallet::constant] - type RateLimiterId: Get<::RateLimiterId>; + type RateLimiterId: Get<>>::RateLimiterId>; + + /// Dispatchable tasks + type Task: DispatchableTask + FullCodec + Debug + Clone + PartialEq + TypeInfo + From>; + + type DelayTasks: DelayTasksManager>; } #[pallet::event] @@ -194,8 +252,8 @@ pub mod module { NotSupportedLocation, /// MinXcmFee not registered for certain reserve location MinXcmFeeNotDefined, - /// Asset transfer is limited by RateLimiter. - RateLimited, + /// Asset transfer is denied by RateLimiter. + RateLimiterDeny, } #[pallet::hooks] @@ -394,11 +452,49 @@ pub mod module { // We first grab the fee let fee: &Asset = assets.get(fee_item as usize).ok_or(Error::::AssetIndexNonExistent)?; - Self::do_transfer_assets(who, assets.clone(), fee.clone(), dest, dest_weight_limit).map(|_| ()) + Self::do_transfer_assets(who, assets.clone(), fee.clone(), dest, dest_weight_limit, true).map(|_| ()) } } impl Pallet { + #[transactional] + fn transfer_assets_delay_check( + who: &T::AccountId, + assets: Assets, + ) -> Result>, DispatchError> { + let rate_limiter_id = T::RateLimiterId::get(); + let asset_len = assets.len(); + + let mut need_delay: Option> = None; + for i in 0..asset_len { + let asset = assets.get(i).ok_or(Error::::AssetIndexNonExistent)?; + + if let Some(currency_id) = T::CurrencyIdConvert::convert(asset.id.0.clone()) { + // per asset check + let amount = match asset.fun { + Fungibility::Fungible(amount) => amount, + Fungibility::NonFungible(_) => 1, + }; + + // try consume quota of the rate limiter. + // NOTE: use CurrencyId as the key, use AccountId as whitelist filter key. + match T::RateLimiter::try_consume(rate_limiter_id, currency_id, amount, Some(who)) { + Ok(_) => {} + Err(_e @ RateLimiterError::Deny) => { + return Err(Error::::RateLimiterDeny.into()); + } + Err(ref _e @ RateLimiterError::Delay { duration }) => { + if duration > need_delay.unwrap_or_default() { + need_delay = Some(duration); + } + } + }; + } + } + + Ok(need_delay) + } + fn do_transfer( who: T::AccountId, currency_id: T::CurrencyId, @@ -413,7 +509,7 @@ pub mod module { ensure!(T::LocationsFilter::contains(&dest), Error::::NotSupportedLocation); let asset: Asset = (location, amount.into()).into(); - Self::do_transfer_assets(who, vec![asset.clone()].into(), asset, dest, dest_weight_limit) + Self::do_transfer_assets(who, vec![asset.clone()].into(), asset, dest, dest_weight_limit, true) } fn do_transfer_with_fee( @@ -439,7 +535,7 @@ pub mod module { assets.push(asset); assets.push(fee_asset.clone()); - Self::do_transfer_assets(who, assets, fee_asset, dest, dest_weight_limit) + Self::do_transfer_assets(who, assets, fee_asset, dest, dest_weight_limit, true) } fn do_transfer_asset( @@ -448,7 +544,7 @@ pub mod module { dest: Location, dest_weight_limit: WeightLimit, ) -> Result, DispatchError> { - Self::do_transfer_assets(who, vec![asset.clone()].into(), asset, dest, dest_weight_limit) + Self::do_transfer_assets(who, vec![asset.clone()].into(), asset, dest, dest_weight_limit, true) } fn do_transfer_asset_with_fee( @@ -463,7 +559,7 @@ pub mod module { assets.push(asset); assets.push(fee.clone()); - Self::do_transfer_assets(who, assets, fee, dest, dest_weight_limit) + Self::do_transfer_assets(who, assets, fee, dest, dest_weight_limit, true) } fn do_transfer_multicurrencies( @@ -502,7 +598,7 @@ pub mod module { let fee: Asset = (fee_location, (*fee_amount).into()).into(); - Self::do_transfer_assets(who, assets, fee, dest, dest_weight_limit) + Self::do_transfer_assets(who, assets, fee, dest, dest_weight_limit, true) } fn do_transfer_assets( @@ -511,6 +607,7 @@ pub mod module { fee: Asset, dest: Location, dest_weight_limit: WeightLimit, + delay_check: bool, ) -> Result, DispatchError> { ensure!( assets.len() <= T::MaxAssetsForTransfer::get(), @@ -549,18 +646,28 @@ pub mod module { Error::::DistinctReserveForAssetAndFee ); } + } - // per asset check - let amount = match asset.fun { - Fungibility::Fungible(amount) => amount, - Fungibility::NonFungible(_) => 1, - }; - - let rate_limiter_id = T::RateLimiterId::get(); - - // try consume quota of the rate limiter. - T::RateLimiter::try_consume(rate_limiter_id, asset.id.clone(), amount, Some(&who)) - .map_err(|_| Error::::RateLimited)?; + if delay_check { + if let Some(delay_block) = Self::transfer_assets_delay_check(&who, assets.clone())? { + return T::DelayTasks::add_delay_task( + XtokensTask::TransferAssets { + who: who.clone(), + assets: assets.clone(), + fee: fee.clone(), + dest: dest.clone(), + dest_weight_limit, + } + .into(), + delay_block, + ) + .map(|_| Transferred { + sender: who, + assets, + fee, + dest, + }); + } } let fee_reserve = T::ReserveProvider::reserve(&fee); @@ -1023,7 +1130,7 @@ pub mod module { dest: Location, dest_weight_limit: WeightLimit, ) -> Result, DispatchError> { - Self::do_transfer_assets(who, assets, fee, dest, dest_weight_limit) + Self::do_transfer_assets(who, assets, fee, dest, dest_weight_limit, true) } } } diff --git a/xtokens/src/mock/para.rs b/xtokens/src/mock/para.rs index 5ece9ab6a..f4752635e 100644 --- a/xtokens/src/mock/para.rs +++ b/xtokens/src/mock/para.rs @@ -1,6 +1,6 @@ use super::{ - AllowTopLevelPaidExecution, Amount, Balance, CurrencyId, CurrencyIdConvert, ParachainXcmRouter, RateLimiter, - CHARLIE, + AllowTopLevelPaidExecution, Amount, Balance, BlockNumberFor, CurrencyId, CurrencyIdConvert, DisabledDelayTask, + ParachainXcmRouter, RateLimiter, XtokensTask, CHARLIE, }; use crate as orml_xtokens; @@ -10,11 +10,12 @@ use frame_support::{ }; use frame_system::EnsureRoot; use pallet_xcm::XcmPassthrough; -use parity_scale_codec::Encode; +use parity_scale_codec::{Decode, Encode}; use polkadot_parachain_primitives::primitives::Sibling; +use scale_info::TypeInfo; use sp_runtime::{ traits::{Convert, IdentityLookup}, - AccountId32, + AccountId32, RuntimeDebug, }; use sp_std::cell::RefCell; use xcm::v4::{prelude::*, Weight}; @@ -26,7 +27,13 @@ use xcm_builder::{ use xcm_executor::{Config, XcmExecutor}; use crate::mock::AllTokensAreCreatedEqualToWeight; -use orml_traits::{location::AbsoluteReserveProvider, parameter_type_with_key, RateLimiterError}; +use orml_traits::{ + define_combined_task, + location::AbsoluteReserveProvider, + parameter_type_with_key, + task::{DispatchableTask, TaskResult}, + RateLimiterError, +}; use orml_xcm_support::{IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset}; pub type AccountId = AccountId32; @@ -226,7 +233,7 @@ thread_local! { } pub struct MockRateLimiter; -impl RateLimiter for MockRateLimiter { +impl RateLimiter> for MockRateLimiter { type RateLimiterId = u8; fn is_whitelist(_: Self::RateLimiterId, key: impl Encode) -> bool { @@ -235,15 +242,17 @@ impl RateLimiter for MockRateLimiter { encoded_key != encoded_charlie } - fn can_consume(_: Self::RateLimiterId, limit_key: impl Encode, value: u128) -> Result<(), RateLimiterError> { + fn can_consume( + _: Self::RateLimiterId, + limit_key: impl Encode, + value: u128, + ) -> Result<(), RateLimiterError>> { let encoded_limit_key = limit_key.encode(); - let r_multi_location: Location = CurrencyIdConvert::convert(CurrencyId::R).unwrap(); - let r_asset_id = AssetId(r_multi_location); - let encoded_r_asset_id = r_asset_id.encode(); + let encoded_currency_id = CurrencyId::R.encode(); - if encoded_limit_key == encoded_r_asset_id { + if encoded_limit_key == encoded_currency_id { let accumulation = R_ACCUMULATION.with(|v| *v.borrow()); - ensure!((accumulation + value) <= 2000, RateLimiterError::ExceedLimit); + ensure!((accumulation + value) <= 2000, RateLimiterError::Deny); } Ok(()) @@ -251,11 +260,9 @@ impl RateLimiter for MockRateLimiter { fn consume(_: Self::RateLimiterId, limit_key: impl Encode, value: u128) { let encoded_limit_key = limit_key.encode(); - let r_multi_location: Location = CurrencyIdConvert::convert(CurrencyId::R).unwrap(); - let r_asset_id = AssetId(r_multi_location); - let encoded_r_asset_id = r_asset_id.encode(); + let encoded_currency_id = CurrencyId::R.encode(); - if encoded_limit_key == encoded_r_asset_id { + if encoded_limit_key == encoded_currency_id { R_ACCUMULATION.with(|v| *v.borrow_mut() += value); } } @@ -265,6 +272,13 @@ parameter_types! { pub const XtokensRateLimiterId: u8 = 0; } +define_combined_task! { + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub enum DelayedTasks { + Xtokens(XtokensTask), + } +} + impl orml_xtokens::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -282,6 +296,8 @@ impl orml_xtokens::Config for Runtime { type ReserveProvider = AbsoluteReserveProvider; type RateLimiter = MockRateLimiter; type RateLimiterId = XtokensRateLimiterId; + type Task = DelayedTasks; + type DelayTasks = DisabledDelayTask; } impl orml_xcm::Config for Runtime { diff --git a/xtokens/src/mock/para_relative_view.rs b/xtokens/src/mock/para_relative_view.rs index 248cef833..9f8dab9cd 100644 --- a/xtokens/src/mock/para_relative_view.rs +++ b/xtokens/src/mock/para_relative_view.rs @@ -1,4 +1,4 @@ -use super::{Amount, Balance, CurrencyId, CurrencyIdConvert, ParachainXcmRouter}; +use super::{Amount, Balance, CurrencyId, CurrencyIdConvert, DisabledDelayTask, ParachainXcmRouter, XtokensTask}; use crate as orml_xtokens; use frame_support::{ @@ -7,10 +7,12 @@ use frame_support::{ }; use frame_system::EnsureRoot; use pallet_xcm::XcmPassthrough; +use parity_scale_codec::{Decode, Encode}; use polkadot_parachain_primitives::primitives::Sibling; +use scale_info::TypeInfo; use sp_runtime::{ traits::{Convert, IdentityLookup}, - AccountId32, BoundedVec, + AccountId32, BoundedVec, RuntimeDebug, }; use xcm::v4::{prelude::*, Weight}; use xcm_builder::{ @@ -22,8 +24,10 @@ use xcm_executor::{Config, XcmExecutor}; use crate::mock::AllTokensAreCreatedEqualToWeight; use orml_traits::{ + define_combined_task, location::{AbsoluteReserveProvider, RelativeReserveProvider}, parameter_type_with_key, + task::{DispatchableTask, TaskResult}, }; use orml_xcm_support::{IsNativeConcrete, MultiCurrencyAdapter, MultiNativeAsset}; @@ -310,6 +314,8 @@ impl Convert> for RelativeCurrencyIdConvert { parameter_types! { pub SelfLocation: Location = Location::here(); pub const MaxAssetsForTransfer: usize = 2; + pub const GetDelayBlocks: u64 = 1000; + pub const GetReserveId: [u8; 8] = *b"xtokensr"; } pub struct ParentOrParachains; @@ -339,6 +345,13 @@ parameter_type_with_key! { }; } +define_combined_task! { + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub enum DelayedTasks { + Xtokens(XtokensTask), + } +} + impl orml_xtokens::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -356,6 +369,8 @@ impl orml_xtokens::Config for Runtime { type ReserveProvider = RelativeReserveProvider; type RateLimiter = (); type RateLimiterId = (); + type Task = (); + type DelayTasks = DisabledDelayTask; } impl orml_xcm::Config for Runtime { diff --git a/xtokens/src/mock/para_teleport.rs b/xtokens/src/mock/para_teleport.rs index 6fcb6f7ef..822f600ba 100644 --- a/xtokens/src/mock/para_teleport.rs +++ b/xtokens/src/mock/para_teleport.rs @@ -1,4 +1,7 @@ -use super::{AllowTopLevelPaidExecution, Amount, Balance, CurrencyId, CurrencyIdConvert, ParachainXcmRouter}; +use super::{ + AllowTopLevelPaidExecution, Amount, Balance, CurrencyId, CurrencyIdConvert, DisabledDelayTask, ParachainXcmRouter, + XtokensTask, +}; use crate as orml_xtokens; use frame_support::{ @@ -7,10 +10,12 @@ use frame_support::{ }; use frame_system::EnsureRoot; use pallet_xcm::XcmPassthrough; +use parity_scale_codec::{Decode, Encode}; use polkadot_parachain_primitives::primitives::Sibling; +use scale_info::TypeInfo; use sp_runtime::{ traits::{Convert, IdentityLookup}, - AccountId32, + AccountId32, RuntimeDebug, }; use xcm::v4::{prelude::*, Weight}; use xcm_builder::{ @@ -22,7 +27,12 @@ use xcm_executor::{Config, XcmExecutor}; use crate::mock::teleport_currency_adapter::MultiTeleportCurrencyAdapter; use crate::mock::AllTokensAreCreatedEqualToWeight; -use orml_traits::{location::AbsoluteReserveProvider, parameter_type_with_key}; +use orml_traits::{ + define_combined_task, + location::AbsoluteReserveProvider, + parameter_type_with_key, + task::{DispatchableTask, TaskResult}, +}; use orml_xcm_support::{DisabledParachainFee, IsNativeConcrete, MultiNativeAsset}; pub type AccountId = AccountId32; @@ -207,6 +217,13 @@ impl Contains for ParentOrParachains { } } +define_combined_task! { + #[derive(Clone, Encode, Decode, PartialEq, RuntimeDebug, TypeInfo)] + pub enum DelayedTasks { + Xtokens(XtokensTask), + } +} + impl orml_xtokens::Config for Runtime { type RuntimeEvent = RuntimeEvent; type Balance = Balance; @@ -224,6 +241,8 @@ impl orml_xtokens::Config for Runtime { type ReserveProvider = AbsoluteReserveProvider; type RateLimiter = (); type RateLimiterId = (); + type Task = (); + type DelayTasks = DisabledDelayTask; } impl orml_xcm::Config for Runtime { diff --git a/xtokens/src/tests.rs b/xtokens/src/tests.rs index 5f2e08b15..3060a452c 100644 --- a/xtokens/src/tests.rs +++ b/xtokens/src/tests.rs @@ -1780,7 +1780,7 @@ fn send_relay_chain_asset_to_relay_chain_at_rate_limit() { ), WeightLimit::Unlimited ), - Error::::RateLimited + Error::::RateLimiterDeny ); assert_eq!(ParaTokens::free_balance(CurrencyId::R, &CHARLIE), 1200); assert_eq!(R_ACCUMULATION.with(|v| *v.borrow()), 1800);