From 330ef71399bc033c53381b5aee2666c3f275a491 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Rodriguez?= Date: Wed, 24 Apr 2024 11:13:22 -0300 Subject: [PATCH] Add support for pallet-xcm precompile (#35) * add pallet-xcm precompile * update interface docs * update comment for Unlimited weight * add comment in record_cost * mock cleanup * remove custom address from solidity interface * start converter implementation (PoC) * use size_of in AssetIdInfo * fmt * refactor converter * fmt * add Erc20PalletMatcher and adapt mock * remove unused import * minor fixes * add new functions * refactor ForeignAssetMatcher * rust fmt * add more tests and handle db reads costs * fix tests * use proper origin in mock * update solidity interface docs * remove TODO comment --- Cargo.lock | 40 ++ Cargo.toml | 2 + precompiles/assets-erc20/Cargo.toml | 4 + precompiles/assets-erc20/src/lib.rs | 11 +- precompiles/assets-erc20/src/mock.rs | 1 + precompiles/pallet-xcm/Cargo.toml | 76 +++ precompiles/pallet-xcm/XcmInterface.sol | 96 ++++ precompiles/pallet-xcm/src/lib.rs | 280 +++++++++++ precompiles/pallet-xcm/src/mock.rs | 606 ++++++++++++++++++++++++ precompiles/pallet-xcm/src/tests.rs | 411 ++++++++++++++++ primitives/xcm/Cargo.toml | 13 + primitives/xcm/src/generators.rs | 38 ++ primitives/xcm/src/lib.rs | 12 + primitives/xcm/src/location_matcher.rs | 116 +++++ 14 files changed, 1696 insertions(+), 10 deletions(-) create mode 100644 precompiles/pallet-xcm/Cargo.toml create mode 100644 precompiles/pallet-xcm/XcmInterface.sol create mode 100644 precompiles/pallet-xcm/src/lib.rs create mode 100644 precompiles/pallet-xcm/src/mock.rs create mode 100644 precompiles/pallet-xcm/src/tests.rs create mode 100644 primitives/xcm/src/generators.rs create mode 100644 primitives/xcm/src/location_matcher.rs diff --git a/Cargo.lock b/Cargo.lock index 273a3ee5..486840bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6997,6 +6997,38 @@ dependencies = [ "sp-std 14.0.0 (git+https://github.com/moonbeam-foundation/polkadot-sdk?branch=moonbeam-polkadot-v1.7.2)", ] +[[package]] +name = "pallet-evm-precompile-xcm" +version = "0.1.0" +dependencies = [ + "cumulus-primitives-core", + "derive_more", + "evm", + "fp-evm", + "frame-support", + "frame-system", + "log", + "num_enum", + "pallet-assets", + "pallet-balances", + "pallet-evm", + "pallet-foreign-asset-creator", + "pallet-timestamp", + "pallet-xcm", + "parity-scale-codec", + "precompile-utils", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/moonbeam-foundation/polkadot-sdk?branch=moonbeam-polkadot-v1.7.2)", + "sp-weights", + "staging-xcm", + "staging-xcm-builder", + "staging-xcm-executor", + "xcm-primitives", +] + [[package]] name = "pallet-evm-precompile-xcm-utils" version = "0.1.0" @@ -7049,6 +7081,7 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std 14.0.0 (git+https://github.com/moonbeam-foundation/polkadot-sdk?branch=moonbeam-polkadot-v1.7.2)", + "xcm-primitives", ] [[package]] @@ -15475,7 +15508,14 @@ dependencies = [ name = "xcm-primitives" version = "0.1.0" dependencies = [ + "frame-support", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "sp-core", "sp-runtime", + "sp-std 14.0.0 (git+https://github.com/moonbeam-foundation/polkadot-sdk?branch=moonbeam-polkadot-v1.7.2)", + "staging-xcm", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a1c147bf..63cb5c61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,8 @@ flume = "0.11.0" jsonrpsee = { version = "0.20.3" } hex-literal = "0.4.1" +# Moonkit +pallet-foreign-asset-creator = { path = "pallets/foreign-asset-creator", default-features = false } # Substrate (wasm) frame-benchmarking = { git = "https://github.com/moonbeam-foundation/polkadot-sdk", branch = "moonbeam-polkadot-v1.7.2", default-features = false } diff --git a/precompiles/assets-erc20/Cargo.toml b/precompiles/assets-erc20/Cargo.toml index ccc668ea..50d36982 100644 --- a/precompiles/assets-erc20/Cargo.toml +++ b/precompiles/assets-erc20/Cargo.toml @@ -27,6 +27,9 @@ sp-std = { workspace = true } fp-evm = { workspace = true } pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } +# Moonkit +xcm-primitives = { workspace = true } + [dev-dependencies] hex-literal = { workspace = true } libsecp256k1 = { workspace = true } @@ -57,4 +60,5 @@ std = [ "sp-io/std", "sp-runtime/std", "sp-std/std", + "xcm-primitives/std", ] diff --git a/precompiles/assets-erc20/src/lib.rs b/precompiles/assets-erc20/src/lib.rs index cc6d8b4b..16e15da9 100644 --- a/precompiles/assets-erc20/src/lib.rs +++ b/precompiles/assets-erc20/src/lib.rs @@ -38,6 +38,7 @@ use sp_std::{ convert::{TryFrom, TryInto}, marker::PhantomData, }; +use xcm_primitives::AccountIdAssetIdConversion; mod eip2612; use eip2612::Eip2612; @@ -63,16 +64,6 @@ pub type BalanceOf = = >::AssetId; -/// This trait ensure we can convert AccountIds to AssetIds -/// We will require Runtime to have this trait implemented -pub trait AccountIdAssetIdConversion { - // Get assetId and prefix from account - fn account_to_asset_id(account: Account) -> Option<(Vec, AssetId)>; - - // Get AccountId from AssetId and prefix - fn asset_id_to_account(prefix: &[u8], asset_id: AssetId) -> Account; -} - /// The following distribution has been decided for the precompiles /// 0-1023: Ethereum Mainnet Precompiles /// 1024-2047 Precompiles that are not in Ethereum Mainnet but are neither Moonbeam specific diff --git a/precompiles/assets-erc20/src/mock.rs b/precompiles/assets-erc20/src/mock.rs index e645bdf9..93920fdc 100644 --- a/precompiles/assets-erc20/src/mock.rs +++ b/precompiles/assets-erc20/src/mock.rs @@ -36,6 +36,7 @@ use sp_runtime::{ traits::{BlakeTwo256, ConstU32, IdentityLookup}, BuildStorage, }; +use xcm_primitives::AccountIdAssetIdConversion; pub type AccountId = MockAccount; pub type AssetId = u128; diff --git a/precompiles/pallet-xcm/Cargo.toml b/precompiles/pallet-xcm/Cargo.toml new file mode 100644 index 00000000..526a52ea --- /dev/null +++ b/precompiles/pallet-xcm/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "pallet-evm-precompile-xcm" +authors = { workspace = true } +description = "A Precompile to make pallet-xcm accessible to pallet-evm" +edition = "2021" +version = "0.1.0" + +[dependencies] +log = { workspace = true } +num_enum = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = ["codec-xcm"] } +xcm-primitives = { workspace = true } + +# Substrate +frame-support = { workspace = true } +frame-system = { workspace = true } +sp-core = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-weights = { workspace = true } + +# Frontier +evm = { workspace = true, features = [ "with-codec" ] } +fp-evm = { workspace = true } +pallet-evm = { workspace = true, features = [ "forbid-evm-reentrancy" ] } + +# Polkadot +xcm = { workspace = true } +pallet-xcm = { workspace = true } + +# Cumulus +cumulus-primitives-core = { workspace = true } + +[dev-dependencies] +derive_more = { workspace = true } + +# Moonbeam +precompile-utils = { workspace = true, features = [ "testing", "codec-xcm" ] } +xcm-primitives = { workspace = true } + +# Substrate +pallet-assets = { workspace = true, features = [ "std" ] } +pallet-balances = { workspace = true, features = [ "std", "insecure_zero_ed" ] } +pallet-foreign-asset-creator = { workspace = true, features = [ "std" ] } +pallet-timestamp = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "max-encoded-len" ] } +scale-info = { workspace = true, features = [ "derive" ] } +sp-io = { workspace = true } + +# Polkadot +xcm-builder = { workspace = true } +xcm-executor = { workspace = true } + +[features] +default = [ "std" ] +std = [ + "cumulus-primitives-core/std", + "frame-support/std", + "frame-system/std", + "pallet-evm/std", + "pallet-xcm/std", + "precompile-utils/std", + "sp-core/std", + "sp-std/std", + "xcm/std", + "xcm-builder/std", + "xcm-executor/std", + "xcm-primitives/std", +] +runtime-benchmarks = [ + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "xcm-builder/runtime-benchmarks", +] diff --git a/precompiles/pallet-xcm/XcmInterface.sol b/precompiles/pallet-xcm/XcmInterface.sol new file mode 100644 index 00000000..303bad53 --- /dev/null +++ b/precompiles/pallet-xcm/XcmInterface.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.3; + +/// @author The Moonbeam Team +/// @title XCM precompile Interface +/// @dev The interface that Solidity contracts use to interact with the substrate pallet-xcm. +interface XCM { + // A location is defined by its number of parents and the encoded junctions (interior) + struct Location { + uint8 parents; + bytes[] interior; + } + + // Support for Weights V2 + struct Weight { + uint64 refTime; + uint64 proofSize; + } + + // A way to represent fungible assets in XCM using Location format + struct AssetLocationInfo { + Location location; + uint256 amount; + } + + // A way to represent fungible assets in XCM using address format + struct AssetAddressInfo { + address asset; + uint256 amount; + } + + /// @dev Function to send assets via XCM using transfer_assets() pallet-xcm extrinsic. + /// @custom:selector 59df8416 + /// @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 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 + ) external; + + /// @dev Function to send assets via XCM to a 20 byte-like parachain + /// using transfer_assets() pallet-xcm extrinsic. + /// @custom:selector b489262e + /// @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 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 + ) external; + + /// @dev Function to send assets via XCM to a 32 byte-like parachain + /// using transfer_assets() pallet-xcm extrinsic. + /// @custom:selector 4461e6f5 + /// @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 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 + ) external; + + /// @dev Function to send assets via XCM to the relay chain + /// using transfer_assets() pallet-xcm extrinsic. + /// @custom:selector d7c89659 + /// @param beneficiary The actual account that will receive the tokens on the relay chain. + /// @param assets The combination (array) of assets to send. + /// @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 + ) external; +} \ No newline at end of file diff --git a/precompiles/pallet-xcm/src/lib.rs b/precompiles/pallet-xcm/src/lib.rs new file mode 100644 index 00000000..eb03077d --- /dev/null +++ b/precompiles/pallet-xcm/src/lib.rs @@ -0,0 +1,280 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use fp_evm::{PrecompileFailure, PrecompileHandle}; +use frame_support::{ + dispatch::{GetDispatchInfo, PostDispatchInfo}, + traits::ConstU32, +}; +use pallet_evm::AddressMapping; +use precompile_utils::prelude::*; + +use sp_core::{MaxEncodedLen, 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, +}; +use xcm_primitives::{ + generators::{ + XcmLocalBeneficiary20Generator, XcmLocalBeneficiary32Generator, + XcmSiblingDestinationGenerator, + }, + location_matcher::AccountIdToLocationMatcher, +}; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub const MAX_ASSETS_ARRAY_LIMIT: u32 = 2; +type GetArrayLimit = ConstU32; + +pub struct PalletXcmPrecompile(PhantomData<(Runtime, LocationMatcher)>); + +#[precompile_utils::precompile] +impl PalletXcmPrecompile +where + Runtime: pallet_xcm::Config + pallet_evm::Config + frame_system::Config, + ::RuntimeCall: + Dispatchable + GetDispatchInfo, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::RuntimeCall: From>, + LocationMatcher: AccountIdToLocationMatcher<::AccountId>, +{ + #[precompile::public( + "transferAssetsLocation(\ + (uint8,bytes[]),\ + (uint8,bytes[]),\ + ((uint8,bytes[]),uint256)[],\ + uint32,\ + (uint64,uint64))" + )] + fn transfer_assets_location( + handle: &mut impl PrecompileHandle, + dest: Location, + 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. + handle.record_cost(1000)?; + + 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 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, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call, 0)?; + Ok(()) + } + + #[precompile::public( + "transferAssetsToPara20(\ + uint32,\ + address,\ + (address,uint256)[],\ + uint32,\ + (uint64,uint64))" + )] + fn transfer_assets_to_para_20( + handle: &mut impl PrecompileHandle, + para_id: u32, + beneficiary: Address, + assets: BoundedVec<(Address, Convert), GetArrayLimit>, + fee_asset_item: u32, + weight: Weight, + ) -> 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 assets_to_send: Vec = Self::check_and_prepare_assets(assets)?; + + let weight_limit = match weight.ref_time() { + u64::MAX => Unlimited, + _ => Limited(weight), + }; + + let dest = XcmSiblingDestinationGenerator::generate(para_id); + let beneficiary = XcmLocalBeneficiary20Generator::generate(beneficiary.0 .0); + + 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.into())), + fee_asset_item, + weight_limit, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call, 0)?; + + Ok(()) + } + + #[precompile::public( + "transferAssetsToPara32(\ + uint32,\ + bytes32,\ + (address,uint256)[],\ + uint32,\ + (uint64,uint64))" + )] + fn transfer_assets_to_para_32( + handle: &mut impl PrecompileHandle, + para_id: u32, + beneficiary: H256, + assets: BoundedVec<(Address, Convert), GetArrayLimit>, + fee_asset_item: u32, + weight: Weight, + ) -> 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 assets_to_send: Vec = Self::check_and_prepare_assets(assets)?; + + let weight_limit = match weight.ref_time() { + u64::MAX => Unlimited, + _ => Limited(weight), + }; + + let dest = XcmSiblingDestinationGenerator::generate(para_id); + let beneficiary = XcmLocalBeneficiary32Generator::generate(beneficiary.0); + + 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.into())), + fee_asset_item, + weight_limit, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call, 0)?; + + Ok(()) + } + + #[precompile::public( + "transferAssetsToRelay(\ + bytes32,\ + (address,uint256)[],\ + uint32,\ + (uint64,uint64))" + )] + 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(). + // + // 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 assets_to_send: Vec = Self::check_and_prepare_assets(assets)?; + + let weight_limit = match weight.ref_time() { + u64::MAX => Unlimited, + _ => Limited(weight), + }; + + let dest = Location::parent(); + let beneficiary = XcmLocalBeneficiary32Generator::generate(beneficiary.0); + + 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.into())), + fee_asset_item, + weight_limit, + }; + + RuntimeHelper::::try_dispatch(handle, Some(origin).into(), call, 0)?; + + Ok(()) + } + + // Helper function to convert and prepare each asset into a proper Location. + fn check_and_prepare_assets( + assets: Vec<(Address, Convert)>, + ) -> Result, PrecompileFailure> { + let mut assets_to_send: Vec = 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 { + return Err(revert("Asset not found")); + } + assets_to_send.push(Asset { + id: AssetId(asset_location.unwrap_or_default()), + fun: Fungibility::Fungible(asset.1.converted()), + }) + } + Ok(assets_to_send) + } +} diff --git a/precompiles/pallet-xcm/src/mock.rs b/precompiles/pallet-xcm/src/mock.rs new file mode 100644 index 00000000..399342a0 --- /dev/null +++ b/precompiles/pallet-xcm/src/mock.rs @@ -0,0 +1,606 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +//! Test utilities +use super::*; +use frame_support::{ + construct_runtime, parameter_types, + traits::{Everything, Nothing, OriginTrait}, + weights::{RuntimeDbWeight, Weight}, +}; +use frame_system::EnsureRoot; +use pallet_evm::{EnsureAddressNever, EnsureAddressRoot, GasWeightMapping}; +use precompile_utils::{ + mock_account, + precompile_set::*, + testing::{AddressInPrefixedSet, MockAccount}, +}; +use sp_core::{ConstU32, H160, H256, U256}; +use sp_runtime::traits::{BlakeTwo256, IdentityLookup, TryConvert}; +use sp_runtime::BuildStorage; +use xcm::latest::{prelude::*, Error as XcmError}; +use xcm_builder::{ + AllowUnpaidExecutionFrom, Case, FixedWeightBounds, IsConcrete, SovereignSignedViaLocation, +}; +use xcm_executor::{ + traits::{ConvertLocation, TransactAsset, WeightTrader}, + AssetsInHolding, +}; +pub use xcm_primitives::{ + location_matcher::{Erc20PalletMatcher, ForeignAssetMatcher, SingleAddressMatcher}, + AccountIdAssetIdConversion, +}; +use Junctions::Here; + +pub type AccountId = MockAccount; +pub type Balance = u128; + +type Block = frame_system::mocking::MockBlockU32; + +// Configure a mock runtime to test the pallet. +construct_runtime!( + pub enum Runtime { + System: frame_system, + Balances: pallet_balances, + Evm: pallet_evm, + Timestamp: pallet_timestamp, + PolkadotXcm: pallet_xcm, + Assets: pallet_assets, + ForeignAssetCreator: pallet_foreign_asset_creator, + } +); + +parameter_types! { + pub ParachainId: cumulus_primitives_core::ParaId = 100.into(); + pub LocalNetworkId: Option = None; +} + +parameter_types! { + pub const BlockHashCount: u32 = 250; + pub const SS58Prefix: u8 = 42; + pub const MockDbWeight: RuntimeDbWeight = RuntimeDbWeight { + read: 1, + write: 5, + }; +} + +impl frame_system::Config for Runtime { + type BaseCallFilter = Everything; + type DbWeight = MockDbWeight; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type RuntimeCall = RuntimeCall; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} +parameter_types! { + pub const ExistentialDeposit: u128 = 0; +} +impl pallet_balances::Config for Runtime { + type MaxReserves = (); + type ReserveIdentifier = (); + type MaxLocks = (); + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type RuntimeHoldReason = (); + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeFreezeReason = (); +} +parameter_types! { + pub const AssetDeposit: u64 = 0; + pub const ApprovalDeposit: u64 = 0; + pub const StringLimit: u32 = 50; + pub const MetadataDepositBase: u64 = 0; + pub const MetadataDepositPerByte: u64 = 0; +} + +pub const FOREIGN_ASSET_ADDRESS_PREFIX: &[u8] = &[255u8; 18]; + +// Instruct how to go from an H160 to an AssetID +// We just take the lowest 2 bytes +impl AccountIdAssetIdConversion for Runtime { + /// The way to convert an account to assetId is by ensuring that the prefix is [0xFF, 18] + /// and by taking the lowest 2 bytes as the assetId + fn account_to_asset_id(account: AccountId) -> Option<(Vec, AssetId)> { + let h160_account: H160 = account.into(); + let mut data = [0u8; 2]; + let (prefix_part, id_part) = h160_account.as_fixed_bytes().split_at(18); + if prefix_part == FOREIGN_ASSET_ADDRESS_PREFIX { + data.copy_from_slice(id_part); + let asset_id: AssetId = u16::from_be_bytes(data); + Some((prefix_part.to_vec(), asset_id)) + } else { + None + } + } + + // The opposite conversion + fn asset_id_to_account(prefix: &[u8], asset_id: AssetId) -> AccountId { + let mut data = [0u8; 20]; + data[0..18].copy_from_slice(prefix); + data[18..20].copy_from_slice(&asset_id.to_be_bytes()); + AccountId::from(data) + } +} + +pub type AssetId = u16; + +impl pallet_assets::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type AssetId = AssetId; + type AssetIdParameter = parity_scale_codec::Compact; + type Currency = Balances; + type CreateOrigin = frame_support::traits::NeverEnsureOrigin; + type ForceOrigin = EnsureRoot; + type AssetDeposit = AssetDeposit; + type AssetAccountDeposit = AssetDeposit; + type MetadataDepositBase = MetadataDepositBase; + type MetadataDepositPerByte = MetadataDepositPerByte; + type ApprovalDeposit = ApprovalDeposit; + type StringLimit = StringLimit; + type Freezer = (); + type Extra = (); + type CallbackHandle = (); + type WeightInfo = (); + type RemoveItemsLimit = ConstU32<1000>; + pallet_assets::runtime_benchmarks_enabled! { + type BenchmarkHelper = (); + } +} + +impl pallet_foreign_asset_creator::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ForeignAsset = Location; + type ForeignAssetCreatorOrigin = EnsureRoot; + type ForeignAssetModifierOrigin = EnsureRoot; + type ForeignAssetDestroyerOrigin = EnsureRoot; + type Fungibles = Assets; + type WeightInfo = (); + type OnForeignAssetCreated = (); + type OnForeignAssetDestroyed = (); +} + +pub type Precompiles = + PrecompileSetBuilder, PalletXcmPrecompile>>; + +pub type AccountIdAlias = ::AccountId; + +pub type SingleAddressMatch = SingleAddressMatcher; + +pub type ForeignAssetMatch = + ForeignAssetMatcher; + +pub type Erc20Match = Erc20PalletMatcher; + +pub type PCall = + PalletXcmPrecompileCall; + +mock_account!(ParentAccount, |_| MockAccount::from_u64(4)); + +// use simple encoding for parachain accounts. +mock_account!( + SiblingParachainAccount(u32), + |v: SiblingParachainAccount| { AddressInPrefixedSet(0xffffffff, v.0 as u128).into() } +); + +const MAX_POV_SIZE: u64 = 5 * 1024 * 1024; +/// Block storage limit in bytes. Set to 40 KB. +const BLOCK_STORAGE_LIMIT: u64 = 40 * 1024; + +parameter_types! { + pub BlockGasLimit: U256 = U256::from(u64::MAX); + pub PrecompilesValue: Precompiles = Precompiles::new(); + pub const WeightPerGas: Weight = Weight::from_parts(1, 0); + pub GasLimitPovSizeRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(MAX_POV_SIZE) + }; + pub GasLimitStorageGrowthRatio: u64 = { + let block_gas_limit = BlockGasLimit::get().min(u64::MAX.into()).low_u64(); + block_gas_limit.saturating_div(BLOCK_STORAGE_LIMIT) + }; +} + +/// A mapping function that converts Ethereum gas to Substrate weight +/// We are mocking this 1-1 to test db read charges too +pub struct MockGasWeightMapping; +impl GasWeightMapping for MockGasWeightMapping { + fn gas_to_weight(gas: u64, _without_base_weight: bool) -> Weight { + Weight::from_parts(gas, 1) + } + fn weight_to_gas(weight: Weight) -> u64 { + weight.ref_time().into() + } +} + +impl pallet_evm::Config for Runtime { + type FeeCalculator = (); + type GasWeightMapping = MockGasWeightMapping; + type WeightPerGas = WeightPerGas; + type CallOrigin = EnsureAddressRoot; + type WithdrawOrigin = EnsureAddressNever; + type AddressMapping = AccountId; + type Currency = Balances; + type RuntimeEvent = RuntimeEvent; + type Runner = pallet_evm::runner::stack::Runner; + type PrecompilesValue = PrecompilesValue; + type PrecompilesType = Precompiles; + type ChainId = (); + type OnChargeTransaction = (); + type BlockGasLimit = BlockGasLimit; + type BlockHashMapping = pallet_evm::SubstrateBlockHashMapping; + type FindAuthor = (); + type OnCreate = (); + type GasLimitPovSizeRatio = GasLimitPovSizeRatio; + type SuicideQuickClearLimit = ConstU32<0>; + type Timestamp = Timestamp; + type WeightInfo = pallet_evm::weights::SubstrateWeight; +} + +parameter_types! { + pub const MinimumPeriod: u64 = 5; +} +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +use frame_system::RawOrigin as SystemRawOrigin; +use xcm::latest::Junction; +pub struct MockAccountToAccountKey20(PhantomData<(Origin, AccountId)>); + +impl> TryConvert + for MockAccountToAccountKey20 +where + Origin::PalletsOrigin: From> + + TryInto, Error = Origin::PalletsOrigin>, +{ + fn try_convert(o: Origin) -> Result { + o.try_with_caller(|caller| match caller.try_into() { + Ok(SystemRawOrigin::Signed(who)) => { + let account_h160: H160 = who.into(); + Ok(Junction::AccountKey20 { + network: None, + key: account_h160.into(), + } + .into()) + } + Ok(other) => Err(other.into()), + Err(other) => Err(other), + }) + } +} + +pub struct MockParentMultilocationToAccountConverter; +impl ConvertLocation for MockParentMultilocationToAccountConverter { + fn convert_location(location: &Location) -> Option { + match location { + Location { + parents: 1, + interior: Here, + } => Some(ParentAccount.into()), + _ => None, + } + } +} + +pub struct MockParachainMultilocationToAccountConverter; +impl ConvertLocation for MockParachainMultilocationToAccountConverter { + fn convert_location(location: &Location) -> Option { + match location.unpack() { + (1, [Parachain(id)]) => Some(SiblingParachainAccount(*id).into()), + _ => None, + } + } +} + +pub type LocationToAccountId = ( + MockParachainMultilocationToAccountConverter, + MockParentMultilocationToAccountConverter, + xcm_builder::AccountKey20Aliases, +); + +pub type Barrier = AllowUnpaidExecutionFrom; + +pub type LocalOriginToLocation = MockAccountToAccountKey20; + +parameter_types! { + pub MatcherLocation: Location = Location::here(); + + pub UniversalLocation: InteriorLocation = + [GlobalConsensus(RelayNetwork::get()), Parachain(ParachainId::get().into())].into(); + + pub const BaseXcmWeight: Weight = Weight::from_parts(1000u64, 1000u64); + pub const RelayNetwork: NetworkId = NetworkId::Polkadot; + + pub MaxInstructions: u32 = 100; + pub const MaxAssetsIntoHolding: u32 = 64; +} + +impl pallet_xcm::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type SendXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmRouter = TestSendXcm; + type ExecuteXcmOrigin = xcm_builder::EnsureXcmOrigin; + type XcmExecuteFilter = frame_support::traits::Everything; + type XcmExecutor = xcm_executor::XcmExecutor; + type XcmTeleportFilter = Everything; + type XcmReserveTransferFilter = Everything; + type Weigher = FixedWeightBounds; + type UniversalLocation = UniversalLocation; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + const VERSION_DISCOVERY_QUEUE_SIZE: u32 = 100; + type AdvertisedXcmVersion = (); + type Currency = Balances; + type CurrencyMatcher = IsConcrete; + type TrustedLockers = (); + type SovereignAccountOf = (); + type MaxLockers = ConstU32<8>; + type WeightInfo = pallet_xcm::TestWeightInfo; + type MaxRemoteLockConsumers = ConstU32<0>; + type RemoteLockConsumerIdentifier = (); + type AdminOrigin = frame_system::EnsureRoot; +} + +use sp_std::cell::RefCell; +use xcm::latest::{opaque, Assets as XcmAssets}; +// Simulates sending a XCM message +thread_local! { + pub static SENT_XCM: RefCell> = RefCell::new(Vec::new()); +} + +pub struct TestSendXcm; +impl SendXcm for TestSendXcm { + type Ticket = (); + + fn validate( + destination: &mut Option, + message: &mut Option, + ) -> SendResult { + SENT_XCM.with(|q| { + q.borrow_mut() + .push((destination.clone().unwrap(), message.clone().unwrap())) + }); + Ok(((), XcmAssets::new())) + } + + fn deliver(_: Self::Ticket) -> Result { + Ok(XcmHash::default()) + } +} + +pub struct DoNothingRouter; +impl SendXcm for DoNothingRouter { + type Ticket = (); + + fn validate( + _destination: &mut Option, + _message: &mut Option>, + ) -> SendResult { + Ok(((), XcmAssets::new())) + } + + fn deliver(_: Self::Ticket) -> Result { + Ok(XcmHash::default()) + } +} + +pub struct DummyAssetTransactor; +impl TransactAsset for DummyAssetTransactor { + fn deposit_asset(_what: &Asset, _who: &Location, _context: Option<&XcmContext>) -> XcmResult { + Ok(()) + } + + fn withdraw_asset( + _what: &Asset, + _who: &Location, + _maybe_context: Option<&XcmContext>, + ) -> Result { + Ok(AssetsInHolding::default()) + } +} + +pub struct DummyWeightTrader; +impl WeightTrader for DummyWeightTrader { + fn new() -> Self { + DummyWeightTrader + } + + fn buy_weight( + &mut self, + _weight: Weight, + _payment: AssetsInHolding, + _context: &XcmContext, + ) -> Result { + Ok(AssetsInHolding::default()) + } +} + +pub type XcmOriginToTransactDispatchOrigin = ( + // Sovereign account converter; this attempts to derive an `AccountId` from the origin location + // using `LocationToAccountId` and then turn that into the usual `Signed` origin. Useful for + // foreign chains who want to have a local sovereign account on this chain which they control. + SovereignSignedViaLocation, +); + +parameter_types! { + pub ForeignReserveLocation: Location = Location::new( + 1, + [Parachain(2)] + ); + + pub ForeignAsset: Asset = Asset { + fun: Fungible(10000000), + id: AssetId(Location::new( + 1, + [Parachain(2), PalletInstance(3)], + )), + }; + + pub RelayLocation: Location = Location::parent(); + + pub RelayAsset: Asset = Asset { + fun: Fungible(10000000), + id: AssetId(Location::parent()), + }; + + pub LocalAsset: (AssetFilter, Location) = (All.into(), Location::here()); + pub TrustedForeignAsset: (AssetFilter, Location) = (ForeignAsset::get().into(), ForeignReserveLocation::get()); + pub RelayForeignAsset: (AssetFilter, Location) = (RelayAsset::get().into(), RelayLocation::get()); +} + +pub struct XcmConfig; +impl xcm_executor::Config for XcmConfig { + type RuntimeCall = RuntimeCall; + type XcmSender = TestSendXcm; + type AssetTransactor = DummyAssetTransactor; + type OriginConverter = XcmOriginToTransactDispatchOrigin; + type IsReserve = ( + Case, + Case, + Case, + ); + type IsTeleporter = (); + type UniversalLocation = UniversalLocation; + type Barrier = Barrier; + type Weigher = FixedWeightBounds; + type Trader = DummyWeightTrader; + type ResponseHandler = (); + type SubscriptionService = (); + type AssetTrap = (); + type AssetClaims = (); + type CallDispatcher = RuntimeCall; + type AssetLocker = (); + type AssetExchanger = (); + type PalletInstancesInfo = (); + type MaxAssetsIntoHolding = MaxAssetsIntoHolding; + type FeeManager = (); + type MessageExporter = (); + type UniversalAliases = Nothing; + type SafeCallFilter = Everything; + type Aliasers = Nothing; + + type TransactionalProcessor = (); +} + +pub fn root_origin() -> ::RuntimeOrigin { + ::RuntimeOrigin::root() +} + +pub fn origin_of(account_id: AccountId) -> ::RuntimeOrigin { + ::RuntimeOrigin::signed(account_id) +} + +#[derive(Clone)] +pub struct XcmAssetDetails { + pub location: Location, + pub admin: ::AccountId, + pub asset_id: ::AssetId, + pub is_sufficient: bool, + pub min_balance: u128, + pub balance_to_mint: u128, +} + +pub(crate) struct ExtBuilder { + // endowed accounts with balances + balances: Vec<(AccountId, Balance)>, + xcm_assets: Vec, +} + +impl Default for ExtBuilder { + fn default() -> ExtBuilder { + ExtBuilder { + balances: vec![], + xcm_assets: vec![], + } + } +} + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId, Balance)>) -> Self { + self.balances = balances; + self + } + pub(crate) fn with_xcm_assets(mut self, xcm_assets: Vec) -> Self { + self.xcm_assets = xcm_assets; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .expect("Frame system builds valid default genesis config"); + + pallet_balances::GenesisConfig:: { + balances: self.balances, + } + .assimilate_storage(&mut t) + .expect("Pallet balances storage can be assimilated"); + + let xcm_assets = self.xcm_assets.clone(); + + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| { + for xcm_asset in xcm_assets { + ForeignAssetCreator::create_foreign_asset( + root_origin(), + xcm_asset.location, + xcm_asset.asset_id, + xcm_asset.admin, + xcm_asset.is_sufficient, + xcm_asset.min_balance, + ) + .unwrap(); + + Assets::mint( + origin_of(xcm_asset.admin.into()), + xcm_asset.asset_id.into(), + xcm_asset.admin, + xcm_asset.balance_to_mint, + ) + .unwrap(); + } + System::set_block_number(1); + }); + ext + } +} diff --git a/precompiles/pallet-xcm/src/tests.rs b/precompiles/pallet-xcm/src/tests.rs new file mode 100644 index 00000000..c1e95920 --- /dev/null +++ b/precompiles/pallet-xcm/src/tests.rs @@ -0,0 +1,411 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use core::str::FromStr; + +use crate::{mock::*, Location}; +use precompile_utils::{prelude::*, testing::*}; +use sp_core::{H160, H256}; +use sp_weights::Weight; +use xcm::latest::Junction::*; + +fn precompiles() -> Precompiles { + PrecompilesValue::get() +} + +#[test] +fn test_solidity_interface_has_all_function_selectors_documented_and_implemented() { + check_precompile_implements_solidity_interfaces(&["XcmInterface.sol"], PCall::supports_selector) +} + +#[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)); +} + +#[test] +fn modifiers() { + ExtBuilder::default().build().execute_with(|| { + let mut tester = + PrecompilesModifierTester::new(PrecompilesValue::get(), Alice, Precompile1); + + tester.test_default_modifier(PCall::transfer_assets_location_selectors()); + }); +} + +#[test] +fn selector_less_than_four_bytes() { + ExtBuilder::default().build().execute_with(|| { + // This selector is only three bytes long when four are required. + precompiles() + .prepare_test(Alice, Precompile1, vec![1u8, 2u8, 3u8]) + .execute_reverts(|output| output == b"Tried to read selector out of bounds"); + }); +} + +#[test] +fn no_selector_exists_but_length_is_right() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test(Alice, Precompile1, vec![1u8, 2u8, 3u8, 4u8]) + .execute_reverts(|output| output == b"Unknown selector"); + }); +} + +#[test] +fn test_transfer_assets_works() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + let dest = Location::new(1, [Parachain(2)]); + + // Specify the beneficiary from the destination's point of view + let beneficiary = Location::new( + 0, + [AccountKey20 { + network: None, + key: [1; 20], + }], + ); + + let destination_asset_location = Location::new(1, [Parachain(2), PalletInstance(3)]); + let origin_asset_location = Location::new(0, [PalletInstance(1)]); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_location { + dest, + beneficiary, + assets: vec![ + (origin_asset_location, 100u128.into()), + (destination_asset_location, 150u128.into()), + ] + .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(100005001) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_success_when_paying_fees_with_foreign_asset() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + let dest = Location::new(1, [Parachain(2)]); + + // Specify the beneficiary from the destination's point of view + let beneficiary = Location::new( + 0, + [AccountKey20 { + network: None, + key: [1; 20], + }], + ); + + let destination_asset_location = Location::new(1, [Parachain(2), PalletInstance(3)]); + let origin_asset_location = Location::new(0, [PalletInstance(1)]); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_location { + dest, + beneficiary, + assets: vec![ + (origin_asset_location, 100u128.into()), + (destination_asset_location, 150u128.into()), + ] + .into(), + // 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(100005001) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_fails_fees_unknown_reserve() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + let dest = Location::new(1, [Parachain(3)]); + + // Specify the beneficiary from the destination's point of view + let beneficiary = Location::new( + 0, + [AccountKey20 { + network: None, + key: [1; 20], + }], + ); + + let destination_asset_location = Location::new(1, [Parachain(3), PalletInstance(3)]); + let origin_asset_location = Location::new(0, [PalletInstance(1)]); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_location { + dest, + beneficiary, + assets: vec![ + (origin_asset_location, 100u128.into()), + (destination_asset_location, 150u128.into()), + ] + .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() + .execute_reverts(|output| output.ends_with(b"InvalidAssetUnknownReserve\") })")); + }); +} + +#[test] +fn test_transfer_assets_to_para_20_native_asset() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + // We send the native currency of the origin chain. + let pallet_balances_address = H160::from_low_u64_be(2050); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_to_para_20 { + para_id: 2u32, + 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(100005002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_to_para_32_native_asset() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + // We send the native currency of the origin chain. + let pallet_balances_address = H160::from_low_u64_be(2050); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_to_para_32 { + para_id: 2u32, + 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(100005002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_to_relay_native_asset() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .build() + .execute_with(|| { + // We send the native currency of the origin chain. + let pallet_balances_address = H160::from_low_u64_be(2050); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_to_relay { + 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(100005002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_to_para_20_foreign_asset() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .with_xcm_assets(vec![XcmAssetDetails { + location: Location::new(1, [Parachain(2), PalletInstance(3)]), + admin: Alice.into(), + asset_id: 5u16, + is_sufficient: true, + balance_to_mint: 10000u128, + min_balance: 1u128, + }]) + .build() + .execute_with(|| { + // Foreign 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); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_to_para_20 { + para_id: 2u32, + beneficiary: Address(Bob.into()), + assets: vec![ + (Address(pallet_balances_address), 500.into()), + (Address(asset_address), 500.into()), + ] + .into(), + fee_asset_item: 0u32, + weight: Weight::from_parts(u64::MAX, 80000), + }, + ) + .expect_cost(100005002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_to_para_32_foreign_asset() { + ExtBuilder::default() + .with_balances(vec![(Alice.into(), 1000)]) + .with_xcm_assets(vec![XcmAssetDetails { + location: Location::new(1, [Parachain(2), PalletInstance(3)]), + admin: Alice.into(), + asset_id: 5u16, + is_sufficient: true, + balance_to_mint: 10000u128, + min_balance: 1u128, + }]) + .build() + .execute_with(|| { + // Foreign 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); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_to_para_32 { + para_id: 2u32, + beneficiary: H256([0u8; 32]), + assets: vec![ + (Address(pallet_balances_address), 500.into()), + (Address(asset_address), 500.into()), + ] + .into(), + fee_asset_item: 0u32, + weight: Weight::from_parts(u64::MAX, 80000), + }, + ) + .expect_cost(100005002) + .expect_no_logs() + .execute_returns(()); + }); +} + +#[test] +fn test_transfer_assets_to_relay_foreign_asset() { + 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 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); + + precompiles() + .prepare_test( + Alice, + Precompile1, + PCall::transfer_assets_to_relay { + beneficiary: H256([0u8; 32]), + assets: vec![ + (Address(pallet_balances_address), 500.into()), + (Address(asset_address), 500.into()), + ] + .into(), + fee_asset_item: 0u32, + weight: Weight::from_parts(u64::MAX, 80000), + }, + ) + .expect_cost(100005002) + .expect_no_logs() + .execute_returns(()); + }); +} diff --git a/primitives/xcm/Cargo.toml b/primitives/xcm/Cargo.toml index d3e1df3c..4acf8b8b 100644 --- a/primitives/xcm/Cargo.toml +++ b/primitives/xcm/Cargo.toml @@ -6,11 +6,24 @@ edition = "2021" version = "0.1.0" [dependencies] +impl-trait-for-tuples = { workspace = true } +log = { workspace = true } + +frame-support = { workspace = true } +sp-core = { workspace = true } sp-runtime = { workspace = true } +sp-std = { workspace = true } +parity-scale-codec = { workspace = true, features = [ "derive" ] } +xcm = { workspace = true } [features] default = [ "std" ] std = [ + "frame-support/std", + "sp-core/std", "sp-runtime/std", + "sp-std/std", + "parity-scale-codec/std", + "xcm/std" ] runtime-benchmarks = [] diff --git a/primitives/xcm/src/generators.rs b/primitives/xcm/src/generators.rs new file mode 100644 index 00000000..214dcf30 --- /dev/null +++ b/primitives/xcm/src/generators.rs @@ -0,0 +1,38 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use xcm::latest::{Junction::*, Location}; + +pub struct XcmSiblingDestinationGenerator; +impl XcmSiblingDestinationGenerator { + pub fn generate(para_id: u32) -> Location { + Location::new(1, Parachain(para_id)) + } +} + +pub struct XcmLocalBeneficiary20Generator; +impl XcmLocalBeneficiary20Generator { + pub fn generate(key: [u8; 20]) -> Location { + Location::new(0, AccountKey20 { network: None, key }) + } +} + +pub struct XcmLocalBeneficiary32Generator; +impl XcmLocalBeneficiary32Generator { + pub fn generate(id: [u8; 32]) -> Location { + Location::new(0, AccountId32 { network: None, id }) + } +} diff --git a/primitives/xcm/src/lib.rs b/primitives/xcm/src/lib.rs index 75a7171a..64bcfc08 100644 --- a/primitives/xcm/src/lib.rs +++ b/primitives/xcm/src/lib.rs @@ -20,6 +20,9 @@ use sp_runtime::DispatchResult; +pub mod generators; +pub mod location_matcher; + /// Pause and resume execution of XCM pub trait PauseXcmExecution { fn suspend_xcm_execution() -> DispatchResult; @@ -33,3 +36,12 @@ impl PauseXcmExecution for () { Ok(()) } } + +/// This trait ensure we can convert AccountIds to AssetIds. +pub trait AccountIdAssetIdConversion { + // Get assetId and prefix from account + fn account_to_asset_id(account: Account) -> Option<(Vec, AssetId)>; + + // Get AccountId from AssetId and prefix + fn asset_id_to_account(prefix: &[u8], asset_id: AssetId) -> Account; +} diff --git a/primitives/xcm/src/location_matcher.rs b/primitives/xcm/src/location_matcher.rs new file mode 100644 index 00000000..48d0f4db --- /dev/null +++ b/primitives/xcm/src/location_matcher.rs @@ -0,0 +1,116 @@ +// Copyright Moonsong Labs +// This file is part of Moonkit. + +// Moonkit is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Moonkit is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Moonkit. If not, see . + +use crate::AccountIdAssetIdConversion; +use frame_support::{traits::PalletInfoAccess, Parameter}; +use sp_core::H160; +use sp_runtime::traits::MaybeEquivalence; +use sp_std::{fmt::Debug, marker::PhantomData}; +use xcm::latest::{Junction::*, Location}; + +/// A converter from AccountId to a XCM Location. +pub trait AccountIdToLocationMatcher { + fn convert(account: AccountId) -> Option; +} + +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl AccountIdToLocationMatcher for Tuple { + fn convert(account: AccountId) -> Option { + for_tuples!( #( + match Tuple::convert(account.clone()) { o @ Some(_) => return o, _ => () } + )* ); + log::trace!(target: "xcm_primitives::convert", "did not match any location to the account: {:?}", &account); + None + } +} + +/// A matcher for any address that we would like to compare against a received account. +/// Tipically used to manage self-reserve currency through the pallet-balances address. +pub struct SingleAddressMatcher( + PhantomData<(AccountId, PalletInstance)>, +); + +impl AccountIdToLocationMatcher + for SingleAddressMatcher +where + AccountId: Parameter + From, + PalletInstance: PalletInfoAccess, +{ + fn convert(account: AccountId) -> Option { + if account == H160::from_low_u64_be(ADDRESS).into() { + return Some(Location::new( + 0, + [PalletInstance( + ::index() as u8, + )], + )); + } + None + } +} + +/// Matcher to compare a received account against some possible foreign asset address. +pub struct ForeignAssetMatcher< + AccountId, + AssetId, + AccountIdAssetIdConverter, + AssetIdToLocationManager, +>( + PhantomData<( + AccountId, + AssetId, + AccountIdAssetIdConverter, + AssetIdToLocationManager, + )>, +); + +impl + AccountIdToLocationMatcher + for ForeignAssetMatcher +where + AccountIdAssetIdConverter: AccountIdAssetIdConversion, + AssetIdToLocationManager: MaybeEquivalence, +{ + fn convert(account: AccountId) -> Option { + if let Some((_prefix, asset_id)) = AccountIdAssetIdConverter::account_to_asset_id(account) { + return AssetIdToLocationManager::convert_back(&asset_id); + } + None + } +} + +// Matcher for any pallet that handles ERC20s internally. +pub struct Erc20PalletMatcher(PhantomData); + +impl AccountIdToLocationMatcher + for Erc20PalletMatcher +where + AccountId: Parameter + Into, +{ + fn convert(account: AccountId) -> Option { + let h160_account = account.into(); + Some(Location::new( + 0, + [ + PalletInstance(PALLET_INDEX), + AccountKey20 { + key: h160_account.0, + network: None, + }, + ], + )) + } +}