Skip to main content

pezpallet_origin_restriction/
lib.rs

1// This file is part of Bizinikiwi.
2
3// Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute
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 pezpallet and transaction extension
19//!
20//! This pezpallet 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 pezframe_support::{
57	dispatch::{DispatchInfo, PostDispatchInfo},
58	pezpallet_prelude::{Pays, Zero},
59	traits::{ContainsPair, OriginTrait},
60	weights::WeightToFee,
61	Parameter, RuntimeDebugNoBound,
62};
63use pezframe_system::pezpallet_prelude::BlockNumberFor;
64use pezpallet_transaction_payment::OnChargeTransaction;
65use pezsp_runtime::{
66	traits::{
67		AsTransactionAuthorizedOrigin, DispatchInfoOf, DispatchOriginOf, Dispatchable, Implication,
68		PostDispatchInfoOf, TransactionExtension, ValidateResult,
69	},
70	transaction_validity::{
71		InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction,
72	},
73	DispatchError::BadOrigin,
74	DispatchResult, RuntimeDebug, SaturatedConversion, Saturating, Weight,
75};
76use scale_info::TypeInfo;
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 pezpallet::*;
99#[pezframe_support::pezpallet]
100pub mod pezpallet {
101	use super::*;
102	use pezframe_support::{pezpallet_prelude::*, traits::ContainsPair};
103	use pezframe_system::pezpallet_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 pezframe_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
116	pub(crate) type BalanceOf<T> =
117		<<T as pezpallet_transaction_payment::Config>::OnChargeTransaction as OnChargeTransaction<
118			T,
119		>>::Balance;
120
121	#[pezpallet::pezpallet]
122	pub struct Pezpallet<T>(_);
123
124	/// The current usage for each entity.
125	#[pezpallet::storage]
126	pub type Usages<T: Config> = StorageMap<
127		_,
128		Blake2_128Concat,
129		T::RestrictedEntity,
130		Usage<BalanceOf<T>, BlockNumberFor<T>>,
131	>;
132
133	#[pezpallet::config]
134	pub trait Config:
135		pezframe_system::Config<
136			RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
137			RuntimeOrigin: AsTransactionAuthorizedOrigin,
138		> + pezpallet_transaction_payment::Config
139		+ Send
140		+ Sync
141	{
142		/// The weight information for this pezpallet.
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>>
166			+ IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
167	}
168
169	#[pezpallet::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	#[pezpallet::event]
178	#[pezpallet::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	#[pezpallet::call(weight = <T as Config>::WeightInfo)]
185	impl<T: Config> Pezpallet<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		#[pezpallet::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 = pezframe_system::Pezpallet::<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(
233	Encode, Decode, Clone, Eq, PartialEq, TypeInfo, RuntimeDebugNoBound, DecodeWithMemTracking,
234)]
235#[scale_info(skip_type_params(T))]
236pub struct RestrictOrigin<T>(bool, core::marker::PhantomData<T>);
237
238impl<T> RestrictOrigin<T> {
239	/// Instantiates a new `RestrictOrigins` extension.
240	pub fn new(enable: bool) -> Self {
241		Self(enable, core::marker::PhantomData)
242	}
243}
244
245/// The info passed between the validate and prepare steps for the `RestrictOrigins` extension.
246#[derive(RuntimeDebugNoBound)]
247pub enum Val<T: Config> {
248	Charge { fee: BalanceOf<T>, entity: T::RestrictedEntity },
249	NoCharge,
250}
251
252/// The info passed between the prepare and post-dispatch steps for the `RestrictOrigins`
253/// extension.
254pub enum Pre<T: Config> {
255	Charge {
256		fee: BalanceOf<T>,
257		entity: T::RestrictedEntity,
258	},
259	NoCharge {
260		// weight initially estimated by the extension, to be refunded
261		refund: Weight,
262	},
263}
264
265impl<T: Config> TransactionExtension<T::RuntimeCall> for RestrictOrigin<T> {
266	const IDENTIFIER: &'static str = "RestrictOrigins";
267	type Implicit = ();
268	type Val = Val<T>;
269	type Pre = Pre<T>;
270
271	fn weight(&self, _call: &T::RuntimeCall) -> pezframe_support::weights::Weight {
272		if !self.0 {
273			return Weight::zero();
274		}
275
276		<T as Config>::WeightInfo::restrict_origin_tx_ext()
277	}
278
279	fn validate(
280		&self,
281		origin: DispatchOriginOf<T::RuntimeCall>,
282		call: &T::RuntimeCall,
283		info: &DispatchInfoOf<T::RuntimeCall>,
284		len: usize,
285		_self_implicit: (),
286		_inherited_implication: &impl Implication,
287		_source: TransactionSource,
288	) -> ValidateResult<Self::Val, T::RuntimeCall> {
289		let origin_caller = origin.caller();
290		let Some(entity) = T::RestrictedEntity::restricted_entity(origin_caller) else {
291			return Ok((ValidTransaction::default(), Val::NoCharge, origin));
292		};
293		let allowance = T::RestrictedEntity::allowance(&entity);
294
295		if !self.0 {
296			// Extension is disabled, but the restriction must happen, the extension should have
297			// been enabled.
298			return Err(InvalidTransaction::Call.into());
299		}
300
301		let now = pezframe_system::Pezpallet::<T>::block_number();
302		let mut usage = match Usages::<T>::get(&entity) {
303			Some(mut usage) => {
304				let elapsed = now.saturating_sub(usage.at_block).saturated_into::<u32>();
305				let receive_back = allowance.recovery_per_block.saturating_mul(elapsed.into());
306				usage.used = usage.used.saturating_sub(receive_back);
307				usage.at_block = now;
308				usage
309			},
310			None => Usage { used: 0u32.into(), at_block: now },
311		};
312
313		// The usage before taking into account this extrinsic.
314		let usage_without_new_xt = usage.used;
315		let fee = extrinsic_fee::<T>(info.total_weight(), len);
316		usage.used = usage.used.saturating_add(fee);
317
318		Usages::<T>::insert(&entity, &usage);
319
320		let allowed_one_time_excess = || {
321			usage_without_new_xt == 0u32.into()
322				&& T::OperationAllowedOneTimeExcess::contains(&entity, call)
323		};
324		if usage.used <= allowance.max || allowed_one_time_excess() {
325			Ok((ValidTransaction::default(), Val::Charge { fee, entity }, origin))
326		} else {
327			Err(InvalidTransaction::Payment.into())
328		}
329	}
330
331	fn prepare(
332		self,
333		val: Self::Val,
334		_origin: &DispatchOriginOf<T::RuntimeCall>,
335		call: &T::RuntimeCall,
336		_info: &DispatchInfoOf<T::RuntimeCall>,
337		_len: usize,
338	) -> Result<Self::Pre, TransactionValidityError> {
339		match val {
340			Val::Charge { fee, entity } => Ok(Pre::Charge { fee, entity }),
341			Val::NoCharge => Ok(Pre::NoCharge { refund: self.weight(call) }),
342		}
343	}
344
345	fn post_dispatch_details(
346		pre: Self::Pre,
347		_info: &DispatchInfoOf<T::RuntimeCall>,
348		post_info: &PostDispatchInfoOf<T::RuntimeCall>,
349		_len: usize,
350		_result: &DispatchResult,
351	) -> Result<Weight, TransactionValidityError> {
352		match pre {
353			Pre::Charge { fee, entity } => {
354				if post_info.pays_fee == Pays::No {
355					Usages::<T>::mutate_exists(entity, |maybe_usage| {
356						if let Some(usage) = maybe_usage {
357							usage.used = usage.used.saturating_sub(fee);
358
359							if usage.used.is_zero() {
360								*maybe_usage = None;
361							}
362						}
363					});
364					Ok(Weight::zero())
365				} else {
366					Ok(Weight::zero())
367				}
368			},
369			Pre::NoCharge { refund } => Ok(refund),
370		}
371	}
372}