1
// Copyright 2019-2022 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
#![cfg_attr(not(feature = "std"), no_std)]
18

            
19
use account::SYSTEM_ACCOUNT_SIZE;
20
use core::fmt::Display;
21
use fp_evm::{ExitError, PrecompileHandle};
22
use frame_support::traits::fungibles::Inspect;
23
use frame_support::traits::fungibles::{
24
	approvals::Inspect as ApprovalInspect, metadata::Inspect as MetadataInspect,
25
	roles::Inspect as RolesInspect,
26
};
27
use frame_support::traits::{Get, OriginTrait};
28
use frame_support::{
29
	dispatch::{GetDispatchInfo, PostDispatchInfo},
30
	sp_runtime::traits::StaticLookup,
31
};
32
use moonkit_xcm_primitives::AccountIdAssetIdConversion;
33
use pallet_evm::AddressMapping;
34
use precompile_utils::prelude::*;
35
use sp_runtime::traits::{Bounded, Dispatchable};
36
use sp_std::vec::Vec;
37

            
38
use sp_core::{MaxEncodedLen, H160, H256, U256};
39
use sp_std::{
40
	convert::{TryFrom, TryInto},
41
	marker::PhantomData,
42
};
43

            
44
mod eip2612;
45
use eip2612::Eip2612;
46

            
47
#[cfg(test)]
48
mod mock;
49
#[cfg(test)]
50
mod tests;
51

            
52
/// Solidity selector of the Transfer log, which is the Keccak of the Log signature.
53
pub const SELECTOR_LOG_TRANSFER: [u8; 32] = keccak256!("Transfer(address,address,uint256)");
54

            
55
/// Solidity selector of the Approval log, which is the Keccak of the Log signature.
56
pub const SELECTOR_LOG_APPROVAL: [u8; 32] = keccak256!("Approval(address,address,uint256)");
57

            
58
/// Alias for the Balance type for the provided Runtime and Instance.
59
pub type BalanceOf<Runtime, Instance = ()> = <Runtime as pallet_assets::Config<Instance>>::Balance;
60

            
61
/// Alias for the Asset Id type for the provided Runtime and Instance.
62
pub type AssetIdOf<Runtime, Instance = ()> = <Runtime as pallet_assets::Config<Instance>>::AssetId;
63

            
64
/// The following distribution has been decided for the precompiles
65
/// 0-1023: Ethereum Mainnet Precompiles
66
/// 1024-2047 Precompiles that are not in Ethereum Mainnet but are neither Moonbeam specific
67
/// 2048-4095 Moonbeam specific precompiles
68
/// Asset precompiles can only fall between
69
/// 	0xFFFFFFFF00000000000000000000000000000000 - 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
70
/// The precompile for AssetId X, where X is a u128 (i.e.16 bytes), if 0XFFFFFFFF + Bytes(AssetId)
71
/// In order to route the address to Erc20AssetsPrecompile<R>, we first check whether the AssetId
72
/// exists in pallet-assets
73
/// We cannot do this right now, so instead we check whether the total supply is zero. If so, we
74
/// do not route to the precompiles
75

            
76
/// This means that every address that starts with 0xFFFFFFFF will go through an additional db read,
77
/// but the probability for this to happen is 2^-32 for random addresses
78
pub struct Erc20AssetsPrecompileSet<Runtime, Instance: 'static = ()>(
79
	PhantomData<(Runtime, Instance)>,
80
);
81

            
82
impl<R, V> Clone for Erc20AssetsPrecompileSet<R, V> {
83
	fn clone(&self) -> Self {
84
		Self(PhantomData)
85
	}
86
}
87

            
88
impl<R, V> Default for Erc20AssetsPrecompileSet<R, V> {
89
198576
	fn default() -> Self {
90
198576
		Self(PhantomData)
91
198576
	}
92
}
93

            
94
impl<Runtime, Instance> Erc20AssetsPrecompileSet<Runtime, Instance> {
95
	pub fn new() -> Self {
96
		Self(PhantomData)
97
	}
98
}
99

            
100
589
#[precompile_utils::precompile]
101
#[precompile::precompile_set]
102
#[precompile::test_concrete_types(mock::Runtime, pallet_assets::Instance1)]
103
impl<Runtime, Instance> Erc20AssetsPrecompileSet<Runtime, Instance>
104
where
105
	Instance: eip2612::InstanceToPrefix + 'static,
106
	Runtime: pallet_assets::Config<Instance> + pallet_evm::Config + frame_system::Config,
107
	Runtime::RuntimeCall: Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
108
	Runtime::RuntimeCall: From<pallet_assets::Call<Runtime, Instance>>,
109
	<Runtime::RuntimeCall as Dispatchable>::RuntimeOrigin: From<Option<Runtime::AccountId>>,
110
	BalanceOf<Runtime, Instance>: TryFrom<U256> + Into<U256> + solidity::Codec,
111
	Runtime: AccountIdAssetIdConversion<Runtime::AccountId, AssetIdOf<Runtime, Instance>>,
112
	<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin: OriginTrait,
113
	AssetIdOf<Runtime, Instance>: Display,
114
	Runtime::AccountId: Into<H160>,
115
{
116
	/// PrecompileSet discriminant. Allows to knows if the address maps to an asset id,
117
	/// and if this is the case which one.
118
	#[precompile::discriminant]
119
266
	fn discriminant(address: H160, gas: u64) -> DiscriminantResult<AssetIdOf<Runtime, Instance>> {
120
266
		let extra_cost = RuntimeHelper::<Runtime>::db_read_gas_cost();
121
266
		if gas < extra_cost {
122
			return DiscriminantResult::OutOfGas;
123
266
		}
124
266

            
125
266
		let account_id = Runtime::AddressMapping::into_account_id(address);
126
266
		let asset_id = match Runtime::account_to_asset_id(account_id) {
127
266
			Some((_, asset_id)) => asset_id,
128
			None => return DiscriminantResult::None(extra_cost),
129
		};
130

            
131
266
		if pallet_assets::Pallet::<Runtime, Instance>::maybe_total_supply(asset_id.clone())
132
266
			.is_some()
133
		{
134
202
			DiscriminantResult::Some(asset_id, extra_cost)
135
		} else {
136
64
			DiscriminantResult::None(extra_cost)
137
		}
138
266
	}
139

            
140
	#[precompile::public("totalSupply()")]
141
	#[precompile::view]
142
4
	fn total_supply(
143
4
		asset_id: AssetIdOf<Runtime, Instance>,
144
4
		handle: &mut impl PrecompileHandle,
145
4
	) -> EvmResult<U256> {
146
4
		// Storage item: Asset:
147
4
		// Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15)
148
4
		handle.record_db_read::<Runtime>(175)?;
149

            
150
4
		Ok(pallet_assets::Pallet::<Runtime, Instance>::total_issuance(asset_id).into())
151
4
	}
152

            
153
	#[precompile::public("balanceOf(address)")]
154
	#[precompile::view]
155
17
	fn balance_of(
156
17
		asset_id: AssetIdOf<Runtime, Instance>,
157
17
		handle: &mut impl PrecompileHandle,
158
17
		who: Address,
159
17
	) -> EvmResult<U256> {
160
17
		// Storage item: Account:
161
17
		// Blake2_128(16) + AssetId(16) + Blake2_128(16) + AccountId(20) + AssetAccount(19 + Extra)
162
17
		handle.record_db_read::<Runtime>(
163
17
			87 + <Runtime as pallet_assets::Config<Instance>>::Extra::max_encoded_len(),
164
17
		)?;
165

            
166
17
		let who: H160 = who.into();
167
17

            
168
17
		// Fetch info.
169
17
		let amount: U256 = {
170
17
			let who: Runtime::AccountId = Runtime::AddressMapping::into_account_id(who);
171
17
			pallet_assets::Pallet::<Runtime, Instance>::balance(asset_id, &who).into()
172
17
		};
173
17

            
174
17
		// Build output.
175
17
		Ok(amount)
176
17
	}
177

            
178
	#[precompile::public("allowance(address,address)")]
179
	#[precompile::view]
180
8
	fn allowance(
181
8
		asset_id: AssetIdOf<Runtime, Instance>,
182
8
		handle: &mut impl PrecompileHandle,
183
8
		owner: Address,
184
8
		spender: Address,
185
8
	) -> EvmResult<U256> {
186
8
		// Storage item: Approvals:
187
8
		// Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(20)) + Approval(32)
188
8
		handle.record_db_read::<Runtime>(136)?;
189

            
190
8
		let owner: H160 = owner.into();
191
8
		let spender: H160 = spender.into();
192
8

            
193
8
		// Fetch info.
194
8
		let amount: U256 = {
195
8
			let owner: Runtime::AccountId = Runtime::AddressMapping::into_account_id(owner);
196
8
			let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
197
8

            
198
8
			// Fetch info.
199
8
			pallet_assets::Pallet::<Runtime, Instance>::allowance(asset_id, &owner, &spender).into()
200
8
		};
201
8

            
202
8
		// Build output.
203
8
		Ok(amount)
204
8
	}
205

            
206
	#[precompile::public("approve(address,uint256)")]
207
12
	fn approve(
208
12
		asset_id: AssetIdOf<Runtime, Instance>,
209
12
		handle: &mut impl PrecompileHandle,
210
12
		spender: Address,
211
12
		value: U256,
212
12
	) -> EvmResult<bool> {
213
12
		handle.record_log_costs_manual(3, 32)?;
214

            
215
12
		let spender: H160 = spender.into();
216
12

            
217
12
		Self::approve_inner(asset_id, handle, handle.context().caller, spender, value)?;
218

            
219
12
		log3(
220
12
			handle.context().address,
221
12
			SELECTOR_LOG_APPROVAL,
222
12
			handle.context().caller,
223
12
			spender,
224
12
			solidity::encode_event_data(value),
225
12
		)
226
12
		.record(handle)?;
227

            
228
		// Build output.
229
12
		Ok(true)
230
12
	}
231

            
232
15
	fn approve_inner(
233
15
		asset_id: AssetIdOf<Runtime, Instance>,
234
15
		handle: &mut impl PrecompileHandle,
235
15
		owner: H160,
236
15
		spender: H160,
237
15
		value: U256,
238
15
	) -> EvmResult {
239
15
		let owner = Runtime::AddressMapping::into_account_id(owner);
240
15
		let spender: Runtime::AccountId = Runtime::AddressMapping::into_account_id(spender);
241
15
		// Amount saturate if too high.
242
15
		let amount: BalanceOf<Runtime, Instance> =
243
15
			value.try_into().unwrap_or_else(|_| Bounded::max_value());
244
15

            
245
15
		// Storage item: Approvals:
246
15
		// Blake2_128(16) + AssetId(16) + (2 * Blake2_128(16) + AccountId(20)) + Approval(32)
247
15
		handle.record_db_read::<Runtime>(136)?;
248

            
249
		// If previous approval exists, we need to clean it
250
15
		if pallet_assets::Pallet::<Runtime, Instance>::allowance(asset_id.clone(), &owner, &spender)
251
15
			!= 0u32.into()
252
		{
253
3
			RuntimeHelper::<Runtime>::try_dispatch(
254
3
				handle,
255
3
				Some(owner.clone()).into(),
256
3
				pallet_assets::Call::<Runtime, Instance>::cancel_approval {
257
3
					id: asset_id.clone().into(),
258
3
					delegate: Runtime::Lookup::unlookup(spender.clone()),
259
3
				},
260
3
				0,
261
3
			)?;
262
12
		}
263
		// Dispatch call (if enough gas).
264
15
		RuntimeHelper::<Runtime>::try_dispatch(
265
15
			handle,
266
15
			Some(owner).into(),
267
15
			pallet_assets::Call::<Runtime, Instance>::approve_transfer {
268
15
				id: asset_id.into(),
269
15
				delegate: Runtime::Lookup::unlookup(spender),
270
15
				amount,
271
15
			},
272
15
			0,
273
15
		)?;
274

            
275
15
		Ok(())
276
15
	}
277

            
278
	#[precompile::public("transfer(address,uint256)")]
279
5
	fn transfer(
280
5
		asset_id: AssetIdOf<Runtime, Instance>,
281
5
		handle: &mut impl PrecompileHandle,
282
5
		to: Address,
283
5
		value: U256,
284
5
	) -> EvmResult<bool> {
285
5
		handle.record_log_costs_manual(3, 32)?;
286

            
287
5
		let to: H160 = to.into();
288
5
		let value = Self::u256_to_amount(value).in_field("value")?;
289

            
290
		// Build call with origin.
291
		{
292
4
			let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
293
4
			let to = Runtime::AddressMapping::into_account_id(to);
294
4

            
295
4
			// Dispatch call (if enough gas).
296
4
			RuntimeHelper::<Runtime>::try_dispatch(
297
4
				handle,
298
4
				Some(origin).into(),
299
4
				pallet_assets::Call::<Runtime, Instance>::transfer {
300
4
					id: asset_id.into(),
301
4
					target: Runtime::Lookup::unlookup(to),
302
4
					amount: value,
303
4
				},
304
4
				SYSTEM_ACCOUNT_SIZE,
305
4
			)?;
306
		}
307

            
308
3
		log3(
309
3
			handle.context().address,
310
3
			SELECTOR_LOG_TRANSFER,
311
3
			handle.context().caller,
312
3
			to,
313
3
			solidity::encode_event_data(value),
314
3
		)
315
3
		.record(handle)?;
316

            
317
3
		Ok(true)
318
5
	}
319

            
320
	#[precompile::public("transferFrom(address,address,uint256)")]
321
7
	fn transfer_from(
322
7
		asset_id: AssetIdOf<Runtime, Instance>,
323
7
		handle: &mut impl PrecompileHandle,
324
7
		from: Address,
325
7
		to: Address,
326
7
		value: U256,
327
7
	) -> EvmResult<bool> {
328
7
		handle.record_log_costs_manual(3, 32)?;
329

            
330
7
		let from: H160 = from.into();
331
7
		let to: H160 = to.into();
332
7
		let value = Self::u256_to_amount(value).in_field("value")?;
333

            
334
		{
335
6
			let caller: Runtime::AccountId =
336
6
				Runtime::AddressMapping::into_account_id(handle.context().caller);
337
6
			let from: Runtime::AccountId = Runtime::AddressMapping::into_account_id(from.clone());
338
6
			let to: Runtime::AccountId = Runtime::AddressMapping::into_account_id(to);
339
6

            
340
6
			// If caller is "from", it can spend as much as it wants from its own balance.
341
6
			if caller != from {
342
				// Dispatch call (if enough gas).
343
5
				RuntimeHelper::<Runtime>::try_dispatch(
344
5
					handle,
345
5
					Some(caller).into(),
346
5
					pallet_assets::Call::<Runtime, Instance>::transfer_approved {
347
5
						id: asset_id.into(),
348
5
						owner: Runtime::Lookup::unlookup(from),
349
5
						destination: Runtime::Lookup::unlookup(to),
350
5
						amount: value,
351
5
					},
352
5
					SYSTEM_ACCOUNT_SIZE,
353
5
				)?;
354
			} else {
355
				// Dispatch call (if enough gas).
356
1
				RuntimeHelper::<Runtime>::try_dispatch(
357
1
					handle,
358
1
					Some(from).into(),
359
1
					pallet_assets::Call::<Runtime, Instance>::transfer {
360
1
						id: asset_id.into(),
361
1
						target: Runtime::Lookup::unlookup(to),
362
1
						amount: value,
363
1
					},
364
1
					SYSTEM_ACCOUNT_SIZE,
365
1
				)?;
366
			}
367
		}
368

            
369
4
		log3(
370
4
			handle.context().address,
371
4
			SELECTOR_LOG_TRANSFER,
372
4
			from,
373
4
			to,
374
4
			solidity::encode_event_data(value),
375
4
		)
376
4
		.record(handle)?;
377

            
378
		// Build output.
379
4
		Ok(true)
380
7
	}
381

            
382
	#[precompile::public("name()")]
383
	#[precompile::view]
384
2
	fn name(
385
2
		asset_id: AssetIdOf<Runtime, Instance>,
386
2
		handle: &mut impl PrecompileHandle,
387
2
	) -> EvmResult<UnboundedBytes> {
388
2
		// Storage item: Metadata:
389
2
		// Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit)
390
2
		// + symbol(StringLimit) + decimals(1) + is_frozen(1)]
391
2
		handle.record_db_read::<Runtime>(
392
2
			50 + (2 * <Runtime as pallet_assets::Config<Instance>>::StringLimit::get()) as usize,
393
2
		)?;
394

            
395
2
		let name = pallet_assets::Pallet::<Runtime, Instance>::name(asset_id)
396
2
			.as_slice()
397
2
			.into();
398
2

            
399
2
		Ok(name)
400
2
	}
401

            
402
	#[precompile::public("symbol()")]
403
	#[precompile::view]
404
2
	fn symbol(
405
2
		asset_id: AssetIdOf<Runtime, Instance>,
406
2
		handle: &mut impl PrecompileHandle,
407
2
	) -> EvmResult<UnboundedBytes> {
408
2
		// Storage item: Metadata:
409
2
		// Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit)
410
2
		// + symbol(StringLimit) + decimals(1) + is_frozen(1)]
411
2
		handle.record_db_read::<Runtime>(
412
2
			50 + (2 * <Runtime as pallet_assets::Config<Instance>>::StringLimit::get()) as usize,
413
2
		)?;
414

            
415
2
		let symbol = pallet_assets::Pallet::<Runtime, Instance>::symbol(asset_id)
416
2
			.as_slice()
417
2
			.into();
418
2

            
419
2
		Ok(symbol)
420
2
	}
421

            
422
	#[precompile::public("decimals()")]
423
	#[precompile::view]
424
2
	fn decimals(
425
2
		asset_id: AssetIdOf<Runtime, Instance>,
426
2
		handle: &mut impl PrecompileHandle,
427
2
	) -> EvmResult<u8> {
428
2
		// Storage item: Metadata:
429
2
		// Blake2_128(16) + AssetId(16) + AssetMetadata[deposit(16) + name(StringLimit)
430
2
		// + symbol(StringLimit) + decimals(1) + is_frozen(1)]
431
2
		handle.record_db_read::<Runtime>(
432
2
			50 + (2 * <Runtime as pallet_assets::Config<Instance>>::StringLimit::get()) as usize,
433
2
		)?;
434

            
435
2
		Ok(pallet_assets::Pallet::<Runtime, Instance>::decimals(
436
2
			asset_id,
437
2
		))
438
2
	}
439

            
440
	#[precompile::public("owner()")]
441
	#[precompile::view]
442
1
	fn owner(
443
1
		asset_id: AssetIdOf<Runtime, Instance>,
444
1
		handle: &mut impl PrecompileHandle,
445
1
	) -> EvmResult<Address> {
446
1
		// Storage item: Asset:
447
1
		// Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15)
448
1
		handle.record_db_read::<Runtime>(175)?;
449

            
450
1
		let owner: H160 = pallet_assets::Pallet::<Runtime, Instance>::owner(asset_id)
451
1
			.ok_or(revert("No owner set"))?
452
1
			.into();
453
1

            
454
1
		Ok(Address(owner))
455
1
	}
456

            
457
	#[precompile::public("issuer()")]
458
	#[precompile::view]
459
1
	fn issuer(
460
1
		asset_id: AssetIdOf<Runtime, Instance>,
461
1
		handle: &mut impl PrecompileHandle,
462
1
	) -> EvmResult<Address> {
463
1
		// Storage item: Asset:
464
1
		// Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15)
465
1
		handle.record_db_read::<Runtime>(175)?;
466

            
467
1
		let issuer: H160 = pallet_assets::Pallet::<Runtime, Instance>::issuer(asset_id)
468
1
			.ok_or(revert("No issuer set"))?
469
1
			.into();
470
1

            
471
1
		Ok(Address(issuer))
472
1
	}
473

            
474
	#[precompile::public("admin()")]
475
	#[precompile::view]
476
1
	fn admin(
477
1
		asset_id: AssetIdOf<Runtime, Instance>,
478
1
		handle: &mut impl PrecompileHandle,
479
1
	) -> EvmResult<Address> {
480
1
		// Storage item: Asset:
481
1
		// Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15)
482
1
		handle.record_db_read::<Runtime>(175)?;
483

            
484
1
		let admin: H160 = pallet_assets::Pallet::<Runtime, Instance>::admin(asset_id)
485
1
			.ok_or(revert("No admin set"))?
486
1
			.into();
487
1

            
488
1
		Ok(Address(admin))
489
1
	}
490

            
491
	#[precompile::public("freezer()")]
492
	#[precompile::view]
493
1
	fn freezer(
494
1
		asset_id: AssetIdOf<Runtime, Instance>,
495
1
		handle: &mut impl PrecompileHandle,
496
1
	) -> EvmResult<Address> {
497
1
		// Storage item: Asset:
498
1
		// Blake2_128(16) + AssetId(16) + AssetDetails((4 * AccountId(20)) + (3 * Balance(16)) + 15)
499
1
		handle.record_db_read::<Runtime>(175)?;
500

            
501
1
		let freezer: H160 = pallet_assets::Pallet::<Runtime, Instance>::freezer(asset_id)
502
1
			.ok_or(revert("No freezer set"))?
503
1
			.into();
504
1

            
505
1
		Ok(Address(freezer))
506
1
	}
507

            
508
	#[precompile::public("permit(address,address,uint256,uint256,uint8,bytes32,bytes32)")]
509
6
	fn eip2612_permit(
510
6
		asset_id: AssetIdOf<Runtime, Instance>,
511
6
		handle: &mut impl PrecompileHandle,
512
6
		owner: Address,
513
6
		spender: Address,
514
6
		value: U256,
515
6
		deadline: U256,
516
6
		v: u8,
517
6
		r: H256,
518
6
		s: H256,
519
6
	) -> EvmResult {
520
6
		<Eip2612<Runtime, Instance>>::permit(
521
6
			asset_id, handle, owner, spender, value, deadline, v, r, s,
522
6
		)
523
6
	}
524

            
525
	#[precompile::public("nonces(address)")]
526
	#[precompile::view]
527
10
	fn eip2612_nonces(
528
10
		asset_id: AssetIdOf<Runtime, Instance>,
529
10
		handle: &mut impl PrecompileHandle,
530
10
		owner: Address,
531
10
	) -> EvmResult<U256> {
532
10
		<Eip2612<Runtime, Instance>>::nonces(asset_id, handle, owner)
533
10
	}
534

            
535
	#[precompile::public("DOMAIN_SEPARATOR()")]
536
	#[precompile::view]
537
1
	fn eip2612_domain_separator(
538
1
		asset_id: AssetIdOf<Runtime, Instance>,
539
1
		handle: &mut impl PrecompileHandle,
540
1
	) -> EvmResult<H256> {
541
1
		<Eip2612<Runtime, Instance>>::domain_separator(asset_id, handle)
542
1
	}
543

            
544
12
	fn u256_to_amount(value: U256) -> MayRevert<BalanceOf<Runtime, Instance>> {
545
12
		value
546
12
			.try_into()
547
12
			.map_err(|_| RevertReason::value_is_too_large("balance type").into())
548
12
	}
549
}