referrerpolicy=no-referrer-when-downgrade

pallet_origin_restriction/
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//! # Origin restriction pallet and transaction extension
19//!
20//! This pallet tracks certain origin and limits how much total "fee usage" they can accumulate.
21//! Usage gradually recovers as blocks pass.
22//!
23//! First the entity is extracted from the restricted origin, the entity represents the granularity
24//! of usage tracking.
25//!
26//! For example, an origin like `DaoOrigin { name: [u8; 8], tally: Percent }`
27//! can have its usage tracked and restricted at the DAO level, so the tracked entity would be
28//! `DaoEntity { name: [u8; 8] }`. This ensures that usage restrictions apply to the DAO as a whole,
29//! independent of any particular voter percentage.
30//!
31//! Then when dispatching a transaction, if the entity’s new usage would exceed its max allowance,
32//! the transaction is invalid, except if the call is in the set of calls permitted to exceed that
33//! limit (see `OperationAllowedOneTimeExcess`). In that case, as long as the entity's usage prior
34//! to dispatch was zero, the transaction is valid (with respect to usage). If the entity's
35//! usage is already above the limit, the transaction is always invalid. After dispatch, any call
36//! flagged as `Pays::No` fully restores the consumed usage.
37//!
38//! To expand on `OperationAllowedOneTimeExcess`, user have to wait for the usage to completely
39//! recover to zero before being able to do an operation that exceed max allowance.
40
41#![cfg_attr(not(feature = "std"), no_std)]
42
43#[cfg(feature = "runtime-benchmarks")]
44mod benchmarking;
45#[cfg(test)]
46mod mock;
47#[cfg(test)]
48mod tests;
49pub mod weights;
50
51extern crate alloc;
52
53pub use weights::WeightInfo;
54
55use codec::{Decode, DecodeWithMemTracking, Encode};
56use frame_support::{
57	dispatch::{DispatchInfo, PostDispatchInfo},
58	pallet_prelude::{Pays, Zero},
59	traits::{ContainsPair, OriginTrait},
60	weights::WeightToFee,
61	DebugNoBound, Parameter,
62};
63use frame_system::pallet_prelude::BlockNumberFor;
64use pallet_transaction_payment::OnChargeTransaction;
65use scale_info::TypeInfo;
66use sp_runtime::{
67	traits::{
68		AsTransactionAuthorizedOrigin, DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication,
69		PostDispatchInfoOf, TransactionExtension, ValidateResult,
70	},
71	transaction_validity::{
72		InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
73	},
74	Debug,
75	DispatchError::BadOrigin,
76	DispatchResult, SaturatedConversion, Saturating, Weight,
77};
78
79/// The allowance for an entity, defining its usage limit and recovery rate.
80#[derive(Clone, Debug)]
81pub struct Allowance<Balance> {
82	/// The maximum usage allowed before transactions are restricted.
83	pub max: Balance,
84	/// The amount of usage recovered per block.
85	pub recovery_per_block: Balance,
86}
87
88/// The restriction of an entity.
89pub trait RestrictedEntity<OriginCaller, Balance>: Sized {
90	/// The allowance given for the entity.
91	fn allowance(&self) -> Allowance<Balance>;
92	/// Whether the origin is restricted, and what entity it belongs to.
93	fn restricted_entity(caller: &OriginCaller) -> Option<Self>;
94
95	#[cfg(feature = "runtime-benchmarks")]
96	fn benchmarked_restricted_origin() -> OriginCaller;
97}
98
99pub use pallet::*;
100#[frame_support::pallet]
101pub mod pallet {
102	use super::*;
103	use frame_support::{pallet_prelude::*, traits::ContainsPair};
104	use frame_system::pallet_prelude::*;
105
106	/// The usage of an entity.
107	#[derive(Encode, Decode, Clone, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
108	pub struct Usage<Balance, BlockNumber> {
109		/// The amount of usage consumed at block `at_block`.
110		pub used: Balance,
111		/// The block number at which the usage was last updated.
112		pub at_block: BlockNumber,
113	}
114
115	pub(crate) type OriginCallerFor<T> =
116		<<T as frame_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
117	pub(crate) type BalanceOf<T> =
118		<<T as pallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
119			T,
120		>>::Balance;
121
122	#[pallet::pallet]
123	pub struct Pallet<T>(_);
124
125	/// The current usage for each entity.
126	#[pallet::storage]
127	pub type Usages<T: Config> = StorageMap<
128		_,
129		Blake2_128Concat,
130		T::RestrictedEntity,
131		Usage<BalanceOf<T>, BlockNumberFor<T>>,
132	>;
133
134	#[pallet::config]
135	pub trait Config:
136		frame_system::Config<
137			RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
138			RuntimeOrigin: AsTransactionAuthorizedOrigin,
139		> + pallet_transaction_payment::Config
140		+ Send
141		+ Sync
142	{
143		/// The weight information for this pallet.
144		type WeightInfo: WeightInfo;
145
146		/// The type that represent the entities tracked, its allowance and the conversion from
147		/// origin is bounded in [`RestrictedEntity`].
148		///
149		/// This is the canonical origin from the point of view of usage tracking.
150		/// Each entity is tracked separately.
151		///
152		/// This is different from origin as a multiple origin can represent a single entity.
153		/// For example, imagine a DAO origin with a percentage of voters, we want to track the DAO
154		/// entity regardless of the voter percentage.
155		type RestrictedEntity: RestrictedEntity<OriginCallerFor<Self>, BalanceOf<Self>>
156			+ Parameter
157			+ MaxEncodedLen;
158
159		/// For some entities, the calls that are allowed to go beyond the max allowance.
160		///
161		/// This must be only for call which have a reasonable maximum weight and length.
162		type OperationAllowedOneTimeExcess: ContainsPair<Self::RestrictedEntity, Self::RuntimeCall>;
163
164		/// The runtime event type.
165		#[allow(deprecated)]
166		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
167	}
168
169	#[pallet::error]
170	pub enum Error<T> {
171		/// The origin has no usage tracked.
172		NoUsage,
173		/// The usage is not zero.
174		NotZero,
175	}
176
177	#[pallet::event]
178	#[pallet::generate_deposit(fn deposit_event)]
179	pub enum Event<T: Config> {
180		/// Usage for an entity is cleaned.
181		UsageCleaned { entity: T::RestrictedEntity },
182	}
183
184	#[pallet::call(weight = <T as Config>::WeightInfo)]
185	impl<T: Config> Pallet<T> {
186		/// Allow to clean usage associated with an entity when it is zero or when there is no
187		/// longer any allowance for the origin.
188		// This could be an unsigned call
189		#[pallet::call_index(1)]
190		pub fn clean_usage(
191			origin: OriginFor<T>,
192			entity: T::RestrictedEntity,
193		) -> DispatchResultWithPostInfo {
194			// `None` origin is better to reject in general, due to being used for inherents and
195			// validate unsigned.
196			if ensure_none(origin.clone()).is_ok() {
197				return Err(BadOrigin.into())
198			}
199
200			let Some(mut usage) = Usages::<T>::take(&entity) else {
201				return Err(Error::<T>::NoUsage.into())
202			};
203
204			let now = frame_system::Pallet::<T>::block_number();
205			let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
206
207			let allowance = entity.allowance();
208			let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
209			usage.used = usage.used.saturating_sub(receive_back);
210
211			ensure!(usage.used.is_zero(), Error::<T>::NotZero);
212
213			Self::deposit_event(Event::UsageCleaned { entity });
214
215			Ok(Pays::No.into())
216		}
217	}
218}
219
220fn extrinsic_fee<T: Config>(weight: Weight, length: usize) -> BalanceOf<T> {
221	let weight_fee = T::WeightToFee::weight_to_fee(&weight);
222	let length_fee = T::LengthToFee::weight_to_fee(&Weight::from_parts(length as u64, 0));
223	weight_fee.saturating_add(length_fee)
224}
225
226/// This transaction extension restricts some origins and prevents them from dispatching calls,
227/// based on their usage and allowance.
228///
229/// The extension can be enabled or disabled with the inner boolean. When enabled, the restriction
230/// process executes. When disabled, only the `RestrictedOrigins` check is executed.
231/// You can always enable it, the only advantage of disabling it is have better pre-dispatch weight.
232#[derive(Encode, Decode, Clone, Eq, PartialEq, TypeInfo, DebugNoBound, DecodeWithMemTracking)]
233#[scale_info(skip_type_params(T))]
234pub struct RestrictOrigin<T>(bool, core::marker::PhantomData<T>);
235
236impl<T> RestrictOrigin<T> {
237	/// Instantiates a new `RestrictOrigins` extension.
238	pub fn new(enable: bool) -> Self {
239		Self(enable, core::marker::PhantomData)
240	}
241}
242
243/// The info passed between the validate and prepare steps for the `RestrictOrigins` extension.
244#[derive(DebugNoBound)]
245pub enum Val<T: Config> {
246	Charge { fee: BalanceOf<T>, entity: T::RestrictedEntity },
247	NoCharge,
248}
249
250/// The info passed between the prepare and post-dispatch steps for the `RestrictOrigins`
251/// extension.
252pub enum Pre<T: Config> {
253	Charge {
254		fee: BalanceOf<T>,
255		entity: T::RestrictedEntity,
256	},
257	NoCharge {
258		// weight initially estimated by the extension, to be refunded
259		refund: Weight,
260	},
261}
262
263impl<T: Config> TransactionExtension<T::RuntimeCall> for RestrictOrigin<T> {
264	const IDENTIFIER: &'static str = "RestrictOrigins";
265	type Implicit = ();
266	type Val = Val<T>;
267	type Pre = Pre<T>;
268
269	fn weight(&self, _call: &T::RuntimeCall) -> frame_support::weights::Weight {
270		if !self.0 {
271			return Weight::zero()
272		}
273
274		<T as Config>::WeightInfo::restrict_origin_tx_ext()
275	}
276
277	fn validate(
278		&self,
279		origin: DispatchOriginOf<T::RuntimeCall>,
280		call: &T::RuntimeCall,
281		info: &DispatchInfoOf<T::RuntimeCall>,
282		len: usize,
283		_self_implicit: (),
284		_inherited_implication: &impl Implication,
285		_source: TransactionSource,
286	) -> ValidateResult<Self::Val, T::RuntimeCall> {
287		let origin_caller = origin.caller();
288		let Some(entity) = T::RestrictedEntity::restricted_entity(origin_caller) else {
289			return Ok((ValidTransaction::default(), Val::NoCharge, origin));
290		};
291		let allowance = T::RestrictedEntity::allowance(&entity);
292
293		if !self.0 {
294			// Extension is disabled, but the restriction must happen, the extension should have
295			// been enabled.
296			return Err(InvalidTransaction::Call.into())
297		}
298
299		let now = frame_system::Pallet::<T>::block_number();
300		let mut usage = match Usages::<T>::get(&entity) {
301			Some(mut usage) => {
302				let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
303				let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
304				usage.used = usage.used.saturating_sub(receive_back);
305				usage.at_block = now;
306				usage
307			},
308			None => Usage { used: 0u32.into(), at_block: now },
309		};
310
311		// The usage before taking into account this extrinsic.
312		let usage_without_new_xt = usage.used;
313		let fee = extrinsic_fee::<T>(info.total_weight(), len);
314		usage.used = usage.used.saturating_add(fee);
315
316		Usages::<T>::insert(&entity, &usage);
317
318		let allowed_one_time_excess = || {
319			usage_without_new_xt == 0u32.into() &&
320				T::OperationAllowedOneTimeExcess::contains(&entity, call)
321		};
322		if usage.used <= allowance.max || allowed_one_time_excess() {
323			Ok((ValidTransaction::default(), Val::Charge { fee, entity }, origin))
324		} else {
325			Err(InvalidTransaction::Payment.into())
326		}
327	}
328
329	fn prepare(
330		self,
331		val: Self::Val,
332		_origin: &DispatchOriginOf<T::RuntimeCall>,
333		call: &T::RuntimeCall,
334		_info: &DispatchInfoOf<T::RuntimeCall>,
335		_len: usize,
336	) -> Result<Self::Pre, TransactionValidityError> {
337		match val {
338			Val::Charge { fee, entity } => Ok(Pre::Charge { fee, entity }),
339			Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
340		}
341	}
342
343	fn post_dispatch_details(
344		pre: Self::Pre,
345		_info: &DispatchInfoOf<T::RuntimeCall>,
346		post_info: &PostDispatchInfoOf<T::RuntimeCall>,
347		_len: usize,
348		_result: &DispatchResult,
349	) -> Result<Weight, TransactionValidityError> {
350		match pre {
351			Pre::Charge { fee, entity } =>
352				if post_info.pays_fee == Pays::No {
353					Usages::<T>::mutate_exists(entity, |maybe_usage| {
354						if let Some(usage) = maybe_usage {
355							usage.used = usage.used.saturating_sub(fee);
356
357							if usage.used.is_zero() {
358								*maybe_usage = None;
359							}
360						}
361					});
362					Ok(Weight::zero())
363				} else {
364					Ok(Weight::zero())
365				},
366			Pre::NoCharge { refund } => Ok(refund),
367		}
368	}
369}