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
use ethereum::{
18
	AccessList, AccessListItem, AuthorizationList, EIP1559Transaction, EIP2930Transaction,
19
	LegacyTransaction, TransactionAction, TransactionV3,
20
};
21
use ethereum_types::{H160, H256, U256};
22
use frame_support::{traits::ConstU32, BoundedVec};
23
use parity_scale_codec::{Decode, DecodeWithMemTracking, Encode};
24
use scale_info::TypeInfo;
25
use 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.
34
pub const DEFAULT_PROOF_SIZE: u64 = 256 * 1024;
35

            
36
/// Max. allowed size of 65_536 bytes.
37
pub 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`.
41
pub trait EnsureProxy<AccountId> {
42
	fn ensure_ok(delegator: AccountId, delegatee: AccountId) -> Result<(), &'static str>;
43
}
44

            
45
162
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
46
/// Manually sets a gas fee.
47
pub 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
162
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
57
pub enum EthereumXcmFee {
58
	/// Manually set gas fee.
59
	Manual(ManualEthereumXcmFee),
60
6
	/// Use the on-chain base fee at the time of processing the xcm.
61
	Auto,
62
}
63

            
64
/// Xcm transact's Ethereum transaction.
65
243
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
66
pub 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.
73
33922
pub fn rs_id() -> H256 {
74
33922
	H256::from_low_u64_be(1u64)
75
33922
}
76

            
77
486
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
78
pub 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
405
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
94
pub 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
486
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo, DecodeWithMemTracking)]
108
pub 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

            
124
pub trait XcmToEthereum {
125
	fn into_transaction(
126
		&self,
127
		nonce: U256,
128
		chain_id: u64,
129
		allow_create: bool,
130
	) -> Option<TransactionV3>;
131
}
132

            
133
impl XcmToEthereum for EthereumXcmTransaction {
134
16956
	fn into_transaction(
135
16956
		&self,
136
16956
		nonce: U256,
137
16956
		chain_id: u64,
138
16956
		allow_create: bool,
139
16956
	) -> Option<TransactionV3> {
140
16956
		match self {
141
972
			EthereumXcmTransaction::V1(v1_tx) => {
142
972
				v1_tx.into_transaction(nonce, chain_id, allow_create)
143
			}
144
432
			EthereumXcmTransaction::V2(v2_tx) => {
145
432
				v2_tx.into_transaction(nonce, chain_id, allow_create)
146
			}
147
15552
			EthereumXcmTransaction::V3(v3_tx) => {
148
15552
				v3_tx.into_transaction(nonce, chain_id, allow_create)
149
			}
150
		}
151
16956
	}
152
}
153

            
154
impl XcmToEthereum for EthereumXcmTransactionV1 {
155
1056
	fn into_transaction(
156
1056
		&self,
157
1056
		nonce: U256,
158
1056
		chain_id: u64,
159
1056
		allow_create: bool,
160
1056
	) -> Option<TransactionV3> {
161
1056
		if !allow_create && self.action == TransactionAction::Create {
162
			// Create not allowed
163
81
			return None;
164
975
		}
165
975
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
166
298
			t.iter()
167
298
				.map(|item| AccessListItem {
168
271
					address: item.0.clone(),
169
271
					storage_keys: item.1.clone(),
170
298
				})
171
298
				.collect::<Vec<AccessListItem>>()
172
298
		};
173

            
174
975
		let (gas_price, max_fee) = match &self.fee_payment {
175
245
			EthereumXcmFee::Manual(fee_config) => {
176
245
				(fee_config.gas_price, fee_config.max_fee_per_gas)
177
			}
178
730
			EthereumXcmFee::Auto => (None, Some(U256::zero())),
179
		};
180

            
181
975
		match (gas_price, max_fee) {
182
245
			(Some(gas_price), None) => {
183
				// Legacy or Eip-2930
184
245
				if let Some(ref access_list) = self.access_list {
185
					// Eip-2930
186
					Some(TransactionV3::EIP2930(EIP2930Transaction {
187
109
						chain_id,
188
109
						nonce,
189
109
						gas_price,
190
109
						gas_limit: self.gas_limit,
191
109
						action: self.action,
192
109
						value: self.value,
193
109
						input: self.input.to_vec(),
194
109
						access_list: from_tuple_to_access_list(access_list),
195
109
						signature: ethereum::eip2930::TransactionSignature::new(
196
109
							true,
197
109
							rs_id(),
198
109
							rs_id(),
199
109
						)?,
200
					}))
201
				} else {
202
					// Legacy
203
					Some(TransactionV3::Legacy(LegacyTransaction {
204
136
						nonce,
205
136
						gas_price,
206
136
						gas_limit: self.gas_limit,
207
136
						action: self.action,
208
136
						value: self.value,
209
136
						input: self.input.to_vec(),
210
136
						signature: ethereum::legacy::TransactionSignature::new(
211
136
							42,
212
136
							rs_id(),
213
136
							rs_id(),
214
136
						)?,
215
					}))
216
				}
217
			}
218
730
			(None, Some(max_fee)) => {
219
730
				// Eip-1559
220
730
				Some(TransactionV3::EIP1559(EIP1559Transaction {
221
730
					chain_id,
222
730
					nonce,
223
730
					max_fee_per_gas: max_fee,
224
730
					max_priority_fee_per_gas: U256::zero(),
225
730
					gas_limit: self.gas_limit,
226
730
					action: self.action,
227
730
					value: self.value,
228
730
					input: self.input.to_vec(),
229
730
					access_list: if let Some(ref access_list) = self.access_list {
230
189
						from_tuple_to_access_list(access_list)
231
					} else {
232
541
						Vec::new()
233
					},
234
730
					signature: ethereum::eip1559::TransactionSignature::new(
235
730
						true,
236
730
						rs_id(),
237
730
						rs_id(),
238
730
					)?,
239
				}))
240
			}
241
			_ => None,
242
		}
243
1056
	}
244
}
245

            
246
impl XcmToEthereum for EthereumXcmTransactionV2 {
247
460
	fn into_transaction(
248
460
		&self,
249
460
		nonce: U256,
250
460
		chain_id: u64,
251
460
		allow_create: bool,
252
460
	) -> Option<TransactionV3> {
253
460
		if !allow_create && self.action == TransactionAction::Create {
254
			// Create not allowed
255
27
			return None;
256
433
		}
257
433
		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
433
			chain_id,
269
433
			nonce,
270
433
			max_fee_per_gas: U256::zero(),
271
433
			max_priority_fee_per_gas: U256::zero(),
272
433
			gas_limit: self.gas_limit,
273
433
			action: self.action,
274
433
			value: self.value,
275
433
			input: self.input.to_vec(),
276
433
			access_list: if let Some(ref access_list) = self.access_list {
277
				from_tuple_to_access_list(access_list)
278
			} else {
279
433
				Vec::new()
280
			},
281
433
			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
282
		}))
283
460
	}
284
}
285

            
286
impl XcmToEthereum for EthereumXcmTransactionV3 {
287
15579
	fn into_transaction(
288
15579
		&self,
289
15579
		nonce: U256,
290
15579
		chain_id: u64,
291
15579
		allow_create: bool,
292
15579
	) -> Option<TransactionV3> {
293
15579
		if !allow_create && self.action == TransactionAction::Create {
294
			// Create not allowed
295
27
			return None;
296
15552
		}
297
15552
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
298
15201
			t.iter()
299
15201
				.map(|item| AccessListItem {
300
					address: item.0,
301
					storage_keys: item.1.clone(),
302
15201
				})
303
15201
				.collect::<Vec<AccessListItem>>()
304
15201
		};
305

            
306
		// EIP-1559
307
		Some(TransactionV3::EIP1559(EIP1559Transaction {
308
15552
			chain_id,
309
15552
			nonce,
310
15552
			max_fee_per_gas: U256::zero(),
311
15552
			max_priority_fee_per_gas: U256::zero(),
312
15552
			gas_limit: self.gas_limit,
313
15552
			action: self.action,
314
15552
			value: self.value,
315
15552
			input: self.input.to_vec(),
316
15552
			access_list: if let Some(ref access_list) = self.access_list {
317
15201
				from_tuple_to_access_list(access_list)
318
			} else {
319
351
				Vec::new()
320
			},
321
15552
			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
322
		}))
323
15579
	}
324
}
325

            
326
#[cfg(test)]
327
mod tests {
328
	use super::*;
329
	#[test]
330
1
	fn test_into_ethereum_tx_with_auto_fee_v1() {
331
1
		let xcm_transaction = EthereumXcmTransactionV1 {
332
1
			gas_limit: U256::one(),
333
1
			fee_payment: EthereumXcmFee::Auto,
334
1
			action: TransactionAction::Call(H160::default()),
335
1
			value: U256::zero(),
336
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
337
1
				.unwrap(),
338
1
			access_list: None,
339
1
		};
340
1
		let nonce = U256::zero();
341
1

            
342
1
		let expected_tx = Some(TransactionV3::EIP1559(EIP1559Transaction {
343
1
			chain_id: 111,
344
1
			nonce,
345
1
			max_fee_per_gas: U256::zero(),
346
1
			max_priority_fee_per_gas: U256::zero(),
347
1
			gas_limit: U256::one(),
348
1
			action: TransactionAction::Call(H160::default()),
349
1
			value: U256::zero(),
350
1
			input: vec![1u8],
351
1
			access_list: vec![],
352
1
			signature: ethereum::eip1559::TransactionSignature::new(
353
1
				true,
354
1
				H256::from_low_u64_be(1u64),
355
1
				H256::from_low_u64_be(1u64),
356
1
			)
357
1
			.unwrap(),
358
1
		}));
359
1

            
360
1
		assert_eq!(
361
1
			xcm_transaction.into_transaction(nonce, 111, false),
362
1
			expected_tx
363
1
		);
364
1
	}
365

            
366
	#[test]
367
1
	fn test_legacy_v1() {
368
1
		let xcm_transaction = EthereumXcmTransactionV1 {
369
1
			gas_limit: U256::one(),
370
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
371
1
				gas_price: Some(U256::zero()),
372
1
				max_fee_per_gas: None,
373
1
			}),
374
1
			action: TransactionAction::Call(H160::default()),
375
1
			value: U256::zero(),
376
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
377
1
				.unwrap(),
378
1
			access_list: None,
379
1
		};
380
1
		let nonce = U256::zero();
381
1
		let expected_tx = Some(TransactionV3::Legacy(LegacyTransaction {
382
1
			nonce,
383
1
			gas_price: U256::zero(),
384
1
			gas_limit: U256::one(),
385
1
			action: TransactionAction::Call(H160::default()),
386
1
			value: U256::zero(),
387
1
			input: vec![1u8],
388
1
			signature: ethereum::legacy::TransactionSignature::new(42, rs_id(), rs_id()).unwrap(),
389
1
		}));
390
1

            
391
1
		assert_eq!(
392
1
			xcm_transaction.into_transaction(nonce, 111, false),
393
1
			expected_tx
394
1
		);
395
1
	}
396
	#[test]
397
1
	fn test_eip_2930_v1() {
398
1
		let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
399
1
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
400
1
			t.iter()
401
1
				.map(|item| AccessListItem {
402
1
					address: item.0.clone(),
403
1
					storage_keys: item.1.clone(),
404
1
				})
405
1
				.collect::<Vec<AccessListItem>>()
406
1
		};
407

            
408
1
		let xcm_transaction = EthereumXcmTransactionV1 {
409
1
			gas_limit: U256::one(),
410
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
411
1
				gas_price: Some(U256::zero()),
412
1
				max_fee_per_gas: None,
413
1
			}),
414
1
			action: TransactionAction::Call(H160::default()),
415
1
			value: U256::zero(),
416
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
417
1
				.unwrap(),
418
1
			access_list: access_list.clone(),
419
1
		};
420
1

            
421
1
		let nonce = U256::zero();
422
1
		let expected_tx = Some(TransactionV3::EIP2930(EIP2930Transaction {
423
1
			chain_id: 111,
424
1
			nonce,
425
1
			gas_price: U256::zero(),
426
1
			gas_limit: U256::one(),
427
1
			action: TransactionAction::Call(H160::default()),
428
1
			value: U256::zero(),
429
1
			input: vec![1u8],
430
1
			access_list: from_tuple_to_access_list(&access_list.unwrap()),
431
1
			signature: ethereum::eip2930::TransactionSignature::new(
432
1
				true,
433
1
				H256::from_low_u64_be(1u64),
434
1
				H256::from_low_u64_be(1u64),
435
1
			)
436
1
			.unwrap(),
437
1
		}));
438
1

            
439
1
		assert_eq!(
440
1
			xcm_transaction.into_transaction(nonce, 111, false),
441
1
			expected_tx
442
1
		);
443
1
	}
444

            
445
	#[test]
446
1
	fn test_eip1559_v2() {
447
1
		let xcm_transaction = EthereumXcmTransactionV2 {
448
1
			gas_limit: U256::one(),
449
1
			action: TransactionAction::Call(H160::default()),
450
1
			value: U256::zero(),
451
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
452
1
				.unwrap(),
453
1
			access_list: None,
454
1
		};
455
1
		let nonce = U256::zero();
456
1
		let expected_tx = Some(TransactionV3::EIP1559(EIP1559Transaction {
457
1
			chain_id: 111,
458
1
			nonce,
459
1
			max_fee_per_gas: U256::zero(),
460
1
			max_priority_fee_per_gas: U256::zero(),
461
1
			gas_limit: U256::one(),
462
1
			action: TransactionAction::Call(H160::default()),
463
1
			value: U256::zero(),
464
1
			input: vec![1u8],
465
1
			access_list: vec![],
466
1
			signature: ethereum::eip1559::TransactionSignature::new(
467
1
				true,
468
1
				H256::from_low_u64_be(1u64),
469
1
				H256::from_low_u64_be(1u64),
470
1
			)
471
1
			.unwrap(),
472
1
		}));
473
1

            
474
1
		assert_eq!(
475
1
			xcm_transaction.into_transaction(nonce, 111, false),
476
1
			expected_tx
477
1
		);
478
1
	}
479
}