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
224
#[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::from(chain_part)),
104
10
			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
105
10
			assets: Box::new(VersionedAssets::from(asset)),
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::from(chain_part)),
165
2
			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
166
2
			assets: Box::new(VersionedAssets::from(asset)),
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 asset = Asset {
205
6
			id: AssetId(asset),
206
6
			fun: Fungibility::Fungible(to_balance),
207
6
		};
208
6

            
209
6
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
210
6
			dest: Box::new(VersionedLocation::from(chain_part)),
211
6
			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
212
6
			assets: Box::new(VersionedAssets::from(asset)),
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::from(chain_part)),
257
2
			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
258
2
			assets: Box::new(VersionedAssets::from(Asset {
259
2
				id: AssetId(asset.clone()),
260
2
				fun: Fungibility::Fungible(amount),
261
2
			})),
262
2
			fee_asset_item: 0,
263
2
			weight_limit: dest_weight_limit,
264
2
		};
265
2

            
266
2
		RuntimeHelper::<Runtime>::try_dispatch(
267
2
			handle,
268
2
			Some(origin).into(),
269
2
			call,
270
2
			SYSTEM_ACCOUNT_SIZE,
271
2
		)?;
272

            
273
2
		Ok(())
274
2
	}
275

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

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

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

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

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

            
328
1
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
329
1
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
330

            
331
1
		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
332
1
			dest: Box::new(VersionedLocation::from(chain_part)),
333
1
			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
334
1
			assets: Box::new(VersionedAssets::from(assets)),
335
1
			fee_asset_item: fee_item,
336
1
			weight_limit: dest_weight_limit,
337
1
		};
338
1

            
339
1
		RuntimeHelper::<Runtime>::try_dispatch(
340
1
			handle,
341
1
			Some(origin).into(),
342
1
			call,
343
1
			SYSTEM_ACCOUNT_SIZE,
344
1
		)?;
345

            
346
1
		Ok(())
347
1
	}
348

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

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

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

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

            
391
1
		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
392
1
			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
393

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

            
402
1
		RuntimeHelper::<Runtime>::try_dispatch(
403
1
			handle,
404
1
			Some(origin).into(),
405
1
			call,
406
1
			SYSTEM_ACCOUNT_SIZE,
407
1
		)?;
408

            
409
1
		Ok(())
410
2
	}
411

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

            
420
// Currency
421
24
#[derive(solidity::Codec)]
422
pub struct Currency {
423
	address: Address,
424
	amount: U256,
425
}
426

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

            
436
26
#[derive(solidity::Codec)]
437
pub struct EvmAsset {
438
	location: Location,
439
	amount: U256,
440
}
441

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