pallet_origin_restriction/
lib.rs1#![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#[derive(Clone, Debug)]
80pub struct Allowance<Balance> {
81 pub max: Balance,
83 pub recovery_per_block: Balance,
85}
86
87pub trait RestrictedEntity<OriginCaller, Balance>: Sized {
89 fn allowance(&self) -> Allowance<Balance>;
91 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 #[derive(Encode, Decode, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
107 pub struct Usage<Balance, BlockNumber> {
108 pub used: Balance,
110 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 #[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 type WeightInfo: WeightInfo;
144
145 type RestrictedEntity: RestrictedEntity<OriginCallerFor<Self>, BalanceOf<Self>>
155 + Parameter
156 + MaxEncodedLen;
157
158 type OperationAllowedOneTimeExcess: ContainsPair<Self::RestrictedEntity, Self::RuntimeCall>;
162
163 #[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 NoUsage,
172 NotZero,
174 }
175
176 #[pallet::event]
177 #[pallet::generate_deposit(fn deposit_event)]
178 pub enum Event<T: Config> {
179 UsageCleaned { entity: T::RestrictedEntity },
181 }
182
183 #[pallet::call(weight = <T as Config>::WeightInfo)]
184 impl<T: Config> Pallet<T> {
185 #[pallet::call_index(1)]
189 pub fn clean_usage(
190 origin: OriginFor<T>,
191 entity: T::RestrictedEntity,
192 ) -> DispatchResultWithPostInfo {
193 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#[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 pub fn new(enable: bool) -> Self {
240 Self(enable, core::marker::PhantomData)
241 }
242}
243
244#[derive(RuntimeDebugNoBound)]
246pub enum Val<T: Config> {
247 Charge { fee: BalanceOf<T>, entity: T::RestrictedEntity },
248 NoCharge,
249}
250
251pub enum Pre<T: Config> {
254 Charge {
255 fee: BalanceOf<T>,
256 entity: T::RestrictedEntity,
257 },
258 NoCharge {
259 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 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 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}