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
//! 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
242
#[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
	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
67
{
68
	#[precompile::public("transfer(address,uint256,(uint8,bytes[]),uint64)")]
69
10
	fn transfer(
70
10
		handle: &mut impl PrecompileHandle,
71
10
		currency_address: Address,
72
10
		amount: U256,
73
10
		destination: Location,
74
10
		weight: u64,
75
10
	) -> EvmResult {
76
10
		let to_address: H160 = currency_address.into();
77
10
		let to_account = Runtime::AddressMapping::into_account_id(to_address);
78

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

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

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

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

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

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

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

            
117
10
		Ok(())
118
10
	}
119

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

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

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

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

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

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

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

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

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

            
178
2
		Ok(())
179
2
	}
180

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

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

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

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

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

            
225
6
		Ok(())
226
6
	}
227

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

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

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

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

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

            
277
2
		Ok(())
278
2
	}
279

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

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

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

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

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

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

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

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

            
350
1
		Ok(())
351
1
	}
352

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

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

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

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

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

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

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

            
413
1
		Ok(())
414
2
	}
415

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

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

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

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

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