From 767eb0c946a452c7f21d59a0c3e5ae2a7de89f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Rodriguez?= Date: Tue, 8 Oct 2024 13:57:25 -0300 Subject: [PATCH] Add support for `transfer_assets_using_type_and_then` extrinsic in pallet-xcm precompile (#54) * start adding transfer_assets_using_type_and_then to pallet-xcm precompile * add function variant for address type * use WeightLimit::Unlimited in all selectors * add remaining functions and overall refactor * remove unnecessary selectors and add a couple tests * add more tests * pr suggestions * add documentation for solidity selectors * fmt * fix test * fix rust tests compilation * add clarification for SCALE encoding in custom xcm --- pallets/emergency-para-xcm/src/mock.rs | 1 + precompiles/pallet-xcm/Cargo.toml | 5 + precompiles/pallet-xcm/XcmInterface.sol | 119 ++++++-- precompiles/pallet-xcm/src/lib.rs | 371 ++++++++++++++++++++---- precompiles/pallet-xcm/src/tests.rs | 209 +++++++++++-- 5 files changed, 601 insertions(+), 104 deletions(-) diff --git a/pallets/emergency-para-xcm/src/mock.rs b/pallets/emergency-para-xcm/src/mock.rs index f742961e..e9a7ea9f 100644 --- a/pallets/emergency-para-xcm/src/mock.rs +++ b/pallets/emergency-para-xcm/src/mock.rs @@ -19,6 +19,7 @@ use crate as pallet_emergency_para_xcm; use cumulus_pallet_parachain_system::ParachainSetCode; use cumulus_primitives_core::{ relay_chain::BlockNumber as RelayBlockNumber, AggregateMessageOrigin, ParaId, + XcmpMessageHandler, }; use frame_support::parameter_types; use frame_support::traits::ConstU32; diff --git a/precompiles/pallet-xcm/Cargo.toml b/precompiles/pallet-xcm/Cargo.toml index 526a52ea..fb70559d 100644 --- a/precompiles/pallet-xcm/Cargo.toml +++ b/precompiles/pallet-xcm/Cargo.toml @@ -16,10 +16,12 @@ xcm-primitives = { workspace = true } # Substrate frame-support = { workspace = true } frame-system = { workspace = true } +scale-info = { workspace = true } sp-core = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } sp-weights = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "derive" ] } # Frontier evm = { workspace = true, features = [ "with-codec" ] } @@ -28,6 +30,7 @@ pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } # Polkadot xcm = { workspace = true } +xcm-executor = { workspace = true } pallet-xcm = { workspace = true } # Cumulus @@ -61,7 +64,9 @@ std = [ "frame-system/std", "pallet-evm/std", "pallet-xcm/std", + "parity-scale-codec/std", "precompile-utils/std", + "scale-info/std", "sp-core/std", "sp-std/std", "xcm/std", diff --git a/precompiles/pallet-xcm/XcmInterface.sol b/precompiles/pallet-xcm/XcmInterface.sol index 303bad53..d657a2b9 100644 --- a/precompiles/pallet-xcm/XcmInterface.sol +++ b/precompiles/pallet-xcm/XcmInterface.sol @@ -29,68 +29,139 @@ interface XCM { uint256 amount; } + // The values start at `0` and are represented as `uint8` + enum TransferType { + Teleport, + LocalReserve, + DestinationReserve + } + /// @dev Function to send assets via XCM using transfer_assets() pallet-xcm extrinsic. - /// @custom:selector 59df8416 + /// @custom:selector 9ea8ada7 /// @param dest The destination chain. /// @param beneficiary The actual account that will receive the tokens on dest. - /// @param assets The combination (array) of assets to send. + /// @param assets The combination (array) of assets to send in Location format. /// @param feeAssetItem The index of the asset that will be used to pay for fees. - /// @param weight The weight to be used for the whole XCM operation. - /// (uint64::MAX in refTime means Unlimited weight) function transferAssetsLocation( Location memory dest, Location memory beneficiary, AssetLocationInfo[] memory assets, - uint32 feeAssetItem, - Weight memory weight + uint32 feeAssetItem ) external; /// @dev Function to send assets via XCM to a 20 byte-like parachain /// using transfer_assets() pallet-xcm extrinsic. - /// @custom:selector b489262e + /// @custom:selector a0aeb5fe /// @param paraId The para-id of the destination chain. /// @param beneficiary The actual account that will receive the tokens on paraId destination. - /// @param assets The combination (array) of assets to send. + /// @param assets The combination (array) of assets to send in Address format. /// @param feeAssetItem The index of the asset that will be used to pay for fees. - /// @param weight The weight to be used for the whole XCM operation. - /// (uint64::MAX in refTime means Unlimited weight) function transferAssetsToPara20( uint32 paraId, address beneficiary, AssetAddressInfo[] memory assets, - uint32 feeAssetItem, - Weight memory weight + uint32 feeAssetItem ) external; /// @dev Function to send assets via XCM to a 32 byte-like parachain /// using transfer_assets() pallet-xcm extrinsic. - /// @custom:selector 4461e6f5 + /// @custom:selector f23032c3 /// @param paraId The para-id of the destination chain. /// @param beneficiary The actual account that will receive the tokens on paraId destination. - /// @param assets The combination (array) of assets to send. + /// @param assets The combination (array) of assets to send in Address format. /// @param feeAssetItem The index of the asset that will be used to pay for fees. - /// @param weight The weight to be used for the whole XCM operation. - /// (uint64::MAX in refTime means Unlimited weight) function transferAssetsToPara32( uint32 paraId, bytes32 beneficiary, AssetAddressInfo[] memory assets, - uint32 feeAssetItem, - Weight memory weight + uint32 feeAssetItem ) external; /// @dev Function to send assets via XCM to the relay chain /// using transfer_assets() pallet-xcm extrinsic. - /// @custom:selector d7c89659 + /// @custom:selector 6521cc2c /// @param beneficiary The actual account that will receive the tokens on the relay chain. - /// @param assets The combination (array) of assets to send. + /// @param assets The combination (array) of assets to send in Address format. /// @param feeAssetItem The index of the asset that will be used to pay for fees. - /// @param weight The weight to be used for the whole XCM operation. - /// (uint64::MAX in refTime means Unlimited weight) function transferAssetsToRelay( bytes32 beneficiary, AssetAddressInfo[] memory assets, - uint32 feeAssetItem, - Weight memory weight + uint32 feeAssetItem + ) external; + + /// @dev Function to send assets through transfer_assets_using_type_and_then() pallet-xcm + /// extrinsic. + /// Important: in this selector RemoteReserve type (for either assets or fees) is not allowed. + /// If users want to send assets and fees (in Location format) with a remote reserve, + /// they must use the selector fc19376c. + /// @custom:selector 8425d893 + /// @param dest The destination chain. + /// @param assets The combination (array) of assets to send in Location format. + /// @param assetsTransferType The TransferType corresponding to assets being sent. + /// @param remoteFeesIdIndex The index of the asset (inside assets array) to use as fees. + /// @param feesTransferType The TransferType corresponding to the asset used as fees. + /// @param customXcmOnDest The XCM message to execute on destination chain (SCALE encoded). + function transferAssetsUsingTypeAndThenLocation( + Location memory dest, + AssetLocationInfo[] memory assets, + TransferType assetsTransferType, + uint8 remoteFeesIdIndex, + TransferType feesTransferType, + bytes memory customXcmOnDest + ) external; + + /// @dev Function to send assets through transfer_assets_using_type_and_then() pallet-xcm + /// extrinsic. + /// @custom:selector fc19376c + /// @param dest The destination chain. + /// @param assets The combination (array) of assets to send in Location format. + /// @param remoteFeesIdIndex The index of the asset (inside assets array) to use as fees. + /// @param customXcmOnDest The XCM message to execute on destination chain (SCALE encoded). + /// @param remoteReserve The remote reserve corresponding for assets and fees. They MUST + /// share the same reserve. + function transferAssetsUsingTypeAndThenLocation( + Location memory dest, + AssetLocationInfo[] memory assets, + uint8 remoteFeesIdIndex, + bytes memory customXcmOnDest, + Location memory remoteReserve + ) external; + + /// @dev Function to send assets through transfer_assets_using_type_and_then() pallet-xcm + /// extrinsic. + /// Important: in this selector RemoteReserve type (for either assets or fees) is not allowed. + /// If users want to send assets and fees (in Address format) with a remote reserve, + /// they must use the selector aaecfc62. + /// @custom:selector 998093ee + /// @param dest The destination chain. + /// @param assets The combination (array) of assets to send in Address format. + /// @param assetsTransferType The TransferType corresponding to assets being sent. + /// @param remoteFeesIdIndex The index of the asset (inside assets array) to use as fees. + /// @param feesTransferType The TransferType corresponding to the asset used as fees. + /// @param customXcmOnDest The XCM message to execute on destination chain (SCALE encoded). + function transferAssetsUsingTypeAndThenAddress( + Location memory dest, + AssetAddressInfo[] memory assets, + TransferType assetsTransferType, + uint8 remoteFeesIdIndex, + TransferType feesTransferType, + bytes memory customXcmOnDest + ) external; + + /// @dev Function to send assets through transfer_assets_using_type_and_then() pallet-xcm + /// extrinsic. + /// @custom:selector aaecfc62 + /// @param dest The destination chain. + /// @param assets The combination (array) of assets to send in Address format. + /// @param remoteFeesIdIndex The index of the asset (inside assets array) to use as fees. + /// @param customXcmOnDest The XCM message to execute on destination chain (SCALE encoded). + /// @param remoteReserve The remote reserve corresponding for assets and fees. They MUST + /// share the same reserve. + function transferAssetsUsingTypeAndThenAddress( + Location memory dest, + AssetAddressInfo[] memory assets, + uint8 remoteFeesIdIndex, + bytes memory customXcmOnDest, + Location memory remoteReserve ) external; } \ No newline at end of file diff --git a/precompiles/pallet-xcm/src/lib.rs b/precompiles/pallet-xcm/src/lib.rs index 0834e0f1..f067142b 100644 --- a/precompiles/pallet-xcm/src/lib.rs +++ b/precompiles/pallet-xcm/src/lib.rs @@ -22,17 +22,18 @@ use frame_support::{ traits::ConstU32, }; use pallet_evm::AddressMapping; +use parity_scale_codec::{Decode, DecodeLimit, Encode, MaxEncodedLen}; use precompile_utils::prelude::*; -use sp_core::{MaxEncodedLen, H256, U256}; +use scale_info::TypeInfo; +use sp_core::{H256, U256}; use sp_runtime::traits::Dispatchable; use sp_std::{boxed::Box, marker::PhantomData, vec, vec::Vec}; -use sp_weights::Weight; use xcm::{ - latest::{Asset, AssetId, Assets, Fungibility, Location}, - prelude::WeightLimit::*, - VersionedAssets, VersionedLocation, + latest::{Asset, AssetId, Assets, Fungibility, Location, WeightLimit}, + VersionedAssetId, VersionedAssets, VersionedLocation, VersionedXcm, MAX_XCM_DECODE_DEPTH, }; +use xcm_executor::traits::TransferType; use xcm_primitives::{ generators::{ XcmLocalBeneficiary20Generator, XcmLocalBeneficiary32Generator, @@ -49,6 +50,18 @@ mod tests; pub const MAX_ASSETS_ARRAY_LIMIT: u32 = 2; type GetArrayLimit = ConstU32; +pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16); +type GetXcmSizeLimit = ConstU32; + +#[derive( + Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Encode, Decode, Debug, MaxEncodedLen, TypeInfo, +)] +pub enum TransferTypeHelper { + Teleport = 0, + LocalReserve = 1, + DestinationReserve = 2, +} + pub struct PalletXcmPrecompile(PhantomData<(Runtime, LocationMatcher)>); #[precompile_utils::precompile] @@ -67,8 +80,7 @@ where (uint8,bytes[]),\ (uint8,bytes[]),\ ((uint8,bytes[]),uint256)[],\ - uint32,\ - (uint64,uint64))" + uint32)" )] fn transfer_assets_location( handle: &mut impl PrecompileHandle, @@ -76,7 +88,6 @@ where beneficiary: Location, assets: BoundedVec<(Location, Convert), GetArrayLimit>, fee_asset_item: u32, - weight: Weight, ) -> EvmResult { // No DB access before try_dispatch but some logical stuff. // To prevent spam, we charge an arbitrary amount of gas. @@ -85,26 +96,14 @@ where let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let assets: Vec<_> = assets.into(); - let assets_to_send: Assets = assets - .into_iter() - .map(|asset| Asset { - id: AssetId(asset.0), - fun: Fungibility::Fungible(asset.1.converted()), - }) - .collect::>() - .into(); - - let weight_limit = match weight.ref_time() { - u64::MAX => Unlimited, - _ => Limited(weight), - }; + let (assets_to_send, _) = Self::get_assets_to_send_and_remote_fees(assets, None)?; let call = pallet_xcm::Call::::transfer_assets { dest: Box::new(VersionedLocation::V4(dest)), beneficiary: Box::new(VersionedLocation::V4(beneficiary)), assets: Box::new(VersionedAssets::V4(assets_to_send)), fee_asset_item, - weight_limit, + weight_limit: WeightLimit::Unlimited, }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; @@ -116,8 +115,7 @@ where uint32,\ address,\ (address,uint256)[],\ - uint32,\ - (uint64,uint64))" + uint32)" )] fn transfer_assets_to_para_20( handle: &mut impl PrecompileHandle, @@ -125,7 +123,6 @@ where beneficiary: Address, assets: BoundedVec<(Address, Convert), GetArrayLimit>, fee_asset_item: u32, - weight: Weight, ) -> EvmResult { // Account for a possible storage read inside LocationMatcher::convert(). // @@ -138,12 +135,8 @@ where let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let assets: Vec<_> = assets.into(); - let assets_to_send: Vec = Self::check_and_prepare_assets(assets)?; - - let weight_limit = match weight.ref_time() { - u64::MAX => Unlimited, - _ => Limited(weight), - }; + let assets_converted = Self::convert_assets(assets)?; + let (assets_to_send, _) = Self::get_assets_to_send_and_remote_fees(assets_converted, None)?; let dest = XcmSiblingDestinationGenerator::generate(para_id); let beneficiary = XcmLocalBeneficiary20Generator::generate(beneficiary.0 .0); @@ -153,7 +146,7 @@ where beneficiary: Box::new(VersionedLocation::V4(beneficiary)), assets: Box::new(VersionedAssets::V4(assets_to_send.into())), fee_asset_item, - weight_limit, + weight_limit: WeightLimit::Unlimited, }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; @@ -166,8 +159,7 @@ where uint32,\ bytes32,\ (address,uint256)[],\ - uint32,\ - (uint64,uint64))" + uint32)" )] fn transfer_assets_to_para_32( handle: &mut impl PrecompileHandle, @@ -175,7 +167,6 @@ where beneficiary: H256, assets: BoundedVec<(Address, Convert), GetArrayLimit>, fee_asset_item: u32, - weight: Weight, ) -> EvmResult { // Account for a possible storage read inside LocationMatcher::convert(). // @@ -188,12 +179,8 @@ where let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let assets: Vec<_> = assets.into(); - let assets_to_send: Vec = Self::check_and_prepare_assets(assets)?; - - let weight_limit = match weight.ref_time() { - u64::MAX => Unlimited, - _ => Limited(weight), - }; + let assets_converted = Self::convert_assets(assets)?; + let (assets_to_send, _) = Self::get_assets_to_send_and_remote_fees(assets_converted, None)?; let dest = XcmSiblingDestinationGenerator::generate(para_id); let beneficiary = XcmLocalBeneficiary32Generator::generate(beneficiary.0); @@ -203,7 +190,7 @@ where beneficiary: Box::new(VersionedLocation::V4(beneficiary)), assets: Box::new(VersionedAssets::V4(assets_to_send.into())), fee_asset_item, - weight_limit, + weight_limit: WeightLimit::Unlimited, }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; @@ -215,15 +202,13 @@ where "transferAssetsToRelay(\ bytes32,\ (address,uint256)[],\ - uint32,\ - (uint64,uint64))" + uint32)" )] fn transfer_assets_to_relay( handle: &mut impl PrecompileHandle, beneficiary: H256, assets: BoundedVec<(Address, Convert), GetArrayLimit>, fee_asset_item: u32, - weight: Weight, ) -> EvmResult { // Account for a possible storage read inside LocationMatcher::convert(). // @@ -236,12 +221,8 @@ where let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); let assets: Vec<_> = assets.into(); - let assets_to_send: Vec = Self::check_and_prepare_assets(assets)?; - - let weight_limit = match weight.ref_time() { - u64::MAX => Unlimited, - _ => Limited(weight), - }; + let assets_converted = Self::convert_assets(assets)?; + let (assets_to_send, _) = Self::get_assets_to_send_and_remote_fees(assets_converted, None)?; let dest = Location::parent(); let beneficiary = XcmLocalBeneficiary32Generator::generate(beneficiary.0); @@ -251,7 +232,241 @@ where beneficiary: Box::new(VersionedLocation::V4(beneficiary)), assets: Box::new(VersionedAssets::V4(assets_to_send.into())), fee_asset_item, - weight_limit, + weight_limit: WeightLimit::Unlimited, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public( + "transferAssetsUsingTypeAndThenLocation(\ + (uint8,bytes[]),\ + ((uint8,bytes[]),uint256)[],\ + uint8,\ + uint8,\ + uint8,\ + bytes)" + )] + fn transfer_assets_using_type_and_then_location_no_remote_reserve( + handle: &mut impl PrecompileHandle, + dest: Location, + assets: BoundedVec<(Location, Convert), GetArrayLimit>, + assets_transfer_type: u8, + remote_fees_id_index: u8, + fees_transfer_type: u8, + custom_xcm_on_dest: BoundedBytes, + ) -> EvmResult { + // No DB access before try_dispatch but some logical stuff. + // To prevent spam, we charge an arbitrary amount of gas. + handle.record_cost(1000)?; + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let assets: Vec<_> = assets.into(); + let custom_xcm_on_dest: Vec = custom_xcm_on_dest.into(); + + let (assets_to_send, remote_fees_id) = Self::get_assets_to_send_and_remote_fees( + assets.clone(), + Some(remote_fees_id_index as usize), + )?; + let remote_fees_id = + remote_fees_id.ok_or_else(|| RevertReason::custom("remote_fees_id not found"))?; + + let assets_transfer_type = Self::parse_transfer_type(assets_transfer_type)?; + let fees_transfer_type = Self::parse_transfer_type(fees_transfer_type)?; + + let custom_xcm_on_dest = VersionedXcm::<()>::decode_all_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut custom_xcm_on_dest.as_slice(), + ) + .map_err(|_| RevertReason::custom("Failed decoding custom XCM message"))?; + + let call = pallet_xcm::Call::::transfer_assets_using_type_and_then { + dest: Box::new(VersionedLocation::V4(dest)), + assets: Box::new(VersionedAssets::V4(assets_to_send)), + assets_transfer_type: Box::new(assets_transfer_type), + remote_fees_id: Box::new(VersionedAssetId::V4(remote_fees_id)), + fees_transfer_type: Box::new(fees_transfer_type), + custom_xcm_on_dest: Box::new(custom_xcm_on_dest), + weight_limit: WeightLimit::Unlimited, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public( + "transferAssetsUsingTypeAndThenLocation(\ + (uint8,bytes[]),\ + ((uint8,bytes[]),uint256)[],\ + uint8,\ + bytes,\ + (uint8,bytes[]))" + )] + fn transfer_assets_using_type_and_then_location_remote_reserve( + handle: &mut impl PrecompileHandle, + dest: Location, + assets: BoundedVec<(Location, Convert), GetArrayLimit>, + remote_fees_id_index: u8, + custom_xcm_on_dest: BoundedBytes, + remote_reserve: Location, + ) -> EvmResult { + // No DB access before try_dispatch but some logical stuff. + // To prevent spam, we charge an arbitrary amount of gas. + handle.record_cost(1000)?; + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let assets: Vec<_> = assets.into(); + let custom_xcm_on_dest: Vec = custom_xcm_on_dest.into(); + + let (assets_to_send, remote_fees_id) = Self::get_assets_to_send_and_remote_fees( + assets.clone(), + Some(remote_fees_id_index as usize), + )?; + let remote_fees_id = + remote_fees_id.ok_or_else(|| RevertReason::custom("remote_fees_id not found"))?; + + let custom_xcm_on_dest = VersionedXcm::<()>::decode_all_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut custom_xcm_on_dest.as_slice(), + ) + .map_err(|_| RevertReason::custom("Failed decoding custom XCM message"))?; + + let asset_and_fees_transfer_type = + TransferType::RemoteReserve(VersionedLocation::V4(remote_reserve)); + + let call = pallet_xcm::Call::::transfer_assets_using_type_and_then { + dest: Box::new(VersionedLocation::V4(dest)), + assets: Box::new(VersionedAssets::V4(assets_to_send)), + assets_transfer_type: Box::new(asset_and_fees_transfer_type.clone()), + remote_fees_id: Box::new(VersionedAssetId::V4(remote_fees_id)), + fees_transfer_type: Box::new(asset_and_fees_transfer_type), + custom_xcm_on_dest: Box::new(custom_xcm_on_dest), + weight_limit: WeightLimit::Unlimited, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public( + "transferAssetsUsingTypeAndThenAddress(\ + (uint8,bytes[]),\ + (address,uint256)[],\ + uint8,\ + uint8,\ + uint8,\ + bytes)" + )] + fn transfer_assets_using_type_and_then_address_no_remote_reserve( + handle: &mut impl PrecompileHandle, + dest: Location, + assets: BoundedVec<(Address, Convert), GetArrayLimit>, + assets_transfer_type: u8, + remote_fees_id_index: u8, + fees_transfer_type: u8, + custom_xcm_on_dest: BoundedBytes, + ) -> EvmResult { + // Account for a possible storage read inside LocationMatcher::convert(). + // + // Storage items: AssetIdToForeignAsset (ForeignAssetCreator pallet) or AssetIdType (AssetManager pallet). + // + // Blake2_128(16) + AssetId(16) + Location + handle.record_db_read::(32 + Location::max_encoded_len())?; + handle.record_cost(1000)?; + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let assets: Vec<_> = assets.into(); + let custom_xcm_on_dest: Vec = custom_xcm_on_dest.into(); + + let assets_converted = Self::convert_assets(assets)?; + let (assets_to_send, remote_fees_id) = Self::get_assets_to_send_and_remote_fees( + assets_converted, + Some(remote_fees_id_index as usize), + )?; + let remote_fees_id = + remote_fees_id.ok_or_else(|| RevertReason::custom("remote_fees_id not found"))?; + + let assets_transfer_type = Self::parse_transfer_type(assets_transfer_type)?; + let fees_transfer_type = Self::parse_transfer_type(fees_transfer_type)?; + + let custom_xcm_on_dest = VersionedXcm::<()>::decode_all_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut custom_xcm_on_dest.as_slice(), + ) + .map_err(|_| RevertReason::custom("Failed decoding custom XCM message"))?; + + let call = pallet_xcm::Call::::transfer_assets_using_type_and_then { + dest: Box::new(VersionedLocation::V4(dest)), + assets: Box::new(VersionedAssets::V4(assets_to_send.into())), + assets_transfer_type: Box::new(assets_transfer_type), + remote_fees_id: Box::new(VersionedAssetId::V4(remote_fees_id)), + fees_transfer_type: Box::new(fees_transfer_type), + custom_xcm_on_dest: Box::new(custom_xcm_on_dest), + weight_limit: WeightLimit::Unlimited, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; + + Ok(()) + } + + #[precompile::public( + "transferAssetsUsingTypeAndThenAddress(\ + (uint8,bytes[]),\ + (address,uint256)[],\ + uint8,\ + bytes,\ + (uint8,bytes[]))" + )] + fn transfer_assets_using_type_and_then_address_remote_reserve( + handle: &mut impl PrecompileHandle, + dest: Location, + assets: BoundedVec<(Address, Convert), GetArrayLimit>, + remote_fees_id_index: u8, + custom_xcm_on_dest: BoundedBytes, + remote_reserve: Location, + ) -> EvmResult { + // Account for a possible storage read inside LocationMatcher::convert(). + // + // Storage items: AssetIdToForeignAsset (ForeignAssetCreator pallet) or AssetIdType (AssetManager pallet). + // + // Blake2_128(16) + AssetId(16) + Location + handle.record_db_read::(32 + Location::max_encoded_len())?; + handle.record_cost(1000)?; + + let origin = Runtime::AddressMapping::into_account_id(handle.context().caller); + let assets: Vec<_> = assets.into(); + let custom_xcm_on_dest: Vec = custom_xcm_on_dest.into(); + + let assets_converted = Self::convert_assets(assets)?; + let (assets_to_send, remote_fees_id) = Self::get_assets_to_send_and_remote_fees( + assets_converted, + Some(remote_fees_id_index as usize), + )?; + let remote_fees_id = + remote_fees_id.ok_or_else(|| RevertReason::custom("remote_fees_id not found"))?; + + let custom_xcm_on_dest = VersionedXcm::<()>::decode_all_with_depth_limit( + MAX_XCM_DECODE_DEPTH, + &mut custom_xcm_on_dest.as_slice(), + ) + .map_err(|_| RevertReason::custom("Failed decoding custom XCM message"))?; + + let asset_and_fees_transfer_type = + TransferType::RemoteReserve(VersionedLocation::V4(remote_reserve)); + + let call = pallet_xcm::Call::::transfer_assets_using_type_and_then { + dest: Box::new(VersionedLocation::V4(dest)), + assets: Box::new(VersionedAssets::V4(assets_to_send.into())), + assets_transfer_type: Box::new(asset_and_fees_transfer_type.clone()), + remote_fees_id: Box::new(VersionedAssetId::V4(remote_fees_id)), + fees_transfer_type: Box::new(asset_and_fees_transfer_type), + custom_xcm_on_dest: Box::new(custom_xcm_on_dest), + weight_limit: WeightLimit::Unlimited, }; RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call)?; @@ -260,21 +475,59 @@ where } // Helper function to convert and prepare each asset into a proper Location. - fn check_and_prepare_assets( + fn convert_assets( assets: Vec<(Address, Convert)>, - ) -> Result, PrecompileFailure> { - let mut assets_to_send: Vec = vec![]; + ) -> Result)>, PrecompileFailure> { + let mut assets_to_send: Vec<(Location, Convert)> = vec![]; for asset in assets { let asset_account = Runtime::AddressMapping::into_account_id(asset.0 .0); let asset_location = LocationMatcher::convert(asset_account); - if asset_location == None { + + if let Some(asset_loc) = asset_location { + assets_to_send.push((asset_loc, asset.1)) + } else { return Err(revert("Asset not found")); } - assets_to_send.push(Asset { - id: AssetId(asset_location.unwrap_or_default()), + } + Ok(assets_to_send) + } + + fn get_assets_to_send_and_remote_fees( + assets: Vec<(Location, Convert)>, + remote_fees_id_index: Option, + ) -> Result<(Assets, Option), PrecompileFailure> { + let assets_to_send: Assets = assets + .into_iter() + .map(|asset| Asset { + id: AssetId(asset.0), fun: Fungibility::Fungible(asset.1.converted()), }) + .collect::>() + .into(); + + if let Some(index) = remote_fees_id_index { + let remote_fees_id: AssetId = { + let asset = assets_to_send + .get(index) + .ok_or_else(|| RevertReason::custom("remote_fees_id not found"))?; + AssetId(asset.id.0.clone()) + }; + return Ok((assets_to_send, Some(remote_fees_id))); + } + + Ok((assets_to_send, None)) + } + + fn parse_transfer_type(transfer_type: u8) -> Result { + let transfer_type_helper: TransferTypeHelper = TransferTypeHelper::decode( + &mut transfer_type.to_le_bytes().as_slice(), + ) + .map_err(|_| RevertReason::custom("Failed decoding value for TransferTypeHelper"))?; + + match transfer_type_helper { + TransferTypeHelper::Teleport => return Ok(TransferType::Teleport), + TransferTypeHelper::LocalReserve => return Ok(TransferType::LocalReserve), + TransferTypeHelper::DestinationReserve => return Ok(TransferType::DestinationReserve), } - Ok(assets_to_send) } } diff --git a/precompiles/pallet-xcm/src/tests.rs b/precompiles/pallet-xcm/src/tests.rs index fec474b9..91508c43 100644 --- a/precompiles/pallet-xcm/src/tests.rs +++ b/precompiles/pallet-xcm/src/tests.rs @@ -16,11 +16,12 @@ use core::str::FromStr; -use crate::{mock::*, Location}; +use crate::{mock::*, Location, TransferTypeHelper}; +use parity_scale_codec::Encode; use precompile_utils::{prelude::*, testing::*}; use sp_core::{H160, H256}; -use sp_weights::Weight; use xcm::latest::Junction::*; +use xcm::prelude::*; fn precompiles() -> Precompiles { PrecompilesValue::get() @@ -33,10 +34,26 @@ fn test_solidity_interface_has_all_function_selectors_documented_and_implemented #[test] fn selectors() { - assert!(PCall::transfer_assets_location_selectors().contains(&0x59df8416)); - assert!(PCall::transfer_assets_to_para_20_selectors().contains(&0xb489262e)); - assert!(PCall::transfer_assets_to_para_32_selectors().contains(&0x4461e6f5)); - assert!(PCall::transfer_assets_to_relay_selectors().contains(&0xd7c89659)); + assert!(PCall::transfer_assets_location_selectors().contains(&0x9ea8ada7)); + assert!(PCall::transfer_assets_to_para_20_selectors().contains(&0xa0aeb5fe)); + assert!(PCall::transfer_assets_to_para_32_selectors().contains(&0xf23032c3)); + assert!(PCall::transfer_assets_to_relay_selectors().contains(&0x6521cc2c)); + assert!( + PCall::transfer_assets_using_type_and_then_location_no_remote_reserve_selectors() + .contains(&0x8425d893) + ); + assert!( + PCall::transfer_assets_using_type_and_then_location_remote_reserve_selectors() + .contains(&0xfc19376c) + ); + assert!( + PCall::transfer_assets_using_type_and_then_address_no_remote_reserve_selectors() + .contains(&0x998093ee) + ); + assert!( + PCall::transfer_assets_using_type_and_then_address_remote_reserve_selectors() + .contains(&0xaaecfc62) + ); } #[test] @@ -101,9 +118,6 @@ fn test_transfer_assets_works() { ] .into(), fee_asset_item: 0u32, - // As we are indicating u64::MAX in ref_time, an Unlimited variant - // will be applied at the end. - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001001) @@ -147,9 +161,6 @@ fn test_transfer_assets_success_when_paying_fees_with_foreign_asset() { // We also act as a reserve for the foreign asset thus when can pay local // fees with it. fee_asset_item: 1u32, - // As we are indicating u64::MAX in ref_time, an Unlimited variant - // will be applied at the end. - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001001) @@ -192,9 +203,6 @@ fn test_transfer_assets_fails_fees_unknown_reserve() { .into(), // No reserve will be found for this asset. fee_asset_item: 1u32, - // As we are indicating u64::MAX in ref_time, an Unlimited variant - // will be applied at the end. - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_no_logs() @@ -220,7 +228,6 @@ fn test_transfer_assets_to_para_20_native_asset() { beneficiary: Address(Bob.into()), assets: vec![(Address(pallet_balances_address), 500.into())].into(), fee_asset_item: 0u32, - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001002) @@ -247,7 +254,6 @@ fn test_transfer_assets_to_para_32_native_asset() { beneficiary: H256([1u8; 32]), assets: vec![(Address(pallet_balances_address), 500.into())].into(), fee_asset_item: 0u32, - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001002) @@ -273,7 +279,6 @@ fn test_transfer_assets_to_relay_native_asset() { beneficiary: H256([1u8; 32]), assets: vec![(Address(pallet_balances_address), 500.into())].into(), fee_asset_item: 0u32, - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001002) @@ -316,7 +321,6 @@ fn test_transfer_assets_to_para_20_foreign_asset() { ] .into(), fee_asset_item: 0u32, - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001002) @@ -359,7 +363,6 @@ fn test_transfer_assets_to_para_32_foreign_asset() { ] .into(), fee_asset_item: 0u32, - weight: Weight::from_parts(u64::MAX, 80000), }, ) .expect_cost(100001002) @@ -401,7 +404,171 @@ fn test_transfer_assets_to_relay_foreign_asset() { ] .into(), fee_asset_item: 0u32, - weight: Weight::from_parts(u64::MAX, 80000), + }, + ) + .expect_cost(100001002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_using_type_and_then_location_no_remote_reserve() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .with_xcm_assets(vec![XcmAssetDetails { + location: Location::parent(), + admin: Alice.into(), + asset_id: 5u16, + is_sufficient: true, + balance_to_mint: 10000u128, + min_balance: 1u128, + }]) + .build() + .execute_with(|| { + let dest = Location::new(1, [Parachain(2)]); + let destination_asset_location = Location::new(1, [Parachain(2), PalletInstance(3)]); + let origin_asset_location = Location::new(0, [PalletInstance(1)]); + + let message: Vec = xcm::VersionedXcm::<()>::V4(Xcm(vec![ClearOrigin])).encode(); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_using_type_and_then_location_no_remote_reserve { + dest, + assets: vec![ + (origin_asset_location, 100u128.into()), + (destination_asset_location, 150u128.into()), + ] + .into(), + assets_transfer_type: TransferTypeHelper::DestinationReserve as u8, + remote_fees_id_index: 0u8, + fees_transfer_type: TransferTypeHelper::LocalReserve as u8, + custom_xcm_on_dest: message.into(), + }, + ) + .expect_cost(100001001) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_using_type_and_then_location_remote_reserve() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .with_xcm_assets(vec![XcmAssetDetails { + location: Location::parent(), + admin: Alice.into(), + asset_id: 5u16, + is_sufficient: true, + balance_to_mint: 10000u128, + min_balance: 1u128, + }]) + .build() + .execute_with(|| { + let dest = Location::new(1, [Parachain(2)]); + let relay_asset_location = Location::parent(); + + let message: Vec = xcm::VersionedXcm::<()>::V4(Xcm(vec![ClearOrigin])).encode(); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_using_type_and_then_location_remote_reserve { + dest, + assets: vec![(relay_asset_location, 150u128.into())].into(), + remote_fees_id_index: 0u8, + custom_xcm_on_dest: message.into(), + remote_reserve: Location::parent(), + }, + ) + .expect_cost(100001001) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_using_type_and_then_address_no_remote_reserve() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .with_xcm_assets(vec![XcmAssetDetails { + location: Location::parent(), + admin: Alice.into(), + asset_id: 5u16, + is_sufficient: true, + balance_to_mint: 10000u128, + min_balance: 1u128, + }]) + .build() + .execute_with(|| { + // Foreign (relay) asset with prefix [255; 18] and assetId of 5u16. + let asset_address = + H160::from_str("0xfFfFFFffFffFFFFffFFfFfffFfFFFFFfffFF0005").unwrap(); + + // We send the native currency of the origin chain and pay fees with it. + let pallet_balances_address = H160::from_low_u64_be(2050); + + let message: Vec = xcm::VersionedXcm::<()>::V4(Xcm(vec![ClearOrigin])).encode(); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_using_type_and_then_address_no_remote_reserve { + dest: Location::parent(), + assets: vec![ + (Address(pallet_balances_address), 500.into()), + (Address(asset_address), 500.into()), + ] + .into(), + assets_transfer_type: TransferTypeHelper::DestinationReserve as u8, + remote_fees_id_index: 0u8, + fees_transfer_type: TransferTypeHelper::LocalReserve as u8, + custom_xcm_on_dest: message.into(), + }, + ) + .expect_cost(100001002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_using_type_and_then_address_remote_reserve() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .with_xcm_assets(vec![XcmAssetDetails { + location: Location::parent(), + admin: Alice.into(), + asset_id: 5u16, + is_sufficient: true, + balance_to_mint: 10000u128, + min_balance: 1u128, + }]) + .build() + .execute_with(|| { + // Foreign (relay) asset with prefix [255; 18] and assetId of 5u16. + let asset_address = + H160::from_str("0xfFfFFFffFffFFFFffFFfFfffFfFFFFFfffFF0005").unwrap(); + + let dest = Location::new(1, [Parachain(2)]); + let message: Vec = xcm::VersionedXcm::<()>::V4(Xcm(vec![ClearOrigin])).encode(); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_using_type_and_then_address_remote_reserve { + dest, + assets: vec![(Address(asset_address), 500.into())].into(), + remote_fees_id_index: 0u8, + custom_xcm_on_dest: message.into(), + remote_reserve: Location::parent(), }, ) .expect_cost(100001002)