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

            
62
2
fn xcm_evm_call_eip_1559_transaction(destination: H160, input: Vec<u8>) -> EthereumXcmTransaction {
63
2
	EthereumXcmTransaction::V2(EthereumXcmTransactionV2 {
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
	})
74
2
}
75

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

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

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

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

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

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

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

            
139
1
	ext.execute_with(|| {
140
1
		let t = EthereumXcmTransactionV2 {
141
1
			gas_limit: U256::from(0x100000),
142
1
			action: ethereum::TransactionAction::Create,
143
1
			value: U256::zero(),
144
1
			input: hex::decode(CONTRACT).unwrap().try_into().unwrap(),
145
1
			access_list: None,
146
1
		}
147
1
		.into_transaction(U256::zero(), Default::default(), true)
148
1
		.unwrap();
149
1
		assert!(matches!(t, TransactionV3::EIP1559(_)));
150
1
		assert_ok!(Ethereum::execute(alice.address, &t, None, None));
151

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
409
1
		hashes.dedup();
410
1

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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