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)]
26
mod mock;
27
#[cfg(test)]
28
mod tests;
29

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

            
45
pub use ethereum::{
46
	AccessListItem, BlockV3 as Block, LegacyTransactionMessage, Log, ReceiptV4 as Receipt,
47
	TransactionAction, TransactionV3 as Transaction,
48
};
49
pub use fp_rpc::TransactionStatus;
50
pub use xcm_primitives::{EnsureProxy, EthereumXcmTransaction, XcmToEthereum};
51

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

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

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

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

            
86
environmental::environmental!(XCM_MESSAGE_HASH: H256);
87

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

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

            
105
pub use self::pallet::*;
106

            
107
699
#[frame_support::pallet(dev_mode)]
108
pub mod pallet {
109
	use super::*;
110
	use fp_evm::AccountProvider;
111
	use frame_support::pallet_prelude::*;
112

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

            
135
69
	#[pallet::pallet]
136
	#[pallet::without_storage_info]
137
	pub struct Pallet<T>(PhantomData<T>);
138

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

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

            
149
	#[pallet::origin]
150
	pub type Origin = RawOrigin;
151

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

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

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

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

            
243
10
			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
6
		pub fn suspend_ethereum_xcm_execution(origin: OriginFor<T>) -> DispatchResult {
251
6
			T::ControllerOrigin::ensure_origin(origin)?;
252

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

            
255
6
			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
4
		pub fn resume_ethereum_xcm_execution(origin: OriginFor<T>) -> DispatchResult {
263
4
			T::ControllerOrigin::ensure_origin(origin)?;
264

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

            
267
4
			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
					EthereumXcmTransaction::V3(v3_tx) => v3_tx.gas_limit.unique_saturated_into(),
280
				}
281
			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(1))
282
		})]
283
		pub fn force_transact_as(
284
			origin: OriginFor<T>,
285
			transact_as: H160,
286
			xcm_transaction: EthereumXcmTransaction,
287
			force_create_address: Option<H160>,
288
127
		) -> DispatchResultWithPostInfo {
289
127
			T::ForceOrigin::ensure_origin(origin)?;
290
127
			ensure!(
291
127
				!EthereumXcmSuspended::<T>::get(),
292
				DispatchErrorWithPostInfo {
293
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
294
					post_info: PostDispatchInfo {
295
						actual_weight: Some(T::DbWeight::get().reads(1)),
296
						pays_fee: Pays::Yes
297
					}
298
				}
299
			);
300

            
301
127
			Self::validate_and_apply(transact_as, xcm_transaction, true, force_create_address)
302
		}
303
	}
304
}
305

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

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

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

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

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

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

            
378
517
			let (dispatch_info, _) =
379
517
				T::ValidatedTransaction::apply(source, transaction, maybe_force_create_address)?;
380

            
381
517
			XCM_MESSAGE_HASH::with(|xcm_msg_hash| {
382
26
				Self::deposit_event(Event::ExecutedFromXcm {
383
26
					xcm_msg_hash: *xcm_msg_hash,
384
26
					eth_tx_hash: tx_hash,
385
26
				});
386
517
			});
387
517

            
388
517
			Ok(dispatch_info)
389
		} else {
390
5
			Err(sp_runtime::DispatchErrorWithPostInfo {
391
5
				post_info: PostDispatchInfo {
392
5
					actual_weight: Some(error_weight),
393
5
					pays_fee: Pays::Yes,
394
5
				},
395
5
				error: sp_runtime::DispatchError::Other("Cannot convert xcm payload to known type"),
396
5
			})
397
		}
398
532
	}
399
}