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_eip_2930_transaction(destination: H160, value: U256) -> EthereumXcmTransaction {
52
8
	let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
53
8

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

            
68
2
fn xcm_evm_call_eip_2930_transaction(destination: H160, input: Vec<u8>) -> EthereumXcmTransaction {
69
2
	let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
70
2

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

            
88
1
fn xcm_erc20_creation_eip_2930_transaction() -> EthereumXcmTransaction {
89
1
	let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
90
1

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

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

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

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

            
129
#[test]
130
1
fn test_transact_xcm_create() {
131
1
	let (pairs, mut ext) = new_test_ext(1);
132
1
	let alice = &pairs[0];
133
1

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

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

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

            
174
1
		let contract_address = hex::decode("32dcab0ef3fb2de2fce1d2e0799d36239671f04a").unwrap();
175
1
		let foo = hex::decode("c2985578").unwrap();
176
1
		let bar = hex::decode("febb0f7e").unwrap();
177
1

            
178
1
		let _ = EthereumXcm::transact(
179
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
180
1
			xcm_evm_call_eip_2930_transaction(H160::from_slice(&contract_address), foo),
181
1
		)
182
1
		.expect("Failed to call `foo`");
183
1

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

            
191
1
		assert!(pallet_ethereum::Pending::<Test>::count() == 2);
192

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

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

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

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

            
253
#[test]
254
1
fn test_ensure_transact_xcm_trough_no_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 r = EthereumXcm::transact_through_proxy(
261
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
262
1
			bob.address,
263
1
			xcm_evm_transfer_eip_2930_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_error() {
275
1
	let (pairs, mut ext) = new_test_ext(2);
276
1
	let alice = &pairs[0];
277
1
	let bob = &pairs[1];
278
1

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

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

            
306
1
	let allowed_proxies = vec![ProxyType::Any];
307

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

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

            
323
1
			let alice_after = System::account(&alice.account_id);
324
1
			let bob_after = System::account(&bob.account_id);
325
1
			let charlie_after = System::account(&charlie.account_id);
326
1

            
327
1
			// Alice remains unchanged
328
1
			assert_eq!(alice_before, alice_after);
329

            
330
			// Bob nonce was increased
331
1
			assert_eq!(bob_after.nonce, bob_before.nonce + 1);
332

            
333
			// Bob sent some funds without paying any fees
334
1
			assert_eq!(bob_after.data.free, bob_before.data.free - 100);
335

            
336
			// Charlie receive some funds
337
1
			assert_eq!(charlie_after.data.free, charlie_before.data.free + 100);
338

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

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

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

            
356
1
		EthereumXcm::transact(
357
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
358
1
			xcm_evm_transfer_eip_2930_transaction(charlie.address, U256::one()),
359
1
		)
360
1
		.expect("Failed to execute transaction from Alice to Charlie");
361
1

            
362
1
		assert_eq!(EthereumXcm::nonce(), U256::one());
363

            
364
1
		EthereumXcm::transact(
365
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
366
1
			xcm_evm_transfer_eip_2930_transaction(charlie.address, U256::one()),
367
1
		)
368
1
		.expect("Failed to execute transaction from Bob to Charlie");
369
1

            
370
1
		assert_eq!(EthereumXcm::nonce(), U256::from(2));
371
1
	});
372
1
}
373

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

            
380
1
	ext.execute_with(|| {
381
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
382

            
383
1
		let access_list = Some(vec![(H160::default(), vec![H256::default()])]);
384
1

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

            
401
1
		EthereumXcm::transact(
402
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
403
1
			invalid_transaction_cost,
404
1
		)
405
1
		.expect_err("Failed to execute transaction from Alice to Bob");
406
1

            
407
1
		assert_eq!(EthereumXcm::nonce(), U256::zero());
408
1
	});
409
1
}
410

            
411
#[test]
412
1
fn test_transaction_hash_collision() {
413
1
	let (pairs, mut ext) = new_test_ext(3);
414
1
	let alice = &pairs[0];
415
1
	let bob = &pairs[1];
416
1
	let charlie = &pairs[2];
417
1

            
418
1
	ext.execute_with(|| {
419
1
		EthereumXcm::transact(
420
1
			RawOrigin::XcmEthereumTransaction(alice.address).into(),
421
1
			xcm_evm_transfer_eip_2930_transaction(charlie.address, U256::one()),
422
1
		)
423
1
		.expect("Failed to execute transaction from Alice to Charlie");
424
1

            
425
1
		EthereumXcm::transact(
426
1
			RawOrigin::XcmEthereumTransaction(bob.address).into(),
427
1
			xcm_evm_transfer_eip_2930_transaction(charlie.address, U256::one()),
428
1
		)
429
1
		.expect("Failed to execute transaction from Bob to Charlie");
430
1

            
431
1
		let mut hashes = pallet_ethereum::Pending::<Test>::iter_values()
432
2
			.map(|(tx, _, _)| tx.hash())
433
1
			.collect::<Vec<ethereum_types::H256>>();
434
1

            
435
1
		// Holds two transactions hashes
436
1
		assert_eq!(hashes.len(), 2);
437

            
438
1
		hashes.dedup();
439
1

            
440
1
		// Still holds two transactions hashes after removing potential consecutive repeated values.
441
1
		assert_eq!(hashes.len(), 2);
442
1
	});
443
1
}