diff --git a/Cargo.lock b/Cargo.lock index 1c1167d96db..e577313b6a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9336,8 +9336,6 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "staging-xcm", - "staging-xcm-executor", "xcm-primitives 0.1.1", ] diff --git a/client/rpc/debug/src/lib.rs b/client/rpc/debug/src/lib.rs index d846310d0a9..9797fb7fd4b 100644 --- a/client/rpc/debug/src/lib.rs +++ b/client/rpc/debug/src/lib.rs @@ -26,8 +26,10 @@ use ethereum_types::H256; use fc_rpc::{frontier_backend_client, internal_err}; use fc_storage::StorageOverride; use fp_rpc::EthereumRuntimeRPCApi; +use moonbeam_client_evm_tracing::formatters::call_tracer::CallTracerInner; use moonbeam_client_evm_tracing::types::block; use moonbeam_client_evm_tracing::types::block::BlockTransactionTrace; +use moonbeam_client_evm_tracing::types::single::TransactionTrace; use moonbeam_client_evm_tracing::{formatters::ResponseFormatter, types::single}; use moonbeam_rpc_core_types::{RequestBlockId, RequestBlockTag}; use moonbeam_rpc_primitives_debug::{DebugRuntimeApi, TracerInput}; @@ -38,6 +40,7 @@ use sp_block_builder::BlockBuilder; use sp_blockchain::{ Backend as BlockchainBackend, Error as BlockChainError, HeaderBackend, HeaderMetadata, }; +use sp_core::H160; use sp_runtime::{ generic::BlockId, traits::{BlakeTwo256, Block as BlockT, Header as HeaderT, UniqueSaturatedInto}, @@ -422,13 +425,32 @@ where .current_transaction_statuses(hash) .unwrap_or_default(); + // Partial ethereum transaction data to check if a trace match an ethereum transaction + struct EthTxPartial { + transaction_hash: H256, + from: H160, + to: Option, + } + // Known ethereum transaction hashes. - let eth_transactions_by_index: BTreeMap = statuses + let eth_transactions_by_index: BTreeMap = statuses .iter() - .map(|t| (t.transaction_index, t.transaction_hash)) + .map(|status| { + ( + status.transaction_index, + EthTxPartial { + transaction_hash: status.transaction_hash, + from: status.from, + to: status.to, + }, + ) + }) .collect(); - let eth_tx_hashes: Vec<_> = eth_transactions_by_index.values().cloned().collect(); + let eth_tx_hashes: Vec<_> = eth_transactions_by_index + .values() + .map(|tx| tx.transaction_hash) + .collect(); // If there are no ethereum transactions in the block return empty trace right away. if eth_tx_hashes.is_empty() { @@ -504,6 +526,9 @@ where Ok(moonbeam_rpc_primitives_debug::Response::Block) }; + // Offset to account for old buggy transactions that are in trace not in the ethereum block + let mut tx_position_offset = 0; + return match trace_type { single::TraceType::CallList => { let mut proxy = moonbeam_client_evm_tracing::listeners::CallList::default(); @@ -517,13 +542,58 @@ where .ok_or("Trace result is empty.") .map_err(|e| internal_err(format!("{:?}", e)))? .into_iter() - .map(|mut trace| { - if let Some(transaction_hash) = - eth_transactions_by_index.get(&trace.tx_position) + .filter_map(|mut trace: BlockTransactionTrace| { + if let Some(EthTxPartial { + transaction_hash, + from, + to, + }) = eth_transactions_by_index + .get(&(trace.tx_position - tx_position_offset)) { - trace.tx_hash = *transaction_hash; + // verify that the trace matches the ethereum transaction + let (trace_from, trace_to) = match trace.result { + TransactionTrace::Raw { .. } => { + (Default::default(), None) + } + TransactionTrace::CallList(_) => { + (Default::default(), None) + } + TransactionTrace::CallListNested(ref call) => { + match call { + single::Call::Blockscout(_) => { + (Default::default(), None) + } + single::Call::CallTracer(call) => ( + call.from, + match call.inner { + CallTracerInner::Call { + to, .. + } => Some(to), + CallTracerInner::Create { .. } => None, + CallTracerInner::SelfDestruct { + .. + } => None, + }, + ), + } + } + }; + if trace_from == *from && trace_to == *to { + trace.tx_hash = *transaction_hash; + Some(trace) + } else { + // if the trace does not match the ethereum transaction + // it means that the trace is about a buggy transaction that is not in the block + // we need to offset the tx_position + tx_position_offset += 1; + None + } + } else { + // If the transaction is not in the ethereum block + // it should not appear in the block trace + tx_position_offset += 1; + None } - trace }) .collect::>(); diff --git a/pallets/erc20-xcm-bridge/src/lib.rs b/pallets/erc20-xcm-bridge/src/lib.rs index 74ee8ee5142..79bf2efdfdb 100644 --- a/pallets/erc20-xcm-bridge/src/lib.rs +++ b/pallets/erc20-xcm-bridge/src/lib.rs @@ -170,18 +170,20 @@ pub mod pallet { match erc20s_origins.drain(contract_address, amount) { // We perform the evm transfers in a storage transaction to ensure that if one // of them fails all the changes of the previous evm calls are rolled back. - Ok(tokens_to_transfer) => tokens_to_transfer - .into_iter() - .try_for_each(|(from, subamount)| { - Self::erc20_transfer( - contract_address, - from, - beneficiary, - subamount, - gas_limit, - ) - }) - .map_err(Into::into), + Ok(tokens_to_transfer) => frame_support::storage::with_storage_layer(|| { + tokens_to_transfer + .into_iter() + .try_for_each(|(from, subamount)| { + Self::erc20_transfer( + contract_address, + from, + beneficiary, + subamount, + gas_limit, + ) + }) + }) + .map_err(Into::into), Err(DrainError::AssetNotFound) => Err(XcmError::AssetNotFound), Err(DrainError::NotEnoughFounds) => Err(XcmError::FailedToTransactAsset( "not enough founds in xcm holding", @@ -213,7 +215,11 @@ pub mod pallet { let gas_limit = Self::gas_limit_of_erc20_transfer(&asset.id); - Self::erc20_transfer(contract_address, from, to, amount, gas_limit)?; + // We perform the evm transfers in a storage transaction to ensure that if it fail + // any contract storage changes are rolled back. + frame_support::storage::with_storage_layer(|| { + Self::erc20_transfer(contract_address, from, to, amount, gas_limit) + })?; Ok(asset.clone().into()) } diff --git a/pallets/ethereum-xcm/Cargo.toml b/pallets/ethereum-xcm/Cargo.toml index 442fe65a861..8391936bb1c 100644 --- a/pallets/ethereum-xcm/Cargo.toml +++ b/pallets/ethereum-xcm/Cargo.toml @@ -35,18 +35,15 @@ fp-evm = { workspace = true } fp-rpc = { workspace = true } fp-self-contained = { workspace = true } pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } -pallet-ethereum = { workspace = true, features = [ "forbid-evm-reentrancy" ] } xcm-primitives = { workspace = true } -# Polkadot -xcm = { workspace = true } -xcm-executor = { workspace = true } - # Benchmarks frame-benchmarking = { workspace = true, optional = true } [dev-dependencies] pallet-evm-precompile-proxy = { workspace = true, features = [ "std" ] } + +pallet-ethereum = { workspace = true, features = [ "forbid-evm-reentrancy", "std" ] } pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy", "std" ] } pallet-proxy = { workspace = true, features = [ "std" ] } @@ -71,7 +68,6 @@ std = [ # Substrate FRAME "frame-support/std", "frame-system/std", - "pallet-ethereum/std", "pallet-evm/std", "pallet-timestamp/std", # Parity @@ -85,9 +81,6 @@ std = [ "sp-runtime/std", "sp-std/std", "xcm-primitives/std", - # Polkadot - "xcm/std", - "xcm-executor/std", ] runtime-benchmarks = [ "frame-benchmarking", @@ -95,7 +88,7 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-evm/runtime-benchmarks", - "pallet-ethereum/runtime-benchmarks", "xcm-primitives/runtime-benchmarks", + "pallet-ethereum/runtime-benchmarks" ] try-runtime = [ "frame-support/try-runtime" ] diff --git a/pallets/ethereum-xcm/src/lib.rs b/pallets/ethereum-xcm/src/lib.rs index 9d66ec726ea..9049cd593ac 100644 --- a/pallets/ethereum-xcm/src/lib.rs +++ b/pallets/ethereum-xcm/src/lib.rs @@ -26,7 +26,6 @@ mod mock; #[cfg(all(feature = "std", test))] mod tests; -mod transactional_processor; use ethereum_types::{H160, H256, U256}; use fp_ethereum::{TransactionData, ValidatedTransaction}; @@ -43,7 +42,6 @@ use scale_info::TypeInfo; use sp_runtime::{traits::UniqueSaturatedInto, DispatchErrorWithPostInfo, RuntimeDebug}; use sp_std::{marker::PhantomData, prelude::*}; -pub use self::transactional_processor::XcmEthTransactionalProcessor; pub use ethereum::{ AccessListItem, BlockV2 as Block, LegacyTransactionMessage, Log, ReceiptV3 as Receipt, TransactionAction, TransactionV2 as Transaction, @@ -374,23 +372,9 @@ impl Pallet { // transaction on chain - we increase the global nonce. >::put(current_nonce.saturating_add(U256::one())); - let (dispatch_info, execution_info) = + let (dispatch_info, _) = T::ValidatedTransaction::apply(source, transaction, maybe_force_create_address)?; - // If the transaction reverted, signal it to XCM Transactional Processor - match execution_info { - fp_evm::CallOrCreateInfo::Call(info) => { - if let fp_evm::ExitReason::Revert(_) = info.exit_reason { - XcmEthTransactionalProcessor::signal_evm_revert(); - } - } - fp_evm::CallOrCreateInfo::Create(info) => { - if let fp_evm::ExitReason::Revert(_) = info.exit_reason { - XcmEthTransactionalProcessor::signal_evm_revert(); - } - } - } - XCM_MESSAGE_HASH::with(|xcm_msg_hash| { Self::deposit_event(Event::ExecutedFromXcm { xcm_msg_hash: *xcm_msg_hash, diff --git a/pallets/ethereum-xcm/src/transactional_processor.rs b/pallets/ethereum-xcm/src/transactional_processor.rs deleted file mode 100644 index b2bb899975b..00000000000 --- a/pallets/ethereum-xcm/src/transactional_processor.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2019-2025 PureStake Inc. -// This file is part of Moonbeam. - -// Moonbeam is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. - -// Moonbeam is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. - -// You should have received a copy of the GNU General Public License -// along with Moonbeam. If not, see .$ - -use frame_support::storage::{with_transaction, TransactionOutcome}; -use sp_runtime::DispatchError; -use xcm::latest::prelude::*; -use xcm_executor::traits::ProcessTransaction; - -environmental::environmental!(IS_EVM_REVERT: bool); - -/// Transactional processor implementation used by the XCM executor -/// to execute each XCM instruction in a transactional way. -/// -/// Behave like FrameTransactionalProcessor except if the XCM instruction call the EVM AND the EVM Revert has occurred. -/// In this case, the storage changes should be committed to include the eth-xcm transaction in the "ethereum block storage". -pub struct XcmEthTransactionalProcessor; - -impl XcmEthTransactionalProcessor { - pub fn signal_evm_revert() { - IS_EVM_REVERT::with(|is_evm_revert| *is_evm_revert = true); - } -} - -impl ProcessTransaction for XcmEthTransactionalProcessor { - const IS_TRANSACTIONAL: bool = true; - - fn process(f: F) -> Result<(), XcmError> - where - F: FnOnce() -> Result<(), XcmError>, - { - IS_EVM_REVERT::using(&mut false, || { - with_transaction(|| -> TransactionOutcome> { - let output = f(); - match &output { - Ok(()) => TransactionOutcome::Commit(Ok(output)), - Err(xcm_error) => { - // If the XCM instruction failed from an EVM revert, - // we should not rollback storage change - if let Some(true) = IS_EVM_REVERT::with(|is_evm_revert| *is_evm_revert) { - TransactionOutcome::Commit(Ok(output)) - } else { - // Otherwise, we should rollback storage changes - // to be consistent with FrameTransactionalProcessor - TransactionOutcome::Rollback(Ok(Err(*xcm_error))) - } - } - } - }) - .map_err(|_| XcmError::ExceedsStackLimit)? - }) - } -} diff --git a/pallets/moonbeam-foreign-assets/src/lib.rs b/pallets/moonbeam-foreign-assets/src/lib.rs index 4a319828f9b..3509d7d9798 100644 --- a/pallets/moonbeam-foreign-assets/src/lib.rs +++ b/pallets/moonbeam-foreign-assets/src/lib.rs @@ -352,11 +352,15 @@ pub mod pallet { beneficiary: T::AccountId, amount: U256, ) -> Result<(), evm::EvmError> { - EvmCaller::::erc20_mint_into( - Self::contract_address_from_asset_id(asset_id), - T::AccountIdToH160::convert(beneficiary), - amount, - ) + // We perform the evm call in a storage transaction to ensure that if it fail + // any contract storage changes are rolled back. + frame_support::storage::with_storage_layer(|| { + EvmCaller::::erc20_mint_into( + Self::contract_address_from_asset_id(asset_id), + T::AccountIdToH160::convert(beneficiary), + amount, + ) + }) .map_err(Into::into) } @@ -367,6 +371,8 @@ pub mod pallet { spender: T::AccountId, amount: U256, ) -> Result<(), evm::EvmError> { + // We perform the evm call in a storage transaction to ensure that if it fail + // any contract storage changes are rolled back. EvmCaller::::erc20_approve( Self::contract_address_from_asset_id(asset_id), T::AccountIdToH160::convert(owner), @@ -668,7 +674,11 @@ pub mod pallet { let beneficiary = T::XcmLocationToH160::convert_location(who) .ok_or(MatchError::AccountIdConversionFailed)?; - EvmCaller::::erc20_mint_into(contract_address, beneficiary, amount)?; + // We perform the evm transfers in a storage transaction to ensure that if it fail + // any contract storage changes are rolled back. + frame_support::storage::with_storage_layer(|| { + EvmCaller::::erc20_mint_into(contract_address, beneficiary, amount) + })?; Ok(()) } @@ -694,7 +704,11 @@ pub mod pallet { let to = T::XcmLocationToH160::convert_location(to) .ok_or(MatchError::AccountIdConversionFailed)?; - EvmCaller::::erc20_transfer(contract_address, from, to, amount)?; + // We perform the evm transfers in a storage transaction to ensure that if it fail + // any contract storage changes are rolled back. + frame_support::storage::with_storage_layer(|| { + EvmCaller::::erc20_transfer(contract_address, from, to, amount) + })?; Ok(asset.clone().into()) } @@ -722,7 +736,11 @@ pub mod pallet { return Err(MatchError::AssetNotHandled.into()); } - EvmCaller::::erc20_burn_from(contract_address, who, amount)?; + // We perform the evm transfers in a storage transaction to ensure that if it fail + // any contract storage changes are rolled back. + frame_support::storage::with_storage_layer(|| { + EvmCaller::::erc20_burn_from(contract_address, who, amount) + })?; Ok(what.clone().into()) } diff --git a/runtime/moonbase/src/xcm_config.rs b/runtime/moonbase/src/xcm_config.rs index 538581bed54..beb6094c1f2 100644 --- a/runtime/moonbase/src/xcm_config.rs +++ b/runtime/moonbase/src/xcm_config.rs @@ -314,7 +314,7 @@ impl xcm_executor::Config for XcmExecutorConfig { type UniversalAliases = Nothing; type SafeCallFilter = SafeCallFilter; type Aliasers = Nothing; - type TransactionalProcessor = pallet_ethereum_xcm::XcmEthTransactionalProcessor; + type TransactionalProcessor = xcm_builder::FrameTransactionalProcessor; type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); diff --git a/runtime/moonbeam/src/xcm_config.rs b/runtime/moonbeam/src/xcm_config.rs index 80638c27e4e..199e94f6532 100644 --- a/runtime/moonbeam/src/xcm_config.rs +++ b/runtime/moonbeam/src/xcm_config.rs @@ -300,7 +300,7 @@ impl xcm_executor::Config for XcmExecutorConfig { type UniversalAliases = Nothing; type SafeCallFilter = SafeCallFilter; type Aliasers = Nothing; - type TransactionalProcessor = pallet_ethereum_xcm::XcmEthTransactionalProcessor; + type TransactionalProcessor = xcm_builder::FrameTransactionalProcessor; type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); diff --git a/runtime/moonriver/src/xcm_config.rs b/runtime/moonriver/src/xcm_config.rs index 48abce5101c..f2f1fe7c12c 100644 --- a/runtime/moonriver/src/xcm_config.rs +++ b/runtime/moonriver/src/xcm_config.rs @@ -308,7 +308,7 @@ impl xcm_executor::Config for XcmExecutorConfig { type UniversalAliases = Nothing; type SafeCallFilter = SafeCallFilter; type Aliasers = Nothing; - type TransactionalProcessor = pallet_ethereum_xcm::XcmEthTransactionalProcessor; + type TransactionalProcessor = xcm_builder::FrameTransactionalProcessor; type HrmpNewChannelOpenRequestHandler = (); type HrmpChannelAcceptedHandler = (); type HrmpChannelClosingHandler = (); diff --git a/scripts/run-tracing-tests.sh b/scripts/run-tracing-tests.sh index f82b4985c8a..88bb3f58907 100755 --- a/scripts/run-tracing-tests.sh +++ b/scripts/run-tracing-tests.sh @@ -1,38 +1,12 @@ #!/bin/bash -# Default values -BUILD_LAST_TRACING_RUNTIME="no" - -# Parse command line arguments -while [[ $# -gt 0 ]]; do - case $1 in - -f|--force-rebuild) - BUILD_LAST_TRACING_RUNTIME="yes" - shift - ;; - -h|--help) - echo "Usage: $0 [options]" - echo "Options:" - echo " -f, --force-rebuild Force rebuild of the tracing runtime" - echo " -h, --help Show this help message" - exit 0 - ;; - *) - echo "Unknown option: $1" - echo "Use -h or --help for usage information" - exit 1 - ;; - esac -done +pnpm i -# Install js dependencies -pnpm install +BUILD_LAST_TRACING_RUNTIME="no" if [ -e test/moonbase-overrides/moonbase-runtime-local-substitute-tracing.wasm ]; then - if [[ "$BUILD_LAST_TRACING_RUNTIME" == "yes" ]]; then - echo "Forcing rebuild of tracing runtime..." - else - echo "Using existing tracing runtime. Use -f to force rebuild." + if [[ "$1" == "-f" ]]; then + BUILD_LAST_TRACING_RUNTIME="yes" fi else BUILD_LAST_TRACING_RUNTIME="yes" @@ -42,7 +16,11 @@ if [[ "$BUILD_LAST_TRACING_RUNTIME" == "yes" ]]; then ./scripts/build-last-tracing-runtime.sh mkdir -p test/moonbase-overrides/ mv build/wasm/moonbase-runtime-local-substitute-tracing.wasm test/moonbase-overrides/ +else + echo "The tracing runtime is not rebuilt, if you want to rebuild it, use the option '-f'." fi echo "Run tracing tests…" -(cd test && pnpm install && pnpm compile-solidity && pnpm moonwall test dev_moonbase_tracing) +cd test || exit +pnpm moonwall test dev_moonbase_tracing +cd .. diff --git a/test/suites/tracing-tests/test-trace-erc20-xcm-2.ts b/test/suites/tracing-tests/test-trace-erc20-xcm-2.ts new file mode 100644 index 00000000000..5bbcae95072 --- /dev/null +++ b/test/suites/tracing-tests/test-trace-erc20-xcm-2.ts @@ -0,0 +1,180 @@ +import { beforeAll, customDevRpcRequest, describeSuite, expect } from "@moonwall/cli"; +import { ALITH_ADDRESS, CHARLETH_ADDRESS, alith } from "@moonwall/util"; +import { hexToNumber, parseEther } from "viem"; +import { + ERC20_TOTAL_SUPPLY, + XcmFragment, + type XcmFragmentConfig, + expectEVMResult, + injectEncodedHrmpMessageAndSeal, + injectHrmpMessage, + injectHrmpMessageAndSeal, + sovereignAccountOfSibling, +} from "../../helpers"; + +describeSuite({ + id: "T21", + title: "Trace ERC20 xcm #2", + foundationMethods: "dev", + testCases: ({ context, it }) => { + let erc20ContractAddress: string; + let eventEmitterAddress: `0x${string}`; + let ethXcmTxHash: string; + let regularEthTxHash: string; + beforeAll(async () => { + const { contractAddress, status } = await context.deployContract!("ERC20WithInitialSupply", { + args: ["ERC20", "20S", ALITH_ADDRESS, ERC20_TOTAL_SUPPLY], + }); + erc20ContractAddress = contractAddress; + expect(status).eq("success"); + + const paraId = 888; + const paraSovereign = sovereignAccountOfSibling(context, paraId); + const amountTransferred = 1_000_000n; + + // Get pallet indices + const metadata = await context.polkadotJs().rpc.state.getMetadata(); + const balancesPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() === "Balances")! + .index.toNumber(); + const erc20XcmPalletIndex = metadata.asLatest.pallets + .find(({ name }) => name.toString() === "Erc20XcmBridge")! + .index.toNumber(); + + // Send some native tokens to the sovereign account of paraId (to pay fees) + await context + .polkadotJs() + .tx.balances.transferAllowDeath(paraSovereign, parseEther("1")) + .signAndSend(alith); + await context.createBlock(); + + // Send some erc20 tokens to the sovereign account of paraId + const rawTx = await context.writeContract!({ + contractName: "ERC20WithInitialSupply", + contractAddress: erc20ContractAddress as `0x${string}`, + functionName: "transfer", + args: [paraSovereign, amountTransferred], + rawTxOnly: true, + }); + const { result } = await context.createBlock(rawTx); + expectEVMResult(result!.events, "Succeed"); + expect( + await context.readContract!({ + contractName: "ERC20WithInitialSupply", + contractAddress: erc20ContractAddress as `0x${string}`, + functionName: "balanceOf", + args: [paraSovereign], + }) + ).equals(amountTransferred); + + // Create an XCM message that try to transfer more than available + const failedConfig: XcmFragmentConfig = { + assets: [ + { + multilocation: { + parents: 0, + interior: { + X1: { PalletInstance: Number(balancesPalletIndex) }, + }, + }, + fungible: 1_700_000_000_000_000n, + }, + { + multilocation: { + parents: 0, + interior: { + X2: [ + { + PalletInstance: erc20XcmPalletIndex, + }, + { + AccountKey20: { + network: null, + key: erc20ContractAddress, + }, + }, + ], + }, + }, + fungible: amountTransferred * 2n, // Try to transfer twice the available amount + }, + ], + beneficiary: CHARLETH_ADDRESS, + }; + + const failedXcmMessage = new XcmFragment(failedConfig) + .withdraw_asset() + .clear_origin() + .buy_execution() + .deposit_asset(2n) + .as_v3(); + + // Mock the reception of the failed xcm message N times + // N should be high enough to fill IdleMaxServiceWeigh + // The goal is to have at least one XCM message queud for next block on_initialize + for (let i = 0; i < 20; i++) { + await injectHrmpMessage(context, paraId, { + type: "XcmVersionedXcm", + payload: failedXcmMessage, + }); + } + await context.createBlock(); + + // By calling deployContract() a new block will be created, + // including the ethereum-xcm transaction (on_initialize) + regular ethereum transaction + const { contractAddress: eventEmitterAddress_ } = await context.deployContract!( + "EventEmitter", + { + from: alith.address, + } as any + ); + eventEmitterAddress = eventEmitterAddress_; + + // The old buggy runtime rollback the eth-xcm tx because XCM executor rollback evm reverts + regularEthTxHash = (await context.viem().getBlock()).transactions[0]; + + // Get the latest block events + const block = await context.polkadotJs().rpc.chain.getBlock(); + const allRecords = await context.polkadotJs().query.system.events.at(block.block.header.hash); + + // Compute XCM message ID + const messageHash = context.polkadotJs().createType("XcmVersionedXcm", failedXcmMessage).hash; + + // Find messageQueue.Processed event with matching message ID + const processedEvent = allRecords.find( + ({ event }) => + event.section === "messageQueue" && + event.method === "Processed" && + event.data[0].toString() === messageHash.toHex() + ); + + expect(processedEvent).to.not.be.undefined; + }); + + // IMPORTANT: this test will fail once we will merge https://github.com/moonbeam-foundation/moonbeam/pull/3258 + it({ + id: "T01", + title: "should doesn't include the failed ERC20 xcm transaction in block trace", + test: async function () { + const number = await context.viem().getBlockNumber(); + const trace = await customDevRpcRequest("debug_traceBlockByNumber", [ + number.toString(), + { tracer: "callTracer" }, + ]); + + // Verify that only the regular eth transaction is included in the block trace. + expect(trace.length).to.eq(1); + + // 1st traced transaction is regular ethereum transaction. + // - `From` is Alith's adddress. + // - `To` is the ethereum contract address. + const txHash = trace[0].txHash; + expect(txHash).to.eq(regularEthTxHash); + const call = trace[0].result; + expect(call.from).to.eq(alith.address.toLowerCase()); + expect(call.to).to.eq(eventEmitterAddress.toLowerCase()); + expect(call.type).be.eq("CREATE"); + }, + }); + }, +}); diff --git a/test/suites/tracing-tests/test-trace-erc20-xcm.ts b/test/suites/tracing-tests/test-trace-erc20-xcm.ts index 702bdea67fb..d9cad7e58bc 100644 --- a/test/suites/tracing-tests/test-trace-erc20-xcm.ts +++ b/test/suites/tracing-tests/test-trace-erc20-xcm.ts @@ -6,6 +6,7 @@ import { XcmFragment, type XcmFragmentConfig, expectEVMResult, + injectHrmpMessage, injectHrmpMessageAndSeal, sovereignAccountOfSibling, } from "../../helpers"; @@ -17,8 +18,8 @@ describeSuite({ testCases: ({ context, it }) => { let erc20ContractAddress: string; let transactionHash: string; - let failedTransactionHash: string; - + let eventEmitterAddress: `0x${string}`; + let createTransactionHash: string; beforeAll(async () => { const { contractAddress, status } = await context.deployContract!("ERC20WithInitialSupply", { args: ["ERC20", "20S", ALITH_ADDRESS, ERC20_TOTAL_SUPPLY], @@ -168,11 +169,23 @@ describeSuite({ .as_v3(); // Mock the reception of the failed xcm message - await injectHrmpMessageAndSeal(context, paraId, { + await injectHrmpMessage(context, paraId, { type: "XcmVersionedXcm", payload: failedXcmMessage, }); + // By calling deployContract() a new block will be created, + // including the ethereum xcm transaction + regular ethereum transaction + const { contractAddress: eventEmitterAddress_ } = await context.deployContract!( + "EventEmitter", + { + from: alith.address, + } as any + ); + eventEmitterAddress = eventEmitterAddress_; + + createTransactionHash = (await context.viem().getBlock()).transactions[0]; + // Get the latest block events const block = await context.polkadotJs().rpc.chain.getBlock(); const allRecords = await context.polkadotJs().query.system.events.at(block.block.header.hash); @@ -189,8 +202,6 @@ describeSuite({ ); expect(processedEvent).to.not.be.undefined; - - failedTransactionHash = (await context.viem().getBlock()).transactions[0]; }); it({ @@ -211,30 +222,29 @@ describeSuite({ }, }); + // IMPORTANT: this test will fail once we will merge https://github.com/moonbeam-foundation/moonbeam/pull/3258 it({ id: "T02", - title: "should trace ERC20 xcm transaction even if it fail", + title: "should doesn't include the failed ERC20 xcm transaction in block trace", test: async function () { - const receipt = await context - .viem() - .getTransactionReceipt({ hash: failedTransactionHash as `0x${string}` }); - - // Verify the transaction failed - expect(receipt.status).toBe("reverted"); - - // Attempt to trace the failed transaction - const trace = await customDevRpcRequest("debug_traceTransaction", [ - failedTransactionHash, + const number = await context.viem().getBlockNumber(); + const trace = await customDevRpcRequest("debug_traceBlockByNumber", [ + number.toString(), { tracer: "callTracer" }, ]); - // Verify we got a trace back - expect(trace).toBeDefined(); - expect(trace.gasUsed).toBeDefined(); - - // The traced gas used should be greater than or equal to the one in the receipt - // since tracing doesn't account for gas refunds - expect(hexToNumber(trace.gasUsed)).gte(Number(receipt.gasUsed)); + // Verify that only the regular eth transaction is included in the block trace. + expect(trace.length).to.eq(1); + + // 1st traced transaction is regular ethereum transaction. + // - `From` is Alith's adddress. + // - `To` is the ethereum contract address. + const txHash = trace[0].txHash; + expect(txHash).to.eq(createTransactionHash); + const call = trace[0].result; + expect(call.from).to.eq(alith.address.toLowerCase()); + expect(call.to).to.eq(eventEmitterAddress.toLowerCase()); + expect(call.type).be.eq("CREATE"); }, }); },