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
//! Precompile to xtokens runtime methods via the EVM
18

            
19
#![cfg_attr(not(feature = "std"), no_std)]
20

            
21
use account::SYSTEM_ACCOUNT_SIZE;
22
use fp_evm::PrecompileHandle;
23
use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
24
use pallet_evm::AddressMapping;
25
use precompile_utils::prelude::*;
26
use sp_core::{ConstU32, H160, U256};
27
use sp_runtime::traits::{Convert, Dispatchable};
28
use sp_std::{boxed::Box, convert::TryInto, marker::PhantomData, vec::Vec};
29
use sp_weights::Weight;
30
use xcm::{
31
	latest::{Asset, AssetId, Assets, Fungibility, Location, WeightLimit},
32
	VersionedAssets, VersionedLocation,
33
};
34
use xcm_primitives::{
35
	split_location_into_chain_part_and_beneficiary, AccountIdToCurrencyId, DEFAULT_PROOF_SIZE,
36
};
37

            
38
#[cfg(test)]
39
mod mock;
40
#[cfg(test)]
41
mod tests;
42

            
43
pub type CurrencyIdOf<Runtime> = <Runtime as pallet_xcm_transactor::Config>::CurrencyId;
44
pub type CurrencyIdToLocationOf<Runtime> =
45
	<Runtime as pallet_xcm_transactor::Config>::CurrencyIdToLocation;
46

            
47
const MAX_ASSETS: u32 = 20;
48

            
49
/// A precompile to wrap the functionality from xtokens
50
pub struct XtokensPrecompile<Runtime>(PhantomData<Runtime>);
51

            
52
232
#[precompile_utils::precompile]
53
#[precompile::test_concrete_types(mock::Runtime)]
54
impl<Runtime> XtokensPrecompile<Runtime>
55
where
56
	Runtime: pallet_evm::Config
57
		+ pallet_xcm::Config
58
		+ pallet_xcm_transactor::Config
59
		+ frame_system::Config,
60
	<Runtime as frame_system::Config>::RuntimeCall:
61
		Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
62
	<Runtime as frame_system::Config>::RuntimeCall: From<pallet_xcm::Call<Runtime>>,
63
	<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
64
		From<Option<Runtime::AccountId>>,
65
	Runtime: AccountIdToCurrencyId<Runtime::AccountId, CurrencyIdOf<Runtime>>,
66
{
67
	#[precompile::public("transfer(address,uint256,(uint8,bytes[]),uint64)")]
68
8
	fn transfer(
69
8
		handle: &mut impl PrecompileHandle,
70
8
		currency_address: Address,
71
8
		amount: U256,
72
8
		destination: Location,
73
8
		weight: u64,
74
8
	) -> EvmResult {
75
8
		let to_address: H160 = currency_address.into();
76
8
		let to_account = Runtime::AddressMapping::into_account_id(to_address);
77

            
78
		// We convert the address into a currency id xtokens understands
79
8
		let currency_id: CurrencyIdOf<Runtime> = Runtime::account_to_currency_id(to_account)
80
8
			.ok_or(revert("cannot convert into currency id"))?;
81

            
82
8
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
83
8
		let amount = amount
84
8
			.try_into()
85
8
			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
86

            
87
8
		let dest_weight_limit = if weight == u64::MAX {
88
1
			WeightLimit::Unlimited
89
		} else {
90
7
			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
91
		};
92

            
93
8
		let asset = Self::currency_to_asset(currency_id, amount).ok_or(
94
8
			RevertReason::custom("Cannot convert currency into xcm asset")
95
8
				.in_field("currency_address"),
96
8
		)?;
97

            
98
8
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
99
8
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
100

            
101
8
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
102
8
			dest: Box::new(VersionedLocation::V4(chain_part)),
103
8
			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
104
8
			assets: Box::new(VersionedAssets::V4(asset.into())),
105
8
			fee_asset_item: 0,
106
8
			weight_limit: dest_weight_limit,
107
8
		};
108
8

            
109
8
		RuntimeHelper::<Runtime>::try_dispatch(
110
8
			handle,
111
8
			Some(origin).into(),
112
8
			call,
113
8
			SYSTEM_ACCOUNT_SIZE,
114
8
		)?;
115

            
116
8
		Ok(())
117
8
	}
118

            
119
	// transfer_with_fee no longer take the fee parameter into account since we start using
120
	// pallet-xcm. Now, if you want to limit the maximum amount of fees, you'll have to use a
121
	// different asset from the one you wish to transfer and use transfer_multi* selectors.
122
	#[precompile::public("transferWithFee(address,uint256,uint256,(uint8,bytes[]),uint64)")]
123
	#[precompile::public("transfer_with_fee(address,uint256,uint256,(uint8,bytes[]),uint64)")]
124
2
	fn transfer_with_fee(
125
2
		handle: &mut impl PrecompileHandle,
126
2
		currency_address: Address,
127
2
		amount: U256,
128
2
		_fee: U256,
129
2
		destination: Location,
130
2
		weight: u64,
131
2
	) -> EvmResult {
132
2
		let to_address: H160 = currency_address.into();
133
2
		let to_account = Runtime::AddressMapping::into_account_id(to_address);
134

            
135
		// We convert the address into a currency id xtokens understands
136
2
		let currency_id: CurrencyIdOf<Runtime> = Runtime::account_to_currency_id(to_account)
137
2
			.ok_or(
138
2
				RevertReason::custom("Cannot convert into currency id").in_field("currencyAddress"),
139
2
			)?;
140

            
141
2
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
142

            
143
		// Transferred amount
144
2
		let amount = amount
145
2
			.try_into()
146
2
			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
147

            
148
2
		let dest_weight_limit = if weight == u64::MAX {
149
			WeightLimit::Unlimited
150
		} else {
151
2
			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
152
		};
153

            
154
2
		let asset = Self::currency_to_asset(currency_id, amount).ok_or(
155
2
			RevertReason::custom("Cannot convert currency into xcm asset")
156
2
				.in_field("currency_address"),
157
2
		)?;
158

            
159
2
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
160
2
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
161

            
162
2
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
163
2
			dest: Box::new(VersionedLocation::V4(chain_part)),
164
2
			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
165
2
			assets: Box::new(VersionedAssets::V4(asset.into())),
166
2
			fee_asset_item: 0,
167
2
			weight_limit: dest_weight_limit,
168
2
		};
169
2

            
170
2
		RuntimeHelper::<Runtime>::try_dispatch(
171
2
			handle,
172
2
			Some(origin).into(),
173
2
			call,
174
2
			SYSTEM_ACCOUNT_SIZE,
175
2
		)?;
176

            
177
2
		Ok(())
178
2
	}
179

            
180
	#[precompile::public("transferMultiasset((uint8,bytes[]),uint256,(uint8,bytes[]),uint64)")]
181
	#[precompile::public("transfer_multiasset((uint8,bytes[]),uint256,(uint8,bytes[]),uint64)")]
182
6
	fn transfer_multiasset(
183
6
		handle: &mut impl PrecompileHandle,
184
6
		asset: Location,
185
6
		amount: U256,
186
6
		destination: Location,
187
6
		weight: u64,
188
6
	) -> EvmResult {
189
6
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
190
6
		let to_balance = amount
191
6
			.try_into()
192
6
			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
193

            
194
6
		let dest_weight_limit = if weight == u64::MAX {
195
			WeightLimit::Unlimited
196
		} else {
197
6
			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
198
		};
199

            
200
6
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
201
6
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
202

            
203
6
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
204
6
			dest: Box::new(VersionedLocation::V4(chain_part)),
205
6
			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
206
6
			assets: Box::new(VersionedAssets::V4(
207
6
				Asset {
208
6
					id: AssetId(asset),
209
6
					fun: Fungibility::Fungible(to_balance),
210
6
				}
211
6
				.into(),
212
6
			)),
213
6
			fee_asset_item: 0,
214
6
			weight_limit: dest_weight_limit,
215
6
		};
216
6

            
217
6
		RuntimeHelper::<Runtime>::try_dispatch(
218
6
			handle,
219
6
			Some(origin).into(),
220
6
			call,
221
6
			SYSTEM_ACCOUNT_SIZE,
222
6
		)?;
223

            
224
6
		Ok(())
225
6
	}
226

            
227
	#[precompile::public(
228
		"transferMultiassetWithFee((uint8,bytes[]),uint256,uint256,(uint8,bytes[]),uint64)"
229
	)]
230
	#[precompile::public(
231
		"transfer_multiasset_with_fee((uint8,bytes[]),uint256,uint256,(uint8,bytes[]),uint64)"
232
	)]
233
2
	fn transfer_multiasset_with_fee(
234
2
		handle: &mut impl PrecompileHandle,
235
2
		asset: Location,
236
2
		amount: U256,
237
2
		_fee: U256,
238
2
		destination: Location,
239
2
		weight: u64,
240
2
	) -> EvmResult {
241
2
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
242
2
		let amount = amount
243
2
			.try_into()
244
2
			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
245

            
246
2
		let dest_weight_limit = if weight == u64::MAX {
247
			WeightLimit::Unlimited
248
		} else {
249
2
			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
250
		};
251

            
252
2
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
253
2
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
254

            
255
2
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
256
2
			dest: Box::new(VersionedLocation::V4(chain_part)),
257
2
			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
258
2
			assets: Box::new(VersionedAssets::V4(
259
2
				Asset {
260
2
					id: AssetId(asset.clone()),
261
2
					fun: Fungibility::Fungible(amount),
262
2
				}
263
2
				.into(),
264
2
			)),
265
2
			fee_asset_item: 0,
266
2
			weight_limit: dest_weight_limit,
267
2
		};
268
2

            
269
2
		RuntimeHelper::<Runtime>::try_dispatch(
270
2
			handle,
271
2
			Some(origin).into(),
272
2
			call,
273
2
			SYSTEM_ACCOUNT_SIZE,
274
2
		)?;
275

            
276
2
		Ok(())
277
2
	}
278

            
279
	#[precompile::public(
280
		"transferMultiCurrencies((address,uint256)[],uint32,(uint8,bytes[]),uint64)"
281
	)]
282
	#[precompile::public(
283
		"transfer_multi_currencies((address,uint256)[],uint32,(uint8,bytes[]),uint64)"
284
	)]
285
1
	fn transfer_multi_currencies(
286
1
		handle: &mut impl PrecompileHandle,
287
1
		currencies: BoundedVec<Currency, ConstU32<MAX_ASSETS>>,
288
1
		fee_item: u32,
289
1
		destination: Location,
290
1
		weight: u64,
291
1
	) -> EvmResult {
292
1
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
293
1

            
294
1
		// Build all currencies
295
1
		let currencies: Vec<_> = currencies.into();
296
1
		let assets = currencies
297
1
			.into_iter()
298
1
			.enumerate()
299
2
			.map(|(index, currency)| {
300
2
				let address_as_h160: H160 = currency.address.into();
301
2
				let amount = currency.amount.try_into().map_err(|_| {
302
					RevertReason::value_is_too_large("balance type")
303
						.in_array(index)
304
						.in_field("currencies")
305
2
				})?;
306

            
307
2
				let currency_id = Runtime::account_to_currency_id(
308
2
					Runtime::AddressMapping::into_account_id(address_as_h160),
309
2
				)
310
2
				.ok_or(
311
2
					RevertReason::custom("Cannot convert into currency id")
312
2
						.in_array(index)
313
2
						.in_field("currencies"),
314
2
				)?;
315

            
316
2
				Self::currency_to_asset(currency_id, amount).ok_or(
317
2
					RevertReason::custom("Cannot convert currency into xcm asset")
318
2
						.in_array(index)
319
2
						.in_field("currencies")
320
2
						.into(),
321
2
				)
322
2
			})
323
1
			.collect::<EvmResult<Vec<_>>>()?;
324

            
325
1
		let dest_weight_limit = if weight == u64::MAX {
326
			WeightLimit::Unlimited
327
		} else {
328
1
			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
329
		};
330

            
331
1
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
332
1
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
333

            
334
1
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
335
1
			dest: Box::new(VersionedLocation::V4(chain_part)),
336
1
			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
337
1
			assets: Box::new(VersionedAssets::V4(assets.into())),
338
1
			fee_asset_item: fee_item,
339
1
			weight_limit: dest_weight_limit,
340
1
		};
341
1

            
342
1
		RuntimeHelper::<Runtime>::try_dispatch(
343
1
			handle,
344
1
			Some(origin).into(),
345
1
			call,
346
1
			SYSTEM_ACCOUNT_SIZE,
347
1
		)?;
348

            
349
1
		Ok(())
350
1
	}
351

            
352
	#[precompile::public(
353
		"transferMultiAssets(((uint8,bytes[]),uint256)[],uint32,(uint8,bytes[]),uint64)"
354
	)]
355
	#[precompile::public(
356
		"transfer_multi_assets(((uint8,bytes[]),uint256)[],uint32,(uint8,bytes[]),uint64)"
357
	)]
358
2
	fn transfer_multi_assets(
359
2
		handle: &mut impl PrecompileHandle,
360
2
		assets: BoundedVec<EvmAsset, ConstU32<MAX_ASSETS>>,
361
2
		fee_item: u32,
362
2
		destination: Location,
363
2
		weight: u64,
364
2
	) -> EvmResult {
365
2
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
366
2

            
367
2
		let assets: Vec<_> = assets.into();
368
2
		let multiasset_vec: EvmResult<Vec<Asset>> = assets
369
2
			.into_iter()
370
2
			.enumerate()
371
4
			.map(|(index, evm_multiasset)| {
372
4
				let to_balance: u128 = evm_multiasset.amount.try_into().map_err(|_| {
373
					RevertReason::value_is_too_large("balance type")
374
						.in_array(index)
375
						.in_field("assets")
376
4
				})?;
377
4
				Ok((evm_multiasset.location, to_balance).into())
378
4
			})
379
2
			.collect();
380

            
381
		// Since multiassets sorts them, we need to check whether the index is still correct,
382
		// and error otherwise as there is not much we can do other than that
383
2
		let assets = Assets::from_sorted_and_deduplicated(multiasset_vec?).map_err(|_| {
384
1
			RevertReason::custom("Provided assets either not sorted nor deduplicated")
385
1
				.in_field("assets")
386
2
		})?;
387

            
388
1
		let dest_weight_limit = if weight == u64::MAX {
389
			WeightLimit::Unlimited
390
		} else {
391
1
			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
392
		};
393

            
394
1
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
395
1
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
396

            
397
1
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
398
1
			dest: Box::new(VersionedLocation::V4(chain_part)),
399
1
			beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
400
1
			assets: Box::new(VersionedAssets::V4(assets)),
401
1
			fee_asset_item: fee_item,
402
1
			weight_limit: dest_weight_limit,
403
1
		};
404
1

            
405
1
		RuntimeHelper::<Runtime>::try_dispatch(
406
1
			handle,
407
1
			Some(origin).into(),
408
1
			call,
409
1
			SYSTEM_ACCOUNT_SIZE,
410
1
		)?;
411

            
412
1
		Ok(())
413
2
	}
414

            
415
12
	fn currency_to_asset(currency_id: CurrencyIdOf<Runtime>, amount: u128) -> Option<Asset> {
416
12
		Some(Asset {
417
12
			fun: Fungibility::Fungible(amount),
418
12
			id: AssetId(<CurrencyIdToLocationOf<Runtime>>::convert(currency_id)?),
419
		})
420
12
	}
421
}
422

            
423
// Currency
424
2
#[derive(solidity::Codec)]
425
pub struct Currency {
426
	address: Address,
427
	amount: U256,
428
}
429

            
430
impl From<(Address, U256)> for Currency {
431
23
	fn from(tuple: (Address, U256)) -> Self {
432
23
		Currency {
433
23
			address: tuple.0,
434
23
			amount: tuple.1,
435
23
		}
436
23
	}
437
}
438

            
439
4
#[derive(solidity::Codec)]
440
pub struct EvmAsset {
441
	location: Location,
442
	amount: U256,
443
}
444

            
445
impl From<(Location, U256)> for EvmAsset {
446
25
	fn from(tuple: (Location, U256)) -> Self {
447
25
		EvmAsset {
448
25
			location: tuple.0,
449
25
			amount: tuple.1,
450
25
		}
451
25
	}
452
}