1#![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
49pub 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 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 #[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 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 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 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 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#[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}