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 super::*;
18
use ethereum::TransactionV3;
19
use frame_support::{
20
	assert_noop,
21
	dispatch::{Pays, PostDispatchInfo},
22
	traits::ConstU32,
23
	weights::Weight,
24
	BoundedVec,
25
};
26
use sp_runtime::{DispatchError, DispatchErrorWithPostInfo};
27
use xcm_primitives::{
28
	EthereumXcmFee, EthereumXcmTransaction, EthereumXcmTransactionV1, ManualEthereumXcmFee,
29
	XcmToEthereum,
30
};
31

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

            
51
8
fn xcm_evm_transfer_legacy_transaction(destination: H160, value: U256) -> EthereumXcmTransaction {
52
8
	EthereumXcmTransaction::V1(EthereumXcmTransactionV1 {
53
8
		fee_payment: EthereumXcmFee::Auto,
54
8
		gas_limit: U256::from(0x100000),
55
8
		action: ethereum::TransactionAction::Call(destination),
56
8
		value,
57
8
		input:
58
8
			BoundedVec::<u8, ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>>::try_from(
59
8
				vec![],
60
8
			)
61
8
			.unwrap(),
62
8
		access_list: None,
63
8
	})
64
8
}
65

            
66
2
fn xcm_evm_call_eip_legacy_transaction(
67
2
	destination: H160,
68
2
	input: Vec<u8>,
69
2
) -> EthereumXcmTransaction {
70
2
	EthereumXcmTransaction::V1(EthereumXcmTransactionV1 {
71
2
		fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
72
2
			gas_price: Some(U256::from(1)),
73
2
			max_fee_per_gas: None,
74
2
		}),
75
2
		gas_limit: U256::from(0x100000),
76
2
		action: ethereum::TransactionAction::Call(destination),
77
2
		value: U256::zero(),
78
2
		input:
79
2
			BoundedVec::<u8, ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>>::try_from(
80
2
				input,
81
2
			)
82
2
			.unwrap(),
83
2
		access_list: None,
84
2
	})
85
2
}
86

            
87
1
fn xcm_erc20_creation_legacy_transaction() -> EthereumXcmTransaction {
88
1
	EthereumXcmTransaction::V1(EthereumXcmTransactionV1 {
89
1
		fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
90
1
			gas_price: Some(U256::from(1)),
91
1
			max_fee_per_gas: None,
92
1
		}),
93
1
		gas_limit: U256::from(0x100000),
94
1
		action: ethereum::TransactionAction::Create,
95
1
		value: U256::zero(),
96
1
		input:
97
1
			BoundedVec::<u8, ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>>::try_from(
98
1
				hex::decode(CONTRACT).unwrap(),
99
1
			)
100
1
			.unwrap(),
101
1
		access_list: None,
102
1
	})
103
1
}
104

            
105
#[test]
106
1
fn test_transact_xcm_evm_transfer() {
107
1
	let (pairs, mut ext) = new_test_ext(2);
108
1
	let alice = &pairs[0];
109
1
	let bob = &pairs[1];
110
1

            
111
1
	ext.execute_with(|| {
112
1
		let balances_before = System::account(&bob.account_id);
113
1
		EthereumXcm::transact(
114
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
115
1
			xcm_evm_transfer_legacy_transaction(bob.address, U256::from(100)),
116
1
		)
117
1
		.expect("Failed to execute transaction");
118
1

            
119
1
		assert_eq!(
120
1
			System::account(&bob.account_id).data.free,
121
1
			balances_before.data.free + 100
122
1
		);
123
1
	});
124
1
}
125

            
126
#[test]
127
1
fn test_transact_xcm_create() {
128
1
	let (pairs, mut ext) = new_test_ext(1);
129
1
	let alice = &pairs[0];
130
1

            
131
1
	ext.execute_with(|| {
132
1
		assert_noop!(
133
1
			EthereumXcm::transact(
134
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
135
1
				xcm_erc20_creation_legacy_transaction()
136
1
			),
137
1
			DispatchErrorWithPostInfo {
138
1
				post_info: PostDispatchInfo {
139
1
					actual_weight: Some(Weight::zero()),
140
1
					pays_fee: Pays::Yes,
141
1
				},
142
1
				error: DispatchError::Other("Cannot convert xcm payload to known type"),
143
1
			}
144
1
		);
145
1
	});
146
1
}
147

            
148
#[test]
149
1
fn test_transact_xcm_evm_call_works() {
150
1
	let (pairs, mut ext) = new_test_ext(2);
151
1
	let alice = &pairs[0];
152
1
	let bob = &pairs[1];
153
1

            
154
1
	ext.execute_with(|| {
155
1
		let t = EthereumXcmTransactionV1 {
156
1
			gas_limit: U256::from(0x100000),
157
1
			fee_payment: EthereumXcmFee::Manual(ManualEthereumXcmFee {
158
1
				gas_price: Some(U256::from(1)),
159
1
				max_fee_per_gas: None,
160
1
			}),
161
1
			action: ethereum::TransactionAction::Create,
162
1
			value: U256::zero(),
163
1
			input: hex::decode(CONTRACT).unwrap().try_into().unwrap(),
164
1
			access_list: None,
165
1
		}
166
1
		.into_transaction(U256::zero(), Default::default(), true)
167
1
		.unwrap();
168
1
		assert!(matches!(t, TransactionV3::Legacy(_)));
169
1
		assert_ok!(Ethereum::execute(alice.address, &t, None, None));
170

            
171
1
		let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap();
172
1
		let foo = hex::decode("c2985578").unwrap();
173
1
		let bar = hex::decode("febb0f7e").unwrap();
174
1

            
175
1
		let _ = EthereumXcm::transact(
176
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
177
1
			xcm_evm_call_eip_legacy_transaction(H160::from_slice(&contract_address), foo),
178
1
		)
179
1
		.expect("Failed to call `foo`");
180
1

            
181
1
		// Evm call failing still succesfully dispatched
182
1
		let _ = EthereumXcm::transact(
183
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
184
1
			xcm_evm_call_eip_legacy_transaction(H160::from_slice(&contract_address), bar),
185
1
		)
186
1
		.expect("Failed to call `bar`");
187
1

            
188
1
		assert!(pallet_ethereum::Pending::<Test>::count() == 2);
189

            
190
		// Transaction is in Pending storage, with nonce 0 and status 1 (evm succeed).
191
1
		let (transaction_0, _, receipt_0) = &pallet_ethereum::Pending::<Test>::get(0).unwrap();
192
1
		match (transaction_0, receipt_0) {
193
1
			(&crate::Transaction::Legacy(ref t), &crate::Receipt::Legacy(ref r)) => {
194
1
				assert!(t.nonce == U256::from(0u8));
195
1
				assert!(r.status_code == 1u8);
196
			}
197
			_ => unreachable!(),
198
		}
199

            
200
		// Transaction is in Pending storage, with nonce 1 and status 0 (evm failed).
201
1
		let (transaction_1, _, receipt_1) = &pallet_ethereum::Pending::<Test>::get(1).unwrap();
202
1
		match (transaction_1, receipt_1) {
203
1
			(&crate::Transaction::Legacy(ref t), &crate::Receipt::Legacy(ref r)) => {
204
1
				assert!(t.nonce == U256::from(1u8));
205
1
				assert!(r.status_code == 0u8);
206
			}
207
			_ => unreachable!(),
208
		}
209
1
	});
210
1
}
211

            
212
#[test]
213
1
fn test_transact_xcm_validation_works() {
214
1
	let (pairs, mut ext) = new_test_ext(2);
215
1
	let alice = &pairs[0];
216
1
	let bob = &pairs[1];
217
1

            
218
1
	ext.execute_with(|| {
219
1
		// Not enough gas limit to cover the transaction cost.
220
1
		assert_noop!(
221
1
			EthereumXcm::transact(
222
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
223
1
				EthereumXcmTransaction::V1(EthereumXcmTransactionV1 {
224
1
					fee_payment: EthereumXcmFee::Manual(xcm_primitives::ManualEthereumXcmFee {
225
1
						gas_price: Some(U256::from(0)),
226
1
						max_fee_per_gas: None,
227
1
					}),
228
1
					gas_limit: U256::from(0x5207),
229
1
					action: ethereum::TransactionAction::Call(bob.address),
230
1
					value: U256::from(1),
231
1
					input: BoundedVec::<
232
1
						u8,
233
1
						ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>,
234
1
					>::try_from(vec![])
235
1
					.unwrap(),
236
1
					access_list: None,
237
1
				}),
238
1
			),
239
1
			DispatchErrorWithPostInfo {
240
1
				post_info: PostDispatchInfo {
241
1
					actual_weight: Some(Weight::zero()),
242
1
					pays_fee: Pays::Yes,
243
1
				},
244
1
				error: DispatchError::Other("Failed to validate ethereum transaction"),
245
1
			}
246
1
		);
247
1
	});
248
1
}
249

            
250
#[test]
251
1
fn test_ensure_transact_xcm_trough_no_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 r = EthereumXcm::transact_through_proxy(
258
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
259
1
			bob.address,
260
1
			xcm_evm_transfer_legacy_transaction(bob.address, U256::from(100)),
261
1
		);
262
1
		assert!(r.is_err());
263
1
		assert_eq!(
264
1
			r.unwrap_err().error,
265
1
			sp_runtime::DispatchError::Other("proxy error: expected `ProxyType::Any`"),
266
1
		);
267
1
	});
268
1
}
269

            
270
#[test]
271
1
fn test_ensure_transact_xcm_trough_proxy_error() {
272
1
	let (pairs, mut ext) = new_test_ext(2);
273
1
	let alice = &pairs[0];
274
1
	let bob = &pairs[1];
275
1

            
276
1
	ext.execute_with(|| {
277
1
		let _ = Proxy::add_proxy_delegate(
278
1
			&bob.account_id,
279
1
			alice.account_id.clone(),
280
1
			ProxyType::NotAllowed,
281
1
			0,
282
1
		);
283
1
		let r = EthereumXcm::transact_through_proxy(
284
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
285
1
			bob.address,
286
1
			xcm_evm_transfer_legacy_transaction(bob.address, U256::from(100)),
287
1
		);
288
1
		assert!(r.is_err());
289
1
		assert_eq!(
290
1
			r.unwrap_err().error,
291
1
			sp_runtime::DispatchError::Other("proxy error: expected `ProxyType::Any`"),
292
1
		);
293
1
	});
294
1
}
295

            
296
#[test]
297
1
fn test_ensure_transact_xcm_trough_proxy_ok() {
298
1
	let (pairs, mut ext) = new_test_ext(3);
299
1
	let alice = &pairs[0];
300
1
	let bob = &pairs[1];
301
1
	let charlie = &pairs[2];
302
1

            
303
1
	let allowed_proxies = vec![ProxyType::Any];
304

            
305
1
	for proxy in allowed_proxies.into_iter() {
306
1
		ext.execute_with(|| {
307
1
			let _ = Proxy::add_proxy_delegate(&bob.account_id, alice.account_id.clone(), proxy, 0);
308
1
			let alice_before = System::account(&alice.account_id);
309
1
			let bob_before = System::account(&bob.account_id);
310
1
			let charlie_before = System::account(&charlie.account_id);
311
1

            
312
1
			let r = EthereumXcm::transact_through_proxy(
313
1
				RawOrigin::XcmEthereumTransaction(alice.address).into(),
314
1
				bob.address,
315
1
				xcm_evm_transfer_legacy_transaction(charlie.address, U256::from(100)),
316
1
			);
317
1
			// Transact succeeded
318
1
			assert!(r.is_ok());
319

            
320
1
			let alice_after = System::account(&alice.account_id);
321
1
			let bob_after = System::account(&bob.account_id);
322
1
			let charlie_after = System::account(&charlie.account_id);
323
1

            
324
1
			// Alice remains unchanged
325
1
			assert_eq!(alice_before, alice_after);
326

            
327
			// Bob nonce was increased
328
1
			assert_eq!(bob_after.nonce, bob_before.nonce + 1);
329

            
330
			// Bob sent some funds without paying any fees
331
1
			assert_eq!(bob_after.data.free, bob_before.data.free - 100);
332

            
333
			// Charlie receive some funds
334
1
			assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
335

            
336
			// Clear proxy
337
			let _ =
338
1
				Proxy::remove_proxy_delegate(&bob.account_id, alice.account_id.clone(), proxy, 0);
339
1
		});
340
1
	}
341
1
}
342

            
343
#[test]
344
1
fn test_global_nonce_incr() {
345
1
	let (pairs, mut ext) = new_test_ext(3);
346
1
	let alice = &pairs[0];
347
1
	let bob = &pairs[1];
348
1
	let charlie = &pairs[2];
349
1

            
350
1
	ext.execute_with(|| {
351
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
352

            
353
1
		EthereumXcm::transact(
354
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
355
1
			xcm_evm_transfer_legacy_transaction(charlie.address, U256::one()),
356
1
		)
357
1
		.expect("Failed to execute transaction from Alice to Charlie");
358
1

            
359
1
		assert_eq!(EthereumXcm::nonce(), U256::one());
360

            
361
1
		EthereumXcm::transact(
362
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
363
1
			xcm_evm_transfer_legacy_transaction(charlie.address, U256::one()),
364
1
		)
365
1
		.expect("Failed to execute transaction from Bob to Charlie");
366
1

            
367
1
		assert_eq!(EthereumXcm::nonce(), U256::from(2));
368
1
	});
369
1
}
370

            
371
#[test]
372
1
fn test_global_nonce_not_incr() {
373
1
	let (pairs, mut ext) = new_test_ext(2);
374
1
	let alice = &pairs[0];
375
1
	let bob = &pairs[1];
376
1

            
377
1
	ext.execute_with(|| {
378
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
379

            
380
1
		let invalid_transaction_cost =
381
1
			EthereumXcmTransaction::V1(
382
1
				EthereumXcmTransactionV1 {
383
1
					fee_payment: EthereumXcmFee::Auto,
384
1
					gas_limit: U256::one(),
385
1
					action: ethereum::TransactionAction::Call(bob.address),
386
1
					value: U256::one(),
387
1
					input: BoundedVec::<
388
1
						u8,
389
1
						ConstU32<{ xcm_primitives::MAX_ETHEREUM_XCM_INPUT_SIZE }>,
390
1
					>::try_from(vec![])
391
1
					.unwrap(),
392
1
					access_list: None,
393
1
				},
394
1
			);
395
1

            
396
1
		EthereumXcm::transact(
397
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
398
1
			invalid_transaction_cost,
399
1
		)
400
1
		.expect_err("Failed to execute transaction from Alice to Bob");
401
1

            
402
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
403
1
	});
404
1
}
405

            
406
#[test]
407
1
fn test_transaction_hash_collision() {
408
1
	let (pairs, mut ext) = new_test_ext(3);
409
1
	let alice = &pairs[0];
410
1
	let bob = &pairs[1];
411
1
	let charlie = &pairs[2];
412
1

            
413
1
	ext.execute_with(|| {
414
1
		EthereumXcm::transact(
415
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
416
1
			xcm_evm_transfer_legacy_transaction(charlie.address, U256::one()),
417
1
		)
418
1
		.expect("Failed to execute transaction from Alice to Charlie");
419
1

            
420
1
		EthereumXcm::transact(
421
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
422
1
			xcm_evm_transfer_legacy_transaction(charlie.address, U256::one()),
423
1
		)
424
1
		.expect("Failed to execute transaction from Bob to Charlie");
425
1

            
426
1
		let mut hashes = pallet_ethereum::Pending::<Test>::iter_values()
427
2
			.map(|(tx, _, _)| tx.hash())
428
1
			.collect::<Vec<ethereum_types::H256>>();
429
1

            
430
1
		// Holds two transactions hashes
431
1
		assert_eq!(hashes.len(), 2);
432

            
433
1
		hashes.dedup();
434
1

            
435
1
		// Still holds two transactions hashes after removing potential consecutive repeated values.
436
1
		assert_eq!(hashes.len(), 2);
437
1
	});
438
1
}