diff --git a/frame/evm/src/lib.rs b/frame/evm/src/lib.rs index 84c688ab0e..3d09b1985e 100644 --- a/frame/evm/src/lib.rs +++ b/frame/evm/src/lib.rs @@ -77,11 +77,13 @@ use frame_support::{ dispatch::{DispatchResultWithPostInfo, Pays, PostDispatchInfo}, storage::child::KillStorageResult, traits::{ + fungible::{Balanced, Credit, Debt}, tokens::{ currency::Currency, fungible::Inspect, imbalance::{Imbalance, OnUnbalanced, SignedImbalance}, - ExistenceRequirement, Fortitude, Preservation, WithdrawReasons, + ExistenceRequirement, Fortitude, Precision, Preservation, WithdrawConsequence, + WithdrawReasons, }, FindAuthor, Get, Time, }, @@ -503,6 +505,8 @@ pub mod pallet { Reentrancy, /// EIP-3607, TransactionMustComeFromEOA, + /// Invalid Transaction + InvalidTransaction, /// Undefined error. Undefined, } @@ -938,7 +942,7 @@ pub trait OnChargeEVMTransaction { corrected_fee: U256, base_fee: U256, already_withdrawn: Self::LiquidityInfo, - ) -> Self::LiquidityInfo; + ) -> Result>; /// Introduced in EIP1559 to handle the priority tip. fn pay_priority_fee(tip: Self::LiquidityInfo); @@ -1002,7 +1006,7 @@ where corrected_fee: U256, base_fee: U256, already_withdrawn: Self::LiquidityInfo, - ) -> Self::LiquidityInfo { + ) -> Result> { if let Some(paid) = already_withdrawn { let account_id = T::AddressMapping::into_account_id(*who); @@ -1038,14 +1042,14 @@ where let adjusted_paid = paid .offset(refund_imbalance) .same() - .unwrap_or_else(|_| C::NegativeImbalance::zero()); + .map_err(|_| Error::::InvalidTransaction)?; let (base_fee, tip) = adjusted_paid.split(base_fee.unique_saturated_into()); // Handle base fee. Can be either burned, rationed, etc ... OU::on_unbalanced(base_fee); - return Some(tip); + return Ok(Some(tip)); } - None + Ok(None) } fn pay_priority_fee(tip: Self::LiquidityInfo) { @@ -1057,6 +1061,89 @@ where } } +/// Implements transaction payment for a pallet implementing the [`fungible`] +/// trait (eg. pallet_balances) using an unbalance handler (implementing +/// [`OnUnbalanced`]). +/// +/// Equivalent of `EVMCurrencyAdapter` but for fungible traits. Similar to `FungibleAdapter` of +/// `pallet_transaction_payment` +pub struct EVMFungibleAdapter(sp_std::marker::PhantomData<(F, OU)>); + +impl OnChargeEVMTransaction for EVMFungibleAdapter +where + T: Config, + F: Balanced, + OU: 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> { + if fee.is_zero() { + return Ok(None); + } + let account_id = T::AddressMapping::into_account_id(*who); + let imbalance = F::withdraw( + &account_id, + fee.unique_saturated_into(), + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ) + .map_err(|_| Error::::BalanceLow)?; + Ok(Some(imbalance)) + } + + fn can_withdraw(who: &H160, amount: U256) -> Result<(), Error> { + let account_id = T::AddressMapping::into_account_id(*who); + let amount = amount.unique_saturated_into(); + if let WithdrawConsequence::Success = F::can_withdraw(&account_id, amount) { + return Ok(()); + } + Err(Error::::BalanceLow) + } + + fn correct_and_deposit_fee( + who: &H160, + corrected_fee: U256, + base_fee: U256, + already_withdrawn: Self::LiquidityInfo, + ) -> Result> { + 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.unique_saturated_into()); + // refund to the account that paid the fees. + let refund_imbalance = F::deposit(&account_id, refund_amount, Precision::BestEffort) + .unwrap_or_else(|_| Debt::::zero()); + + // merge the imbalance caused by paying the fees and refunding parts of it again. + let adjusted_paid = paid + .offset(refund_imbalance) + .same() + .map_err(|_| Error::::InvalidTransaction)?; + + let (base_fee, tip) = adjusted_paid.split(base_fee.unique_saturated_into()); + // Handle base fee. Can be either burned, rationed, etc ... + OU::on_unbalanced(base_fee); + return Ok(Some(tip)); + } + Ok(None) + } + + fn pay_priority_fee(tip: Self::LiquidityInfo) { + // Default Ethereum behaviour: issue the tip to the block author. + if let Some(tip) = tip { + let account_id = T::AddressMapping::into_account_id(>::find_author()); + let _ = F::deposit(&account_id, tip.peek(), Precision::BestEffort); + } + } +} + /// Implementation for () does not specify what to do with imbalance impl OnChargeEVMTransaction for () where @@ -1090,7 +1177,7 @@ U256: UniqueSaturatedInto>, corrected_fee: U256, base_fee: U256, already_withdrawn: Self::LiquidityInfo, - ) -> Self::LiquidityInfo { + ) -> Result> { ::Currency, ()> as OnChargeEVMTransaction>::correct_and_deposit_fee(who, corrected_fee, base_fee, already_withdrawn) } diff --git a/frame/evm/src/runner/stack.rs b/frame/evm/src/runner/stack.rs index 10e6da8c36..881e4a8e85 100644 --- a/frame/evm/src/runner/stack.rs +++ b/frame/evm/src/runner/stack.rs @@ -351,7 +351,8 @@ where actual_base_fee, // Fee initially withdrawn. fee, - ); + ) + .map_err(|e| RunnerError { error: e, weight })?; T::OnChargeTransaction::pay_priority_fee(actual_priority_fee); let state = executor.into_state(); diff --git a/frame/evm/src/tests.rs b/frame/evm/src/tests.rs index e05607be62..455d2b3d19 100644 --- a/frame/evm/src/tests.rs +++ b/frame/evm/src/tests.rs @@ -965,7 +965,7 @@ fn fee_deduction() { 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), U256::from(5), imbalance); + <::OnChargeTransaction as OnChargeEVMTransaction>::correct_and_deposit_fee(&evm_addr, U256::from(5), U256::from(5), imbalance).unwrap(); assert_eq!(Balances::free_balance(substrate_addr), 95); }); }