diff --git a/Cargo.lock b/Cargo.lock index 28f586a9b..81cd56d6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7478,9 +7478,10 @@ dependencies = [ [[package]] name = "pallet-evm-precompile-xcm" -version = "0.9.0" +version = "0.10.0" dependencies = [ "assert_matches", + "astar-primitives", "derive_more", "fp-evm", "frame-support", @@ -7488,6 +7489,9 @@ dependencies = [ "hex-literal", "log", "num_enum 0.5.11", + "orml-traits", + "orml-xcm-support", + "orml-xtokens", "pallet-assets", "pallet-balances", "pallet-evm", diff --git a/precompiles/utils/src/bytes.rs b/precompiles/utils/src/bytes.rs index ba86df929..f1e6994b9 100644 --- a/precompiles/utils/src/bytes.rs +++ b/precompiles/utils/src/bytes.rs @@ -135,7 +135,7 @@ impl> EvmData for BoundedBytesString { writer.write_pointer( EvmDataWriter::new() .write(U256::from(length)) - .write(value) + .write_raw_bytes(&value) .build(), ); } diff --git a/precompiles/utils/src/data.rs b/precompiles/utils/src/data.rs index 2504171b3..3d4c1242c 100644 --- a/precompiles/utils/src/data.rs +++ b/precompiles/utils/src/data.rs @@ -285,7 +285,7 @@ impl EvmDataWriter { /// Write arbitrary bytes. /// Doesn't handle any alignement checks, prefer using `write` instead if possible. - fn write_raw_bytes(mut self, value: &[u8]) -> Self { + pub fn write_raw_bytes(mut self, value: &[u8]) -> Self { self.data.extend_from_slice(value); self } diff --git a/precompiles/utils/src/lib.rs b/precompiles/utils/src/lib.rs index ac7cfce9e..a58db7f77 100644 --- a/precompiles/utils/src/lib.rs +++ b/precompiles/utils/src/lib.rs @@ -41,6 +41,7 @@ use sp_std::{marker::PhantomData, vec, vec::Vec}; pub mod bytes; pub mod data; +pub mod xcm; pub use data::{Address, Bytes, EvmData, EvmDataReader, EvmDataWriter}; pub use precompile_utils_macro::{generate_function_selector, keccak256}; diff --git a/precompiles/utils/src/xcm.rs b/precompiles/utils/src/xcm.rs new file mode 100644 index 000000000..16897c774 --- /dev/null +++ b/precompiles/utils/src/xcm.rs @@ -0,0 +1,466 @@ +// This file is part of Astar. + +// Copyright 2019-2022 PureStake Inc. +// Copyright (C) 2022-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later +// +// This file is part of Utils package, originally developed by Purestake Inc. +// Utils package used in Astar Network in terms of GPLv3. +// +// Utils 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. + +// Utils 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 Utils. If not, see . + +//! Encoding of XCM types for solidity + +use crate::Address; +use sp_core::U256; +use sp_runtime::traits::Zero; +use { + crate::{bytes::*, revert, EvmData, EvmDataReader, EvmDataWriter, EvmResult}, + frame_support::{ensure, pallet_prelude::Weight, traits::ConstU32}, + sp_core::H256, + sp_std::vec::Vec, + xcm::latest::{Junction, Junctions, MultiLocation, NetworkId}, +}; +pub const JUNCTION_SIZE_LIMIT: u32 = 2u32.pow(16); + +// Function to convert network id to bytes +// Each NetworkId variant is represented as bytes +// The first byte represents the enum variant to be used. +// - Indexes 0,2,3 represent XCM V2 variants +// - Index 1 changes name in V3 (`ByGenesis`), but is compatible with V2 `Named` +// - Indexes 4~10 represent new XCM V3 variants +// The rest of the bytes (if any), represent the additional data that such enum variant requires +// In such a case, since NetworkIds will be appended at the end, we will read the buffer until the +// end to recover the name + +pub(crate) fn network_id_to_bytes(network_id: Option) -> Vec { + let mut encoded: Vec = Vec::new(); + match network_id.clone() { + None => { + encoded.push(0u8); + encoded + } + Some(NetworkId::ByGenesis(id)) => { + encoded.push(1u8); + encoded.append(&mut id.into()); + encoded + } + Some(NetworkId::Polkadot) => { + encoded.push(2u8); + encoded.push(2u8); + encoded + } + Some(NetworkId::Kusama) => { + encoded.push(3u8); + encoded.push(3u8); + encoded + } + Some(NetworkId::ByFork { + block_number, + block_hash, + }) => { + encoded.push(4u8); + encoded.push(1u8); + encoded.append(&mut block_number.to_be_bytes().into()); + encoded.append(&mut block_hash.into()); + encoded + } + Some(NetworkId::Westend) => { + encoded.push(5u8); + encoded.push(4u8); + encoded + } + Some(NetworkId::Rococo) => { + encoded.push(6u8); + encoded.push(5u8); + encoded + } + Some(NetworkId::Wococo) => { + encoded.push(7u8); + encoded.push(6u8); + encoded + } + Some(NetworkId::Ethereum { chain_id }) => { + encoded.push(8u8); + encoded.push(7u8); + encoded.append(&mut chain_id.to_be_bytes().into()); + encoded + } + Some(NetworkId::BitcoinCore) => { + encoded.push(9u8); + encoded.push(8u8); + encoded + } + Some(NetworkId::BitcoinCash) => { + encoded.push(10u8); + encoded.push(9u8); + encoded + } + } +} + +// Function to convert bytes to networkId +pub(crate) fn network_id_from_bytes(encoded_bytes: Vec) -> EvmResult> { + ensure!(encoded_bytes.len() > 0, revert("Junctions cannot be empty")); + let mut encoded_network_id = EvmDataReader::new(&encoded_bytes); + + let network_selector = encoded_network_id + .read_raw_bytes(1) + .map_err(|_| revert("network selector (1 byte)"))?; + + match network_selector[0] { + 0 => Ok(None), + 1 => Ok(Some(NetworkId::ByGenesis( + encoded_network_id + .read_till_end() + .map_err(|_| revert("can't read till end"))? + .to_vec() + .try_into() + .map_err(|_| revert("network by genesis"))?, + ))), + 2 => Ok(Some(NetworkId::Polkadot)), + 3 => Ok(Some(NetworkId::Kusama)), + 4 => { + let mut block_number: [u8; 8] = Default::default(); + block_number.copy_from_slice(&encoded_network_id.read_raw_bytes(8)?); + + let mut block_hash: [u8; 32] = Default::default(); + block_hash.copy_from_slice(&encoded_network_id.read_raw_bytes(32)?); + Ok(Some(NetworkId::ByFork { + block_number: u64::from_be_bytes(block_number), + block_hash, + })) + } + 5 => Ok(Some(NetworkId::Westend)), + 6 => Ok(Some(NetworkId::Rococo)), + 7 => Ok(Some(NetworkId::Wococo)), + 8 => { + let mut chain_id: [u8; 8] = Default::default(); + chain_id.copy_from_slice(&encoded_network_id.read_raw_bytes(8)?); + Ok(Some(NetworkId::Ethereum { + chain_id: u64::from_be_bytes(chain_id), + })) + } + 9 => Ok(Some(NetworkId::BitcoinCore)), + 10 => Ok(Some(NetworkId::BitcoinCash)), + _ => Err(revert("Non-valid Network Id").into()), + } +} + +impl EvmData for Junction { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let junction = reader.read::>>()?; + let junction_bytes: Vec<_> = junction.into(); + + ensure!( + junction_bytes.len() > 0, + revert("Junctions cannot be empty") + ); + + // For simplicity we use an EvmReader here + let mut encoded_junction = EvmDataReader::new(&junction_bytes); + + // We take the first byte + let enum_selector = encoded_junction + .read_raw_bytes(1) + .map_err(|_| revert("junction variant"))?; + + // The firs byte selects the enum variant + match enum_selector[0] { + 0 => { + // In the case of Junction::Parachain, we need 4 additional bytes + let mut data: [u8; 4] = Default::default(); + data.copy_from_slice(&encoded_junction.read_raw_bytes(4)?); + let para_id = u32::from_be_bytes(data); + Ok(Junction::Parachain(para_id)) + } + 1 => { + // In the case of Junction::AccountId32, we need 32 additional bytes plus NetworkId + let mut account: [u8; 32] = Default::default(); + account.copy_from_slice(&encoded_junction.read_raw_bytes(32)?); + + let network = encoded_junction.read_till_end()?.to_vec(); + Ok(Junction::AccountId32 { + network: network_id_from_bytes(network)?, + id: account, + }) + } + 2 => { + // In the case of Junction::AccountIndex64, we need 8 additional bytes plus NetworkId + let mut index: [u8; 8] = Default::default(); + index.copy_from_slice(&encoded_junction.read_raw_bytes(8)?); + // Now we read the network + let network = encoded_junction.read_till_end()?.to_vec(); + Ok(Junction::AccountIndex64 { + network: network_id_from_bytes(network)?, + index: u64::from_be_bytes(index), + }) + } + 3 => { + // In the case of Junction::AccountKey20, we need 20 additional bytes plus NetworkId + let mut account: [u8; 20] = Default::default(); + account.copy_from_slice(&encoded_junction.read_raw_bytes(20)?); + + let network = encoded_junction.read_till_end()?.to_vec(); + Ok(Junction::AccountKey20 { + network: network_id_from_bytes(network)?, + key: account, + }) + } + 4 => Ok(Junction::PalletInstance( + encoded_junction.read_raw_bytes(1)?[0], + )), + 5 => { + // In the case of Junction::GeneralIndex, we need 16 additional bytes + let mut general_index: [u8; 16] = Default::default(); + general_index.copy_from_slice(&encoded_junction.read_raw_bytes(16)?); + Ok(Junction::GeneralIndex(u128::from_be_bytes(general_index))) + } + 6 => { + let length = encoded_junction + .read_raw_bytes(1) + .map_err(|_| revert("General Key length"))?[0]; + + let data = encoded_junction + .read::() + .map_err(|_| revert("can't read"))? + .into(); + + Ok(Junction::GeneralKey { length, data }) + } + 7 => Ok(Junction::OnlyChild), + 8 => Err(revert("Junction::Plurality not supported yet").into()), + 9 => { + let network = encoded_junction.read_till_end()?.to_vec(); + if let Some(network_id) = network_id_from_bytes(network)? { + Ok(Junction::GlobalConsensus(network_id)) + } else { + Err(revert("Unknown NetworkId").into()) + } + } + _ => Err(revert("Unknown Junction variant").into()), + } + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + let mut encoded: Vec = Vec::new(); + let encoded_bytes: UnboundedBytes = match value { + Junction::Parachain(para_id) => { + encoded.push(0u8); + encoded.append(&mut para_id.to_be_bytes().to_vec()); + encoded.as_slice().into() + } + Junction::AccountId32 { network, id } => { + encoded.push(1u8); + encoded.append(&mut id.to_vec()); + encoded.append(&mut network_id_to_bytes(network)); + encoded.as_slice().into() + } + Junction::AccountIndex64 { network, index } => { + encoded.push(2u8); + encoded.append(&mut index.to_be_bytes().to_vec()); + encoded.append(&mut network_id_to_bytes(network)); + encoded.as_slice().into() + } + Junction::AccountKey20 { network, key } => { + encoded.push(3u8); + encoded.append(&mut key.to_vec()); + encoded.append(&mut network_id_to_bytes(network)); + encoded.as_slice().into() + } + Junction::PalletInstance(intance) => { + encoded.push(4u8); + encoded.append(&mut intance.to_be_bytes().to_vec()); + encoded.as_slice().into() + } + Junction::GeneralIndex(id) => { + encoded.push(5u8); + encoded.append(&mut id.to_be_bytes().to_vec()); + encoded.as_slice().into() + } + Junction::GeneralKey { length, data } => { + encoded.push(6u8); + encoded.push(length); + encoded.append(&mut data.into()); + encoded.as_slice().into() + } + Junction::OnlyChild => { + encoded.push(7u8); + encoded.as_slice().into() + } + Junction::GlobalConsensus(network_id) => { + encoded.push(9u8); + encoded.append(&mut network_id_to_bytes(Some(network_id))); + encoded.as_slice().into() + } + // TODO: The only missing item here is Junciton::Plurality. This is a complex encoded + // type that we need to evaluate how to support + _ => unreachable!("Junction::Plurality not supported yet"), + }; + EvmData::write(writer, encoded_bytes); + } + + fn has_static_size() -> bool { + false + } +} + +impl EvmData for Junctions { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let junctions_bytes: Vec = reader.read()?; + let mut junctions = Junctions::Here; + for item in junctions_bytes { + junctions + .push(item) + .map_err(|_| revert("overflow when reading junctions"))?; + } + + Ok(junctions) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + let encoded: Vec = value.iter().map(|junction| junction.clone()).collect(); + EvmData::write(writer, encoded); + } + + fn has_static_size() -> bool { + false + } +} + +// Cannot used derive macro since it is a foreign struct. +impl EvmData for MultiLocation { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let (parents, interior) = reader.read()?; + Ok(MultiLocation { parents, interior }) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + EvmData::write(writer, (value.parents, value.interior)); + } + + fn has_static_size() -> bool { + <(u8, Junctions)>::has_static_size() + } +} + +#[derive(Debug, Clone)] +pub struct WeightV2 { + ref_time: u64, + proof_size: u64, +} +impl WeightV2 { + pub fn from(ref_time: u64, proof_size: u64) -> Self { + WeightV2 { + ref_time, + proof_size, + } + } + pub fn get_weight(&self) -> Weight { + Weight::from_parts(self.ref_time, self.proof_size) + } + pub fn is_zero(&self) -> bool { + self.ref_time.is_zero() + } +} +impl EvmData for WeightV2 { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let (ref_time, proof_size) = reader.read()?; + Ok(WeightV2 { + ref_time, + proof_size, + }) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + EvmData::write(writer, (value.ref_time, value.proof_size)); + } + + fn has_static_size() -> bool { + <(U256, U256)>::has_static_size() + } +} +#[derive(Debug)] +pub struct EvmMultiAsset { + location: MultiLocation, + amount: U256, +} + +impl EvmMultiAsset { + pub fn get_location(&self) -> MultiLocation { + self.location + } + pub fn get_amount(&self) -> U256 { + self.amount + } +} +impl From<(MultiLocation, U256)> for EvmMultiAsset { + fn from(tuple: (MultiLocation, U256)) -> Self { + EvmMultiAsset { + location: tuple.0, + amount: tuple.1, + } + } +} +impl EvmData for EvmMultiAsset { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let (location, amount) = reader.read()?; + Ok(EvmMultiAsset { location, amount }) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + EvmData::write(writer, (value.location, value.amount)); + } + + fn has_static_size() -> bool { + <(MultiLocation, U256)>::has_static_size() + } +} + +pub struct Currency { + address: Address, + amount: U256, +} + +impl Currency { + pub fn get_address(&self) -> Address { + self.address + } + pub fn get_amount(&self) -> U256 { + self.amount + } +} +impl From<(Address, U256)> for Currency { + fn from(tuple: (Address, U256)) -> Self { + Currency { + address: tuple.0, + amount: tuple.1, + } + } +} + +impl EvmData for Currency { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let (address, amount) = reader.read()?; + Ok(Currency { address, amount }) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + EvmData::write(writer, (value.address, value.amount)); + } + + fn has_static_size() -> bool { + <(Address, U256)>::has_static_size() + } +} diff --git a/precompiles/xcm/Cargo.toml b/precompiles/xcm/Cargo.toml index 8065e4e71..63df2c623 100644 --- a/precompiles/xcm/Cargo.toml +++ b/precompiles/xcm/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "pallet-evm-precompile-xcm" description = "Basic XCM support for EVM." -version = "0.9.0" +version = "0.10.0" authors.workspace = true edition.workspace = true homepage.workspace = true repository.workspace = true [dependencies] +astar-primitives = { workspace = true } log = { workspace = true } num_enum = { workspace = true } pallet-evm-precompile-assets-erc20 = { workspace = true } @@ -28,6 +29,9 @@ fp-evm = { workspace = true } pallet-evm = { workspace = true } # Polkadot +orml-traits = { workspace = true } +orml-xcm-support = { workspace = true } +orml-xtokens = { workspace = true } xcm = { workspace = true } xcm-executor = { workspace = true } @@ -62,5 +66,8 @@ std = [ "sp-io/std", "xcm/std", "xcm-executor/std", + "orml-xtokens/std", + "orml-xcm-support/std", + "orml-traits/std", ] runtime-benchmarks = [] diff --git a/precompiles/xcm/XCM.sol b/precompiles/xcm/XCM.sol index 19af7ae9b..408e1e07e 100644 --- a/precompiles/xcm/XCM.sol +++ b/precompiles/xcm/XCM.sol @@ -4,8 +4,8 @@ pragma solidity ^0.8.0; * @title XCM interface. */ interface XCM { + /** - * @dev Withdraw assets using PalletXCM call. * @param asset_id - list of XC20 asset addresses * @param asset_amount - list of transfer amounts (must match with asset addresses above) * @param recipient_account_id - SS58 public key of the destination account @@ -28,7 +28,6 @@ interface XCM { ) external returns (bool); /** - * @dev Withdraw assets using PalletXCM call. * @param asset_id - list of XC20 asset addresses * @param asset_amount - list of transfer amounts (must match with asset addresses above) * @param recipient_account_id - ETH address of the destination account @@ -51,7 +50,6 @@ interface XCM { ) external returns (bool); /** - * @dev Execute a transaction on a remote chain. * @param parachain_id - destination parachain Id (ignored if is_relay is true) * @param is_relay - if true, destination is relay_chain, if false it is parachain (see previous argument) * @param payment_asset_id - ETH address of the local asset derivate used to pay for execution in the destination chain @@ -70,7 +68,6 @@ interface XCM { ) external returns (bool); /** - * @dev Reserve transfer assets using PalletXCM call. * @param asset_id - list of XC20 asset addresses * @param asset_amount - list of transfer amounts (must match with asset addresses above) * @param recipient_account_id - SS58 public key of the destination account @@ -93,7 +90,6 @@ interface XCM { ) external returns (bool); /** - * @dev Reserve transfer using PalletXCM call. * @param asset_id - list of XC20 asset addresses * @param asset_amount - list of transfer amounts (must match with asset addresses above) * @param recipient_account_id - ETH address of the destination account @@ -114,4 +110,5 @@ interface XCM { uint256 parachain_id, uint256 fee_index ) external returns (bool); + } diff --git a/precompiles/xcm/XCM_v2.sol b/precompiles/xcm/XCM_v2.sol new file mode 100644 index 000000000..fa035ef2d --- /dev/null +++ b/precompiles/xcm/XCM_v2.sol @@ -0,0 +1,136 @@ + +pragma solidity ^0.8.0; + + +/** + * @title XCM interface. + */ +interface XCM { + // A multilocation is defined by its number of parents and the encoded junctions (interior) + struct Multilocation { + uint8 parents; + bytes[] interior; + } + + struct WeightV2{ + uint64 ref_time; + uint64 proof_size; + } + + // A MultiAsset is defined by a multilocation and an amount + struct MultiAsset { + Multilocation location; + uint256 amount; + } + + // A Currency is defined by address and the amount to be transferred + struct Currency { + address currencyAddress; + uint256 amount; + } + + /// Transfer a token through XCM based on its address + /// + /// @dev The token transfer burns/transfers the corresponding amount before sending + /// @param currencyAddress The ERC20 address of the currency we want to transfer + /// @param amount The amount of tokens we want to transfer + /// @param destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain, to set the + /// weightlimit to Unlimited, you should use the value 0 for ref_time + function transfer( + address currencyAddress, + uint256 amount, + Multilocation memory destination, + WeightV2 memory weight + ) external returns (bool); + + /// Transfer a token through XCM based on its address specifying fee + /// + /// @dev The token transfer burns/transfers the corresponding amount before sending + /// @param currencyAddress The ERC20 address of the currency we want to transfer + /// @param amount The amount of tokens we want to transfer + /// @param fee The amount to be spent to pay for execution in destination chain + /// @param destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain, to set the + /// weightlimit to Unlimited, you should use the value 0 for ref_time + function transfer_with_fee( + address currencyAddress, + uint256 amount, + uint256 fee, + Multilocation memory destination, + WeightV2 memory weight + ) external returns (bool); + + /// Transfer a token through XCM based on its MultiLocation + /// + /// @dev The token transfer burns/transfers the corresponding amount before sending + /// @param asset The asset we want to transfer, defined by its multilocation. + /// Currently only Concrete Fungible assets + /// @param amount The amount of tokens we want to transfer + /// @param destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain, to set the + /// weightlimit to Unlimited, you should use the value 0 for ref_time + function transfer_multiasset( + Multilocation memory asset, + uint256 amount, + Multilocation memory destination, + WeightV2 memory weight + ) external returns (bool); + + /// Transfer a token through XCM based on its MultiLocation specifying fee + /// + /// @dev The token transfer burns/transfers the corresponding amount before sending + /// @param asset The asset we want to transfer, defined by its multilocation. + /// Currently only Concrete Fungible assets + /// @param amount The amount of tokens we want to transfer + /// @param fee The amount to be spent to pay for execution in destination chain + /// @param destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain, to set the + /// weightlimit to Unlimited, you should use the value 0 for ref_time + function transfer_multiasset_with_fee( + Multilocation memory asset, + uint256 amount, + uint256 fee, + Multilocation memory destination, + WeightV2 memory weight + ) external returns (bool); + + /// Transfer several tokens at once through XCM based on its address specifying fee + /// + /// @dev The token transfer burns/transfers the corresponding amount before sending + /// @param currencies The currencies we want to transfer, defined by their address and amount. + /// @param feeItem Which of the currencies to be used as fee + /// @param destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain, to set the + /// weightlimit to Unlimited, you should use the value 0 for ref_time + function transfer_multi_currencies( + Currency[] memory currencies, + uint32 feeItem, + Multilocation memory destination, + WeightV2 memory weight + ) external returns (bool); + + /// Transfer several tokens at once through XCM based on its location specifying fee + /// + /// @dev The token transfer burns/transfers the corresponding amount before sending + /// @param assets The assets we want to transfer, defined by their location and amount. + /// @param feeItem Which of the currencies to be used as fee + /// @param destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain, to set the + /// weightlimit to Unlimited, you should use the value 0 for ref_time + function transfer_multi_assets( + MultiAsset[] memory assets, + uint32 feeItem, + Multilocation memory destination, + WeightV2 memory weight + ) external returns (bool); + + /** + * @param destination - Multilocation of destination chain where to send this call + * @param xcm_call - encoded xcm call you want to send to destination + **/ + function send_xcm( + Multilocation memory destination, + bytes memory xcm_call + ) external returns (bool); +} diff --git a/precompiles/xcm/src/lib.rs b/precompiles/xcm/src/lib.rs index a8453b36a..b0c14204a 100644 --- a/precompiles/xcm/src/lib.rs +++ b/precompiles/xcm/src/lib.rs @@ -18,26 +18,33 @@ #![cfg_attr(not(feature = "std"), no_std)] +use astar_primitives::xcm::XCM_SIZE_LIMIT; use fp_evm::{PrecompileHandle, PrecompileOutput}; use frame_support::{ dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, pallet_prelude::Weight, - traits::Get, + traits::{ConstU32, Get}, }; +type GetXcmSizeLimit = ConstU32; + use pallet_evm::{AddressMapping, Precompile}; +use parity_scale_codec::DecodeLimit; use sp_core::{H160, H256, U256}; + use sp_std::marker::PhantomData; use sp_std::prelude::*; -use xcm::latest::prelude::*; +use xcm::{latest::prelude::*, VersionedMultiAsset, VersionedMultiAssets, VersionedMultiLocation}; use xcm_executor::traits::Convert; use pallet_evm_precompile_assets_erc20::AddressToAssetId; use precompile_utils::{ - revert, succeed, Address, Bytes, EvmDataWriter, EvmResult, FunctionModifier, - PrecompileHandleExt, RuntimeHelper, + bytes::BoundedBytes, + data::BoundedVec, + revert, succeed, + xcm::{Currency, EvmMultiAsset, WeightV2}, + Address, Bytes, EvmDataWriter, EvmResult, FunctionModifier, PrecompileHandleExt, RuntimeHelper, }; - #[cfg(test)] mod mock; #[cfg(test)] @@ -53,25 +60,58 @@ pub enum Action { "assets_reserve_transfer(address[],uint256[],bytes32,bool,uint256,uint256)", AssetsReserveTransferEvm = "assets_reserve_transfer(address[],uint256[],address,bool,uint256,uint256)", + SendXCM = "send_xcm((uint8,bytes[]),bytes)", + XtokensTransfer = "transfer(address,uint256,(uint8,bytes[]),(uint64,uint64))", + XtokensTransferWithFee = + "transfer_with_fee(address,uint256,uint256,(uint8,bytes[]),(uint64,uint64))", + XtokensTransferMultiasset = + "transfer_multiasset((uint8,bytes[]),uint256,(uint8,bytes[]),(uint64,uint64))", + XtokensTransferMultiassetWithFee = "transfer_multiasset_with_fee((uint8,bytes[]),uint256,uint256,(uint8,bytes[]),(uint64,uint64))", + XtokensTransferMulticurrencies = + "transfer_multi_currencies((address,uint256)[],uint32,(uint8,bytes[]),(uint64,uint64))", + XtokensTransferMultiassets = + "transfet_multi_assets(((uint8,bytes[]),uint256)[],uint32,(uint8,bytes[]),(uint64,uint64))", } /// Dummy H160 address representing native currency (e.g. ASTR or SDN) const NATIVE_ADDRESS: H160 = H160::zero(); +/// Default proof_size of 256KB +const DEFAULT_PROOF_SIZE: u64 = 1024 * 256; + +pub type XBalanceOf = ::Balance; + +pub struct GetMaxAssets(PhantomData); + +impl Get for GetMaxAssets +where + R: orml_xtokens::Config, +{ + fn get() -> u32 { + ::MaxAssetsForTransfer::get() as u32 + } +} /// A precompile that expose XCM related functions. -pub struct XcmPrecompile(PhantomData<(T, C)>); +pub struct XcmPrecompile(PhantomData<(Runtime, C)>); -impl Precompile for XcmPrecompile +impl Precompile for XcmPrecompile where - R: pallet_evm::Config + Runtime: pallet_evm::Config + pallet_xcm::Config + pallet_assets::Config - + AddressToAssetId<::AssetId>, - <::RuntimeCall as Dispatchable>::RuntimeOrigin: - From>, - ::RuntimeCall: - From> + Dispatchable + GetDispatchInfo, - C: Convert::AssetId>, + + orml_xtokens::Config + + AddressToAssetId<::AssetId>, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::AccountId: Into<[u8; 32]>, + ::RuntimeCall: From> + + From> + + Dispatchable + + GetDispatchInfo, + XBalanceOf: TryFrom + Into, + ::CurrencyId: + From<::AssetId>, + C: Convert::AssetId>, { fn execute(handle: &mut impl PrecompileHandle) -> EvmResult { log::trace!(target: "xcm-precompile", "In XCM precompile"); @@ -83,16 +123,25 @@ where // Dispatch the call match selector { Action::AssetsWithdrawNative => { - Self::assets_withdraw(handle, BeneficiaryType::Account32) + Self::assets_withdraw_v1(handle, BeneficiaryType::Account32) } - Action::AssetsWithdrawEvm => Self::assets_withdraw(handle, BeneficiaryType::Account20), - Action::RemoteTransact => Self::remote_transact(handle), + Action::AssetsWithdrawEvm => { + Self::assets_withdraw_v1(handle, BeneficiaryType::Account20) + } + Action::RemoteTransact => Self::remote_transact_v1(handle), Action::AssetsReserveTransferNative => { - Self::assets_reserve_transfer(handle, BeneficiaryType::Account32) + Self::assets_reserve_transfer_v1(handle, BeneficiaryType::Account32) } Action::AssetsReserveTransferEvm => { - Self::assets_reserve_transfer(handle, BeneficiaryType::Account20) + Self::assets_reserve_transfer_v1(handle, BeneficiaryType::Account20) } + Action::SendXCM => Self::send_xcm(handle), + Action::XtokensTransfer => Self::transfer(handle), + Action::XtokensTransferWithFee => Self::transfer_with_fee(handle), + Action::XtokensTransferMultiasset => Self::transfer_multiasset(handle), + Action::XtokensTransferMultiassetWithFee => Self::transfer_multiasset_with_fee(handle), + Action::XtokensTransferMulticurrencies => Self::transfer_multi_currencies(handle), + Action::XtokensTransferMultiassets => Self::transfer_multi_assets(handle), } } } @@ -105,19 +154,26 @@ enum BeneficiaryType { Account20, } -impl XcmPrecompile +impl XcmPrecompile where - R: pallet_evm::Config + Runtime: pallet_evm::Config + pallet_xcm::Config + + orml_xtokens::Config + pallet_assets::Config - + AddressToAssetId<::AssetId>, - <::RuntimeCall as Dispatchable>::RuntimeOrigin: - From>, - ::RuntimeCall: - From> + Dispatchable + GetDispatchInfo, - C: Convert::AssetId>, + + AddressToAssetId<::AssetId>, + <::RuntimeCall as Dispatchable>::RuntimeOrigin: + From>, + ::AccountId: Into<[u8; 32]>, + ::RuntimeCall: From> + + From> + + Dispatchable + + GetDispatchInfo, + XBalanceOf: TryFrom + Into, + ::CurrencyId: + From<::AssetId>, + C: Convert::AssetId>, { - fn assets_withdraw( + fn assets_withdraw_v1( handle: &mut impl PrecompileHandle, beneficiary_type: BeneficiaryType, ) -> EvmResult { @@ -130,7 +186,7 @@ where .iter() .cloned() .filter_map(|address| { - R::address_to_asset_id(address.into()).and_then(|x| C::reverse_ref(x).ok()) + Runtime::address_to_asset_id(address.into()).and_then(|x| C::reverse_ref(x).ok()) }) .collect(); let amounts_raw = input.read::>()?; @@ -188,8 +244,11 @@ where .into(); // Build call with origin. - let origin = Some(R::AddressMapping::into_account_id(handle.context().caller)).into(); - let call = pallet_xcm::Call::::reserve_withdraw_assets { + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + let call = pallet_xcm::Call::::reserve_withdraw_assets { dest: Box::new(dest.into()), beneficiary: Box::new(beneficiary.into()), assets: Box::new(assets.into()), @@ -197,12 +256,12 @@ where }; // Dispatch a call. - RuntimeHelper::::try_dispatch(handle, origin, call)?; + RuntimeHelper::::try_dispatch(handle, origin, call)?; Ok(succeed(EvmDataWriter::new().write(true).build())) } - fn remote_transact(handle: &mut impl PrecompileHandle) -> EvmResult { + fn remote_transact_v1(handle: &mut impl PrecompileHandle) -> EvmResult { let mut input = handle.read_input()?; input.expect_arguments(6)?; @@ -234,7 +293,7 @@ where if address == NATIVE_ADDRESS { Here.into() } else { - let fee_asset_id = R::address_to_asset_id(address) + let fee_asset_id = Runtime::address_to_asset_id(address) .ok_or(revert("Failed to resolve fee asset id from address"))?; C::reverse_ref(fee_asset_id).map_err(|_| { revert("Failed to resolve fee asset multilocation from local id") @@ -247,11 +306,8 @@ where } let fee_amount = fee_amount.low_u128(); - let context = R::UniversalLocation::get(); - let fee_multilocation = MultiAsset { - id: Concrete(fee_asset), - fun: Fungible(fee_amount), - }; + let context = ::UniversalLocation::get(); + let fee_multilocation: MultiAsset = (fee_asset, fee_amount).into(); let fee_multilocation = fee_multilocation .reanchored(&dest, context) .map_err(|_| revert("Failed to reanchor fee asset"))?; @@ -265,7 +321,7 @@ where }, Transact { origin_kind: OriginKind::SovereignAccount, - require_weight_at_most: Weight::from_parts(transact_weight, 0), + require_weight_at_most: Weight::from_parts(transact_weight, DEFAULT_PROOF_SIZE), call: remote_call.into(), }, ]); @@ -273,19 +329,22 @@ where log::trace!(target: "xcm-precompile:remote_transact", "Processed arguments: dest: {:?}, fee asset: {:?}, XCM: {:?}", dest, fee_multilocation, xcm); // Build call with origin. - let origin = Some(R::AddressMapping::into_account_id(handle.context().caller)).into(); - let call = pallet_xcm::Call::::send { + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + let call = pallet_xcm::Call::::send { dest: Box::new(dest.into()), message: Box::new(xcm::VersionedXcm::V3(xcm)), }; // Dispatch a call. - RuntimeHelper::::try_dispatch(handle, origin, call)?; + RuntimeHelper::::try_dispatch(handle, origin, call)?; Ok(succeed(EvmDataWriter::new().write(true).build())) } - fn assets_reserve_transfer( + fn assets_reserve_transfer_v1( handle: &mut impl PrecompileHandle, beneficiary_type: BeneficiaryType, ) -> EvmResult { @@ -304,7 +363,7 @@ where if address == NATIVE_ADDRESS { Some(Here.into()) } else { - R::address_to_asset_id(address).and_then(|x| C::reverse_ref(x).ok()) + Runtime::address_to_asset_id(address).and_then(|x| C::reverse_ref(x).ok()) } }) .collect(); @@ -365,8 +424,11 @@ where .into(); // Build call with origin. - let origin = Some(R::AddressMapping::into_account_id(handle.context().caller)).into(); - let call = pallet_xcm::Call::::reserve_transfer_assets { + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + let call = pallet_xcm::Call::::reserve_transfer_assets { dest: Box::new(dest.into()), beneficiary: Box::new(beneficiary.into()), assets: Box::new(assets.into()), @@ -374,7 +436,337 @@ where }; // Dispatch a call. - RuntimeHelper::::try_dispatch(handle, origin, call)?; + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn send_xcm(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(2)?; + + // Raw call arguments + let dest: MultiLocation = input.read::()?; + let xcm_call: Vec = input.read::>()?.into(); + + log::trace!(target:"xcm-precompile::send_xcm", "Raw arguments: dest: {:?}, xcm_call: {:?}", dest, xcm_call); + + let xcm = xcm::VersionedXcm::<()>::decode_all_with_depth_limit( + xcm::MAX_XCM_DECODE_DEPTH, + &mut xcm_call.as_slice(), + ) + .map_err(|_| revert("Failed to decode xcm instructions"))?; + + // Build call with origin. + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + let call = pallet_xcm::Call::::send { + dest: Box::new(dest.into()), + message: Box::new(xcm), + }; + log::trace!(target: "xcm-send_xcm", "Processed arguments: XCM call: {:?}", call); + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn transfer(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + + // Read call arguments + let currency_address = input.read::
()?; + let amount_of_tokens = input + .read::()? + .try_into() + .map_err(|_| revert("error converting amount_of_tokens, maybe value too large"))?; + let destination = input.read::()?; + let weight = input.read::()?; + + let asset_id = Runtime::address_to_asset_id(currency_address.into()) + .ok_or(revert("Failed to resolve fee asset id from address"))?; + let dest_weight_limit = if weight.is_zero() { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(weight.get_weight()) + }; + + log::trace!(target: "xcm-precompile::transfer", "Raw arguments: currency_address: {:?}, amount_of_tokens: {:?}, destination: {:?}, \ + weight: {:?}, calculated asset_id: {:?}", + currency_address, amount_of_tokens, destination, weight, asset_id); + + let call = orml_xtokens::Call::::transfer { + currency_id: asset_id.into(), + amount: amount_of_tokens, + dest: Box::new(VersionedMultiLocation::V3(destination)), + dest_weight_limit, + }; + + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn transfer_with_fee(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(5)?; + + // Read call arguments + let currency_address = input.read::
()?; + let amount_of_tokens = input + .read::()? + .try_into() + .map_err(|_| revert("error converting amount_of_tokens, maybe value too large"))?; + let fee = input + .read::()? + .try_into() + .map_err(|_| revert("can't convert fee"))?; + + let destination = input.read::()?; + let weight = input.read::()?; + + let asset_id = Runtime::address_to_asset_id(currency_address.into()) + .ok_or(revert("Failed to resolve fee asset id from address"))?; + let dest_weight_limit = if weight.is_zero() { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(weight.get_weight()) + }; + + log::trace!(target: "xcm-precompile::transfer_with_fee", "Raw arguments: currency_address: {:?}, amount_of_tokens: {:?}, destination: {:?}, \ + weight: {:?}, calculated asset_id: {:?}", + currency_address, amount_of_tokens, destination, weight, asset_id); + + let call = orml_xtokens::Call::::transfer_with_fee { + currency_id: asset_id.into(), + amount: amount_of_tokens, + fee, + dest: Box::new(VersionedMultiLocation::V3(destination)), + dest_weight_limit, + }; + + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn transfer_multiasset(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + + // Read call arguments + let asset_location = input.read::()?; + let amount_of_tokens: u128 = input + .read::()? + .try_into() + .map_err(|_| revert("error converting amount_of_tokens, maybe value too large"))?; + let destination = input.read::()?; + let weight = input.read::()?; + + let dest_weight_limit = if weight.is_zero() { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(weight.get_weight()) + }; + + log::trace!(target: "xcm-precompile::transfer_multiasset", "Raw arguments: asset_location: {:?}, amount_of_tokens: {:?}, destination: {:?}, \ + weight: {:?}", + asset_location, amount_of_tokens, destination, weight); + + let call = orml_xtokens::Call::::transfer_multiasset { + asset: Box::new(VersionedMultiAsset::V3( + (asset_location, amount_of_tokens).into(), + )), + dest: Box::new(VersionedMultiLocation::V3(destination)), + dest_weight_limit, + }; + + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn transfer_multiasset_with_fee( + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(5)?; + + // Read call arguments + let asset_location = input.read::()?; + let amount_of_tokens: u128 = input + .read::()? + .try_into() + .map_err(|_| revert("error converting amount_of_tokens, maybe value too large"))?; + let fee: u128 = input + .read::()? + .try_into() + .map_err(|_| revert("can't convert fee"))?; + let destination = input.read::()?; + let weight = input.read::()?; + + let dest_weight_limit = if weight.is_zero() { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(weight.get_weight()) + }; + + log::trace!(target: "xcm-precompile::transfer_multiasset_with_fee", "Raw arguments: asset_location: {:?}, amount_of_tokens: {:?}, fee{:?}, destination: {:?}, \ + weight: {:?}", + asset_location, amount_of_tokens, fee, destination, weight); + + let call = orml_xtokens::Call::::transfer_multiasset_with_fee { + asset: Box::new(VersionedMultiAsset::V3( + (asset_location, amount_of_tokens).into(), + )), + fee: Box::new(VersionedMultiAsset::V3((asset_location, fee).into())), + dest: Box::new(VersionedMultiLocation::V3(destination)), + dest_weight_limit, + }; + + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn transfer_multi_currencies( + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + + let currencies: Vec<_> = input + .read::>>()? + .into(); + let fee_item = input.read::()?; + let destination = input.read::()?; + let weight = input.read::()?; + + let currencies = currencies + .into_iter() + .map(|currency| { + let currency_address: H160 = currency.get_address().into(); + let amount = currency + .get_amount() + .try_into() + .map_err(|_| revert("value too large: in currency"))?; + + Ok(( + Runtime::address_to_asset_id(currency_address.into()) + .ok_or(revert("can't convert into currency id"))? + .into(), + amount, + )) + }) + .collect::>()?; + let dest_weight_limit = if weight.is_zero() { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(weight.get_weight()) + }; + + log::trace!(target: "xcm-precompile::transfer_multi_currencies", "Raw arguments: currencies: {:?}, fee_item{:?}, destination: {:?}, \ + weight: {:?}", + currencies, fee_item, destination, weight); + + let call = orml_xtokens::Call::::transfer_multicurrencies { + currencies, + fee_item, + dest: Box::new(VersionedMultiLocation::V3(destination)), + dest_weight_limit, + }; + + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn transfer_multi_assets(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(4)?; + + let assets: Vec<_> = input + .read::>>()? + .into(); + let fee_item = input.read::()?; + let destination = input.read::()?; + let weight = input.read::()?; + + let dest_weight_limit = if weight.is_zero() { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(weight.get_weight()) + }; + + log::trace!(target: "xcm-precompile::transfer_multi_assets", "Raw arguments: assets: {:?}, fee_item{:?}, destination: {:?}, \ + weight: {:?}", + assets, fee_item, destination, weight); + + let multiasset_vec: EvmResult> = assets + .into_iter() + .map(|evm_multiasset| { + let to_balance: u128 = evm_multiasset + .get_amount() + .try_into() + .map_err(|_| revert("value too large in assets"))?; + Ok((evm_multiasset.get_location(), to_balance).into()) + }) + .collect(); + + // Since multiassets sorts them, we need to check whether the index is still correct, + // and error otherwise as there is not much we can do other than that + let multiassets = + MultiAssets::from_sorted_and_deduplicated(multiasset_vec?).map_err(|_| { + revert("In field Assets, Provided assets either not sorted nor deduplicated") + })?; + + let call = orml_xtokens::Call::::transfer_multiassets { + assets: Box::new(VersionedMultiAssets::V3(multiassets)), + fee_item, + dest: Box::new(VersionedMultiLocation::V3(destination)), + dest_weight_limit, + }; + + let origin = Some(Runtime::AddressMapping::into_account_id( + handle.context().caller, + )) + .into(); + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; Ok(succeed(EvmDataWriter::new().write(true).build())) } diff --git a/precompiles/xcm/src/mock.rs b/precompiles/xcm/src/mock.rs index b2d2ace44..f792866b8 100644 --- a/precompiles/xcm/src/mock.rs +++ b/precompiles/xcm/src/mock.rs @@ -26,7 +26,6 @@ use frame_support::{ traits::{AsEnsureOriginWithArg, ConstU64, Everything, Nothing}, weights::Weight, }; -use frame_system::EnsureRoot; use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; @@ -48,6 +47,9 @@ use xcm_builder::{ AllowTopLevelPaidExecutionFrom, FixedWeightBounds, SignedToAccountId32, TakeWeightCredit, }; use xcm_executor::XcmExecutor; +// orml imports +use orml_traits::location::{RelativeReserveProvider, Reserve}; +use orml_xcm_support::DisabledParachainFee; pub type AccountId = TestAccount; pub type AssetId = u128; @@ -55,6 +57,22 @@ pub type Balance = u128; pub type BlockNumber = u64; pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; pub type Block = frame_system::mocking::MockBlock; +pub type CurrencyId = u128; + +/// Multilocations for assetId +const PARENT: MultiLocation = MultiLocation::parent(); +const PARACHAIN: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::X1(Parachain(10)), +}; +const GENERAL_INDEX: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::X2(Parachain(10), GeneralIndex(20)), +}; +const LOCAL_ASSET: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(GeneralIndex(20)), +}; pub const PRECOMPILE_ADDRESS: H160 = H160::repeat_byte(0x7B); pub const ASSET_PRECOMPILE_ADDRESS_PREFIX: &[u8] = &[255u8; 4]; @@ -149,6 +167,49 @@ impl AddressToAssetId for Runtime { } } +pub struct CurrencyIdToMultiLocation; + +impl sp_runtime::traits::Convert> for CurrencyIdToMultiLocation { + fn convert(currency: CurrencyId) -> Option { + match currency { + 1u128 => Some(PARENT), + 2u128 => Some(PARACHAIN), + 3u128 => Some(GENERAL_INDEX), + 4u128 => Some(LOCAL_ASSET), + _ => None, + } + } +} + +/// Convert `AccountId` to `MultiLocation`. +pub struct AccountIdToMultiLocation; +impl sp_runtime::traits::Convert for AccountIdToMultiLocation { + fn convert(account: AccountId) -> MultiLocation { + X1(AccountId32 { + network: None, + id: account.into(), + }) + .into() + } +} + +/// `MultiAsset` reserve location provider. It's based on `RelativeReserveProvider` and in +/// addition will convert self absolute location to relative location. +pub struct AbsoluteAndRelativeReserveProvider(PhantomData); +impl> Reserve + for AbsoluteAndRelativeReserveProvider +{ + fn reserve(asset: &MultiAsset) -> Option { + RelativeReserveProvider::reserve(asset).map(|reserve_location| { + if reserve_location == AbsoluteLocation::get() { + MultiLocation::here() + } else { + reserve_location + } + }) + } +} + parameter_types! { pub const BlockHashCount: u64 = 250; pub const SS58Prefix: u8 = 42; @@ -184,24 +245,24 @@ impl frame_system::Config for Runtime { #[derive(Debug, Clone, Copy)] pub struct TestPrecompileSet(PhantomData); -impl PrecompileSet for TestPrecompileSet +impl PrecompileSet for TestPrecompileSet where - R: pallet_evm::Config + Runtime: pallet_evm::Config + pallet_xcm::Config + pallet_assets::Config - + AddressToAssetId<::AssetId>, - XcmPrecompile>: Precompile, + + AddressToAssetId<::AssetId>, + XcmPrecompile>: Precompile, { fn execute(&self, handle: &mut impl PrecompileHandle) -> Option { match handle.code_address() { - a if a == PRECOMPILE_ADDRESS => Some( - XcmPrecompile::>::execute(handle), - ), + a if a == PRECOMPILE_ADDRESS => { + Some(XcmPrecompile::>::execute(handle)) + } _ => None, } } - fn is_precompile(&self, address: H160, _gas: u64) -> IsPrecompileResult { + fn is_precompile(&self, address: H160, _remaining_gas: u64) -> IsPrecompileResult { IsPrecompileResult::Answer { is_precompile: address == PRECOMPILE_ADDRESS, extra_cost: 0, @@ -236,8 +297,8 @@ impl pallet_balances::Config for Runtime { type WeightInfo = (); type HoldIdentifier = (); type FreezeIdentifier = (); - type MaxHolds = ConstU32<0>; - type MaxFreezes = ConstU32<0>; + type MaxHolds = (); + type MaxFreezes = (); } // These parameters dont matter much as this will only be called by root with the forced arguments @@ -298,7 +359,7 @@ where parameter_types! { pub const PrecompilesValue: TestPrecompileSet = TestPrecompileSet(PhantomData); - pub WeightPerGas: Weight = Weight::from_parts(1, 0); + pub WeightPerGas: Weight = Weight::from_parts(1,0); } impl pallet_evm::Config for Runtime { @@ -313,7 +374,6 @@ impl pallet_evm::Config for Runtime { type Runner = pallet_evm::runner::stack::Runner; type PrecompilesType = TestPrecompileSet; type PrecompilesValue = PrecompilesValue; - type Timestamp = Timestamp; type ChainId = (); type OnChargeTransaction = (); type BlockGasLimit = (); @@ -322,12 +382,13 @@ impl pallet_evm::Config for Runtime { type OnCreate = (); type WeightInfo = (); type GasLimitPovSizeRatio = ConstU64<4>; + type Timestamp = Timestamp; } parameter_types! { - pub const RelayLocation: MultiLocation = Here.into_location(); + pub RelayNetwork: Option = Some(NetworkId::Polkadot); pub const AnyNetwork: Option = None; - pub UniversalLocation: InteriorMultiLocation = Here; + pub UniversalLocation: InteriorMultiLocation = X2(GlobalConsensus(RelayNetwork::get().unwrap()), Parachain(123)); pub Ancestry: MultiLocation = Here.into(); pub UnitWeightCost: u64 = 1_000; pub const MaxAssetsIntoHolding: u32 = 64; @@ -389,6 +450,14 @@ impl xcm_executor::Config for XcmConfig { parameter_types! { pub static AdvertisedXcmVersion: XcmVersion = 3; + pub const MaxAssetsForTransfer: usize = 2; + pub const SelfLocation: MultiLocation = Here.into_location(); + pub SelfLocationAbsolute: MultiLocation = MultiLocation { + parents: 1, + interior: X1( + Parachain(123) + ) + }; } pub type LocalOriginToLocation = SignedToAccountId32; @@ -457,9 +526,27 @@ impl pallet_xcm::Config for Runtime { type CurrencyMatcher = (); type MaxLockers = frame_support::traits::ConstU32<8>; type WeightInfo = pallet_xcm::TestWeightInfo; + type AdminOrigin = frame_system::EnsureRoot; #[cfg(feature = "runtime-benchmarks")] type ReachableDest = ReachableDest; - type AdminOrigin = EnsureRoot; +} + +impl orml_xtokens::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type Balance = Balance; + type CurrencyId = AssetId; + type CurrencyIdConvert = CurrencyIdToMultiLocation; + type AccountIdToMultiLocation = AccountIdToMultiLocation; + type SelfLocation = SelfLocation; + type XcmExecutor = XcmExecutor; + type Weigher = FixedWeightBounds; + type BaseXcmWeight = UnitWeightCost; + type UniversalLocation = UniversalLocation; + type MaxAssetsForTransfer = MaxAssetsForTransfer; + // Default impl. Refer to `orml-xtokens` docs for more details. + type MinXcmFee = DisabledParachainFee; + type MultiLocationsFilter = Everything; + type ReserveProvider = AbsoluteAndRelativeReserveProvider; } // Configure a mock runtime to test the pallet. @@ -475,6 +562,7 @@ construct_runtime!( Evm: pallet_evm, Timestamp: pallet_timestamp, XcmPallet: pallet_xcm, + Xtokens: orml_xtokens, } ); @@ -492,3 +580,10 @@ impl ExtBuilder { ext } } + +pub(crate) fn events() -> Vec { + System::events() + .into_iter() + .map(|r| r.event) + .collect::>() +} diff --git a/precompiles/xcm/src/tests.rs b/precompiles/xcm/src/tests.rs index 4bfebd3c2..91fa0e7ee 100644 --- a/precompiles/xcm/src/tests.rs +++ b/precompiles/xcm/src/tests.rs @@ -16,267 +16,794 @@ // You should have received a copy of the GNU General Public License // along with Astar. If not, see . -use assert_matches::assert_matches; - use crate::mock::*; use crate::*; +use xcm::latest::{ + AssetId, Fungibility, Junction, Junctions, MultiAsset, MultiAssets, MultiLocation, +}; +use orml_xtokens::Event as XtokensEvent; +use parity_scale_codec::Encode; use precompile_utils::testing::*; use precompile_utils::EvmDataWriter; -use sp_core::H160; +use sp_core::{H160, H256}; +use sp_runtime::traits::Convert; +use xcm::VersionedXcm; fn precompiles() -> TestPrecompileSet { PrecompilesValue::get() } -#[test] -fn wrong_assets_len_or_fee_index_reverts() { - ExtBuilder::default().build().execute_with(|| { - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsWithdrawNative) - .write(vec![Address::from(H160::repeat_byte(0xF1))]) - .write(Vec::::new()) - .write(H256::repeat_byte(0xF1)) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), - ) - .expect_no_logs() - .execute_reverts(|output| output == b"Assets resolution failure."); - - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsWithdrawNative) - .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) - .write(vec![U256::from(42000u64)]) - .write(H256::repeat_byte(0xF1)) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(2_u64)) - .build(), - ) - .expect_no_logs() - .execute_reverts(|output| output == b"Bad fee index."); - }); -} +mod xcm_old_interface_test { + use super::*; + #[test] + fn wrong_assets_len_or_fee_index_reverts() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdrawNative) + .write(vec![Address::from(H160::repeat_byte(0xF1))]) + .write(Vec::::new()) + .write(H256::repeat_byte(0xF1)) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Assets resolution failure."); -#[test] -fn assets_withdraw_works() { - ExtBuilder::default().build().execute_with(|| { - // SS58 - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsWithdrawNative) - .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) - .write(vec![U256::from(42000u64)]) - .write(H256::repeat_byte(0xF1)) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), - ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - - // H160 - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsWithdrawEvm) - .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) - .write(vec![U256::from(42000u64)]) - .write(Address::from(H160::repeat_byte(0xDE))) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), - ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - }); -} + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdrawNative) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(H256::repeat_byte(0xF1)) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(2_u64)) + .build(), + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Bad fee index."); + }); + } -#[test] -fn remote_transact_works() { - ExtBuilder::default().build().execute_with(|| { - // SS58 - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::RemoteTransact) - .write(U256::from(0_u64)) - .write(true) - .write(Address::from(Runtime::asset_id_to_address(1_u128))) - .write(U256::from(367)) - .write(vec![0xff_u8, 0xaa, 0x77, 0x00]) - .write(U256::from(3_000_000_000u64)) - .build(), - ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - }); + #[test] + fn assets_withdraw_works() { + ExtBuilder::default().build().execute_with(|| { + // SS58 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdrawNative) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(H256::repeat_byte(0xF1)) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + // H160 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdrawEvm) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(Address::from(H160::repeat_byte(0xDE))) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + }); + } + + #[test] + fn remote_transact_works() { + ExtBuilder::default().build().execute_with(|| { + // SS58 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::RemoteTransact) + .write(U256::from(0_u64)) + .write(true) + .write(Address::from(Runtime::asset_id_to_address(1_u128))) + .write(U256::from(367)) + .write(vec![0xff_u8, 0xaa, 0x77, 0x00]) + .write(U256::from(3_000_000_000u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + }); + } + + #[test] + fn reserve_transfer_assets_works() { + ExtBuilder::default().build().execute_with(|| { + // SS58 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransferNative) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(H256::repeat_byte(0xF1)) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + // H160 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransferEvm) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(Address::from(H160::repeat_byte(0xDE))) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + }); + + for (location, Xcm(instructions)) in take_sent_xcm() { + assert_eq!( + location, + MultiLocation { + parents: 1, + interior: Here + } + ); + + let non_native_asset = MultiAsset { + fun: Fungible(42000), + id: xcm::v3::AssetId::from(MultiLocation { + parents: 0, + interior: Here, + }), + }; + + assert!(matches!( + instructions.as_slice(), + [ + ReserveAssetDeposited(assets), + ClearOrigin, + BuyExecution { + fees, + .. + }, + DepositAsset { + beneficiary: MultiLocation { + parents: 0, + interior: X1(_), + }, + .. + } + ] + + if fees.contains(&non_native_asset) && assets.contains(&non_native_asset) + )); + } + } + + #[test] + fn reserve_transfer_currency_works() { + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransferNative) + .write(vec![Address::from(H160::zero())]) // zero address by convention + .write(vec![U256::from(42000u64)]) + .write(H256::repeat_byte(0xF1)) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransferEvm) + .write(vec![Address::from(H160::zero())]) // zero address by convention + .write(vec![U256::from(42000u64)]) + .write(Address::from(H160::repeat_byte(0xDE))) + .write(true) + .write(U256::from(0_u64)) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + }); + + for (location, Xcm(instructions)) in take_sent_xcm() { + assert_eq!( + location, + MultiLocation { + parents: 1, + interior: Here + } + ); + + let native_asset = MultiAsset { + fun: Fungible(42000), + id: xcm::v3::AssetId::from(MultiLocation { + parents: 0, + interior: X1(Parachain(123)), + }), + }; + + assert!(matches!( + instructions.as_slice(), + [ + ReserveAssetDeposited(assets), + ClearOrigin, + BuyExecution { + fees, + .. + }, + DepositAsset { + beneficiary: MultiLocation { + parents: 0, + interior: X1(_), + }, + .. + } + ] + if fees.contains(&native_asset) && assets.contains(&native_asset) + )); + } + } + + #[test] + fn test_send_clear_origin() { + ExtBuilder::default().build().execute_with(|| { + let dest: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::AccountId32 { + network: None, + id: H256::repeat_byte(0xF1).into(), + }), + }; + let xcm_to_send = VersionedXcm::<()>::V3(Xcm(vec![ClearOrigin])).encode(); + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::SendXCM) + .write(dest) + .write(Bytes::from(xcm_to_send.as_slice())) + .build(), + ) + // Fixed: TestWeightInfo + (BaseXcmWeight * MessageLen) + .expect_cost(100001000) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let sent_messages = take_sent_xcm(); + let (_, sent_message) = sent_messages.first().unwrap(); + // Lets make sure the message is as expected + assert!(sent_message.0.contains(&ClearOrigin)); + }) + } } -#[test] -fn reserve_transfer_assets_works() { - ExtBuilder::default().build().execute_with(|| { - // SS58 - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsReserveTransferNative) - .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) - .write(vec![U256::from(42000u64)]) - .write(H256::repeat_byte(0xF1)) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), - ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - - // H160 - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsReserveTransferEvm) - .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) - .write(vec![U256::from(42000u64)]) - .write(Address::from(H160::repeat_byte(0xDE))) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), - ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - }); - - for (location, Xcm(instructions)) in take_sent_xcm() { - assert_eq!( - location, - MultiLocation { +mod xcm_new_interface_test { + use super::*; + #[test] + fn xtokens_transfer_works() { + let weight = WeightV2::from(3_000_000_000u64, 1024); + + ExtBuilder::default().build().execute_with(|| { + let parent_destination = MultiLocation { parents: 1, - interior: Here - } - ); + interior: Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), + }; - let non_native_asset = MultiAsset { - fun: Fungible(42000), - id: xcm::v3::AssetId::from(MultiLocation { - parents: 0, - interior: Here, - }), - }; - - assert_matches!( - instructions.as_slice(), - [ - ReserveAssetDeposited(assets), - ClearOrigin, - BuyExecution { - fees, - .. - }, - DepositAsset { - beneficiary: MultiLocation { - parents: 0, - interior: X1(_), + let sibling_parachain_location = MultiLocation { + parents: 1, + interior: Junctions::X2( + Junction::Parachain(10), + Junction::AccountId32 { + network: None, + id: [1u8; 32], }, - .. - } - ] + ), + }; + + // sending relay token back to relay chain + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransfer) + .write(Address::from(Runtime::asset_id_to_address(1u128))) // zero address by convention + .write(U256::from(42000u64)) + .write(parent_destination) + .write(weight.clone()) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected_asset: MultiAsset = MultiAsset { + id: AssetId::Concrete(CurrencyIdToMultiLocation::convert(1).unwrap()), + fun: Fungibility::Fungible(42000), + }; + + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset.clone()].into(), + fee: expected_asset, + dest: parent_destination, + }) + .into(); + assert!(events().contains(&expected)); + + // sending parachain token back to parachain + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransfer) + .write(Address::from(Runtime::asset_id_to_address(2u128))) // zero address by convention + .write(U256::from(42000u64)) + .write(sibling_parachain_location) + .write(weight) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected_asset: MultiAsset = MultiAsset { + id: AssetId::Concrete(CurrencyIdToMultiLocation::convert(2).unwrap()), + fun: Fungibility::Fungible(42000), + }; + + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset.clone()].into(), + fee: expected_asset, + dest: sibling_parachain_location, + }) + .into(); + assert!(events().contains(&expected)); + }); + } + + #[test] + fn xtokens_transfer_with_fee_works() { + let weight = WeightV2::from(3_000_000_000u64, 1024); + ExtBuilder::default().build().execute_with(|| { + let parent_destination = MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), + }; + + // sending relay token back to relay chain + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferWithFee) + .write(Address::from(Runtime::asset_id_to_address(1u128))) // zero address by convention + .write(U256::from(42000u64)) + .write(U256::from(50)) + .write(parent_destination) + .write(weight) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); - if fees.contains(&non_native_asset) && assets.contains(&non_native_asset) + let expected_asset: MultiAsset = MultiAsset { + id: AssetId::Concrete(CurrencyIdToMultiLocation::convert(1).unwrap()), + fun: Fungibility::Fungible(42000), + }; + let expected_fee: MultiAsset = MultiAsset { + id: AssetId::Concrete(CurrencyIdToMultiLocation::convert(1).unwrap()), + fun: Fungibility::Fungible(50), + }; + + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset.clone(), expected_fee.clone()].into(), + fee: expected_fee, + dest: parent_destination, + }) + .into(); + assert!(events().contains(&expected)); + }); + } + + #[test] + fn transfer_multiasset_works() { + let weight = WeightV2::from(3_000_000_000u64, 1024); + ExtBuilder::default().build().execute_with(|| { + let relay_token_location = MultiLocation { + parents: 1, + interior: Junctions::Here, + }; + let relay_destination = MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), + }; + let para_destination = MultiLocation { + parents: 1, + interior: Junctions::X2( + Junction::Parachain(10), + Junction::AccountId32 { + network: None, + id: [1u8; 32], + }, + ), + }; + let native_token_location: MultiLocation = (Here).into(); + + let amount = 4200u64; + // relay token to relay + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMultiasset) + .write(relay_token_location) // zero address by convention + .write(U256::from(amount)) + .write(relay_destination) + .write(weight.clone()) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected_asset: MultiAsset = MultiAsset { + id: AssetId::Concrete(relay_token_location), + fun: Fungibility::Fungible(amount.into()), + }; + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset.clone()].into(), + fee: expected_asset, + dest: relay_destination, + }) + .into(); + + // Assert that the events vector contains the one expected + assert!(events().contains(&expected)); + + // relay to para + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMultiasset) + .write(relay_token_location) // zero address by convention + .write(U256::from(amount)) + .write(para_destination) + .write(weight.clone()) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected_asset: MultiAsset = MultiAsset { + id: AssetId::Concrete(relay_token_location), + fun: Fungibility::Fungible(amount.into()), + }; + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset.clone()].into(), + fee: expected_asset, + dest: para_destination, + }) + .into(); + + // Assert that the events vector contains the one expected + assert!(events().contains(&expected)); + + // native token to para + + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMultiasset) + .write(native_token_location) // zero address by convention + .write(U256::from(amount)) + .write(para_destination) + .write(weight.clone()) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected_asset: MultiAsset = MultiAsset { + id: AssetId::Concrete(native_token_location), + fun: Fungibility::Fungible(amount.into()), + }; + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset.clone()].into(), + fee: expected_asset, + dest: para_destination, + }) + .into(); + + // Assert that the events vector contains the one expected + assert!(events().contains(&expected)); + }); + } + + #[test] + fn transfer_multi_currencies_works() { + let destination = MultiLocation::new( + 1, + Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), ); + + let weight = WeightV2::from(3_000_000_000u64, 1024); + + // NOTE: Currently only support `ToReserve` with relay-chain asset as fee. other case + // like `NonReserve` or `SelfReserve` with relay-chain fee is not support. + let currencies: Vec = vec![ + ( + Address::from(Runtime::asset_id_to_address(2u128)), + U256::from(500), + ) + .into(), + ( + Address::from(Runtime::asset_id_to_address(3u128)), + U256::from(500), + ) + .into(), + ]; + + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMulticurrencies) + .write(currencies) // zero address by convention + .write(U256::from(0)) + .write(destination) + .write(weight) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected_asset_1: MultiAsset = MultiAsset { + id: AssetId::Concrete(CurrencyIdToMultiLocation::convert(2u128).unwrap()), + fun: Fungibility::Fungible(500), + }; + let expected_asset_2: MultiAsset = MultiAsset { + id: AssetId::Concrete(CurrencyIdToMultiLocation::convert(3u128).unwrap()), + fun: Fungibility::Fungible(500), + }; + + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: vec![expected_asset_1.clone(), expected_asset_2].into(), + fee: expected_asset_1, + dest: destination, + }) + .into(); + assert!(events().contains(&expected)); + }); } -} -#[test] -fn reserve_transfer_currency_works() { - ExtBuilder::default().build().execute_with(|| { - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsReserveTransferNative) - .write(vec![Address::from(H160::zero())]) // zero address by convention - .write(vec![U256::from(42000u64)]) - .write(H256::repeat_byte(0xF1)) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), + #[test] + fn transfer_multi_currencies_cannot_insert_more_than_max() { + let destination = MultiLocation::new( + 1, + Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), + ); + let weight = WeightV2::from(3_000_000_000u64, 1024); + // we only allow upto 2 currencies to be transfered + let currencies: Vec = vec![ + ( + Address::from(Runtime::asset_id_to_address(2u128)), + U256::from(500), ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - - precompiles() - .prepare_test( - TestAccount::Alice, - PRECOMPILE_ADDRESS, - EvmDataWriter::new_with_selector(Action::AssetsReserveTransferEvm) - .write(vec![Address::from(H160::zero())]) // zero address by convention - .write(vec![U256::from(42000u64)]) - .write(Address::from(H160::repeat_byte(0xDE))) - .write(true) - .write(U256::from(0_u64)) - .write(U256::from(0_u64)) - .build(), + .into(), + ( + Address::from(Runtime::asset_id_to_address(3u128)), + U256::from(500), ) - .expect_no_logs() - .execute_returns(EvmDataWriter::new().write(true).build()); - }); - - for (location, Xcm(instructions)) in take_sent_xcm() { - assert_eq!( - location, - MultiLocation { - parents: 1, - interior: Here - } + .into(), + ( + Address::from(Runtime::asset_id_to_address(4u128)), + U256::from(500), + ) + .into(), + ]; + + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMulticurrencies) + .write(currencies) // zero address by convention + .write(U256::from(0)) + .write(destination) + .write(weight) + .build(), + ) + .expect_no_logs() + .execute_reverts(|output| { + output == b"value too large : Array has more than max items allowed" + }); + }); + } + + #[test] + fn transfer_multiassets_works() { + let destination = MultiLocation::new( + 1, + Junctions::X2( + Junction::Parachain(2), + Junction::AccountId32 { + network: None, + id: [1u8; 32], + }, + ), ); + let weight = WeightV2::from(3_000_000_000u64, 1024); - let native_asset = MultiAsset { - fun: Fungible(42000), - id: xcm::v3::AssetId::from(MultiLocation { - parents: 0, - interior: X1(OnlyChild), - }), - }; - - assert_matches!( - instructions.as_slice(), - [ - ReserveAssetDeposited(assets), - ClearOrigin, - BuyExecution { - fees, - .. + let asset_1_location = MultiLocation::new( + 1, + Junctions::X2(Junction::Parachain(2), Junction::GeneralIndex(0u128)), + ); + let asset_2_location = MultiLocation::new( + 1, + Junctions::X2(Junction::Parachain(2), Junction::GeneralIndex(1u128)), + ); + + let assets: Vec = vec![ + (asset_1_location.clone(), U256::from(500)).into(), + (asset_2_location.clone(), U256::from(500)).into(), + ]; + + let multiassets = MultiAssets::from_sorted_and_deduplicated(vec![ + (asset_1_location.clone(), 500).into(), + (asset_2_location, 500).into(), + ]) + .unwrap(); + + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMultiassets) + .write(assets) // zero address by convention + .write(U256::from(0)) + .write(destination) + .write(weight) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let expected: crate::mock::RuntimeEvent = + mock::RuntimeEvent::Xtokens(XtokensEvent::TransferredMultiAssets { + sender: TestAccount::Alice.into(), + assets: multiassets, + fee: (asset_1_location, 500).into(), + dest: destination, + }) + .into(); + assert!(events().contains(&expected)); + }); + } + + #[test] + fn transfer_multiassets_cannot_insert_more_than_max() { + // We have definaed MaxAssetsForTransfer = 2, + // so any number greater than MaxAssetsForTransfer will result in error + let destination = MultiLocation::new( + 1, + Junctions::X2( + Junction::Parachain(2), + Junction::AccountId32 { + network: None, + id: [1u8; 32], }, - DepositAsset { - beneficiary: MultiLocation { - parents: 0, - interior: X1(_), - }, - .. - } - ] + ), + ); + let weight = WeightV2::from(3_000_000_000u64, 1024); - if fees.contains(&native_asset) && assets.contains(&native_asset) + let asset_1_location = MultiLocation::new( + 1, + Junctions::X2(Junction::Parachain(2), Junction::GeneralIndex(0u128)), + ); + let asset_2_location = MultiLocation::new( + 1, + Junctions::X2(Junction::Parachain(2), Junction::GeneralIndex(1u128)), ); + let asset_3_location = MultiLocation::new( + 1, + Junctions::X2(Junction::Parachain(2), Junction::GeneralIndex(3u128)), + ); + + let assets: Vec = vec![ + (asset_1_location.clone(), U256::from(500)).into(), + (asset_2_location.clone(), U256::from(500)).into(), + (asset_3_location.clone(), U256::from(500)).into(), + ]; + + ExtBuilder::default().build().execute_with(|| { + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::XtokensTransferMultiassets) + .write(assets) // zero address by convention + .write(U256::from(0)) + .write(destination) + .write(weight) + .build(), + ) + .expect_no_logs() + .execute_reverts(|output| { + output == b"value too large : Array has more than max items allowed" + }); + }); } } diff --git a/primitives/src/xcm/mod.rs b/primitives/src/xcm/mod.rs index e59e01283..a0945c753 100644 --- a/primitives/src/xcm/mod.rs +++ b/primitives/src/xcm/mod.rs @@ -30,8 +30,6 @@ //! Please refer to implementation below for more info. //! -#![cfg_attr(not(feature = "std"), no_std)] - use crate::AccountId; use frame_support::{ @@ -54,6 +52,8 @@ use pallet_xc_asset_config::{ExecutionPaymentRate, XcAssetLocation}; #[cfg(test)] mod tests; +pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16); + /// Used to convert between cross-chain asset multilocation and local asset Id. /// /// This implementation relies on `XcAssetConfig` pallet to handle mapping.