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
180
#[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
180
#[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
270
#[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
37690
pub fn rs_id() -> H256 {
74
37690
	H256::from_low_u64_be(1u64)
75
37690
}
76

            
77
540
#[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
450
#[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
540
#[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
18840
	fn into_transaction(
135
18840
		&self,
136
18840
		nonce: U256,
137
18840
		chain_id: u64,
138
18840
		allow_create: bool,
139
18840
	) -> Option<TransactionV3> {
140
18840
		match self {
141
1080
			EthereumXcmTransaction::V1(v1_tx) => {
142
1080
				v1_tx.into_transaction(nonce, chain_id, allow_create)
143
			}
144
480
			EthereumXcmTransaction::V2(v2_tx) => {
145
480
				v2_tx.into_transaction(nonce, chain_id, allow_create)
146
			}
147
17280
			EthereumXcmTransaction::V3(v3_tx) => {
148
17280
				v3_tx.into_transaction(nonce, chain_id, allow_create)
149
			}
150
		}
151
18840
	}
152
}
153

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

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

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

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

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

            
306
		// EIP-1559
307
		Some(TransactionV3::EIP1559(EIP1559Transaction {
308
17280
			chain_id,
309
17280
			nonce,
310
17280
			max_fee_per_gas: U256::zero(),
311
17280
			max_priority_fee_per_gas: U256::zero(),
312
17280
			gas_limit: self.gas_limit,
313
17280
			action: self.action,
314
17280
			value: self.value,
315
17280
			input: self.input.to_vec(),
316
17280
			access_list: if let Some(ref access_list) = self.access_list {
317
16890
				from_tuple_to_access_list(access_list)
318
			} else {
319
390
				Vec::new()
320
			},
321
17280
			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
322
		}))
323
17310
	}
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
}