pallet_evm_precompile_xtokens/
lib.rs

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
21use account::SYSTEM_ACCOUNT_SIZE;
22use fp_evm::PrecompileHandle;
23use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
24use pallet_evm::AddressMapping;
25use precompile_utils::prelude::*;
26use sp_core::{ConstU32, H160, U256};
27use sp_runtime::traits::{Convert, Dispatchable};
28use sp_std::{boxed::Box, convert::TryInto, marker::PhantomData, vec::Vec};
29use sp_weights::Weight;
30use xcm::{
31	latest::{Asset, AssetId, Assets, Fungibility, Location, WeightLimit},
32	VersionedAssets, VersionedLocation,
33};
34use xcm_primitives::{
35	split_location_into_chain_part_and_beneficiary, AccountIdToCurrencyId, DEFAULT_PROOF_SIZE,
36};
37
38#[cfg(test)]
39mod mock;
40#[cfg(test)]
41mod tests;
42
43pub type CurrencyIdOf<Runtime> = <Runtime as pallet_xcm_transactor::Config>::CurrencyId;
44pub type CurrencyIdToLocationOf<Runtime> =
45	<Runtime as pallet_xcm_transactor::Config>::CurrencyIdToLocation;
46
47const MAX_ASSETS: u32 = 20;
48
49/// A precompile to wrap the functionality from xtokens
50pub struct XtokensPrecompile<Runtime>(PhantomData<Runtime>);
51
52#[precompile_utils::precompile]
53#[precompile::test_concrete_types(mock::Runtime)]
54impl<Runtime> XtokensPrecompile<Runtime>
55where
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	fn transfer(
70		handle: &mut impl PrecompileHandle,
71		currency_address: Address,
72		amount: U256,
73		destination: Location,
74		weight: u64,
75	) -> EvmResult {
76		let to_address: H160 = currency_address.into();
77		let to_account = Runtime::AddressMapping::into_account_id(to_address);
78
79		// We convert the address into a currency id xtokens understands
80		let currency_id: CurrencyIdOf<Runtime> = Runtime::account_to_currency_id(to_account)
81			.ok_or(revert("cannot convert into currency id"))?;
82
83		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
84		let amount = amount
85			.try_into()
86			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
87
88		let dest_weight_limit = if weight == u64::MAX {
89			WeightLimit::Unlimited
90		} else {
91			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
92		};
93
94		let asset = Self::currency_to_asset(currency_id, amount).ok_or(
95			RevertReason::custom("Cannot convert currency into xcm asset")
96				.in_field("currency_address"),
97		)?;
98
99		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
100			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
101
102		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
103			dest: Box::new(VersionedLocation::from(chain_part)),
104			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
105			assets: Box::new(VersionedAssets::from(asset)),
106			fee_asset_item: 0,
107			weight_limit: dest_weight_limit,
108		};
109
110		RuntimeHelper::<Runtime>::try_dispatch(
111			handle,
112			Some(origin).into(),
113			call,
114			SYSTEM_ACCOUNT_SIZE,
115		)?;
116
117		Ok(())
118	}
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	fn transfer_with_fee(
126		handle: &mut impl PrecompileHandle,
127		currency_address: Address,
128		amount: U256,
129		_fee: U256,
130		destination: Location,
131		weight: u64,
132	) -> EvmResult {
133		let to_address: H160 = currency_address.into();
134		let to_account = Runtime::AddressMapping::into_account_id(to_address);
135
136		// We convert the address into a currency id xtokens understands
137		let currency_id: CurrencyIdOf<Runtime> = Runtime::account_to_currency_id(to_account)
138			.ok_or(
139				RevertReason::custom("Cannot convert into currency id").in_field("currencyAddress"),
140			)?;
141
142		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
143
144		// Transferred amount
145		let amount = amount
146			.try_into()
147			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
148
149		let dest_weight_limit = if weight == u64::MAX {
150			WeightLimit::Unlimited
151		} else {
152			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
153		};
154
155		let asset = Self::currency_to_asset(currency_id, amount).ok_or(
156			RevertReason::custom("Cannot convert currency into xcm asset")
157				.in_field("currency_address"),
158		)?;
159
160		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
161			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
162
163		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
164			dest: Box::new(VersionedLocation::from(chain_part)),
165			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
166			assets: Box::new(VersionedAssets::from(asset)),
167			fee_asset_item: 0,
168			weight_limit: dest_weight_limit,
169		};
170
171		RuntimeHelper::<Runtime>::try_dispatch(
172			handle,
173			Some(origin).into(),
174			call,
175			SYSTEM_ACCOUNT_SIZE,
176		)?;
177
178		Ok(())
179	}
180
181	#[precompile::public("transferMultiasset((uint8,bytes[]),uint256,(uint8,bytes[]),uint64)")]
182	#[precompile::public("transfer_multiasset((uint8,bytes[]),uint256,(uint8,bytes[]),uint64)")]
183	fn transfer_multiasset(
184		handle: &mut impl PrecompileHandle,
185		asset: Location,
186		amount: U256,
187		destination: Location,
188		weight: u64,
189	) -> EvmResult {
190		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
191		let to_balance = amount
192			.try_into()
193			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
194
195		let dest_weight_limit = if weight == u64::MAX {
196			WeightLimit::Unlimited
197		} else {
198			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
199		};
200
201		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
202			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
203
204		let asset = Asset {
205			id: AssetId(asset),
206			fun: Fungibility::Fungible(to_balance),
207		};
208
209		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
210			dest: Box::new(VersionedLocation::from(chain_part)),
211			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
212			assets: Box::new(VersionedAssets::from(asset)),
213			fee_asset_item: 0,
214			weight_limit: dest_weight_limit,
215		};
216
217		RuntimeHelper::<Runtime>::try_dispatch(
218			handle,
219			Some(origin).into(),
220			call,
221			SYSTEM_ACCOUNT_SIZE,
222		)?;
223
224		Ok(())
225	}
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	fn transfer_multiasset_with_fee(
234		handle: &mut impl PrecompileHandle,
235		asset: Location,
236		amount: U256,
237		_fee: U256,
238		destination: Location,
239		weight: u64,
240	) -> EvmResult {
241		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
242		let amount = amount
243			.try_into()
244			.map_err(|_| RevertReason::value_is_too_large("balance type").in_field("amount"))?;
245
246		let dest_weight_limit = if weight == u64::MAX {
247			WeightLimit::Unlimited
248		} else {
249			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
250		};
251
252		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
253			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
254
255		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
256			dest: Box::new(VersionedLocation::from(chain_part)),
257			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
258			assets: Box::new(VersionedAssets::from(Asset {
259				id: AssetId(asset.clone()),
260				fun: Fungibility::Fungible(amount),
261			})),
262			fee_asset_item: 0,
263			weight_limit: dest_weight_limit,
264		};
265
266		RuntimeHelper::<Runtime>::try_dispatch(
267			handle,
268			Some(origin).into(),
269			call,
270			SYSTEM_ACCOUNT_SIZE,
271		)?;
272
273		Ok(())
274	}
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	fn transfer_multi_currencies(
283		handle: &mut impl PrecompileHandle,
284		currencies: BoundedVec<Currency, ConstU32<MAX_ASSETS>>,
285		fee_item: u32,
286		destination: Location,
287		weight: u64,
288	) -> EvmResult {
289		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
290
291		// Build all currencies
292		let currencies: Vec<_> = currencies.into();
293		let assets = currencies
294			.into_iter()
295			.enumerate()
296			.map(|(index, currency)| {
297				let address_as_h160: H160 = currency.address.into();
298				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				})?;
303
304				let currency_id = Runtime::account_to_currency_id(
305					Runtime::AddressMapping::into_account_id(address_as_h160),
306				)
307				.ok_or(
308					RevertReason::custom("Cannot convert into currency id")
309						.in_array(index)
310						.in_field("currencies"),
311				)?;
312
313				Self::currency_to_asset(currency_id, amount).ok_or(
314					RevertReason::custom("Cannot convert currency into xcm asset")
315						.in_array(index)
316						.in_field("currencies")
317						.into(),
318				)
319			})
320			.collect::<EvmResult<Vec<_>>>()?;
321
322		let dest_weight_limit = if weight == u64::MAX {
323			WeightLimit::Unlimited
324		} else {
325			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
326		};
327
328		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
329			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
330
331		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
332			dest: Box::new(VersionedLocation::from(chain_part)),
333			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
334			assets: Box::new(VersionedAssets::from(assets)),
335			fee_asset_item: fee_item,
336			weight_limit: dest_weight_limit,
337		};
338
339		RuntimeHelper::<Runtime>::try_dispatch(
340			handle,
341			Some(origin).into(),
342			call,
343			SYSTEM_ACCOUNT_SIZE,
344		)?;
345
346		Ok(())
347	}
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	fn transfer_multi_assets(
356		handle: &mut impl PrecompileHandle,
357		assets: BoundedVec<EvmAsset, ConstU32<MAX_ASSETS>>,
358		fee_item: u32,
359		destination: Location,
360		weight: u64,
361	) -> EvmResult {
362		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
363
364		let assets: Vec<_> = assets.into();
365		let multiasset_vec: EvmResult<Vec<Asset>> = assets
366			.into_iter()
367			.enumerate()
368			.map(|(index, evm_multiasset)| {
369				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				})?;
374				Ok((evm_multiasset.location, to_balance).into())
375			})
376			.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		let assets = Assets::from_sorted_and_deduplicated(multiasset_vec?).map_err(|_| {
381			RevertReason::custom("Provided assets either not sorted nor deduplicated")
382				.in_field("assets")
383		})?;
384
385		let dest_weight_limit = if weight == u64::MAX {
386			WeightLimit::Unlimited
387		} else {
388			WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE))
389		};
390
391		let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(destination)
392			.ok_or_else(|| RevertReason::custom("Invalid destination").in_field("destination"))?;
393
394		let call = pallet_xcm::Call::<Runtime>::transfer_assets {
395			dest: Box::new(VersionedLocation::from(chain_part)),
396			beneficiary: Box::new(VersionedLocation::from(beneficiary)),
397			assets: Box::new(VersionedAssets::from(assets)),
398			fee_asset_item: fee_item,
399			weight_limit: dest_weight_limit,
400		};
401
402		RuntimeHelper::<Runtime>::try_dispatch(
403			handle,
404			Some(origin).into(),
405			call,
406			SYSTEM_ACCOUNT_SIZE,
407		)?;
408
409		Ok(())
410	}
411
412	fn currency_to_asset(currency_id: CurrencyIdOf<Runtime>, amount: u128) -> Option<Asset> {
413		Some(Asset {
414			fun: Fungibility::Fungible(amount),
415			id: AssetId(<CurrencyIdToLocationOf<Runtime>>::convert(currency_id)?),
416		})
417	}
418}
419
420// Currency
421#[derive(solidity::Codec)]
422pub struct Currency {
423	address: Address,
424	amount: U256,
425}
426
427impl From<(Address, U256)> for Currency {
428	fn from(tuple: (Address, U256)) -> Self {
429		Currency {
430			address: tuple.0,
431			amount: tuple.1,
432		}
433	}
434}
435
436#[derive(solidity::Codec)]
437pub struct EvmAsset {
438	location: Location,
439	amount: U256,
440}
441
442impl From<(Location, U256)> for EvmAsset {
443	fn from(tuple: (Location, U256)) -> Self {
444		EvmAsset {
445			location: tuple.0,
446			amount: tuple.1,
447		}
448	}
449}