1#![cfg_attr(not(feature = "std"), no_std)]
27
28extern crate alloc;
29
30const LOG_TARGET: &str = "runtime::pgas-allowance";
31
32use codec::{Decode, DecodeWithMemTracking, Encode};
33use frame_support::{
34 dispatch::{DispatchInfo, DispatchResult, PostDispatchInfo},
35 pallet_prelude::TransactionSource,
36 traits::{
37 Contains, Get,
38 tokens::{
39 Fortitude, Precision, Preservation,
40 fungibles::{self, Credit},
41 },
42 },
43 weights::Weight,
44};
45use frame_system::pallet_prelude::OriginFor;
46use pallet_transaction_payment::ChargeTransactionPayment;
47use scale_info::{StaticTypeInfo, TypeInfo};
48use sp_runtime::{
49 traits::{
50 AsSystemOriginSigner, DispatchInfoOf, Dispatchable, Implication, PostDispatchInfoOf,
51 RefundWeight, TransactionExtension, ValidateResult, Zero,
52 },
53 transaction_validity::{InvalidTransaction, TransactionValidityError, ValidTransaction},
54};
55
56pub use pallet::*;
57pub use weights::WeightInfo;
58
59#[cfg(feature = "runtime-benchmarks")]
60mod benchmarking;
61#[cfg(test)]
62mod mock;
63#[cfg(test)]
64mod tests;
65pub mod weights;
66
67type BalanceOf<T> = <<T as pallet_transaction_payment::Config>::OnChargeTransaction as
68 pallet_transaction_payment::OnChargeTransaction<T>>::Balance;
69
70type AssetIdOf<T> =
71 <<T as Config>::Assets as fungibles::Inspect<<T as frame_system::Config>::AccountId>>::AssetId;
72
73#[cfg(feature = "runtime-benchmarks")]
75pub trait BenchmarkHelperTrait<AccountId, AssetId, Balance> {
76 fn mint_pgas(who: &AccountId, asset_id: AssetId, amount: Balance);
78}
79
80#[frame_support::pallet]
81pub mod pallet {
82 use super::*;
83
84 #[pallet::config]
85 pub trait Config:
86 frame_system::Config<RuntimeEvent: From<Event<Self>>> + pallet_transaction_payment::Config
87 {
88 type Assets: fungibles::Balanced<Self::AccountId, Balance = BalanceOf<Self>>;
90
91 type PGASAssetId: frame_support::traits::Get<AssetIdOf<Self>>;
93
94 type CallFilter: Contains<<Self as frame_system::Config>::RuntimeCall>;
96
97 type WeightInfo: WeightInfo;
99
100 #[cfg(feature = "runtime-benchmarks")]
103 type BenchmarkHelper: crate::BenchmarkHelperTrait<Self::AccountId, AssetIdOf<Self>, BalanceOf<Self>>;
104 }
105
106 #[pallet::pallet]
107 pub struct Pallet<T>(_);
108
109 #[pallet::event]
110 #[pallet::generate_deposit(pub(super) fn deposit_event)]
111 pub enum Event<T: Config> {
112 PGASFeePaid { who: T::AccountId, actual_fee: BalanceOf<T> },
115 }
116}
117
118#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq)]
122pub struct ChargePGAS<T, S> {
123 inner: S,
124 #[codec(skip)]
127 skip_pgas: bool,
128 _phantom: core::marker::PhantomData<T>,
129}
130
131impl<T, S: StaticTypeInfo> TypeInfo for ChargePGAS<T, S> {
132 type Identity = S;
133 fn type_info() -> scale_info::Type {
134 S::type_info()
135 }
136}
137
138impl<T, S: Default> Default for ChargePGAS<T, S> {
139 fn default() -> Self {
140 Self { inner: S::default(), skip_pgas: false, _phantom: core::marker::PhantomData }
141 }
142}
143
144impl<T, S> ChargePGAS<T, S> {
145 pub fn new_skip_pgas(inner: S) -> Self {
148 Self { inner, skip_pgas: true, _phantom: core::marker::PhantomData }
149 }
150}
151
152impl<T, S> From<S> for ChargePGAS<T, S> {
153 fn from(inner: S) -> Self {
154 Self { inner, skip_pgas: false, _phantom: core::marker::PhantomData }
155 }
156}
157
158impl<T, S: core::fmt::Debug> core::fmt::Debug for ChargePGAS<T, S> {
159 #[cfg(feature = "std")]
160 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
161 write!(f, "ChargePGAS({:?})", self.inner)
162 }
163 #[cfg(not(feature = "std"))]
164 fn fmt(&self, _: &mut core::fmt::Formatter) -> core::fmt::Result {
165 Ok(())
166 }
167}
168
169pub enum Val<InnerVal, T: Config> {
171 PGAS { who: T::AccountId, fee: BalanceOf<T> },
173 Inner(InnerVal),
175}
176
177pub enum Pre<InnerPre, T: Config> {
179 PGAS {
181 who: T::AccountId,
183 credit: Credit<T::AccountId, T::Assets>,
185 weight_refund: Weight,
188 },
189 Inner {
191 inner: InnerPre,
193 extra_refund: Weight,
195 },
196}
197
198impl<T: Config + Send + Sync, S: TransactionExtension<T::RuntimeCall>>
199 TransactionExtension<T::RuntimeCall> for ChargePGAS<T, S>
200where
201 T::RuntimeCall: Dispatchable<Info = DispatchInfo, PostInfo = PostDispatchInfo>,
202 BalanceOf<T>: Send + Sync,
203 AssetIdOf<T>: Send + Sync,
204 <T::RuntimeCall as Dispatchable>::RuntimeOrigin: AsSystemOriginSigner<T::AccountId> + Clone,
205{
206 const IDENTIFIER: &'static str = S::IDENTIFIER;
207 type Implicit = S::Implicit;
208 type Val = Val<S::Val, T>;
209 type Pre = Pre<S::Pre, T>;
210
211 fn implicit(&self) -> Result<Self::Implicit, TransactionValidityError> {
212 self.inner.implicit()
213 }
214
215 fn metadata() -> alloc::vec::Vec<sp_runtime::traits::TransactionExtensionMetadata> {
216 S::metadata()
217 }
218
219 fn weight(&self, call: &T::RuntimeCall) -> Weight {
220 let inner = self.inner.weight(call);
221 if self.skip_pgas {
222 return inner;
223 }
224 if T::CallFilter::contains(call) {
225 <T as Config>::WeightInfo::charge_pgas()
226 .max(inner.saturating_add(<T as Config>::WeightInfo::charge_pgas_skip()))
227 } else {
228 inner
229 }
230 }
231
232 fn validate(
233 &self,
234 origin: OriginFor<T>,
235 call: &T::RuntimeCall,
236 info: &DispatchInfoOf<T::RuntimeCall>,
237 len: usize,
238 self_implicit: S::Implicit,
239 inherited_implication: &impl Implication,
240 source: TransactionSource,
241 ) -> ValidateResult<Self::Val, T::RuntimeCall> {
242 if !self.skip_pgas &&
245 let Some(who) = origin.as_system_origin_signer().cloned() &&
246 T::CallFilter::contains(call)
247 {
248 let fee = pallet_transaction_payment::Pallet::<T>::compute_fee(
249 len as u32,
250 info,
251 Zero::zero(),
252 );
253 let pgas = <T::Assets as fungibles::Inspect<T::AccountId>>::reducible_balance(
256 T::PGASAssetId::get(),
257 &who,
258 Preservation::Expendable,
259 Fortitude::Polite,
260 );
261 if pgas >= fee {
262 let priority =
263 ChargeTransactionPayment::<T>::get_priority(info, len, Zero::zero(), fee);
264 return Ok((
265 ValidTransaction { priority, ..Default::default() },
266 Val::PGAS { who, fee },
267 origin,
268 ));
269 }
270 }
271
272 let (validity, val, origin) = self.inner.validate(
274 origin,
275 call,
276 info,
277 len,
278 self_implicit,
279 inherited_implication,
280 source,
281 )?;
282 Ok((validity, Val::Inner(val), origin))
283 }
284
285 fn prepare(
286 self,
287 val: Self::Val,
288 origin: &OriginFor<T>,
289 call: &T::RuntimeCall,
290 info: &DispatchInfoOf<T::RuntimeCall>,
291 len: usize,
292 ) -> Result<Self::Pre, TransactionValidityError> {
293 let inner_weight = self.inner.weight(call);
294 let charge_pgas = <T as Config>::WeightInfo::charge_pgas();
295 let charge_pgas_skip = <T as Config>::WeightInfo::charge_pgas_skip();
296 match val {
297 Val::PGAS { who, fee } => {
298 let credit = <T::Assets as fungibles::Balanced<T::AccountId>>::withdraw(
301 T::PGASAssetId::get(),
302 &who,
303 fee,
304 Precision::Exact,
305 Preservation::Expendable,
306 Fortitude::Polite,
307 )
308 .map_err(|_| InvalidTransaction::Payment)?;
309
310 let reserved = charge_pgas.max(inner_weight.saturating_add(charge_pgas_skip));
313 let weight_refund = reserved.saturating_sub(charge_pgas);
314 Ok(Pre::PGAS { who, credit, weight_refund })
315 },
316 Val::Inner(val) => {
317 let extra_refund = if !self.skip_pgas && T::CallFilter::contains(call) {
318 let reserved = charge_pgas.max(inner_weight.saturating_add(charge_pgas_skip));
321 let consumed = if origin.as_system_origin_signer().is_some() {
322 inner_weight.saturating_add(charge_pgas_skip)
323 } else {
324 inner_weight
325 };
326 reserved.saturating_sub(consumed)
327 } else {
328 Weight::zero()
330 };
331 let inner = self.inner.prepare(val, origin, call, info, len)?;
332 Ok(Pre::Inner { inner, extra_refund })
333 },
334 }
335 }
336
337 fn post_dispatch_details(
338 pre: Self::Pre,
339 info: &DispatchInfoOf<T::RuntimeCall>,
340 post_info: &PostDispatchInfoOf<T::RuntimeCall>,
341 len: usize,
342 result: &DispatchResult,
343 ) -> Result<Weight, TransactionValidityError> {
344 match pre {
345 Pre::PGAS { who, credit, weight_refund } => {
346 let mut actual_post_info = *post_info;
347 actual_post_info.refund(weight_refund);
348 let actual_fee = pallet_transaction_payment::Pallet::<T>::compute_actual_fee(
349 len as u32,
350 info,
351 &actual_post_info,
352 Zero::zero(),
353 );
354
355 let reserved = credit.peek();
358 let (consumed, fee_refund) = credit.split(actual_fee);
359 let burned = if fee_refund.peek().is_zero() {
362 actual_fee
363 } else {
364 match <T::Assets as fungibles::Balanced<T::AccountId>>::resolve(
365 &who, fee_refund,
366 ) {
367 Ok(()) => actual_fee,
368 Err(fee_refund) => {
369 log::debug!(target: LOG_TARGET, "PGAS fee refund to {who:?} failed; burning full reserved fee {reserved:?}");
370 let _ = consumed.merge(fee_refund);
371 reserved
372 },
373 }
374 };
375 Pallet::<T>::deposit_event(Event::PGASFeePaid { who, actual_fee: burned });
376 Ok(weight_refund)
377 },
378 Pre::Inner { inner, extra_refund } => {
379 let inner_refund = S::post_dispatch_details(inner, info, post_info, len, result)?;
380 Ok(inner_refund.saturating_add(extra_refund))
381 },
382 }
383 }
384}