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
10
fn encode_ticker(str_: &str) -> BoundedVec<u8, ConstU32<256>> {
26
10
	BoundedVec::try_from(str_.as_bytes().to_vec()).expect("too long")
27
10
}
28

            
29
10
fn encode_token_name(str_: &str) -> BoundedVec<u8, ConstU32<256>> {
30
10
	BoundedVec::try_from(str_.as_bytes().to_vec()).expect("too long")
31
10
}
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
1

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

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

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

            
52
1
		assert_eq!(
53
1
			EvmForeignAssets::assets_by_id(1),
54
1
			Some(asset_location.clone())
55
1
		);
56
1
		assert_eq!(
57
1
			EvmForeignAssets::assets_by_location(asset_location.clone()),
58
1
			Some((1, AssetStatus::Active)),
59
1
		);
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
1

            
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
1
		);
79
1
		assert_eq!(
80
1
			EvmForeignAssets::assets_by_location(&asset_location),
81
1
			Some((1, AssetStatus::Active))
82
1
		);
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
1
		);
89

            
90
		// Freeze should work
91
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
92
1
			RuntimeOrigin::signed(PARA_A),
93
1
			1,
94
1
			true
95
1
		),);
96
1
		assert_eq!(
97
1
			EvmForeignAssets::assets_by_location(&asset_location),
98
1
			Some((1, AssetStatus::FrozenXcmDepositAllowed))
99
1
		);
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
1
		);
106

            
107
		// Unfreeze should work
108
1
		assert_ok!(EvmForeignAssets::unfreeze_foreign_asset(
109
1
			RuntimeOrigin::signed(PARA_A),
110
1
			1
111
1
		),);
112
1
		assert_eq!(
113
1
			EvmForeignAssets::assets_by_location(&asset_location),
114
1
			Some((1, AssetStatus::Active))
115
1
		);
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
1
		// create foreign asset
123
1
		assert_ok!(EvmForeignAssets::create_foreign_asset(
124
1
			RuntimeOrigin::root(),
125
1
			1,
126
1
			Location::parent(),
127
1
			18,
128
1
			encode_ticker("MTT"),
129
1
			encode_token_name("Mytoken"),
130
1
		));
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
1
			Some((1, AssetStatus::Active)),
136
1
		);
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
1

            
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
1
			Some((1, AssetStatus::Active))
156
1
		);
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
1
		);
163

            
164
		// Freeze should work
165
1
		assert_ok!(EvmForeignAssets::freeze_foreign_asset(
166
1
			RuntimeOrigin::root(),
167
1
			1,
168
1
			true
169
1
		),);
170
1
		assert_eq!(
171
1
			EvmForeignAssets::assets_by_location(&Location::parent()),
172
1
			Some((1, AssetStatus::FrozenXcmDepositAllowed))
173
1
		);
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
1
		);
180

            
181
		// Unfreeze should work
182
1
		assert_ok!(EvmForeignAssets::unfreeze_foreign_asset(
183
1
			RuntimeOrigin::root(),
184
1
			1
185
1
		),);
186
1
		assert_eq!(
187
1
			EvmForeignAssets::assets_by_location(&Location::parent()),
188
1
			Some((1, AssetStatus::Active))
189
1
		);
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
			1,
199
1
			Location::parent(),
200
1
			18,
201
1
			encode_ticker("MTT"),
202
1
			encode_token_name("Mytoken"),
203
1
		));
204
1
		assert_eq!(
205
1
			EvmForeignAssets::assets_by_id(1).unwrap(),
206
1
			Location::parent()
207
1
		);
208
1
		assert_noop!(
209
1
			EvmForeignAssets::create_foreign_asset(
210
1
				RuntimeOrigin::root(),
211
1
				1,
212
1
				Location::parent(),
213
1
				18,
214
1
				encode_ticker("MTT"),
215
1
				encode_token_name("Mytoken"),
216
1
			),
217
1
			Error::<Test>::AssetAlreadyExists
218
1
		);
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
				1,
229
1
				Location::parent(),
230
1
				18,
231
1
				encode_ticker("MTT"),
232
1
				encode_token_name("Mytoken"),
233
1
			),
234
1
			sp_runtime::DispatchError::BadOrigin
235
1
		);
236

            
237
1
		assert_noop!(
238
1
			EvmForeignAssets::change_xcm_location(
239
1
				RuntimeOrigin::signed(Bob.into()),
240
1
				1,
241
1
				Location::parent()
242
1
			),
243
1
			sp_runtime::DispatchError::BadOrigin
244
1
		);
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
			1,
254
1
			Location::parent(),
255
1
			18,
256
1
			encode_ticker("MTT"),
257
1
			encode_token_name("Mytoken"),
258
1
		));
259

            
260
1
		assert_ok!(EvmForeignAssets::change_xcm_location(
261
1
			RuntimeOrigin::root(),
262
1
			1,
263
1
			Location::here()
264
1
		));
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
			(1, AssetStatus::Active),
271
1
		);
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
1
		])
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
1
		);
301
1
	});
302
1
}
303

            
304
#[test]
305
1
fn test_location_already_exist_error() {
306
1
	ExtBuilder::default().build().execute_with(|| {
307
1
		// 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
			1,
311
1
			Location::parent(),
312
1
			18,
313
1
			encode_ticker("MTT"),
314
1
			encode_token_name("Mytoken"),
315
1
		));
316

            
317
1
		assert_noop!(
318
1
			EvmForeignAssets::create_foreign_asset(
319
1
				RuntimeOrigin::root(),
320
1
				2,
321
1
				Location::parent(),
322
1
				18,
323
1
				encode_ticker("MTT"),
324
1
				encode_token_name("Mytoken"),
325
1
			),
326
1
			Error::<Test>::LocationAlreadyExists
327
1
		);
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
1
			2,
333
1
			Location::new(2, *&[]),
334
1
			18,
335
1
			encode_ticker("MTT"),
336
1
			encode_token_name("Mytoken"),
337
1
		));
338

            
339
1
		assert_noop!(
340
1
			EvmForeignAssets::change_xcm_location(RuntimeOrigin::root(), 2, Location::parent()),
341
1
			Error::<Test>::LocationAlreadyExists
342
1
		);
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
1

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

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

            
356
1
		// 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
1
			10,
362
1
			encode_ticker("PARC"),
363
1
			encode_token_name("Parachain C Token"),
364
1
		));
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
1
		);
372
1
		assert_eq!(
373
1
			EvmForeignAssets::assets_by_location(asset_location),
374
1
			Some((asset_id, AssetStatus::Active)),
375
1
		);
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
1
		);
382

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

            
385
1
		// 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
1
			),
392
1
			Error::<Test>::LocationOutsideOfOrigin,
393
1
		);
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
1
		));
401

            
402
1
		assert_eq!(
403
1
			EvmForeignAssets::assets_by_id(asset_id),
404
1
			Some(new_asset_location.clone())
405
1
		);
406
1
		assert_eq!(
407
1
			EvmForeignAssets::assets_by_location(new_asset_location),
408
1
			Some((asset_id, AssetStatus::Active)),
409
1
		);
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
1
		);
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
1
			true
422
1
		));
423
1
	});
424
1
}