diff --git a/Cargo.lock b/Cargo.lock index 5b0fb04223..13872e0eaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5102,6 +5102,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-evm-balances" +version = "1.0.0-dev" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "log", + "pallet-evm", + "pallet-evm-system", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-chain-id" version = "1.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 51120747fd..32637848f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "frame/dynamic-fee", "frame/ethereum", "frame/evm", + "frame/evm-balances", "frame/evm-chain-id", "frame/evm-system", "frame/hotfix-sufficients", @@ -144,6 +145,7 @@ pallet-base-fee = { version = "1.0.0", path = "frame/base-fee", default-features pallet-dynamic-fee = { version = "4.0.0-dev", path = "frame/dynamic-fee", default-features = false } pallet-ethereum = { version = "4.0.0-dev", path = "frame/ethereum", default-features = false } pallet-evm = { version = "6.0.0-dev", path = "frame/evm", default-features = false } +pallet-evm-balances = { version = "1.0.0-dev", path = "frame/evm-balances", default-features = false } pallet-evm-chain-id = { version = "1.0.0-dev", path = "frame/evm-chain-id", default-features = false } pallet-evm-system = { version = "1.0.0-dev", path = "frame/evm-system", default-features = false } pallet-evm-precompile-modexp = { version = "2.0.0-dev", path = "frame/evm/precompile/modexp", default-features = false } diff --git a/frame/evm-balances/Cargo.toml b/frame/evm-balances/Cargo.toml new file mode 100644 index 0000000000..d4597e4ca4 --- /dev/null +++ b/frame/evm-balances/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "pallet-evm-balances" +version = "1.0.0-dev" +description = "FRAME EVM BALANCES pallet." +edition = { workspace = true } +repository = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +log = { workspace = true, default-features = false } +scale-codec = { package = "parity-scale-codec", workspace = true } +scale-info = { workspace = true } +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +[dev-dependencies] +fp-evm = { workspace = true } +pallet-evm = { workspace = true } +pallet-evm-system = { workspace = true } +pallet-timestamp = { workspace = true } +sp-core = { workspace = true } +sp-io = { workspace = true } + +[features] +default = ["std"] +std = [ + "log/std", + "scale-codec/std", + "scale-info/std", + # Substrate + "frame-support/std", + "frame-system/std", + "pallet-timestamp/std", + "sp-runtime/std", + "sp-std/std", + # Frontier + "fp-evm/std", + "pallet-evm/std", + "pallet-evm-system/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/frame/evm-balances/src/account_data.rs b/frame/evm-balances/src/account_data.rs new file mode 100644 index 0000000000..3b70e21bc7 --- /dev/null +++ b/frame/evm-balances/src/account_data.rs @@ -0,0 +1,46 @@ +//! Account balances logic. + +use frame_support::traits::WithdrawReasons; + +use super::*; + +/// All balance information for an account. +#[derive(Encode, Decode, Clone, PartialEq, Eq, Default, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct AccountData { + /// Non-reserved part of the balance. There may still be restrictions on this, but it is the + /// total pool what may in principle be transferred, reserved and used for tipping. + /// + /// This is the only balance that matters in terms of most operations on tokens. It + /// alone is used to determine the balance when in the contract execution environment. + pub free: Balance, +} + +impl AccountData { + /// The total balance in this account. + pub(crate) fn total(&self) -> Balance { + self.free + } +} + +/// Simplified reasons for withdrawing balance. +#[derive(Encode, Decode, Clone, Copy, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum Reasons { + /// Paying system transaction fees. + Fee = 0, + /// Any reason other than paying system transaction fees. + Misc = 1, + /// Any reason at all. + All = 2, +} + +impl From for Reasons { + fn from(r: WithdrawReasons) -> Reasons { + if r == WithdrawReasons::TRANSACTION_PAYMENT { + Reasons::Fee + } else if r.contains(WithdrawReasons::TRANSACTION_PAYMENT) { + Reasons::All + } else { + Reasons::Misc + } + } +} diff --git a/frame/evm-balances/src/imbalances.rs b/frame/evm-balances/src/imbalances.rs new file mode 100644 index 0000000000..1a5c619ab0 --- /dev/null +++ b/frame/evm-balances/src/imbalances.rs @@ -0,0 +1,172 @@ +//! Imbalances implementation. + +use frame_support::traits::{SameOrOther, TryDrop}; +use sp_std::{cmp::Ordering, mem}; + +use super::*; + +/// Opaque, move-only struct with private fields that serves as a token denoting that +/// funds have been created without any equal and opposite accounting. +#[must_use] +#[derive(RuntimeDebug, PartialEq, Eq)] +pub struct PositiveImbalance, I: 'static = ()>(T::Balance); + +impl, I: 'static> PositiveImbalance { + /// Create a new positive imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + PositiveImbalance(amount) + } +} + +/// Opaque, move-only struct with private fields that serves as a token denoting that +/// funds have been destroyed without any equal and opposite accounting. +#[must_use] +#[derive(RuntimeDebug, PartialEq, Eq)] +pub struct NegativeImbalance, I: 'static = ()>(T::Balance); + +impl, I: 'static> NegativeImbalance { + /// Create a new negative imbalance from a balance. + pub fn new(amount: T::Balance) -> Self { + NegativeImbalance(amount) + } +} + +impl, I: 'static> TryDrop for PositiveImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl, I: 'static> Default for PositiveImbalance { + fn default() -> Self { + Self::zero() + } +} + +impl, I: 'static> Imbalance for PositiveImbalance { + type Opposite = NegativeImbalance; + + fn zero() -> Self { + Self(Zero::zero()) + } + + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self(first), Self(second)) + } + + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + match a.cmp(&b) { + Ordering::Greater => SameOrOther::Same(Self(a - b)), + Ordering::Less => SameOrOther::Other(NegativeImbalance::new(b - a)), + Ordering::Equal => SameOrOther::None, + } + } + + fn peek(&self) -> T::Balance { + self.0 + } +} + +impl, I: 'static> TryDrop for NegativeImbalance { + fn try_drop(self) -> result::Result<(), Self> { + self.drop_zero() + } +} + +impl, I: 'static> Default for NegativeImbalance { + fn default() -> Self { + Self::zero() + } +} + +impl, I: 'static> Imbalance for NegativeImbalance { + type Opposite = PositiveImbalance; + + fn zero() -> Self { + Self(Zero::zero()) + } + + fn drop_zero(self) -> result::Result<(), Self> { + if self.0.is_zero() { + Ok(()) + } else { + Err(self) + } + } + + fn split(self, amount: T::Balance) -> (Self, Self) { + let first = self.0.min(amount); + let second = self.0 - first; + + mem::forget(self); + (Self(first), Self(second)) + } + + fn merge(mut self, other: Self) -> Self { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + + self + } + + fn subsume(&mut self, other: Self) { + self.0 = self.0.saturating_add(other.0); + mem::forget(other); + } + + fn offset(self, other: Self::Opposite) -> SameOrOther { + let (a, b) = (self.0, other.0); + mem::forget((self, other)); + + match a.cmp(&b) { + Ordering::Greater => SameOrOther::Same(Self(a - b)), + Ordering::Less => SameOrOther::Other(PositiveImbalance::new(b - a)), + Ordering::Equal => SameOrOther::None, + } + } + + fn peek(&self) -> T::Balance { + self.0 + } +} + +impl, I: 'static> Drop for PositiveImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + TotalIssuance::::mutate(|v| *v = v.saturating_add(self.0)); + } +} + +impl, I: 'static> Drop for NegativeImbalance { + /// Basic drop handler will just square up the total issuance. + fn drop(&mut self) { + TotalIssuance::::mutate(|v| *v = v.saturating_sub(self.0)); + } +} diff --git a/frame/evm-balances/src/lib.rs b/frame/evm-balances/src/lib.rs new file mode 100644 index 0000000000..4b8cc88174 --- /dev/null +++ b/frame/evm-balances/src/lib.rs @@ -0,0 +1,739 @@ +//! # EVM Balances Pallet. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + ensure, + traits::{ + fungible, + tokens::{DepositConsequence, WithdrawConsequence}, + Currency, ExistenceRequirement, + ExistenceRequirement::AllowDeath, + Get, Imbalance, OnUnbalanced, SignedImbalance, StorageVersion, StoredMap, WithdrawReasons, + }, +}; +use scale_codec::{Codec, Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{Bounded, CheckedAdd, CheckedSub, MaybeSerializeDeserialize, Zero}, + ArithmeticError, DispatchError, DispatchResult, RuntimeDebug, Saturating, +}; +use sp_std::{cmp, fmt::Debug, result}; + +mod account_data; +pub use account_data::{AccountData, Reasons}; + +mod imbalances; +pub use imbalances::{NegativeImbalance, PositiveImbalance}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use pallet::*; + +/// The current storage version. +const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use sp_runtime::{ + traits::{AtLeast32BitUnsigned, MaybeDisplay}, + FixedPointOperand, + }; + use sp_std::fmt::Debug; + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// The user account identifier type. + type AccountId: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + MaybeDisplay + + Ord + + MaxEncodedLen; + + /// The balance of an account. + type Balance: Parameter + + Member + + AtLeast32BitUnsigned + + Codec + + Default + + Copy + + MaybeSerializeDeserialize + + Debug + + MaxEncodedLen + + TypeInfo + + FixedPointOperand; + + /// The minimum amount required to keep an account open. + #[pallet::constant] + type ExistentialDeposit: Get; + + /// The means of storing the balances of an account. + type AccountStore: StoredMap<>::AccountId, AccountData>; + + /// Handler for the unbalanced reduction when removing a dust account. + type DustRemoval: OnUnbalanced>; + } + + /// The total units issued. + #[pallet::storage] + #[pallet::getter(fn total_issuance)] + #[pallet::whitelist_storage] + pub type TotalIssuance, I: 'static = ()> = StorageValue<_, T::Balance, ValueQuery>; + + /// The total units of outstanding deactivated balance. + #[pallet::storage] + #[pallet::getter(fn inactive_issuance)] + #[pallet::whitelist_storage] + pub type InactiveIssuance, I: 'static = ()> = + StorageValue<_, T::Balance, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// An account was created with some free balance. + Endowed { + account: >::AccountId, + free_balance: T::Balance, + }, + /// An account was removed whose balance was non-zero but below ExistentialDeposit, + /// resulting in an outright loss. + DustLost { + account: >::AccountId, + amount: T::Balance, + }, + /// Transfer succeeded. + Transfer { + from: >::AccountId, + to: >::AccountId, + amount: T::Balance, + }, + /// A balance was set by root. + BalanceSet { + who: >::AccountId, + free: T::Balance, + }, + /// Some amount was deposited (e.g. for transaction fees). + Deposit { + who: >::AccountId, + amount: T::Balance, + }, + /// Some amount was withdrawn from the account (e.g. for transaction fees). + Withdraw { + who: >::AccountId, + amount: T::Balance, + }, + /// Some amount was removed from the account (e.g. for misbehavior). + Slashed { + who: >::AccountId, + amount: T::Balance, + }, + } + + #[pallet::error] + pub enum Error { + /// Account liquidity restrictions prevent withdrawal + LiquidityRestrictions, + /// Balance too low to send value. + InsufficientBalance, + /// Value too low to create account due to existential deposit + ExistentialDeposit, + /// Transfer/payment would kill account + KeepAlive, + /// Beneficiary account must pre-exist + DeadAccount, + } +} + +/// Removes a dust account whose balance was non-zero but below `ExistentialDeposit`. +pub struct DustCleaner, I: 'static = ()>( + Option<(>::AccountId, NegativeImbalance)>, +); + +impl, I: 'static> Drop for DustCleaner { + fn drop(&mut self) { + if let Some((who, dust)) = self.0.take() { + Pallet::::deposit_event(Event::DustLost { + account: who, + amount: dust.peek(), + }); + T::DustRemoval::on_unbalanced(dust); + } + } +} + +impl, I: 'static> Pallet { + /// Get the free balance of an account. + pub fn free_balance( + who: impl sp_std::borrow::Borrow<>::AccountId>, + ) -> T::Balance { + Self::account(who.borrow()).free + } + + /// Get all data information for an account. + fn account(who: &>::AccountId) -> AccountData { + T::AccountStore::get(who) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the + /// result of `f` is an `Err`. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn try_mutate_account>( + who: &>::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result { + Self::try_mutate_account_with_dust(who, f).map(|(result, dust_cleaner)| { + drop(dust_cleaner); + result + }) + } + + /// Mutate an account to some new value, or delete it entirely with `None`. Will enforce + /// `ExistentialDeposit` law, annulling the account as needed. This will do nothing if the + /// result of `f` is an `Err`. + /// + /// It returns both the result from the closure, and an optional `DustCleaner` instance which + /// should be dropped once it is known that all nested mutates that could affect storage items + /// what the dust handler touches have completed. + /// + /// NOTE: Doesn't do any preparatory work for creating a new account, so should only be used + /// when it is known that the account already exists. + /// + /// NOTE: LOW-LEVEL: This will not attempt to maintain total issuance. It is expected that + /// the caller will do this. + fn try_mutate_account_with_dust>( + who: &>::AccountId, + f: impl FnOnce(&mut AccountData, bool) -> Result, + ) -> Result<(R, DustCleaner), E> { + let result = T::AccountStore::try_mutate_exists(who, |maybe_account| { + let is_new = maybe_account.is_none(); + let mut account = maybe_account.take().unwrap_or_default(); + f(&mut account, is_new).map(move |result| { + let maybe_endowed = if is_new { Some(account.free) } else { None }; + let maybe_account_maybe_dust = Self::post_mutation(who, account); + *maybe_account = maybe_account_maybe_dust.0; + (maybe_endowed, maybe_account_maybe_dust.1, result) + }) + }); + result.map(|(maybe_endowed, maybe_dust, result)| { + if let Some(endowed) = maybe_endowed { + Self::deposit_event(Event::Endowed { + account: who.clone(), + free_balance: endowed, + }); + } + let dust_cleaner = DustCleaner(maybe_dust.map(|dust| (who.clone(), dust))); + (result, dust_cleaner) + }) + } + + /// Handles any steps needed after mutating an account. + /// + /// This includes `DustRemoval` unbalancing, in the case than the `new` account's total balance + /// is non-zero but below ED. + /// + /// Returns two values: + /// - `Some` containing the the `new` account, iff the account has sufficient balance. + /// - `Some` containing the dust to be dropped, iff some dust should be dropped. + fn post_mutation( + _who: &>::AccountId, + new: AccountData, + ) -> ( + Option>, + Option>, + ) { + let total = new.total(); + if total < T::ExistentialDeposit::get() { + if total.is_zero() { + (None, None) + } else { + (None, Some(NegativeImbalance::new(total))) + } + } else { + (Some(new), None) + } + } + + fn deposit_consequence( + _who: &>::AccountId, + amount: T::Balance, + account: &AccountData, + mint: bool, + ) -> DepositConsequence { + if amount.is_zero() { + return DepositConsequence::Success; + } + + if mint && TotalIssuance::::get().checked_add(&amount).is_none() { + return DepositConsequence::Overflow; + } + + let new_total_balance = match account.total().checked_add(&amount) { + Some(x) => x, + None => return DepositConsequence::Overflow, + }; + + if new_total_balance < T::ExistentialDeposit::get() { + return DepositConsequence::BelowMinimum; + } + + // NOTE: We assume that we are a provider, so don't need to do any checks in the + // case of account creation. + + DepositConsequence::Success + } + + fn withdraw_consequence( + _who: &>::AccountId, + amount: T::Balance, + account: &AccountData, + ) -> WithdrawConsequence { + if amount.is_zero() { + return WithdrawConsequence::Success; + } + + if TotalIssuance::::get().checked_sub(&amount).is_none() { + return WithdrawConsequence::Underflow; + } + + let new_total_balance = match account.total().checked_sub(&amount) { + Some(x) => x, + None => return WithdrawConsequence::NoFunds, + }; + + // Provider restriction - total account balance cannot be reduced to zero if it cannot + // sustain the loss of a provider reference. + // NOTE: This assumes that the pallet is a provider (which is true). Is this ever changes, + // then this will need to adapt accordingly. + let ed = T::ExistentialDeposit::get(); + if new_total_balance < ed { + return WithdrawConsequence::WouldDie; + } + + // Enough free funds to have them be reduced. + match account.free.checked_sub(&amount) { + Some(_) => WithdrawConsequence::Success, + None => WithdrawConsequence::NoFunds, + } + } +} + +impl, I: 'static> Currency<>::AccountId> for Pallet +where + T::Balance: MaybeSerializeDeserialize + Debug, +{ + type Balance = T::Balance; + type PositiveImbalance = PositiveImbalance; + type NegativeImbalance = NegativeImbalance; + + fn total_balance(who: &>::AccountId) -> Self::Balance { + Self::account(who).total() + } + + fn can_slash(who: &>::AccountId, value: Self::Balance) -> bool { + if value.is_zero() { + return true; + } + Self::free_balance(who) >= value + } + + fn total_issuance() -> Self::Balance { + TotalIssuance::::get() + } + + fn active_issuance() -> Self::Balance { + >::AccountId>>::active_issuance() + } + + fn deactivate(amount: Self::Balance) { + InactiveIssuance::::mutate(|b| b.saturating_accrue(amount)); + } + + fn reactivate(amount: Self::Balance) { + InactiveIssuance::::mutate(|b| b.saturating_reduce(amount)); + } + + fn minimum_balance() -> Self::Balance { + T::ExistentialDeposit::get() + } + + fn burn(mut amount: Self::Balance) -> Self::PositiveImbalance { + if amount.is_zero() { + return PositiveImbalance::zero(); + } + >::mutate(|issued| { + *issued = issued.checked_sub(&amount).unwrap_or_else(|| { + amount = *issued; + Zero::zero() + }); + }); + PositiveImbalance::new(amount) + } + + fn issue(mut amount: Self::Balance) -> Self::NegativeImbalance { + if amount.is_zero() { + return NegativeImbalance::zero(); + } + >::mutate(|issued| { + *issued = issued.checked_add(&amount).unwrap_or_else(|| { + amount = Self::Balance::max_value() - *issued; + Self::Balance::max_value() + }) + }); + NegativeImbalance::new(amount) + } + + fn free_balance(who: &>::AccountId) -> Self::Balance { + Self::account(who).free + } + + // We don't have any existing withdrawal restrictions like locked and reserved balance. + fn ensure_can_withdraw( + _who: &>::AccountId, + _amount: T::Balance, + _reasons: WithdrawReasons, + _new_balance: T::Balance, + ) -> DispatchResult { + Ok(()) + } + + fn transfer( + transactor: &>::AccountId, + dest: &>::AccountId, + value: Self::Balance, + existence_requirement: ExistenceRequirement, + ) -> DispatchResult { + if value.is_zero() || transactor == dest { + return Ok(()); + } + + Self::try_mutate_account_with_dust( + dest, + |to_account, _| -> Result, DispatchError> { + Self::try_mutate_account_with_dust( + transactor, + |from_account, _| -> DispatchResult { + from_account.free = from_account + .free + .checked_sub(&value) + .ok_or(Error::::InsufficientBalance)?; + + to_account.free = to_account + .free + .checked_add(&value) + .ok_or(ArithmeticError::Overflow)?; + + let ed = T::ExistentialDeposit::get(); + ensure!(to_account.total() >= ed, Error::::ExistentialDeposit); + + Self::ensure_can_withdraw( + transactor, + value, + WithdrawReasons::TRANSFER, + from_account.free, + ) + .map_err(|_| Error::::LiquidityRestrictions)?; + + let allow_death = existence_requirement == ExistenceRequirement::AllowDeath; + ensure!( + allow_death || from_account.total() >= ed, + Error::::KeepAlive + ); + + Ok(()) + }, + ) + .map(|(_, maybe_dust_cleaner)| maybe_dust_cleaner) + }, + )?; + + // Emit transfer event. + Self::deposit_event(Event::Transfer { + from: transactor.clone(), + to: dest.clone(), + amount: value, + }); + + Ok(()) + } + + /// Slash a target account `who`, returning the negative imbalance created and any left over + /// amount that could not be slashed. + /// + /// Is a no-op if `value` to be slashed is zero or the account does not exist. + /// + /// NOTE: `slash()` prefers free balance, but assumes that reserve balance can be drawn + /// from in extreme circumstances. `can_slash()` should be used prior to `slash()` to avoid + /// having to draw from reserved funds, however we err on the side of punishment if things are + /// inconsistent or `can_slash` wasn't used appropriately. + fn slash( + who: &>::AccountId, + value: Self::Balance, + ) -> (Self::NegativeImbalance, Self::Balance) { + if value.is_zero() { + return (NegativeImbalance::zero(), Zero::zero()); + } + if Self::total_balance(who).is_zero() { + return (NegativeImbalance::zero(), value); + } + + for attempt in 0..2 { + match Self::try_mutate_account( + who, + |account, + _is_new| + -> Result<(Self::NegativeImbalance, Self::Balance), DispatchError> { + // Best value is the most amount we can slash following liveness rules. + let best_value = match attempt { + // First attempt we try to slash the full amount, and see if liveness issues + // happen. + 0 => value, + // If acting as a critical provider (i.e. first attempt failed), then slash + // as much as possible while leaving at least at ED. + _ => value.min(account.free.saturating_sub(T::ExistentialDeposit::get())), + }; + + let free_slash = cmp::min(account.free, best_value); + account.free -= free_slash; // Safe because of above check + + Ok(( + NegativeImbalance::new(free_slash), + value - free_slash, // Safe because value is gt or eq to total slashed + )) + }, + ) { + Ok((imbalance, not_slashed)) => { + Self::deposit_event(Event::Slashed { + who: who.clone(), + amount: value.saturating_sub(not_slashed), + }); + return (imbalance, not_slashed); + } + Err(_) => (), + } + } + + // Should never get here. But we'll be defensive anyway. + (Self::NegativeImbalance::zero(), value) + } + + /// Deposit some `value` into the free balance of an existing target account `who`. + /// + /// Is a no-op if the `value` to be deposited is zero. + fn deposit_into_existing( + who: &>::AccountId, + value: Self::Balance, + ) -> Result { + if value.is_zero() { + return Ok(PositiveImbalance::zero()); + } + + Self::try_mutate_account( + who, + |account, is_new| -> Result { + ensure!(!is_new, Error::::DeadAccount); + account.free = account + .free + .checked_add(&value) + .ok_or(ArithmeticError::Overflow)?; + Self::deposit_event(Event::Deposit { + who: who.clone(), + amount: value, + }); + Ok(PositiveImbalance::new(value)) + }, + ) + } + + /// Deposit some `value` into the free balance of `who`, possibly creating a new account. + /// + /// This function is a no-op if: + /// - the `value` to be deposited is zero; or + /// - the `value` to be deposited is less than the required ED and the account does not yet + /// exist; or + /// - the deposit would necessitate the account to exist and there are no provider references; + /// or + /// - `value` is so large it would cause the balance of `who` to overflow. + fn deposit_creating( + who: &>::AccountId, + value: Self::Balance, + ) -> Self::PositiveImbalance { + if value.is_zero() { + return Self::PositiveImbalance::zero(); + } + + Self::try_mutate_account( + who, + |account, is_new| -> Result { + let ed = T::ExistentialDeposit::get(); + ensure!(value >= ed || !is_new, Error::::ExistentialDeposit); + + // defensive only: overflow should never happen, however in case it does, then this + // operation is a no-op. + account.free = match account.free.checked_add(&value) { + Some(x) => x, + None => return Ok(Self::PositiveImbalance::zero()), + }; + + Self::deposit_event(Event::Deposit { + who: who.clone(), + amount: value, + }); + Ok(PositiveImbalance::new(value)) + }, + ) + .unwrap_or_else(|_| Self::PositiveImbalance::zero()) + } + + /// Withdraw some free balance from an account, respecting existence requirements. + /// + /// Is a no-op if value to be withdrawn is zero. + fn withdraw( + who: &>::AccountId, + value: Self::Balance, + reasons: WithdrawReasons, + liveness: ExistenceRequirement, + ) -> result::Result { + if value.is_zero() { + return Ok(NegativeImbalance::zero()); + } + + Self::try_mutate_account( + who, + |account, _| -> Result { + let new_free_account = account + .free + .checked_sub(&value) + .ok_or(Error::::InsufficientBalance)?; + + // bail if we need to keep the account alive and this would kill it. + let ed = T::ExistentialDeposit::get(); + let would_be_dead = new_free_account < ed; + let would_kill = would_be_dead && account.free >= ed; + ensure!( + liveness == AllowDeath || !would_kill, + Error::::KeepAlive + ); + + Self::ensure_can_withdraw(who, value, reasons, new_free_account)?; + + account.free = new_free_account; + + Self::deposit_event(Event::Withdraw { + who: who.clone(), + amount: value, + }); + Ok(NegativeImbalance::new(value)) + }, + ) + } + + /// Force the new free balance of a target account `who` to some new value `balance`. + fn make_free_balance_be( + who: &>::AccountId, + value: Self::Balance, + ) -> SignedImbalance { + Self::try_mutate_account( + who, + |account, + is_new| + -> Result, DispatchError> { + let ed = T::ExistentialDeposit::get(); + let total = value; + // If we're attempting to set an existing account to less than ED, then + // bypass the entire operation. It's a no-op if you follow it through, but + // since this is an instance where we might account for a negative imbalance + // (in the dust cleaner of set_account) before we account for its actual + // equal and opposite cause (returned as an Imbalance), then in the + // instance that there's no other accounts on the system at all, we might + // underflow the issuance and our arithmetic will be off. + ensure!(total >= ed || !is_new, Error::::ExistentialDeposit); + + let imbalance = if account.free <= value { + SignedImbalance::Positive(PositiveImbalance::new(value - account.free)) + } else { + SignedImbalance::Negative(NegativeImbalance::new(account.free - value)) + }; + account.free = value; + Self::deposit_event(Event::BalanceSet { + who: who.clone(), + free: account.free, + }); + Ok(imbalance) + }, + ) + .unwrap_or_else(|_| SignedImbalance::Positive(Self::PositiveImbalance::zero())) + } +} + +impl, I: 'static> fungible::Inspect<>::AccountId> for Pallet { + type Balance = T::Balance; + + fn total_issuance() -> Self::Balance { + TotalIssuance::::get() + } + + fn active_issuance() -> Self::Balance { + TotalIssuance::::get().saturating_sub(InactiveIssuance::::get()) + } + + fn minimum_balance() -> Self::Balance { + T::ExistentialDeposit::get() + } + + fn balance(who: &>::AccountId) -> Self::Balance { + Self::account(who).total() + } + + fn reducible_balance(who: &>::AccountId, keep_alive: bool) -> Self::Balance { + let a = Self::account(who); + // Liquid balance is what is neither reserved nor locked/frozen. + let liquid = a.free; + if !keep_alive { + liquid + } else { + // `must_remain_to_exist` is the part of liquid balance which must remain to keep total + // over ED. + let must_remain_to_exist = + T::ExistentialDeposit::get().saturating_sub(a.total() - liquid); + liquid.saturating_sub(must_remain_to_exist) + } + } + + fn can_deposit( + who: &>::AccountId, + amount: Self::Balance, + mint: bool, + ) -> DepositConsequence { + Self::deposit_consequence(who, amount, &Self::account(who), mint) + } + + fn can_withdraw( + who: &>::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + Self::withdraw_consequence(who, amount, &Self::account(who)) + } +} diff --git a/frame/evm-balances/src/mock.rs b/frame/evm-balances/src/mock.rs new file mode 100644 index 0000000000..a57c2a5228 --- /dev/null +++ b/frame/evm-balances/src/mock.rs @@ -0,0 +1,224 @@ +// SPDX-License-Identifier: Apache-2.0 +// This file is part of Frontier. +// +// Copyright (c) 2020-2022 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Test mock for unit tests. + +use std::collections::BTreeMap; + +use frame_support::{ + parameter_types, + traits::{ConstU32, ConstU64, FindAuthor}, + weights::Weight, +}; +use pallet_evm::{EnsureAddressNever, FixedGasWeightMapping, IdentityAddressMapping}; +use sp_core::{H160, H256, U256}; +use sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, ConsensusEngineId, +}; +use sp_std::{boxed::Box, prelude::*, str::FromStr}; + +use crate::{self as pallet_evm_balances, *}; + +pub(crate) const INIT_BALANCE: u64 = 10_000_000_000_000_000; + +pub(crate) fn alice() -> H160 { + H160::from_str("1000000000000000000000000000000000000000").unwrap() +} + +pub(crate) fn bob() -> H160 { + H160::from_str("2000000000000000000000000000000000000000").unwrap() +} + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime! { + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Timestamp: pallet_timestamp, + EvmSystem: pallet_evm_system, + EvmBalances: pallet_evm_balances, + EVM: pallet_evm, + } +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Index = u64; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Header = generic::Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; +} + +impl pallet_evm_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = H160; + type Index = u64; + type AccountData = AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); +} + +impl pallet_evm_balances::Config for Test { + type RuntimeEvent = RuntimeEvent; + type AccountId = H160; + type Balance = u64; + type ExistentialDeposit = ConstU64<1>; + type AccountStore = EvmSystem; + type DustRemoval = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 1000; +} +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub struct FixedGasPrice; + +impl pallet_evm::FeeCalculator for FixedGasPrice { + fn min_gas_price() -> (U256, Weight) { + // Return some meaningful gas price and weight + (1_000_000_000u128.into(), Weight::from_ref_time(7u64)) + } +} + +pub struct FindAuthorTruncated; + +impl FindAuthor for FindAuthorTruncated { + fn find_author<'a, I>(_digests: I) -> Option + where + I: 'a + IntoIterator, + { + Some(H160::from_str("1234500000000000000000000000000000000000").unwrap()) + } +} + +parameter_types! { + pub BlockGasLimit: U256 = U256::max_value(); + pub WeightPerGas: Weight = Weight::from_ref_time(20_000); +} + +impl pallet_evm::Config for Test { + type AccountProvider = EvmSystem; + type FeeCalculator = FixedGasPrice; + type GasWeightMapping = FixedGasWeightMapping; + type WeightPerGas = WeightPerGas; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type CallOrigin = + EnsureAddressNever<::AccountId>; + type WithdrawOrigin = + EnsureAddressNever<::AccountId>; + type AddressMapping = IdentityAddressMapping; + type Currency = EvmBalances; + type RuntimeEvent = RuntimeEvent; + type PrecompilesType = (); + type PrecompilesValue = (); + type ChainId = (); + type BlockGasLimit = BlockGasLimit; + type Runner = pallet_evm::runner::stack::Runner; + type OnChargeTransaction = (); + type OnCreate = (); + type FindAuthor = FindAuthorTruncated; + type Timestamp = Timestamp; + type WeightInfo = (); +} + +/// Build test externalities from the custom genesis. +/// Using this call requires manual assertions on the genesis init logic. +pub fn new_test_ext() -> sp_io::TestExternalities { + // Build genesis. + let config = GenesisConfig { + evm: EVMConfig { + accounts: { + let mut map = BTreeMap::new(); + let init_genesis_account = fp_evm::GenesisAccount { + balance: INIT_BALANCE.into(), + code: Default::default(), + nonce: Default::default(), + storage: Default::default(), + }; + map.insert(alice(), init_genesis_account.clone()); + map.insert(bob(), init_genesis_account); + map + }, + }, + ..Default::default() + }; + let storage = config.build_storage().unwrap(); + + // Make test externalities from the storage. + storage.into() +} + +pub fn runtime_lock() -> std::sync::MutexGuard<'static, ()> { + static MOCK_RUNTIME_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + + // Ignore the poisoning for the tests that panic. + // We only care about concurrency here, not about the poisoning. + match MOCK_RUNTIME_MUTEX.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } +} + +pub trait TestExternalitiesExt { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R; +} + +impl TestExternalitiesExt for frame_support::sp_io::TestExternalities { + fn execute_with_ext(&mut self, execute: E) -> R + where + E: for<'e> FnOnce(&'e ()) -> R, + { + let guard = runtime_lock(); + let result = self.execute_with(|| execute(&guard)); + drop(guard); + result + } +} diff --git a/frame/evm-balances/src/tests.rs b/frame/evm-balances/src/tests.rs new file mode 100644 index 0000000000..2fe270c5e5 --- /dev/null +++ b/frame/evm-balances/src/tests.rs @@ -0,0 +1,490 @@ +//! Unit tests. + +use frame_support::{assert_noop, assert_ok, weights::Weight}; +use pallet_evm::{FeeCalculator, Runner}; +use sp_core::{H160, U256}; +use sp_runtime::traits::UniqueSaturatedInto; +use sp_std::str::FromStr; + +use crate::{mock::*, *}; + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with_ext(|_| { + // Check the accounts. + assert_eq!( + ::account(&alice()), + account_data::AccountData { free: INIT_BALANCE } + ); + assert_eq!( + ::account(&bob()), + account_data::AccountData { free: INIT_BALANCE } + ); + + // Check the total balance value. + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE); + }); +} + +#[test] +fn currency_total_balance_works() { + new_test_ext().execute_with_ext(|_| { + // Check the total balance value. + assert_eq!(EvmBalances::total_balance(&alice()), INIT_BALANCE); + }); +} + +#[test] +fn currency_can_slash_works() { + new_test_ext().execute_with_ext(|_| { + // Check possible slashing. + assert!(EvmBalances::can_slash(&alice(), 100)); + }); +} + +#[test] +fn currency_total_issuance_works() { + new_test_ext().execute_with_ext(|_| { + // Check the total issuance value. + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE); + }); +} + +#[test] +fn currency_active_issuance_works() { + new_test_ext().execute_with_ext(|_| { + // Check the active issuance value. + assert_eq!(EvmBalances::active_issuance(), 2 * INIT_BALANCE); + }); +} + +#[test] +fn currency_deactivate_reactivate_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(>::get(), 0); + + // Deactivate some balance. + EvmBalances::deactivate(100); + // Assert state changes. + assert_eq!(>::get(), 100); + // Reactivate some balance. + EvmBalances::reactivate(40); + // Assert state changes. + assert_eq!(>::get(), 60); + }); +} + +#[test] +fn currency_burn_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE); + + // Burn some balance. + let imbalance = EvmBalances::burn(100); + + // Assert state changes. + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE - 100); + drop(imbalance); + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE); + }); +} + +#[test] +fn currency_issue_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE); + + // Issue some balance. + let imbalance = EvmBalances::issue(100); + + // Assert state changes. + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE + 100); + drop(imbalance); + assert_eq!(EvmBalances::total_issuance(), 2 * INIT_BALANCE); + }); +} + +#[test] +fn currency_transfer_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(EvmBalances::total_balance(&alice()), INIT_BALANCE); + + let transfered_amount = 100; + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + assert_ok!(EvmBalances::transfer( + &alice(), + &bob(), + transfered_amount, + ExistenceRequirement::KeepAlive + )); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice()), + INIT_BALANCE - transfered_amount + ); + assert_eq!( + EvmBalances::total_balance(&bob()), + INIT_BALANCE + transfered_amount + ); + System::assert_has_event(RuntimeEvent::EvmBalances(crate::Event::Transfer { + from: alice(), + to: bob(), + amount: transfered_amount, + })); + }); +} + +#[test] +fn currency_slash_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(EvmBalances::total_balance(&alice()), INIT_BALANCE); + + let slashed_amount = 1000; + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + assert!(EvmBalances::slash(&alice(), 1000).1.is_zero()); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice()), + INIT_BALANCE - slashed_amount + ); + System::assert_has_event(RuntimeEvent::EvmBalances(crate::Event::Slashed { + who: alice(), + amount: slashed_amount, + })); + }); +} + +#[test] +fn currency_deposit_into_existing_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(EvmBalances::total_balance(&alice()), INIT_BALANCE); + + let deposited_amount = 10; + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + assert_ok!(EvmBalances::deposit_into_existing( + &alice(), + deposited_amount + )); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice()), + INIT_BALANCE + deposited_amount + ); + System::assert_has_event(RuntimeEvent::EvmBalances(crate::Event::Deposit { + who: alice(), + amount: deposited_amount, + })); + }); +} + +#[test] +fn currency_deposit_creating_works() { + new_test_ext().execute_with_ext(|_| { + // Prepare test preconditions. + let charlie = H160::from_str("1000000000000000000000000000000000000003").unwrap(); + let deposited_amount = 10; + assert!(!EvmSystem::account_exists(&charlie)); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + let _ = EvmBalances::deposit_creating(&charlie, deposited_amount); + + // Assert state changes. + assert_eq!(EvmBalances::total_balance(&charlie), deposited_amount); + System::assert_has_event(RuntimeEvent::EvmBalances(crate::Event::Deposit { + who: charlie, + amount: deposited_amount, + })); + assert!(EvmSystem::account_exists(&charlie)); + System::assert_has_event(RuntimeEvent::EvmSystem( + pallet_evm_system::Event::NewAccount { account: charlie }, + )); + }); +} + +#[test] +fn currency_withdraw_works() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert_eq!(EvmBalances::total_balance(&alice()), INIT_BALANCE); + + let withdrawed_amount = 1000; + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + assert_ok!(EvmBalances::withdraw( + &alice(), + 1000, + WithdrawReasons::FEE, + ExistenceRequirement::KeepAlive + )); + + // Assert state changes. + assert_eq!( + EvmBalances::total_balance(&alice()), + INIT_BALANCE - withdrawed_amount + ); + System::assert_has_event(RuntimeEvent::EvmBalances(crate::Event::Withdraw { + who: alice(), + amount: withdrawed_amount, + })); + }); +} + +#[test] +fn currency_make_free_balance_be_works() { + new_test_ext().execute_with(|| { + // Prepare test preconditions. + let charlie = H160::from_str("1000000000000000000000000000000000000003").unwrap(); + let made_free_balance = 100; + + // Check test preconditions. + assert_eq!(EvmBalances::total_balance(&charlie), 0); + + // Invoke the function under test. + let _ = EvmBalances::make_free_balance_be(&charlie, made_free_balance); + + // Assert state changes. + assert_eq!(EvmBalances::total_balance(&charlie), made_free_balance); + }); +} + +#[test] +fn evm_system_account_should_be_reaped() { + new_test_ext().execute_with_ext(|_| { + // Check test preconditions. + assert!(EvmSystem::account_exists(&bob())); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + assert_ok!(EvmBalances::transfer( + &bob(), + &alice(), + INIT_BALANCE, + ExistenceRequirement::AllowDeath + )); + + // Assert state changes. + assert_eq!(EvmBalances::free_balance(&bob()), 0); + assert!(!EvmSystem::account_exists(&bob())); + System::assert_has_event(RuntimeEvent::EvmSystem( + pallet_evm_system::Event::KilledAccount { account: bob() }, + )); + }); +} + +#[test] +fn evm_balances_transferring_too_high_value_should_not_panic() { + new_test_ext().execute_with(|| { + // Prepare test preconditions. + let charlie = H160::from_str("1000000000000000000000000000000000000003").unwrap(); + let eve = H160::from_str("1000000000000000000000000000000000000004").unwrap(); + EvmBalances::make_free_balance_be(&charlie, u64::MAX); + EvmBalances::make_free_balance_be(&eve, 1); + + // Invoke the function under test. + assert_noop!( + EvmBalances::transfer(&charlie, &eve, u64::MAX, ExistenceRequirement::AllowDeath), + ArithmeticError::Overflow, + ); + }); +} + +#[test] +fn evm_fee_deduction() { + new_test_ext().execute_with(|| { + let charlie = H160::from_str("1000000000000000000000000000000000000003").unwrap(); + + // Seed account + let _ = ::Currency::deposit_creating(&charlie, 100); + assert_eq!(EvmBalances::free_balance(&charlie), 100); + + // Deduct fees as 10 units + let imbalance = + <::OnChargeTransaction as pallet_evm::OnChargeEVMTransaction>::withdraw_fee( + &charlie, + U256::from(10), + ) + .unwrap(); + assert_eq!(EvmBalances::free_balance(&charlie), 90); + + // Refund fees as 5 units + <::OnChargeTransaction as pallet_evm::OnChargeEVMTransaction>::correct_and_deposit_fee(&charlie, U256::from(5), U256::from(5), imbalance); + assert_eq!(EvmBalances::free_balance(&charlie), 95); + }); +} + +#[test] +fn evm_issuance_after_tip() { + new_test_ext().execute_with(|| { + let before_tip = ::Currency::total_issuance(); + + assert_ok!(::Runner::call( + alice(), + bob(), + Vec::new(), + U256::from(1), + 1000000, + Some(U256::from(2_000_000_000)), + None, + None, + Vec::new(), + true, + true, + ::config(), + )); + + // Only base fee is burned + let base_fee: u64 = ::FeeCalculator::min_gas_price() + .0 + .unique_saturated_into(); + + let after_tip = ::Currency::total_issuance(); + + assert_eq!(after_tip, (before_tip - (base_fee * 21_000))); + }); +} + +#[test] +fn evm_refunds_should_work() { + new_test_ext().execute_with(|| { + let before_call = EVM::account_basic(&alice()).0.balance; + // Gas price is not part of the actual fee calculations anymore, only the base fee. + // + // Because we first deduct max_fee_per_gas * gas_limit (2_000_000_000 * 1000000) we need + // to ensure that the difference (max fee VS base fee) is refunded. + + let _ = ::Runner::call( + alice(), + bob(), + Vec::new(), + U256::from(1), + 1000000, + Some(U256::from(2_000_000_000)), + None, + None, + Vec::new(), + true, + true, + ::config(), + ); + + let (base_fee, _) = ::FeeCalculator::min_gas_price(); + let total_cost = (U256::from(21_000) * base_fee) + U256::from(1); + let after_call = EVM::account_basic(&alice()).0.balance; + assert_eq!(after_call, before_call - total_cost); + }); +} + +#[test] +fn evm_refunds_and_priority_should_work() { + new_test_ext().execute_with(|| { + let before_call = EVM::account_basic(&alice()).0.balance; + // We deliberately set a base fee + max tip > max fee. + // The effective priority tip will be 1GWEI instead 1.5GWEI: + // (max_fee_per_gas - base_fee).min(max_priority_fee) + // (2 - 1).min(1.5) + let tip = U256::from(1_500_000_000); + let max_fee_per_gas = U256::from(2_000_000_000); + let used_gas = U256::from(21_000); + + let _ = ::Runner::call( + alice(), + bob(), + Vec::new(), + U256::from(1), + 1000000, + Some(max_fee_per_gas), + Some(tip), + None, + Vec::new(), + true, + true, + ::config(), + ); + + let (base_fee, _) = ::FeeCalculator::min_gas_price(); + let actual_tip = (max_fee_per_gas - base_fee).min(tip) * used_gas; + let total_cost = (used_gas * base_fee) + U256::from(actual_tip) + U256::from(1); + let after_call = EVM::account_basic(&alice()).0.balance; + // The tip is deducted but never refunded to the caller. + assert_eq!(after_call, before_call - total_cost); + }); +} + +#[test] +fn evm_call_should_fail_with_priority_greater_than_max_fee() { + new_test_ext().execute_with(|| { + // Max priority greater than max fee should fail. + let tip: u128 = 1_100_000_000; + + let result = ::Runner::call( + alice(), + bob(), + Vec::new(), + U256::from(1), + 1000000, + Some(U256::from(1_000_000_000)), + Some(U256::from(tip)), + None, + Vec::new(), + true, + true, + ::config(), + ); + assert!(result.is_err()); + // Some used weight is returned as part of the error. + assert_eq!(result.unwrap_err().weight, Weight::from_ref_time(7)); + }); +} + +#[test] +fn evm_call_should_succeed_with_priority_equal_to_max_fee() { + new_test_ext().execute_with(|| { + let tip: u128 = 1_000_000_000; + // Mimics the input for pre-eip-1559 transaction types where `gas_price` + // is used for both `max_fee_per_gas` and `max_priority_fee_per_gas`. + let result = ::Runner::call( + alice(), + bob(), + Vec::new(), + U256::from(1), + 1000000, + Some(U256::from(1_000_000_000)), + Some(U256::from(tip)), + None, + Vec::new(), + true, + true, + ::config(), + ); + assert!(result.is_ok()); + }); +}