diff --git a/primitives/src/evm.rs b/primitives/src/evm.rs index 2a4d848c4b..29874ab19d 100644 --- a/primitives/src/evm.rs +++ b/primitives/src/evm.rs @@ -18,11 +18,18 @@ use crate::{AccountId, AssetId}; -use frame_support::ensure; -use pallet_evm::{AddressMapping, HashedAddressMapping}; +use frame_support::{ + ensure, + traits::{ + fungible::{Balanced, Credit}, + tokens::{fungible::Inspect, imbalance::OnUnbalanced}, + }, +}; +use pallet_evm::{AddressMapping, HashedAddressMapping, OnChargeEVMTransaction}; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; -use sp_core::{Hasher, H160, H256}; +use sp_core::{Hasher, H160, H256, U256}; +use sp_runtime::traits::UniqueSaturatedInto; use sp_std::marker::PhantomData; use pallet_assets::AssetsCallback; @@ -139,3 +146,52 @@ impl
UnifiedAddress
{ } } } + +/// Wrapper around the `EvmFungibleAdapter` from the `pallet-evm`. +/// +/// While it provides most of the functionality we need, +/// it doesn't allow the tip to be deposited into an arbitrary account. +/// This adapter allows us to do that. +/// +/// Two separate `OnUnbalanced` handers are used: +/// - `UOF` for the fee +/// - `OUT` for the tip +pub struct EVMFungibleAdapterWrapper( + core::marker::PhantomData<(F, FeeHandler, TipHandler)>, +); +impl OnChargeEVMTransaction + for EVMFungibleAdapterWrapper +where + T: pallet_evm::Config, + F: Balanced, + FeeHandler: OnUnbalanced>, + TipHandler: OnUnbalanced>, + U256: UniqueSaturatedInto<::AccountId>>::Balance>, +{ + // Kept type as Option to satisfy bound of Default + type LiquidityInfo = Option>; + + fn withdraw_fee(who: &H160, fee: U256) -> Result> { + pallet_evm::EVMFungibleAdapter::::withdraw_fee(who, fee) + } + + fn correct_and_deposit_fee( + who: &H160, + corrected_fee: U256, + base_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Self::LiquidityInfo { + as OnChargeEVMTransaction>::correct_and_deposit_fee( + who, + corrected_fee, + base_fee, + already_withdrawn, + ) + } + + fn pay_priority_fee(tip: Self::LiquidityInfo) { + if let Some(tip) = tip { + TipHandler::on_unbalanceds(Some(tip).into_iter()); + } + } +} diff --git a/runtime/astar/src/lib.rs b/runtime/astar/src/lib.rs index 17a6e0f090..1fad5c74b3 100644 --- a/runtime/astar/src/lib.rs +++ b/runtime/astar/src/lib.rs @@ -84,7 +84,7 @@ use astar_primitives::{ AccountCheck as DappStakingAccountCheck, CycleConfiguration, DAppId, EraNumber, PeriodNumber, RankedTier, SmartContract, StandardTierSlots, }, - evm::EvmRevertCodeHandler, + evm::{EVMFungibleAdapterWrapper, EvmRevertCodeHandler}, governance::{ CommunityCouncilCollectiveInst, CommunityCouncilMembershipInst, CommunityTreasuryInst, EnsureRootOrAllMainCouncil, EnsureRootOrAllTechnicalCommittee, @@ -474,7 +474,7 @@ impl pallet_inflation::PayoutPerBlock> for Inflation } fn collators(reward: Credit) { - ToStakingPot::on_unbalanced(reward); + CollatorRewardPot::on_unbalanced(reward); } } @@ -614,8 +614,8 @@ parameter_types! { pub TreasuryAccountId: AccountId = TreasuryPalletId::get().into_account_truncating(); } -pub struct ToStakingPot; -impl OnUnbalanced> for ToStakingPot { +pub struct CollatorRewardPot; +impl OnUnbalanced> for CollatorRewardPot { fn on_nonzero_unbalanced(amount: Credit) { let staking_pot = PotId::get().into_account_truncating(); let _ = Balances::resolve(&staking_pot, amount); @@ -838,9 +838,13 @@ impl OnUnbalanced> for DealWithFees { drop(to_burn); // pay fees to collator - >::on_unbalanced(collator); + >::on_unbalanced(collator); } } + + fn on_unbalanced(amount: Credit) { + Self::on_unbalanceds(Some(amount).into_iter()); + } } impl pallet_transaction_payment::Config for Runtime { @@ -946,7 +950,7 @@ impl pallet_evm::Config for Runtime { type PrecompilesType = Precompiles; type PrecompilesValue = PrecompilesValue; type ChainId = ChainId; - type OnChargeTransaction = pallet_evm::EVMFungibleAdapter; + type OnChargeTransaction = EVMFungibleAdapterWrapper; type BlockGasLimit = BlockGasLimit; type Timestamp = Timestamp; type OnCreate = (); @@ -1026,7 +1030,7 @@ pub enum ProxyType { /// Allows all runtime calls for proxy account Any, /// Allows only NonTransfer runtime calls for proxy account - /// To know exact calls check InstanceFilter inmplementation for ProxyTypes + /// To know exact calls check InstanceFilter implementation for ProxyTypes NonTransfer, /// All Runtime calls from Pallet Balances allowed for proxy account Balances, @@ -1062,26 +1066,15 @@ impl InstanceFilter for ProxyType { c, RuntimeCall::System(..) | RuntimeCall::Identity(..) - | RuntimeCall::Timestamp(..) | RuntimeCall::Multisig(..) | RuntimeCall::Proxy(..) - | RuntimeCall::ParachainSystem(..) - | RuntimeCall::ParachainInfo(..) - // Skip entire Balances pallet - | RuntimeCall::Vesting(pallet_vesting::Call::vest{..}) - | RuntimeCall::Vesting(pallet_vesting::Call::vest_other{..}) - // Specifically omitting Vesting `vested_transfer`, and `force_vested_transfer` + | RuntimeCall::Vesting( + pallet_vesting::Call::vest { .. } + | pallet_vesting::Call::vest_other { .. } + ) | RuntimeCall::DappStaking(..) - // Skip entire Assets pallet | RuntimeCall::CollatorSelection(..) - | RuntimeCall::Session(..) - | RuntimeCall::XcmpQueue(..) - | RuntimeCall::PolkadotXcm(..) - | RuntimeCall::CumulusXcm(..) | RuntimeCall::XcAssetConfig(..) - // Skip entire EVM pallet - // Skip entire Ethereum pallet - | RuntimeCall::DynamicEvmBaseFee(..) // Skip entire Contracts pallet ) } ProxyType::Balances => { diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index b290d574a3..e75c009ec3 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -811,18 +811,12 @@ impl InstanceFilter for ProxyType { matches!( c, RuntimeCall::System(..) - | RuntimeCall::Timestamp(..) - | RuntimeCall::Scheduler(..) | RuntimeCall::Proxy(..) - | RuntimeCall::Grandpa(..) - // Skip entire Balances pallet - | RuntimeCall::Vesting(pallet_vesting::Call::vest{..}) - | RuntimeCall::Vesting(pallet_vesting::Call::vest_other{..}) - // Specifically omitting Vesting `vested_transfer`, and `force_vested_transfer` + | RuntimeCall::Vesting( + pallet_vesting::Call::vest { .. } + | pallet_vesting::Call::vest_other { .. } + ) | RuntimeCall::DappStaking(..) - // Skip entire EVM pallet - // Skip entire Ethereum pallet - | RuntimeCall::DynamicEvmBaseFee(..) // Skip entire Contracts pallet ) } // All Runtime calls from Pallet Balances allowed for proxy account diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 9a4b1fffcb..7b8a463900 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -83,7 +83,7 @@ use astar_primitives::{ AccountCheck as DappStakingAccountCheck, CycleConfiguration, DAppId, EraNumber, PeriodNumber, RankedTier, SmartContract, StandardTierSlots, }, - evm::{EvmRevertCodeHandler, HashedDefaultMappings}, + evm::{EVMFungibleAdapterWrapper, EvmRevertCodeHandler, HashedDefaultMappings}, governance::{ CommunityCouncilCollectiveInst, CommunityCouncilMembershipInst, CommunityTreasuryInst, EnsureRootOrAllMainCouncil, EnsureRootOrAllTechnicalCommittee, @@ -497,7 +497,7 @@ impl pallet_inflation::PayoutPerBlock> for Inflation } fn collators(reward: Credit) { - ToStakingPot::on_unbalanced(reward); + CollatorRewardPot::on_unbalanced(reward); } } @@ -638,8 +638,8 @@ parameter_types! { pub TreasuryAccountId: AccountId = TreasuryPalletId::get().into_account_truncating(); } -pub struct ToStakingPot; -impl OnUnbalanced> for ToStakingPot { +pub struct CollatorRewardPot; +impl OnUnbalanced> for CollatorRewardPot { fn on_nonzero_unbalanced(amount: Credit) { let staking_pot = PotId::get().into_account_truncating(); let _ = Balances::resolve(&staking_pot, amount); @@ -834,9 +834,13 @@ impl OnUnbalanced> for DealWithFees { drop(to_burn); // pay fees to collator - >::on_unbalanced(collator); + >::on_unbalanced(collator); } } + + fn on_unbalanced(amount: Credit) { + Self::on_unbalanceds(Some(amount).into_iter()); + } } impl pallet_transaction_payment::Config for Runtime { @@ -947,7 +951,7 @@ impl pallet_evm::Config for Runtime { // Ethereum-compatible chain_id: // * Shibuya: 81 type ChainId = EVMChainId; - type OnChargeTransaction = pallet_evm::EVMFungibleAdapter; + type OnChargeTransaction = EVMFungibleAdapterWrapper; type BlockGasLimit = BlockGasLimit; type Timestamp = Timestamp; type OnCreate = (); @@ -1042,27 +1046,14 @@ impl InstanceFilter for ProxyType { c, RuntimeCall::System(..) | RuntimeCall::Identity(..) - | RuntimeCall::Timestamp(..) | RuntimeCall::Multisig(..) - | RuntimeCall::Scheduler(..) | RuntimeCall::Proxy(..) - | RuntimeCall::ParachainSystem(..) - | RuntimeCall::ParachainInfo(..) - // Skip entire Balances pallet - | RuntimeCall::Vesting(pallet_vesting::Call::vest{..}) - | RuntimeCall::Vesting(pallet_vesting::Call::vest_other{..}) - // Specifically omitting Vesting `vested_transfer`, and `force_vested_transfer` + | RuntimeCall::Vesting( + pallet_vesting::Call::vest { .. } + | pallet_vesting::Call::vest_other { .. } + ) | RuntimeCall::DappStaking(..) - // Skip entire Assets pallet | RuntimeCall::CollatorSelection(..) - | RuntimeCall::Session(..) - | RuntimeCall::XcmpQueue(..) - | RuntimeCall::PolkadotXcm(..) - | RuntimeCall::CumulusXcm(..) - | RuntimeCall::XcAssetConfig(..) - // Skip entire EVM pallet - // Skip entire Ethereum pallet - | RuntimeCall::DynamicEvmBaseFee(..) // Skip entire Contracts pallet ) } // All Runtime calls from Pallet Balances allowed for proxy account diff --git a/runtime/shiden/src/lib.rs b/runtime/shiden/src/lib.rs index c6165decf7..a37b1201c0 100644 --- a/runtime/shiden/src/lib.rs +++ b/runtime/shiden/src/lib.rs @@ -80,7 +80,7 @@ use astar_primitives::{ AccountCheck as DappStakingAccountCheck, CycleConfiguration, DAppId, EraNumber, PeriodNumber, RankedTier, SmartContract, TierSlots as TierSlotsFunc, }, - evm::EvmRevertCodeHandler, + evm::{EVMFungibleAdapterWrapper, EvmRevertCodeHandler}, governance::OracleMembershipInst, oracle::{CurrencyAmount, CurrencyId, DummyCombineData, Price}, xcm::AssetLocationIdConverter, @@ -462,7 +462,7 @@ impl pallet_inflation::PayoutPerBlock> for Inflation } fn collators(reward: Credit) { - ToStakingPot::on_unbalanced(reward); + CollatorRewardPot::on_unbalanced(reward); } } @@ -601,8 +601,8 @@ parameter_types! { pub TreasuryAccountId: AccountId = TreasuryPalletId::get().into_account_truncating(); } -pub struct ToStakingPot; -impl OnUnbalanced> for ToStakingPot { +pub struct CollatorRewardPot; +impl OnUnbalanced> for CollatorRewardPot { fn on_nonzero_unbalanced(amount: Credit) { let staking_pot = PotId::get().into_account_truncating(); let _ = Balances::resolve(&staking_pot, amount); @@ -813,9 +813,13 @@ impl OnUnbalanced> for DealWithFees { drop(to_burn); // pay fees to collator - >::on_unbalanced(collator); + >::on_unbalanced(collator); } } + + fn on_unbalanced(amount: Credit) { + Self::on_unbalanceds(Some(amount).into_iter()); + } } impl pallet_transaction_payment::Config for Runtime { @@ -920,7 +924,7 @@ impl pallet_evm::Config for Runtime { type PrecompilesType = Precompiles; type PrecompilesValue = PrecompilesValue; type ChainId = ChainId; - type OnChargeTransaction = pallet_evm::EVMFungibleAdapter; + type OnChargeTransaction = EVMFungibleAdapterWrapper; type BlockGasLimit = BlockGasLimit; type Timestamp = Timestamp; type OnCreate = (); @@ -1007,26 +1011,14 @@ impl InstanceFilter for ProxyType { c, RuntimeCall::System(..) | RuntimeCall::Identity(..) - | RuntimeCall::Timestamp(..) | RuntimeCall::Multisig(..) | RuntimeCall::Proxy(..) - | RuntimeCall::ParachainSystem(..) - | RuntimeCall::ParachainInfo(..) - // Skip entire Balances pallet - | RuntimeCall::Vesting(pallet_vesting::Call::vest{..}) - | RuntimeCall::Vesting(pallet_vesting::Call::vest_other{..}) - // Specifically omitting Vesting `vested_transfer`, and `force_vested_transfer` + | RuntimeCall::Vesting( + pallet_vesting::Call::vest { .. } + | pallet_vesting::Call::vest_other { .. } + ) | RuntimeCall::DappStaking(..) - // Skip entire Assets pallet | RuntimeCall::CollatorSelection(..) - | RuntimeCall::Session(..) - | RuntimeCall::XcmpQueue(..) - | RuntimeCall::PolkadotXcm(..) - | RuntimeCall::CumulusXcm(..) - | RuntimeCall::XcAssetConfig(..) - // Skip entire EVM pallet - // Skip entire Ethereum pallet - | RuntimeCall::DynamicEvmBaseFee(..) // Skip entire Contracts pallet ) } ProxyType::Balances => { diff --git a/tests/integration/src/fees.rs b/tests/integration/src/fees.rs new file mode 100644 index 0000000000..0befc3a6aa --- /dev/null +++ b/tests/integration/src/fees.rs @@ -0,0 +1,86 @@ +// This file is part of Astar. + +// Copyright (C) Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use crate::setup::*; + +use frame_support::traits::Currency; +use pallet_evm::{AddressMapping, OnChargeEVMTransaction}; +use sp_core::{H160, U256}; +use sp_runtime::traits::AccountIdConversion; + +#[test] +fn evm_fees_work() { + new_test_ext().execute_with(|| { + let address = H160::repeat_byte(0xbe); + let mapped_address = + ::AddressMapping::into_account_id(address); + Balances::make_free_balance_be(&mapped_address, 1_000_000_000_000_000); + + type EvmFeeHandler = ::OnChargeTransaction; + + // 0. Define init values + let (base_fee, tip, init_fee) = (500, 100, 1000); + let corrected_fee = base_fee + tip; + + let pot_account = PotId::get().into_account_truncating(); + let init_reward_pot = Balances::free_balance(&pot_account); + let init_total_issuance = Balances::total_issuance(); + + // 1. Withdraw some init fee + let result = >::withdraw_fee( + &address, + U256::from(init_fee), + ); + let already_withdrawn = result.expect("Account is funded, must succeed."); + + // 2. Correct the charged fee + let calculated_tip = + >::correct_and_deposit_fee( + &address, + U256::from(corrected_fee), + U256::from(base_fee), + already_withdrawn, + ); + assert!(calculated_tip.is_some()); + + // The expectation is that 20% of the fee was deposited into the reward pot, and the rest was burned. + assert_eq!( + init_reward_pot + base_fee / 5, + Balances::free_balance(&pot_account) + ); + assert_eq!( + init_total_issuance - base_fee / 5 * 4, + Balances::total_issuance() + ); + + // 3. Deposit the tip + let issuance = Balances::total_issuance(); + let pot = Balances::free_balance(&pot_account); + >::pay_priority_fee(calculated_tip); + assert_eq!( + issuance, + Balances::total_issuance(), + "Total issuance should not change since tip isn't burned." + ); + assert_eq!( + pot + tip, + Balances::free_balance(&pot_account), + "Pot should increase by the tip amount." + ); + }) +} diff --git a/tests/integration/src/lib.rs b/tests/integration/src/lib.rs index f4dfb40e88..198888dac5 100644 --- a/tests/integration/src/lib.rs +++ b/tests/integration/src/lib.rs @@ -52,3 +52,6 @@ mod governance; #[cfg(any(feature = "shibuya", feature = "shiden", feature = "astar"))] mod xcm_api; + +#[cfg(any(feature = "shibuya", feature = "shiden", feature = "astar"))] +mod fees;