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, EIP1559Transaction, EIP2930Transaction, LegacyTransaction,
19
	TransactionAction, TransactionSignature, TransactionV2,
20
};
21
use ethereum_types::{H160, H256, U256};
22
use frame_support::{traits::ConstU32, BoundedVec};
23
use parity_scale_codec::{Decode, 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
192
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
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
192
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
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
288
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
66
pub enum EthereumXcmTransaction {
67
6
	V1(EthereumXcmTransactionV1),
68
3
	V2(EthereumXcmTransactionV2),
69
}
70

            
71
/// Value for `r` and `s` for the invalid signature included in Xcm transact's Ethereum transaction.
72
5898
pub fn rs_id() -> H256 {
73
5898
	H256::from_low_u64_be(1u64)
74
5898
}
75

            
76
576
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
77
pub struct EthereumXcmTransactionV1 {
78
	/// Gas limit to be consumed by EVM execution.
79
	pub gas_limit: U256,
80
	/// Fee configuration of choice.
81
	pub fee_payment: EthereumXcmFee,
82
	/// Either a Call (the callee, account or contract address) or Create (unsupported for v1).
83
	pub action: TransactionAction,
84
	/// Value to be transfered.
85
	pub value: U256,
86
	/// Input data for a contract call.
87
	pub input: BoundedVec<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>,
88
	/// Map of addresses to be pre-paid to warm storage.
89
	pub access_list: Option<Vec<(H160, Vec<H256>)>>,
90
}
91

            
92
480
#[derive(Clone, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
93
pub struct EthereumXcmTransactionV2 {
94
	/// Gas limit to be consumed by EVM execution.
95
	pub gas_limit: U256,
96
	/// Either a Call (the callee, account or contract address) or Create).
97
	pub action: TransactionAction,
98
	/// Value to be transfered.
99
	pub value: U256,
100
	/// Input data for a contract call. Max. size 65_536 bytes.
101
	pub input: BoundedVec<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>,
102
	/// Map of addresses to be pre-paid to warm storage.
103
	pub access_list: Option<Vec<(H160, Vec<H256>)>>,
104
}
105

            
106
pub trait XcmToEthereum {
107
	fn into_transaction_v2(
108
		&self,
109
		nonce: U256,
110
		chain_id: u64,
111
		allow_create: bool,
112
	) -> Option<TransactionV2>;
113
}
114

            
115
impl XcmToEthereum for EthereumXcmTransaction {
116
3072
	fn into_transaction_v2(
117
3072
		&self,
118
3072
		nonce: U256,
119
3072
		chain_id: u64,
120
3072
		allow_create: bool,
121
3072
	) -> Option<TransactionV2> {
122
3072
		match self {
123
1152
			EthereumXcmTransaction::V1(v1_tx) => {
124
1152
				v1_tx.into_transaction_v2(nonce, chain_id, allow_create)
125
			}
126
1920
			EthereumXcmTransaction::V2(v2_tx) => {
127
1920
				v2_tx.into_transaction_v2(nonce, chain_id, allow_create)
128
			}
129
		}
130
3072
	}
131
}
132

            
133
impl XcmToEthereum for EthereumXcmTransactionV1 {
134
1155
	fn into_transaction_v2(&self, nonce: U256, chain_id: u64, _: bool) -> Option<TransactionV2> {
135
1155
		// We dont support creates for now
136
1155
		if self.action == TransactionAction::Create {
137
96
			return None;
138
1059
		}
139
1059
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
140
321
			t.iter()
141
321
				.map(|item| AccessListItem {
142
321
					address: item.0.clone(),
143
321
					storage_keys: item.1.clone(),
144
321
				})
145
321
				.collect::<Vec<AccessListItem>>()
146
321
		};
147

            
148
1059
		let (gas_price, max_fee) = match &self.fee_payment {
149
226
			EthereumXcmFee::Manual(fee_config) => {
150
226
				(fee_config.gas_price, fee_config.max_fee_per_gas)
151
			}
152
833
			EthereumXcmFee::Auto => (None, Some(U256::zero())),
153
		};
154
1059
		match (gas_price, max_fee) {
155
226
			(Some(gas_price), None) => {
156
				// Legacy or Eip-2930
157
226
				if let Some(ref access_list) = self.access_list {
158
					// Eip-2930
159
97
					Some(TransactionV2::EIP2930(EIP2930Transaction {
160
97
						chain_id,
161
97
						nonce,
162
97
						gas_price,
163
97
						gas_limit: self.gas_limit,
164
97
						action: self.action,
165
97
						value: self.value,
166
97
						input: self.input.to_vec(),
167
97
						access_list: from_tuple_to_access_list(access_list),
168
97
						odd_y_parity: true,
169
97
						r: rs_id(),
170
97
						s: rs_id(),
171
97
					}))
172
				} else {
173
					// Legacy
174
					Some(TransactionV2::Legacy(LegacyTransaction {
175
129
						nonce,
176
129
						gas_price,
177
129
						gas_limit: self.gas_limit,
178
129
						action: self.action,
179
129
						value: self.value,
180
129
						input: self.input.to_vec(),
181
129
						signature: TransactionSignature::new(42, rs_id(), rs_id())?,
182
					}))
183
				}
184
			}
185
833
			(None, Some(max_fee)) => {
186
833
				// Eip-1559
187
833
				Some(TransactionV2::EIP1559(EIP1559Transaction {
188
833
					chain_id,
189
833
					nonce,
190
833
					max_fee_per_gas: max_fee,
191
833
					max_priority_fee_per_gas: U256::zero(),
192
833
					gas_limit: self.gas_limit,
193
833
					action: self.action,
194
833
					value: self.value,
195
833
					input: self.input.to_vec(),
196
833
					access_list: if let Some(ref access_list) = self.access_list {
197
224
						from_tuple_to_access_list(access_list)
198
					} else {
199
609
						Vec::new()
200
					},
201
					odd_y_parity: true,
202
833
					r: rs_id(),
203
833
					s: rs_id(),
204
				}))
205
			}
206
			_ => None,
207
		}
208
1155
	}
209
}
210

            
211
impl XcmToEthereum for EthereumXcmTransactionV2 {
212
1921
	fn into_transaction_v2(
213
1921
		&self,
214
1921
		nonce: U256,
215
1921
		chain_id: u64,
216
1921
		allow_create: bool,
217
1921
	) -> Option<TransactionV2> {
218
1921
		if !allow_create && self.action == TransactionAction::Create {
219
			// Create not allowed
220
32
			return None;
221
1889
		}
222
1889
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
223
1408
			t.iter()
224
1408
				.map(|item| AccessListItem {
225
					address: item.0,
226
					storage_keys: item.1.clone(),
227
1408
				})
228
1408
				.collect::<Vec<AccessListItem>>()
229
1408
		};
230
		// Eip-1559
231
		Some(TransactionV2::EIP1559(EIP1559Transaction {
232
1889
			chain_id,
233
1889
			nonce,
234
1889
			max_fee_per_gas: U256::zero(),
235
1889
			max_priority_fee_per_gas: U256::zero(),
236
1889
			gas_limit: self.gas_limit,
237
1889
			action: self.action,
238
1889
			value: self.value,
239
1889
			input: self.input.to_vec(),
240
1889
			access_list: if let Some(ref access_list) = self.access_list {
241
1408
				from_tuple_to_access_list(access_list)
242
			} else {
243
481
				Vec::new()
244
			},
245
			odd_y_parity: true,
246
1889
			r: rs_id(),
247
1889
			s: rs_id(),
248
		}))
249
1921
	}
250
}
251

            
252
/// The EthereumXcmTracingStatus storage key.
253
pub const ETHEREUM_XCM_TRACING_STORAGE_KEY: &[u8] = b":ethereum_xcm_tracing";
254

            
255
/// The current EthereumXcmTransaction trace status.
256
#[derive(Clone, Debug, Encode, Decode, PartialEq, Eq)]
257
pub enum EthereumXcmTracingStatus {
258
	/// A full block trace.
259
	Block,
260
20
	/// A single transaction.
261
	Transaction(H256),
262
	/// Exit signal.
263
	TransactionExited,
264
}
265

            
266
#[cfg(test)]
267
mod tests {
268
	use super::*;
269
	#[test]
270
1
	fn test_into_ethereum_tx_with_auto_fee_v1() {
271
1
		let xcm_transaction = EthereumXcmTransactionV1 {
272
1
			gas_limit: U256::one(),
273
1
			fee_payment: EthereumXcmFee::Auto,
274
1
			action: TransactionAction::Call(H160::default()),
275
1
			value: U256::zero(),
276
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
277
1
				.unwrap(),
278
1
			access_list: None,
279
1
		};
280
1
		let nonce = U256::zero();
281
1
		let expected_tx = Some(TransactionV2::EIP1559(EIP1559Transaction {
282
1
			chain_id: 111,
283
1
			nonce,
284
1
			max_fee_per_gas: U256::zero(),
285
1
			max_priority_fee_per_gas: U256::zero(),
286
1
			gas_limit: U256::one(),
287
1
			action: TransactionAction::Call(H160::default()),
288
1
			value: U256::zero(),
289
1
			input: vec![1u8],
290
1
			access_list: vec![],
291
1
			odd_y_parity: true,
292
1
			r: H256::from_low_u64_be(1u64),
293
1
			s: H256::from_low_u64_be(1u64),
294
1
		}));
295
1

            
296
1
		assert_eq!(
297
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
298
1
			expected_tx
299
1
		);
300
1
	}
301

            
302
	#[test]
303
1
	fn test_legacy_v1() {
304
1
		let xcm_transaction = EthereumXcmTransactionV1 {
305
1
			gas_limit: U256::one(),
306
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
307
1
				gas_price: Some(U256::zero()),
308
1
				max_fee_per_gas: None,
309
1
			}),
310
1
			action: TransactionAction::Call(H160::default()),
311
1
			value: U256::zero(),
312
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
313
1
				.unwrap(),
314
1
			access_list: None,
315
1
		};
316
1
		let nonce = U256::zero();
317
1
		let expected_tx = Some(TransactionV2::Legacy(LegacyTransaction {
318
1
			nonce,
319
1
			gas_price: U256::zero(),
320
1
			gas_limit: U256::one(),
321
1
			action: TransactionAction::Call(H160::default()),
322
1
			value: U256::zero(),
323
1
			input: vec![1u8],
324
1
			signature: TransactionSignature::new(42, rs_id(), rs_id()).unwrap(),
325
1
		}));
326
1

            
327
1
		assert_eq!(
328
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
329
1
			expected_tx
330
1
		);
331
1
	}
332
	#[test]
333
1
	fn test_eip_2930_v1() {
334
1
		let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
335
1
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
336
1
			t.iter()
337
1
				.map(|item| AccessListItem {
338
1
					address: item.0.clone(),
339
1
					storage_keys: item.1.clone(),
340
1
				})
341
1
				.collect::<Vec<AccessListItem>>()
342
1
		};
343

            
344
1
		let xcm_transaction = EthereumXcmTransactionV1 {
345
1
			gas_limit: U256::one(),
346
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
347
1
				gas_price: Some(U256::zero()),
348
1
				max_fee_per_gas: None,
349
1
			}),
350
1
			action: TransactionAction::Call(H160::default()),
351
1
			value: U256::zero(),
352
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
353
1
				.unwrap(),
354
1
			access_list: access_list.clone(),
355
1
		};
356
1

            
357
1
		let nonce = U256::zero();
358
1
		let expected_tx = Some(TransactionV2::EIP2930(EIP2930Transaction {
359
1
			chain_id: 111,
360
1
			nonce,
361
1
			gas_price: U256::zero(),
362
1
			gas_limit: U256::one(),
363
1
			action: TransactionAction::Call(H160::default()),
364
1
			value: U256::zero(),
365
1
			input: vec![1u8],
366
1
			access_list: from_tuple_to_access_list(&access_list.unwrap()),
367
1
			odd_y_parity: true,
368
1
			r: H256::from_low_u64_be(1u64),
369
1
			s: H256::from_low_u64_be(1u64),
370
1
		}));
371
1

            
372
1
		assert_eq!(
373
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
374
1
			expected_tx
375
1
		);
376
1
	}
377

            
378
	#[test]
379
1
	fn test_eip1559_v2() {
380
1
		let xcm_transaction = EthereumXcmTransactionV2 {
381
1
			gas_limit: U256::one(),
382
1
			action: TransactionAction::Call(H160::default()),
383
1
			value: U256::zero(),
384
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
385
1
				.unwrap(),
386
1
			access_list: None,
387
1
		};
388
1
		let nonce = U256::zero();
389
1
		let expected_tx = Some(TransactionV2::EIP1559(EIP1559Transaction {
390
1
			chain_id: 111,
391
1
			nonce,
392
1
			max_fee_per_gas: U256::zero(),
393
1
			max_priority_fee_per_gas: U256::zero(),
394
1
			gas_limit: U256::one(),
395
1
			action: TransactionAction::Call(H160::default()),
396
1
			value: U256::zero(),
397
1
			input: vec![1u8],
398
1
			access_list: vec![],
399
1
			odd_y_parity: true,
400
1
			r: H256::from_low_u64_be(1u64),
401
1
			s: H256::from_low_u64_be(1u64),
402
1
		}));
403
1

            
404
1
		assert_eq!(
405
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
406
1
			expected_tx
407
1
		);
408
1
	}
409
}