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
174
#[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
174
#[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
261
#[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
5810
pub fn rs_id() -> H256 {
73
5810
	H256::from_low_u64_be(1u64)
74
5810
}
75

            
76
522
#[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
435
#[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
3016
	fn into_transaction_v2(
117
3016
		&self,
118
3016
		nonce: U256,
119
3016
		chain_id: u64,
120
3016
		allow_create: bool,
121
3016
	) -> Option<TransactionV2> {
122
3016
		match self {
123
1044
			EthereumXcmTransaction::V1(v1_tx) => {
124
1044
				v1_tx.into_transaction_v2(nonce, chain_id, allow_create)
125
			}
126
1972
			EthereumXcmTransaction::V2(v2_tx) => {
127
1972
				v2_tx.into_transaction_v2(nonce, chain_id, allow_create)
128
			}
129
		}
130
3016
	}
131
}
132

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

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

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

            
252
#[cfg(test)]
253
mod tests {
254
	use super::*;
255
	#[test]
256
1
	fn test_into_ethereum_tx_with_auto_fee_v1() {
257
1
		let xcm_transaction = EthereumXcmTransactionV1 {
258
1
			gas_limit: U256::one(),
259
1
			fee_payment: EthereumXcmFee::Auto,
260
1
			action: TransactionAction::Call(H160::default()),
261
1
			value: U256::zero(),
262
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
263
1
				.unwrap(),
264
1
			access_list: None,
265
1
		};
266
1
		let nonce = U256::zero();
267
1
		let expected_tx = Some(TransactionV2::EIP1559(EIP1559Transaction {
268
1
			chain_id: 111,
269
1
			nonce,
270
1
			max_fee_per_gas: U256::zero(),
271
1
			max_priority_fee_per_gas: U256::zero(),
272
1
			gas_limit: U256::one(),
273
1
			action: TransactionAction::Call(H160::default()),
274
1
			value: U256::zero(),
275
1
			input: vec![1u8],
276
1
			access_list: vec![],
277
1
			odd_y_parity: true,
278
1
			r: H256::from_low_u64_be(1u64),
279
1
			s: H256::from_low_u64_be(1u64),
280
1
		}));
281
1

            
282
1
		assert_eq!(
283
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
284
1
			expected_tx
285
1
		);
286
1
	}
287

            
288
	#[test]
289
1
	fn test_legacy_v1() {
290
1
		let xcm_transaction = EthereumXcmTransactionV1 {
291
1
			gas_limit: U256::one(),
292
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
293
1
				gas_price: Some(U256::zero()),
294
1
				max_fee_per_gas: None,
295
1
			}),
296
1
			action: TransactionAction::Call(H160::default()),
297
1
			value: U256::zero(),
298
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
299
1
				.unwrap(),
300
1
			access_list: None,
301
1
		};
302
1
		let nonce = U256::zero();
303
1
		let expected_tx = Some(TransactionV2::Legacy(LegacyTransaction {
304
1
			nonce,
305
1
			gas_price: U256::zero(),
306
1
			gas_limit: U256::one(),
307
1
			action: TransactionAction::Call(H160::default()),
308
1
			value: U256::zero(),
309
1
			input: vec![1u8],
310
1
			signature: TransactionSignature::new(42, rs_id(), rs_id()).unwrap(),
311
1
		}));
312
1

            
313
1
		assert_eq!(
314
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
315
1
			expected_tx
316
1
		);
317
1
	}
318
	#[test]
319
1
	fn test_eip_2930_v1() {
320
1
		let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
321
1
		let from_tuple_to_access_list = |t: &Vec<(H160, Vec<H256>)>| -> AccessList {
322
1
			t.iter()
323
1
				.map(|item| AccessListItem {
324
1
					address: item.0.clone(),
325
1
					storage_keys: item.1.clone(),
326
1
				})
327
1
				.collect::<Vec<AccessListItem>>()
328
1
		};
329

            
330
1
		let xcm_transaction = EthereumXcmTransactionV1 {
331
1
			gas_limit: U256::one(),
332
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
333
1
				gas_price: Some(U256::zero()),
334
1
				max_fee_per_gas: None,
335
1
			}),
336
1
			action: TransactionAction::Call(H160::default()),
337
1
			value: U256::zero(),
338
1
			input: BoundedVec::<u8, ConstU32<MAX_ETHEREUM_XCM_INPUT_SIZE>>::try_from(vec![1u8])
339
1
				.unwrap(),
340
1
			access_list: access_list.clone(),
341
1
		};
342
1

            
343
1
		let nonce = U256::zero();
344
1
		let expected_tx = Some(TransactionV2::EIP2930(EIP2930Transaction {
345
1
			chain_id: 111,
346
1
			nonce,
347
1
			gas_price: U256::zero(),
348
1
			gas_limit: U256::one(),
349
1
			action: TransactionAction::Call(H160::default()),
350
1
			value: U256::zero(),
351
1
			input: vec![1u8],
352
1
			access_list: from_tuple_to_access_list(&access_list.unwrap()),
353
1
			odd_y_parity: true,
354
1
			r: H256::from_low_u64_be(1u64),
355
1
			s: H256::from_low_u64_be(1u64),
356
1
		}));
357
1

            
358
1
		assert_eq!(
359
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
360
1
			expected_tx
361
1
		);
362
1
	}
363

            
364
	#[test]
365
1
	fn test_eip1559_v2() {
366
1
		let xcm_transaction = EthereumXcmTransactionV2 {
367
1
			gas_limit: U256::one(),
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(TransactionV2::EIP1559(EIP1559Transaction {
376
1
			chain_id: 111,
377
1
			nonce,
378
1
			max_fee_per_gas: U256::zero(),
379
1
			max_priority_fee_per_gas: U256::zero(),
380
1
			gas_limit: U256::one(),
381
1
			action: TransactionAction::Call(H160::default()),
382
1
			value: U256::zero(),
383
1
			input: vec![1u8],
384
1
			access_list: vec![],
385
1
			odd_y_parity: true,
386
1
			r: H256::from_low_u64_be(1u64),
387
1
			s: H256::from_low_u64_be(1u64),
388
1
		}));
389
1

            
390
1
		assert_eq!(
391
1
			xcm_transaction.into_transaction_v2(nonce, 111, false),
392
1
			expected_tx
393
1
		);
394
1
	}
395
}