pallet_dynamic_evm_base_fee/
lib.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! Dynamic Evm Base Fee Pallet
20//!
21//! ## Overview
22//!
23//! The pallet is responsible for calculating `Base Fee Per Gas` value, according to the current system parameters.
24//! This is not like `EIP-1559`, instead it's intended for `Astar` and `Astar-like` networks, which allow both
25//! **Substrate native transactions** (which in `Astar` case reuse Polkadot transaction fee approach)
26//! and **EVM transactions** (which use `Base Fee Per Gas`).
27//!
28//! For a more detailed description, reader is advised to refer to Astar Network forum post about [Tokenomics 2.0](https://forum.astar.network/t/astar-tokenomics-2-0-a-dynamically-adjusted-inflation/4924).
29//!
30//! ## Approach
31//!
32//! The core formula this pallet tries to satisfy is:
33//!
34//! base_fee_per_gas = adjustment_factor * weight_factor * 25 / 98974
35//!
36//! Where:
37//! * **adjustment_factor** - is a value that changes in-between the blocks, related to the block fill ratio.
38//! * **weight_factor** - fixed constant, used to convert consumed _weight_ to _fee_.
39//!
40//! The implementation doesn't make any hard requirements on these values, and only requires that a type implementing `Get<_>` provides them.
41//!
42//! ## Implementation
43//!
44//! The core logic is implemented in `on_finalize` hook, which is called at the end of each block.
45//! This pallet's hook should be called AFTER whichever pallet's hook is responsible for updating **adjustment factor**.
46//!
47//! The hook will calculate the ideal new `base_fee_per_gas` value, and then clamp it in between the allowed limits.
48//!
49//! ## Interface
50//!
51//! Pallet provides an implementation of `FeeCalculator` trait. This makes it usable directly in `pallet-evm`.
52//!
53//! A _root-only_ extrinsic is provided to allow setting the `base_fee_per_gas` value manually.
54//!
55//! ## Practical Remarks
56//!
57//! According to the proposed **Tokenomics 2.0**, max amount that adjustment factor will be able to change on live networks in-between blocks is:
58//!
59//! adjustment_new = adjustment_old * (1 + adj + adj^2/2)
60//!
61//! adj = v * (s - s*)
62//! --> recommended _v_ value: 0.000_015
63//! --> largest 's' delta: (1 - 0.25) = **0.75**
64//!
65//! (for variable explanation please check the linked forum post above)
66//! (in short: `v` - variability factor, `s` - current block fill ratio, `s*` - ideal block fill ratio)
67//!
68//! adj = 0.000015 * (1 - 0.25) = **0.000_011_25**
69//! (1 + 0.000_011_25 + 0.000_011_25^2/2) = (1 + 0.000_011_25 + 0.000_000_000_063_281) = **1,000_011_250_063_281**
70//!
71//! Discarding the **1**, and only considering the decimals, this can be expressed as ratio:
72//! Expressed as ratio: 11_250_063_281 / 1_000_000_000_000_000.
73//! This is a much smaller change compared to the max step limit ratio we'll use to limit bfpg alignment.
74//! This means that once equilibrium is reached (fees are aligned), the `StepLimitRatio` will be larger than the max possible adjustment, essentially eliminating its effect.
75
76#![cfg_attr(not(feature = "std"), no_std)]
77
78use frame_support::weights::Weight;
79use sp_core::U256;
80use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber, FixedU128, Perquintill};
81
82pub use self::pallet::*;
83
84#[cfg(test)]
85mod mock;
86#[cfg(test)]
87mod tests;
88
89#[cfg(feature = "runtime-benchmarks")]
90mod benchmarking;
91
92pub mod weights;
93pub use weights::WeightInfo;
94
95#[frame_support::pallet]
96pub mod pallet {
97    use frame_support::pallet_prelude::*;
98    use frame_system::pallet_prelude::*;
99
100    use super::*;
101
102    /// The current storage version.
103    const STORAGE_VERSION: StorageVersion = StorageVersion::new(1);
104
105    #[pallet::pallet]
106    #[pallet::storage_version(STORAGE_VERSION)]
107    pub struct Pallet<T>(PhantomData<T>);
108
109    #[pallet::config]
110    pub trait Config: frame_system::Config {
111        /// Default base fee per gas value. Used in genesis if no other value specified explicitly.
112        type DefaultBaseFeePerGas: Get<U256>;
113        /// Minimum value 'base fee per gas' can be adjusted to. This is a defensive measure to prevent the fee from being too low.
114        type MinBaseFeePerGas: Get<U256>;
115        /// Maximum value 'base fee per gas' can be adjusted to. This is a defensive measure to prevent the fee from being too high.
116        type MaxBaseFeePerGas: Get<U256>;
117        /// Getter for the fee adjustment factor used in 'base fee per gas' formula. This is expected to change in-between the blocks (doesn't have to though).
118        type AdjustmentFactor: Get<FixedU128>;
119        /// The so-called `weight_factor` in the 'base fee per gas' formula.
120        type WeightFactor: Get<u128>;
121        /// Ratio limit on how much the 'base fee per gas' can change in-between two blocks.
122        /// It's expressed as percentage, and used to calculate the delta between the old and new value.
123        /// E.g. if the current 'base fee per gas' is 100, and the limit is 10%, then the new base fee per gas can be between 90 and 110.
124        type StepLimitRatio: Get<Perquintill>;
125        /// Weight information for extrinsics & functions of this pallet.
126        type WeightInfo: WeightInfo;
127    }
128
129    #[pallet::type_value]
130    pub fn DefaultBaseFeePerGas<T: Config>() -> U256 {
131        T::DefaultBaseFeePerGas::get()
132    }
133
134    #[pallet::storage]
135    pub type BaseFeePerGas<T> = StorageValue<_, U256, ValueQuery, DefaultBaseFeePerGas<T>>;
136
137    #[pallet::event]
138    #[pallet::generate_deposit(pub(super) fn deposit_event)]
139    pub enum Event {
140        /// New `base fee per gas` value has been force-set.
141        NewBaseFeePerGas { fee: U256 },
142    }
143
144    #[pallet::error]
145    pub enum Error<T> {
146        /// Specified value is outside of the allowed range.
147        ValueOutOfBounds,
148    }
149
150    #[pallet::hooks]
151    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
152        fn on_initialize(_: BlockNumberFor<T>) -> Weight {
153            T::WeightInfo::base_fee_per_gas_adjustment()
154        }
155
156        fn on_finalize(_n: BlockNumberFor<T>) {
157            BaseFeePerGas::<T>::mutate(|base_fee_per_gas| {
158                let old_bfpg = *base_fee_per_gas;
159
160                // Maximum step we're allowed to move the base fee per gas by.
161                let max_step = {
162                    let old_bfpg_u128: u128 = old_bfpg.unique_saturated_into();
163                    let step = T::StepLimitRatio::get() * old_bfpg_u128;
164                    U256::from(step)
165                };
166
167                // It's possible current base fee per gas is outside of the allowed range.
168                // This can & will happen when this solution is deployed on live networks.
169                //
170                // In such scenario, we will discard the lower & upper bounds configured in the runtime.
171                // Once these bounds are reached ONCE, the runtime logic will prevent them from going out of bounds again.
172                let apply_configured_bounds = old_bfpg >= T::MinBaseFeePerGas::get()
173                    && old_bfpg <= T::MaxBaseFeePerGas::get();
174                let (lower_limit, upper_limit) = if apply_configured_bounds {
175                    (
176                        T::MinBaseFeePerGas::get().max(old_bfpg.saturating_sub(max_step)),
177                        T::MaxBaseFeePerGas::get().min(old_bfpg.saturating_add(max_step)),
178                    )
179                } else {
180                    (
181                        old_bfpg.saturating_sub(max_step),
182                        old_bfpg.saturating_add(max_step),
183                    )
184                };
185
186                // Calculate ideal new 'base_fee_per_gas' according to the formula
187                let ideal_new_bfpg = T::AdjustmentFactor::get()
188                    // Weight factor should be multiplied first since it's a larger number, to avoid precision loss.
189                    .saturating_mul_int(T::WeightFactor::get())
190                    .saturating_mul(25)
191                    .saturating_div(98974);
192
193                // Clamp the ideal value in between the allowed limits
194                *base_fee_per_gas = U256::from(ideal_new_bfpg).clamp(lower_limit, upper_limit);
195            })
196        }
197
198        fn integrity_test() {
199            assert!(T::MinBaseFeePerGas::get() <= T::MaxBaseFeePerGas::get(),
200                "Minimum base fee per gas has to be equal or lower than maximum allowed base fee per gas.");
201
202            assert!(T::DefaultBaseFeePerGas::get() >= T::MinBaseFeePerGas::get(),
203                "Default base fee per gas has to be equal or higher than minimum allowed base fee per gas.");
204            assert!(T::DefaultBaseFeePerGas::get() <= T::MaxBaseFeePerGas::get(),
205                "Default base fee per gas has to be equal or lower than maximum allowed base fee per gas.");
206
207            assert!(T::MaxBaseFeePerGas::get() <= U256::from(u128::MAX),
208                "Maximum base fee per gas has to be equal or lower than u128::MAX, otherwise precision loss will occur.");
209        }
210    }
211
212    #[pallet::call]
213    impl<T: Config> Pallet<T> {
214        /// `root-only` extrinsic to set the `base_fee_per_gas` value manually.
215        /// The specified value has to respect min & max limits configured in the runtime.
216        #[pallet::call_index(0)]
217        #[pallet::weight(T::WeightInfo::set_base_fee_per_gas())]
218        pub fn set_base_fee_per_gas(origin: OriginFor<T>, fee: U256) -> DispatchResult {
219            ensure_root(origin)?;
220            ensure!(
221                fee >= T::MinBaseFeePerGas::get() && fee <= T::MaxBaseFeePerGas::get(),
222                Error::<T>::ValueOutOfBounds
223            );
224
225            BaseFeePerGas::<T>::put(fee);
226            Self::deposit_event(Event::NewBaseFeePerGas { fee });
227            Ok(())
228        }
229    }
230}
231
232impl<T: Config> fp_evm::FeeCalculator for Pallet<T> {
233    fn min_gas_price() -> (U256, Weight) {
234        (BaseFeePerGas::<T>::get(), T::WeightInfo::min_gas_price())
235    }
236}