Skip to main content

pallet_tx_pause/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Transaction Pause
19//!
20//! Allows dynamic, chain-state-based pausing and unpausing of specific extrinsics via call filters.
21//!
22//! ## Pallet API
23//!
24//! See the [`pallet`] module for more information about the interfaces this pallet exposes,
25//! including its configuration trait, dispatchables, storage items, events, and errors.
26//!
27//! ## Overview
28//!
29//! A dynamic call filter that can be controlled with extrinsics.
30//!
31//! Pausing an extrinsic means that the extrinsic CANNOT be called again until it is unpaused.
32//! The exception is calls that use `dispatch_bypass_filter`, typically only with the root origin.
33//!
34//! ### Primary Features
35//!
36//! - Calls that should never be paused can be added to a whitelist.
37//! - Separate origins are configurable for pausing and pausing.
38//! - Pausing is triggered using the string representation of the call.
39//! - Pauses can target a single extrinsic or an entire pallet.
40//! - Pauses can target future extrinsics or pallets.
41//!
42//! ### Example
43//!
44//! Configuration of call filters:
45//!
46//! ```ignore
47//! impl frame_system::Config for Runtime {
48//!   // …
49//!   type BaseCallFilter = InsideBoth<DefaultFilter, TxPause>;
50//!   // …
51//! }
52//! ```
53//!
54//! Pause specific all:
55#![doc = docify::embed!("src/tests.rs", can_pause_specific_call)]
56//! Unpause specific all:
57#![doc = docify::embed!("src/tests.rs", can_unpause_specific_call)]
58//! Pause all calls in a pallet:
59#![doc = docify::embed!("src/tests.rs", can_pause_all_calls_in_pallet_except_on_whitelist)]
60//! ## Low Level / Implementation Details
61//!
62//! ### Use Cost
63//!
64//! A storage map (`PausedCalls`) is used to store currently paused calls.
65//! Using the call filter will require a db read of that storage on each extrinsic.
66
67#![cfg_attr(not(feature = "std"), no_std)]
68#![deny(rustdoc::broken_intra_doc_links)]
69
70mod benchmarking;
71pub mod mock;
72mod tests;
73pub mod weights;
74
75extern crate alloc;
76
77use alloc::vec::Vec;
78use frame::{
79	prelude::*,
80	traits::{TransactionPause, TransactionPauseError},
81};
82pub use pallet::*;
83pub use weights::*;
84
85/// The stringy name of a pallet from [`GetCallMetadata`] for [`Config::RuntimeCall`] variants.
86pub type PalletNameOf<T> = BoundedVec<u8, <T as Config>::MaxNameLen>;
87
88/// The stringy name of a call (within a pallet) from [`GetCallMetadata`] for
89/// [`Config::RuntimeCall`] variants.
90pub type PalletCallNameOf<T> = BoundedVec<u8, <T as Config>::MaxNameLen>;
91
92/// A fully specified pallet ([`PalletNameOf`]) and optional call ([`PalletCallNameOf`])
93/// to partially or fully specify an item a variant of a  [`Config::RuntimeCall`].
94pub type RuntimeCallNameOf<T> = (PalletNameOf<T>, PalletCallNameOf<T>);
95
96#[frame::pallet]
97pub mod pallet {
98	use super::*;
99
100	#[pallet::pallet]
101	pub struct Pallet<T>(PhantomData<T>);
102
103	#[pallet::config]
104	pub trait Config: frame_system::Config {
105		/// The overarching event type.
106		#[allow(deprecated)]
107		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
108
109		/// The overarching call type.
110		type RuntimeCall: Parameter
111			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
112			+ GetDispatchInfo
113			+ GetCallMetadata
114			+ From<frame_system::Call<Self>>
115			+ IsSubType<Call<Self>>
116			+ IsType<<Self as frame_system::Config>::RuntimeCall>;
117
118		/// The only origin that can pause calls.
119		type PauseOrigin: EnsureOrigin<Self::RuntimeOrigin>;
120
121		/// The only origin that can un-pause calls.
122		type UnpauseOrigin: EnsureOrigin<Self::RuntimeOrigin>;
123
124		/// Contains all calls that cannot be paused.
125		///
126		/// The `TxMode` pallet cannot pause its own calls, and does not need to be explicitly
127		/// added here.
128		type WhitelistedCalls: Contains<RuntimeCallNameOf<Self>>;
129
130		/// Maximum length for pallet name and call name SCALE encoded string names.
131		///
132		/// TOO LONG NAMES WILL BE TREATED AS PAUSED.
133		#[pallet::constant]
134		type MaxNameLen: Get<u32>;
135
136		// Weight information for extrinsics in this pallet.
137		type WeightInfo: WeightInfo;
138	}
139
140	/// The set of calls that are explicitly paused.
141	#[pallet::storage]
142	pub type PausedCalls<T: Config> =
143		StorageMap<_, Blake2_128Concat, RuntimeCallNameOf<T>, (), OptionQuery>;
144
145	#[pallet::error]
146	pub enum Error<T> {
147		/// The call is paused.
148		IsPaused,
149
150		/// The call is unpaused.
151		IsUnpaused,
152
153		/// The call is whitelisted and cannot be paused.
154		Unpausable,
155
156		// The pallet or call does not exist in the runtime.
157		NotFound,
158	}
159
160	#[pallet::event]
161	#[pallet::generate_deposit(pub(super) fn deposit_event)]
162	pub enum Event<T: Config> {
163		/// This pallet, or a specific call is now paused.
164		CallPaused { full_name: RuntimeCallNameOf<T> },
165		/// This pallet, or a specific call is now unpaused.
166		CallUnpaused { full_name: RuntimeCallNameOf<T> },
167	}
168
169	/// Configure the initial state of this pallet in the genesis block.
170	#[pallet::genesis_config]
171	#[derive(DefaultNoBound)]
172	pub struct GenesisConfig<T: Config> {
173		/// Initially paused calls.
174		pub paused: Vec<RuntimeCallNameOf<T>>,
175	}
176
177	#[pallet::genesis_build]
178	impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
179		fn build(&self) {
180			for call in &self.paused {
181				Pallet::<T>::ensure_can_pause(&call).expect("Genesis data is known good; qed");
182				PausedCalls::<T>::insert(&call, ());
183			}
184		}
185	}
186
187	#[pallet::call]
188	impl<T: Config> Pallet<T> {
189		/// Pause a call.
190		///
191		/// Can only be called by [`Config::PauseOrigin`].
192		/// Emits an [`Event::CallPaused`] event on success.
193		#[pallet::call_index(0)]
194		#[pallet::weight(T::WeightInfo::pause())]
195		pub fn pause(origin: OriginFor<T>, full_name: RuntimeCallNameOf<T>) -> DispatchResult {
196			T::PauseOrigin::ensure_origin(origin)?;
197
198			Self::do_pause(full_name).map_err(Into::into)
199		}
200
201		/// Un-pause a call.
202		///
203		/// Can only be called by [`Config::UnpauseOrigin`].
204		/// Emits an [`Event::CallUnpaused`] event on success.
205		#[pallet::call_index(1)]
206		#[pallet::weight(T::WeightInfo::unpause())]
207		pub fn unpause(origin: OriginFor<T>, ident: RuntimeCallNameOf<T>) -> DispatchResult {
208			T::UnpauseOrigin::ensure_origin(origin)?;
209
210			Self::do_unpause(ident).map_err(Into::into)
211		}
212	}
213}
214
215impl<T: Config> Pallet<T> {
216	pub(crate) fn do_pause(ident: RuntimeCallNameOf<T>) -> Result<(), Error<T>> {
217		Self::ensure_can_pause(&ident)?;
218		PausedCalls::<T>::insert(&ident, ());
219		Self::deposit_event(Event::CallPaused { full_name: ident });
220
221		Ok(())
222	}
223
224	pub(crate) fn do_unpause(ident: RuntimeCallNameOf<T>) -> Result<(), Error<T>> {
225		Self::ensure_can_unpause(&ident)?;
226		PausedCalls::<T>::remove(&ident);
227		Self::deposit_event(Event::CallUnpaused { full_name: ident });
228
229		Ok(())
230	}
231
232	/// Return whether this call is paused.
233	pub fn is_paused(full_name: &RuntimeCallNameOf<T>) -> bool {
234		if T::WhitelistedCalls::contains(full_name) {
235			return false;
236		}
237
238		<PausedCalls<T>>::contains_key(full_name)
239	}
240
241	/// Same as [`Self::is_paused`] but for inputs unbound by max-encoded-len.
242	pub fn is_paused_unbound(pallet: Vec<u8>, call: Vec<u8>) -> bool {
243		let pallet = PalletNameOf::<T>::try_from(pallet);
244		let call = PalletCallNameOf::<T>::try_from(call);
245
246		match (pallet, call) {
247			(Ok(pallet), Ok(call)) => Self::is_paused(&(pallet, call)),
248			_ => true,
249		}
250	}
251
252	/// Ensure that this call can be paused.
253	pub fn ensure_can_pause(full_name: &RuntimeCallNameOf<T>) -> Result<(), Error<T>> {
254		// SAFETY: The `TxPause` pallet can never pause itself.
255		if full_name.0.as_slice() == <Self as PalletInfoAccess>::name().as_bytes() {
256			return Err(Error::<T>::Unpausable);
257		}
258
259		if T::WhitelistedCalls::contains(&full_name) {
260			return Err(Error::<T>::Unpausable);
261		}
262		if Self::is_paused(&full_name) {
263			return Err(Error::<T>::IsPaused);
264		}
265		Ok(())
266	}
267
268	/// Ensure that this call can be un-paused.
269	pub fn ensure_can_unpause(full_name: &RuntimeCallNameOf<T>) -> Result<(), Error<T>> {
270		if Self::is_paused(&full_name) {
271			// SAFETY: Everything that is paused, can be un-paused.
272			Ok(())
273		} else {
274			Err(Error::IsUnpaused)
275		}
276	}
277}
278
279impl<T: pallet::Config> Contains<<T as frame_system::Config>::RuntimeCall> for Pallet<T>
280where
281	<T as frame_system::Config>::RuntimeCall: GetCallMetadata,
282{
283	/// Return whether the call is allowed to be dispatched.
284	fn contains(call: &<T as frame_system::Config>::RuntimeCall) -> bool {
285		let CallMetadata { pallet_name, function_name } = call.get_call_metadata();
286		!Pallet::<T>::is_paused_unbound(pallet_name.into(), function_name.into())
287	}
288}
289
290impl<T: Config> TransactionPause for Pallet<T> {
291	type CallIdentifier = RuntimeCallNameOf<T>;
292
293	fn is_paused(full_name: Self::CallIdentifier) -> bool {
294		Self::is_paused(&full_name)
295	}
296
297	fn can_pause(full_name: Self::CallIdentifier) -> bool {
298		Self::ensure_can_pause(&full_name).is_ok()
299	}
300
301	fn pause(full_name: Self::CallIdentifier) -> Result<(), TransactionPauseError> {
302		Self::do_pause(full_name).map_err(Into::into)
303	}
304
305	fn unpause(full_name: Self::CallIdentifier) -> Result<(), TransactionPauseError> {
306		Self::do_unpause(full_name).map_err(Into::into)
307	}
308}
309
310impl<T: Config> From<Error<T>> for TransactionPauseError {
311	fn from(err: Error<T>) -> Self {
312		match err {
313			Error::<T>::NotFound => Self::NotFound,
314			Error::<T>::Unpausable => Self::Unpausable,
315			Error::<T>::IsPaused => Self::AlreadyPaused,
316			Error::<T>::IsUnpaused => Self::AlreadyUnpaused,
317			_ => Self::Unknown,
318		}
319	}
320}