referrerpolicy=no-referrer-when-downgrade

pallet_whitelist/
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//! # Whitelist Pallet
19//!
20//! - [`Config`]
21//! - [`Call`]
22//!
23//! ## Overview
24//!
25//! Allow some configurable origin: [`Config::WhitelistOrigin`] to whitelist some hash of a call,
26//! and allow another configurable origin: [`Config::DispatchWhitelistedOrigin`] to dispatch them
27//! with the root origin.
28//!
29//! In the meantime the call corresponding to the hash must have been submitted to the pre-image
30//! handler [`pallet::Config::Preimages`].
31
32#![cfg_attr(not(feature = "std"), no_std)]
33
34#[cfg(feature = "runtime-benchmarks")]
35mod benchmarking;
36#[cfg(test)]
37mod mock;
38#[cfg(test)]
39mod tests;
40pub mod weights;
41pub use weights::WeightInfo;
42
43extern crate alloc;
44
45use alloc::boxed::Box;
46use codec::{DecodeLimit, Encode, FullCodec};
47use frame::{
48	prelude::*,
49	traits::{QueryPreimage, StorePreimage},
50};
51use scale_info::TypeInfo;
52
53pub use pallet::*;
54
55/// The Block number that we use to measure time.
56///
57/// Deferral expirations are tracked against this provider rather than the local system block,
58/// so on a parachain it can be the relay chain block number. All `DeferredDispatch` `expire_at`
59/// values and the [`Config::DeferredDispatchExpiration`] window are denominated in it.
60pub type ProvidedBlockNumberFor<T> =
61	<<T as Config>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
62
63#[frame::pallet]
64pub mod pallet {
65	use super::*;
66
67	#[pallet::config]
68	pub trait Config: frame_system::Config {
69		/// The overarching event type.
70		#[allow(deprecated)]
71		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
72
73		/// The overarching call type.
74		type RuntimeCall: IsType<<Self as frame_system::Config>::RuntimeCall>
75			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin, PostInfo = PostDispatchInfo>
76			+ GetDispatchInfo
77			+ FullCodec
78			+ TypeInfo
79			+ From<frame_system::Call<Self>>
80			+ Parameter;
81
82		/// Required origin for whitelisting a call.
83		type WhitelistOrigin: EnsureOrigin<Self::RuntimeOrigin>;
84
85		/// Required origin for dispatching whitelisted call with root origin.
86		type DispatchWhitelistedOrigin: EnsureOrigin<Self::RuntimeOrigin>;
87
88		/// The handler of pre-images.
89		type Preimages: QueryPreimage<H = Self::Hashing> + StorePreimage;
90
91		/// The number of provided blocks after which a deferred dispatch expires.
92		type DeferredDispatchExpiration: Get<ProvidedBlockNumberFor<Self>>;
93
94		/// Provider for the block number.
95		type BlockNumberProvider: BlockNumberProvider;
96
97		/// The weight information for this pallet.
98		type WeightInfo: WeightInfo;
99	}
100
101	#[pallet::pallet]
102	pub struct Pallet<T>(_);
103
104	#[pallet::event]
105	#[pallet::generate_deposit(pub(super) fn deposit_event)]
106	pub enum Event<T: Config> {
107		CallWhitelisted {
108			call_hash: T::Hash,
109		},
110		WhitelistedCallRemoved {
111			call_hash: T::Hash,
112		},
113		WhitelistedCallDispatched {
114			call_hash: T::Hash,
115			result: DispatchResultWithPostInfo,
116		},
117		/// A call dispatch has been deferred to a future provided block.
118		DispatchDeferred {
119			call_hash: T::Hash,
120		},
121		/// A deferred dispatch entry has been removed after expiration.
122		DeferredDispatchRemoved {
123			call_hash: T::Hash,
124		},
125		/// A relayer (signed origin) executed a deferred dispatch.
126		///
127		/// Emitted whenever the deferred entry is consumed by a relayer, regardless of whether the
128		/// inner call itself succeeded; the inner call's outcome is reported separately by
129		/// [`Event::WhitelistedCallDispatched`].
130		DeferredDispatchExecuted {
131			call_hash: T::Hash,
132			who: T::AccountId,
133		},
134	}
135
136	#[pallet::error]
137	pub enum Error<T> {
138		/// The preimage of the call hash could not be loaded.
139		UnavailablePreImage,
140		/// The call could not be decoded.
141		UndecodableCall,
142		/// The weight of the decoded call was higher than the witness.
143		InvalidCallWeightWitness,
144		/// The call was not whitelisted.
145		CallIsNotWhitelisted,
146		/// The call was already whitelisted; No-Op.
147		CallAlreadyWhitelisted,
148		/// No deferred dispatch entry exists for this call hash.
149		DeferredDispatchNotFound,
150		/// The deferred dispatch entry has not yet expired.
151		DeferredDispatchNotExpired,
152		/// The dispatch has already been deferred.
153		AlreadyDeferred,
154		/// The deferred dispatch has expired.
155		DeferredDispatchExpired,
156	}
157
158	#[pallet::storage]
159	pub type WhitelistedCall<T: Config> = StorageMap<_, Twox64Concat, T::Hash, (), OptionQuery>;
160
161	/// Deferred dispatches, mapping a call hash to the provided block number at which the deferral
162	/// expires and the entry can be permissionlessly removed.
163	#[pallet::storage]
164	pub type DeferredDispatch<T: Config> =
165		StorageMap<_, Twox64Concat, T::Hash, ProvidedBlockNumberFor<T>, OptionQuery>;
166
167	#[pallet::call]
168	impl<T: Config> Pallet<T> {
169		#[pallet::call_index(0)]
170		#[pallet::weight(T::WeightInfo::whitelist_call())]
171		pub fn whitelist_call(origin: OriginFor<T>, call_hash: T::Hash) -> DispatchResult {
172			T::WhitelistOrigin::ensure_origin(origin)?;
173
174			ensure!(
175				!WhitelistedCall::<T>::contains_key(call_hash),
176				Error::<T>::CallAlreadyWhitelisted,
177			);
178
179			WhitelistedCall::<T>::insert(call_hash, ());
180			T::Preimages::request(&call_hash);
181
182			Self::deposit_event(Event::<T>::CallWhitelisted { call_hash });
183			Ok(())
184		}
185
186		#[pallet::call_index(1)]
187		#[pallet::weight(T::WeightInfo::remove_whitelisted_call())]
188		pub fn remove_whitelisted_call(origin: OriginFor<T>, call_hash: T::Hash) -> DispatchResult {
189			T::WhitelistOrigin::ensure_origin(origin)?;
190
191			WhitelistedCall::<T>::take(call_hash).ok_or(Error::<T>::CallIsNotWhitelisted)?;
192
193			T::Preimages::unrequest(&call_hash);
194
195			Self::deposit_event(Event::<T>::WhitelistedCallRemoved { call_hash });
196
197			Ok(())
198		}
199
200		#[pallet::call_index(2)]
201		#[pallet::weight(
202			T::WeightInfo::dispatch_whitelisted_call(*call_encoded_len)
203				.saturating_add(*call_weight_witness)
204		)]
205		pub fn dispatch_whitelisted_call(
206			origin: OriginFor<T>,
207			call_hash: T::Hash,
208			call_encoded_len: u32,
209			call_weight_witness: Weight,
210		) -> DispatchResultWithPostInfo {
211			let relayer = match T::DispatchWhitelistedOrigin::try_origin(origin) {
212				Ok(_) if WhitelistedCall::<T>::contains_key(call_hash) => None,
213				Ok(_) => {
214					Self::defer_dispatch(call_hash)?;
215					return Ok(Some(T::WeightInfo::defer_dispatch(0)).into());
216				},
217				Err(dispatch_origin) => {
218					Some(Self::ensure_signed_deferred_dispatch(dispatch_origin, call_hash)?)
219				},
220			};
221
222			let call_data = T::Preimages::fetch(&call_hash, Some(call_encoded_len))
223				.map_err(|_| Error::<T>::UnavailablePreImage)?;
224
225			let call = <T as Config>::RuntimeCall::decode_all_with_depth_limit(
226				frame::deps::frame_support::MAX_EXTRINSIC_DEPTH,
227				&mut &call_data[..],
228			)
229			.map_err(|_| Error::<T>::UndecodableCall)?;
230
231			ensure!(
232				call.get_dispatch_info().call_weight.all_lte(call_weight_witness),
233				Error::<T>::InvalidCallWeightWitness
234			);
235
236			// Relayer isn't charged; the privileged direct path still pays.
237			let pays_fee = if relayer.is_some() { Pays::No } else { Pays::Yes };
238
239			let call_actual_weight = Self::clean_and_dispatch(call_hash, call);
240			if let Some(who) = relayer {
241				Self::deposit_event(Event::<T>::DeferredDispatchExecuted { call_hash, who });
242			}
243
244			let actual_weight = call_actual_weight.map(|w| {
245				w.saturating_add(T::WeightInfo::dispatch_whitelisted_call(call_encoded_len))
246			});
247			Ok(PostDispatchInfo { actual_weight, pays_fee })
248		}
249
250		#[pallet::call_index(3)]
251		#[pallet::weight({
252			let call_weight = call.get_dispatch_info().call_weight;
253			let call_len = call.encoded_size() as u32;
254			T::WeightInfo::dispatch_whitelisted_call_with_preimage(call_len)
255				.saturating_add(call_weight)
256		})]
257		pub fn dispatch_whitelisted_call_with_preimage(
258			origin: OriginFor<T>,
259			call: Box<<T as Config>::RuntimeCall>,
260		) -> DispatchResultWithPostInfo {
261			let call_hash = T::Hashing::hash_of(&call).into();
262			let call_len = call.encoded_size() as u32;
263
264			let relayer = match T::DispatchWhitelistedOrigin::try_origin(origin) {
265				Ok(_) if WhitelistedCall::<T>::contains_key(call_hash) => None,
266				Ok(_) => {
267					Self::defer_dispatch(call_hash)?;
268					return Ok(Some(T::WeightInfo::defer_dispatch(call_len)).into());
269				},
270				Err(dispatch_origin) => {
271					Some(Self::ensure_signed_deferred_dispatch(dispatch_origin, call_hash)?)
272				},
273			};
274
275			// Relayer isn't charged; the privileged direct path still pays.
276			let pays_fee = if relayer.is_some() { Pays::No } else { Pays::Yes };
277
278			let call_actual_weight = Self::clean_and_dispatch(call_hash, *call);
279			if let Some(who) = relayer {
280				Self::deposit_event(Event::<T>::DeferredDispatchExecuted { call_hash, who });
281			}
282
283			let actual_weight = call_actual_weight.map(|w| {
284				w.saturating_add(T::WeightInfo::dispatch_whitelisted_call_with_preimage(call_len))
285			});
286			Ok(PostDispatchInfo { actual_weight, pays_fee })
287		}
288
289		#[pallet::call_index(4)]
290		#[pallet::weight(T::WeightInfo::remove_deferred_dispatch())]
291		pub fn remove_deferred_dispatch(
292			origin: OriginFor<T>,
293			call_hash: T::Hash,
294		) -> DispatchResultWithPostInfo {
295			ensure_signed(origin)?;
296
297			let expire_at = DeferredDispatch::<T>::get(call_hash)
298				.ok_or(Error::<T>::DeferredDispatchNotFound)?;
299
300			let now = T::BlockNumberProvider::current_block_number();
301
302			ensure!(now >= expire_at, Error::<T>::DeferredDispatchNotExpired);
303
304			DeferredDispatch::<T>::remove(call_hash);
305
306			Self::deposit_event(Event::<T>::DeferredDispatchRemoved { call_hash });
307
308			Ok(Pays::No.into())
309		}
310	}
311}
312
313impl<T: Config> Pallet<T> {
314	/// Defer the dispatch of a whitelisted call to a future block.
315	///
316	/// This function stores the call hash for later execution by any signed origin
317	/// before the expiration block.
318	fn defer_dispatch(call_hash: T::Hash) -> DispatchResult {
319		let now = T::BlockNumberProvider::current_block_number();
320
321		let expire_at = now.saturating_add(T::DeferredDispatchExpiration::get());
322
323		ensure!(!DeferredDispatch::<T>::contains_key(call_hash), Error::<T>::AlreadyDeferred);
324
325		DeferredDispatch::<T>::insert(call_hash, expire_at);
326
327		Self::deposit_event(Event::<T>::DispatchDeferred { call_hash });
328
329		Ok(())
330	}
331
332	/// Deferred dispatch sanity check.
333	///
334	/// Validates that:
335	/// - The origin is a signed account.
336	/// - A deferred dispatch entry exists for the call hash.
337	/// - The deferred dispatch has not yet expired.
338	/// - The call is still whitelisted.
339	///
340	/// The whitelist is always re-checked so that revoking the whitelist (via
341	/// [`Pallet::remove_whitelisted_call`]) prevents a relayer from executing a still-deferred
342	/// call.
343	///
344	/// Returns the signed account ID if all checks pass.
345	fn ensure_signed_deferred_dispatch(
346		origin: T::RuntimeOrigin,
347		call_hash: T::Hash,
348	) -> Result<T::AccountId, DispatchError> {
349		let who = ensure_signed(origin)?;
350
351		let expire_at =
352			DeferredDispatch::<T>::get(call_hash).ok_or(Error::<T>::DeferredDispatchNotFound)?;
353
354		ensure!(
355			T::BlockNumberProvider::current_block_number() < expire_at,
356			Error::<T>::DeferredDispatchExpired
357		);
358
359		ensure!(WhitelistedCall::<T>::contains_key(call_hash), Error::<T>::CallIsNotWhitelisted);
360
361		Ok(who)
362	}
363
364	/// Clean whitelisting/preimage and dispatch call.
365	///
366	/// Returns the inner call's actual weight.
367	fn clean_and_dispatch(call_hash: T::Hash, call: <T as Config>::RuntimeCall) -> Option<Weight> {
368		WhitelistedCall::<T>::remove(call_hash);
369		T::Preimages::unrequest(&call_hash);
370		DeferredDispatch::<T>::remove(call_hash);
371
372		let result = call.dispatch(frame_system::Origin::<T>::Root.into());
373
374		let call_actual_weight = match result {
375			Ok(call_post_info) => call_post_info.actual_weight,
376			Err(call_err) => call_err.post_info.actual_weight,
377		};
378		Self::deposit_event(Event::<T>::WhitelistedCallDispatched { call_hash, result });
379
380		call_actual_weight
381	}
382}