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}