diff --git a/crates/humanode-runtime/src/frontier_precompiles.rs b/crates/humanode-runtime/src/frontier_precompiles.rs index f22f85ba3..1daa3f030 100644 --- a/crates/humanode-runtime/src/frontier_precompiles.rs +++ b/crates/humanode-runtime/src/frontier_precompiles.rs @@ -19,7 +19,7 @@ use precompile_utils::EvmData; use sp_core::{H160, U256}; use sp_std::marker::PhantomData; -use crate::{evm_swap, ConstU64}; +use crate::{evm_swap, ConstU64, EvmAccountId}; /// A set of constant values used to indicate precompiles. pub mod precompiles_constants { @@ -187,6 +187,12 @@ where } } +impl pallet_evm_system::IsPrecompile for FrontierPrecompiles { + fn is_precompile(account_id: &EvmAccountId) -> bool { + Self::used_addresses().contains(account_id) + } +} + pub struct FrontierPrecompilesAddresses(PhantomData); impl sp_core::TypedGet for FrontierPrecompilesAddresses { diff --git a/crates/humanode-runtime/src/lib.rs b/crates/humanode-runtime/src/lib.rs index 6e9e561f7..1f64c6413 100644 --- a/crates/humanode-runtime/src/lib.rs +++ b/crates/humanode-runtime/src/lib.rs @@ -615,6 +615,7 @@ impl pallet_evm_system::Config for Runtime { type AccountId = EvmAccountId; type Index = Index; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = FrontierPrecompiles; type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/pallet-currency-swap/src/mock.rs b/crates/pallet-currency-swap/src/mock.rs index e76cf0a2e..8b09e2e64 100644 --- a/crates/pallet-currency-swap/src/mock.rs +++ b/crates/pallet-currency-swap/src/mock.rs @@ -89,6 +89,7 @@ impl pallet_evm_system::Config for Test { type AccountId = EvmAccountId; type Index = u64; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/pallet-dummy-precompiles-code/src/mock/v0.rs b/crates/pallet-dummy-precompiles-code/src/mock/v0.rs index 7563ef317..73a664f8a 100644 --- a/crates/pallet-dummy-precompiles-code/src/mock/v0.rs +++ b/crates/pallet-dummy-precompiles-code/src/mock/v0.rs @@ -94,6 +94,7 @@ impl pallet_evm_system::Config for Test { type AccountId = EvmAccountId; type Index = u64; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/pallet-dummy-precompiles-code/src/mock/v1.rs b/crates/pallet-dummy-precompiles-code/src/mock/v1.rs index 4f4e069b7..66910e020 100644 --- a/crates/pallet-dummy-precompiles-code/src/mock/v1.rs +++ b/crates/pallet-dummy-precompiles-code/src/mock/v1.rs @@ -100,6 +100,7 @@ impl pallet_evm_system::Config for Test { type AccountId = EvmAccountId; type Index = u64; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/pallet-evm-balances/src/mock.rs b/crates/pallet-evm-balances/src/mock.rs index efe1e8e3f..74b2498cc 100644 --- a/crates/pallet-evm-balances/src/mock.rs +++ b/crates/pallet-evm-balances/src/mock.rs @@ -92,6 +92,7 @@ impl pallet_evm_system::Config for Test { type AccountId = u64; type Index = u64; type AccountData = AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/pallet-evm-system/src/lib.rs b/crates/pallet-evm-system/src/lib.rs index 172e651c5..f0ef06cfb 100644 --- a/crates/pallet-evm-system/src/lib.rs +++ b/crates/pallet-evm-system/src/lib.rs @@ -72,6 +72,9 @@ pub mod pallet { /// pallet does regardless). type AccountData: Member + FullCodec + Clone + Default + TypeInfo + MaxEncodedLen; + /// Checks whether a given account is a precompile or not. + type IsPrecompile: IsPrecompile<::AccountId>; + /// Handler for when a new account has just been created. type OnNewAccount: OnNewAccount<::AccountId>; @@ -196,7 +199,7 @@ impl StoredMap<::AccountId, ::AccountData> Account::::mutate(k, |a| a.data = data); } (None, true) => { - if nonce != ::Index::zero() { + if nonce != ::Index::zero() || T::IsPrecompile::is_precompile(k) { Account::::mutate(k, |a| a.data = Default::default()); } else { Account::::remove(k); @@ -233,6 +236,18 @@ impl fp_evm::AccountProvider for Pallet { } } +/// A trait that allows checking whether a given account is a precompile or not. +pub trait IsPrecompile { + /// Check if the given account is a precompile. + fn is_precompile(account_id: &AccountId) -> bool; +} + +impl IsPrecompile for () { + fn is_precompile(_account_id: &AccountId) -> bool { + false + } +} + /// Interface to handle account creation. pub trait OnNewAccount { /// A new account `who` has been registered. diff --git a/crates/pallet-evm-system/src/mock.rs b/crates/pallet-evm-system/src/mock.rs index 8beb62295..f39ee285b 100644 --- a/crates/pallet-evm-system/src/mock.rs +++ b/crates/pallet-evm-system/src/mock.rs @@ -69,11 +69,21 @@ impl frame_system::Config for Test { type MaxConsumers = ConstU32<16>; } +mock! { + #[derive(Debug)] + pub IsPrecompile {} + + impl IsPrecompile for IsPrecompile { + pub fn is_precompile(who: &H160) -> bool; + } +} + impl pallet_evm_system::Config for Test { type RuntimeEvent = RuntimeEvent; type AccountId = H160; type Index = u64; type AccountData = u64; + type IsPrecompile = MockIsPrecompile; type OnNewAccount = MockDummyOnNewAccount; type OnKilledAccount = MockDummyOnKilledAccount; } diff --git a/crates/pallet-evm-system/src/tests.rs b/crates/pallet-evm-system/src/tests.rs index c8d6c51fd..f9e378a78 100644 --- a/crates/pallet-evm-system/src/tests.rs +++ b/crates/pallet-evm-system/src/tests.rs @@ -233,6 +233,13 @@ fn try_mutate_exists_account_removed() { assert!(EvmSystem::account_exists(&account_id)); // Set mock expectations. + let is_precompile_ctx = MockIsPrecompile::is_precompile_context(); + is_precompile_ctx + .expect() + .once() + .with(predicate::eq(account_id)) + .return_const(false); + let on_killed_account_ctx = MockDummyOnKilledAccount::on_killed_account_context(); on_killed_account_ctx .expect() @@ -257,6 +264,7 @@ fn try_mutate_exists_account_removed() { })); // Assert mock invocations. + is_precompile_ctx.checkpoint(); on_killed_account_ctx.checkpoint(); }); } @@ -296,6 +304,55 @@ fn try_mutate_exists_account_retained() { }); } +/// This test verifies that `try_mutate_exists` works as expected in case data was providing +/// and returned data is `None`, account is precompiled. As a result, the account has been retained. +#[test] +fn try_mutate_exists_precompiled_account_retained() { + new_test_ext().execute_with_ext(|_| { + // Prepare test data. + let precompile = H160::from_str("1000000000000000000000000000000000000001").unwrap(); + let nonce = 0; + let data = 100; + + let account_info = AccountInfo { nonce, data }; + >::insert(precompile, account_info); + + // Check test preconditions. + assert!(EvmSystem::account_exists(&precompile)); + + // Set mock expectations. + let is_precompile_ctx = MockIsPrecompile::is_precompile_context(); + is_precompile_ctx + .expect() + .once() + .with(predicate::eq(precompile)) + .return_const(true); + + // Set block number to enable events. + System::set_block_number(1); + + // Invoke the function under test. + EvmSystem::try_mutate_exists(&precompile, |maybe_data| -> Result<(), DispatchError> { + *maybe_data = None; + Ok(()) + }) + .unwrap(); + + // Assert state changes. + assert!(EvmSystem::account_exists(&precompile)); + assert_eq!(>::get(precompile), AccountInfo::default()); + + // Assert that there is no a corresponding `KilledAccount` event. + assert!(System::events().iter().all(|record| record.event + != RuntimeEvent::EvmSystem(Event::KilledAccount { + account: precompile, + }))); + + // Assert mock invocations. + is_precompile_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] diff --git a/crates/pallet-native-to-evm-swap/src/mock.rs b/crates/pallet-native-to-evm-swap/src/mock.rs index 2b755dfd7..6cbc7e5a4 100644 --- a/crates/pallet-native-to-evm-swap/src/mock.rs +++ b/crates/pallet-native-to-evm-swap/src/mock.rs @@ -100,6 +100,7 @@ impl pallet_evm_system::Config for Test { type AccountId = EvmAccountId; type Index = u64; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/precompile-evm-to-native-swap/src/mock.rs b/crates/precompile-evm-to-native-swap/src/mock.rs index aaf37c79f..c561fb484 100644 --- a/crates/precompile-evm-to-native-swap/src/mock.rs +++ b/crates/precompile-evm-to-native-swap/src/mock.rs @@ -98,6 +98,7 @@ impl pallet_evm_system::Config for Test { type AccountId = EvmAccountId; type Index = u64; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/crates/precompile-native-currency/src/mock.rs b/crates/precompile-native-currency/src/mock.rs index 103556e00..37548bfd7 100644 --- a/crates/precompile-native-currency/src/mock.rs +++ b/crates/precompile-native-currency/src/mock.rs @@ -104,6 +104,7 @@ impl pallet_evm_system::Config for Test { type AccountId = EvmAccountId; type Index = u64; type AccountData = pallet_evm_balances::AccountData; + type IsPrecompile = (); type OnNewAccount = (); type OnKilledAccount = (); } diff --git a/utils/e2e-tests/ts/lib/substrateUtils.ts b/utils/e2e-tests/ts/lib/substrateUtils.ts index dc902396b..a6fcda382 100644 --- a/utils/e2e-tests/ts/lib/substrateUtils.ts +++ b/utils/e2e-tests/ts/lib/substrateUtils.ts @@ -1,6 +1,7 @@ //! Common substrate utils. import * as substrate from "../lib/substrate"; +import { AnyJson } from "@polkadot/types-codec/types"; type SystemAccount = { data: { @@ -22,3 +23,25 @@ export const getNativeBalance = async ( // We should explicitly convert to native bigint for math operations. return BigInt(free); }; + +type Event = { + event: { + method: string; + section: string; + data: AnyJson; + }; +}; + +/// A helper function to get events at specified block. +export const getEvents = async ( + substrateApi: substrate.Api, + blockNumber: bigint, +) => { + const blockHash = await substrateApi.rpc.chain.getBlockHash(blockNumber); + const substrateApiAt = await substrateApi.at(blockHash); + const events = (await substrateApiAt.query["system"]?.[ + "events" + ]?.()) as unknown as Event[]; + + return events; +}; diff --git a/utils/e2e-tests/ts/tests/swap/evmToNative.ts b/utils/e2e-tests/ts/tests/swap/evmToNative.ts index 9c5abeff4..8927caead 100644 --- a/utils/e2e-tests/ts/tests/swap/evmToNative.ts +++ b/utils/e2e-tests/ts/tests/swap/evmToNative.ts @@ -6,7 +6,7 @@ import { beforeEachWithCleanup } from "../../lib/lifecycle"; import evmSwap from "../../lib/abis/evmSwap"; import { decodeEventLog } from "viem"; import * as substrate from "../../lib/substrate"; -import { getNativeBalance } from "../../lib/substrateUtils"; +import { getEvents, getNativeBalance } from "../../lib/substrateUtils"; const evmToNativeSwapPrecompileAddress = "0x0000000000000000000000000000000000000900"; @@ -136,5 +136,27 @@ describe("evm to native tokens swap", () => { address: evmToNativeSwapPrecompileAddress, }); expect(evmSwapPrecompileBalance).toEqual(0n); + + const substrateEvents = await getEvents( + substrateApi, + swapTxReceipt.blockNumber, + ); + + substrateEvents.forEach((item) => { + const section = item.event.section; + const method = item.event.method; + const data = JSON.stringify(item.event.data); + + expect([section, method, data]).not.toEqual([ + "evmSystem", + "NewAccount", + JSON.stringify([evmToNativeSwapPrecompileAddress]), + ]); + expect([section, method, data]).not.toEqual([ + "evmSystem", + "KilledAccount", + JSON.stringify([evmToNativeSwapPrecompileAddress]), + ]); + }); }); });