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

            
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, BlockV2 as Block, LegacyTransactionMessage, Log, ReceiptV3 as Receipt,
47
	TransactionAction, TransactionV2 as Transaction,
48
};
49
pub use fp_rpc::TransactionStatus;
50
pub use xcm_primitives::{EnsureProxy, EthereumXcmTransaction, XcmToEthereum};
51

            
52
96
#[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
283
	fn try_origin(o: O) -> Result<Self::Success, O> {
73
283
		o.into().map(|o| match o {
74
283
			RawOrigin::XcmEthereumTransaction(id) => id,
75
283
		})
76
283
	}
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
	fn process_message(
93
		message: &[u8],
94
		origin: Self::Origin,
95
		meter: &mut frame_support::weights::WeightMeter,
96
		id: &mut [u8; 32],
97
	) -> Result<bool, frame_support::traits::ProcessMessageError> {
98
		let mut xcm_msg_hash = H256(sp_io::hashing::blake2_256(message));
99
		XCM_MESSAGE_HASH::using(&mut xcm_msg_hash, || {
100
			Inner::process_message(message, origin, meter, id)
101
		})
102
	}
103
}
104

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

            
107
510
#[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
73
	#[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
1816
	#[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
760
	#[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
8
	#[pallet::error]
153
	pub enum Error<T> {
154
		/// Xcm to Ethereum execution is suspended
155
		EthereumXcmExecutionSuspended,
156
	}
157

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

            
168
645
	#[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
				}
182
			}, without_base_extrinsic_weight).saturating_add(T::DbWeight::get().reads(1))
183
		})]
184
		pub fn transact(
185
			origin: OriginFor<T>,
186
			xcm_transaction: EthereumXcmTransaction,
187
263
		) -> DispatchResultWithPostInfo {
188
263
			let source = T::XcmEthereumOrigin::ensure_origin(origin)?;
189
263
			ensure!(
190
263
				!EthereumXcmSuspended::<T>::get(),
191
1
				DispatchErrorWithPostInfo {
192
1
					error: Error::<T>::EthereumXcmExecutionSuspended.into(),
193
1
					post_info: PostDispatchInfo {
194
1
						actual_weight: Some(T::DbWeight::get().reads(1)),
195
1
						pays_fee: Pays::Yes
196
1
					}
197
1
				}
198
			);
199
262
			Self::validate_and_apply(source, xcm_transaction, false, None)
200
		}
201

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

            
241
8
			Self::validate_and_apply(transact_as, xcm_transaction, false, None)
242
		}
243

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

            
251
3
			EthereumXcmSuspended::<T>::put(true);
252
3

            
253
3
			Ok(())
254
		}
255

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

            
263
2
			EthereumXcmSuspended::<T>::put(false);
264
2

            
265
2
			Ok(())
266
		}
267

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

            
298
82
			Self::validate_and_apply(transact_as, xcm_transaction, true, force_create_address)
299
		}
300
	}
301
}
302

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

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

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

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

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

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

            
375
340
			let (dispatch_info, _) =
376
340
				T::ValidatedTransaction::apply(source, transaction, maybe_force_create_address)?;
377

            
378
340
			XCM_MESSAGE_HASH::with(|xcm_msg_hash| {
379
				Self::deposit_event(Event::ExecutedFromXcm {
380
					xcm_msg_hash: *xcm_msg_hash,
381
					eth_tx_hash: tx_hash,
382
				});
383
340
			});
384
340

            
385
340
			Ok(dispatch_info)
386
		} else {
387
4
			Err(sp_runtime::DispatchErrorWithPostInfo {
388
4
				post_info: PostDispatchInfo {
389
4
					actual_weight: Some(error_weight),
390
4
					pays_fee: Pays::Yes,
391
4
				},
392
4
				error: sp_runtime::DispatchError::Other("Cannot convert xcm payload to known type"),
393
4
			})
394
		}
395
352
	}
396
}