Skip to content

Commit

Permalink
Allow handling fee deductions done by EVM (#307)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Fix formatting

Signed-off-by: lovesh <[email protected]>

* Fix trait implementation in test

Signed-off-by: lovesh <[email protected]>
  • Loading branch information
lovesh authored Mar 7, 2021
1 parent 9bfb053 commit 42de772
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 19 deletions.
1 change: 1 addition & 0 deletions frame/ethereum/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,7 @@ impl pallet_evm::Config for Test {
type Precompiles = ();
type Runner = pallet_evm::runner::stack::Runner<Self>;
type ChainId = ChainId;
type OnChargeTransaction = ();
}

parameter_types! {
Expand Down
122 changes: 107 additions & 15 deletions frame/evm/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;

/// Type alias for negative imbalance during fees
type NegativeImbalanceOf<C, T> = <C as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;

/// Trait that outputs the current transaction gas price.
pub trait FeeCalculator {
/// Return the minimal required gas price.
Expand Down Expand Up @@ -252,6 +255,11 @@ pub trait Config: frame_system::Config + pallet_timestamp::Config {
/// EVM execution runner.
type Runner: Runner<Self>;

/// 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<Self>;

/// EVM config used in the module.
fn config() -> &'static EvmConfig {
&ISTANBUL_CONFIG
Expand Down Expand Up @@ -557,28 +565,112 @@ impl<T: Config> Module<T> {
balance: U256::from(UniqueSaturatedInto::<u128>::unique_saturated_into(balance)),
}
}
}

/// Withdraw fee.
pub fn withdraw_fee(address: &H160, value: U256) -> Result<(), Error<T>> {
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<T: Config> {
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<Self::LiquidityInfo, Error<T>>;

/// 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<T>>;
}

/// 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<C, OU>(sp_std::marker::PhantomData<(C, OU)>);

impl<T, C, OU> OnChargeEVMTransaction<T> for EVMCurrencyAdapter<C, OU>
where
T: Config,
C: Currency<<T as frame_system::Config>::AccountId>,
C::PositiveImbalance: Imbalance<
<C as Currency<<T as frame_system::Config>::AccountId>>::Balance,
Opposite = C::NegativeImbalance,
>,
C::NegativeImbalance: Imbalance<
<C as Currency<<T as frame_system::Config>::AccountId>>::Balance,
Opposite = C::PositiveImbalance,
>,
OU: OnUnbalanced<NegativeImbalanceOf<C, T>>,
{
// Kept type as Option to satisfy bound of Default
type LiquidityInfo = Option<NegativeImbalanceOf<C, T>>;

drop(T::Currency::withdraw(
fn withdraw_fee(who: &H160, fee: U256) -> Result<Self::LiquidityInfo, Error<T>> {
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::<T>::BalanceLow)?);
)
.map_err(|_| Error::<T>::BalanceLow)?;
Ok(Some(imbalance))
}

fn correct_and_deposit_fee(
who: &H160,
corrected_fee: U256,
already_withdrawn: Self::LiquidityInfo,
) -> Result<(), Error<T>> {
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::<T>::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<T> OnChargeEVMTransaction<T> for ()
where
T: Config,
<T::Currency as Currency<<T as frame_system::Config>::AccountId>>::PositiveImbalance:
Imbalance<<T::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance, Opposite = <T::Currency as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance>,
<T::Currency as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance:
Imbalance<<T::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance, Opposite = <T::Currency as Currency<<T as frame_system::Config>::AccountId>>::PositiveImbalance>, {
// Kept type as Option to satisfy bound of Default
type LiquidityInfo = Option<NegativeImbalanceOf<T::Currency, T>>;

fn withdraw_fee(
who: &H160,
fee: U256,
) -> Result<Self::LiquidityInfo, Error<T>> {
EVMCurrencyAdapter::<<T as Config>::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<T>> {
EVMCurrencyAdapter::<<T as Config>::Currency, ()>::correct_and_deposit_fee(who, corrected_fee, already_withdrawn)
}
}
11 changes: 7 additions & 4 deletions frame/evm/src/runner/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -81,12 +81,14 @@ impl<T: Config> Runner<T> {
let source_account = Module::<T>::account_basic(&source);
ensure!(source_account.balance >= total_payment, Error::<T>::BalanceLow);

Module::<T>::withdraw_fee(&source, total_fee)?;

if let Some(nonce) = nonce {
ensure!(source_account.nonce == nonce, Error::<T>::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());
Expand All @@ -101,7 +103,8 @@ impl<T: Config> Runner<T> {
actual_fee
);

Module::<T>::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();

Expand Down
22 changes: 22 additions & 0 deletions frame/evm/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ impl Config for Test {
type Event = Event<Test>;
type Precompiles = ();
type ChainId = ();
type OnChargeTransaction = ();
}

type System = frame_system::Module<Test>;
Expand Down Expand Up @@ -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 = <Test as Config>::AddressMapping::into_account_id(evm_addr);

// Seed account
let _ = <Test as Config>::Currency::deposit_creating(&substrate_addr, 100);
assert_eq!(Balances::free_balance(&substrate_addr), 100);

// Deduct fees as 10 units
let imbalance = <<Test as Config>::OnChargeTransaction as OnChargeEVMTransaction<Test>>::withdraw_fee(&evm_addr, U256::from(10)).unwrap();
assert_eq!(Balances::free_balance(&substrate_addr), 90);

// Refund fees as 5 units
<<Test as Config>::OnChargeTransaction as OnChargeEVMTransaction<Test>>::correct_and_deposit_fee(&evm_addr, U256::from(5), imbalance).unwrap();
assert_eq!(Balances::free_balance(&substrate_addr), 95);
});
}
1 change: 1 addition & 0 deletions template/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ impl pallet_evm::Config for Runtime {
pallet_evm_precompile_simple::Identity,
);
type ChainId = ChainId;
type OnChargeTransaction = ();
}

pub struct EthereumFindAuthor<F>(PhantomData<F>);
Expand Down

0 comments on commit 42de772

Please sign in to comment.