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_types::{H160, U256};
18
use frame_support::{
19
	assert_noop, assert_ok,
20
	dispatch::{Pays, PostDispatchInfo},
21
	traits::{ConstU32, Get},
22
	weights::Weight,
23
	BoundedVec,
24
};
25
use sp_runtime::{DispatchError, DispatchErrorWithPostInfo};
26
use xcm_primitives::{EthereumXcmTransaction, EthereumXcmTransactionV3};
27

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

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

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

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

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

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

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

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

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

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

            
141
1
	ext.execute_with(|| {
142
1
		let t = EIP7702UnsignedTransaction {
143
1
			nonce: U256::zero(),
144
1
			max_priority_fee_per_gas: U256::one(),
145
1
			max_fee_per_gas: U256::one(),
146
1
			gas_limit: U256::from(0x100000),
147
1
			destination: ethereum::TransactionAction::Create,
148
1
			value: U256::zero(),
149
1
			data: hex::decode(CONTRACT).unwrap(),
150
1
		}
151
1
		.sign(&alice.private_key, None, Vec::new());
152
1
		assert_ok!(Ethereum::execute(alice.address, &t, None, None));
153

            
154
1
		let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap();
155
1
		let foo = hex::decode("c2985578").unwrap();
156
1
		let bar = hex::decode("febb0f7e").unwrap();
157
1

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

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

            
171
1
		assert!(pallet_ethereum::Pending::<Test>::count() == 2);
172

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

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

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

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

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

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

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

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

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

            
283
1
	let allowed_proxies = vec![ProxyType::Any];
284

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

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

            
300
1
			let alice_after = System::account(&alice.account_id);
301
1
			let bob_after = System::account(&bob.account_id);
302
1
			let charlie_after = System::account(&charlie.account_id);
303
1

            
304
1
			// Alice remains unchanged
305
1
			assert_eq!(alice_before, alice_after);
306

            
307
			// Bob nonce was increased
308
1
			assert_eq!(bob_after.nonce, bob_before.nonce + 1);
309

            
310
			// Bob sent some funds without paying any fees
311
1
			assert_eq!(bob_after.data.free, bob_before.data.free - 100);
312

            
313
			// Charlie receive some funds
314
1
			assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
315

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

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

            
330
1
	ext.execute_with(|| {
331
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
332

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

            
339
1
		assert_eq!(EthereumXcm::nonce(), U256::one());
340

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

            
347
1
		assert_eq!(EthereumXcm::nonce(), U256::from(2));
348
1
	});
349
1
}
350

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

            
357
1
	ext.execute_with(|| {
358
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
359

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

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

            
382
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
383
1
	});
384
1
}
385

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

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

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

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

            
410
1
		// Holds two transactions hashes
411
1
		assert_eq!(hashes.len(), 2);
412

            
413
1
		hashes.dedup();
414
1

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

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

            
426
1
	let db_weights: frame_support::weights::RuntimeDbWeight =
427
1
		<Test as frame_system::Config>::DbWeight::get();
428
1

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

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

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

            
470
1
	ext.execute_with(|| {
471
1
		let bob_before = System::account(&bob.account_id);
472
1

            
473
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
474
1
			RuntimeOrigin::root()
475
1
		));
476

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

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

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

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

            
505
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
506
1
			RuntimeOrigin::root()
507
1
		));
508

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

            
518
1
		let alice_after = System::account(&alice.account_id);
519
1
		let bob_after = System::account(&bob.account_id);
520
1
		let charlie_after = System::account(&charlie.account_id);
521
1

            
522
1
		// Alice remains unchanged
523
1
		assert_eq!(alice_before, alice_after);
524

            
525
		// Bob nonce was increased
526
1
		assert_eq!(bob_after.nonce, bob_before.nonce + 1);
527

            
528
		// Bob sent some funds without paying any fees
529
1
		assert_eq!(bob_after.data.free, bob_before.data.free - 100);
530

            
531
		// Charlie receive some funds
532
1
		assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
533
1
	});
534
1
}