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, DecodeWithMemTracking, 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
#[derive(
53
	PartialEq,
54
	Eq,
55
	Clone,
56
	Encode,
57
	Decode,
58
	RuntimeDebug,
59
54
	TypeInfo,
60
	MaxEncodedLen,
61
	DecodeWithMemTracking,
62
)]
63
pub enum RawOrigin {
64
	XcmEthereumTransaction(H160),
65
}
66

            
67
pub fn ensure_xcm_ethereum_transaction<OuterOrigin>(o: OuterOrigin) -> Result<H160, &'static str>
68
where
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

            
77
pub struct EnsureXcmEthereumTransaction;
78
impl<O: Into<Result<RawOrigin, O>> + From<RawOrigin>> EnsureOrigin<O>
79
	for EnsureXcmEthereumTransaction
80
{
81
	type Success = H160;
82
913
	fn try_origin(o: O) -> Result<Self::Success, O> {
83
913
		o.into().map(|o| match o {
84
913
			RawOrigin::XcmEthereumTransaction(id) => id,
85
913
		})
86
913
	}
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

            
96
environmental::environmental!(XCM_MESSAGE_HASH: H256);
97

            
98
pub struct MessageProcessorWrapper<Inner>(core::marker::PhantomData<Inner>);
99
impl<Inner: ProcessMessage> ProcessMessage for MessageProcessorWrapper<Inner> {
100
	type Origin = <Inner as ProcessMessage>::Origin;
101

            
102
2
	fn process_message(
103
2
		message: &[u8],
104
2
		origin: Self::Origin,
105
2
		meter: &mut frame_support::weights::WeightMeter,
106
2
		id: &mut [u8; 32],
107
2
	) -> Result<bool, frame_support::traits::ProcessMessageError> {
108
2
		let mut xcm_msg_hash = H256(sp_io::hashing::blake2_256(message));
109
2
		XCM_MESSAGE_HASH::using(&mut xcm_msg_hash, || {
110
2
			Inner::process_message(message, origin, meter, id)
111
2
		})
112
2
	}
113
}
114

            
115
pub use self::pallet::*;
116

            
117
320
#[frame_support::pallet(dev_mode)]
118
pub mod pallet {
119
	use super::*;
120
	use fp_evm::AccountProvider;
121
	use frame_support::pallet_prelude::*;
122

            
123
	#[pallet::config]
124
	pub trait Config: frame_system::Config + pallet_evm::Config {
125
		/// The overarching event type.
126
		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
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
130
	#[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
5930
	#[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
2420
	#[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
20
	#[pallet::error]
163
	pub enum Error<T> {
164
		/// Xcm to Ethereum execution is suspended
165
		EthereumXcmExecutionSuspended,
166
	}
167

            
168
20
	#[pallet::event]
169
26
	#[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
20
	#[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
888
		) -> DispatchResultWithPostInfo {
199
888
			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
200
888
			ensure!(
201
888
				!EthereumXcmSuspended::<T>::get(),
202
2
				DispatchErrorWithPostInfo {
203
2
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
204
2
					post_info: PostDispatchInfo {
205
2
						actual_weight: Some(T::DbWeight::get().reads(1)),
206
2
						pays_fee: Pays::Yes
207
2
					}
208
2
				}
209
			);
210
886
			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
25
		) -> DispatchResultWithPostInfo {
230
25
			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
231
25
			ensure!(
232
25
				!EthereumXcmSuspended::<T>::get(),
233
2
				DispatchErrorWithPostInfo {
234
2
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
235
2
					post_info: PostDispatchInfo {
236
2
						actual_weight: Some(T::DbWeight::get().reads(1)),
237
2
						pays_fee: Pays::Yes
238
2
					}
239
2
				}
240
			);
241
23
			let _ = T::EnsureProxy::ensure_ok(
242
23
				T::AddressMapping::into_account_id(transact_as),
243
23
				T::AddressMapping::into_account_id(source),
244
23
			)
245
23
			.map_err(|e| sp_runtime::DispatchErrorWithPostInfo {
246
13
				post_info: PostDispatchInfo {
247
13
					actual_weight: Some(T::DbWeight::get().reads(2)),
248
13
					pays_fee: Pays::Yes,
249
13
				},
250
13
				error: sp_runtime::DispatchError::Other(e),
251
23
			})?;
252

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

            
263
6
			EthereumXcmSuspended::<T>::put(true);
264
6

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

            
275
4
			EthereumXcmSuspended::<T>::put(false);
276
4

            
277
4
			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
277
		) -> DispatchResultWithPostInfo {
299
277
			T::ForceOrigin::ensure_origin(origin)?;
300
277
			ensure!(
301
277
				!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
277
			Self::validate_and_apply(transact_as, xcm_transaction, true, force_create_address)
312
		}
313
	}
314
}
315

            
316
impl<T: Config> Pallet<T> {
317
1114
	fn transaction_len(transaction: &Transaction) -> u64 {
318
1114
		transaction
319
1114
			.encode()
320
1114
			.len()
321
1114
			// pallet + call indexes
322
1114
			.saturating_add(2) as u64
323
1114
	}
324

            
325
1173
	fn validate_and_apply(
326
1173
		source: H160,
327
1173
		xcm_transaction: EthereumXcmTransaction,
328
1173
		allow_create: bool,
329
1173
		maybe_force_create_address: Option<H160>,
330
1173
	) -> DispatchResultWithPostInfo {
331
1173
		// The lack of a real signature where different callers with the
332
1173
		// same nonce are providing identical transaction payloads results in a collision and
333
1173
		// the same ethereum tx hash.
334
1173
		// We use a global nonce instead the user nonce for all Xcm->Ethereum transactions to avoid
335
1173
		// this.
336
1173
		let current_nonce = Self::nonce();
337
1173
		let error_weight = T::DbWeight::get().reads(1);
338
1173

            
339
1173
		let transaction: Option<Transaction> =
340
1173
			xcm_transaction.into_transaction(current_nonce, T::ChainId::get(), allow_create);
341
1173
		if let Some(transaction) = transaction {
342
1168
			let tx_hash = transaction.hash();
343
1168
			let transaction_data: TransactionData = (&transaction).into();
344

            
345
1168
			let (weight_limit, proof_size_base_cost) =
346
1168
				match <T as pallet_evm::Config>::GasWeightMapping::gas_to_weight(
347
1168
					transaction_data.gas_limit.unique_saturated_into(),
348
1168
					true,
349
1168
				) {
350
1168
					weight_limit if weight_limit.proof_size() > 0 => (
351
1114
						Some(weight_limit),
352
1114
						Some(Self::transaction_len(&transaction)),
353
1114
					),
354
54
					_ => (None, None),
355
				};
356

            
357
1168
			let _ = CheckEvmTransaction::<T::InvalidEvmTransactionError>::new(
358
1168
				CheckEvmTransactionConfig {
359
1168
					evm_config: T::config(),
360
1168
					block_gas_limit: U256::from(
361
1168
						<T as pallet_evm::Config>::GasWeightMapping::weight_to_gas(
362
1168
							T::ReservedXcmpWeight::get(),
363
1168
						),
364
1168
					),
365
1168
					base_fee: U256::zero(),
366
1168
					chain_id: 0u64,
367
1168
					is_transactional: true,
368
1168
				},
369
1168
				transaction_data.into(),
370
1168
				weight_limit,
371
1168
				proof_size_base_cost,
372
1168
			)
373
1168
			// We only validate the gas limit against the evm transaction cost.
374
1168
			// No need to validate fee payment, as it is handled by the xcm executor.
375
1168
			.validate_common()
376
1168
			.map_err(|_| sp_runtime::DispatchErrorWithPostInfo {
377
10
				post_info: PostDispatchInfo {
378
10
					actual_weight: Some(error_weight),
379
10
					pays_fee: Pays::Yes,
380
10
				},
381
10
				error: sp_runtime::DispatchError::Other("Failed to validate ethereum transaction"),
382
1168
			})?;
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
1158
			<Nonce<T>>::put(current_nonce.saturating_add(U256::one()));
387

            
388
1158
			let (dispatch_info, _) =
389
1158
				T::ValidatedTransaction::apply(source, transaction, maybe_force_create_address)?;
390

            
391
1158
			XCM_MESSAGE_HASH::with(|xcm_msg_hash| {
392
26
				Self::deposit_event(Event::ExecutedFromXcm {
393
26
					xcm_msg_hash: *xcm_msg_hash,
394
26
					eth_tx_hash: tx_hash,
395
26
				});
396
1158
			});
397
1158

            
398
1158
			Ok(dispatch_info)
399
		} else {
400
5
			Err(sp_runtime::DispatchErrorWithPostInfo {
401
5
				post_info: PostDispatchInfo {
402
5
					actual_weight: Some(error_weight),
403
5
					pays_fee: Pays::Yes,
404
5
				},
405
5
				error: sp_runtime::DispatchError::Other("Cannot convert xcm payload to known type"),
406
5
			})
407
		}
408
1173
	}
409
}