1
// Copyright 2025 Moonbeam foundation
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
use crate::{mock::*, Error, RawOrigin};
17
use ethereum::TransactionV3;
18
use ethereum_types::{H160, U256};
19
use frame_support::{
20
	assert_noop, assert_ok,
21
	dispatch::{Pays, PostDispatchInfo},
22
	traits::{ConstU32, Get},
23
	weights::Weight,
24
	BoundedVec,
25
};
26
use sp_runtime::{DispatchError, DispatchErrorWithPostInfo};
27
use xcm_primitives::{EthereumXcmTransaction, EthereumXcmTransactionV3, XcmToEthereum};
28

            
29
// 	pragma solidity ^0.6.6;
30
// 	contract Test {
31
// 		function foo() external pure returns (bool) {
32
// 			return true;
33
// 		}
34
// 		function bar() external pure {
35
// 			require(false, "error_msg");
36
// 		}
37
// 	}
38
const CONTRACT: &str = "608060405234801561001057600080fd5b50610113806100206000396000f3fe6080604052\
39
						348015600f57600080fd5b506004361060325760003560e01c8063c2985578146037578063\
40
						febb0f7e146057575b600080fd5b603d605f565b6040518082151515158152602001915050\
41
						60405180910390f35b605d6068565b005b60006001905090565b600060db576040517f08c3\
42
						79a00000000000000000000000000000000000000000000000000000000081526004018080\
43
						602001828103825260098152602001807f6572726f725f6d73670000000000000000000000\
44
						00000000000000000000000081525060200191505060405180910390fd5b56fea264697066\
45
						7358221220fde68a3968e0e99b16fabf9b2997a78218b32214031f8e07e2c502daf603a69e\
46
						64736f6c63430006060033";
47

            
48
12
fn xcm_evm_transfer_eip_7702_transaction(destination: H160, value: U256) -> EthereumXcmTransaction {
49
12
	EthereumXcmTransaction::V3(EthereumXcmTransactionV3 {
50
12
		gas_limit: U256::from(0x5208),
51
12
		action: ethereum::TransactionAction::Call(destination),
52
12
		value,
53
12
		input:
54
12
			BoundedVec::<u8, ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>>::try_from(
55
12
				vec![],
56
12
			)
57
12
			.unwrap(),
58
12
		access_list: None,
59
12
		authorization_list: None,
60
12
	})
61
12
}
62

            
63
2
fn xcm_evm_call_eip_7702_transaction(destination: H160, input: Vec<u8>) -> EthereumXcmTransaction {
64
2
	EthereumXcmTransaction::V3(EthereumXcmTransactionV3 {
65
2
		gas_limit: U256::from(0x100000),
66
2
		action: ethereum::TransactionAction::Call(destination),
67
2
		value: U256::zero(),
68
2
		input:
69
2
			BoundedVec::<u8, ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>>::try_from(
70
2
				input,
71
2
			)
72
2
			.unwrap(),
73
2
		access_list: None,
74
2
		authorization_list: None,
75
2
	})
76
2
}
77

            
78
1
fn xcm_erc20_creation_eip_7702_transaction() -> EthereumXcmTransaction {
79
1
	EthereumXcmTransaction::V3(EthereumXcmTransactionV3 {
80
1
		gas_limit: U256::from(0x100000),
81
1
		action: ethereum::TransactionAction::Create,
82
1
		value: U256::zero(),
83
1
		input:
84
1
			BoundedVec::<u8, ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>>::try_from(
85
1
				hex::decode(CONTRACT).unwrap(),
86
1
			)
87
1
			.unwrap(),
88
1
		access_list: None,
89
1
		authorization_list: None,
90
1
	})
91
1
}
92

            
93
#[test]
94
1
fn test_transact_xcm_evm_transfer() {
95
1
	let (pairs, mut ext) = new_test_ext(2);
96
1
	let alice = &pairs[0];
97
1
	let bob = &pairs[1];
98
1

            
99
1
	ext.execute_with(|| {
100
1
		let balances_before = System::account(&bob.account_id);
101
1
		EthereumXcm::transact(
102
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
103
1
			xcm_evm_transfer_eip_7702_transaction(bob.address, U256::from(100)),
104
1
		)
105
1
		.expect("Failed to execute transaction");
106
1

            
107
1
		assert_eq!(
108
1
			System::account(&bob.account_id).data.free,
109
1
			balances_before.data.free + 100
110
1
		);
111
1
	});
112
1
}
113

            
114
#[test]
115
1
fn test_transact_xcm_create() {
116
1
	let (pairs, mut ext) = new_test_ext(1);
117
1
	let alice = &pairs[0];
118
1

            
119
1
	ext.execute_with(|| {
120
1
		assert_noop!(
121
1
			EthereumXcm::transact(
122
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
123
1
				xcm_erc20_creation_eip_7702_transaction()
124
1
			),
125
1
			DispatchErrorWithPostInfo {
126
1
				post_info: PostDispatchInfo {
127
1
					actual_weight: Some(Weight::zero()),
128
1
					pays_fee: Pays::Yes,
129
1
				},
130
1
				error: DispatchError::Other("Cannot convert xcm payload to known type"),
131
1
			}
132
1
		);
133
1
	});
134
1
}
135

            
136
#[test]
137
1
fn test_transact_xcm_evm_call_works() {
138
1
	let (pairs, mut ext) = new_test_ext(2);
139
1
	let alice = &pairs[0];
140
1
	let bob = &pairs[1];
141
1

            
142
1
	ext.execute_with(|| {
143
1
		let t = EthereumXcmTransactionV3 {
144
1
			gas_limit: U256::from(0x100000),
145
1
			action: ethereum::TransactionAction::Create,
146
1
			value: U256::zero(),
147
1
			input: hex::decode(CONTRACT).unwrap().try_into().unwrap(),
148
1
			access_list: None,
149
1
			authorization_list: vec![].into(),
150
1
		}
151
1
		.into_transaction(U256::zero(), Default::default(), true)
152
1
		.unwrap();
153
1
		// We do not support EIP7702 transactions from XCM
154
1
		assert!(matches!(t, TransactionV3::EIP1559(_)));
155
1
		assert_ok!(Ethereum::execute(alice.address, &t, None, None));
156

            
157
1
		let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap();
158
1
		let foo = hex::decode("c2985578").unwrap();
159
1
		let bar = hex::decode("febb0f7e").unwrap();
160
1

            
161
1
		let _ = EthereumXcm::transact(
162
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
163
1
			xcm_evm_call_eip_7702_transaction(H160::from_slice(&contract_address), foo),
164
1
		)
165
1
		.expect("Failed to call `foo`");
166
1

            
167
1
		// Evm call failing still succesfully dispatched
168
1
		let _ = EthereumXcm::transact(
169
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
170
1
			xcm_evm_call_eip_7702_transaction(H160::from_slice(&contract_address), bar),
171
1
		)
172
1
		.expect("Failed to call `bar`");
173
1

            
174
1
		assert!(pallet_ethereum::Pending::<Test>::count() == 2);
175

            
176
		// Transaction is in Pending storage, with nonce 0 and status 1 (evm succeed).
177
1
		let (transaction_0, _, receipt_0) = &pallet_ethereum::Pending::<Test>::get(0).unwrap();
178
1
		match (transaction_0, receipt_0) {
179
1
			(&crate::Transaction::EIP1559(ref t), &crate::Receipt::EIP1559(ref r)) => {
180
1
				assert!(t.nonce == U256::from(0u8));
181
1
				assert!(r.status_code == 1u8);
182
			}
183
			_ => unreachable!(),
184
		}
185

            
186
		// Transaction is in Pending storage, with nonce 1 and status 0 (evm failed).
187
1
		let (transaction_1, _, receipt_1) = &pallet_ethereum::Pending::<Test>::get(1).unwrap();
188
1
		match (transaction_1, receipt_1) {
189
1
			(&crate::Transaction::EIP1559(ref t), &crate::Receipt::EIP1559(ref r)) => {
190
1
				assert!(t.nonce == U256::from(1u8));
191
1
				assert!(r.status_code == 0u8);
192
			}
193
			_ => unreachable!(),
194
		}
195
1
	});
196
1
}
197

            
198
#[test]
199
1
fn test_transact_xcm_validation_works() {
200
1
	let (pairs, mut ext) = new_test_ext(2);
201
1
	let alice = &pairs[0];
202
1
	let bob = &pairs[1];
203
1

            
204
1
	ext.execute_with(|| {
205
1
		// Not enough gas limit to cover the transaction cost.
206
1
		assert_noop!(
207
1
			EthereumXcm::transact(
208
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
209
1
				EthereumXcmTransaction::V3(EthereumXcmTransactionV3 {
210
1
					gas_limit: U256::from(0x5207),
211
1
					action: ethereum::TransactionAction::Call(bob.address),
212
1
					value: U256::one(),
213
1
					input: BoundedVec::<
214
1
						u8,
215
1
						ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>,
216
1
					>::try_from(vec![])
217
1
					.unwrap(),
218
1
					access_list: None,
219
1
					authorization_list: None,
220
1
				}),
221
1
			),
222
1
			DispatchErrorWithPostInfo {
223
1
				post_info: PostDispatchInfo {
224
1
					actual_weight: Some(Weight::zero()),
225
1
					pays_fee: Pays::Yes,
226
1
				},
227
1
				error: DispatchError::Other("Failed to validate ethereum transaction"),
228
1
			}
229
1
		);
230
1
	});
231
1
}
232

            
233
#[test]
234
1
fn test_ensure_transact_xcm_trough_no_proxy_error() {
235
1
	let (pairs, mut ext) = new_test_ext(2);
236
1
	let alice = &pairs[0];
237
1
	let bob = &pairs[1];
238
1

            
239
1
	ext.execute_with(|| {
240
1
		let r = EthereumXcm::transact_through_proxy(
241
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
242
1
			bob.address,
243
1
			xcm_evm_transfer_eip_7702_transaction(bob.address, U256::from(100)),
244
1
		);
245
1
		assert!(r.is_err());
246
1
		assert_eq!(
247
1
			r.unwrap_err().error,
248
1
			sp_runtime::DispatchError::Other("proxy error: expected `ProxyType::Any`"),
249
1
		);
250
1
	});
251
1
}
252

            
253
#[test]
254
1
fn test_ensure_transact_xcm_trough_proxy_error() {
255
1
	let (pairs, mut ext) = new_test_ext(2);
256
1
	let alice = &pairs[0];
257
1
	let bob = &pairs[1];
258
1

            
259
1
	ext.execute_with(|| {
260
1
		let _ = Proxy::add_proxy_delegate(
261
1
			&bob.account_id,
262
1
			alice.account_id.clone(),
263
1
			ProxyType::NotAllowed,
264
1
			0,
265
1
		);
266
1
		let r = EthereumXcm::transact_through_proxy(
267
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
268
1
			bob.address,
269
1
			xcm_evm_transfer_eip_7702_transaction(bob.address, U256::from(100)),
270
1
		);
271
1
		assert!(r.is_err());
272
1
		assert_eq!(
273
1
			r.unwrap_err().error,
274
1
			sp_runtime::DispatchError::Other("proxy error: expected `ProxyType::Any`"),
275
1
		);
276
1
	});
277
1
}
278

            
279
#[test]
280
1
fn test_ensure_transact_xcm_trough_proxy_ok() {
281
1
	let (pairs, mut ext) = new_test_ext(3);
282
1
	let alice = &pairs[0];
283
1
	let bob = &pairs[1];
284
1
	let charlie = &pairs[2];
285
1

            
286
1
	let allowed_proxies = vec![ProxyType::Any];
287

            
288
1
	for proxy in allowed_proxies.into_iter() {
289
1
		ext.execute_with(|| {
290
1
			let _ = Proxy::add_proxy_delegate(&bob.account_id, alice.account_id.clone(), proxy, 0);
291
1
			let alice_before = System::account(&alice.account_id);
292
1
			let bob_before = System::account(&bob.account_id);
293
1
			let charlie_before = System::account(&charlie.account_id);
294
1

            
295
1
			let r = EthereumXcm::transact_through_proxy(
296
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
297
1
				bob.address,
298
1
				xcm_evm_transfer_eip_7702_transaction(charlie.address, U256::from(100)),
299
1
			);
300
1
			// Transact succeeded
301
1
			assert!(r.is_ok());
302

            
303
1
			let alice_after = System::account(&alice.account_id);
304
1
			let bob_after = System::account(&bob.account_id);
305
1
			let charlie_after = System::account(&charlie.account_id);
306
1

            
307
1
			// Alice remains unchanged
308
1
			assert_eq!(alice_before, alice_after);
309

            
310
			// Bob nonce was increased
311
1
			assert_eq!(bob_after.nonce, bob_before.nonce + 1);
312

            
313
			// Bob sent some funds without paying any fees
314
1
			assert_eq!(bob_after.data.free, bob_before.data.free - 100);
315

            
316
			// Charlie receive some funds
317
1
			assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
318

            
319
			// Clear proxy
320
			let _ =
321
1
				Proxy::remove_proxy_delegate(&bob.account_id, alice.account_id.clone(), proxy, 0);
322
1
		});
323
1
	}
324
1
}
325

            
326
#[test]
327
1
fn test_global_nonce_incr() {
328
1
	let (pairs, mut ext) = new_test_ext(3);
329
1
	let alice = &pairs[0];
330
1
	let bob = &pairs[1];
331
1
	let charlie = &pairs[2];
332
1

            
333
1
	ext.execute_with(|| {
334
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
335

            
336
1
		EthereumXcm::transact(
337
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
338
1
			xcm_evm_transfer_eip_7702_transaction(charlie.address, U256::one()),
339
1
		)
340
1
		.expect("Failed to execute transaction from Alice to Charlie");
341
1

            
342
1
		assert_eq!(EthereumXcm::nonce(), U256::one());
343

            
344
1
		EthereumXcm::transact(
345
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
346
1
			xcm_evm_transfer_eip_7702_transaction(charlie.address, U256::one()),
347
1
		)
348
1
		.expect("Failed to execute transaction from Bob to Charlie");
349
1

            
350
1
		assert_eq!(EthereumXcm::nonce(), U256::from(2));
351
1
	});
352
1
}
353

            
354
#[test]
355
1
fn test_global_nonce_not_incr() {
356
1
	let (pairs, mut ext) = new_test_ext(2);
357
1
	let alice = &pairs[0];
358
1
	let bob = &pairs[1];
359
1

            
360
1
	ext.execute_with(|| {
361
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
362

            
363
1
		let invalid_transaction_cost =
364
1
			EthereumXcmTransaction::V3(
365
1
				EthereumXcmTransactionV3 {
366
1
					gas_limit: U256::one(),
367
1
					action: ethereum::TransactionAction::Call(bob.address),
368
1
					value: U256::one(),
369
1
					input: BoundedVec::<
370
1
						u8,
371
1
						ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>,
372
1
					>::try_from(vec![])
373
1
					.unwrap(),
374
1
					access_list: None,
375
1
					authorization_list: None,
376
1
				},
377
1
			);
378
1

            
379
1
		EthereumXcm::transact(
380
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
381
1
			invalid_transaction_cost,
382
1
		)
383
1
		.expect_err("Failed to execute transaction from Alice to Bob");
384
1

            
385
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
386
1
	});
387
1
}
388

            
389
#[test]
390
1
fn test_transaction_hash_collision() {
391
1
	let (pairs, mut ext) = new_test_ext(3);
392
1
	let alice = &pairs[0];
393
1
	let bob = &pairs[1];
394
1
	let charlie = &pairs[2];
395
1

            
396
1
	ext.execute_with(|| {
397
1
		EthereumXcm::transact(
398
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
399
1
			xcm_evm_transfer_eip_7702_transaction(charlie.address, U256::one()),
400
1
		)
401
1
		.expect("Failed to execute transaction from Alice to Charlie");
402
1

            
403
1
		EthereumXcm::transact(
404
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
405
1
			xcm_evm_transfer_eip_7702_transaction(charlie.address, U256::one()),
406
1
		)
407
1
		.expect("Failed to execute transaction from Bob to Charlie");
408
1

            
409
1
		let mut hashes = pallet_ethereum::Pending::<Test>::iter_values()
410
2
			.map(|(tx, _, _)| tx.hash())
411
1
			.collect::<Vec<ethereum_types::H256>>();
412
1

            
413
1
		// Holds two transactions hashes
414
1
		assert_eq!(hashes.len(), 2);
415

            
416
1
		hashes.dedup();
417
1

            
418
1
		// Still holds two transactions hashes after removing potential consecutive repeated values.
419
1
		assert_eq!(hashes.len(), 2);
420
1
	});
421
1
}
422

            
423
#[test]
424
1
fn check_suspend_ethereum_to_xcm_works() {
425
1
	let (pairs, mut ext) = new_test_ext(2);
426
1
	let alice = &pairs[0];
427
1
	let bob = &pairs[1];
428
1

            
429
1
	let db_weights: frame_support::weights::RuntimeDbWeight =
430
1
		<Test as frame_system::Config>::DbWeight::get();
431
1

            
432
1
	ext.execute_with(|| {
433
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
434
1
			RuntimeOrigin::root(),
435
1
		));
436
1
		assert_noop!(
437
1
			EthereumXcm::transact(
438
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
439
1
				xcm_evm_transfer_eip_7702_transaction(bob.address, U256::from(100)),
440
1
			),
441
1
			DispatchErrorWithPostInfo {
442
1
				error: Error::<Test>::EthereumXcmExecutionSuspended.into(),
443
1
				post_info: PostDispatchInfo {
444
1
					actual_weight: Some(db_weights.reads(1)),
445
1
					pays_fee: Pays::Yes
446
1
				}
447
1
			}
448
1
		);
449

            
450
1
		assert_noop!(
451
1
			EthereumXcm::transact_through_proxy(
452
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
453
1
				bob.address,
454
1
				xcm_evm_transfer_eip_7702_transaction(bob.address, U256::from(100)),
455
1
			),
456
1
			DispatchErrorWithPostInfo {
457
1
				error: Error::<Test>::EthereumXcmExecutionSuspended.into(),
458
1
				post_info: PostDispatchInfo {
459
1
					actual_weight: Some(db_weights.reads(1)),
460
1
					pays_fee: Pays::Yes
461
1
				}
462
1
			}
463
1
		);
464
1
	});
465
1
}
466

            
467
#[test]
468
1
fn transact_after_resume_ethereum_to_xcm_works() {
469
1
	let (pairs, mut ext) = new_test_ext(2);
470
1
	let alice = &pairs[0];
471
1
	let bob = &pairs[1];
472
1

            
473
1
	ext.execute_with(|| {
474
1
		let bob_before = System::account(&bob.account_id);
475
1

            
476
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
477
1
			RuntimeOrigin::root()
478
1
		));
479

            
480
1
		assert_ok!(EthereumXcm::resume_ethereum_xcm_execution(
481
1
			RuntimeOrigin::root()
482
1
		));
483
1
		assert_ok!(EthereumXcm::transact(
484
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
485
1
			xcm_evm_transfer_eip_7702_transaction(bob.address, U256::from(100)),
486
1
		));
487
1
		let bob_after = System::account(&bob.account_id);
488
1

            
489
1
		// Bob sent some funds without paying any fees
490
1
		assert_eq!(bob_after.data.free, bob_before.data.free + 100);
491
1
	});
492
1
}
493

            
494
#[test]
495
1
fn transact_through_proxy_after_resume_ethereum_to_xcm_works() {
496
1
	let (pairs, mut ext) = new_test_ext(3);
497
1
	let alice = &pairs[0];
498
1
	let bob = &pairs[1];
499
1
	let charlie = &pairs[2];
500
1

            
501
1
	ext.execute_with(|| {
502
1
		let _ =
503
1
			Proxy::add_proxy_delegate(&bob.account_id, alice.account_id.clone(), ProxyType::Any, 0);
504
1
		let alice_before = System::account(&alice.account_id);
505
1
		let bob_before = System::account(&bob.account_id);
506
1
		let charlie_before = System::account(&charlie.account_id);
507
1

            
508
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
509
1
			RuntimeOrigin::root()
510
1
		));
511

            
512
1
		assert_ok!(EthereumXcm::resume_ethereum_xcm_execution(
513
1
			RuntimeOrigin::root()
514
1
		));
515
1
		assert_ok!(EthereumXcm::transact_through_proxy(
516
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
517
1
			bob.address,
518
1
			xcm_evm_transfer_eip_7702_transaction(charlie.address, U256::from(100)),
519
1
		));
520

            
521
1
		let alice_after = System::account(&alice.account_id);
522
1
		let bob_after = System::account(&bob.account_id);
523
1
		let charlie_after = System::account(&charlie.account_id);
524
1

            
525
1
		// Alice remains unchanged
526
1
		assert_eq!(alice_before, alice_after);
527

            
528
		// Bob nonce was increased
529
1
		assert_eq!(bob_after.nonce, bob_before.nonce + 1);
530

            
531
		// Bob sent some funds without paying any fees
532
1
		assert_eq!(bob_after.data.free, bob_before.data.free - 100);
533

            
534
		// Charlie receive some funds
535
1
		assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
536
1
	});
537
1
}