Skip to main content

pallet_transaction_fees/
lib.rs

1//! Pallet for charging and re-distributing transaction fees.
2
3#![cfg_attr(not(feature = "std"), no_std)]
4// TODO: Remove once FRAME macros stop generating `.clone()` calls on `Copy` types with user spans
5#![expect(clippy::clone_on_copy, reason = "Comes from Substrate")]
6#![forbid(unsafe_code)]
7#![warn(rust_2018_idioms, missing_debug_implementations)]
8
9pub mod weights;
10
11use frame_support::sp_runtime::SaturatedConversion;
12use frame_support::sp_runtime::traits::Zero;
13use frame_support::traits::{Currency, Get};
14use frame_support::weights::Weight;
15use frame_system::pallet_prelude::*;
16pub use pallet::*;
17use parity_scale_codec::{Codec, Decode, Encode};
18use scale_info::TypeInfo;
19use subspace_runtime_primitives::FindBlockRewardAddress;
20
21type BalanceOf<T> =
22    <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
23
24pub trait WeightInfo {
25    fn on_initialize() -> Weight;
26}
27
28#[derive(Encode, Decode, TypeInfo)]
29struct CollectedFees<Balance: Codec> {
30    storage: Balance,
31    compute: Balance,
32    tips: Balance,
33}
34
35#[frame_support::pallet]
36mod pallet {
37    use super::{BalanceOf, CollectedFees, WeightInfo};
38    use frame_support::pallet_prelude::*;
39    use frame_support::traits::Currency;
40    use frame_system::pallet_prelude::*;
41    use subspace_runtime_primitives::{BlockTransactionByteFee, FindBlockRewardAddress};
42
43    #[pallet::config]
44    pub trait Config: frame_system::Config<RuntimeEvent: From<Event<Self>>> {
45        /// Minimum desired number of replicas of the blockchain to be stored by the network,
46        /// impacts storage fees.
47        #[pallet::constant]
48        type MinReplicationFactor: Get<u16>;
49
50        /// How many credits there is in circulation.
51        #[pallet::constant]
52        type CreditSupply: Get<BalanceOf<Self>>;
53
54        /// How much space there is on the network.
55        #[pallet::constant]
56        type TotalSpacePledged: Get<u128>;
57
58        /// How big is the history of the blockchain in archived state (thus includes erasure
59        /// coding, but not replication).
60        #[pallet::constant]
61        type BlockchainHistorySize: Get<u128>;
62
63        type Currency: Currency<Self::AccountId>;
64
65        type FindBlockRewardAddress: FindBlockRewardAddress<Self::AccountId>;
66
67        /// Whether dynamic cost of storage should be used
68        type DynamicCostOfStorage: Get<bool>;
69
70        type WeightInfo: WeightInfo;
71    }
72
73    /// The value of `transaction_byte_fee` for both the current and the next block.
74    ///
75    /// The `next` value of `transaction_byte_fee` is updated at block finalization and used to
76    /// validate extrinsic to be included in the next block, the value is move to `current` at
77    /// block initialization and used to execute extrinsic in the current block. Together it
78    /// ensure we use the same value for both validating and executing the extrinsic.
79    ///
80    /// NOTE: both the `current` and `next` value is set to the default `Balance::max_value` in
81    /// the genesis block which means there will be no signed extrinsic included in block #1.
82    #[pallet::storage]
83    pub(super) type TransactionByteFee<T> =
84        StorageValue<_, BlockTransactionByteFee<BalanceOf<T>>, ValueQuery>;
85
86    /// Temporary value (cleared at block finalization) used to determine if the `transaction_byte_fee`
87    /// is used to validate extrinsic or execute extrinsic.
88    #[pallet::storage]
89    pub(super) type IsDuringBlockExecution<T: Config> = StorageValue<_, bool, ValueQuery>;
90
91    /// Temporary value (cleared at block finalization) which contains current block author, so we
92    /// can issue fees during block finalization.
93    #[pallet::storage]
94    pub(super) type BlockAuthor<T: Config> = StorageValue<_, T::AccountId>;
95
96    /// Temporary value (cleared at block finalization) which contains current block fees, so we can
97    /// issue fees during block finalization.
98    #[pallet::storage]
99    pub(super) type CollectedBlockFees<T: Config> = StorageValue<_, CollectedFees<BalanceOf<T>>>;
100
101    /// Pallet transaction fees for issuing fees to block authors.
102    #[pallet::pallet]
103    #[pallet::without_storage_info]
104    pub struct Pallet<T>(_);
105
106    /// `pallet-transaction-fees` events
107    #[pallet::event]
108    #[pallet::generate_deposit(pub(super) fn deposit_event)]
109    pub enum Event<T: Config> {
110        /// Storage fees.
111        #[codec(index = 0)]
112        BlockFees {
113            /// Block author that received the fees.
114            who: T::AccountId,
115            /// Amount of collected storage fees.
116            storage: BalanceOf<T>,
117            /// Amount of collected compute fees.
118            compute: BalanceOf<T>,
119            /// Amount of collected tips.
120            tips: BalanceOf<T>,
121        },
122        /// Fees burned due to equivocated block author or rewards not enabled.
123        #[codec(index = 1)]
124        BurnedBlockFees {
125            /// Amount of burned storage fees.
126            storage: BalanceOf<T>,
127            /// Amount of burned compute fees.
128            compute: BalanceOf<T>,
129            /// Amount of burned tips.
130            tips: BalanceOf<T>,
131        },
132    }
133
134    #[pallet::hooks]
135    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T>
136    where
137        BalanceOf<T>: From<u8> + From<u64>,
138    {
139        fn on_initialize(now: BlockNumberFor<T>) -> Weight {
140            Self::do_initialize(now);
141            T::WeightInfo::on_initialize()
142        }
143
144        fn on_finalize(now: BlockNumberFor<T>) {
145            Self::do_finalize(now);
146        }
147    }
148}
149
150impl<T: Config> Pallet<T>
151where
152    BalanceOf<T>: From<u64>,
153{
154    fn do_initialize(_n: BlockNumberFor<T>) {
155        // Block author may equivocate, in which case they'll not be present here
156        if let Some(block_author) = T::FindBlockRewardAddress::find_block_reward_address() {
157            BlockAuthor::<T>::put(block_author);
158        }
159
160        CollectedBlockFees::<T>::put(CollectedFees {
161            storage: BalanceOf::<T>::zero(),
162            compute: BalanceOf::<T>::zero(),
163            tips: BalanceOf::<T>::zero(),
164        });
165
166        // Update the `current` value to the `next`
167        TransactionByteFee::<T>::mutate(|transaction_byte_fee| {
168            transaction_byte_fee.current = transaction_byte_fee.next
169        });
170        IsDuringBlockExecution::<T>::set(true);
171    }
172
173    fn do_finalize(_n: BlockNumberFor<T>) {
174        // Update the value for the next `transaction_byte_fee`
175        TransactionByteFee::<T>::mutate(|transaction_byte_fee| {
176            transaction_byte_fee.next = Self::calculate_transaction_byte_fee()
177        });
178        IsDuringBlockExecution::<T>::take();
179
180        let collected_fees = CollectedBlockFees::<T>::take()
181            .expect("`CollectedBlockFees` was set in `on_initialize`; qed");
182
183        let total = collected_fees.storage + collected_fees.compute + collected_fees.tips;
184
185        if !total.is_zero() {
186            // Block author may equivocate, in which case they'll not be present here
187            if let Some(block_author) = BlockAuthor::<T>::take() {
188                let _imbalance = T::Currency::deposit_creating(&block_author, total);
189                Self::deposit_event(Event::<T>::BlockFees {
190                    who: block_author.clone(),
191                    storage: collected_fees.storage,
192                    compute: collected_fees.compute,
193                    tips: collected_fees.tips,
194                });
195            } else {
196                // If farmer equivocated, fees are burned
197                let amount = collected_fees.storage + collected_fees.compute + collected_fees.tips;
198                if !amount.is_zero() {
199                    Self::deposit_event(Event::<T>::BurnedBlockFees {
200                        storage: collected_fees.storage,
201                        compute: collected_fees.compute,
202                        tips: collected_fees.tips,
203                    });
204                }
205            }
206        }
207    }
208
209    /// Return the current `transaction_byte_fee` value for executing extrinsic and
210    /// return the next `transaction_byte_fee` value for validating extrinsic to be
211    /// included in the next block
212    pub fn transaction_byte_fee() -> BalanceOf<T> {
213        if !T::DynamicCostOfStorage::get() {
214            return BalanceOf::<T>::from(1);
215        }
216
217        if IsDuringBlockExecution::<T>::get() {
218            TransactionByteFee::<T>::get().current
219        } else {
220            TransactionByteFee::<T>::get().next
221        }
222    }
223
224    pub fn calculate_transaction_byte_fee() -> BalanceOf<T> {
225        let credit_supply = T::CreditSupply::get();
226
227        match (T::TotalSpacePledged::get() / u128::from(T::MinReplicationFactor::get()))
228            .checked_sub(T::BlockchainHistorySize::get())
229        {
230            Some(free_space) if free_space > 0 => {
231                credit_supply / BalanceOf::<T>::saturated_from(free_space)
232            }
233            _ => credit_supply,
234        }
235    }
236
237    pub fn note_transaction_fees(
238        storage_fee: BalanceOf<T>,
239        compute_fee: BalanceOf<T>,
240        tip: BalanceOf<T>,
241    ) {
242        CollectedBlockFees::<T>::mutate(|collected_block_fees| {
243            // `CollectedBlockFees` was set in `on_initialize` if it is `None` means this
244            // function is called offchain (i.e. transaction validation) thus safe to skip
245            if let Some(collected_block_fees) = collected_block_fees.as_mut() {
246                collected_block_fees.storage += storage_fee;
247                collected_block_fees.compute += compute_fee;
248                collected_block_fees.tips += tip;
249            }
250        });
251    }
252}
253
254impl<T: Config> subspace_runtime_primitives::StorageFee<BalanceOf<T>> for Pallet<T>
255where
256    BalanceOf<T>: From<u64>,
257{
258    fn transaction_byte_fee() -> BalanceOf<T> {
259        Self::transaction_byte_fee()
260    }
261
262    fn note_storage_fees(storage_fee: BalanceOf<T>) {
263        Self::note_transaction_fees(storage_fee, Zero::zero(), Zero::zero())
264    }
265}