From f95f51c7b17be8a70a58e776396558ed2bc0b64a Mon Sep 17 00:00:00 2001 From: Dmitry Lavrenov <39522748+dmitrylavrenov@users.noreply.github.com> Date: Mon, 10 Jun 2024 17:25:50 +0300 Subject: [PATCH] Sovereign accounts and EVM system layer (#123) * Sovereign accounts and EVM system layer (#94) (#99) (#106) * Sovereign accounts and EVM system layer (#94) * Sovereign accounts and EVM system layer (#86) * Frame evm system (#62) * Add initial impl of evm-system * Check account existence * Improve creation account logic * Add new line * Add default implementations * Add mock * Use DispatchResult instead of custom enums * Basic create account tests * Add simple tests with remove account and nonce update * Remove default implementations for OnNewAccount and OnKilledAccount * Add mock objects for OnNewAccount and OnKilledAccount * Use mock logic in tests * Some tests improvements * Add docs to tests * Check events in tests * Add default implementation for OnNewAccount and OnKilledAccount for empty tuple (#63) * Implement StoredMap for EvmSystem (#64) * Add try-runtime feature into `pallet-evm-system` (#67) Add try-runtime feature at pallet-evm-system * Use `sp_std` library to add FromStr trait for tests at `pallet-evm-system` (#68) Use sp_std library to add FromStr trait for tests at pallet-evm-system * Rename FullAccount into Account at `pallet-evm-system` (#69) Rename FullAccount into Account at pallet-evm-system * Fix `try_mutate_exists` implementation and add tests to check it (#70) * Fix try_mutate_exists logic * Add tests * Fix AccountData type at mock * Remove redundant mock expectations * Add comments for new tests * More explicitly handle (none,false) case * Rename some_data back to maybe_account_data * Add data changes for try_mutate_exists_fails_without_changes test * Add try_mutate_exists_account_not_created test * Add assert_noop to check state chages * Return success for try_mutate_exists_account_not_created test * Use workspace deps * Remove license * Implement missed AccountProvider for EvmSystem * Remove deprecated trait Store * Add Apache-2.0 license * Remove deprecated storage getter * Remove redundant brackets * Apply formatter * Use execute_with_ext * Fix mock * Fix removing account logic * Remove unused importa --- Cargo.lock | 17 ++ Cargo.toml | 3 + frame/evm-system/Cargo.toml | 46 ++++++ frame/evm-system/src/lib.rs | 270 ++++++++++++++++++++++++++++++ frame/evm-system/src/mock.rs | 133 +++++++++++++++ frame/evm-system/src/tests.rs | 303 ++++++++++++++++++++++++++++++++++ 6 files changed, 772 insertions(+) create mode 100644 frame/evm-system/Cargo.toml create mode 100644 frame/evm-system/src/lib.rs create mode 100644 frame/evm-system/src/mock.rs create mode 100644 frame/evm-system/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index 8bbdb782de..b1559d09c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5723,6 +5723,23 @@ dependencies = [ "sp-io", ] +[[package]] +name = "pallet-evm-system" +version = "1.0.0-dev" +dependencies = [ + "fp-evm", + "frame-support", + "frame-system", + "log", + "mockall", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-evm-test-vector-support" version = "1.0.0-dev" diff --git a/Cargo.toml b/Cargo.toml index 749968b3de..931d4d5593 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "frame/ethereum", "frame/evm", "frame/evm-chain-id", + "frame/evm-system", "frame/hotfix-sufficients", "frame/evm/precompile/sha3fips", "frame/evm/precompile/simple", @@ -60,6 +61,7 @@ jsonrpsee = "0.16.3" kvdb-rocksdb = "0.19.0" libsecp256k1 = { version = "0.7.1", default-features = false } log = { version = "0.4.20", default-features = false } +mockall = "0.11" num_enum = { version = "0.6.1", default-features = false } parity-db = "0.4.10" parking_lot = "0.12.1" @@ -168,6 +170,7 @@ pallet-dynamic-fee = { version = "4.0.0-dev", path = "frame/dynamic-fee", defaul 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-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 } pallet-evm-precompile-sha3fips = { version = "2.0.0-dev", path = "frame/evm/precompile/sha3fips", default-features = false } pallet-evm-precompile-simple = { version = "2.0.0-dev", path = "frame/evm/precompile/simple", default-features = false } diff --git a/frame/evm-system/Cargo.toml b/frame/evm-system/Cargo.toml new file mode 100644 index 0000000000..1a114304fe --- /dev/null +++ b/frame/evm-system/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "pallet-evm-system" +version = "1.0.0-dev" +license = "Apache-2.0" +description = "FRAME EVM SYSTEM 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 } +# Frontier +fp-evm = { workspace = true } + +[dev-dependencies] +mockall = { 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", + "sp-runtime/std", + "sp-std/std", + # Frontier + "fp-evm/std", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", +] diff --git a/frame/evm-system/src/lib.rs b/frame/evm-system/src/lib.rs new file mode 100644 index 0000000000..c7d5d6b76b --- /dev/null +++ b/frame/evm-system/src/lib.rs @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 + +//! # EVM System Pallet. + +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::StoredMap; +use scale_codec::{Decode, Encode, FullCodec, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{traits::One, DispatchError, RuntimeDebug}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub use pallet::*; + +/// Account information. +#[derive( + Clone, + Eq, + PartialEq, + Default, + RuntimeDebug, + Encode, + Decode, + TypeInfo, + MaxEncodedLen +)] +pub struct AccountInfo { + /// The number of transactions this account has sent. + pub nonce: Index, + /// The additional data that belongs to this account. Used to store the balance(s) in a lot of + /// chains. + pub data: AccountData, +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::pallet_prelude::*; + use sp_runtime::traits::{AtLeast32Bit, MaybeDisplay}; + use sp_std::fmt::Debug; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[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; + + /// Nonce type. This stores the number of previous transactions + /// associated with a sender account. + type Nonce: Parameter + + Member + + MaybeSerializeDeserialize + + Debug + + Default + + MaybeDisplay + + AtLeast32Bit + + Copy + + MaxEncodedLen; + + /// Data to be associated with an account (other than nonce/transaction counter, which this + /// pallet does regardless). + type AccountData: Member + FullCodec + Clone + Default + TypeInfo + MaxEncodedLen; + + /// Handler for when a new account has just been created. + type OnNewAccount: OnNewAccount<::AccountId>; + + /// A function that is invoked when an account has been determined to be dead. + /// + /// All resources should be cleaned up associated with the given account. + type OnKilledAccount: OnKilledAccount<::AccountId>; + } + + /// The full account information for a particular account ID. + #[pallet::storage] + pub type Account = StorageMap< + _, + Blake2_128Concat, + ::AccountId, + AccountInfo<::Nonce, ::AccountData>, + ValueQuery, + >; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// A new account was created. + NewAccount { account: ::AccountId }, + /// An account was reaped. + KilledAccount { account: ::AccountId }, + } + + #[pallet::error] + pub enum Error { + /// The account already exists in case creating it. + AccountAlreadyExist, + /// The account doesn't exist in case removing it. + AccountNotExist, + } +} + +/// The outcome of the account creation operation. +#[derive(Eq, PartialEq, RuntimeDebug)] +pub enum AccountCreationOutcome { + /// Account was created. + Created, + /// Account already exists in the system, so action was taken. + AlreadyExists, +} + +/// The outcome of the account removal operation. +#[derive(Eq, PartialEq, RuntimeDebug)] +pub enum AccountRemovalOutcome { + /// Account was destroyed and no longer exists. + Reaped, + /// Account was non-empty, and it was retained and still exists in the system. + Retained, + /// Account did not exist in the first place, so no action was taken. + DidNotExist, +} + +impl Pallet { + /// Check the account existence. + pub fn account_exists(who: &::AccountId) -> bool { + Account::::contains_key(who) + } + + /// An account is being created. + fn on_created_account(who: ::AccountId) { + ::OnNewAccount::on_new_account(&who); + Self::deposit_event(Event::NewAccount { account: who }); + } + + /// Do anything that needs to be done after an account has been killed. + fn on_killed_account(who: ::AccountId) { + ::OnKilledAccount::on_killed_account(&who); + Self::deposit_event(Event::KilledAccount { account: who }); + } + + /// Retrieve the account transaction counter from storage. + pub fn account_nonce(who: &::AccountId) -> ::Nonce { + Account::::get(who).nonce + } + + /// Increment a particular account's nonce by 1. + pub fn inc_account_nonce(who: &::AccountId) { + Account::::mutate(who, |a| a.nonce += ::Nonce::one()); + } + + /// Create an account. + pub fn create_account(who: &::AccountId) -> AccountCreationOutcome { + if Self::account_exists(who) { + return AccountCreationOutcome::AlreadyExists; + } + + Account::::insert(who.clone(), AccountInfo::<_, _>::default()); + Self::on_created_account(who.clone()); + AccountCreationOutcome::Created + } + + /// Remove an account. + pub fn remove_account(who: &::AccountId) -> AccountRemovalOutcome { + if !Self::account_exists(who) { + return AccountRemovalOutcome::DidNotExist; + } + + if Account::::get(who).data != ::AccountData::default() { + return AccountRemovalOutcome::Retained; + } + + Account::::remove(who); + Self::on_killed_account(who.clone()); + AccountRemovalOutcome::Reaped + } +} + +impl StoredMap<::AccountId, ::AccountData> for Pallet { + fn get(k: &::AccountId) -> ::AccountData { + Account::::get(k).data + } + + fn try_mutate_exists>( + k: &::AccountId, + f: impl FnOnce(&mut Option<::AccountData>) -> Result, + ) -> Result { + let (mut maybe_account_data, was_providing) = if Self::account_exists(k) { + (Some(Account::::get(k).data), true) + } else { + (None, false) + }; + + let result = f(&mut maybe_account_data)?; + + match (maybe_account_data, was_providing) { + (Some(data), false) => { + Account::::mutate(k, |a| a.data = data); + Self::on_created_account(k.clone()); + } + (Some(data), true) => { + Account::::mutate(k, |a| a.data = data); + } + (None, true) => { + Account::::remove(k); + Self::on_killed_account(k.clone()); + } + (None, false) => { + // Do nothing. + } + } + + Ok(result) + } +} + +impl fp_evm::AccountProvider for Pallet { + type AccountId = ::AccountId; + type Nonce = ::Nonce; + + fn create_account(who: &Self::AccountId) { + let _ = Self::create_account(who); + } + + fn remove_account(who: &Self::AccountId) { + let _ = Self::remove_account(who); + } + + fn account_nonce(who: &Self::AccountId) -> Self::Nonce { + Self::account_nonce(who) + } + + fn inc_account_nonce(who: &Self::AccountId) { + Self::inc_account_nonce(who); + } +} + +/// Interface to handle account creation. +pub trait OnNewAccount { + /// A new account `who` has been registered. + fn on_new_account(who: &AccountId); +} + +impl OnNewAccount for () { + fn on_new_account(_who: &AccountId) {} +} + +/// Interface to handle account killing. +pub trait OnKilledAccount { + /// The account with the given id was reaped. + fn on_killed_account(who: &AccountId); +} + +impl OnKilledAccount for () { + fn on_killed_account(_who: &AccountId) {} +} diff --git a/frame/evm-system/src/mock.rs b/frame/evm-system/src/mock.rs new file mode 100644 index 0000000000..4baa303936 --- /dev/null +++ b/frame/evm-system/src/mock.rs @@ -0,0 +1,133 @@ +// 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 frame_support::traits::{ConstU32, ConstU64}; +use mockall::mock; +use sp_core::{H160, H256}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; +use sp_std::{boxed::Box, prelude::*}; + +use crate::{self as pallet_evm_system, *}; + +mock! { + #[derive(Debug)] + pub DummyOnNewAccount {} + + impl OnNewAccount for DummyOnNewAccount { + pub fn on_new_account(who: &H160); + } +} + +mock! { + #[derive(Debug)] + pub DummyOnKilledAccount {} + + impl OnKilledAccount for DummyOnKilledAccount { + pub fn on_killed_account(who: &H160); + } +} + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime! { + pub enum Test { + System: frame_system, + EvmSystem: pallet_evm_system, + } +} + +impl frame_system::Config for Test { + type RuntimeEvent = RuntimeEvent; + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = H160; + type Lookup = IdentityLookup; + type Block = Block; + 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 Nonce = u64; + type AccountData = u64; + type OnNewAccount = MockDummyOnNewAccount; + type OnKilledAccount = MockDummyOnKilledAccount; +} + +/// 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 = RuntimeGenesisConfig { + ..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-system/src/tests.rs b/frame/evm-system/src/tests.rs new file mode 100644 index 0000000000..a7e747661a --- /dev/null +++ b/frame/evm-system/src/tests.rs @@ -0,0 +1,303 @@ +//! Unit tests. + +use sp_std::str::FromStr; + +use frame_support::{assert_noop, assert_storage_noop}; +use mockall::predicate; +use sp_core::H160; + +use crate::{mock::*, *}; + +/// This test verifies that creating account works in the happy path. +#[test] +fn create_account_works() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + assert!(!EvmSystem::account_exists(&account_id)); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + let on_new_account_ctx = MockDummyOnNewAccount::on_new_account_context(); + on_new_account_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(()); + + // Invoke the function under test. + assert_eq!( + EvmSystem::create_account(&account_id), + AccountCreationOutcome::Created + ); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::NewAccount { + account: account_id, + })); + + // Assert mock invocations. + on_new_account_ctx.checkpoint(); + }); +} + +/// This test verifies that creating account fails when the account already exists. +#[test] +fn create_account_fails() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + >::insert(account_id.clone(), AccountInfo::<_, _>::default()); + + // Invoke the function under test. + assert_storage_noop!(assert_eq!( + EvmSystem::create_account(&account_id), + AccountCreationOutcome::AlreadyExists + )); + }); +} + +/// This test verifies that removing account works in the happy path. +#[test] +fn remove_account_works() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + >::insert(account_id.clone(), AccountInfo::<_, _>::default()); + + // Set block number to enable events. + System::set_block_number(1); + + // Set mock expectations. + let on_killed_account_ctx = MockDummyOnKilledAccount::on_killed_account_context(); + on_killed_account_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(()); + + // Invoke the function under test. + assert_eq!( + EvmSystem::remove_account(&account_id), + AccountRemovalOutcome::Reaped + ); + + // Assert state changes. + assert!(!EvmSystem::account_exists(&account_id)); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::KilledAccount { + account: account_id, + })); + + // Assert mock invocations. + on_killed_account_ctx.checkpoint(); + }); +} + +/// This test verifies that removing account fails when the account doesn't exist. +#[test] +fn remove_account_fails() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Invoke the function under test. + assert_storage_noop!(assert_eq!( + EvmSystem::remove_account(&account_id), + AccountRemovalOutcome::DidNotExist + )); + }); +} + +/// This test verifies that incrementing account nonce works in the happy path. +#[test] +fn inc_account_nonce_works() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + let nonce_before = EvmSystem::account_nonce(&account_id); + + // Invoke the function under test. + EvmSystem::inc_account_nonce(&account_id); + + // Assert state changes. + assert_eq!(EvmSystem::account_nonce(&account_id), nonce_before + 1); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data wasn't providing +/// and returned data is `Some`. As a result, a new account has been created. +#[test] +fn try_mutate_exists_account_created() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + assert!(!EvmSystem::account_exists(&account_id)); + + // Set mock expectations. + let on_new_account_ctx = MockDummyOnNewAccount::on_new_account_context(); + on_new_account_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(()); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&account_id, |maybe_data| -> Result<(), DispatchError> { + *maybe_data = Some(1); + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + assert_eq!(EvmSystem::get(&account_id), 1); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::NewAccount { + account: account_id, + })); + + // Assert mock invocations. + on_new_account_ctx.checkpoint(); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data was providing +/// and returned data is `Some`. As a result, the account has been updated. +#[test] +fn try_mutate_exists_account_updated() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 1; + let data = 1; + >::insert(account_id.clone(), AccountInfo { nonce, data }); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&account_id)); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&account_id, |maybe_data| -> Result<(), DispatchError> { + if let Some(ref mut data) = maybe_data { + *data += 1; + } + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + assert_eq!(EvmSystem::get(&account_id), data + 1); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data was providing +/// and returned data is `None`. As a result, the account has been removed. +#[test] +fn try_mutate_exists_account_removed() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 1; + let data = 1; + >::insert(account_id.clone(), AccountInfo { nonce, data }); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&account_id)); + + // Set mock expectations. + let on_killed_account_ctx = MockDummyOnKilledAccount::on_killed_account_context(); + on_killed_account_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(()); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&account_id, |maybe_data| -> Result<(), DispatchError> { + *maybe_data = None; + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(!EvmSystem::account_exists(&account_id)); + System::assert_has_event(RuntimeEvent::EvmSystem(Event::KilledAccount { + account: account_id, + })); + + // Assert mock invocations. + on_killed_account_ctx.checkpoint(); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case data wasn't providing +/// and returned data is `None`. As a result, the account hasn't been created. +#[test] +fn try_mutate_exists_account_not_created() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + + // Check test preconditions. + assert!(!EvmSystem::account_exists(&account_id)); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + >::try_mutate_exists(account_id, |maybe_data| -> Result<(), ()> { + *maybe_data = None; + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(!EvmSystem::account_exists(&account_id)); + }); +} + +/// This test verifies that try_mutate_exists works as expected in case getting error +/// during data mutation. +#[test] +fn try_mutate_exists_fails_without_changes() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let account_id = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 1; + let data = 1; + >::insert(account_id.clone(), AccountInfo { nonce, data }); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&account_id)); + + // Invoke the function under test. + assert_noop!( + >::try_mutate_exists(account_id, |maybe_data| -> Result<(), ()> { + *maybe_data = None; + Err(()) + }), + () + ); + + // Assert state changes. + assert!(EvmSystem::account_exists(&account_id)); + assert_eq!(EvmSystem::get(&account_id), data); + }); +}