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 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#[derive(Clone, Debug)]
81pub struct Allowance<Balance> {
82 pub max: Balance,
84 pub recovery_per_block: Balance,
86}
87
88pub trait RestrictedEntity<OriginCaller, Balance>: Sized {
90 fn allowance(&self) -> Allowance<Balance>;
92 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 #[derive(Encode, Decode, Clone, Eq, PartialEq, Debug, TypeInfo, MaxEncodedLen)]
108 pub struct Usage<Balance, BlockNumber> {
109 pub used: Balance,
111 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 #[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 type WeightInfo: WeightInfo;
145
146 type RestrictedEntity: RestrictedEntity<OriginCallerFor<Self>, BalanceOf<Self>>
156 + Parameter
157 + MaxEncodedLen;
158
159 type OperationAllowedOneTimeExcess: ContainsPair<Self::RestrictedEntity, Self::RuntimeCall>;
163
164 #[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 NoUsage,
173 NotZero,
175 }
176
177 #[pallet::event]
178 #[pallet::generate_deposit(fn deposit_event)]
179 pub enum Event<T: Config> {
180 UsageCleaned { entity: T::RestrictedEntity },
182 }
183
184 #[pallet::call(weight = <T as Config>::WeightInfo)]
185 impl<T: Config> Pallet<T> {
186 #[pallet::call_index(1)]
190 pub fn clean_usage(
191 origin: OriginFor<T>,
192 entity: T::RestrictedEntity,
193 ) -> DispatchResultWithPostInfo {
194 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#[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 pub fn new(enable: bool) -> Self {
239 Self(enable, core::marker::PhantomData)
240 }
241}
242
243#[derive(DebugNoBound)]
245pub enum Val<T: Config> {
246 Charge { fee: BalanceOf<T>, entity: T::RestrictedEntity },
247 NoCharge,
248}
249
250pub enum Pre<T: Config> {
253 Charge {
254 fee: BalanceOf<T>,
255 entity: T::RestrictedEntity,
256 },
257 NoCharge {
258 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 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 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}