referrerpolicy=no-referrer-when-downgrade

pallet_pgas_allowance/
lib.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: Apache-2.0
3
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// 	http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16//! # Gas Allowance Pallet
17//!
18//! Provides the [`ChargePGAS`] transaction extension. When a signed transaction dispatching a
19//! call that passes [`Config::CallFilter`] is submitted by an account holding at least the
20//! required fee in the PGAS asset, the fee is withdrawn as a [`fungibles::Credit`] held in the
21//! extension's `Pre`. Any unused portion is refunded from that credit in `post_dispatch`; the
22//! remainder is dropped, which burns the consumed fee via `OnDropCredit`. A
23//! [`Event::PGASFeePaid`] event is emitted mirroring
24//! [`pallet_transaction_payment::Event::TransactionFeePaid`] so PGAS fee payments are observable.
25
26#![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/// Trait used by runtimes to mint PGAS to the benchmark caller.
74#[cfg(feature = "runtime-benchmarks")]
75pub trait BenchmarkHelperTrait<AccountId, AssetId, Balance> {
76	/// Mint `amount` of PGAS to `who`.
77	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		/// Access to the PGAS asset.
89		type Assets: fungibles::Balanced<Self::AccountId, Balance = BalanceOf<Self>>;
90
91		/// The PGAS asset id.
92		type PGASAssetId: frame_support::traits::Get<AssetIdOf<Self>>;
93
94		/// Filter deciding which calls are eligible to be paid with PGAS.
95		type CallFilter: Contains<<Self as frame_system::Config>::RuntimeCall>;
96
97		/// Weight information for the extension.
98		type WeightInfo: WeightInfo;
99
100		/// Helper used by the extension benchmarks to endow the caller with enough PGAS to cover
101		/// the fee.
102		#[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		/// A transaction fee `actual_fee` has been paid by `who` in PGAS and burned. Mirrors
113		/// [`pallet_transaction_payment::Event::TransactionFeePaid`].
114		PGASFeePaid { who: T::AccountId, actual_fee: BalanceOf<T> },
115	}
116}
117
118/// Transaction extension that charges transaction fees in PGAS when the caller holds enough and
119/// the dispatched call passes [`Config::CallFilter`]. Otherwise it delegates to the wrapped
120/// extension `S`.
121#[derive(Encode, Decode, DecodeWithMemTracking, Clone, Eq, PartialEq)]
122pub struct ChargePGAS<T, S> {
123	inner: S,
124	/// When set, the PGAS path is unconditionally skipped and the extension behaves as a pure
125	/// delegate to `inner`. Skipped in the codec because it can only be set by runtime code
126	#[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	/// Create a new `ChargePGAS` that unconditionally delegates to `inner`, skipping the PGAS
146	/// path entirely.
147	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
169/// Info passed from `validate` to `prepare`.
170pub enum Val<InnerVal, T: Config> {
171	/// Caller pays with PGAS: `fee` units will be withdrawn in `prepare`.
172	PGAS { who: T::AccountId, fee: BalanceOf<T> },
173	/// Delegate to the inner extension.
174	Inner(InnerVal),
175}
176
177/// Info passed from `prepare` to `post_dispatch`.
178pub enum Pre<InnerPre, T: Config> {
179	/// Fee withdrawn as a credit against the PGAS asset.
180	PGAS {
181		/// Account the fee was withdrawn from.
182		who: T::AccountId,
183		/// Credit holding the full reserved fee.
184		credit: Credit<T::AccountId, T::Assets>,
185		/// Weight difference between what [`ChargePGAS::weight`] reserved and the full PGAS path
186		/// (`charge_pgas`), returned to the caller in `post_dispatch`.
187		weight_refund: Weight,
188	},
189	/// Inner extension was used (filter miss, unsigned, or caller lacked PGAS).
190	Inner {
191		/// `Pre` produced by the inner extension, forwarded to its `post_dispatch`.
192		inner: InnerPre,
193		/// Weight to refund on top of whatever the inner extension refunds
194		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		// PGAS path: signed origin, call passes the filter, and caller holds at least `fee`.
243		// Skipped entirely when the extension was constructed with `new_skip_pgas`.
244		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			// `Expendable`: PGAS is meant to be minted across many accounts per user, so
254			// allow fee withdrawal to dust the account once it drops below ED.
255			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		// Fall through to the inner extension.
273		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				// PGAS is committed at `validate`; if the balance dropped since, the tx is
299				// rejected rather than falling back to the inner extension.
300				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				// `weight()` reserved `charge_pgas.max(inner + charge_pgas_skip)`; the PGAS path
311				// only consumes `charge_pgas`, so the excess is refunded.
312				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					// Filter matched, but likely the caller didn't hold enough PGAS, so we fell
319					// back to `S`.
320					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					// `skip_pgas` reserved only `inner_weight` in `weight()`, so no extra refund.
329					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				// Split the reserved credit into the consumed portion (dropped below to burn)
356				// and the refund owed back to `who`.
357				let reserved = credit.peek();
358				let (consumed, fee_refund) = credit.split(actual_fee);
359				// Equals `actual_fee` on the happy path; if the refund cannot be returned to
360				// `who` we burn the full reserved amount and report it.
361				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}