From 42de7729389beac65879147959396e90eb68c12e Mon Sep 17 00:00:00 2001 From: Lovesh Harchandani Date: Sun, 7 Mar 2021 12:07:36 +0530 Subject: [PATCH] Allow handling fee deductions done by EVM (#307) * Allow handling fee deductions done by EVM Projects using `pallet_ethereum` need a way to know about fee deducted by EVM to take appropriate action on the fee like giving it to the miner. This adds a type `OnChargeTransaction` to pallet's `Config` trait similar to `OnChargeTransaction` of `pallet_transaction_payment` Signed-off-by: lovesh * Fix formatting Signed-off-by: lovesh * Fix trait implementation in test Signed-off-by: lovesh --- frame/ethereum/src/mock.rs | 1 + frame/evm/src/lib.rs | 122 +++++++++++++++++++++++++++++----- frame/evm/src/runner/stack.rs | 11 +-- frame/evm/src/tests.rs | 22 ++++++ template/runtime/src/lib.rs | 1 + 5 files changed, 138 insertions(+), 19 deletions(-) diff --git a/frame/ethereum/src/mock.rs b/frame/ethereum/src/mock.rs index fda3b44ff..d55bd6497 100644 --- a/frame/ethereum/src/mock.rs +++ b/frame/ethereum/src/mock.rs @@ -143,6 +143,7 @@ impl pallet_evm::Config for Test { type Precompiles = (); type Runner = pallet_evm::runner::stack::Runner; type ChainId = ChainId; + type OnChargeTransaction = (); } parameter_types! { diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index 7d32c938c..a7c66b5aa 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -70,16 +70,19 @@ use codec::{Encode, Decode}; use serde::{Serialize, Deserialize}; use frame_support::{decl_module, decl_storage, decl_event, decl_error}; use frame_support::weights::{Weight, Pays, PostDispatchInfo}; -use frame_support::traits::{Currency, ExistenceRequirement, Get, WithdrawReasons}; +use frame_support::traits::{Currency, ExistenceRequirement, Get, WithdrawReasons, Imbalance, OnUnbalanced}; use frame_support::dispatch::DispatchResultWithPostInfo; use frame_system::RawOrigin; use sp_core::{U256, H256, H160, Hasher}; -use sp_runtime::{AccountId32, traits::{UniqueSaturatedInto, BadOrigin}}; +use sp_runtime::{AccountId32, traits::{UniqueSaturatedInto, BadOrigin, Saturating}}; use evm::Config as EvmConfig; /// Type alias for currency balance. pub type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; +/// Type alias for negative imbalance during fees +type NegativeImbalanceOf = ::AccountId>>::NegativeImbalance; + /// Trait that outputs the current transaction gas price. pub trait FeeCalculator { /// Return the minimal required gas price. @@ -252,6 +255,11 @@ pub trait Config: frame_system::Config + pallet_timestamp::Config { /// EVM execution runner. type Runner: Runner; + /// To handle fee deduction for EVM transactions. An example is this pallet being used by `pallet_ethereum` + /// where the chain implementing `pallet_ethereum` should be able to configure what happens to the fees + /// Similar to `OnChargeTransaction` of `pallet_transaction_payment` + type OnChargeTransaction: OnChargeEVMTransaction; + /// EVM config used in the module. fn config() -> &'static EvmConfig { &ISTANBUL_CONFIG @@ -557,28 +565,112 @@ impl Module { balance: U256::from(UniqueSaturatedInto::::unique_saturated_into(balance)), } } +} - /// Withdraw fee. - pub fn withdraw_fee(address: &H160, value: U256) -> Result<(), Error> { - let account_id = T::AddressMapping::into_account_id(*address); +/// Handle withdrawing, refunding and depositing of transaction fees. +/// Similar to `OnChargeTransaction` of `pallet_transaction_payment` +pub trait OnChargeEVMTransaction { + type LiquidityInfo: Default; + + /// Before the transaction is executed the payment of the transaction fees + /// need to be secured. + fn withdraw_fee(who: &H160, fee: U256) -> Result>; + + /// After the transaction was executed the actual fee can be calculated. + /// This function should refund any overpaid fees and optionally deposit + /// the corrected amount. + fn correct_and_deposit_fee( + who: &H160, + corrected_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), Error>; +} + +/// Implements the transaction payment for a module implementing the `Currency` +/// trait (eg. the pallet_balances) using an unbalance handler (implementing +/// `OnUnbalanced`). +/// Similar to `CurrencyAdapter` of `pallet_transaction_payment` +pub struct EVMCurrencyAdapter(sp_std::marker::PhantomData<(C, OU)>); + +impl OnChargeEVMTransaction for EVMCurrencyAdapter +where + T: Config, + C: Currency<::AccountId>, + C::PositiveImbalance: Imbalance< + ::AccountId>>::Balance, + Opposite = C::NegativeImbalance, + >, + C::NegativeImbalance: Imbalance< + ::AccountId>>::Balance, + Opposite = C::PositiveImbalance, + >, + OU: OnUnbalanced>, +{ + // Kept type as Option to satisfy bound of Default + type LiquidityInfo = Option>; - drop(T::Currency::withdraw( + fn withdraw_fee(who: &H160, fee: U256) -> Result> { + let account_id = T::AddressMapping::into_account_id(*who); + let imbalance = C::withdraw( &account_id, - value.low_u128().unique_saturated_into(), + fee.low_u128().unique_saturated_into(), WithdrawReasons::FEE, ExistenceRequirement::AllowDeath, - ).map_err(|_| Error::::BalanceLow)?); + ) + .map_err(|_| Error::::BalanceLow)?; + Ok(Some(imbalance)) + } + fn correct_and_deposit_fee( + who: &H160, + corrected_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), Error> { + if let Some(paid) = already_withdrawn { + let account_id = T::AddressMapping::into_account_id(*who); + + // Calculate how much refund we should return + let refund_amount = paid + .peek() + .saturating_sub(corrected_fee.low_u128().unique_saturated_into()); + // refund to the account that paid the fees. If this fails, the + // account might have dropped below the existential balance. In + // that case we don't refund anything. + let refund_imbalance = C::deposit_into_existing(&account_id, refund_amount) + .unwrap_or_else(|_| C::PositiveImbalance::zero()); + // merge the imbalance caused by paying the fees and refunding parts of it again. + let adjusted_paid = paid + .offset(refund_imbalance) + .map_err(|_| Error::::BalanceLow)?; + OU::on_unbalanced(adjusted_paid); + } Ok(()) } +} - /// Deposit fee. - pub fn deposit_fee(address: &H160, value: U256) { - let account_id = T::AddressMapping::into_account_id(*address); +/// Implementation for () does not specify what to do with imbalance +impl OnChargeEVMTransaction for () + where + T: Config, + ::AccountId>>::PositiveImbalance: + Imbalance<::AccountId>>::Balance, Opposite = ::AccountId>>::NegativeImbalance>, + ::AccountId>>::NegativeImbalance: + Imbalance<::AccountId>>::Balance, Opposite = ::AccountId>>::PositiveImbalance>, { + // Kept type as Option to satisfy bound of Default + type LiquidityInfo = Option>; + + fn withdraw_fee( + who: &H160, + fee: U256, + ) -> Result> { + EVMCurrencyAdapter::<::Currency, ()>::withdraw_fee(who, fee) + } - drop(T::Currency::deposit_creating( - &account_id, - value.low_u128().unique_saturated_into(), - )); + fn correct_and_deposit_fee( + who: &H160, + corrected_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Result<(), Error> { + EVMCurrencyAdapter::<::Currency, ()>::correct_and_deposit_fee(who, corrected_fee, already_withdrawn) } } diff --git a/frame/evm/src/runner/stack.rs b/frame/evm/src/runner/stack.rs index 623883f8e..abc67ed4c 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -31,7 +31,7 @@ use evm::backend::Backend as BackendT; use evm::executor::{StackExecutor, StackSubstateMetadata, StackState as StackStateT}; use crate::{ Config, AccountStorages, FeeCalculator, AccountCodes, Module, Event, - Error, AddressMapping, PrecompileSet, + Error, AddressMapping, PrecompileSet, OnChargeEVMTransaction }; use crate::runner::Runner as RunnerT; @@ -81,12 +81,14 @@ impl Runner { let source_account = Module::::account_basic(&source); ensure!(source_account.balance >= total_payment, Error::::BalanceLow); - Module::::withdraw_fee(&source, total_fee)?; - if let Some(nonce) = nonce { ensure!(source_account.nonce == nonce, Error::::InvalidNonce); } + // Deduct fee from the `source` account. + let fee = T::OnChargeTransaction::withdraw_fee(&source, total_fee)?; + + // Execute the EVM call. let (reason, retv) = f(&mut executor); let used_gas = U256::from(executor.used_gas()); @@ -101,7 +103,8 @@ impl Runner { actual_fee ); - Module::::deposit_fee(&source, total_fee.saturating_sub(actual_fee)); + // Refund fees to the `source` account if deducted more before, + T::OnChargeTransaction::correct_and_deposit_fee(&source, actual_fee, fee)?; let state = executor.into_state(); diff --git a/frame/evm/src/tests.rs b/frame/evm/src/tests.rs index c41e64a60..c4b10f08a 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -117,6 +117,7 @@ impl Config for Test { type Event = Event; type Precompiles = (); type ChainId = (); + type OnChargeTransaction = (); } type System = frame_system::Module; @@ -181,3 +182,24 @@ fn fail_call_return_ok() { )); }); } + +#[test] +fn fee_deduction() { + new_test_ext().execute_with(|| { + // Create an EVM address and the corresponding Substrate address that will be charged fees and refunded + let evm_addr = H160::from_str("1000000000000000000000000000000000000003").unwrap(); + let substrate_addr = ::AddressMapping::into_account_id(evm_addr); + + // Seed account + let _ = ::Currency::deposit_creating(&substrate_addr, 100); + assert_eq!(Balances::free_balance(&substrate_addr), 100); + + // Deduct fees as 10 units + let imbalance = <::OnChargeTransaction as OnChargeEVMTransaction>::withdraw_fee(&evm_addr, U256::from(10)).unwrap(); + assert_eq!(Balances::free_balance(&substrate_addr), 90); + + // Refund fees as 5 units + <::OnChargeTransaction as OnChargeEVMTransaction>::correct_and_deposit_fee(&evm_addr, U256::from(5), imbalance).unwrap(); + assert_eq!(Balances::free_balance(&substrate_addr), 95); + }); +} diff --git a/template/runtime/src/lib.rs b/template/runtime/src/lib.rs index 05fbeb54c..1468f0616 100644 --- a/template/runtime/src/lib.rs +++ b/template/runtime/src/lib.rs @@ -291,6 +291,7 @@ impl pallet_evm::Config for Runtime { pallet_evm_precompile_simple::Identity, ); type ChainId = ChainId; + type OnChargeTransaction = (); } pub struct EthereumFindAuthor(PhantomData);