pallet_ethereum_xcm/
lib.rs

1// Copyright 2019-2025 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17//! # Ethereum Xcm pallet
18//!
19//! The Xcm Ethereum pallet is a bridge for Xcm Transact to Ethereum pallet
20
21// Ensure we're `no_std` when compiling for Wasm.
22#![cfg_attr(not(feature = "std"), no_std)]
23#![allow(clippy::comparison_chain, clippy::large_enum_variant)]
24
25#[cfg(test)]
26mod mock;
27#[cfg(test)]
28mod tests;
29
30use ethereum_types::{H160, H256, U256};
31use fp_ethereum::{TransactionData, ValidatedTransaction};
32use fp_evm::{CheckEvmTransaction, CheckEvmTransactionConfig, TransactionValidationError};
33use frame_support::{
34	dispatch::{DispatchResultWithPostInfo, Pays, PostDispatchInfo},
35	traits::{EnsureOrigin, Get, ProcessMessage},
36	weights::Weight,
37};
38use frame_system::pallet_prelude::OriginFor;
39use pallet_evm::{AddressMapping, GasWeightMapping};
40use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen};
41use scale_info::TypeInfo;
42use sp_runtime::{traits::UniqueSaturatedInto, DispatchErrorWithPostInfo, RuntimeDebug};
43use sp_std::{marker::PhantomData, prelude::*};
44
45pub use ethereum::{
46	AccessListItem, BlockV3 as Block, LegacyTransactionMessage, Log, ReceiptV4 as Receipt,
47	TransactionAction, TransactionV3 as Transaction,
48};
49pub use fp_rpc::TransactionStatus;
50pub use xcm_primitives::{EnsureProxy, EthereumXcmTransaction, XcmToEthereum};
51
52#[derive(
53	PartialEq,
54	Eq,
55	Clone,
56	Encode,
57	Decode,
58	RuntimeDebug,
59	TypeInfo,
60	MaxEncodedLen,
61	DecodeWithMemTracking,
62)]
63pub enum RawOrigin {
64	XcmEthereumTransaction(H160),
65}
66
67pub fn ensure_xcm_ethereum_transaction<OuterOrigin>(o: OuterOrigin) -> Result<H160, &'static str>
68where
69	OuterOrigin: Into<Result<RawOrigin, OuterOrigin>>,
70{
71	match o.into() {
72		Ok(RawOrigin::XcmEthereumTransaction(n)) => Ok(n),
73		_ => Err("bad origin: expected to be a xcm Ethereum transaction"),
74	}
75}
76
77pub struct EnsureXcmEthereumTransaction;
78impl<O: Into<Result<RawOrigin, O>> + From<RawOrigin>> EnsureOrigin<O>
79	for EnsureXcmEthereumTransaction
80{
81	type Success = H160;
82	fn try_origin(o: O) -> Result<Self::Success, O> {
83		o.into().map(|o| match o {
84			RawOrigin::XcmEthereumTransaction(id) => id,
85		})
86	}
87
88	#[cfg(feature = "runtime-benchmarks")]
89	fn try_successful_origin() -> Result<O, ()> {
90		Ok(O::from(RawOrigin::XcmEthereumTransaction(
91			Default::default(),
92		)))
93	}
94}
95
96environmental::environmental!(XCM_MESSAGE_HASH: H256);
97
98pub struct MessageProcessorWrapper<Inner>(core::marker::PhantomData<Inner>);
99impl<Inner: ProcessMessage> ProcessMessage for MessageProcessorWrapper<Inner> {
100	type Origin = <Inner as ProcessMessage>::Origin;
101
102	fn process_message(
103		message: &[u8],
104		origin: Self::Origin,
105		meter: &mut frame_support::weights::WeightMeter,
106		id: &mut [u8; 32],
107	) -> Result<bool, frame_support::traits::ProcessMessageError> {
108		let mut xcm_msg_hash = H256(sp_io::hashing::blake2_256(message));
109		XCM_MESSAGE_HASH::using(&mut xcm_msg_hash, || {
110			Inner::process_message(message, origin, meter, id)
111		})
112	}
113}
114
115pub use self::pallet::*;
116
117#[frame_support::pallet(dev_mode)]
118pub mod pallet {
119	use super::*;
120	use fp_evm::AccountProvider;
121	use frame_support::pallet_prelude::*;
122
123	#[pallet::config]
124	pub trait Config:
125		frame_system::Config<RuntimeEvent: From<Event<Self>>> + pallet_evm::Config
126	{
127		/// Invalid transaction error
128		type InvalidEvmTransactionError: From<TransactionValidationError>;
129		/// Handler for applying an already validated transaction
130		type ValidatedTransaction: ValidatedTransaction;
131		/// Origin for xcm transact
132		type XcmEthereumOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = H160>;
133		/// Maximum Weight reserved for xcm in a block
134		type ReservedXcmpWeight: Get<Weight>;
135		/// Ensure proxy
136		type EnsureProxy: EnsureProxy<
137			<<Self as pallet_evm::Config>::AccountProvider as AccountProvider>::AccountId,
138		>;
139		/// The origin that is allowed to resume or suspend the XCM to Ethereum executions.
140		type ControllerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
141		/// An origin that can submit a create tx type
142		type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
143	}
144
145	#[pallet::pallet]
146	#[pallet::without_storage_info]
147	pub struct Pallet<T>(PhantomData<T>);
148
149	/// Global nonce used for building Ethereum transaction payload.
150	#[pallet::storage]
151	#[pallet::getter(fn nonce)]
152	pub(crate) type Nonce<T: Config> = StorageValue<_, U256, ValueQuery>;
153
154	/// Whether or not Ethereum-XCM is suspended from executing
155	#[pallet::storage]
156	#[pallet::getter(fn ethereum_xcm_suspended)]
157	pub(super) type EthereumXcmSuspended<T: Config> = StorageValue<_, bool, ValueQuery>;
158
159	#[pallet::origin]
160	pub type Origin = RawOrigin;
161
162	#[pallet::error]
163	pub enum Error<T> {
164		/// Xcm to Ethereum execution is suspended
165		EthereumXcmExecutionSuspended,
166	}
167
168	#[pallet::event]
169	#[pallet::generate_deposit(pub(super) fn deposit_event)]
170	pub enum Event<T> {
171		/// Ethereum transaction executed from XCM
172		ExecutedFromXcm {
173			xcm_msg_hash: H256,
174			eth_tx_hash: H256,
175		},
176	}
177
178	#[pallet::call]
179	impl<T: Config> Pallet<T>
180	where
181		OriginFor<T>: Into<Result<RawOrigin, OriginFor<T>>>,
182	{
183		/// Xcm Transact an Ethereum transaction.
184		/// Weight: Gas limit plus the db read involving the suspension check
185		#[pallet::weight({
186			let without_base_extrinsic_weight = false;
187			<T as pallet_evm::Config>::GasWeightMapping::gas_to_weight({
188				match xcm_transaction {
189					EthereumXcmTransaction::V1(v1_tx) =>  v1_tx.gas_limit.unique_saturated_into(),
190					EthereumXcmTransaction::V2(v2_tx) =>  v2_tx.gas_limit.unique_saturated_into(),
191					EthereumXcmTransaction::V3(v3_tx) =>  v3_tx.gas_limit.unique_saturated_into(),
192				}
193			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(1))
194		})]
195		pub fn transact(
196			origin: OriginFor<T>,
197			xcm_transaction: EthereumXcmTransaction,
198		) -> DispatchResultWithPostInfo {
199			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
200			ensure!(
201				!EthereumXcmSuspended::<T>::get(),
202				DispatchErrorWithPostInfo {
203					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
204					post_info: PostDispatchInfo {
205						actual_weight: Some(T::DbWeight::get().reads(1)),
206						pays_fee: Pays::Yes
207					}
208				}
209			);
210			Self::validate_and_apply(source, xcm_transaction, false, None)
211		}
212
213		/// Xcm Transact an Ethereum transaction through proxy.
214		/// Weight: Gas limit plus the db reads involving the suspension and proxy checks
215		#[pallet::weight({
216			let without_base_extrinsic_weight = false;
217			<T as pallet_evm::Config>::GasWeightMapping::gas_to_weight({
218				match xcm_transaction {
219					EthereumXcmTransaction::V1(v1_tx) =>  v1_tx.gas_limit.unique_saturated_into(),
220					EthereumXcmTransaction::V2(v2_tx) =>  v2_tx.gas_limit.unique_saturated_into(),
221					EthereumXcmTransaction::V3(v3_tx) =>  v3_tx.gas_limit.unique_saturated_into(),
222				}
223			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(2))
224		})]
225		pub fn transact_through_proxy(
226			origin: OriginFor<T>,
227			transact_as: H160,
228			xcm_transaction: EthereumXcmTransaction,
229		) -> DispatchResultWithPostInfo {
230			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
231			ensure!(
232				!EthereumXcmSuspended::<T>::get(),
233				DispatchErrorWithPostInfo {
234					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
235					post_info: PostDispatchInfo {
236						actual_weight: Some(T::DbWeight::get().reads(1)),
237						pays_fee: Pays::Yes
238					}
239				}
240			);
241			let _ = T::EnsureProxy::ensure_ok(
242				T::AddressMapping::into_account_id(transact_as),
243				T::AddressMapping::into_account_id(source),
244			)
245			.map_err(|e| sp_runtime::DispatchErrorWithPostInfo {
246				post_info: PostDispatchInfo {
247					actual_weight: Some(T::DbWeight::get().reads(2)),
248					pays_fee: Pays::Yes,
249				},
250				error: sp_runtime::DispatchError::Other(e),
251			})?;
252
253			Self::validate_and_apply(transact_as, xcm_transaction, false, None)
254		}
255
256		/// Suspends all Ethereum executions from XCM.
257		///
258		/// - `origin`: Must pass `ControllerOrigin`.
259		#[pallet::weight((T::DbWeight::get().writes(1), DispatchClass::Operational,))]
260		pub fn suspend_ethereum_xcm_execution(origin: OriginFor<T>) -> DispatchResult {
261			T::ControllerOrigin::ensure_origin(origin)?;
262
263			EthereumXcmSuspended::<T>::put(true);
264
265			Ok(())
266		}
267
268		/// Resumes all Ethereum executions from XCM.
269		///
270		/// - `origin`: Must pass `ControllerOrigin`.
271		#[pallet::weight((T::DbWeight::get().writes(1), DispatchClass::Operational,))]
272		pub fn resume_ethereum_xcm_execution(origin: OriginFor<T>) -> DispatchResult {
273			T::ControllerOrigin::ensure_origin(origin)?;
274
275			EthereumXcmSuspended::<T>::put(false);
276
277			Ok(())
278		}
279
280		/// Xcm Transact an Ethereum transaction, but allow to force the caller and create address.
281		/// This call should be restricted (callable only by the runtime or governance).
282		/// Weight: Gas limit plus the db reads involving the suspension and proxy checks
283		#[pallet::weight({
284			let without_base_extrinsic_weight = false;
285			<T as pallet_evm::Config>::GasWeightMapping::gas_to_weight({
286				match xcm_transaction {
287					EthereumXcmTransaction::V1(v1_tx) => v1_tx.gas_limit.unique_saturated_into(),
288					EthereumXcmTransaction::V2(v2_tx) => v2_tx.gas_limit.unique_saturated_into(),
289					EthereumXcmTransaction::V3(v3_tx) => v3_tx.gas_limit.unique_saturated_into(),
290				}
291			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(1))
292		})]
293		pub fn force_transact_as(
294			origin: OriginFor<T>,
295			transact_as: H160,
296			xcm_transaction: EthereumXcmTransaction,
297			force_create_address: Option<H160>,
298		) -> DispatchResultWithPostInfo {
299			T::ForceOrigin::ensure_origin(origin)?;
300			ensure!(
301				!EthereumXcmSuspended::<T>::get(),
302				DispatchErrorWithPostInfo {
303					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
304					post_info: PostDispatchInfo {
305						actual_weight: Some(T::DbWeight::get().reads(1)),
306						pays_fee: Pays::Yes
307					}
308				}
309			);
310
311			Self::validate_and_apply(transact_as, xcm_transaction, true, force_create_address)
312		}
313	}
314}
315
316impl<T: Config> Pallet<T> {
317	fn transaction_len(transaction: &Transaction) -> u64 {
318		transaction
319			.encode()
320			.len()
321			// pallet + call indexes
322			.saturating_add(2) as u64
323	}
324
325	fn validate_and_apply(
326		source: H160,
327		xcm_transaction: EthereumXcmTransaction,
328		allow_create: bool,
329		maybe_force_create_address: Option<H160>,
330	) -> DispatchResultWithPostInfo {
331		// The lack of a real signature where different callers with the
332		// same nonce are providing identical transaction payloads results in a collision and
333		// the same ethereum tx hash.
334		// We use a global nonce instead the user nonce for all Xcm->Ethereum transactions to avoid
335		// this.
336		let current_nonce = Self::nonce();
337		let error_weight = T::DbWeight::get().reads(1);
338
339		let transaction: Option<Transaction> =
340			xcm_transaction.into_transaction(current_nonce, T::ChainId::get(), allow_create);
341		if let Some(transaction) = transaction {
342			let tx_hash = transaction.hash();
343			let transaction_data: TransactionData = (&transaction).into();
344
345			let (weight_limit, proof_size_base_cost) =
346				match <T as pallet_evm::Config>::GasWeightMapping::gas_to_weight(
347					transaction_data.gas_limit.unique_saturated_into(),
348					true,
349				) {
350					weight_limit if weight_limit.proof_size() > 0 => (
351						Some(weight_limit),
352						Some(Self::transaction_len(&transaction)),
353					),
354					_ => (None, None),
355				};
356
357			let _ = CheckEvmTransaction::<T::InvalidEvmTransactionError>::new(
358				CheckEvmTransactionConfig {
359					evm_config: T::config(),
360					block_gas_limit: U256::from(
361						<T as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
362							T::ReservedXcmpWeight::get(),
363						),
364					),
365					base_fee: U256::zero(),
366					chain_id: 0u64,
367					is_transactional: true,
368				},
369				transaction_data.into(),
370				weight_limit,
371				proof_size_base_cost,
372			)
373			// We only validate the gas limit against the evm transaction cost.
374			// No need to validate fee payment, as it is handled by the xcm executor.
375			.validate_common()
376			.map_err(|_| sp_runtime::DispatchErrorWithPostInfo {
377				post_info: PostDispatchInfo {
378					actual_weight: Some(error_weight),
379					pays_fee: Pays::Yes,
380				},
381				error: sp_runtime::DispatchError::Other("Failed to validate ethereum transaction"),
382			})?;
383
384			// Once we know a new transaction hash exists - the user can afford storing the
385			// transaction on chain - we increase the global nonce.
386			<Nonce<T>>::put(current_nonce.saturating_add(U256::one()));
387
388			let (dispatch_info, _) =
389				T::ValidatedTransaction::apply(source, transaction, maybe_force_create_address)?;
390
391			XCM_MESSAGE_HASH::with(|xcm_msg_hash| {
392				Self::deposit_event(Event::ExecutedFromXcm {
393					xcm_msg_hash: *xcm_msg_hash,
394					eth_tx_hash: tx_hash,
395				});
396			});
397
398			Ok(dispatch_info)
399		} else {
400			Err(sp_runtime::DispatchErrorWithPostInfo {
401				post_info: PostDispatchInfo {
402					actual_weight: Some(error_weight),
403					pays_fee: Pays::Yes,
404				},
405				error: sp_runtime::DispatchError::Other("Cannot convert xcm payload to known type"),
406			})
407		}
408	}
409}