xcm_primitives/
ethereum_xcm.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
17use ethereum::{
18	AccessList, AccessListItem, AuthorizationList, EIP1559Transaction, EIP2930Transaction,
19	LegacyTransaction, TransactionAction, TransactionV3,
20};
21use ethereum_types::{H160, H256, U256};
22use frame_support::{traits::ConstU32, BoundedVec};
23use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode};
24use scale_info::TypeInfo;
25use sp_std::vec::Vec;
26
27// polkadot/blob/19f6665a6162e68cd2651f5fe3615d6676821f90/xcm/src/v3/mod.rs#L1193
28// Defensively we increase this value to allow UMP fragments through xcm-transactor to prepare our
29// runtime for a relay upgrade where the xcm instruction weights are not ZERO hardcoded. If that
30// happens stuff will break in our side.
31// Rationale behind the value: e.g. staking unbond will go above 64kb and thus
32// required_weight_at_most must be below overall weight but still above whatever value we decide to
33// set. For this reason we set here a value that makes sense for the overall weight.
34pub const DEFAULT_PROOF_SIZE: u64 = 256 * 1024;
35
36/// Max. allowed size of 65_536 bytes.
37pub const MAX_ETHEREUM_XCM_INPUT_SIZE: u32 = 2u32.pow(16);
38
39/// Ensure that a proxy between `delegator` and `delegatee` exists in order to deny or grant
40/// permission to do xcm-transact to `transact_through_proxy`.
41pub trait EnsureProxy<AccountId> {
42	fn ensure_ok(delegator: AccountId, delegatee: AccountId) -> Result<(), &'static str>;
43}
44
45#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
46/// Manually sets a gas fee.
47pub struct ManualEthereumXcmFee {
48	/// Legacy or Eip-2930, all fee will be used.
49	pub gas_price: Option<U256>,
50	/// Eip-1559, must be at least the on-chain base fee at the time of applying the xcm
51	/// and will use up to the defined value.
52	pub max_fee_per_gas: Option<U256>,
53}
54
55/// Xcm transact's Ethereum transaction configurable fee.
56#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
57pub enum EthereumXcmFee {
58	/// Manually set gas fee.
59	Manual(ManualEthereumXcmFee),
60	/// Use the on-chain base fee at the time of processing the xcm.
61	Auto,
62}
63
64/// Xcm transact's Ethereum transaction.
65#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
66pub enum EthereumXcmTransaction {
67	V1(EthereumXcmTransactionV1),
68	V2(EthereumXcmTransactionV2),
69	V3(EthereumXcmTransactionV3),
70}
71
72/// Value for `r` and `s` for the invalid signature included in Xcm transact's Ethereum transaction.
73pub fn rs_id() -> H256 {
74	H256::from_low_u64_be(1u64)
75}
76
77#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
78pub struct EthereumXcmTransactionV1 {
79	/// Gas limit to be consumed by EVM execution.
80	pub gas_limit: U256,
81	/// Fee configuration of choice.
82	pub fee_payment: EthereumXcmFee,
83	/// Either a Call (the callee, account or contract address) or Create (unsupported for v1).
84	pub action: TransactionAction,
85	/// Value to be transfered.
86	pub value: U256,
87	/// Input data for a contract call.
88	pub input: BoundedVec<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>,
89	/// Map of addresses to be pre-paid to warm storage.
90	pub access_list: Option<Vec<(H160, Vec<H256>)>>,
91}
92
93#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
94pub struct EthereumXcmTransactionV2 {
95	/// Gas limit to be consumed by EVM execution.
96	pub gas_limit: U256,
97	/// Either a Call (the callee, account or contract address) or Create).
98	pub action: TransactionAction,
99	/// Value to be transfered.
100	pub value: U256,
101	/// Input data for a contract call. Max. size 65_536 bytes.
102	pub input: BoundedVec<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>,
103	/// Map of addresses to be pre-paid to warm storage.
104	pub access_list: Option<Vec<(H160, Vec<H256>)>>,
105}
106
107#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
108pub struct EthereumXcmTransactionV3 {
109	/// Gas limit to be consumed by EVM execution.
110	pub gas_limit: U256,
111	/// Either a Call (the callee, account or contract address) or Create).
112	pub action: TransactionAction,
113	/// Value to be transfered.
114	pub value: U256,
115	/// Input data for a contract call. Max. size 65_536 bytes.
116	pub input: BoundedVec<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>,
117	/// Map of addresses to be pre-paid to warm storage.
118	pub access_list: Option<Vec<(H160, Vec<H256>)>>,
119	/// Authorization list as defined in EIP-7702.
120	/// Currently not supported from XCM, but reserved for future use.
121	pub authorization_list: Option<AuthorizationList>,
122}
123
124pub trait XcmToEthereum {
125	fn into_transaction(
126		&self,
127		nonce: U256,
128		chain_id: u64,
129		allow_create: bool,
130	) -> Option<TransactionV3>;
131}
132
133impl XcmToEthereum for EthereumXcmTransaction {
134	fn into_transaction(
135		&self,
136		nonce: U256,
137		chain_id: u64,
138		allow_create: bool,
139	) -> Option<TransactionV3> {
140		match self {
141			EthereumXcmTransaction::V1(v1_tx) => {
142				v1_tx.into_transaction(nonce, chain_id, allow_create)
143			}
144			EthereumXcmTransaction::V2(v2_tx) => {
145				v2_tx.into_transaction(nonce, chain_id, allow_create)
146			}
147			EthereumXcmTransaction::V3(v3_tx) => {
148				v3_tx.into_transaction(nonce, chain_id, allow_create)
149			}
150		}
151	}
152}
153
154impl XcmToEthereum for EthereumXcmTransactionV1 {
155	fn into_transaction(
156		&self,
157		nonce: U256,
158		chain_id: u64,
159		allow_create: bool,
160	) -> Option<TransactionV3> {
161		if !allow_create && self.action == TransactionAction::Create {
162			// Create not allowed
163			return None;
164		}
165		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
166			t.iter()
167				.map(|item| AccessListItem {
168					address: item.0.clone(),
169					storage_keys: item.1.clone(),
170				})
171				.collect::<Vec<AccessListItem>>()
172		};
173
174		let (gas_price, max_fee) = match &self.fee_payment {
175			EthereumXcmFee::Manual(fee_config) => {
176				(fee_config.gas_price, fee_config.max_fee_per_gas)
177			}
178			EthereumXcmFee::Auto => (None, Some(U256::zero())),
179		};
180
181		match (gas_price, max_fee) {
182			(Some(gas_price), None) => {
183				// Legacy or Eip-2930
184				if let Some(ref access_list) = self.access_list {
185					// Eip-2930
186					Some(TransactionV3::EIP2930(EIP2930Transaction {
187						chain_id,
188						nonce,
189						gas_price,
190						gas_limit: self.gas_limit,
191						action: self.action,
192						value: self.value,
193						input: self.input.to_vec(),
194						access_list: from_tuple_to_access_list(access_list),
195						signature: ethereum::eip2930::TransactionSignature::new(
196							true,
197							rs_id(),
198							rs_id(),
199						)?,
200					}))
201				} else {
202					// Legacy
203					Some(TransactionV3::Legacy(LegacyTransaction {
204						nonce,
205						gas_price,
206						gas_limit: self.gas_limit,
207						action: self.action,
208						value: self.value,
209						input: self.input.to_vec(),
210						signature: ethereum::legacy::TransactionSignature::new(
211							42,
212							rs_id(),
213							rs_id(),
214						)?,
215					}))
216				}
217			}
218			(None, Some(max_fee)) => {
219				// Eip-1559
220				Some(TransactionV3::EIP1559(EIP1559Transaction {
221					chain_id,
222					nonce,
223					max_fee_per_gas: max_fee,
224					max_priority_fee_per_gas: U256::zero(),
225					gas_limit: self.gas_limit,
226					action: self.action,
227					value: self.value,
228					input: self.input.to_vec(),
229					access_list: if let Some(ref access_list) = self.access_list {
230						from_tuple_to_access_list(access_list)
231					} else {
232						Vec::new()
233					},
234					signature: ethereum::eip1559::TransactionSignature::new(
235						true,
236						rs_id(),
237						rs_id(),
238					)?,
239				}))
240			}
241			_ => None,
242		}
243	}
244}
245
246impl XcmToEthereum for EthereumXcmTransactionV2 {
247	fn into_transaction(
248		&self,
249		nonce: U256,
250		chain_id: u64,
251		allow_create: bool,
252	) -> Option<TransactionV3> {
253		if !allow_create && self.action == TransactionAction::Create {
254			// Create not allowed
255			return None;
256		}
257		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
258			t.iter()
259				.map(|item| AccessListItem {
260					address: item.0,
261					storage_keys: item.1.clone(),
262				})
263				.collect::<Vec<AccessListItem>>()
264		};
265
266		// Eip-1559
267		Some(TransactionV3::EIP1559(EIP1559Transaction {
268			chain_id,
269			nonce,
270			max_fee_per_gas: U256::zero(),
271			max_priority_fee_per_gas: U256::zero(),
272			gas_limit: self.gas_limit,
273			action: self.action,
274			value: self.value,
275			input: self.input.to_vec(),
276			access_list: if let Some(ref access_list) = self.access_list {
277				from_tuple_to_access_list(access_list)
278			} else {
279				Vec::new()
280			},
281			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
282		}))
283	}
284}
285
286impl XcmToEthereum for EthereumXcmTransactionV3 {
287	fn into_transaction(
288		&self,
289		nonce: U256,
290		chain_id: u64,
291		allow_create: bool,
292	) -> Option<TransactionV3> {
293		if !allow_create && self.action == TransactionAction::Create {
294			// Create not allowed
295			return None;
296		}
297		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
298			t.iter()
299				.map(|item| AccessListItem {
300					address: item.0,
301					storage_keys: item.1.clone(),
302				})
303				.collect::<Vec<AccessListItem>>()
304		};
305
306		// EIP-1559
307		Some(TransactionV3::EIP1559(EIP1559Transaction {
308			chain_id,
309			nonce,
310			max_fee_per_gas: U256::zero(),
311			max_priority_fee_per_gas: U256::zero(),
312			gas_limit: self.gas_limit,
313			action: self.action,
314			value: self.value,
315			input: self.input.to_vec(),
316			access_list: if let Some(ref access_list) = self.access_list {
317				from_tuple_to_access_list(access_list)
318			} else {
319				Vec::new()
320			},
321			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
322		}))
323	}
324}
325
326#[cfg(test)]
327mod tests {
328	use super::*;
329	#[test]
330	fn test_into_ethereum_tx_with_auto_fee_v1() {
331		let xcm_transaction = EthereumXcmTransactionV1 {
332			gas_limit: U256::one(),
333			fee_payment: EthereumXcmFee::Auto,
334			action: TransactionAction::Call(H160::default()),
335			value: U256::zero(),
336			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
337				.unwrap(),
338			access_list: None,
339		};
340		let nonce = U256::zero();
341
342		let expected_tx = Some(TransactionV3::EIP1559(EIP1559Transaction {
343			chain_id: 111,
344			nonce,
345			max_fee_per_gas: U256::zero(),
346			max_priority_fee_per_gas: U256::zero(),
347			gas_limit: U256::one(),
348			action: TransactionAction::Call(H160::default()),
349			value: U256::zero(),
350			input: vec![1u8],
351			access_list: vec![],
352			signature: ethereum::eip1559::TransactionSignature::new(
353				true,
354				H256::from_low_u64_be(1u64),
355				H256::from_low_u64_be(1u64),
356			)
357			.unwrap(),
358		}));
359
360		assert_eq!(
361			xcm_transaction.into_transaction(nonce, 111, false),
362			expected_tx
363		);
364	}
365
366	#[test]
367	fn test_legacy_v1() {
368		let xcm_transaction = EthereumXcmTransactionV1 {
369			gas_limit: U256::one(),
370			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
371				gas_price: Some(U256::zero()),
372				max_fee_per_gas: None,
373			}),
374			action: TransactionAction::Call(H160::default()),
375			value: U256::zero(),
376			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
377				.unwrap(),
378			access_list: None,
379		};
380		let nonce = U256::zero();
381		let expected_tx = Some(TransactionV3::Legacy(LegacyTransaction {
382			nonce,
383			gas_price: U256::zero(),
384			gas_limit: U256::one(),
385			action: TransactionAction::Call(H160::default()),
386			value: U256::zero(),
387			input: vec![1u8],
388			signature: ethereum::legacy::TransactionSignature::new(42, rs_id(), rs_id()).unwrap(),
389		}));
390
391		assert_eq!(
392			xcm_transaction.into_transaction(nonce, 111, false),
393			expected_tx
394		);
395	}
396	#[test]
397	fn test_eip_2930_v1() {
398		let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
399		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
400			t.iter()
401				.map(|item| AccessListItem {
402					address: item.0.clone(),
403					storage_keys: item.1.clone(),
404				})
405				.collect::<Vec<AccessListItem>>()
406		};
407
408		let xcm_transaction = EthereumXcmTransactionV1 {
409			gas_limit: U256::one(),
410			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
411				gas_price: Some(U256::zero()),
412				max_fee_per_gas: None,
413			}),
414			action: TransactionAction::Call(H160::default()),
415			value: U256::zero(),
416			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
417				.unwrap(),
418			access_list: access_list.clone(),
419		};
420
421		let nonce = U256::zero();
422		let expected_tx = Some(TransactionV3::EIP2930(EIP2930Transaction {
423			chain_id: 111,
424			nonce,
425			gas_price: U256::zero(),
426			gas_limit: U256::one(),
427			action: TransactionAction::Call(H160::default()),
428			value: U256::zero(),
429			input: vec![1u8],
430			access_list: from_tuple_to_access_list(&access_list.unwrap()),
431			signature: ethereum::eip2930::TransactionSignature::new(
432				true,
433				H256::from_low_u64_be(1u64),
434				H256::from_low_u64_be(1u64),
435			)
436			.unwrap(),
437		}));
438
439		assert_eq!(
440			xcm_transaction.into_transaction(nonce, 111, false),
441			expected_tx
442		);
443	}
444
445	#[test]
446	fn test_eip1559_v2() {
447		let xcm_transaction = EthereumXcmTransactionV2 {
448			gas_limit: U256::one(),
449			action: TransactionAction::Call(H160::default()),
450			value: U256::zero(),
451			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
452				.unwrap(),
453			access_list: None,
454		};
455		let nonce = U256::zero();
456		let expected_tx = Some(TransactionV3::EIP1559(EIP1559Transaction {
457			chain_id: 111,
458			nonce,
459			max_fee_per_gas: U256::zero(),
460			max_priority_fee_per_gas: U256::zero(),
461			gas_limit: U256::one(),
462			action: TransactionAction::Call(H160::default()),
463			value: U256::zero(),
464			input: vec![1u8],
465			access_list: vec![],
466			signature: ethereum::eip1559::TransactionSignature::new(
467				true,
468				H256::from_low_u64_be(1u64),
469				H256::from_low_u64_be(1u64),
470			)
471			.unwrap(),
472		}));
473
474		assert_eq!(
475			xcm_transaction.into_transaction(nonce, 111, false),
476			expected_tx
477		);
478	}
479}