// This file is part of Substrate. // Copyright (C) Parity Technologies (UK) Ltd. // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! # Transaction Pause //! //! Allows dynamic, chain-state-based pausing and unpausing of specific extrinsics via call filters. //! //! ## WARNING //! //! NOT YET AUDITED. DO NOT USE IN PRODUCTION. //! //! ## Pallet API //! //! See the [`pallet`] module for more information about the interfaces this pallet exposes, //! including its configuration trait, dispatchables, storage items, events, and errors. //! //! ## Overview //! //! A dynamic call filter that can be controlled with extrinsics. //! //! Pausing an extrinsic means that the extrinsic CANNOT be called again until it is unpaused. //! The exception is calls that use `dispatch_bypass_filter`, typically only with the root origin. //! //! ### Primary Features //! //! - Calls that should never be paused can be added to a whitelist. //! - Separate origins are configurable for pausing and pausing. //! - Pausing is triggered using the string representation of the call. //! - Pauses can target a single extrinsic or an entire pallet. //! - Pauses can target future extrinsics or pallets. //! //! ### Example //! //! Configuration of call filters: //! //! ```ignore //! impl frame_system::Config for Runtime { //! // … //! type BaseCallFilter = InsideBoth; //! // … //! } //! ``` //! //! Pause specific all: #![doc = docify::embed!("src/tests.rs", can_pause_specific_call)] //! //! Unpause specific all: #![doc = docify::embed!("src/tests.rs", can_unpause_specific_call)] //! //! Pause all calls in a pallet: #![doc = docify::embed!("src/tests.rs", can_pause_all_calls_in_pallet_except_on_whitelist)] //! //! ## Low Level / Implementation Details //! //! ### Use Cost //! //! A storage map (`PausedCalls`) is used to store currently paused calls. //! Using the call filter will require a db read of that storage on each extrinsic. #![cfg_attr(not(feature = "std"), no_std)] #![deny(rustdoc::broken_intra_doc_links)] mod benchmarking; pub mod mock; mod tests; pub mod weights; use frame_support::{ dispatch::GetDispatchInfo, pallet_prelude::*, traits::{CallMetadata, Contains, GetCallMetadata, IsSubType, IsType}, DefaultNoBound, }; use frame_system::pallet_prelude::*; use sp_runtime::{traits::Dispatchable, DispatchResult}; use sp_std::{convert::TryInto, prelude::*}; pub use pallet::*; pub use weights::*; /// The stringy name of a pallet from [`GetCallMetadata`] for [`Config::RuntimeCall`] variants. pub type PalletNameOf = BoundedVec::MaxNameLen>; /// The stringy name of a call (within a pallet) from [`GetCallMetadata`] for /// [`Config::RuntimeCall`] variants. pub type PalletCallNameOf = BoundedVec::MaxNameLen>; /// A fully specified pallet ([`PalletNameOf`]) and optional call ([`PalletCallNameOf`]) /// to partially or fully specify an item a variant of a [`Config::RuntimeCall`]. pub type RuntimeCallNameOf = (PalletNameOf, PalletCallNameOf); #[frame_support::pallet] pub mod pallet { use super::*; #[pallet::pallet] pub struct Pallet(PhantomData); #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The overarching call type. type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo + GetCallMetadata + From> + IsSubType> + IsType<::RuntimeCall>; /// The only origin that can pause calls. type PauseOrigin: EnsureOrigin; /// The only origin that can un-pause calls. type UnpauseOrigin: EnsureOrigin; /// Contains all calls that cannot be paused. /// /// The `TxMode` pallet cannot pause its own calls, and does not need to be explicitly /// added here. type WhitelistedCalls: Contains>; /// Maximum length for pallet name and call name SCALE encoded string names. /// /// TOO LONG NAMES WILL BE TREATED AS PAUSED. #[pallet::constant] type MaxNameLen: Get; // Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } /// The set of calls that are explicitly paused. #[pallet::storage] pub type PausedCalls = StorageMap<_, Blake2_128Concat, RuntimeCallNameOf, (), OptionQuery>; #[pallet::error] pub enum Error { /// The call is paused. IsPaused, /// The call is unpaused. IsUnpaused, /// The call is whitelisted and cannot be paused. Unpausable, // The pallet or call does not exist in the runtime. NotFound, } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// This pallet, or a specific call is now paused. CallPaused { full_name: RuntimeCallNameOf }, /// This pallet, or a specific call is now unpaused. CallUnpaused { full_name: RuntimeCallNameOf }, } /// Configure the initial state of this pallet in the genesis block. #[pallet::genesis_config] #[derive(DefaultNoBound)] pub struct GenesisConfig { /// Initially paused calls. pub paused: Vec>, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { for call in &self.paused { Pallet::::ensure_can_pause(&call).expect("Genesis data is known good; qed"); PausedCalls::::insert(&call, ()); } } } #[pallet::call] impl Pallet { /// Pause a call. /// /// Can only be called by [`Config::PauseOrigin`]. /// Emits an [`Event::CallPaused`] event on success. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::pause())] pub fn pause(origin: OriginFor, full_name: RuntimeCallNameOf) -> DispatchResult { T::PauseOrigin::ensure_origin(origin)?; Self::do_pause(full_name).map_err(Into::into) } /// Un-pause a call. /// /// Can only be called by [`Config::UnpauseOrigin`]. /// Emits an [`Event::CallUnpaused`] event on success. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::unpause())] pub fn unpause(origin: OriginFor, ident: RuntimeCallNameOf) -> DispatchResult { T::UnpauseOrigin::ensure_origin(origin)?; Self::do_unpause(ident).map_err(Into::into) } } } impl Pallet { pub(crate) fn do_pause(ident: RuntimeCallNameOf) -> Result<(), Error> { Self::ensure_can_pause(&ident)?; PausedCalls::::insert(&ident, ()); Self::deposit_event(Event::CallPaused { full_name: ident }); Ok(()) } pub(crate) fn do_unpause(ident: RuntimeCallNameOf) -> Result<(), Error> { Self::ensure_can_unpause(&ident)?; PausedCalls::::remove(&ident); Self::deposit_event(Event::CallUnpaused { full_name: ident }); Ok(()) } /// Return whether this call is paused. pub fn is_paused(full_name: &RuntimeCallNameOf) -> bool { if T::WhitelistedCalls::contains(full_name) { return false } >::contains_key(full_name) } /// Same as [`Self::is_paused`] but for inputs unbound by max-encoded-len. pub fn is_paused_unbound(pallet: Vec, call: Vec) -> bool { let pallet = PalletNameOf::::try_from(pallet); let call = PalletCallNameOf::::try_from(call); match (pallet, call) { (Ok(pallet), Ok(call)) => Self::is_paused(&(pallet, call)), _ => true, } } /// Ensure that this call can be paused. pub fn ensure_can_pause(full_name: &RuntimeCallNameOf) -> Result<(), Error> { // SAFETY: The `TxPause` pallet can never pause itself. if full_name.0.as_slice() == ::name().as_bytes() { return Err(Error::::Unpausable) } if T::WhitelistedCalls::contains(&full_name) { return Err(Error::::Unpausable) } if Self::is_paused(&full_name) { return Err(Error::::IsPaused) } Ok(()) } /// Ensure that this call can be un-paused. pub fn ensure_can_unpause(full_name: &RuntimeCallNameOf) -> Result<(), Error> { if Self::is_paused(&full_name) { // SAFETY: Everything that is paused, can be un-paused. Ok(()) } else { Err(Error::IsUnpaused) } } } impl Contains<::RuntimeCall> for Pallet where ::RuntimeCall: GetCallMetadata, { /// Return whether the call is allowed to be dispatched. fn contains(call: &::RuntimeCall) -> bool { let CallMetadata { pallet_name, function_name } = call.get_call_metadata(); !Pallet::::is_paused_unbound(pallet_name.into(), function_name.into()) } } impl frame_support::traits::TransactionPause for Pallet { type CallIdentifier = RuntimeCallNameOf; fn is_paused(full_name: Self::CallIdentifier) -> bool { Self::is_paused(&full_name) } fn can_pause(full_name: Self::CallIdentifier) -> bool { Self::ensure_can_pause(&full_name).is_ok() } fn pause( full_name: Self::CallIdentifier, ) -> Result<(), frame_support::traits::TransactionPauseError> { Self::do_pause(full_name).map_err(Into::into) } fn unpause( full_name: Self::CallIdentifier, ) -> Result<(), frame_support::traits::TransactionPauseError> { Self::do_unpause(full_name).map_err(Into::into) } } impl From> for frame_support::traits::TransactionPauseError { fn from(err: Error) -> Self { match err { Error::::NotFound => Self::NotFound, Error::::Unpausable => Self::Unpausable, Error::::IsPaused => Self::AlreadyPaused, Error::::IsUnpaused => Self::AlreadyUnpaused, _ => Self::Unknown, } } }