diff --git a/runtime/common/src/deal_with_fees.rs b/runtime/common/src/deal_with_fees.rs new file mode 100644 index 0000000000..dd65f9c2d6 --- /dev/null +++ b/runtime/common/src/deal_with_fees.rs @@ -0,0 +1,114 @@ +// Copyright 2024 Moonbeam foundation +// This file is part of Moonbeam. + +// Moonbeam 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. + +// Moonbeam 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 Moonbeam. If not, see . + +use frame_support::__private::Get; +use frame_support::pallet_prelude::TypedGet; +use frame_support::traits::fungible::Credit; +use frame_support::traits::tokens::imbalance::ResolveTo; +use frame_support::traits::Imbalance; +use frame_support::traits::OnUnbalanced; +use pallet_treasury::TreasuryAccountId; +use sp_runtime::Perbill; + +/// Deal with substrate based fees and tip. This should be used with pallet_transaction_payment. +pub struct DealWithSubstrateFeesAndTip( + sp_std::marker::PhantomData<(R, FeesTreasuryProportion)>, +); +impl DealWithSubstrateFeesAndTip +where + R: pallet_balances::Config + pallet_treasury::Config + pallet_author_inherent::Config, + pallet_author_inherent::Pallet: Get, + FeesTreasuryProportion: Get, +{ + fn deal_with_fees(amount: Credit>) { + // Balances pallet automatically burns dropped Credits by decreasing + // total_supply accordingly + let treasury_proportion = FeesTreasuryProportion::get(); + let treasury_part = treasury_proportion.deconstruct(); + let burn_part = Perbill::one().deconstruct() - treasury_part; + let (_, to_treasury) = amount.ration(burn_part, treasury_part); + ResolveTo::, pallet_balances::Pallet>::on_unbalanced(to_treasury); + } + + fn deal_with_tip(amount: Credit>) { + ResolveTo::, pallet_balances::Pallet>::on_unbalanced(amount); + } +} + +impl OnUnbalanced>> + for DealWithSubstrateFeesAndTip +where + R: pallet_balances::Config + pallet_treasury::Config + pallet_author_inherent::Config, + pallet_author_inherent::Pallet: Get, + FeesTreasuryProportion: Get, +{ + fn on_unbalanceds( + mut fees_then_tips: impl Iterator>>, + ) { + if let Some(fees) = fees_then_tips.next() { + Self::deal_with_fees(fees); + if let Some(tip) = fees_then_tips.next() { + Self::deal_with_tip(tip); + } + } + } +} + +/// Deal with ethereum based fees. To handle tips/priority fees, use DealWithEthereumPriorityFees. +pub struct DealWithEthereumBaseFees( + sp_std::marker::PhantomData<(R, FeesTreasuryProportion)>, +); +impl OnUnbalanced>> + for DealWithEthereumBaseFees +where + R: pallet_balances::Config + pallet_treasury::Config, + FeesTreasuryProportion: Get, +{ + fn on_nonzero_unbalanced(amount: Credit>) { + // Balances pallet automatically burns dropped Credits by decreasing + // total_supply accordingly + let treasury_proportion = FeesTreasuryProportion::get(); + let treasury_part = treasury_proportion.deconstruct(); + let burn_part = Perbill::one().deconstruct() - treasury_part; + let (_, to_treasury) = amount.ration(burn_part, treasury_part); + ResolveTo::, pallet_balances::Pallet>::on_unbalanced(to_treasury); + } +} + +pub struct BlockAuthorAccountId(sp_std::marker::PhantomData); +impl TypedGet for BlockAuthorAccountId +where + R: frame_system::Config + pallet_author_inherent::Config, + pallet_author_inherent::Pallet: Get, +{ + type Type = R::AccountId; + fn get() -> Self::Type { + as Get>::get() + } +} + +/// Deal with ethereum based priority fees/tips. See DealWithEthereumBaseFees for base fees. +pub struct DealWithEthereumPriorityFees(sp_std::marker::PhantomData); +impl OnUnbalanced>> + for DealWithEthereumPriorityFees +where + R: pallet_balances::Config + pallet_author_inherent::Config, + pallet_author_inherent::Pallet: Get, +{ + fn on_nonzero_unbalanced(amount: Credit>) { + ResolveTo::, pallet_balances::Pallet>::on_unbalanced(amount); + } +} diff --git a/runtime/common/src/impl_on_charge_evm_transaction.rs b/runtime/common/src/impl_on_charge_evm_transaction.rs index 0fe3b9c026..1cb816c73c 100644 --- a/runtime/common/src/impl_on_charge_evm_transaction.rs +++ b/runtime/common/src/impl_on_charge_evm_transaction.rs @@ -22,13 +22,17 @@ macro_rules! impl_on_charge_evm_transaction { type BalanceFor = <::Currency as Inspect>>::Balance; - pub struct OnChargeEVMTransaction(sp_std::marker::PhantomData); + pub struct OnChargeEVMTransaction( + sp_std::marker::PhantomData<(BaseFeesOU, PriorityFeesOU)> + ); - impl OnChargeEVMTransactionT for OnChargeEVMTransaction + impl OnChargeEVMTransactionT + for OnChargeEVMTransaction where T: pallet_evm::Config, T::Currency: Balanced>, - OU: OnUnbalanced, T::Currency>>, + BaseFeesOU: OnUnbalanced, T::Currency>>, + PriorityFeesOU: OnUnbalanced, T::Currency>>, U256: UniqueSaturatedInto<>>::Balance>, T::AddressMapping: pallet_evm::AddressMapping, { @@ -48,14 +52,14 @@ macro_rules! impl_on_charge_evm_transaction { base_fee: U256, already_withdrawn: Self::LiquidityInfo, ) -> Self::LiquidityInfo { - ::Currency, OU> as OnChargeEVMTransactionT< + ::Currency, BaseFeesOU> as OnChargeEVMTransactionT< T, >>::correct_and_deposit_fee(who, corrected_fee, base_fee, already_withdrawn) } fn pay_priority_fee(tip: Self::LiquidityInfo) { if let Some(tip) = tip { - OU::on_unbalanced(tip); + PriorityFeesOU::on_unbalanced(tip); } } } diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 735a565ce3..1df85e1e52 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -19,6 +19,7 @@ mod apis; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; +pub mod deal_with_fees; mod impl_moonbeam_xcm_call; mod impl_moonbeam_xcm_call_tracing; mod impl_on_charge_evm_transaction; diff --git a/runtime/moonbase/src/lib.rs b/runtime/moonbase/src/lib.rs index 073da4d764..ab9a1db6c8 100644 --- a/runtime/moonbase/src/lib.rs +++ b/runtime/moonbase/src/lib.rs @@ -64,10 +64,9 @@ use frame_support::{ parameter_types, traits::{ fungible::{Balanced, Credit, HoldConsideration, Inspect}, - tokens::imbalance::ResolveTo, tokens::{PayFromAccount, UnityAssetBalanceConversion}, ConstBool, ConstU128, ConstU16, ConstU32, ConstU64, ConstU8, Contains, EitherOfDiverse, - EqualPrivilegeOnly, FindAuthor, Imbalance, InstanceFilter, LinearStoragePrice, OnFinalize, + EqualPrivilegeOnly, FindAuthor, InstanceFilter, LinearStoragePrice, OnFinalize, OnUnbalanced, }, weights::{ @@ -90,7 +89,6 @@ use pallet_evm::{ OnChargeEVMTransaction as OnChargeEVMTransactionT, Runner, }; use pallet_transaction_payment::{FungibleAdapter, Multiplier, TargetedFeeAdjustment}; -use pallet_treasury::TreasuryAccountId; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use runtime_params::*; use scale_info::TypeInfo; @@ -338,52 +336,6 @@ impl pallet_balances::Config for Runtime { type WeightInfo = moonbase_weights::pallet_balances::WeightInfo; } -pub struct DealWithFees(sp_std::marker::PhantomData); -impl OnUnbalanced>> for DealWithFees -where - R: pallet_balances::Config + pallet_treasury::Config, -{ - // this seems to be called for substrate-based transactions - fn on_unbalanceds( - mut fees_then_tips: impl Iterator>>, - ) { - if let Some(fees) = fees_then_tips.next() { - let treasury_proportion = - runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion::get(); - let treasury_part = treasury_proportion.deconstruct(); - let burn_part = Perbill::one().deconstruct() - treasury_part; - let (_, to_treasury) = fees.ration(burn_part, treasury_part); - // Balances pallet automatically burns dropped Credits by decreasing - // total_supply accordingly - ResolveTo::, pallet_balances::Pallet>::on_unbalanced( - to_treasury, - ); - - // handle tip if there is one - if let Some(tip) = fees_then_tips.next() { - // for now we use the same burn/treasury strategy used for regular fees - let (_, to_treasury) = tip.ration(burn_part, treasury_part); - ResolveTo::, pallet_balances::Pallet>::on_unbalanced( - to_treasury, - ); - } - } - } - - // this is called from pallet_evm for Ethereum-based transactions - // (technically, it calls on_unbalanced, which calls this when non-zero) - fn on_nonzero_unbalanced(amount: Credit>) { - // Balances pallet automatically burns dropped Credits by decreasing - // total_supply accordingly - let treasury_proportion = - runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion::get(); - let treasury_part = treasury_proportion.deconstruct(); - let burn_part = Perbill::one().deconstruct() - treasury_part; - let (_, to_treasury) = amount.ration(burn_part, treasury_part); - ResolveTo::, pallet_balances::Pallet>::on_unbalanced(to_treasury); - } -} - pub struct LengthToFee; impl WeightToFeePolynomial for LengthToFee { type Balance = Balance; @@ -408,7 +360,13 @@ impl WeightToFeePolynomial for LengthToFee { impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FungibleAdapter< + Balances, + DealWithSubstrateFeesAndTip< + Runtime, + dynamic_params::runtime_config::FeesTreasuryProportion, + >, + >; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = ConstantMultiplier>; type LengthToFee = LengthToFee; @@ -542,7 +500,10 @@ impl pallet_evm::Config for Runtime { type PrecompilesType = MoonbasePrecompiles; type PrecompilesValue = PrecompilesValue; type ChainId = EthereumChainId; - type OnChargeTransaction = OnChargeEVMTransaction>; + type OnChargeTransaction = OnChargeEVMTransaction< + DealWithEthereumBaseFees, + DealWithEthereumPriorityFees, + >; type BlockGasLimit = BlockGasLimit; type FindAuthor = FindAuthorAdapter; type OnCreate = (); @@ -1496,6 +1457,10 @@ pub type Executive = frame_executive::Executive< #[cfg(feature = "runtime-benchmarks")] use moonbeam_runtime_common::benchmarking::BenchmarkHelper; +use moonbeam_runtime_common::deal_with_fees::{ + DealWithEthereumBaseFees, DealWithEthereumPriorityFees, DealWithSubstrateFeesAndTip, +}; + #[cfg(feature = "runtime-benchmarks")] mod benches { frame_support::parameter_types! { diff --git a/runtime/moonbase/tests/common/mod.rs b/runtime/moonbase/tests/common/mod.rs index 19df108ab7..794988584f 100644 --- a/runtime/moonbase/tests/common/mod.rs +++ b/runtime/moonbase/tests/common/mod.rs @@ -364,6 +364,9 @@ pub fn set_parachain_inherent_data() { use cumulus_primitives_core::PersistedValidationData; use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; + let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); + let mut relay_sproof = RelayStateSproofBuilder::default(); relay_sproof.para_id = 100u32.into(); relay_sproof.included_para_head = Some(HeadData(vec![1, 2, 3])); diff --git a/runtime/moonbase/tests/integration_test.rs b/runtime/moonbase/tests/integration_test.rs index 9cc5ef1a25..e9ab621fb2 100644 --- a/runtime/moonbase/tests/integration_test.rs +++ b/runtime/moonbase/tests/integration_test.rs @@ -77,12 +77,13 @@ use moonkit_xcm_primitives::AccountIdAssetIdConversion; use nimbus_primitives::NimbusId; use pallet_evm::PrecompileSet; //use pallet_evm_precompileset_assets_erc20::{SELECTOR_LOG_APPROVAL, SELECTOR_LOG_TRANSFER}; +use moonbase_runtime::runtime_params::dynamic_params; use pallet_moonbeam_foreign_assets::AssetStatus; use pallet_transaction_payment::Multiplier; use pallet_xcm_transactor::{Currency, CurrencyPayment, HrmpOperation, TransactWeights}; use parity_scale_codec::Encode; use sha3::{Digest, Keccak256}; -use sp_core::{crypto::UncheckedFrom, ByteArray, Pair, H160, H256, U256}; +use sp_core::{crypto::UncheckedFrom, ByteArray, Get, Pair, H160, H256, U256}; use sp_runtime::{bounded_vec, DispatchError, ModuleError}; use std::cell::Cell; use std::rc::Rc; @@ -1881,6 +1882,7 @@ fn transfer_ed_0_evm() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); // EVM transfer assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(ALICE), @@ -1933,7 +1935,7 @@ fn refund_ed_0_evm() { } #[test] -fn author_does_not_receive_priority_fee() { +fn author_does_receive_priority_fee() { ExtBuilder::default() .with_balances(vec![( AccountId::from(BOB), @@ -1943,6 +1945,7 @@ fn author_does_not_receive_priority_fee() { .execute_with(|| { // Some block author as seen by pallet-evm. let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); // Currently the default impl of the evm uses `deposit_into_existing`. // If we were to use this implementation, and for an author to receive eventual tips, // the account needs to be somehow initialized, otherwise the deposit would fail. @@ -1961,8 +1964,10 @@ fn author_does_not_receive_priority_fee() { access_list: Vec::new(), }) .dispatch(::RuntimeOrigin::root())); - // Author free balance didn't change. - assert_eq!(Balances::free_balance(author), 100 * UNIT,); + + let priority_fee = 200 * GIGAWEI * 21_000; + // Author free balance increased by priority fee. + assert_eq!(Balances::free_balance(author), 100 * UNIT + priority_fee,); }); } @@ -1983,6 +1988,8 @@ fn total_issuance_after_evm_transaction_with_priority_fee() { .build() .execute_with(|| { let issuance_before = ::Currency::total_issuance(); + let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); // EVM transfer. assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(BOB), @@ -1998,14 +2005,18 @@ fn total_issuance_after_evm_transaction_with_priority_fee() { .dispatch(::RuntimeOrigin::root())); let issuance_after = ::Currency::total_issuance(); - // Fee is 1 * base_fee + tip. - let fee = ((2 * BASE_FEE_GENISIS) * 21_000) as f64; - // 80% was burned. - let expected_burn = (fee * 0.8) as u128; - assert_eq!(issuance_after, issuance_before - expected_burn,); - // 20% was sent to treasury. - let expected_treasury = (fee * 0.2) as u128; - assert_eq!(moonbase_runtime::Treasury::pot(), expected_treasury); + + let base_fee: Balance = BASE_FEE_GENISIS * 21_000; + + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + // only base fee is split between being burned and sent to treasury + let treasury_base_fee_part: Balance = treasury_proportion.mul_floor(base_fee); + let burnt_base_fee_part: Balance = base_fee - treasury_base_fee_part; + + assert_eq!(issuance_after, issuance_before - burnt_base_fee_part); + + assert_eq!(moonbase_runtime::Treasury::pot(), treasury_base_fee_part); }); } @@ -2026,6 +2037,7 @@ fn total_issuance_after_evm_transaction_without_priority_fee() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); let issuance_before = ::Currency::total_issuance(); // EVM transfer. assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { @@ -2045,13 +2057,18 @@ fn total_issuance_after_evm_transaction_without_priority_fee() { // Fee is 1 GWEI base fee. let base_fee = TransactionPaymentAsGasPrice::min_gas_price().0; assert_eq!(base_fee.as_u128(), BASE_FEE_GENISIS); // hint in case following asserts fail - let fee = (base_fee.as_u128() * 21_000u128) as f64; - // 80% was burned. - let expected_burn = (fee * 0.8) as u128; - assert_eq!(issuance_after, issuance_before - expected_burn,); - // 20% was sent to treasury. - let expected_treasury = (fee * 0.2) as u128; - assert_eq!(moonbase_runtime::Treasury::pot(), expected_treasury); + + let base_fee: Balance = BASE_FEE_GENISIS * 21_000; + + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + // only base fee is split between being burned and sent to treasury + let treasury_base_fee_part: Balance = treasury_proportion.mul_floor(base_fee); + let burnt_base_fee_part: Balance = base_fee - treasury_base_fee_part; + + assert_eq!(issuance_after, issuance_before - burnt_base_fee_part); + + assert_eq!(moonbase_runtime::Treasury::pot(), treasury_base_fee_part); }); } @@ -2780,21 +2797,27 @@ fn substrate_based_fees_zero_txn_costs_only_base_extrinsic() { #[test] fn deal_with_fees_handles_tip() { use frame_support::traits::OnUnbalanced; - use moonbase_runtime::{DealWithFees, Treasury}; + use moonbase_runtime::Treasury; + use moonbeam_runtime_common::deal_with_fees::DealWithSubstrateFeesAndTip; ExtBuilder::default().build().execute_with(|| { - // This test checks the functionality of the `DealWithFees` trait implementation in the runtime. - // It simulates a scenario where a fee and a tip are issued to an account and ensures that the - // treasury receives the correct amount (20% of the total), and the rest is burned (80%). - // - // The test follows these steps: - // 1. It issues a fee of 100 and a tip of 1000. - // 2. It checks the total supply before the fee and tip are dealt with, which should be 1_100. - // 3. It checks that the treasury's balance is initially 0. - // 4. It calls `DealWithFees::on_unbalanceds` with the fee and tip. - // 5. It checks that the treasury's balance is now 220 (20% of the fee and tip). - // 6. It checks that the total supply has decreased by 880 (80% of the fee and tip), indicating - // that this amount was burned. + set_parachain_inherent_data(); + // This test validates the functionality of the `DealWithSubstrateFeesAndTip` trait implementation + // in the Moonbeam runtime. It verifies that: + // - The correct proportion of the fee is sent to the treasury. + // - The remaining fee is burned (removed from the total supply). + // - The entire tip is sent to the block author. + + // The test details: + // 1. Simulate issuing a `fee` of 100 and a `tip` of 1000. + // 2. Confirm the initial total supply is 1,100 (equal to the sum of the issued fee and tip). + // 3. Confirm the treasury's balance is initially 0. + // 4. Execute the `DealWithSubstrateFeesAndTip::on_unbalanceds` function with the `fee` and `tip`. + // 5. Validate that the treasury's balance has increased by 20% of the fee (based on FeesTreasuryProportion). + // 6. Validate that 80% of the fee is burned, and the total supply decreases accordingly. + // 7. Validate that the entire tip (100%) is sent to the block author (collator). + + // Step 1: Issue the fee and tip amounts. let fee = as frame_support::traits::fungible::Balanced< AccountId, >>::issue(100); @@ -2802,18 +2825,41 @@ fn deal_with_fees_handles_tip() { AccountId, >>::issue(1000); + // Step 2: Validate the initial supply and balances. let total_supply_before = Balances::total_issuance(); + let block_author = pallet_author_inherent::Pallet::::get(); + let block_author_balance_before = Balances::free_balance(&block_author); assert_eq!(total_supply_before, 1_100); assert_eq!(Balances::free_balance(&Treasury::account_id()), 0); - DealWithFees::on_unbalanceds(vec![fee, tip].into_iter()); + // Step 3: Execute the fees handling logic. + DealWithSubstrateFeesAndTip::< + Runtime, + dynamic_params::runtime_config::FeesTreasuryProportion, + >::on_unbalanceds(vec![fee, tip].into_iter()); + + // Step 4: Compute the split between treasury and burned fees based on FeesTreasuryProportion (20%). + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + let treasury_fee_part: Balance = treasury_proportion.mul_floor(100); + let burnt_fee_part: Balance = 100 - treasury_fee_part; - // treasury should have received 20% - assert_eq!(Balances::free_balance(&Treasury::account_id()), 220); + // Step 5: Validate the treasury received 20% of the fee. + assert_eq!( + Balances::free_balance(&Treasury::account_id()), + treasury_fee_part, + ); - // verify 80% burned + // Step 6: Verify that 80% of the fee was burned (removed from the total supply). let total_supply_after = Balances::total_issuance(); - assert_eq!(total_supply_before - total_supply_after, 880); + assert_eq!(total_supply_before - total_supply_after, burnt_fee_part,); + + // Step 7: Validate that the block author (collator) received 100% of the tip. + let block_author_balance_after = Balances::free_balance(&block_author); + assert_eq!( + block_author_balance_after - block_author_balance_before, + 1000, + ); }); } diff --git a/runtime/moonbase/tests/runtime_apis.rs b/runtime/moonbase/tests/runtime_apis.rs index 7cfb9dc103..66f1e0edce 100644 --- a/runtime/moonbase/tests/runtime_apis.rs +++ b/runtime/moonbase/tests/runtime_apis.rs @@ -216,6 +216,8 @@ fn ethereum_runtime_rpc_api_current_transaction_statuses() { )]) .build() .execute_with(|| { + set_parachain_inherent_data(); + let _result = Executive::apply_extrinsic(unchecked_eth_tx(VALID_ETH_TX)); rpc_run_to_block(2); let statuses = @@ -273,6 +275,7 @@ fn ethereum_runtime_rpc_api_current_receipts() { )]) .build() .execute_with(|| { + set_parachain_inherent_data(); let _result = Executive::apply_extrinsic(unchecked_eth_tx(VALID_ETH_TX)); rpc_run_to_block(2); let receipts = Runtime::current_receipts().expect("Receipts result."); diff --git a/runtime/moonbeam/src/lib.rs b/runtime/moonbeam/src/lib.rs index b5816bede2..74be2eeeec 100644 --- a/runtime/moonbeam/src/lib.rs +++ b/runtime/moonbeam/src/lib.rs @@ -46,11 +46,9 @@ use frame_support::{ parameter_types, traits::{ fungible::{Balanced, Credit, HoldConsideration, Inspect}, - tokens::imbalance::ResolveTo, tokens::{PayFromAccount, UnityAssetBalanceConversion}, ConstBool, ConstU128, ConstU16, ConstU32, ConstU64, ConstU8, Contains, EitherOfDiverse, - EqualPrivilegeOnly, Imbalance, InstanceFilter, LinearStoragePrice, OnFinalize, - OnUnbalanced, + EqualPrivilegeOnly, InstanceFilter, LinearStoragePrice, OnFinalize, OnUnbalanced, }, weights::{ constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, Weight, WeightToFeeCoefficient, @@ -75,7 +73,6 @@ use pallet_evm::{ }; pub use pallet_parachain_staking::{weights::WeightInfo, InflationInfo, Range}; use pallet_transaction_payment::{FungibleAdapter, Multiplier, TargetedFeeAdjustment}; -use pallet_treasury::TreasuryAccountId; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; @@ -332,52 +329,6 @@ impl pallet_balances::Config for Runtime { type WeightInfo = moonbeam_weights::pallet_balances::WeightInfo; } -pub struct DealWithFees(sp_std::marker::PhantomData); -impl OnUnbalanced>> for DealWithFees -where - R: pallet_balances::Config + pallet_treasury::Config, -{ - // this seems to be called for substrate-based transactions - fn on_unbalanceds( - mut fees_then_tips: impl Iterator>>, - ) { - if let Some(fees) = fees_then_tips.next() { - let treasury_proportion = - runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion::get(); - let treasury_part = treasury_proportion.deconstruct(); - let burn_part = Perbill::one().deconstruct() - treasury_part; - let (_, to_treasury) = fees.ration(burn_part, treasury_part); - // Balances pallet automatically burns dropped Credits by decreasing - // total_supply accordingly - ResolveTo::, pallet_balances::Pallet>::on_unbalanced( - to_treasury, - ); - - // handle tip if there is one - if let Some(tip) = fees_then_tips.next() { - // for now we use the same burn/treasury strategy used for regular fees - let (_, to_treasury) = tip.ration(burn_part, treasury_part); - ResolveTo::, pallet_balances::Pallet>::on_unbalanced( - to_treasury, - ); - } - } - } - - // this is called from pallet_evm for Ethereum-based transactions - // (technically, it calls on_unbalanced, which calls this when non-zero) - fn on_nonzero_unbalanced(amount: Credit>) { - // Balances pallet automatically burns dropped Credits by decreasing - // total_supply accordingly - let treasury_proportion = - runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion::get(); - let treasury_part = treasury_proportion.deconstruct(); - let burn_part = Perbill::one().deconstruct() - treasury_part; - let (_, to_treasury) = amount.ration(burn_part, treasury_part); - ResolveTo::, pallet_balances::Pallet>::on_unbalanced(to_treasury); - } -} - pub struct LengthToFee; impl WeightToFeePolynomial for LengthToFee { type Balance = Balance; @@ -402,7 +353,13 @@ impl WeightToFeePolynomial for LengthToFee { impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FungibleAdapter< + Balances, + DealWithSubstrateFeesAndTip< + Runtime, + dynamic_params::runtime_config::FeesTreasuryProportion, + >, + >; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = ConstantMultiplier>; type LengthToFee = LengthToFee; @@ -533,7 +490,10 @@ impl pallet_evm::Config for Runtime { type PrecompilesType = MoonbeamPrecompiles; type PrecompilesValue = PrecompilesValue; type ChainId = EthereumChainId; - type OnChargeTransaction = OnChargeEVMTransaction>; + type OnChargeTransaction = OnChargeEVMTransaction< + DealWithEthereumBaseFees, + DealWithEthereumPriorityFees, + >; type BlockGasLimit = BlockGasLimit; type FindAuthor = FindAuthorAdapter; type OnCreate = (); @@ -1491,6 +1451,10 @@ construct_runtime! { #[cfg(feature = "runtime-benchmarks")] use moonbeam_runtime_common::benchmarking::BenchmarkHelper; +use moonbeam_runtime_common::deal_with_fees::{ + DealWithEthereumBaseFees, DealWithEthereumPriorityFees, DealWithSubstrateFeesAndTip, +}; + #[cfg(feature = "runtime-benchmarks")] mod benches { frame_support::parameter_types! { diff --git a/runtime/moonbeam/tests/common/mod.rs b/runtime/moonbeam/tests/common/mod.rs index 9301dd123b..f83d7ad8a0 100644 --- a/runtime/moonbeam/tests/common/mod.rs +++ b/runtime/moonbeam/tests/common/mod.rs @@ -345,6 +345,9 @@ pub fn set_parachain_inherent_data() { use cumulus_primitives_core::PersistedValidationData; use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; + let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); + let mut relay_sproof = RelayStateSproofBuilder::default(); relay_sproof.para_id = 100u32.into(); relay_sproof.included_para_head = Some(HeadData(vec![1, 2, 3])); diff --git a/runtime/moonbeam/tests/integration_test.rs b/runtime/moonbeam/tests/integration_test.rs index c56a3c2da1..ec8d54e953 100644 --- a/runtime/moonbeam/tests/integration_test.rs +++ b/runtime/moonbeam/tests/integration_test.rs @@ -33,6 +33,7 @@ use frame_support::{ StorageHasher, Twox128, }; use moonbeam_runtime::currency::{GIGAWEI, WEI}; +use moonbeam_runtime::runtime_params::dynamic_params; use moonbeam_runtime::{ asset_config::ForeignAssetInstance, currency::GLMR, @@ -57,7 +58,7 @@ use precompile_utils::{ testing::*, }; use sha3::{Digest, Keccak256}; -use sp_core::{ByteArray, Pair, H160, U256}; +use sp_core::{ByteArray, Get, Pair, H160, U256}; use sp_runtime::{ traits::{Convert, Dispatchable}, BuildStorage, DispatchError, ModuleError, @@ -1444,6 +1445,7 @@ fn transfer_ed_0_evm() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); // EVM transfer assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(ALICE), @@ -1474,6 +1476,7 @@ fn refund_ed_0_evm() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); // EVM transfer that zeroes ALICE assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(ALICE), @@ -1496,7 +1499,7 @@ fn refund_ed_0_evm() { } #[test] -fn author_does_not_receive_priority_fee() { +fn author_does_receive_priority_fee() { ExtBuilder::default() .with_balances(vec![( AccountId::from(BOB), @@ -1506,6 +1509,7 @@ fn author_does_not_receive_priority_fee() { .execute_with(|| { // Some block author as seen by pallet-evm. let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); // Currently the default impl of the evm uses `deposit_into_existing`. // If we were to use this implementation, and for an author to receive eventual tips, // the account needs to be somehow initialized, otherwise the deposit would fail. @@ -1524,13 +1528,16 @@ fn author_does_not_receive_priority_fee() { access_list: Vec::new(), }) .dispatch(::RuntimeOrigin::root())); - // Author free balance didn't change. - assert_eq!(Balances::free_balance(author), 100 * GLMR,); + + let priority_fee = 200 * GIGAWEI * 21_000; + // Author free balance increased by priority fee. + assert_eq!(Balances::free_balance(author), 100 * GLMR + priority_fee,); }); } #[test] fn total_issuance_after_evm_transaction_with_priority_fee() { + use fp_evm::FeeCalculator; ExtBuilder::default() .with_balances(vec![ ( @@ -1545,6 +1552,8 @@ fn total_issuance_after_evm_transaction_with_priority_fee() { .build() .execute_with(|| { let issuance_before = ::Currency::total_issuance(); + let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); // EVM transfer. assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(BOB), @@ -1560,19 +1569,26 @@ fn total_issuance_after_evm_transaction_with_priority_fee() { .dispatch(::RuntimeOrigin::root())); let issuance_after = ::Currency::total_issuance(); - // Fee is 25 GWEI base fee + 100 GWEI tip. - let fee = ((125 * GIGAWEI) * 21_000) as f64; - // 80% was burned. - let expected_burn = (fee * 0.8) as u128; - assert_eq!(issuance_after, issuance_before - expected_burn,); - // 20% was sent to treasury. - let expected_treasury = (fee * 0.2) as u128; - assert_eq!(moonbeam_runtime::Treasury::pot(), expected_treasury); + + let base_fee = TransactionPaymentAsGasPrice::min_gas_price().0.as_u128(); + + let base_fee: Balance = base_fee * 21_000; + + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + // only base fee is split between being burned and sent to treasury + let treasury_base_fee_part: Balance = treasury_proportion.mul_floor(base_fee); + let burnt_base_fee_part: Balance = base_fee - treasury_base_fee_part; + + assert_eq!(issuance_after, issuance_before - burnt_base_fee_part); + + assert_eq!(moonbeam_runtime::Treasury::pot(), treasury_base_fee_part); }); } #[test] fn total_issuance_after_evm_transaction_without_priority_fee() { + use fp_evm::FeeCalculator; ExtBuilder::default() .with_balances(vec![ ( @@ -1586,6 +1602,7 @@ fn total_issuance_after_evm_transaction_without_priority_fee() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); let issuance_before = ::Currency::total_issuance(); // EVM transfer. assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { @@ -1602,14 +1619,20 @@ fn total_issuance_after_evm_transaction_without_priority_fee() { .dispatch(::RuntimeOrigin::root())); let issuance_after = ::Currency::total_issuance(); - // Fee is 100 GWEI base fee. - let fee = (BASE_FEE_GENESIS * 21_000) as f64; - // 80% was burned. - let expected_burn = (fee * 0.8) as u128; - assert_eq!(issuance_after, issuance_before - expected_burn,); - // 20% was sent to treasury. - let expected_treasury = (fee * 0.2) as u128; - assert_eq!(moonbeam_runtime::Treasury::pot(), expected_treasury); + + let base_fee = TransactionPaymentAsGasPrice::min_gas_price().0.as_u128(); + + let base_fee: Balance = base_fee * 21_000; + + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + // only base fee is split between being burned and sent to treasury + let treasury_base_fee_part: Balance = treasury_proportion.mul_floor(base_fee); + let burnt_base_fee_part: Balance = base_fee - treasury_base_fee_part; + + assert_eq!(issuance_after, issuance_before - burnt_base_fee_part); + + assert_eq!(moonbeam_runtime::Treasury::pot(), treasury_base_fee_part); }); } @@ -2595,21 +2618,27 @@ fn removed_precompiles() { #[test] fn deal_with_fees_handles_tip() { use frame_support::traits::OnUnbalanced; - use moonbeam_runtime::{DealWithFees, Treasury}; + use moonbeam_runtime::Treasury; + use moonbeam_runtime_common::deal_with_fees::DealWithSubstrateFeesAndTip; ExtBuilder::default().build().execute_with(|| { - // This test checks the functionality of the `DealWithFees` trait implementation in the runtime. - // It simulates a scenario where a fee and a tip are issued to an account and ensures that the - // treasury receives the correct amount (20% of the total), and the rest is burned (80%). - // - // The test follows these steps: - // 1. It issues a fee of 100 and a tip of 1000. - // 2. It checks the total supply before the fee and tip are dealt with, which should be 1_100. - // 3. It checks that the treasury's balance is initially 0. - // 4. It calls `DealWithFees::on_unbalanceds` with the fee and tip. - // 5. It checks that the treasury's balance is now 220 (20% of the fee and tip). - // 6. It checks that the total supply has decreased by 880 (80% of the fee and tip), indicating - // that this amount was burned. + set_parachain_inherent_data(); + // This test validates the functionality of the `DealWithSubstrateFeesAndTip` trait implementation + // in the Moonbeam runtime. It verifies that: + // - The correct proportion of the fee is sent to the treasury. + // - The remaining fee is burned (removed from the total supply). + // - The entire tip is sent to the block author. + + // The test details: + // 1. Simulate issuing a `fee` of 100 and a `tip` of 1000. + // 2. Confirm the initial total supply is 1,100 (equal to the sum of the issued fee and tip). + // 3. Confirm the treasury's balance is initially 0. + // 4. Execute the `DealWithSubstrateFeesAndTip::on_unbalanceds` function with the `fee` and `tip`. + // 5. Validate that the treasury's balance has increased by 20% of the fee (based on FeesTreasuryProportion). + // 6. Validate that 80% of the fee is burned, and the total supply decreases accordingly. + // 7. Validate that the entire tip (100%) is sent to the block author (collator). + + // Step 1: Issue the fee and tip amounts. let fee = as frame_support::traits::fungible::Balanced< AccountId, >>::issue(100); @@ -2617,18 +2646,41 @@ fn deal_with_fees_handles_tip() { AccountId, >>::issue(1000); + // Step 2: Validate the initial supply and balances. let total_supply_before = Balances::total_issuance(); + let block_author = pallet_author_inherent::Pallet::::get(); + let block_author_balance_before = Balances::free_balance(&block_author); assert_eq!(total_supply_before, 1_100); assert_eq!(Balances::free_balance(&Treasury::account_id()), 0); - DealWithFees::on_unbalanceds(vec![fee, tip].into_iter()); + // Step 3: Execute the fees handling logic. + DealWithSubstrateFeesAndTip::< + Runtime, + dynamic_params::runtime_config::FeesTreasuryProportion, + >::on_unbalanceds(vec![fee, tip].into_iter()); + + // Step 4: Compute the split between treasury and burned fees based on FeesTreasuryProportion (20%). + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); - // treasury should have received 20% - assert_eq!(Balances::free_balance(&Treasury::account_id()), 220); + let treasury_fee_part: Balance = treasury_proportion.mul_floor(100); + let burnt_fee_part: Balance = 100 - treasury_fee_part; - // verify 80% burned + // Step 5: Validate the treasury received 20% of the fee. + assert_eq!( + Balances::free_balance(&Treasury::account_id()), + treasury_fee_part, + ); + + // Step 6: Verify that 80% of the fee was burned (removed from the total supply). let total_supply_after = Balances::total_issuance(); - assert_eq!(total_supply_before - total_supply_after, 880); + assert_eq!(total_supply_before - total_supply_after, burnt_fee_part,); + + // Step 7: Validate that the block author (collator) received 100% of the tip. + let block_author_balance_after = Balances::free_balance(&block_author); + assert_eq!( + block_author_balance_after - block_author_balance_before, + 1000, + ); }); } diff --git a/runtime/moonriver/src/lib.rs b/runtime/moonriver/src/lib.rs index 47e564ea5a..f0e5062258 100644 --- a/runtime/moonriver/src/lib.rs +++ b/runtime/moonriver/src/lib.rs @@ -47,11 +47,9 @@ use frame_support::{ parameter_types, traits::{ fungible::{Balanced, Credit, HoldConsideration, Inspect}, - tokens::imbalance::ResolveTo, tokens::{PayFromAccount, UnityAssetBalanceConversion}, ConstBool, ConstU128, ConstU16, ConstU32, ConstU64, ConstU8, Contains, EitherOfDiverse, - EqualPrivilegeOnly, Imbalance, InstanceFilter, LinearStoragePrice, OnFinalize, - OnUnbalanced, + EqualPrivilegeOnly, InstanceFilter, LinearStoragePrice, OnFinalize, OnUnbalanced, }, weights::{ constants::WEIGHT_REF_TIME_PER_SECOND, ConstantMultiplier, Weight, WeightToFeeCoefficient, @@ -65,6 +63,9 @@ pub use moonbeam_core_primitives::{ Index, Signature, }; use moonbeam_rpc_primitives_txpool::TxPoolResponse; +use moonbeam_runtime_common::deal_with_fees::{ + DealWithEthereumBaseFees, DealWithEthereumPriorityFees, DealWithSubstrateFeesAndTip, +}; use moonbeam_runtime_common::timestamp::{ConsensusHookWrapperForRelayTimestamp, RelayTimestamp}; pub use pallet_author_slot_filter::EligibilityValue; use pallet_ethereum::Call::transact; @@ -76,7 +77,6 @@ use pallet_evm::{ }; pub use pallet_parachain_staking::{weights::WeightInfo, InflationInfo, Range}; use pallet_transaction_payment::{FungibleAdapter, Multiplier, TargetedFeeAdjustment}; -use pallet_treasury::TreasuryAccountId; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use sp_api::impl_runtime_apis; @@ -334,52 +334,6 @@ impl pallet_balances::Config for Runtime { type WeightInfo = moonriver_weights::pallet_balances::WeightInfo; } -pub struct DealWithFees(sp_std::marker::PhantomData); -impl OnUnbalanced>> for DealWithFees -where - R: pallet_balances::Config + pallet_treasury::Config, -{ - // this seems to be called for substrate-based transactions - fn on_unbalanceds( - mut fees_then_tips: impl Iterator>>, - ) { - if let Some(fees) = fees_then_tips.next() { - let treasury_proportion = - runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion::get(); - let treasury_part = treasury_proportion.deconstruct(); - let burn_part = Perbill::one().deconstruct() - treasury_part; - let (_, to_treasury) = fees.ration(burn_part, treasury_part); - // Balances pallet automatically burns dropped Credits by decreasing - // total_supply accordingly - ResolveTo::, pallet_balances::Pallet>::on_unbalanced( - to_treasury, - ); - - // handle tip if there is one - if let Some(tip) = fees_then_tips.next() { - // for now we use the same burn/treasury strategy used for regular fees - let (_, to_treasury) = tip.ration(burn_part, treasury_part); - ResolveTo::, pallet_balances::Pallet>::on_unbalanced( - to_treasury, - ); - } - } - } - - // this is called from pallet_evm for Ethereum-based transactions - // (technically, it calls on_unbalanced, which calls this when non-zero) - fn on_nonzero_unbalanced(amount: Credit>) { - // Balances pallet automatically burns dropped Credits by decreasing - // total_supply accordingly - let treasury_proportion = - runtime_params::dynamic_params::runtime_config::FeesTreasuryProportion::get(); - let treasury_part = treasury_proportion.deconstruct(); - let burn_part = Perbill::one().deconstruct() - treasury_part; - let (_, to_treasury) = amount.ration(burn_part, treasury_part); - ResolveTo::, pallet_balances::Pallet>::on_unbalanced(to_treasury); - } -} - pub struct LengthToFee; impl WeightToFeePolynomial for LengthToFee { type Balance = Balance; @@ -404,7 +358,13 @@ impl WeightToFeePolynomial for LengthToFee { impl pallet_transaction_payment::Config for Runtime { type RuntimeEvent = RuntimeEvent; - type OnChargeTransaction = FungibleAdapter>; + type OnChargeTransaction = FungibleAdapter< + Balances, + DealWithSubstrateFeesAndTip< + Runtime, + dynamic_params::runtime_config::FeesTreasuryProportion, + >, + >; type OperationalFeeMultiplier = ConstU8<5>; type WeightToFee = ConstantMultiplier>; type LengthToFee = LengthToFee; @@ -535,7 +495,10 @@ impl pallet_evm::Config for Runtime { type PrecompilesType = MoonriverPrecompiles; type PrecompilesValue = PrecompilesValue; type ChainId = EthereumChainId; - type OnChargeTransaction = OnChargeEVMTransaction>; + type OnChargeTransaction = OnChargeEVMTransaction< + DealWithEthereumBaseFees, + DealWithEthereumPriorityFees, + >; type BlockGasLimit = BlockGasLimit; type FindAuthor = FindAuthorAdapter; type OnCreate = (); diff --git a/runtime/moonriver/tests/common/mod.rs b/runtime/moonriver/tests/common/mod.rs index 249debb227..32d0dd8b33 100644 --- a/runtime/moonriver/tests/common/mod.rs +++ b/runtime/moonriver/tests/common/mod.rs @@ -352,6 +352,9 @@ pub fn set_parachain_inherent_data() { use cumulus_primitives_core::PersistedValidationData; use cumulus_test_relay_sproof_builder::RelayStateSproofBuilder; + let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); + let mut relay_sproof = RelayStateSproofBuilder::default(); relay_sproof.para_id = 100u32.into(); relay_sproof.included_para_head = Some(HeadData(vec![1, 2, 3])); diff --git a/runtime/moonriver/tests/integration_test.rs b/runtime/moonriver/tests/integration_test.rs index 66787031e1..148e14c2a6 100644 --- a/runtime/moonriver/tests/integration_test.rs +++ b/runtime/moonriver/tests/integration_test.rs @@ -33,6 +33,7 @@ use frame_support::{ use moonbeam_xcm_benchmarks::weights::XcmWeight; use moonkit_xcm_primitives::AccountIdAssetIdConversion; use moonriver_runtime::currency::{GIGAWEI, WEI}; +use moonriver_runtime::runtime_params::dynamic_params; use moonriver_runtime::{ asset_config::ForeignAssetInstance, xcm_config::{CurrencyId, SelfReserve}, @@ -53,7 +54,7 @@ use precompile_utils::{ testing::*, }; use sha3::{Digest, Keccak256}; -use sp_core::{ByteArray, Pair, H160, U256}; +use sp_core::{ByteArray, Get, Pair, H160, U256}; use sp_runtime::{ traits::{Convert, Dispatchable}, BuildStorage, DispatchError, ModuleError, @@ -1428,6 +1429,7 @@ fn transfer_ed_0_evm() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); // EVM transfer assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(ALICE), @@ -1458,6 +1460,7 @@ fn refund_ed_0_evm() { ]) .build() .execute_with(|| { + set_parachain_inherent_data(); // EVM transfer that zeroes ALICE assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(ALICE), @@ -1480,7 +1483,7 @@ fn refund_ed_0_evm() { } #[test] -fn author_does_not_receive_priority_fee() { +fn author_does_receive_priority_fee() { ExtBuilder::default() .with_balances(vec![( AccountId::from(BOB), @@ -1490,6 +1493,7 @@ fn author_does_not_receive_priority_fee() { .execute_with(|| { // Some block author as seen by pallet-evm. let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); // Currently the default impl of the evm uses `deposit_into_existing`. // If we were to use this implementation, and for an author to receive eventual tips, // the account needs to be somehow initialized, otherwise the deposit would fail. @@ -1508,13 +1512,16 @@ fn author_does_not_receive_priority_fee() { access_list: Vec::new(), }) .dispatch(::RuntimeOrigin::root())); - // Author free balance didn't change. - assert_eq!(Balances::free_balance(author), 100 * MOVR,); + + let priority_fee = 200 * GIGAWEI * 21_000; + // Author free balance increased by priority fee. + assert_eq!(Balances::free_balance(author), 100 * MOVR + priority_fee,); }); } #[test] fn total_issuance_after_evm_transaction_with_priority_fee() { + use fp_evm::FeeCalculator; ExtBuilder::default() .with_balances(vec![ ( @@ -1529,6 +1536,8 @@ fn total_issuance_after_evm_transaction_with_priority_fee() { .build() .execute_with(|| { let issuance_before = ::Currency::total_issuance(); + let author = AccountId::from(>::find_author()); + pallet_author_inherent::Author::::put(author); // EVM transfer. assert_ok!(RuntimeCall::EVM(pallet_evm::Call::::call { source: H160::from(BOB), @@ -1544,18 +1553,26 @@ fn total_issuance_after_evm_transaction_with_priority_fee() { .dispatch(::RuntimeOrigin::root())); let issuance_after = ::Currency::total_issuance(); - let fee = ((2 * BASE_FEE_GENESIS) * 21_000) as f64; - // 80% was burned. - let expected_burn = (fee * 0.8) as u128; - assert_eq!(issuance_after, issuance_before - expected_burn,); - // 20% was sent to treasury. - let expected_treasury = (fee * 0.2) as u128; - assert_eq!(moonriver_runtime::Treasury::pot(), expected_treasury); + + let base_fee = TransactionPaymentAsGasPrice::min_gas_price().0.as_u128(); + + let base_fee: Balance = base_fee * 21_000; + + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + // only base fee is split between being burned and sent to treasury + let treasury_base_fee_part: Balance = treasury_proportion.mul_floor(base_fee); + let burnt_base_fee_part: Balance = base_fee - treasury_base_fee_part; + + assert_eq!(issuance_after, issuance_before - burnt_base_fee_part); + + assert_eq!(moonriver_runtime::Treasury::pot(), treasury_base_fee_part); }); } #[test] fn total_issuance_after_evm_transaction_without_priority_fee() { + use fp_evm::FeeCalculator; ExtBuilder::default() .with_balances(vec![ ( @@ -1578,20 +1595,27 @@ fn total_issuance_after_evm_transaction_without_priority_fee() { value: (1 * MOVR).into(), gas_limit: 21_000u64, max_fee_per_gas: U256::from(BASE_FEE_GENESIS), - max_priority_fee_per_gas: Some(U256::from(BASE_FEE_GENESIS)), + max_priority_fee_per_gas: None, nonce: Some(U256::from(0)), access_list: Vec::new(), }) .dispatch(::RuntimeOrigin::root())); let issuance_after = ::Currency::total_issuance(); - let fee = ((1 * BASE_FEE_GENESIS) * 21_000) as f64; - // 80% was burned. - let expected_burn = (fee * 0.8) as u128; - assert_eq!(issuance_after, issuance_before - expected_burn,); - // 20% was sent to treasury. - let expected_treasury = (fee * 0.2) as u128; - assert_eq!(moonriver_runtime::Treasury::pot(), expected_treasury); + + let base_fee = TransactionPaymentAsGasPrice::min_gas_price().0.as_u128(); + + let base_fee: Balance = base_fee * 21_000; + + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); + + // only base fee is split between being burned and sent to treasury + let treasury_base_fee_part: Balance = treasury_proportion.mul_floor(base_fee); + let burnt_base_fee_part: Balance = base_fee - treasury_base_fee_part; + + assert_eq!(issuance_after, issuance_before - burnt_base_fee_part); + + assert_eq!(moonriver_runtime::Treasury::pot(), treasury_base_fee_part); }); } @@ -2493,21 +2517,27 @@ fn removed_precompiles() { #[test] fn deal_with_fees_handles_tip() { use frame_support::traits::OnUnbalanced; - use moonriver_runtime::{DealWithFees, Treasury}; + use moonbeam_runtime_common::deal_with_fees::DealWithSubstrateFeesAndTip; + use moonriver_runtime::Treasury; ExtBuilder::default().build().execute_with(|| { - // This test checks the functionality of the `DealWithFees` trait implementation in the runtime. - // It simulates a scenario where a fee and a tip are issued to an account and ensures that the - // treasury receives the correct amount (20% of the total), and the rest is burned (80%). - // - // The test follows these steps: - // 1. It issues a fee of 100 and a tip of 1000. - // 2. It checks the total supply before the fee and tip are dealt with, which should be 1_100. - // 3. It checks that the treasury's balance is initially 0. - // 4. It calls `DealWithFees::on_unbalanceds` with the fee and tip. - // 5. It checks that the treasury's balance is now 220 (20% of the fee and tip). - // 6. It checks that the total supply has decreased by 880 (80% of the fee and tip), indicating - // that this amount was burned. + set_parachain_inherent_data(); + // This test validates the functionality of the `DealWithSubstrateFeesAndTip` trait implementation + // in the Moonriver runtime. It verifies that: + // - The correct proportion of the fee is sent to the treasury. + // - The remaining fee is burned (removed from the total supply). + // - The entire tip is sent to the block author. + + // The test details: + // 1. Simulate issuing a `fee` of 100 and a `tip` of 1000. + // 2. Confirm the initial total supply is 1,100 (equal to the sum of the issued fee and tip). + // 3. Confirm the treasury's balance is initially 0. + // 4. Execute the `DealWithSubstrateFeesAndTip::on_unbalanceds` function with the `fee` and `tip`. + // 5. Validate that the treasury's balance has increased by 20% of the fee (based on FeesTreasuryProportion). + // 6. Validate that 80% of the fee is burned, and the total supply decreases accordingly. + // 7. Validate that the entire tip (100%) is sent to the block author (collator). + + // Step 1: Issue the fee and tip amounts. let fee = as frame_support::traits::fungible::Balanced< AccountId, >>::issue(100); @@ -2515,18 +2545,41 @@ fn deal_with_fees_handles_tip() { AccountId, >>::issue(1000); + // Step 2: Validate the initial supply and balances. let total_supply_before = Balances::total_issuance(); + let block_author = pallet_author_inherent::Pallet::::get(); + let block_author_balance_before = Balances::free_balance(&block_author); assert_eq!(total_supply_before, 1_100); assert_eq!(Balances::free_balance(&Treasury::account_id()), 0); - DealWithFees::on_unbalanceds(vec![fee, tip].into_iter()); + // Step 3: Execute the fees handling logic. + DealWithSubstrateFeesAndTip::< + Runtime, + dynamic_params::runtime_config::FeesTreasuryProportion, + >::on_unbalanceds(vec![fee, tip].into_iter()); + + // Step 4: Compute the split between treasury and burned fees based on FeesTreasuryProportion (20%). + let treasury_proportion = dynamic_params::runtime_config::FeesTreasuryProportion::get(); - // treasury should have received 20% - assert_eq!(Balances::free_balance(&Treasury::account_id()), 220); + let treasury_fee_part: Balance = treasury_proportion.mul_floor(100); + let burnt_fee_part: Balance = 100 - treasury_fee_part; - // verify 80% burned + // Step 5: Validate the treasury received 20% of the fee. + assert_eq!( + Balances::free_balance(&Treasury::account_id()), + treasury_fee_part, + ); + + // Step 6: Verify that 80% of the fee was burned (removed from the total supply). let total_supply_after = Balances::total_issuance(); - assert_eq!(total_supply_before - total_supply_after, 880); + assert_eq!(total_supply_before - total_supply_after, burnt_fee_part,); + + // Step 7: Validate that the block author (collator) received 100% of the tip. + let block_author_balance_after = Balances::free_balance(&block_author); + assert_eq!( + block_author_balance_after - block_author_balance_before, + 1000, + ); }); } diff --git a/test/helpers/block.ts b/test/helpers/block.ts index a2ab88991f..076c3ba34d 100644 --- a/test/helpers/block.ts +++ b/test/helpers/block.ts @@ -4,7 +4,6 @@ import { type BlockRangeOption, EXTRINSIC_BASE_WEIGHT, WEIGHT_PER_GAS, - calculateFeePortions, mapExtrinsics, } from "@moonwall/util"; import type { ApiPromise } from "@polkadot/api"; @@ -17,6 +16,8 @@ import type { AccountId20, Block } from "@polkadot/types/interfaces/runtime/type import chalk from "chalk"; import type { Debugger } from "debug"; import Debug from "debug"; +import { calculateFeePortions, split } from "./fees.ts"; +import { getFeesTreasuryProportion } from "./parameters.ts"; const debug = Debug("test:blocks"); @@ -149,6 +150,8 @@ export const verifyBlockFees = async ( ? (event.data[0] as DispatchInfo) : (event.data[1] as DispatchInfo); + const feesTreasuryProportion = await getFeesTreasuryProportion(context); + // We are only interested in fee paying extrinsics: // Either ethereum transactions or signed extrinsics with fees (substrate tx) if ( @@ -175,40 +178,47 @@ export const verifyBlockFees = async ( const baseFeePerGas = ( await context.viem().getBlock({ blockNumber: BigInt(number - 1) }) ).baseFeePerGas!; - let priorityFee; + let priorityFee; + let gasFee; // Transaction is an enum now with as many variants as supported transaction types. if (ethTxWrapper.isLegacy) { priorityFee = ethTxWrapper.asLegacy.gasPrice.toBigInt(); + gasFee = priorityFee; } else if (ethTxWrapper.isEip2930) { priorityFee = ethTxWrapper.asEip2930.gasPrice.toBigInt(); + gasFee = priorityFee; } else if (ethTxWrapper.isEip1559) { priorityFee = ethTxWrapper.asEip1559.maxPriorityFeePerGas.toBigInt(); + gasFee = ethTxWrapper.asEip1559.maxFeePerGas.toBigInt(); } - let effectiveTipPerGas = priorityFee - baseFeePerGas; - if (effectiveTipPerGas < 0n) { - effectiveTipPerGas = 0n; + let effectiveTipPerGas = gasFee - baseFeePerGas; + if (effectiveTipPerGas > priorityFee) { + effectiveTipPerGas = priorityFee; } - // Calculate the fees paid for base fee independently from tip fee. Both are subject - // to 80/20 split (burn/treasury) but calculating these over the sum of the two - // rather than independently leads to off-by-one errors. + // Calculate the fees paid for the base fee and tip fee independently. + // Only the base fee is subject to the split between burn and treasury. const baseFeesPaid = gasUsed * baseFeePerGas; const tipAsFeesPaid = gasUsed * effectiveTipPerGas; - const baseFeePortions = calculateFeePortions(baseFeesPaid); - const tipFeePortions = calculateFeePortions(tipAsFeesPaid); + const { burnt: baseFeePortionsBurnt } = calculateFeePortions( + feesTreasuryProportion, + baseFeesPaid + ); txFees += baseFeesPaid + tipAsFeesPaid; - txBurnt += baseFeePortions.burnt; - txBurnt += tipFeePortions.burnt; + txBurnt += baseFeePortionsBurnt; } else { // For a regular substrate tx, we use the partialFee - const feePortions = calculateFeePortions(fee.partialFee.toBigInt()); - const tipPortions = calculateFeePortions(extrinsic.tip.toBigInt()); + const feePortions = calculateFeePortions( + feesTreasuryProportion, + fee.partialFee.toBigInt() + ); + txFees += fee.partialFee.toBigInt() + extrinsic.tip.toBigInt(); - txBurnt += feePortions.burnt + tipPortions.burnt; + txBurnt += feePortions.burnt; // verify entire substrate txn fee const apiAt = await context.polkadotJs().at(previousBlockHash); @@ -239,11 +249,12 @@ export const verifyBlockFees = async ( })) as any ).toBigInt(); - // const tip = extrinsic.tip.toBigInt(); + const tip = extrinsic.tip.toBigInt(); const expectedPartialFee = lengthFee + weightFee + baseFee; - // Verify the computed fees are equal to the actual fees - expect(expectedPartialFee).to.eq((paymentEvent!.data[1] as u128).toBigInt()); + // Verify the computed fees are equal to the actual fees + tip + expect(expectedPartialFee + tip).to.eq((paymentEvent!.data[1] as u128).toBigInt()); + expect(tip).to.eq((paymentEvent!.data[2] as u128).toBigInt()); // Verify the computed fees are equal to the rpc computed fees expect(expectedPartialFee).to.eq(fee.partialFee.toBigInt()); diff --git a/test/helpers/fees.ts b/test/helpers/fees.ts new file mode 100644 index 0000000000..2b832c77d9 --- /dev/null +++ b/test/helpers/fees.ts @@ -0,0 +1,31 @@ +import { BN } from "@polkadot/util"; + +/// Recreation of fees.ration(burn_part, treasury_part) +export const split = (value: BN, part1: BN, part2: BN): [BN, BN] => { + const total = part1.add(part2); + if (total.eq(new BN(0)) || value.eq(new BN(0))) { + return [new BN(0), new BN(0)]; + } + const part1BN = value.mul(part1).div(total); + const part2BN = value.sub(part1BN); + return [part1BN, part2BN]; +}; + +export const calculateFeePortions = ( + feesTreasuryProportion: bigint, + fees: bigint +): { + burnt: bigint; + treasury: bigint; +} => { + const feesBN = new BN(fees.toString()); + const treasuryPartBN = new BN(feesTreasuryProportion.toString()); + const burntPartBN = new BN(1e9).sub(treasuryPartBN); + + const [burntBN, treasuryBN] = split(feesBN, burntPartBN, treasuryPartBN); + + return { + burnt: BigInt(burntBN.toString()), + treasury: BigInt(treasuryBN.toString()), + }; +}; diff --git a/test/helpers/index.ts b/test/helpers/index.ts index 3c64aadda8..71efcf002a 100644 --- a/test/helpers/index.ts +++ b/test/helpers/index.ts @@ -24,3 +24,5 @@ export * from "./voting"; export * from "./wormhole"; export * from "./xcm"; export * from "./tracingFns"; +export * from "./fees.ts"; +export * from "./parameters.ts"; diff --git a/test/helpers/parameters.ts b/test/helpers/parameters.ts new file mode 100644 index 0000000000..1bdb6d0c5c --- /dev/null +++ b/test/helpers/parameters.ts @@ -0,0 +1,14 @@ +import type { DevModeContext } from "@moonwall/cli"; + +export const getFeesTreasuryProportion = async (context: DevModeContext): Promise => { + const parameter = await context.polkadotJs().query.parameters.parameters({ + RuntimeConfig: "FeesTreasuryProportion", + }); + + // 20% default value + let feesTreasuryProportion = 200_000_000n; + if (parameter.isSome) { + feesTreasuryProportion = parameter.value.asRuntimeConfig.asFeesTreasuryProportion.toBigInt(); + } + return feesTreasuryProportion; +}; diff --git a/test/scripts/update-local-types.ts b/test/scripts/update-local-types.ts index 8e9945dfb2..d143bb741d 100644 --- a/test/scripts/update-local-types.ts +++ b/test/scripts/update-local-types.ts @@ -60,6 +60,7 @@ const startNode = (network: string, rpcPort: string, port: string) => { "--wasm-execution=interpreted-i-know-what-i-do", "--no-telemetry", "--no-prometheus", + "--rpc-cors=all", "--tmp", ], { @@ -107,7 +108,7 @@ const executeUpdateAPIScript = async () => { // Bundle types await executeScript("../../moonbeam-types-bundle", "pnpm i"); await executeScript("../../moonbeam-types-bundle", "pnpm build"); - await executeScript("../../moonbeam-types-bundle", "pnpm fmt:fix"); + await executeScript("../../moonbeam-types-bundle", "pnpm check:fix"); // Generate types console.log("Extracting metadata for all runtimes..."); diff --git a/test/suites/dev/moonbase/test-balance/test-balance-transfer.ts b/test/suites/dev/moonbase/test-balance/test-balance-transfer.ts index b4cc85ae45..13608adc3d 100644 --- a/test/suites/dev/moonbase/test-balance/test-balance-transfer.ts +++ b/test/suites/dev/moonbase/test-balance/test-balance-transfer.ts @@ -2,6 +2,7 @@ import "@moonbeam-network/api-augment"; import { beforeEach, describeSuite, expect } from "@moonwall/cli"; import { ALITH_ADDRESS, + CHARLETH_ADDRESS, BALTATHAR_ADDRESS, BALTATHAR_PRIVATE_KEY, CHARLETH_PRIVATE_KEY, @@ -77,16 +78,17 @@ describeSuite({ id: "T03", title: "should decrease from account", test: async function () { - const balanceBefore = await context.viem().getBalance({ address: ALITH_ADDRESS }); + const balanceBefore = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); const fees = 21000n * GENESIS_BASE_FEE; await context.createBlock( await createRawTransfer(context, randomAddress, 512n, { gas: 21000n, gasPrice: GENESIS_BASE_FEE, txnType: "legacy", + privateKey: CHARLETH_PRIVATE_KEY, }) ); - const balanceAfter = await context.viem().getBalance({ address: ALITH_ADDRESS }); + const balanceAfter = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); expect(balanceBefore - balanceAfter - fees).toBe(512n); }, }); @@ -190,7 +192,7 @@ describeSuite({ id: "T08", title: "should handle max_fee_per_gas", test: async function () { - const balanceBefore = await checkBalance(context); + const balanceBefore = await checkBalance(context, CHARLETH_ADDRESS); await context.createBlock( await createRawTransfer(context, randomAddress, 1n * GLMR, { gas: 21000n, @@ -198,9 +200,10 @@ describeSuite({ maxPriorityFeePerGas: parseGwei("0.2"), gasPrice: GENESIS_BASE_FEE, type: "eip1559", + privateKey: CHARLETH_PRIVATE_KEY, }) ); - const balanceAfter = await checkBalance(context); + const balanceAfter = await checkBalance(context, CHARLETH_ADDRESS); const fee = 21000n * GENESIS_BASE_FEE; expect(balanceAfter + fee + 1n * GLMR).toBe(balanceBefore); diff --git a/test/suites/dev/moonbase/test-contract/test-contract-error.ts b/test/suites/dev/moonbase/test-contract/test-contract-error.ts index 67c2563ccd..e886d92f1a 100644 --- a/test/suites/dev/moonbase/test-contract/test-contract-error.ts +++ b/test/suites/dev/moonbase/test-contract/test-contract-error.ts @@ -6,7 +6,12 @@ import { describeSuite, expect, } from "@moonwall/cli"; -import { ALITH_ADDRESS, createEthersTransaction } from "@moonwall/util"; +import { + CHARLETH_PRIVATE_KEY, + CHARLETH_ADDRESS, + createEthersTransaction, + ALITH_ADDRESS, +} from "@moonwall/util"; import { encodeFunctionData, type Abi } from "viem"; import { verifyLatestBlockFees } from "../../../../helpers"; @@ -31,10 +36,10 @@ describeSuite({ id: `T0${TransactionTypes.indexOf(txnType) + 1}`, title: `"should return OutOfGas on inifinite loop ${txnType} call`, test: async function () { - expect( + await expect( async () => await context.viem().call({ - account: ALITH_ADDRESS, + account: CHARLETH_ADDRESS, to: looperAddress, data: encodeFunctionData({ abi: looperAbi, functionName: "infinite", args: [] }), gas: 12_000_000n, @@ -45,16 +50,25 @@ describeSuite({ }); it({ - id: `T0${TransactionTypes.indexOf(txnType) * 2 + 1}`, + id: `T0${TransactionTypes.indexOf(txnType) + 1 + TransactionTypes.length}`, title: `should fail with OutOfGas on infinite loop ${txnType} transaction`, test: async function () { + const nonce = await context.viem().getTransactionCount({ address: CHARLETH_ADDRESS }); + const rawSigned = await createEthersTransaction(context, { to: looperAddress, data: encodeFunctionData({ abi: looperAbi, functionName: "infinite", args: [] }), txnType, + nonce, + privateKey: CHARLETH_PRIVATE_KEY, + }); + + const { result } = await context.createBlock(rawSigned, { + signer: { type: "ethereum", privateKey: CHARLETH_PRIVATE_KEY }, }); - const { result } = await context.createBlock(rawSigned); + expect(result.successful).to.be.true; + const receipt = await context .viem("public") .getTransactionReceipt({ hash: result!.hash as `0x${string}` }); @@ -63,16 +77,24 @@ describeSuite({ }); it({ - id: `T0${TransactionTypes.indexOf(txnType) * 3 + 1}`, + id: `T0${TransactionTypes.indexOf(txnType) + 1 + TransactionTypes.length * 2}`, title: `should fail with OutOfGas on infinite loop ${txnType} transaction - check fees`, test: async function () { + const nonce = await context.viem().getTransactionCount({ address: CHARLETH_ADDRESS }); + const rawSigned = await createEthersTransaction(context, { to: looperAddress, data: encodeFunctionData({ abi: looperAbi, functionName: "infinite", args: [] }), txnType, + nonce, + privateKey: CHARLETH_PRIVATE_KEY, + }); + + const { result } = await context.createBlock(rawSigned, { + signer: { type: "ethereum", privateKey: CHARLETH_PRIVATE_KEY }, }); - await context.createBlock(rawSigned); + expect(result.successful).to.be.true; await verifyLatestBlockFees(context); }, }); diff --git a/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-error.ts b/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-error.ts index dbb560dced..b026a5ca48 100644 --- a/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-error.ts +++ b/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-error.ts @@ -13,6 +13,7 @@ import { } from "@moonwall/util"; import { parseGwei } from "viem"; import { ALITH_GENESIS_TRANSFERABLE_BALANCE, ConstantStore } from "../../../../helpers"; +import { UNIT } from "../test-parameters/test-parameters"; describeSuite({ id: "D011102", @@ -155,8 +156,12 @@ describeSuite({ id: "T07", title: "insufficient funds for gas * price + value", test: async function () { - const amount = ALITH_GENESIS_TRANSFERABLE_BALANCE - 21000n * 10_000_000_000n + 1n; - const tx = await createRawTransfer(context, BALTATHAR_ADDRESS, amount); + const CHARLETH_GENESIS_TRANSFERABLE_BALANCE = + ALITH_GENESIS_TRANSFERABLE_BALANCE + 1000n * UNIT + 10n * 100_000_000_000_000n; + const amount = CHARLETH_GENESIS_TRANSFERABLE_BALANCE - 21000n * 10_000_000_000n + 1n; + const tx = await createRawTransfer(context, BALTATHAR_ADDRESS, amount, { + privateKey: CHARLETH_PRIVATE_KEY, + }); expect( async () => await customDevRpcRequest("eth_sendRawTransaction", [tx]) diff --git a/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-resubmit-txn.ts b/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-resubmit-txn.ts index b5ba7380dd..80f95da08c 100644 --- a/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-resubmit-txn.ts +++ b/test/suites/dev/moonbase/test-eth-pool/test-eth-pool-resubmit-txn.ts @@ -1,5 +1,11 @@ import { beforeEach, describeSuite, expect } from "@moonwall/cli"; -import { ALITH_ADDRESS, createRawTransfer, GLMR, sendRawTransaction } from "@moonwall/util"; +import { + CHARLETH_ADDRESS, + CHARLETH_PRIVATE_KEY, + createRawTransfer, + GLMR, + sendRawTransaction, +} from "@moonwall/util"; import { parseGwei } from "viem"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; @@ -10,10 +16,14 @@ describeSuite({ testCases: ({ context, it, log }) => { let randomAddress: `0x${string}`; let currentNonce: number; + let actorPrivateKey: `0x${string}`; + let actorAddress: `0x${string}`; beforeEach(async function () { + actorPrivateKey = CHARLETH_PRIVATE_KEY; + actorAddress = CHARLETH_ADDRESS; randomAddress = privateKeyToAccount(generatePrivateKey()).address; - currentNonce = await context.viem().getTransactionCount({ address: ALITH_ADDRESS }); + currentNonce = await context.viem().getTransactionCount({ address: actorAddress }); }); it({ @@ -25,11 +35,13 @@ describeSuite({ nonce: currentNonce, maxFeePerGas: parseGwei("300"), maxPriorityFeePerGas: parseGwei("300"), + privateKey: actorPrivateKey, }), await createRawTransfer(context, randomAddress, 2, { nonce: currentNonce, maxFeePerGas: parseGwei("400"), maxPriorityFeePerGas: parseGwei("300"), + privateKey: actorPrivateKey, // same priority fee but higher max fee so higher tip }), ]); @@ -45,10 +57,12 @@ describeSuite({ await createRawTransfer(context, randomAddress, 1, { nonce: currentNonce, maxFeePerGas: parseGwei("20"), + privateKey: actorPrivateKey, }), await createRawTransfer(context, randomAddress, 2, { nonce: currentNonce, maxFeePerGas: parseGwei("10"), + privateKey: actorPrivateKey, }), ]); expect(await context.viem().getBalance({ address: randomAddress })).to.equal(1n); @@ -65,11 +79,13 @@ describeSuite({ nonce: currentNonce, maxFeePerGas: parseGwei("10"), gas: 1048575n, + privateKey: actorPrivateKey, }), await createRawTransfer(context, randomAddress, 2, { nonce: currentNonce, maxFeePerGas: parseGwei("20"), gas: 65536n, + privateKey: actorPrivateKey, }), ]); @@ -82,13 +98,14 @@ describeSuite({ title: "should prioritize higher gas tips", test: async function () { // GasFee are using very high value to ensure gasPrice is not impacting - const alithGLMR = (await context.viem().getBalance({ address: ALITH_ADDRESS })) / GLMR; + const addressGLMR = (await context.viem().getBalance({ address: actorAddress })) / GLMR; await sendRawTransaction( context, await createRawTransfer(context, randomAddress, 66, { nonce: currentNonce, maxFeePerGas: 1n * GLMR, maxPriorityFeePerGas: 1n * GLMR, + privateKey: actorPrivateKey, }) ); @@ -100,14 +117,15 @@ describeSuite({ nonce: currentNonce, maxFeePerGas: gasPrice, maxPriorityFeePerGas: gasPrice, + privateKey: actorPrivateKey, }) ) ); await context.createBlock(txns); - expect((await context.viem().getBalance({ address: ALITH_ADDRESS })) / GLMR).to.equal( - alithGLMR - 21000n * 20n + expect((await context.viem().getBalance({ address: actorAddress })) / GLMR).to.equal( + addressGLMR - 21000n * 20n ); expect(await context.viem().getBalance({ address: randomAddress })).to.equal(77n); }, @@ -122,11 +140,13 @@ describeSuite({ nonce: currentNonce, maxFeePerGas: parseGwei("300"), maxPriorityFeePerGas: parseGwei("10"), + privateKey: actorPrivateKey, }), await createRawTransfer(context, randomAddress, 2, { nonce: currentNonce, maxFeePerGas: parseGwei("400"), maxPriorityFeePerGas: parseGwei("10"), + privateKey: actorPrivateKey, }), ]); expect(await context.viem().getBalance({ address: randomAddress })).to.equal(1n); diff --git a/test/suites/dev/moonbase/test-fees/test-fees-repartition.ts b/test/suites/dev/moonbase/test-fees/test-fees-repartition.ts index 81ee135eed..cd6047c78c 100644 --- a/test/suites/dev/moonbase/test-fees/test-fees-repartition.ts +++ b/test/suites/dev/moonbase/test-fees/test-fees-repartition.ts @@ -2,6 +2,7 @@ import "@moonbeam-network/api-augment"; import { TransactionTypes, describeSuite, expect } from "@moonwall/cli"; import { BALTATHAR_ADDRESS, TREASURY_ACCOUNT, createRawTransfer, extractFee } from "@moonwall/util"; +// These tests are checking the default value of FeesTreasuryProportion which is set to 20% describeSuite({ id: "D011605", title: "Fees - Transaction", diff --git a/test/suites/dev/moonbase/test-parameters/test-parameters-runtime-FeesTreasuryProportion.ts b/test/suites/dev/moonbase/test-parameters/test-parameters-runtime-FeesTreasuryProportion.ts index 7b9a5b017a..bc3b13b4ff 100644 --- a/test/suites/dev/moonbase/test-parameters/test-parameters-runtime-FeesTreasuryProportion.ts +++ b/test/suites/dev/moonbase/test-parameters/test-parameters-runtime-FeesTreasuryProportion.ts @@ -1,176 +1,223 @@ -import { describeSuite, expect, TransactionTypes } from "@moonwall/cli"; +import { describeSuite, expect, extractInfo, TransactionTypes } from "@moonwall/cli"; import { alith, + ALITH_ADDRESS, baltathar, - BALTATHAR_ADDRESS, + BALTATHAR_PRIVATE_KEY, + CHARLETH_ADDRESS, createRawTransfer, extractFee, Perbill, TREASURY_ACCOUNT, + WEIGHT_PER_GAS, } from "@moonwall/util"; import { parameterType, UNIT } from "./test-parameters"; import { BN } from "@polkadot/util"; +import { calculateFeePortions, ConstantStore, verifyLatestBlockFees } from "../../../../helpers"; +import { parseGwei } from "viem"; interface TestCase { proportion: Perbill; transfer_amount: bigint; tipAmount: bigint; + priorityFeePerGas: bigint; } -// Recreation on fees.ration(burn_part, treasury_part) -const split = (value: BN, part1: BN, part2: BN): [BN, BN] => { - const total = part1.add(part2); - if (total.eq(new BN(0)) || value.eq(new BN(0))) { - return [new BN(0), new BN(0)]; - } - const part1BN = value.mul(part1).div(total); - const part2BN = value.sub(part1BN); - return [part1BN, part2BN]; -}; - describeSuite({ id: "DTemp03", title: "Parameters - RuntimeConfig", foundationMethods: "dev", testCases: ({ it, context, log }) => { let testCounter = 0; + const collatorAddress = ALITH_ADDRESS; + const senderPrivateKey = BALTATHAR_PRIVATE_KEY; + const senderKeyPair = baltathar; + const receiver = CHARLETH_ADDRESS; const testCases: TestCase[] = [ { proportion: new Perbill(0), transfer_amount: 10n * UNIT, tipAmount: 1n * UNIT, + priorityFeePerGas: parseGwei("1"), }, { proportion: new Perbill(1, 100), transfer_amount: 1000n, tipAmount: 100n, + priorityFeePerGas: 100n, }, { proportion: new Perbill(355, 1000), transfer_amount: 5n * UNIT, tipAmount: 111112n, + priorityFeePerGas: 111112n, }, { proportion: new Perbill(400, 1000), transfer_amount: 10n * UNIT, - tipAmount: 1n * UNIT, + tipAmount: 2n * UNIT, + priorityFeePerGas: parseGwei("2"), }, { proportion: new Perbill(500, 1000), transfer_amount: 10n * UNIT, tipAmount: 1n * UNIT, + priorityFeePerGas: parseGwei("1"), }, { proportion: new Perbill(963, 1000), transfer_amount: 10n * UNIT, tipAmount: 128n, + priorityFeePerGas: 128, }, { proportion: new Perbill(99, 100), transfer_amount: 10n * UNIT, tipAmount: 3n, + priorityFeePerGas: 3n, }, { proportion: new Perbill(1, 1), transfer_amount: 10n * UNIT, tipAmount: 32n, + priorityFeePerGas: 32n, }, ]; for (const t of testCases) { - const burnProportion = new Perbill(new BN(1e9).sub(t.proportion.value())); - + const treasuryPerbill = new BN(t.proportion.value()); const treasuryPercentage = t.proportion.value().toNumber() / 1e7; - const burnPercentage = burnProportion.value().toNumber() / 1e7; + const burnPercentage = 100 - treasuryPercentage; const calcTreasuryIncrease = (feeWithTip: bigint, tip?: bigint): bigint => { const issuanceDecrease = calcIssuanceDecrease(feeWithTip, tip); const treasuryIncrease = feeWithTip - issuanceDecrease; return treasuryIncrease; }; - const calcIssuanceDecrease = (feeWithTip: bigint, tip?: bigint): bigint => { - const feeWithTipBN = new BN(feeWithTip.toString()); - const tipBN = new BN(tip?.toString() || "0"); - const feeWithoutTipBN = feeWithTipBN.sub(tipBN); - - const [burnFeePart, _treasuryFeePart] = split( - feeWithoutTipBN, - burnProportion.value(), - t.proportion.value() - ); - const [burnTipPart, _treasuryTipPart] = split( - tipBN, - burnProportion.value(), - t.proportion.value() - ); - - return BigInt(burnFeePart.add(burnTipPart).toString()); + const calcIssuanceDecrease = (feeWithTip: bigint, maybeTip?: bigint): bigint => { + const tip = maybeTip ?? 0n; + const feeWithoutTip = feeWithTip - tip; + const { burnt: feeBurnt } = calculateFeePortions(treasuryPerbill, feeWithoutTip); + const { burnt: tipBurnt } = calculateFeePortions(treasuryPerbill, tip); + + return feeBurnt + tipBurnt; }; for (const txnType of TransactionTypes) { - it({ - id: `T${++testCounter}`, - title: - `Changing FeesTreasuryProportion to ${treasuryPercentage}% for Ethereum ` + - `${txnType} transfers`, - test: async () => { - const param = parameterType( - context, - "RuntimeConfig", - "FeesTreasuryProportion", - t.proportion.value() - ); - await context.createBlock( - context - .polkadotJs() - .tx.sudo.sudo(context.polkadotJs().tx.parameters.setParameter(param.toU8a())) - .signAsync(alith), - { allowFailures: false } - ); + for (const withTip of txnType === "eip1559" ? [false, true] : [false]) { + testCounter++; + it({ + id: `T${testCounter}x`, + title: + `Changing FeesTreasuryProportion to ${treasuryPercentage}% for Ethereum ` + + `${txnType} transfers with ${withTip ? "" : "no "}tip`, + test: async () => { + const { specVersion } = context.polkadotJs().consts.system.version; + const GENESIS_BASE_FEE = ConstantStore(context).GENESIS_BASE_FEE.get( + specVersion.toNumber() + ); + const WEIGHT_FEE = ConstantStore(context).WEIGHT_FEE.get(specVersion.toNumber()); - const balBefore = await context.viem().getBalance({ address: TREASURY_ACCOUNT }); - const issuanceBefore = ( - await context.polkadotJs().query.balances.totalIssuance() - ).toBigInt(); - const { result } = await context.createBlock( - await createRawTransfer(context, BALTATHAR_ADDRESS, t.transfer_amount, { - type: txnType, - }) - ); + const param = parameterType( + context, + "RuntimeConfig", + "FeesTreasuryProportion", + t.proportion.value() + ); + await context.createBlock( + context + .polkadotJs() + .tx.sudo.sudo(context.polkadotJs().tx.parameters.setParameter(param.toU8a())) + .signAsync(alith), + { allowFailures: false } + ); - const balAfter = await context.viem().getBalance({ address: TREASURY_ACCOUNT }); - const issuanceAfter = ( - await context.polkadotJs().query.balances.totalIssuance() - ).toBigInt(); + const balBefore = await context.viem().getBalance({ address: TREASURY_ACCOUNT }); + const collatorBalBefore = await context + .viem() + .getBalance({ address: collatorAddress }); + const issuanceBefore = ( + await context.polkadotJs().query.balances.totalIssuance() + ).toBigInt(); - const treasuryIncrease = balAfter - balBefore; - const issuanceDecrease = issuanceBefore - issuanceAfter; - const fee = extractFee(result?.events)!.amount.toBigInt(); + const nextFeeMultiplier = ( + await context.polkadotJs().query.transactionPayment.nextFeeMultiplier() + ).toBigInt(); + const baseFee = + (nextFeeMultiplier * (WEIGHT_FEE * WEIGHT_PER_GAS)) / 1_000_000_000_000_000_000n; - expect( - treasuryIncrease + issuanceDecrease, - `Sum of TreasuryIncrease and IssuanceDecrease should be equal to the fees` - ).to.equal(fee); + const { result } = await context.createBlock( + await createRawTransfer(context, receiver, t.transfer_amount, { + privateKey: senderPrivateKey, + type: txnType, + maxFeePerGas: withTip ? GENESIS_BASE_FEE : undefined, + maxPriorityFeePerGas: withTip ? t.priorityFeePerGas : undefined, + }) + ); - expect( - treasuryIncrease, - `${treasuryPercentage}% of the fees should go to treasury` - ).to.equal(calcTreasuryIncrease(fee)); + const receipt = await context + .viem("public") + .getTransactionReceipt({ hash: result!.hash as `0x${string}` }); - expect(issuanceDecrease, `${burnPercentage}% of the fees should be burned`).to.equal( - calcIssuanceDecrease(fee) - ); - }, - }); + const balAfter = await context.viem().getBalance({ address: TREASURY_ACCOUNT }); + const collatorBalAfter = await context + .viem() + .getBalance({ address: collatorAddress }); + const issuanceAfter = ( + await context.polkadotJs().query.balances.totalIssuance() + ).toBigInt(); + + const treasuryIncrease = balAfter - balBefore; + const issuanceDecrease = issuanceBefore - issuanceAfter; + const collatorIncrease = collatorBalAfter - collatorBalBefore; + const tipPaid = withTip + ? (() => { + let priorityPerGas = GENESIS_BASE_FEE - baseFee; + if (priorityPerGas > t.priorityFeePerGas) { + priorityPerGas = t.priorityFeePerGas; + } + return BigInt(priorityPerGas) * BigInt(receipt!.gasUsed); + })() + : 0n; + const fee = extractFee(result?.events)!.amount.toBigInt(); + const feeWithoutTip = fee - tipPaid; + + expect( + treasuryIncrease + issuanceDecrease, + `Sum of TreasuryIncrease and IssuanceDecrease should be equal to the fees without tip` + ).to.equal(feeWithoutTip); + + expect( + treasuryIncrease, + `${treasuryPercentage}% of the fees should go to treasury` + ).to.equal(calcTreasuryIncrease(feeWithoutTip)); + + expect(issuanceDecrease, `${burnPercentage}% of the fees should be burned`).to.equal( + calcIssuanceDecrease(feeWithoutTip) + ); + + if (withTip) { + expect(collatorIncrease, "100% of the tip should go to the collator").to.equal( + tipPaid + ); + } else { + expect(collatorIncrease, "No tip should be paid to the collator").to.equal(0n); + } + + await verifyLatestBlockFees(context, t.transfer_amount); + }, + }); + } } for (const withTip of [false, true]) { + testCounter++; it({ - id: `T${++testCounter}`, + id: `T${testCounter}x`, title: - `Changing FeesTreasuryProportion to ${treasuryPercentage}% for Substrate based` + + `Changing FeesTreasuryProportion to ${treasuryPercentage}% for Substrate based ` + `transactions with ${withTip ? "" : "no "}tip`, test: async () => { const param = parameterType( @@ -191,12 +238,13 @@ describeSuite({ const issuanceBefore = ( await context.polkadotJs().query.balances.totalIssuance() ).toBigInt(); + const collatorBalBefore = await context.viem().getBalance({ address: collatorAddress }); const { result } = await context.createBlock( context .polkadotJs() - .tx.balances.transferKeepAlive(alith.address, t.transfer_amount) - .signAsync(baltathar, { tip: withTip ? t.tipAmount : 0n }), + .tx.balances.transferKeepAlive(receiver, t.transfer_amount) + .signAsync(senderKeyPair, { tip: withTip ? t.tipAmount : 0n }), { allowFailures: false } ); @@ -204,24 +252,37 @@ describeSuite({ const issuanceAfter = ( await context.polkadotJs().query.balances.totalIssuance() ).toBigInt(); + const collatorBalAfter = await context.viem().getBalance({ address: collatorAddress }); const treasuryIncrease = balanceAfter - balanceBefore; const issuanceDecrease = issuanceBefore - issuanceAfter; - const fee = extractFee(result?.events)!.amount.toBigInt(); + const collatorIncrease = collatorBalAfter - collatorBalBefore; + const tipPaid = withTip ? t.tipAmount : 0n; + const feeWithoutTip = extractFee(result?.events)!.amount.toBigInt() - tipPaid; expect( treasuryIncrease + issuanceDecrease, - `Sum of TreasuryIncrease and IssuanceDecrease should be equal to the fees` - ).to.equal(fee); + `Sum of TreasuryIncrease and IssuanceDecrease should be equal to the fees without tip` + ).to.equal(feeWithoutTip); expect( treasuryIncrease, `${treasuryPercentage}% of the fees should go to treasury` - ).to.equal(calcTreasuryIncrease(fee, withTip ? t.tipAmount : undefined)); + ).to.equal(calcTreasuryIncrease(feeWithoutTip)); expect(issuanceDecrease, `${burnPercentage}% of the fees should be burned`).to.equal( - calcIssuanceDecrease(fee, withTip ? t.tipAmount : undefined) + calcIssuanceDecrease(feeWithoutTip) ); + + if (withTip) { + expect(collatorIncrease, "100% of the tip should go to the collator").to.equal( + t.tipAmount + ); + } else { + expect(collatorIncrease, "No tip should be paid to the collator").to.equal(0n); + } + + await verifyLatestBlockFees(context, t.transfer_amount); }, }); } diff --git a/test/suites/dev/moonbase/test-precompile/test-precompile-xtokens.ts b/test/suites/dev/moonbase/test-precompile/test-precompile-xtokens.ts index db14721c5d..89fe34d91f 100644 --- a/test/suites/dev/moonbase/test-precompile/test-precompile-xtokens.ts +++ b/test/suites/dev/moonbase/test-precompile/test-precompile-xtokens.ts @@ -1,6 +1,13 @@ import "@moonbeam-network/api-augment"; import { beforeAll, describeSuite, expect, fetchCompiledContract } from "@moonwall/cli"; -import { ALITH_ADDRESS, GLMR, PRECOMPILES, createViemTransaction } from "@moonwall/util"; +import { + ALITH_ADDRESS, + GLMR, + PRECOMPILES, + createViemTransaction, + CHARLETH_ADDRESS, + CHARLETH_PRIVATE_KEY, +} from "@moonwall/util"; import { verifyLatestBlockFees, expectEVMResult, @@ -214,7 +221,7 @@ describeSuite({ const fee_item = 0; const weight = 100; - const balBefore = await context.viem().getBalance({ address: ALITH_ADDRESS }); + const balBefore = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); const { abi } = fetchCompiledContract("Xtokens"); const data = encodeFunctionData({ abi, @@ -229,10 +236,11 @@ describeSuite({ txnType: "legacy", gas: 500_000n, gasPrice: BigInt(DEFAULT_TXN_MAX_BASE_FEE), + privateKey: CHARLETH_PRIVATE_KEY, }); const { result } = await context.createBlock(rawTxn); - const balAfter = await context.viem().getBalance({ address: ALITH_ADDRESS }); + const balAfter = await context.viem().getBalance({ address: CHARLETH_ADDRESS }); const receipt = await context .viem() .getTransactionReceipt({ hash: result!.hash as `0x${string}` });