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