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
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, EthereumXcmTransactionV2};
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_1559_transaction(destination: H160, value: U256) -> EthereumXcmTransaction {
48
12
	EthereumXcmTransaction::V2(EthereumXcmTransactionV2 {
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
	})
59
12
}
60

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

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

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

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

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

            
110
#[test]
111
1
fn test_transact_xcm_create() {
112
1
	let (pairs, mut ext) = new_test_ext(1);
113
1
	let alice = &pairs[0];
114
1

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

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

            
138
1
	ext.execute_with(|| {
139
1
		let t = EIP1559UnsignedTransaction {
140
1
			nonce: U256::zero(),
141
1
			max_priority_fee_per_gas: U256::one(),
142
1
			max_fee_per_gas: U256::one(),
143
1
			gas_limit: U256::from(0x100000),
144
1
			action: ethereum::TransactionAction::Create,
145
1
			value: U256::zero(),
146
1
			input: hex::decode(CONTRACT).unwrap(),
147
1
		}
148
1
		.sign(&alice.private_key, None);
149
1
		assert_ok!(Ethereum::execute(alice.address, &t, None, None));
150

            
151
1
		let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap();
152
1
		let foo = hex::decode("c2985578").unwrap();
153
1
		let bar = hex::decode("febb0f7e").unwrap();
154
1

            
155
1
		let _ = EthereumXcm::transact(
156
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
157
1
			xcm_evm_call_eip_1559_transaction(H160::from_slice(&contract_address), foo),
158
1
		)
159
1
		.expect("Failed to call `foo`");
160
1

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

            
168
1
		assert!(pallet_ethereum::Pending::<Test>::count() == 2);
169

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

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

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

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

            
226
#[test]
227
1
fn test_ensure_transact_xcm_trough_no_proxy_error() {
228
1
	let (pairs, mut ext) = new_test_ext(2);
229
1
	let alice = &pairs[0];
230
1
	let bob = &pairs[1];
231
1

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

            
246
#[test]
247
1
fn test_ensure_transact_xcm_trough_proxy_error() {
248
1
	let (pairs, mut ext) = new_test_ext(2);
249
1
	let alice = &pairs[0];
250
1
	let bob = &pairs[1];
251
1

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

            
272
#[test]
273
1
fn test_ensure_transact_xcm_trough_proxy_ok() {
274
1
	let (pairs, mut ext) = new_test_ext(3);
275
1
	let alice = &pairs[0];
276
1
	let bob = &pairs[1];
277
1
	let charlie = &pairs[2];
278
1

            
279
1
	let allowed_proxies = vec![ProxyType::Any];
280

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

            
288
1
			let r = EthereumXcm::transact_through_proxy(
289
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
290
1
				bob.address,
291
1
				xcm_evm_transfer_eip_1559_transaction(charlie.address, U256::from(100)),
292
1
			);
293
1
			// Transact succeeded
294
1
			assert!(r.is_ok());
295

            
296
1
			let alice_after = System::account(&alice.account_id);
297
1
			let bob_after = System::account(&bob.account_id);
298
1
			let charlie_after = System::account(&charlie.account_id);
299
1

            
300
1
			// Alice remains unchanged
301
1
			assert_eq!(alice_before, alice_after);
302

            
303
			// Bob nonce was increased
304
1
			assert_eq!(bob_after.nonce, bob_before.nonce + 1);
305

            
306
			// Bob sent some funds without paying any fees
307
1
			assert_eq!(bob_after.data.free, bob_before.data.free - 100);
308

            
309
			// Charlie receive some funds
310
1
			assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
311

            
312
			// Clear proxy
313
			let _ =
314
1
				Proxy::remove_proxy_delegate(&bob.account_id, alice.account_id.clone(), proxy, 0);
315
1
		});
316
1
	}
317
1
}
318

            
319
#[test]
320
1
fn test_global_nonce_incr() {
321
1
	let (pairs, mut ext) = new_test_ext(3);
322
1
	let alice = &pairs[0];
323
1
	let bob = &pairs[1];
324
1
	let charlie = &pairs[2];
325
1

            
326
1
	ext.execute_with(|| {
327
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
328

            
329
1
		EthereumXcm::transact(
330
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
331
1
			xcm_evm_transfer_eip_1559_transaction(charlie.address, U256::one()),
332
1
		)
333
1
		.expect("Failed to execute transaction from Alice to Charlie");
334
1

            
335
1
		assert_eq!(EthereumXcm::nonce(), U256::one());
336

            
337
1
		EthereumXcm::transact(
338
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
339
1
			xcm_evm_transfer_eip_1559_transaction(charlie.address, U256::one()),
340
1
		)
341
1
		.expect("Failed to execute transaction from Bob to Charlie");
342
1

            
343
1
		assert_eq!(EthereumXcm::nonce(), U256::from(2));
344
1
	});
345
1
}
346

            
347
#[test]
348
1
fn test_global_nonce_not_incr() {
349
1
	let (pairs, mut ext) = new_test_ext(2);
350
1
	let alice = &pairs[0];
351
1
	let bob = &pairs[1];
352
1

            
353
1
	ext.execute_with(|| {
354
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
355

            
356
1
		let invalid_transaction_cost =
357
1
			EthereumXcmTransaction::V2(
358
1
				EthereumXcmTransactionV2 {
359
1
					gas_limit: U256::one(),
360
1
					action: ethereum::TransactionAction::Call(bob.address),
361
1
					value: U256::one(),
362
1
					input: BoundedVec::<
363
1
						u8,
364
1
						ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>,
365
1
					>::try_from(vec![])
366
1
					.unwrap(),
367
1
					access_list: None,
368
1
				},
369
1
			);
370
1

            
371
1
		EthereumXcm::transact(
372
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
373
1
			invalid_transaction_cost,
374
1
		)
375
1
		.expect_err("Failed to execute transaction from Alice to Bob");
376
1

            
377
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
378
1
	});
379
1
}
380

            
381
#[test]
382
1
fn test_transaction_hash_collision() {
383
1
	let (pairs, mut ext) = new_test_ext(3);
384
1
	let alice = &pairs[0];
385
1
	let bob = &pairs[1];
386
1
	let charlie = &pairs[2];
387
1

            
388
1
	ext.execute_with(|| {
389
1
		EthereumXcm::transact(
390
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
391
1
			xcm_evm_transfer_eip_1559_transaction(charlie.address, U256::one()),
392
1
		)
393
1
		.expect("Failed to execute transaction from Alice to Charlie");
394
1

            
395
1
		EthereumXcm::transact(
396
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
397
1
			xcm_evm_transfer_eip_1559_transaction(charlie.address, U256::one()),
398
1
		)
399
1
		.expect("Failed to execute transaction from Bob to Charlie");
400
1

            
401
1
		let mut hashes = pallet_ethereum::Pending::<Test>::iter_values()
402
2
			.map(|(tx, _, _)| tx.hash())
403
1
			.collect::<Vec<ethereum_types::H256>>();
404
1

            
405
1
		// Holds two transactions hashes
406
1
		assert_eq!(hashes.len(), 2);
407

            
408
1
		hashes.dedup();
409
1

            
410
1
		// Still holds two transactions hashes after removing potential consecutive repeated values.
411
1
		assert_eq!(hashes.len(), 2);
412
1
	});
413
1
}
414

            
415
#[test]
416
1
fn check_suspend_ethereum_to_xcm_works() {
417
1
	let (pairs, mut ext) = new_test_ext(2);
418
1
	let alice = &pairs[0];
419
1
	let bob = &pairs[1];
420
1

            
421
1
	let db_weights: frame_support::weights::RuntimeDbWeight =
422
1
		<Test as frame_system::Config>::DbWeight::get();
423
1

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

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

            
459
#[test]
460
1
fn transact_after_resume_ethereum_to_xcm_works() {
461
1
	let (pairs, mut ext) = new_test_ext(2);
462
1
	let alice = &pairs[0];
463
1
	let bob = &pairs[1];
464
1

            
465
1
	ext.execute_with(|| {
466
1
		let bob_before = System::account(&bob.account_id);
467
1

            
468
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
469
1
			RuntimeOrigin::root()
470
1
		));
471

            
472
1
		assert_ok!(EthereumXcm::resume_ethereum_xcm_execution(
473
1
			RuntimeOrigin::root()
474
1
		));
475
1
		assert_ok!(EthereumXcm::transact(
476
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
477
1
			xcm_evm_transfer_eip_1559_transaction(bob.address, U256::from(100)),
478
1
		));
479
1
		let bob_after = System::account(&bob.account_id);
480
1

            
481
1
		// Bob sent some funds without paying any fees
482
1
		assert_eq!(bob_after.data.free, bob_before.data.free + 100);
483
1
	});
484
1
}
485

            
486
#[test]
487
1
fn transact_through_proxy_after_resume_ethereum_to_xcm_works() {
488
1
	let (pairs, mut ext) = new_test_ext(3);
489
1
	let alice = &pairs[0];
490
1
	let bob = &pairs[1];
491
1
	let charlie = &pairs[2];
492
1

            
493
1
	ext.execute_with(|| {
494
1
		let _ =
495
1
			Proxy::add_proxy_delegate(&bob.account_id, alice.account_id.clone(), ProxyType::Any, 0);
496
1
		let alice_before = System::account(&alice.account_id);
497
1
		let bob_before = System::account(&bob.account_id);
498
1
		let charlie_before = System::account(&charlie.account_id);
499
1

            
500
1
		assert_ok!(EthereumXcm::suspend_ethereum_xcm_execution(
501
1
			RuntimeOrigin::root()
502
1
		));
503

            
504
1
		assert_ok!(EthereumXcm::resume_ethereum_xcm_execution(
505
1
			RuntimeOrigin::root()
506
1
		));
507
1
		assert_ok!(EthereumXcm::transact_through_proxy(
508
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
509
1
			bob.address,
510
1
			xcm_evm_transfer_eip_1559_transaction(charlie.address, U256::from(100)),
511
1
		));
512

            
513
1
		let alice_after = System::account(&alice.account_id);
514
1
		let bob_after = System::account(&bob.account_id);
515
1
		let charlie_after = System::account(&charlie.account_id);
516
1

            
517
1
		// Alice remains unchanged
518
1
		assert_eq!(alice_before, alice_after);
519

            
520
		// Bob nonce was increased
521
1
		assert_eq!(bob_after.nonce, bob_before.nonce + 1);
522

            
523
		// Bob sent some funds without paying any fees
524
1
		assert_eq!(bob_after.data.free, bob_before.data.free - 100);
525

            
526
		// Charlie receive some funds
527
1
		assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
528
1
	});
529
1
}