diff --git a/Cargo.lock b/Cargo.lock index 6f0720a152..d458637d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3152,6 +3152,13 @@ dependencies = [ "sp-std", ] +[[package]] +name = "fp-rent" +version = "2.0.0-dev" +dependencies = [ + "sp-core", +] + [[package]] name = "fp-rpc" version = "3.0.0-dev" @@ -3580,6 +3587,7 @@ dependencies = [ "pallet-evm-precompile-modexp", "pallet-evm-precompile-sha3fips", "pallet-evm-precompile-simple", + "pallet-evm-rent", "pallet-grandpa", "pallet-hotfix-sufficients", "pallet-sudo", @@ -6143,6 +6151,7 @@ dependencies = [ "fp-consensus", "fp-ethereum", "fp-evm", + "fp-rent", "fp-rpc", "fp-self-contained", "fp-storage", @@ -6170,6 +6179,7 @@ dependencies = [ "evm", "fp-account", "fp-evm", + "fp-rent", "frame-benchmarking", "frame-support", "frame-system", @@ -6305,6 +6315,22 @@ dependencies = [ "sp-io", ] +[[package]] +name = "pallet-evm-rent" +version = "1.0.0" +dependencies = [ + "fp-rent", + "frame-support", + "frame-system", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-test-vector-support" version = "1.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 2e724b07f9..cda133ec08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "frame/evm/precompile/bls12377", "frame/evm/precompile/dispatch", "frame/evm/precompile/curve25519", + "frame/evm-rent", "client/api", "client/consensus", "client/rpc-core", @@ -41,6 +42,7 @@ members = [ "primitives/bifrost/rpc/evm-tracing-events", "primitives/bifrost/rpc/debug", "primitives/bifrost/rpc/txpool", + "primitives/rent", "runtime/evm-tracer", "template/node", "template/runtime", @@ -192,6 +194,7 @@ fp-evm = { version = "3.0.0-dev", path = "primitives/evm", default-features = fa fp-rpc = { version = "3.0.0-dev", path = "primitives/rpc", default-features = false } fp-self-contained = { version = "1.0.0-dev", path = "primitives/self-contained", default-features = false } fp-storage = { version = "2.0.0", path = "primitives/storage", default-features = false } +fp-rent = { version = "2.0.0-dev", path = "primitives/rent", default-features = false } # Frontier BIFROST Primitive fp-ext = { version = "0.1.0", path = "primitives/bifrost/ext", default-features = false } fp-rpc-debug = { version = "0.1.0", path = "primitives/bifrost/rpc/debug", default-features = false } @@ -211,6 +214,7 @@ pallet-evm-precompile-sha3fips = { version = "2.0.0-dev", path = "frame/evm/prec pallet-evm-precompile-simple = { version = "2.0.0-dev", path = "frame/evm/precompile/simple", default-features = false } pallet-evm-test-vector-support = { version = "1.0.0-dev", path = "frame/evm/test-vector-support" } pallet-hotfix-sufficients = { version = "1.0.0", path = "frame/hotfix-sufficients", default-features = false } +pallet-evm-rent = { version = "1.0.0", path = "frame/evm-rent", default-features = false} # Frontier Template frontier-template-runtime = { path = "template/runtime", default-features = false } # Frontier Utility diff --git a/client/rpc/src/eth/format.rs b/client/rpc/src/eth/format.rs index 70b2439b45..3e1cf28ecb 100644 --- a/client/rpc/src/eth/format.rs +++ b/client/rpc/src/eth/format.rs @@ -48,6 +48,7 @@ impl Geth { "max priority fee per gas higher than max fee per gas".into() } VError::InvalidFeeInput => "invalid fee input".into(), + VError::InsufficientRent => "insufficient balance for rent + gas * price + value".into(), _ => "transaction validation error".into(), }, _ => "unknown error".into(), diff --git a/frame/ethereum/Cargo.toml b/frame/ethereum/Cargo.toml index 10bc7620da..7494f3abf0 100644 --- a/frame/ethereum/Cargo.toml +++ b/frame/ethereum/Cargo.toml @@ -29,6 +29,7 @@ fp-evm = { workspace = true } fp-rpc = { workspace = true } fp-storage = { workspace = true } pallet-evm = { workspace = true } +fp-rent = { workspace = true } [dev-dependencies] hex = { workspace = true } @@ -64,6 +65,7 @@ std = [ "fp-self-contained/std", "fp-storage/std", "pallet-evm/std", + "fp-rent/std", ] runtime-benchmarks = [ "frame-support/runtime-benchmarks", diff --git a/frame/ethereum/src/lib.rs b/frame/ethereum/src/lib.rs index fa7d0f2eb6..cb91974606 100644 --- a/frame/ethereum/src/lib.rs +++ b/frame/ethereum/src/lib.rs @@ -64,6 +64,7 @@ use fp_evm::{ CallOrCreateInfo, CheckEvmTransaction, CheckEvmTransactionConfig, TransactionValidationError, }; pub use fp_rpc::TransactionStatus; +pub use fp_rent::EvmRentCalculator; use fp_storage::{EthereumStorageSchema, PALLET_ETHEREUM_SCHEMA}; use pallet_evm::{BlockHashMapping, FeeCalculator, GasWeightMapping, Runner}; @@ -489,6 +490,43 @@ impl Pallet { } } + fn calculate_max_transaction_fee( + transaction_data: &TransactionData, + ) -> Result { + match ( + transaction_data.gas_price, + transaction_data.max_fee_per_gas, + transaction_data.max_priority_fee_per_gas, + ) { + // Legacy or EIP-2930 transaction + (Some(gas_price), None, None) => { + Ok(gas_price.saturating_mul(transaction_data.gas_limit)) + }, + + // EIP-1559 transaction without tip + (None, Some(max_fee_per_gas), None) => { + Ok(max_fee_per_gas.saturating_mul(transaction_data.gas_limit)) + }, + + // EIP-1559 with tip + (None, Some(max_fee_per_gas), Some(max_priority_fee_per_gas)) => { + if max_priority_fee_per_gas > max_fee_per_gas { + return Err(TransactionValidityError::Invalid( + InvalidTransaction::Custom(TransactionValidationError::PriorityFeeTooHigh as u8) + )); + } + Ok(max_fee_per_gas.saturating_mul(transaction_data.gas_limit)) + } + + _ => { + // must be transactional tx + Err(TransactionValidityError::Invalid( + InvalidTransaction::Custom(TransactionValidationError::InvalidFeeInput as u8) + )) + } + } + } + // Controls that must be performed by the pool. // The controls common with the State Transition Function (STF) are in // the function `validate_transaction_common`. @@ -520,6 +558,22 @@ impl Pallet { .and_then(|v| v.with_balance_for(&who)) .map_err(|e| e.0)?; + let rent_amount = T::EvmRentCalculator::estimate_rent(origin).0; + if rent_amount > 0 { + let fee = Self::calculate_max_transaction_fee(&transaction_data)?; + + let total_payment = transaction_data.value.saturating_add(fee); + let total_with_rent = total_payment.saturating_add(U256::from(rent_amount)); + + if who.balance < total_with_rent { + return Err( + TransactionValidityError::Invalid( + InvalidTransaction::Custom(TransactionValidationError::InsufficientRent as u8) + ) + ); + } + } + // EIP-3607: https://eips.ethereum.org/EIPS/eip-3607 // Do not allow transactions for which `tx.sender` has any code deployed. // @@ -948,7 +1002,7 @@ impl Pallet { chain_id: T::ChainId::get(), is_transactional: true, }, - transaction_data.into(), + transaction_data.clone().into(), weight_limit, proof_size_base_cost, ) @@ -958,6 +1012,22 @@ impl Pallet { .and_then(|v| v.with_balance_for(&who)) .map_err(|e| TransactionValidityError::Invalid(e.0))?; + let rent_amount = T::EvmRentCalculator::estimate_rent(origin).0; + if rent_amount > 0 { + let fee = Self::calculate_max_transaction_fee(&transaction_data)?; + + let total_payment = transaction_data.value.saturating_add(fee); + let total_with_rent = total_payment.saturating_add(U256::from(rent_amount)); + + if who.balance < total_with_rent { + return Err( + TransactionValidityError::Invalid( + InvalidTransaction::Custom(TransactionValidationError::InsufficientRent as u8) + ) + ); + } + } + Ok(()) } @@ -1094,6 +1164,9 @@ impl From for InvalidTransactionWrapper { TransactionValidationError::GasPriceTooLow => InvalidTransactionWrapper( InvalidTransaction::Custom(TransactionValidationError::GasPriceTooLow as u8), ), + TransactionValidationError::InsufficientRent => InvalidTransactionWrapper( + InvalidTransaction::Custom(TransactionValidationError::InsufficientRent as u8), + ), TransactionValidationError::UnknownError => InvalidTransactionWrapper( InvalidTransaction::Custom(TransactionValidationError::UnknownError as u8), ), diff --git a/frame/ethereum/src/mock.rs b/frame/ethereum/src/mock.rs index c55d497a54..4f541c84f0 100644 --- a/frame/ethereum/src/mock.rs +++ b/frame/ethereum/src/mock.rs @@ -177,6 +177,7 @@ impl pallet_evm::Config for Test { type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; type Timestamp = Timestamp; + type EvmRentCalculator = (); type WeightInfo = (); } diff --git a/frame/evm-rent/Cargo.toml b/frame/evm-rent/Cargo.toml new file mode 100644 index 0000000000..12141c4a19 --- /dev/null +++ b/frame/evm-rent/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "pallet-evm-rent" +version = "1.0.0" +authors = ["Parity Technologies "] +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.dev" +repository = "https://github.com/paritytech/frontier/" +description = "EVM account rent calculation and processing" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +scale-codec = { package = "parity-scale-codec", workspace = true } +scale-info = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-timestamp = { workspace = true } + +fp-rent = { version = "2.0.0-dev", default-features = false, path = "../../primitives/rent" } + +[dev-dependencies] +sp-io = { workspace = true } +sp-std = { workspace = true } + +[features] +default = ["std"] +std = [ + "scale-codec/std", + "scale-info/std", + "sp-core/std", + "sp-runtime/std", + "frame-support/std", + "frame-system/std", + + "pallet-timestamp/std", + + "fp-rent/std", +] diff --git a/frame/evm-rent/src/lib.rs b/frame/evm-rent/src/lib.rs new file mode 100644 index 0000000000..23b08796e8 --- /dev/null +++ b/frame/evm-rent/src/lib.rs @@ -0,0 +1,243 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +pub use pallet::*; + +use fp_rent::EvmRentCalculator; +use frame_support::pallet_prelude::*; +use sp_core::H160; +use sp_runtime::traits::SaturatedConversion; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{dispatch::DispatchResult, traits::EnsureOrigin}; + use frame_system::{ensure_root, pallet_prelude::OriginFor}; + + // ========================================== + // 1. Configuration & Constants + // ========================================== + + // 2026-01-01 00:00:00 UTC + pub const DEFAULT_RENT_START_TIME: u64 = 1_767_225_600_000; + // 2025-12-10 00:00:00 UTC + pub const MIN_RENT_START_TIME: u64 = 1_765_324_800_000; + // 1 satoshi + pub const SATOSHI: u128 = 10_000_000_000; + // 10 satoshis = 100 Gwei (10 * 10^10) + pub const DEFAULT_DAILY_RENT: u128 = 10 * SATOSHI; + // Milliseconds per day + pub const MILLISECONDS_PER_DAY: u64 = 86_400_000; + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_timestamp::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// A majority of the council can execute some transactions. + type CouncilOrigin: EnsureOrigin; + } + + #[pallet::pallet] + pub struct Pallet(_); + + // ========================================== + // 2. Storage Layer + // ========================================== + + /// System rent activation timestamp (milliseconds) + #[pallet::storage] + #[pallet::getter(fn active_timestamp)] + pub type ActiveTimestamp = StorageValue<_, u64, ValueQuery, DefaultActiveTimestamp>; + + /// Daily rent amount (default 100 Gwei) + #[pallet::storage] + #[pallet::getter(fn daily_rent)] + pub type DailyRent = StorageValue<_, u128, ValueQuery, DefaultDailyRent>; + + /// Account rent status structure + #[derive( + Encode, + Decode, + Clone, + PartialEq, + Eq, + RuntimeDebug, + TypeInfo, + MaxEncodedLen + )] + pub struct RentStatus { + pub last_rent_paid_time: u64, // Last settlement time + pub accumulated_rent: u128, // Total accumulated rent (statistical purpose) + } + + /// Core storage: H160 -> Rent status + #[pallet::storage] + #[pallet::getter(fn account_rent_status)] + pub type AccountRentMap = StorageMap<_, Twox64Concat, H160, RentStatus, OptionQuery>; + + // Default value implementations + pub struct DefaultActiveTimestamp; + impl Get for DefaultActiveTimestamp { + fn get() -> u64 { + DEFAULT_RENT_START_TIME + } + } + + pub struct DefaultDailyRent; + impl Get for DefaultDailyRent { + fn get() -> u128 { + DEFAULT_DAILY_RENT + } + } + + // ========================================== + // 3. Events + // ========================================== + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Rent charged: [Account, DaysPaid, Amount] + RentChargedToBurn(H160, u64, u128), + } + + #[pallet::error] + pub enum Error { + /// Invalid timestamp + InvalidTimestamp, + /// Invalid daily rent + InvalidDailyRent, + } + + // ========================================== + // 4. Dispatchable Functions + // ========================================== + #[pallet::call] + impl Pallet { + #[pallet::call_index(0)] + #[pallet::weight({0})] + pub fn set_active_timestamp(origin: OriginFor, timestamp: u64) -> DispatchResult { + ::CouncilOrigin::try_origin(origin) + .map(|_| ()) + .or_else(ensure_root)?; + + ensure!( + timestamp >= MIN_RENT_START_TIME, + Error::::InvalidTimestamp + ); + + >::put(timestamp); + + Ok(()) + } + + #[pallet::call_index(1)] + #[pallet::weight({0})] + pub fn set_daily_rent(origin: OriginFor, rent: u128) -> DispatchResult { + ::CouncilOrigin::try_origin(origin) + .map(|_| ()) + .or_else(ensure_root)?; + + ensure!(rent % SATOSHI == 0, Error::::InvalidDailyRent); + + >::put(rent); + + Ok(()) + } + } + + // ========================================== + // 5. Implementation + // ========================================== + + impl EvmRentCalculator for Pallet { + /// Calculate and update state, return amount to charge + /// This method should be called by EVM Adapter (OnChargeEVMTransaction) + /// return rent value by Wei + fn process_rent(who: H160) -> u128 { + let now = >::now().saturated_into::(); + let start_time: u64 = Self::active_timestamp(); + + // 1. if zero address(used for eth_call), no charge + if who == H160::default() { + return 0; + } + + // 2. If current time is before rent start time, no charge + if now < start_time { + return 0; + } + + // 3. Get user status + let mut status = Self::account_rent_status(who).unwrap_or(RentStatus { + last_rent_paid_time: start_time, // Default to system rent start time + accumulated_rent: 0, + }); + + // Defensive check: Prevent time regression + if now <= status.last_rent_paid_time { + return 0; + } + + // 4. Calculate elapsed time and days (floor) + let elapsed_ms = now - status.last_rent_paid_time; + let days_to_pay = elapsed_ms / MILLISECONDS_PER_DAY; + + // 5. Less than 1 day, no charge, no state update + if days_to_pay == 0 { + return 0; + } + + // 6. Calculate amount + let daily_rent = Self::daily_rent(); + let rent_amount = (days_to_pay as u128).saturating_mul(daily_rent); + + // 7. Update state + // Key: Only advance paid days, keep remainder + let time_paid_for = days_to_pay * MILLISECONDS_PER_DAY; + status.last_rent_paid_time += time_paid_for; + status.accumulated_rent = status.accumulated_rent.saturating_add(rent_amount); + + // 8. Write to storage + >::insert(who, status); + + // 9. Emit event + Self::deposit_event(Event::RentChargedToBurn(who, days_to_pay, rent_amount)); + + rent_amount + } + + /// Corresponds to Solidity: estimateRent(address account)(uint256,uint64) + /// return rent value by Wei + fn estimate_rent(who: H160) -> (u128, u64) { + let now = >::now().saturated_into::(); + let start_time: u64 = Self::active_timestamp(); + + if now < start_time { + return (0, 0); + } + + let status = Self::account_rent_status(who).unwrap_or(RentStatus { + last_rent_paid_time: start_time, + accumulated_rent: 0, + }); + + if now <= status.last_rent_paid_time { + return (0, 0); + } + + let elapsed_ms = now - status.last_rent_paid_time; + let days = elapsed_ms / MILLISECONDS_PER_DAY; + + if days == 0 { + return (0, 0); + } + + let rent_amount = (days as u128).saturating_mul(Self::daily_rent()); + + (rent_amount, days) + } + } +} + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; diff --git a/frame/evm-rent/src/mock.rs b/frame/evm-rent/src/mock.rs new file mode 100644 index 0000000000..2fa9a637eb --- /dev/null +++ b/frame/evm-rent/src/mock.rs @@ -0,0 +1,75 @@ +use crate::pallet as pallet_rent; +use frame_support::{ + pallet_prelude::{ConstU32, Weight}, + parameter_types, + traits::ConstU64, +}; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} +impl frame_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = frame_system::mocking::MockBlock; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<1>; + type WeightInfo = (); +} + +impl pallet_rent::Config for Test { + type RuntimeEvent = RuntimeEvent; + type CouncilOrigin = frame_system::EnsureRoot; +} + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Timestamp: pallet_timestamp::{Pallet, Call, Storage}, + Rent: pallet_rent::{Pallet, Call, Storage, Event}, + } +); + +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext +} + +pub fn set_time(ms: u64) { + Timestamp::set_timestamp(ms); +} diff --git a/frame/evm-rent/src/tests.rs b/frame/evm-rent/src/tests.rs new file mode 100644 index 0000000000..ba2be8c5c5 --- /dev/null +++ b/frame/evm-rent/src/tests.rs @@ -0,0 +1,117 @@ +use super::mock::*; +use crate::pallet::*; +use fp_rent::EvmRentCalculator; +use frame_support::{assert_noop, assert_ok}; +use sp_core::H160; + +#[test] +fn set_active_timestamp_works() { + new_test_ext().execute_with(|| { + let new_time = MIN_RENT_START_TIME + 1000; + assert_noop!( + Rent::set_active_timestamp(RuntimeOrigin::signed(1), new_time), + sp_runtime::DispatchError::BadOrigin + ); + assert_ok!(Rent::set_active_timestamp(RuntimeOrigin::root(), new_time)); + assert_eq!(Rent::active_timestamp(), new_time); + assert_noop!( + Rent::set_active_timestamp(RuntimeOrigin::root(), MIN_RENT_START_TIME - 1), + Error::::InvalidTimestamp + ); + }); +} + +#[test] +fn set_daily_rent_works() { + new_test_ext().execute_with(|| { + let valid_rent = 2 * SATOSHI; + let invalid_rent = SATOSHI - 1; + + assert_ok!(Rent::set_daily_rent(RuntimeOrigin::root(), valid_rent)); + assert_eq!(Rent::daily_rent(), valid_rent); + + assert_noop!( + Rent::set_daily_rent(RuntimeOrigin::root(), invalid_rent), + Error::::InvalidDailyRent + ); + }); +} + +#[test] +fn process_rent_logic_flow() { + new_test_ext().execute_with(|| { + let alice = H160::from_low_u64_be(1); + let start_time = DEFAULT_RENT_START_TIME; // 1_767_225_600_000 + + // 1. Before start time, rent should be 0 + set_time(start_time - 1000); + assert_eq!(Rent::process_rent(alice), 0); + + // 2. Exactly at start time, but less than 1 day + set_time(start_time); + assert_eq!(Rent::process_rent(alice), 0); + + // 3. After 1.5 days, charge 1 day rent, update last settlement to start_time + 1 day + set_time(start_time + (MILLISECONDS_PER_DAY * 3 / 2)); + let expected_rent = DEFAULT_DAILY_RENT; + assert_eq!(Rent::process_rent(alice), expected_rent); + + // Verify state update + let status = Rent::account_rent_status(alice).unwrap(); + assert_eq!( + status.last_rent_paid_time, + start_time + MILLISECONDS_PER_DAY + ); + assert_eq!(status.accumulated_rent, expected_rent); + + // 4. Another 0.8 days (total 2.3 days), 1 day since last settlement, charge and update to start_time + 2 days + set_time(start_time + (MILLISECONDS_PER_DAY * 23 / 10)); + assert_eq!(Rent::process_rent(alice), DEFAULT_DAILY_RENT); + + let status = Rent::account_rent_status(alice).unwrap(); + assert_eq!( + status.last_rent_paid_time, + start_time + 2 * MILLISECONDS_PER_DAY + ); + assert_eq!(status.accumulated_rent, expected_rent * 2); + + // 5. Another 1.2 days (total 3.5 days), 1.5 days since last settlement + // Charge 1 day, total accumulated should be 3 days + set_time(start_time + (MILLISECONDS_PER_DAY * 35 / 10)); + assert_eq!(Rent::process_rent(alice), DEFAULT_DAILY_RENT); + + let status = Rent::account_rent_status(alice).unwrap(); + assert_eq!( + status.last_rent_paid_time, + start_time + 3 * MILLISECONDS_PER_DAY + ); + assert_eq!(status.accumulated_rent, DEFAULT_DAILY_RENT * 3); + }); +} + +#[test] +fn estimate_rent_matches_process() { + new_test_ext().execute_with(|| { + let bob = H160::from_low_u64_be(2); + let start_time = DEFAULT_RENT_START_TIME; + + set_time(start_time + (MILLISECONDS_PER_DAY * 5)); + + let (est_amount, est_days) = Rent::estimate_rent(bob); + assert_eq!(est_days, 5); + assert_eq!(est_amount, DEFAULT_DAILY_RENT * 5); + + // Execute actual rent charging + let actual_amount = Rent::process_rent(bob); + assert_eq!(actual_amount, est_amount); + }); +} + +#[test] +fn zero_address_is_exempt() { + new_test_ext().execute_with(|| { + let zero_addr = H160::default(); + set_time(DEFAULT_RENT_START_TIME + (MILLISECONDS_PER_DAY * 10)); + assert_eq!(Rent::process_rent(zero_addr), 0); + }); +} diff --git a/frame/evm/Cargo.toml b/frame/evm/Cargo.toml index 74241e916c..5999b08cc1 100644 --- a/frame/evm/Cargo.toml +++ b/frame/evm/Cargo.toml @@ -33,6 +33,7 @@ sp-std = { workspace = true } # Frontier fp-account = { workspace = true } fp-evm = { workspace = true, features = ["serde"] } +fp-rent = { workspace = true } [dev-dependencies] hex = { workspace = true } @@ -63,6 +64,7 @@ std = [ # Frontier "fp-account/std", "fp-evm/std", + "fp-rent/std", ] runtime-benchmarks = [ "hex", diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index 8f563fe720..98f02d9be4 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -103,6 +103,8 @@ pub use fp_evm::{ PrecompileOutput, PrecompileResult, PrecompileSet, TransactionValidationError, Vicinity, }; +pub use fp_rent::EvmRentCalculator; + pub use self::{ pallet::*, runner::{Runner, RunnerError}, @@ -175,6 +177,9 @@ pub mod pallet { /// Get the timestamp for the current block. type Timestamp: Time; + /// Charge(Burn) evm rent + type EvmRentCalculator: EvmRentCalculator; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; @@ -500,6 +505,8 @@ pub mod pallet { Reentrancy, /// EIP-3607, TransactionMustComeFromEOA, + /// Insufficient Rent + Fee + Value + InsufficientRent, /// Undefined error. Undefined, } @@ -517,6 +524,7 @@ pub mod pallet { TransactionValidationError::InvalidFeeInput => Error::::GasPriceTooLow, TransactionValidationError::InvalidChainId => Error::::InvalidChainId, TransactionValidationError::InvalidSignature => Error::::InvalidSignature, + TransactionValidationError::InsufficientRent => Error::InsufficientRent, TransactionValidationError::UnknownError => Error::::Undefined, } } @@ -986,10 +994,46 @@ impl OnChargeEVMTransaction for EVMCurrencyAdapter type LiquidityInfo = Option>; fn withdraw_fee(who: &H160, fee: U256) -> Result> { + // ==================================================== + // Handle EVM Rent (burn logic) + + // 1. H160 -> T::AccountId + let account_id = T::AddressMapping::into_account_id(*who); + + // 2. call process_rent get rent to burn and update state + let rent_to_burn_u128 = T::EvmRentCalculator::process_rent(*who); + if rent_to_burn_u128 > 0 { + let rent_balance: C::Balance = rent_to_burn_u128.unique_saturated_into(); + + // Withdraw + // WithdrawReasons::FEE + // ExistenceRequirement::AllowDeath + match C::withdraw( + &account_id, + rent_balance, + WithdrawReasons::FEE, + ExistenceRequirement::AllowDeath + ) { + Ok(rent_imbalance) => { + // !!! KeyPoint: Burn !!! + // rent_imbalance is NegativeImbalance + // We don't call resolve_creating or deposit_into_existing. + // We do nothing and just let it drop here. + // Substrate automatically reduces TotalIssuance when rent_imbalance goes out of scope. + drop(rent_imbalance); + }, + Err(_) => { + // The transaction fails if the balance is insufficient to pay the rent. + return Err(Error::::BalanceLow); + } + } + } + // ==================================================== + if fee.is_zero() { return Ok(None); } - let account_id = T::AddressMapping::into_account_id(*who); + let imbalance = C::withdraw( &account_id, fee.unique_saturated_into(), diff --git a/frame/evm/src/mock.rs b/frame/evm/src/mock.rs index c6529bd9b1..a2c136e1e5 100644 --- a/frame/evm/src/mock.rs +++ b/frame/evm/src/mock.rs @@ -155,6 +155,7 @@ impl crate::Config for Test { type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; type Timestamp = Timestamp; + type EvmRentCalculator = (); type WeightInfo = (); } diff --git a/primitives/evm/src/validation.rs b/primitives/evm/src/validation.rs index 405f09a4b3..5b8c2f9696 100644 --- a/primitives/evm/src/validation.rs +++ b/primitives/evm/src/validation.rs @@ -78,6 +78,8 @@ pub enum TransactionValidationError { InvalidChainId, /// The transaction signature is invalid InvalidSignature, + /// Insufficient rent for the transaction + InsufficientRent, /// Unknown error #[num_enum(default)] UnknownError, @@ -260,6 +262,7 @@ mod tests { InvalidFeeInput, InvalidChainId, InvalidSignature, + InsufficientRent, UnknownError, } @@ -278,6 +281,7 @@ mod tests { TransactionValidationError::InvalidFeeInput => TestError::InvalidFeeInput, TransactionValidationError::InvalidChainId => TestError::InvalidChainId, TransactionValidationError::InvalidSignature => TestError::InvalidSignature, + TransactionValidationError::InsufficientRent => TestError::InsufficientRent, TransactionValidationError::UnknownError => TestError::UnknownError, } } diff --git a/primitives/rent/Cargo.toml b/primitives/rent/Cargo.toml new file mode 100644 index 0000000000..a614e8a72a --- /dev/null +++ b/primitives/rent/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fp-rent" +version = "2.0.0-dev" +authors = ["Parity Technologies "] +description = "Primitives for EVM rent" +edition = "2021" +license = "Apache-2.0" +homepage = "https://substrate.io" +repository = "https://github.com/paritytech/frontier/" + +[dependencies] +sp-core = { workspace = true } + +[features] +default = ["std"] +std = [ + "sp-core/std" +] diff --git a/primitives/rent/src/lib.rs b/primitives/rent/src/lib.rs new file mode 100644 index 0000000000..653d12b5de --- /dev/null +++ b/primitives/rent/src/lib.rs @@ -0,0 +1,25 @@ +//! # EVM Rent +#![cfg_attr(not(feature = "std"), no_std)] + +use sp_core::H160; + +pub trait EvmRentCalculator { + /// Calculate and charge (burn) the outstanding rent for an account, + /// then update its status map. + /// return rent value by Wei + fn process_rent(who: H160) -> u128; + /// Estimate the amount of rent to be burned and the corresponding + /// number of rented days for an account. + /// return rent value by Wei + fn estimate_rent(who: H160) -> (u128, u64); +} + +impl EvmRentCalculator for () { + fn process_rent(_who: H160) -> u128 { + 0u128 + } + + fn estimate_rent(_who: H160) -> (u128, u64) { + (0u128, 0u64) + } +} diff --git a/template/runtime/Cargo.toml b/template/runtime/Cargo.toml index 9dee37bd4b..297f410e7d 100644 --- a/template/runtime/Cargo.toml +++ b/template/runtime/Cargo.toml @@ -58,6 +58,7 @@ pallet-evm-precompile-modexp = { workspace = true } pallet-evm-precompile-sha3fips = { workspace = true } pallet-evm-precompile-simple = { workspace = true } pallet-hotfix-sufficients = { workspace = true } +pallet-evm-rent = { workspace = true } fp-rpc-debug = { workspace = true } fp-rpc-txpool = { workspace = true } @@ -122,6 +123,7 @@ std = [ "pallet-evm-precompile-sha3fips/std", "pallet-evm-precompile-simple/std", "pallet-hotfix-sufficients/std", + "pallet-evm-rent/std", ] runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks", diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index 8afe937021..364d7b07e9 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -38,6 +38,7 @@ use frame_support::{ traits::{ConstBool, ConstU32, ConstU8, FindAuthor, OnFinalize, OnTimestampSet}, weights::{constants::WEIGHT_REF_TIME_PER_MILLIS, IdentityFee, Weight}, }; +use frame_system::EnsureRoot; use pallet_grandpa::{ fg_primitives, AuthorityId as GrandpaId, AuthorityList as GrandpaAuthorityList, }; @@ -349,6 +350,7 @@ impl pallet_evm::Config for Runtime { type GasLimitPovSizeRatio = GasLimitPovSizeRatio; type SuicideQuickClearLimit = SuicideQuickClearLimit; type Timestamp = Timestamp; + type EvmRentCalculator = EvmRent; type WeightInfo = pallet_evm::weights::SubstrateWeight; } @@ -401,6 +403,11 @@ impl pallet_hotfix_sufficients::Config for Runtime { type WeightInfo = pallet_hotfix_sufficients::weights::SubstrateWeight; } +impl pallet_evm_rent::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type CouncilOrigin = EnsureRoot; +} + // Create the runtime by composing the FRAME pallets that were previously configured. construct_runtime!( pub enum Runtime { @@ -417,6 +424,7 @@ construct_runtime!( DynamicFee: pallet_dynamic_fee, BaseFee: pallet_base_fee, HotfixSufficients: pallet_hotfix_sufficients, + EvmRent: pallet_evm_rent, } );