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

            
17
use crate::*;
18
use mock::*;
19

            
20
use frame_support::traits::Currency;
21
use frame_support::{assert_noop, assert_ok};
22
use precompile_utils::testing::Bob;
23
use xcm::latest::prelude::*;
24

            
25
17
fn encode_ticker(str_: &str) -> BoundedVec<u8, ConstU32<256>> {
26
17
	BoundedVec::try_from(str_.as_bytes().to_vec()).expect("too long")
27
17
}
28

            
29
17
fn encode_token_name(str_: &str) -> BoundedVec<u8, ConstU32<256>> {
30
17
	BoundedVec::try_from(str_.as_bytes().to_vec()).expect("too long")
31
17
}
32

            
33
#[test]
34
1
fn create_foreign_and_freeze_unfreeze_using_xcm() {
35
1
	ExtBuilder::default().build().execute_with(|| {
36
1
		let deposit = ForeignAssetCreationDeposit::get();
37

            
38
1
		Balances::make_free_balance_be(&PARA_A, deposit);
39

            
40
1
		let asset_location: Location = (Parent, Parachain(1), PalletInstance(13)).into();
41

            
42
		// create foreign asset
43
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
44
1
			RuntimeOrigin::signed(PARA_A),
45
			1,
46
1
			asset_location.clone(),
47
			18,
48
1
			encode_ticker("MTT"),
49
1
			encode_token_name("Mytoken"),
50
		));
51

            
52
1
		assert_eq!(
53
1
			EvmForeignAssets::assets_by_id(1),
54
1
			Some(asset_location.clone())
55
		);
56
1
		assert_eq!(
57
1
			EvmForeignAssets::assets_by_location(asset_location.clone()),
58
			Some((1, AssetStatus::Active)),
59
		);
60
1
		expect_events(vec![Event::ForeignAssetCreated {
61
1
			contract_address: H160([
62
1
				255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
63
1
			]),
64
1
			asset_id: 1,
65
1
			xcm_location: asset_location.clone(),
66
1
			deposit: Some(deposit),
67
1
		}]);
68

            
69
1
		let (xcm_location, asset_id): (Location, u128) = get_asset_created_hook_invocation()
70
1
			.expect("Decoding of invocation data should not fail");
71
1
		assert_eq!(xcm_location, asset_location.clone());
72
1
		assert_eq!(asset_id, 1u128);
73

            
74
		// Check storage
75
1
		assert_eq!(
76
1
			EvmForeignAssets::assets_by_id(&1),
77
1
			Some(asset_location.clone())
78
		);
79
1
		assert_eq!(
80
1
			EvmForeignAssets::assets_by_location(&asset_location),
81
			Some((1, AssetStatus::Active))
82
		);
83

            
84
		// Unfreeze should return AssetNotFrozen error
85
1
		assert_noop!(
86
1
			EvmForeignAssets::unfreeze_foreign_asset(RuntimeOrigin::signed(PARA_A), 1),
87
1
			Error::<Test>::AssetNotFrozen
88
		);
89

            
90
		// Freeze should work
91
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
92
1
			RuntimeOrigin::signed(PARA_A),
93
			1,
94
			true
95
		),);
96
1
		assert_eq!(
97
1
			EvmForeignAssets::assets_by_location(&asset_location),
98
			Some((1, AssetStatus::FrozenXcmDepositAllowed))
99
		);
100

            
101
		// Should not be able to freeze an asset already frozen
102
1
		assert_noop!(
103
1
			EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::signed(PARA_A), 1, true),
104
1
			Error::<Test>::AssetAlreadyFrozen
105
		);
106

            
107
		// Unfreeze should work
108
1
		assert_ok!(EvmForeignAssets::unfreeze_foreign_asset(
109
1
			RuntimeOrigin::signed(PARA_A),
110
			1
111
		),);
112
1
		assert_eq!(
113
1
			EvmForeignAssets::assets_by_location(&asset_location),
114
			Some((1, AssetStatus::Active))
115
		);
116
1
	});
117
1
}
118

            
119
#[test]
120
1
fn create_foreign_and_freeze_unfreeze_using_root() {
121
1
	ExtBuilder::default().build().execute_with(|| {
122
		// create foreign asset
123
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
124
1
			RuntimeOrigin::root(),
125
			1,
126
1
			Location::parent(),
127
			18,
128
1
			encode_ticker("MTT"),
129
1
			encode_token_name("Mytoken"),
130
		));
131

            
132
1
		assert_eq!(EvmForeignAssets::assets_by_id(1), Some(Location::parent()));
133
1
		assert_eq!(
134
1
			EvmForeignAssets::assets_by_location(Location::parent()),
135
			Some((1, AssetStatus::Active)),
136
		);
137
1
		expect_events(vec![crate::Event::ForeignAssetCreated {
138
1
			contract_address: H160([
139
1
				255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
140
1
			]),
141
1
			asset_id: 1,
142
1
			xcm_location: Location::parent(),
143
1
			deposit: None,
144
1
		}]);
145

            
146
1
		let (xcm_location, asset_id): (Location, u128) = get_asset_created_hook_invocation()
147
1
			.expect("Decoding of invocation data should not fail");
148
1
		assert_eq!(xcm_location, Location::parent());
149
1
		assert_eq!(asset_id, 1u128);
150

            
151
		// Check storage
152
1
		assert_eq!(EvmForeignAssets::assets_by_id(&1), Some(Location::parent()));
153
1
		assert_eq!(
154
1
			EvmForeignAssets::assets_by_location(&Location::parent()),
155
			Some((1, AssetStatus::Active))
156
		);
157

            
158
		// Unfreeze should return AssetNotFrozen error
159
1
		assert_noop!(
160
1
			EvmForeignAssets::unfreeze_foreign_asset(RuntimeOrigin::root(), 1),
161
1
			Error::<Test>::AssetNotFrozen
162
		);
163

            
164
		// Freeze should work
165
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
166
1
			RuntimeOrigin::root(),
167
			1,
168
			true
169
		),);
170
1
		assert_eq!(
171
1
			EvmForeignAssets::assets_by_location(&Location::parent()),
172
			Some((1, AssetStatus::FrozenXcmDepositAllowed))
173
		);
174

            
175
		// Should not be able to freeze an asset already frozen
176
1
		assert_noop!(
177
1
			EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::root(), 1, true),
178
1
			Error::<Test>::AssetAlreadyFrozen
179
		);
180

            
181
		// Unfreeze should work
182
1
		assert_ok!(EvmForeignAssets::unfreeze_foreign_asset(
183
1
			RuntimeOrigin::root(),
184
			1
185
		),);
186
1
		assert_eq!(
187
1
			EvmForeignAssets::assets_by_location(&Location::parent()),
188
			Some((1, AssetStatus::Active))
189
		);
190
1
	});
191
1
}
192

            
193
#[test]
194
1
fn test_asset_exists_error() {
195
1
	ExtBuilder::default().build().execute_with(|| {
196
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
197
1
			RuntimeOrigin::root(),
198
			1,
199
1
			Location::parent(),
200
			18,
201
1
			encode_ticker("MTT"),
202
1
			encode_token_name("Mytoken"),
203
		));
204
1
		assert_eq!(
205
1
			EvmForeignAssets::assets_by_id(1).unwrap(),
206
1
			Location::parent()
207
		);
208
1
		assert_noop!(
209
1
			EvmForeignAssets::create_foreign_asset(
210
1
				RuntimeOrigin::root(),
211
				1,
212
1
				Location::parent(),
213
				18,
214
1
				encode_ticker("MTT"),
215
1
				encode_token_name("Mytoken"),
216
			),
217
1
			Error::<Test>::AssetAlreadyExists
218
		);
219
1
	});
220
1
}
221

            
222
#[test]
223
1
fn test_regular_user_cannot_call_extrinsics() {
224
1
	ExtBuilder::default().build().execute_with(|| {
225
1
		assert_noop!(
226
1
			EvmForeignAssets::create_foreign_asset(
227
1
				RuntimeOrigin::signed(Bob.into()),
228
				1,
229
1
				Location::parent(),
230
				18,
231
1
				encode_ticker("MTT"),
232
1
				encode_token_name("Mytoken"),
233
			),
234
1
			sp_runtime::DispatchError::BadOrigin
235
		);
236

            
237
1
		assert_noop!(
238
1
			EvmForeignAssets::change_xcm_location(
239
1
				RuntimeOrigin::signed(Bob.into()),
240
				1,
241
1
				Location::parent()
242
			),
243
1
			sp_runtime::DispatchError::BadOrigin
244
		);
245
1
	});
246
1
}
247

            
248
#[test]
249
1
fn test_root_can_change_foreign_asset_for_asset_id() {
250
1
	ExtBuilder::default().build().execute_with(|| {
251
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
252
1
			RuntimeOrigin::root(),
253
			1,
254
1
			Location::parent(),
255
			18,
256
1
			encode_ticker("MTT"),
257
1
			encode_token_name("Mytoken"),
258
		));
259

            
260
1
		assert_ok!(EvmForeignAssets::change_xcm_location(
261
1
			RuntimeOrigin::root(),
262
			1,
263
1
			Location::here()
264
		));
265

            
266
		// New associations are stablished
267
1
		assert_eq!(EvmForeignAssets::assets_by_id(1).unwrap(), Location::here());
268
1
		assert_eq!(
269
1
			EvmForeignAssets::assets_by_location(Location::here()).unwrap(),
270
			(1, AssetStatus::Active),
271
		);
272

            
273
		// Old ones are deleted
274
1
		assert!(EvmForeignAssets::assets_by_location(Location::parent()).is_none());
275

            
276
1
		expect_events(vec![
277
1
			crate::Event::ForeignAssetCreated {
278
1
				contract_address: H160([
279
1
					255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
280
1
				]),
281
1
				asset_id: 1,
282
1
				xcm_location: Location::parent(),
283
1
				deposit: None,
284
1
			},
285
1
			crate::Event::ForeignAssetXcmLocationChanged {
286
1
				asset_id: 1,
287
1
				previous_xcm_location: Location::parent(),
288
1
				new_xcm_location: Location::here(),
289
1
			},
290
		])
291
1
	});
292
1
}
293

            
294
#[test]
295
1
fn test_asset_id_non_existent_error() {
296
1
	ExtBuilder::default().build().execute_with(|| {
297
1
		assert_noop!(
298
1
			EvmForeignAssets::change_xcm_location(RuntimeOrigin::root(), 1, Location::parent()),
299
1
			Error::<Test>::AssetDoesNotExist
300
		);
301
1
	});
302
1
}
303

            
304
#[test]
305
1
fn test_location_already_exist_error() {
306
1
	ExtBuilder::default().build().execute_with(|| {
307
		// Setup: create a first foreign asset taht we will try to override
308
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
309
1
			RuntimeOrigin::root(),
310
			1,
311
1
			Location::parent(),
312
			18,
313
1
			encode_ticker("MTT"),
314
1
			encode_token_name("Mytoken"),
315
		));
316

            
317
1
		assert_noop!(
318
1
			EvmForeignAssets::create_foreign_asset(
319
1
				RuntimeOrigin::root(),
320
				2,
321
1
				Location::parent(),
322
				18,
323
1
				encode_ticker("MTT"),
324
1
				encode_token_name("Mytoken"),
325
			),
326
1
			Error::<Test>::LocationAlreadyExists
327
		);
328

            
329
		// Setup: create a second foreign asset that will try to override the first one
330
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
331
1
			RuntimeOrigin::root(),
332
			2,
333
1
			Location::new(2, *&[]),
334
			18,
335
1
			encode_ticker("MTT"),
336
1
			encode_token_name("Mytoken"),
337
		));
338

            
339
1
		assert_noop!(
340
1
			EvmForeignAssets::change_xcm_location(RuntimeOrigin::root(), 2, Location::parent()),
341
1
			Error::<Test>::LocationAlreadyExists
342
		);
343
1
	});
344
1
}
345

            
346
#[test]
347
1
fn test_governance_can_change_any_asset_location() {
348
1
	ExtBuilder::default().build().execute_with(|| {
349
1
		let deposit = ForeignAssetCreationDeposit::get();
350

            
351
1
		Balances::make_free_balance_be(&PARA_C, deposit + 10);
352

            
353
1
		let asset_location: Location = (Parent, Parachain(3), PalletInstance(22)).into();
354
1
		let asset_id = 5;
355

            
356
		// create foreign asset using para c
357
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
358
1
			RuntimeOrigin::signed(PARA_C),
359
1
			asset_id,
360
1
			asset_location.clone(),
361
			10,
362
1
			encode_ticker("PARC"),
363
1
			encode_token_name("Parachain C Token"),
364
		));
365

            
366
1
		assert_eq!(Balances::free_balance(&PARA_C), 10);
367

            
368
1
		assert_eq!(
369
1
			EvmForeignAssets::assets_by_id(asset_id),
370
1
			Some(asset_location.clone())
371
		);
372
1
		assert_eq!(
373
1
			EvmForeignAssets::assets_by_location(asset_location),
374
1
			Some((asset_id, AssetStatus::Active)),
375
		);
376

            
377
		// This asset doesn't belong to PARA A, so it should not be able to change the location
378
1
		assert_noop!(
379
1
			EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::signed(PARA_A), asset_id, true),
380
1
			Error::<Test>::LocationOutsideOfOrigin,
381
		);
382

            
383
1
		let new_asset_location: Location = (Parent, Parachain(1), PalletInstance(1)).into();
384

            
385
		// Also PARA A cannot change the location
386
1
		assert_noop!(
387
1
			EvmForeignAssets::change_xcm_location(
388
1
				RuntimeOrigin::signed(PARA_A),
389
1
				asset_id,
390
1
				new_asset_location.clone(),
391
			),
392
1
			Error::<Test>::LocationOutsideOfOrigin,
393
		);
394

            
395
		// Change location using root, now PARA A can control this asset
396
1
		assert_ok!(EvmForeignAssets::change_xcm_location(
397
1
			RuntimeOrigin::root(),
398
1
			asset_id,
399
1
			new_asset_location.clone(),
400
		));
401

            
402
1
		assert_eq!(
403
1
			EvmForeignAssets::assets_by_id(asset_id),
404
1
			Some(new_asset_location.clone())
405
		);
406
1
		assert_eq!(
407
1
			EvmForeignAssets::assets_by_location(new_asset_location),
408
1
			Some((asset_id, AssetStatus::Active)),
409
		);
410

            
411
		// Freeze will not work since this asset has been moved from PARA C to PARA A
412
1
		assert_noop!(
413
1
			EvmForeignAssets::freeze_foreign_asset(RuntimeOrigin::signed(PARA_C), asset_id, true),
414
1
			Error::<Test>::LocationOutsideOfOrigin,
415
		);
416

            
417
		// But if we try using PARA A, it should work
418
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
419
1
			RuntimeOrigin::signed(PARA_A),
420
1
			asset_id,
421
			true
422
		));
423
1
	});
424
1
}
425

            
426
#[test]
427
1
fn xcm_deposit_succeeds_on_frozen_xcm_deposit_allowed_asset() {
428
1
	ExtBuilder::default().build().execute_with(|| {
429
1
		let asset_location = Location::parent();
430
1
		let beneficiary_location = Location::new(
431
			0,
432
1
			[AccountKey20 {
433
1
				network: None,
434
1
				key: [1u8; 20],
435
1
			}],
436
		);
437
1
		let beneficiary_h160 = H160([1u8; 20]);
438

            
439
		// Create foreign asset (deploys the ERC20 contract)
440
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
441
1
			RuntimeOrigin::root(),
442
			1,
443
1
			asset_location.clone(),
444
			18,
445
1
			encode_ticker("MTT"),
446
1
			encode_token_name("Mytoken"),
447
		));
448

            
449
1
		let xcm_asset = xcm::latest::Asset {
450
1
			id: xcm::latest::AssetId(asset_location.clone()),
451
1
			fun: Fungibility::Fungible(100),
452
1
		};
453

            
454
		// Deposit succeeds on an active (unpaused) asset — mints directly via EVM
455
1
		assert_ok!(
456
1
			<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
457
1
				&xcm_asset,
458
1
				&beneficiary_location,
459
1
				None,
460
			)
461
		);
462
		// No pending deposit for active asset
463
1
		assert_eq!(
464
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
465
			None
466
		);
467

            
468
		// Freeze with allow_xcm_deposit = true
469
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
470
1
			RuntimeOrigin::root(),
471
			1,
472
			true,
473
		));
474
1
		assert_eq!(
475
1
			EvmForeignAssets::assets_by_location(&asset_location),
476
			Some((1, AssetStatus::FrozenXcmDepositAllowed))
477
		);
478

            
479
		// Deposit succeeds but goes to PendingDeposits storage (not EVM mint)
480
1
		assert_ok!(
481
1
			<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
482
1
				&xcm_asset,
483
1
				&beneficiary_location,
484
1
				None,
485
			)
486
		);
487
1
		assert_eq!(
488
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
489
1
			Some(U256::from(100))
490
		);
491
1
	});
492
1
}
493

            
494
#[test]
495
1
fn pending_deposits_accumulate() {
496
1
	ExtBuilder::default().build().execute_with(|| {
497
1
		let asset_location = Location::parent();
498
1
		let beneficiary_location = Location::new(
499
			0,
500
1
			[AccountKey20 {
501
1
				network: None,
502
1
				key: [1u8; 20],
503
1
			}],
504
		);
505
1
		let beneficiary_h160 = H160([1u8; 20]);
506

            
507
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
508
1
			RuntimeOrigin::root(),
509
			1,
510
1
			asset_location.clone(),
511
			18,
512
1
			encode_ticker("MTT"),
513
1
			encode_token_name("Mytoken"),
514
		));
515

            
516
		// Freeze with allow_xcm_deposit = true
517
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
518
1
			RuntimeOrigin::root(),
519
			1,
520
			true,
521
		));
522

            
523
1
		let xcm_asset_100 = xcm::latest::Asset {
524
1
			id: xcm::latest::AssetId(asset_location.clone()),
525
1
			fun: Fungibility::Fungible(100),
526
1
		};
527
1
		let xcm_asset_200 = xcm::latest::Asset {
528
1
			id: xcm::latest::AssetId(asset_location.clone()),
529
1
			fun: Fungibility::Fungible(200),
530
1
		};
531

            
532
		// First deposit
533
1
		assert_ok!(
534
1
			<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
535
1
				&xcm_asset_100,
536
1
				&beneficiary_location,
537
1
				None,
538
			)
539
		);
540
1
		assert_eq!(
541
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
542
1
			Some(U256::from(100))
543
		);
544

            
545
		// Second deposit accumulates
546
1
		assert_ok!(
547
1
			<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
548
1
				&xcm_asset_200,
549
1
				&beneficiary_location,
550
1
				None,
551
			)
552
		);
553
1
		assert_eq!(
554
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
555
1
			Some(U256::from(300))
556
		);
557
1
	});
558
1
}
559

            
560
#[test]
561
1
fn pending_deposits_overflow() {
562
1
	ExtBuilder::default().build().execute_with(|| {
563
1
		let asset_location = Location::parent();
564
1
		let beneficiary_location = Location::new(
565
			0,
566
1
			[AccountKey20 {
567
1
				network: None,
568
1
				key: [1u8; 20],
569
1
			}],
570
		);
571
1
		let beneficiary_h160 = H160([1u8; 20]);
572

            
573
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
574
1
			RuntimeOrigin::root(),
575
			1,
576
1
			asset_location.clone(),
577
			18,
578
1
			encode_ticker("MTT"),
579
1
			encode_token_name("Mytoken"),
580
		));
581

            
582
		// Freeze with allow_xcm_deposit = true
583
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
584
1
			RuntimeOrigin::root(),
585
			1,
586
			true,
587
		));
588

            
589
		// Seed pending deposits to U256::MAX directly
590
1
		crate::PendingDeposits::<Test>::insert(1, beneficiary_h160, U256::MAX);
591

            
592
		// Any further deposit should overflow
593
1
		let xcm_asset_one = xcm::latest::Asset {
594
1
			id: xcm::latest::AssetId(asset_location.clone()),
595
1
			fun: Fungibility::Fungible(1),
596
1
		};
597
1
		let result = <EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
598
1
			&xcm_asset_one,
599
1
			&beneficiary_location,
600
1
			None,
601
		);
602
1
		assert_eq!(result, Err(xcm::latest::Error::Overflow.into()));
603

            
604
		// Pending deposit unchanged
605
1
		assert_eq!(
606
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
607
			Some(U256::MAX)
608
		);
609
1
	});
610
1
}
611

            
612
#[test]
613
1
fn claim_pending_deposit_success() {
614
1
	ExtBuilder::default().build().execute_with(|| {
615
1
		let asset_location = Location::parent();
616
1
		let beneficiary_location = Location::new(
617
			0,
618
1
			[AccountKey20 {
619
1
				network: None,
620
1
				key: [1u8; 20],
621
1
			}],
622
		);
623
1
		let beneficiary_h160 = H160([1u8; 20]);
624

            
625
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
626
1
			RuntimeOrigin::root(),
627
			1,
628
1
			asset_location.clone(),
629
			18,
630
1
			encode_ticker("MTT"),
631
1
			encode_token_name("Mytoken"),
632
		));
633

            
634
		// Freeze with allow_xcm_deposit = true
635
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
636
1
			RuntimeOrigin::root(),
637
			1,
638
			true,
639
		));
640

            
641
1
		let xcm_asset = xcm::latest::Asset {
642
1
			id: xcm::latest::AssetId(asset_location.clone()),
643
1
			fun: Fungibility::Fungible(500),
644
1
		};
645

            
646
		// Deposit goes to pending
647
1
		assert_ok!(
648
1
			<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
649
1
				&xcm_asset,
650
1
				&beneficiary_location,
651
1
				None,
652
			)
653
		);
654
1
		assert_eq!(
655
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
656
1
			Some(U256::from(500))
657
		);
658

            
659
		// Unfreeze the asset
660
1
		assert_ok!(EvmForeignAssets::unfreeze_foreign_asset(
661
1
			RuntimeOrigin::root(),
662
			1
663
		));
664

            
665
		// Verify balance is zero before claim
666
1
		let balance_before =
667
1
			crate::evm::EvmCaller::<Test>::erc20_balance_of(1, beneficiary_h160).unwrap();
668
1
		assert_eq!(balance_before, U256::zero());
669

            
670
		// Claim the pending deposit (any signed origin can call this)
671
1
		assert_ok!(EvmForeignAssets::claim_pending_deposit(
672
1
			RuntimeOrigin::signed(PARA_A),
673
			1,
674
1
			beneficiary_h160,
675
		));
676

            
677
		// Pending deposit should be cleared
678
1
		assert_eq!(
679
1
			EvmForeignAssets::pending_deposits(1, beneficiary_h160),
680
			None
681
		);
682

            
683
		// Verify beneficiary received the tokens
684
1
		let balance_after =
685
1
			crate::evm::EvmCaller::<Test>::erc20_balance_of(1, beneficiary_h160).unwrap();
686
1
		assert_eq!(balance_after, U256::from(500));
687
1
	});
688
1
}
689

            
690
#[test]
691
1
fn claim_pending_deposit_fails_when_asset_frozen() {
692
1
	ExtBuilder::default().build().execute_with(|| {
693
1
		let asset_location = Location::parent();
694
1
		let beneficiary_h160 = H160([1u8; 20]);
695
1
		let beneficiary_location = Location::new(
696
			0,
697
1
			[AccountKey20 {
698
1
				network: None,
699
1
				key: [1u8; 20],
700
1
			}],
701
		);
702

            
703
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
704
1
			RuntimeOrigin::root(),
705
			1,
706
1
			asset_location.clone(),
707
			18,
708
1
			encode_ticker("MTT"),
709
1
			encode_token_name("Mytoken"),
710
		));
711

            
712
		// Freeze
713
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
714
1
			RuntimeOrigin::root(),
715
			1,
716
			true,
717
		));
718

            
719
1
		let xcm_asset = xcm::latest::Asset {
720
1
			id: xcm::latest::AssetId(asset_location.clone()),
721
1
			fun: Fungibility::Fungible(100),
722
1
		};
723

            
724
		// Deposit goes to pending
725
1
		assert_ok!(
726
1
			<EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
727
1
				&xcm_asset,
728
1
				&beneficiary_location,
729
1
				None,
730
			)
731
		);
732

            
733
		// Claim fails because asset is still frozen
734
1
		assert_noop!(
735
1
			EvmForeignAssets::claim_pending_deposit(
736
1
				RuntimeOrigin::signed(PARA_A),
737
				1,
738
1
				beneficiary_h160,
739
			),
740
1
			Error::<Test>::AssetNotActive
741
		);
742
1
	});
743
1
}
744

            
745
#[test]
746
1
fn claim_pending_deposit_fails_when_no_pending() {
747
1
	ExtBuilder::default().build().execute_with(|| {
748
1
		let asset_location = Location::parent();
749
1
		let beneficiary_h160 = H160([1u8; 20]);
750

            
751
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
752
1
			RuntimeOrigin::root(),
753
			1,
754
1
			asset_location.clone(),
755
			18,
756
1
			encode_ticker("MTT"),
757
1
			encode_token_name("Mytoken"),
758
		));
759

            
760
		// No pending deposit exists — claim fails
761
1
		assert_noop!(
762
1
			EvmForeignAssets::claim_pending_deposit(
763
1
				RuntimeOrigin::signed(PARA_A),
764
				1,
765
1
				beneficiary_h160,
766
			),
767
1
			Error::<Test>::NoPendingDeposit
768
		);
769
1
	});
770
1
}
771

            
772
#[test]
773
1
fn xcm_deposit_blocked_on_frozen_xcm_deposit_forbidden_asset() {
774
	// Verifies that deposit_asset correctly rejects deposits when the asset
775
	// status is FrozenXcmDepositForbidden (blocked at the pallet level before
776
	// reaching the EVM).
777
1
	ExtBuilder::default().build().execute_with(|| {
778
1
		let asset_location = Location::parent();
779
1
		let beneficiary_location = Location::new(
780
			0,
781
1
			[AccountKey20 {
782
1
				network: None,
783
1
				key: [1u8; 20],
784
1
			}],
785
		);
786

            
787
		// Create foreign asset
788
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
789
1
			RuntimeOrigin::root(),
790
			1,
791
1
			asset_location.clone(),
792
			18,
793
1
			encode_ticker("MTT"),
794
1
			encode_token_name("Mytoken"),
795
		));
796

            
797
1
		let xcm_asset = xcm::latest::Asset {
798
1
			id: xcm::latest::AssetId(asset_location.clone()),
799
1
			fun: Fungibility::Fungible(100),
800
1
		};
801

            
802
		// Freeze with allow_xcm_deposit = false
803
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
804
1
			RuntimeOrigin::root(),
805
			1,
806
			false,
807
		));
808
1
		assert_eq!(
809
1
			EvmForeignAssets::assets_by_location(&asset_location),
810
			Some((1, AssetStatus::FrozenXcmDepositForbidden))
811
		);
812

            
813
		// Deposit is rejected at the pallet level (before EVM call)
814
1
		let result = <EvmForeignAssets as xcm_executor::traits::TransactAsset>::deposit_asset(
815
1
			&xcm_asset,
816
1
			&beneficiary_location,
817
1
			None,
818
		);
819
1
		assert_eq!(
820
			result,
821
			Err(XcmError::FailedToTransactAsset(
822
				"asset is frozen and XCM deposits are forbidden"
823
			)),
824
		);
825
1
	});
826
1
}