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