diff --git a/Cargo.lock b/Cargo.lock index e82eced0c5..20d049c330 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7153,6 +7153,8 @@ dependencies = [ "fp-evm", "frame-support", "frame-system", + "pallet-balances", + "pallet-timestamp", "pallet-transaction-payment", "parity-scale-codec", "scale-info", diff --git a/pallets/dynamic-evm-base-fee/Cargo.toml b/pallets/dynamic-evm-base-fee/Cargo.toml index 6ba0e41c13..90dc93dfe5 100644 --- a/pallets/dynamic-evm-base-fee/Cargo.toml +++ b/pallets/dynamic-evm-base-fee/Cargo.toml @@ -15,13 +15,17 @@ scale-info = { workspace = true } # Substrate frame-support = { workspace = true } frame-system = { workspace = true } +pallet-transaction-payment = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } -pallet-transaction-payment = { workspace = true } # Frontier fp-evm = { workspace = true } +[dev-dependencies] +pallet-balances = { workspace = true } +pallet-timestamp = { workspace = true } + [features] default = ["std"] std = [ @@ -32,12 +36,14 @@ std = [ "frame-system/std", "sp-core/std", "sp-runtime/std", - "pallet-transaction-payment/std", + "pallet-transaction-payment/std", + "pallet-balances/std", + "pallet-timestamp/std", # Frontier "fp-evm/std", ] try-runtime = [ "frame-support/try-runtime", "frame-system/try-runtime", - "pallet-transaction-payment/try-runtime", + "pallet-transaction-payment/try-runtime", ] diff --git a/pallets/dynamic-evm-base-fee/src/lib.rs b/pallets/dynamic-evm-base-fee/src/lib.rs index 03cbf9c040..b4a6fa4397 100644 --- a/pallets/dynamic-evm-base-fee/src/lib.rs +++ b/pallets/dynamic-evm-base-fee/src/lib.rs @@ -16,14 +16,21 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . +//! TODO: Rustdoc!!! + #![cfg_attr(not(feature = "std"), no_std)] use frame_support::{traits::Get, weights::Weight}; use sp_core::U256; -use sp_runtime::{traits::UniqueSaturatedInto, Perquintill}; +use sp_runtime::{traits::UniqueSaturatedInto, FixedPointNumber, FixedU128, Perquintill}; pub use self::pallet::*; +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; @@ -45,7 +52,7 @@ pub mod pallet { /// Maximum value 'base fee per gas' can be adjusted to. This is a defensive measure to prevent the fee from being too high. type MaxBaseFeePerGas: Get; /// 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). - type AdjustmentFactor: Get; + type AdjustmentFactor: Get; /// The so-called `weight_factor` in the 'base fee per gas' formula. type WeightFactor: Get; /// Ratio limit on how much the 'base fee per gas' can change in-between two blocks. @@ -87,10 +94,16 @@ pub mod pallet { #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { - /// New `base fee per gas` value has been forced-set. + /// New `base fee per gas` value has been force-set. NewBaseFeePerGas { fee: U256 }, } + #[pallet::error] + pub enum Error { + /// Specified value is outside of the allowed range. + ValueOutOfBounds, + } + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(_: T::BlockNumber) -> Weight { @@ -115,6 +128,7 @@ pub mod pallet { // TODO: maybe add a DB entry to check until when should we apply max step adjustment? // Once 'equilibrium' is reached, it's safe to just follow the formula without limit updates. + // Or we could abuse the sudo for this. // Lower & upper limit between which the new base fee per gas should be clamped. let lower_limit = T::MinBaseFeePerGas::get().max(old_bfpg.saturating_sub(max_step)); @@ -122,7 +136,8 @@ pub mod pallet { // Calculate ideal new 'base_fee_per_gas' according to the formula let ideal_new_bfpg = T::AdjustmentFactor::get() - .saturating_mul(T::WeightFactor::get()) + // Weight factor should be multiplied first since it's a larger number, to avoid precision loss. + .saturating_mul_int(T::WeightFactor::get()) .saturating_mul(25) .saturating_div(98974); @@ -138,6 +153,11 @@ pub mod pallet { #[pallet::weight(10_000 + T::DbWeight::get().writes(1).ref_time())] pub fn set_base_fee_per_gas(origin: OriginFor, fee: U256) -> DispatchResult { ensure_root(origin)?; + ensure!( + fee >= T::MinBaseFeePerGas::get() && fee <= T::MaxBaseFeePerGas::get(), + Error::::ValueOutOfBounds + ); + BaseFeePerGas::::put(fee); Self::deposit_event(Event::NewBaseFeePerGas { fee }); Ok(()) diff --git a/pallets/dynamic-evm-base-fee/src/mock.rs b/pallets/dynamic-evm-base-fee/src/mock.rs new file mode 100644 index 0000000000..19b3c45288 --- /dev/null +++ b/pallets/dynamic-evm-base-fee/src/mock.rs @@ -0,0 +1,167 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg(test)] + +use super::*; +use crate as pallet_dynamic_evm_base_fee; + +use frame_support::{ + construct_runtime, parameter_types, + sp_io::TestExternalities, + storage, + traits::{ConstU128, ConstU32, ConstU64}, + weights::Weight, +}; +use parity_scale_codec::Encode; +use sp_core::H256; +use sp_runtime::{ + testing::Header, + traits::{BlakeTwo256, IdentityLookup, One}, + FixedU128, Perquintill, +}; + +pub(crate) type AccountId = u128; +pub(crate) type BlockNumber = u64; +pub(crate) type Balance = u128; + +parameter_types! { + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for TestRuntime { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for TestRuntime { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<2>; + type AccountStore = System; + type WeightInfo = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +impl pallet_timestamp::Config for TestRuntime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<3>; + type WeightInfo = (); +} + +parameter_types! { + pub WeightPerGas: Weight = Weight::from_parts(1, 0); + pub DefaultBaseFeePerGas: U256 = U256::from(1_500_000_000_000_u128); + pub MinBaseFeePerGas: U256 = U256::from(800_000_000_000_u128); + pub MaxBaseFeePerGas: U256 = U256::from(80_000_000_000_000_u128); + pub StepLimitRation: Perquintill = Perquintill::from_rational(30_u128, 1_000_000); +} + +impl pallet_dynamic_evm_base_fee::Config for TestRuntime { + type RuntimeEvent = RuntimeEvent; + type DefaultBaseFeePerGas = DefaultBaseFeePerGas; + type MinBaseFeePerGas = MinBaseFeePerGas; + type MaxBaseFeePerGas = MaxBaseFeePerGas; + type AdjustmentFactor = GetAdjustmentFactor; + type WeightFactor = ConstU128<30_000_000_000_000_000>; + type StepLimitRatio = StepLimitRation; +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +construct_runtime!( + pub struct TestRuntime + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Timestamp: pallet_timestamp, + Balances: pallet_balances, + DynamicEvmBaseFee: pallet_dynamic_evm_base_fee, + } +); + +const ADJUSTMENT_FACTOR: &[u8] = b":adj_factor_evm"; + +/// Helper method to set the adjustment factor used by the pallet. +pub fn set_adjustment_factor(factor: FixedU128) { + storage::unhashed::put_raw(&ADJUSTMENT_FACTOR, &factor.encode()); +} + +/// Helper function to get the adjustment factor used by the pallet. +/// In case it's unset, it returns 0. +pub fn get_adjustment_factor() -> FixedU128 { + storage::unhashed::get::(&ADJUSTMENT_FACTOR).unwrap_or_default() +} + +pub struct GetAdjustmentFactor; +impl Get for GetAdjustmentFactor { + fn get() -> FixedU128 { + get_adjustment_factor() + } +} + +pub struct ExtBuilder; +impl ExtBuilder { + pub fn build() -> TestExternalities { + let storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + set_adjustment_factor(FixedU128::one()); + System::set_block_number(1); + }); + ext + } +} diff --git a/pallets/dynamic-evm-base-fee/src/tests.rs b/pallets/dynamic-evm-base-fee/src/tests.rs new file mode 100644 index 0000000000..c44f60a3f2 --- /dev/null +++ b/pallets/dynamic-evm-base-fee/src/tests.rs @@ -0,0 +1,91 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +#![cfg(test)] + +use super::*; +use mock::*; + +use frame_support::{assert_noop, assert_ok}; +use sp_runtime::traits::BadOrigin; + +#[test] +fn set_base_fee_per_gas_works() { + ExtBuilder::build().execute_with(|| { + // sanity check + assert_eq!( + BaseFeePerGas::::get(), + ::DefaultBaseFeePerGas::get() + ); + + // Ensure we can change the bfpg value via root + for new_base_fee_per_gas in [ + ::MinBaseFeePerGas::get(), + ::MaxBaseFeePerGas::get(), + ] { + assert_ok!(DynamicEvmBaseFee::set_base_fee_per_gas( + RuntimeOrigin::root(), + new_base_fee_per_gas + )); + System::assert_last_event(mock::RuntimeEvent::DynamicEvmBaseFee( + Event::NewBaseFeePerGas { + fee: new_base_fee_per_gas, + }, + )); + assert_eq!(BaseFeePerGas::::get(), new_base_fee_per_gas); + } + }); +} + +#[test] +fn set_base_fee_per_gas_value_out_of_bounds_fails() { + ExtBuilder::build().execute_with(|| { + // Out of bound values + let too_small_base_fee_per_gas = + ::MinBaseFeePerGas::get() - 1; + let too_big_base_fee_per_gas = ::MaxBaseFeePerGas::get() + 1; + + assert_noop!( + DynamicEvmBaseFee::set_base_fee_per_gas( + RuntimeOrigin::root(), + too_small_base_fee_per_gas + ), + Error::::ValueOutOfBounds + ); + assert_noop!( + DynamicEvmBaseFee::set_base_fee_per_gas( + RuntimeOrigin::root(), + too_big_base_fee_per_gas + ), + Error::::ValueOutOfBounds + ); + }); +} + +#[test] +fn set_base_fee_per_gas_non_root_fails() { + ExtBuilder::build().execute_with(|| { + assert_noop!( + DynamicEvmBaseFee::set_base_fee_per_gas( + RuntimeOrigin::signed(1), + ::MinBaseFeePerGas::get() + ), + BadOrigin + ); + }); +}