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

            
77
504
#[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
420
#[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
504
#[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
	pub authorization_list: Option<AuthorizationList>,
121
}
122

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

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

            
153
impl XcmToEthereum for EthereumXcmTransactionV1 {
154
1011
	fn into_transaction(&self, nonce: U256, chain_id: u64, _: bool) -> Option<TransactionV3> {
155
1011
		// We dont support creates for now
156
1011
		if self.action == TransactionAction::Create {
157
84
			return None;
158
927
		}
159
927
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
160
281
			t.iter()
161
281
				.map(|item| AccessListItem {
162
281
					address: item.0.clone(),
163
281
					storage_keys: item.1.clone(),
164
281
				})
165
281
				.collect::<Vec<AccessListItem>>()
166
281
		};
167

            
168
927
		let (gas_price, max_fee) = match &self.fee_payment {
169
198
			EthereumXcmFee::Manual(fee_config) => {
170
198
				(fee_config.gas_price, fee_config.max_fee_per_gas)
171
			}
172
729
			EthereumXcmFee::Auto => (None, Some(U256::zero())),
173
		};
174

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

            
240
impl XcmToEthereum for EthereumXcmTransactionV2 {
241
449
	fn into_transaction(
242
449
		&self,
243
449
		nonce: U256,
244
449
		chain_id: u64,
245
449
		allow_create: bool,
246
449
	) -> Option<TransactionV3> {
247
449
		if !allow_create && self.action == TransactionAction::Create {
248
			// Create not allowed
249
28
			return None;
250
421
		}
251
421
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
252
			t.iter()
253
				.map(|item| AccessListItem {
254
					address: item.0,
255
					storage_keys: item.1.clone(),
256
				})
257
				.collect::<Vec<AccessListItem>>()
258
		};
259

            
260
		// Eip-1559
261
		Some(TransactionV3::EIP1559(EIP1559Transaction {
262
421
			chain_id,
263
421
			nonce,
264
421
			max_fee_per_gas: U256::zero(),
265
421
			max_priority_fee_per_gas: U256::zero(),
266
421
			gas_limit: self.gas_limit,
267
421
			action: self.action,
268
421
			value: self.value,
269
421
			input: self.input.to_vec(),
270
421
			access_list: if let Some(ref access_list) = self.access_list {
271
				from_tuple_to_access_list(access_list)
272
			} else {
273
421
				Vec::new()
274
			},
275
421
			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
276
		}))
277
449
	}
278
}
279

            
280
impl XcmToEthereum for EthereumXcmTransactionV3 {
281
16128
	fn into_transaction(
282
16128
		&self,
283
16128
		nonce: U256,
284
16128
		chain_id: u64,
285
16128
		allow_create: bool,
286
16128
	) -> Option<TransactionV3> {
287
16128
		if !allow_create && self.action == TransactionAction::Create {
288
			// Create not allowed
289
28
			return None;
290
16100
		}
291
16100
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
292
15764
			t.iter()
293
15764
				.map(|item| AccessListItem {
294
					address: item.0,
295
					storage_keys: item.1.clone(),
296
15764
				})
297
15764
				.collect::<Vec<AccessListItem>>()
298
15764
		};
299

            
300
		// EIP-1559
301
		Some(TransactionV3::EIP1559(EIP1559Transaction {
302
16100
			chain_id,
303
16100
			nonce,
304
16100
			max_fee_per_gas: U256::zero(),
305
16100
			max_priority_fee_per_gas: U256::zero(),
306
16100
			gas_limit: self.gas_limit,
307
16100
			action: self.action,
308
16100
			value: self.value,
309
16100
			input: self.input.to_vec(),
310
16100
			access_list: if let Some(ref access_list) = self.access_list {
311
15764
				from_tuple_to_access_list(access_list)
312
			} else {
313
336
				Vec::new()
314
			},
315
16100
			signature: ethereum::eip1559::TransactionSignature::new(true, rs_id(), rs_id())?,
316
		}))
317
16128
	}
318
}
319

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

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

            
354
1
		assert_eq!(
355
1
			xcm_transaction.into_transaction(nonce, 111, false),
356
1
			expected_tx
357
1
		);
358
1
	}
359

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

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

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

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

            
433
1
		assert_eq!(
434
1
			xcm_transaction.into_transaction(nonce, 111, false),
435
1
			expected_tx
436
1
		);
437
1
	}
438

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

            
468
1
		assert_eq!(
469
1
			xcm_transaction.into_transaction(nonce, 111, false),
470
1
			expected_tx
471
1
		);
472
1
	}
473
}