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(all(feature = "std", test))]
26
mod mock;
27
#[cfg(all(feature = "std", test))]
28
mod tests;
29
mod transactional_processor;
30

            
31
use ethereum_types::{H160, H256, U256};
32
use fp_ethereum::{TransactionData, ValidatedTransaction};
33
use fp_evm::{CheckEvmTransaction, CheckEvmTransactionConfig, TransactionValidationError};
34
use frame_support::{
35
	dispatch::{DispatchResultWithPostInfo, Pays, PostDispatchInfo},
36
	traits::{EnsureOrigin, Get, ProcessMessage},
37
	weights::Weight,
38
};
39
use frame_system::pallet_prelude::OriginFor;
40
use pallet_evm::{AddressMapping, GasWeightMapping};
41
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
42
use scale_info::TypeInfo;
43
use sp_runtime::{traits::UniqueSaturatedInto, DispatchErrorWithPostInfo, RuntimeDebug};
44
use sp_std::{marker::PhantomData, prelude::*};
45

            
46
pub use self::transactional_processor::XcmEthTransactionalProcessor;
47
pub use ethereum::{
48
	AccessListItem, BlockV2 as Block, LegacyTransactionMessage, Log, ReceiptV3 as Receipt,
49
	TransactionAction, TransactionV2 as Transaction,
50
};
51
pub use fp_rpc::TransactionStatus;
52
pub use xcm_primitives::{EnsureProxy, EthereumXcmTransaction, XcmToEthereum};
53

            
54
51
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
55
pub enum RawOrigin {
56
	XcmEthereumTransaction(H160),
57
}
58

            
59
pub fn ensure_xcm_ethereum_transaction<OuterOrigin>(o: OuterOrigin) -> Result<H160, &'static str>
60
where
61
	OuterOrigin: Into<Result<RawOrigin, OuterOrigin>>,
62
{
63
	match o.into() {
64
		Ok(RawOrigin::XcmEthereumTransaction(n)) => Ok(n),
65
		_ => Err("bad origin: expected to be a xcm Ethereum transaction"),
66
	}
67
}
68

            
69
pub struct EnsureXcmEthereumTransaction;
70
impl<O: Into<Result<RawOrigin, O>> + From<RawOrigin>> EnsureOrigin<O>
71
	for EnsureXcmEthereumTransaction
72
{
73
	type Success = H160;
74
159
	fn try_origin(o: O) -> Result<Self::Success, O> {
75
159
		o.into().map(|o| match o {
76
159
			RawOrigin::XcmEthereumTransaction(id) => id,
77
159
		})
78
159
	}
79

            
80
	#[cfg(feature = "runtime-benchmarks")]
81
	fn try_successful_origin() -> Result<O, ()> {
82
		Ok(O::from(RawOrigin::XcmEthereumTransaction(
83
			Default::default(),
84
		)))
85
	}
86
}
87

            
88
90
environmental::environmental!(XCM_MESSAGE_HASH: H256);
89

            
90
pub struct MessageProcessorWrapper<Inner>(core::marker::PhantomData<Inner>);
91
impl<Inner: ProcessMessage> ProcessMessage for MessageProcessorWrapper<Inner> {
92
	type Origin = <Inner as ProcessMessage>::Origin;
93

            
94
	fn process_message(
95
		message: &[u8],
96
		origin: Self::Origin,
97
		meter: &mut frame_support::weights::WeightMeter,
98
		id: &mut [u8; 32],
99
	) -> Result<bool, frame_support::traits::ProcessMessageError> {
100
		let mut xcm_msg_hash = H256(sp_io::hashing::blake2_256(message));
101
		XCM_MESSAGE_HASH::using(&mut xcm_msg_hash, || {
102
			Inner::process_message(message, origin, meter, id)
103
		})
104
	}
105
}
106

            
107
pub use self::pallet::*;
108

            
109
397
#[frame_support::pallet(dev_mode)]
110
pub mod pallet {
111
	use super::*;
112
	use fp_evm::AccountProvider;
113
	use frame_support::pallet_prelude::*;
114

            
115
	#[pallet::config]
116
	pub trait Config: frame_system::Config + pallet_evm::Config {
117
		/// The overarching event type.
118
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
119
		/// Invalid transaction error
120
		type InvalidEvmTransactionError: From<TransactionValidationError>;
121
		/// Handler for applying an already validated transaction
122
		type ValidatedTransaction: ValidatedTransaction;
123
		/// Origin for xcm transact
124
		type XcmEthereumOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = H160>;
125
		/// Maximum Weight reserved for xcm in a block
126
		type ReservedXcmpWeight: Get<Weight>;
127
		/// Ensure proxy
128
		type EnsureProxy: EnsureProxy<
129
			<<Self as pallet_evm::Config>::AccountProvider as AccountProvider>::AccountId,
130
		>;
131
		/// The origin that is allowed to resume or suspend the XCM to Ethereum executions.
132
		type ControllerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
133
		/// An origin that can submit a create tx type
134
		type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
135
	}
136

            
137
79
	#[pallet::pallet]
138
	#[pallet::without_storage_info]
139
	pub struct Pallet<T>(PhantomData<T>);
140

            
141
	/// Global nonce used for building Ethereum transaction payload.
142
1076
	#[pallet::storage]
143
	#[pallet::getter(fn nonce)]
144
	pub(crate) type Nonce<T: Config> = StorageValue<_, U256, ValueQuery>;
145

            
146
	/// Whether or not Ethereum-XCM is suspended from executing
147
452
	#[pallet::storage]
148
	#[pallet::getter(fn ethereum_xcm_suspended)]
149
	pub(super) type EthereumXcmSuspended<T: Config> = StorageValue<_, bool, ValueQuery>;
150

            
151
	#[pallet::origin]
152
	pub type Origin = RawOrigin;
153

            
154
4
	#[pallet::error]
155
	pub enum Error<T> {
156
		/// Xcm to Ethereum execution is suspended
157
		EthereumXcmExecutionSuspended,
158
	}
159

            
160
	#[pallet::event]
161
	#[pallet::generate_deposit(pub(super) fn deposit_event)]
162
	pub enum Event<T> {
163
		/// Ethereum transaction executed from XCM
164
		ExecutedFromXcm {
165
			xcm_msg_hash: H256,
166
			eth_tx_hash: H256,
167
		},
168
	}
169

            
170
348
	#[pallet::call]
171
	impl<T: Config> Pallet<T>
172
	where
173
		OriginFor<T>: Into<Result<RawOrigin, OriginFor<T>>>,
174
	{
175
		/// Xcm Transact an Ethereum transaction.
176
		/// Weight: Gas limit plus the db read involving the suspension check
177
		#[pallet::weight({
178
			let without_base_extrinsic_weight = false;
179
			<T as pallet_evm::Config>::GasWeightMapping::gas_to_weight({
180
				match xcm_transaction {
181
					EthereumXcmTransaction::V1(v1_tx) =>  v1_tx.gas_limit.unique_saturated_into(),
182
					EthereumXcmTransaction::V2(v2_tx) =>  v2_tx.gas_limit.unique_saturated_into()
183
				}
184
			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(1))
185
		})]
186
		pub fn transact(
187
			origin: OriginFor<T>,
188
			xcm_transaction: EthereumXcmTransaction,
189
139
		) -> DispatchResultWithPostInfo {
190
139
			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
191
139
			ensure!(
192
139
				!EthereumXcmSuspended::<T>::get(),
193
1
				DispatchErrorWithPostInfo {
194
1
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
195
1
					post_info: PostDispatchInfo {
196
1
						actual_weight: Some(T::DbWeight::get().reads(1)),
197
1
						pays_fee: Pays::Yes
198
1
					}
199
1
				}
200
			);
201
138
			Self::validate_and_apply(source, xcm_transaction, false, None)
202
		}
203

            
204
		/// Xcm Transact an Ethereum transaction through proxy.
205
		/// Weight: Gas limit plus the db reads involving the suspension and proxy checks
206
		#[pallet::weight({
207
			let without_base_extrinsic_weight = false;
208
			<T as pallet_evm::Config>::GasWeightMapping::gas_to_weight({
209
				match xcm_transaction {
210
					EthereumXcmTransaction::V1(v1_tx) =>  v1_tx.gas_limit.unique_saturated_into(),
211
					EthereumXcmTransaction::V2(v2_tx) =>  v2_tx.gas_limit.unique_saturated_into()
212
				}
213
			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(2))
214
		})]
215
		pub fn transact_through_proxy(
216
			origin: OriginFor<T>,
217
			transact_as: H160,
218
			xcm_transaction: EthereumXcmTransaction,
219
20
		) -> DispatchResultWithPostInfo {
220
20
			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
221
20
			ensure!(
222
20
				!EthereumXcmSuspended::<T>::get(),
223
1
				DispatchErrorWithPostInfo {
224
1
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
225
1
					post_info: PostDispatchInfo {
226
1
						actual_weight: Some(T::DbWeight::get().reads(1)),
227
1
						pays_fee: Pays::Yes
228
1
					}
229
1
				}
230
			);
231
19
			let _ = T::EnsureProxy::ensure_ok(
232
19
				T::AddressMapping::into_account_id(transact_as),
233
19
				T::AddressMapping::into_account_id(source),
234
19
			)
235
19
			.map_err(|e| sp_runtime::DispatchErrorWithPostInfo {
236
11
				post_info: PostDispatchInfo {
237
11
					actual_weight: Some(T::DbWeight::get().reads(2)),
238
11
					pays_fee: Pays::Yes,
239
11
				},
240
11
				error: sp_runtime::DispatchError::Other(e),
241
19
			})?;
242

            
243
8
			Self::validate_and_apply(transact_as, xcm_transaction, false, None)
244
		}
245

            
246
		/// Suspends all Ethereum executions from XCM.
247
		///
248
		/// - `origin`: Must pass `ControllerOrigin`.
249
		#[pallet::weight((T::DbWeight::get().writes(1), DispatchClass::Operational,))]
250
3
		pub fn suspend_ethereum_xcm_execution(origin: OriginFor<T>) -> DispatchResult {
251
3
			T::ControllerOrigin::ensure_origin(origin)?;
252

            
253
3
			EthereumXcmSuspended::<T>::put(true);
254
3

            
255
3
			Ok(())
256
		}
257

            
258
		/// Resumes all Ethereum executions from XCM.
259
		///
260
		/// - `origin`: Must pass `ControllerOrigin`.
261
		#[pallet::weight((T::DbWeight::get().writes(1), DispatchClass::Operational,))]
262
2
		pub fn resume_ethereum_xcm_execution(origin: OriginFor<T>) -> DispatchResult {
263
2
			T::ControllerOrigin::ensure_origin(origin)?;
264

            
265
2
			EthereumXcmSuspended::<T>::put(false);
266
2

            
267
2
			Ok(())
268
		}
269

            
270
		/// Xcm Transact an Ethereum transaction, but allow to force the caller and create address.
271
		/// This call should be restricted (callable only by the runtime or governance).
272
		/// Weight: Gas limit plus the db reads involving the suspension and proxy checks
273
		#[pallet::weight({
274
			let without_base_extrinsic_weight = false;
275
			<T as pallet_evm::Config>::GasWeightMapping::gas_to_weight({
276
				match xcm_transaction {
277
					EthereumXcmTransaction::V1(v1_tx) =>  v1_tx.gas_limit.unique_saturated_into(),
278
					EthereumXcmTransaction::V2(v2_tx) =>  v2_tx.gas_limit.unique_saturated_into()
279
				}
280
			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(1))
281
		})]
282
		pub fn force_transact_as(
283
			origin: OriginFor<T>,
284
			transact_as: H160,
285
			xcm_transaction: EthereumXcmTransaction,
286
			force_create_address: Option<H160>,
287
62
		) -> DispatchResultWithPostInfo {
288
62
			T::ForceOrigin::ensure_origin(origin)?;
289
62
			ensure!(
290
62
				!EthereumXcmSuspended::<T>::get(),
291
				DispatchErrorWithPostInfo {
292
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
293
					post_info: PostDispatchInfo {
294
						actual_weight: Some(T::DbWeight::get().reads(1)),
295
						pays_fee: Pays::Yes
296
					}
297
				}
298
			);
299

            
300
62
			Self::validate_and_apply(transact_as, xcm_transaction, true, force_create_address)
301
		}
302
	}
303
}
304

            
305
impl<T: Config> Pallet<T> {
306
156
	fn transaction_len(transaction: &Transaction) -> u64 {
307
156
		transaction
308
156
			.encode()
309
156
			.len()
310
156
			// pallet + call indexes
311
156
			.saturating_add(2) as u64
312
156
	}
313

            
314
208
	fn validate_and_apply(
315
208
		source: H160,
316
208
		xcm_transaction: EthereumXcmTransaction,
317
208
		allow_create: bool,
318
208
		maybe_force_create_address: Option<H160>,
319
208
	) -> DispatchResultWithPostInfo {
320
208
		// The lack of a real signature where different callers with the
321
208
		// same nonce are providing identical transaction payloads results in a collision and
322
208
		// the same ethereum tx hash.
323
208
		// We use a global nonce instead the user nonce for all Xcm->Ethereum transactions to avoid
324
208
		// this.
325
208
		let current_nonce = Self::nonce();
326
208
		let error_weight = T::DbWeight::get().reads(1);
327
208

            
328
208
		let transaction: Option<Transaction> =
329
208
			xcm_transaction.into_transaction_v2(current_nonce, T::ChainId::get(), allow_create);
330
208
		if let Some(transaction) = transaction {
331
204
			let tx_hash = transaction.hash();
332
204
			let transaction_data: TransactionData = (&transaction).into();
333

            
334
204
			let (weight_limit, proof_size_base_cost) =
335
204
				match <T as pallet_evm::Config>::GasWeightMapping::gas_to_weight(
336
204
					transaction_data.gas_limit.unique_saturated_into(),
337
204
					true,
338
204
				) {
339
204
					weight_limit if weight_limit.proof_size() > 0 => (
340
156
						Some(weight_limit),
341
156
						Some(Self::transaction_len(&transaction)),
342
156
					),
343
48
					_ => (None, None),
344
				};
345

            
346
204
			let _ = CheckEvmTransaction::<T::InvalidEvmTransactionError>::new(
347
204
				CheckEvmTransactionConfig {
348
204
					evm_config: T::config(),
349
204
					block_gas_limit: U256::from(
350
204
						<T as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
351
204
							T::ReservedXcmpWeight::get(),
352
204
						),
353
204
					),
354
204
					base_fee: U256::zero(),
355
204
					chain_id: 0u64,
356
204
					is_transactional: true,
357
204
				},
358
204
				transaction_data.into(),
359
204
				weight_limit,
360
204
				proof_size_base_cost,
361
204
			)
362
204
			// We only validate the gas limit against the evm transaction cost.
363
204
			// No need to validate fee payment, as it is handled by the xcm executor.
364
204
			.validate_common()
365
204
			.map_err(|_| sp_runtime::DispatchErrorWithPostInfo {
366
8
				post_info: PostDispatchInfo {
367
8
					actual_weight: Some(error_weight),
368
8
					pays_fee: Pays::Yes,
369
8
				},
370
8
				error: sp_runtime::DispatchError::Other("Failed to validate ethereum transaction"),
371
204
			})?;
372

            
373
			// Once we know a new transaction hash exists - the user can afford storing the
374
			// transaction on chain - we increase the global nonce.
375
196
			<Nonce<T>>::put(current_nonce.saturating_add(U256::one()));
376

            
377
196
			let (dispatch_info, execution_info) =
378
196
				T::ValidatedTransaction::apply(source, transaction, maybe_force_create_address)?;
379

            
380
			// If the transaction reverted, signal it to XCM Transactional Processor
381
196
			match execution_info {
382
134
				fp_evm::CallOrCreateInfo::Call(info) => {
383
134
					if let fp_evm::ExitReason::Revert(_) = info.exit_reason {
384
4
						XcmEthTransactionalProcessor::signal_evm_revert();
385
130
					}
386
				}
387
62
				fp_evm::CallOrCreateInfo::Create(info) => {
388
62
					if let fp_evm::ExitReason::Revert(_) = info.exit_reason {
389
						XcmEthTransactionalProcessor::signal_evm_revert();
390
62
					}
391
				}
392
			}
393

            
394
196
			XCM_MESSAGE_HASH::with(|xcm_msg_hash| {
395
				Self::deposit_event(Event::ExecutedFromXcm {
396
					xcm_msg_hash: *xcm_msg_hash,
397
					eth_tx_hash: tx_hash,
398
				});
399
196
			});
400
196

            
401
196
			Ok(dispatch_info)
402
		} else {
403
4
			Err(sp_runtime::DispatchErrorWithPostInfo {
404
4
				post_info: PostDispatchInfo {
405
4
					actual_weight: Some(error_weight),
406
4
					pays_fee: Pays::Yes,
407
4
				},
408
4
				error: sp_runtime::DispatchError::Other("Cannot convert xcm payload to known type"),
409
4
			})
410
		}
411
208
	}
412
}