diff --git a/Cargo.toml b/Cargo.toml index 3eb8697f..a469b907 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,6 +127,9 @@ xcm = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.3 xcm-executor = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.39", default-features = false } polkadot-parachain = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.39", default-features = false } xcm-builder = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.39", default-features = false } +orml-xtokens = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.39", default-features = false } +orml-xcm-support = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.39", default-features = false } +orml-traits = { git = "https://github.com/open-web3-stack/open-runtime-module-library", branch = "polkadot-v0.9.39", default-features = false } # (native) polkadot-runtime-parachains = { git = "https://github.com/paritytech/polkadot", branch = "release-v0.9.39" } diff --git a/precompiles/utils/src/bytes.rs b/precompiles/utils/src/bytes.rs new file mode 100644 index 00000000..047b97bf --- /dev/null +++ b/precompiles/utils/src/bytes.rs @@ -0,0 +1,220 @@ +// 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 . + +use super::*; +use alloc::borrow::ToOwned; +pub use alloc::string::String; +use sp_core::{ConstU32, Get}; + +type ConstU32Max = ConstU32<{ u32::MAX }>; + +pub type UnboundedBytes = BoundedBytesString; +pub type BoundedBytes = BoundedBytesString; + +trait Kind { + fn signature() -> String; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BytesKind; + +impl Kind for BytesKind { + fn signature() -> String { + String::from("bytes") + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct StringKind; + +impl Kind for StringKind { + fn signature() -> String { + String::from("string") + } +} + +/// The `bytes/string` type of Solidity. +/// It is different from `Vec` which will be serialized with padding for each `u8` element +/// of the array, while `Bytes` is tightly packed. +#[derive(Debug)] +pub struct BoundedBytesString { + data: Vec, + _phantom: PhantomData<(K, S)>, +} + +impl> Clone for BoundedBytesString { + fn clone(&self) -> Self { + Self { + data: self.data.clone(), + _phantom: PhantomData, + } + } +} + +impl PartialEq> for BoundedBytesString { + fn eq(&self, other: &BoundedBytesString) -> bool { + self.data.eq(&other.data) + } +} + +impl Eq for BoundedBytesString {} + +impl> BoundedBytesString { + pub fn as_bytes(&self) -> &[u8] { + &self.data + } + + pub fn as_str(&self) -> Result<&str, sp_std::str::Utf8Error> { + sp_std::str::from_utf8(&self.data) + } +} + +impl> EvmData for BoundedBytesString { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let mut inner_reader = reader.read_pointer()?; + + // Read bytes/string size. + let array_size: usize = inner_reader + .read::() + .map_err(|_| revert("length, out of bounds"))? + .try_into() + .map_err(|_| revert("length, value too large"))?; + + if array_size > S::get() as usize { + return Err(revert("length, value too large").into()); + } + + // Get valid range over the bytes data. + let range = inner_reader.move_cursor(array_size)?; + + let data = inner_reader + .get_input_from_range(range) + .ok_or_else(|| revert(K::signature()))?; + + let bytes = Self { + data: data.to_owned(), + _phantom: PhantomData, + }; + + Ok(bytes) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + let value: Vec<_> = value.into(); + let length = value.len(); + + // Pad the data. + // Leave it as is if a multiple of 32, otherwise pad to next + // multiple or 32. + let chunks = length / 32; + let padded_size = match length % 32 { + 0 => chunks * 32, + _ => (chunks + 1) * 32, + }; + + let mut value = value.to_vec(); + value.resize(padded_size, 0); + + writer.write_pointer( + EvmDataWriter::new() + .write(U256::from(length)) + .write_raw_bytes(&value) + .build(), + ); + } + + fn has_static_size() -> bool { + false + } +} + +// BytesString <=> Vec/&[u8] + +impl From> for Vec { + fn from(value: BoundedBytesString) -> Self { + value.data + } +} + +impl From> for BoundedBytesString { + fn from(value: Vec) -> Self { + Self { + data: value, + _phantom: PhantomData, + } + } +} + +impl From<&[u8]> for BoundedBytesString { + fn from(value: &[u8]) -> Self { + Self { + data: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From<[u8; N]> for BoundedBytesString { + fn from(value: [u8; N]) -> Self { + Self { + data: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From<&[u8; N]> for BoundedBytesString { + fn from(value: &[u8; N]) -> Self { + Self { + data: value.to_vec(), + _phantom: PhantomData, + } + } +} + +// BytesString <=> String/str + +impl TryFrom> for String { + type Error = alloc::string::FromUtf8Error; + + fn try_from(value: BoundedBytesString) -> Result { + alloc::string::String::from_utf8(value.data) + } +} + +impl From<&str> for BoundedBytesString { + fn from(value: &str) -> Self { + Self { + data: value.as_bytes().into(), + _phantom: PhantomData, + } + } +} + +impl From for BoundedBytesString { + fn from(value: String) -> Self { + Self { + data: value.as_bytes().into(), + _phantom: PhantomData, + } + } +} diff --git a/precompiles/utils/src/data.rs b/precompiles/utils/src/data.rs index 2359bcef..75f32c91 100644 --- a/precompiles/utils/src/data.rs +++ b/precompiles/utils/src/data.rs @@ -23,9 +23,9 @@ use crate::{revert, EvmResult}; use alloc::borrow::ToOwned; -use core::{any::type_name, ops::Range}; +use core::{any::type_name, marker::PhantomData, ops::Range}; use impl_trait_for_tuples::impl_for_tuples; -use sp_core::{H160, H256, U256}; +use sp_core::{Get, H160, H256, U256}; use sp_std::{convert::TryInto, vec, vec::Vec}; /// The `address` type of Solidity. @@ -175,6 +175,11 @@ impl<'a> EvmDataReader<'a> { }) } + /// Return Option<&[u8]> from a given range for EvmDataReader + pub fn get_input_from_range(&self, range: Range) -> Option<&[u8]> { + self.input.get(range) + } + /// Read remaining bytes pub fn read_till_end(&mut self) -> EvmResult<&[u8]> { let range = self.move_cursor(self.input.len() - self.cursor)?; @@ -190,7 +195,7 @@ impl<'a> EvmDataReader<'a> { /// Move the reading cursor with provided length, and return a range from the previous cursor /// location to the new one. /// Checks cursor overflows. - fn move_cursor(&mut self, len: usize) -> EvmResult> { + pub fn move_cursor(&mut self, len: usize) -> EvmResult> { let start = self.cursor; let end = self .cursor @@ -285,7 +290,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 } @@ -604,3 +609,104 @@ impl EvmData for Bytes { false } } +/// Wrapper around a Vec that provides a max length bound on read. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BoundedVec { + inner: Vec, + _phantom: PhantomData, +} + +impl> EvmData for BoundedVec { + fn read(reader: &mut EvmDataReader) -> EvmResult { + let mut inner_reader = reader.read_pointer()?; + + let array_size: usize = inner_reader + .read::() + .map_err(|_| revert("out of bounds: length of array"))? + .try_into() + .map_err(|_| revert("value too large : Array has more than max items allowed"))?; + + if array_size > S::get() as usize { + return Err(revert("value too large : Array has more than max items allowed").into()); + } + + let mut array = vec![]; + + let mut item_reader = EvmDataReader { + input: inner_reader + .input + .get(32..) + .ok_or_else(|| revert("read out of bounds: array content"))?, + cursor: 0, + }; + + for _ in 0..array_size { + array.push(item_reader.read()?); + } + + Ok(BoundedVec { + inner: array, + _phantom: PhantomData, + }) + } + + fn write(writer: &mut EvmDataWriter, value: Self) { + let value: Vec<_> = value.into(); + let mut inner_writer = EvmDataWriter::new().write(U256::from(value.len())); + + for inner in value { + // Any offset in items are relative to the start of the item instead of the + // start of the array. However if there is offseted data it must but appended after + // all items (offsets) are written. We thus need to rely on `compute_offsets` to do + // that, and must store a "shift" to correct the offsets. + let shift = inner_writer.data.len(); + let item_writer = EvmDataWriter::new().write(inner); + + inner_writer = inner_writer.write_raw_bytes(&item_writer.data); + for mut offset_datum in item_writer.offset_data { + offset_datum.offset_shift += 32; + offset_datum.offset_position += shift; + inner_writer.offset_data.push(offset_datum); + } + } + + writer.write_pointer(inner_writer.build()); + } + + fn has_static_size() -> bool { + false + } +} + +impl From> for BoundedVec { + fn from(value: Vec) -> Self { + BoundedVec { + inner: value, + _phantom: PhantomData, + } + } +} + +impl From<&[T]> for BoundedVec { + fn from(value: &[T]) -> Self { + BoundedVec { + inner: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From<[T; N]> for BoundedVec { + fn from(value: [T; N]) -> Self { + BoundedVec { + inner: value.to_vec(), + _phantom: PhantomData, + } + } +} + +impl From> for Vec { + fn from(value: BoundedVec) -> Self { + value.inner + } +} diff --git a/precompiles/utils/src/lib.rs b/precompiles/utils/src/lib.rs index f36412a2..ab27e200 100644 --- a/precompiles/utils/src/lib.rs +++ b/precompiles/utils/src/lib.rs @@ -37,7 +37,9 @@ use pallet_evm::{GasWeightMapping, Log}; use sp_core::{H160, H256, U256}; use sp_std::{marker::PhantomData, vec, vec::Vec}; -mod data; +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/tests.rs b/precompiles/utils/src/tests.rs index 6756687b..57938fac 100644 --- a/precompiles/utils/src/tests.rs +++ b/precompiles/utils/src/tests.rs @@ -20,9 +20,15 @@ // You should have received a copy of the GNU General Public License // along with Utils. If not, see . +use crate::bytes::UnboundedBytes; + use super::*; use hex_literal::hex; use sp_core::{H256, U256}; +use { + crate::xcm::{network_id_from_bytes, network_id_to_bytes}, + ::xcm::latest::{Junction, Junctions, NetworkId}, +}; fn u256_repeat_byte(byte: u8) -> U256 { let value = H256::repeat_byte(byte); @@ -506,6 +512,19 @@ fn read_bytes() { assert_eq!(data, parsed.as_bytes()); } +#[test] +fn read_unbounded_bytes() { + let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\ + tempor incididunt ut labore et dolore magna aliqua."; + let writer_output = EvmDataWriter::new() + .write(UnboundedBytes::from(&data[..])) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: UnboundedBytes = reader.read().expect("to correctly parse Bytes"); + + assert_eq!(data, parsed.as_bytes()); +} #[test] fn write_bytes() { let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\ @@ -529,6 +548,31 @@ fn write_bytes() { assert_eq!(read("read part 3"), H256::from_slice(&padded[0x40..0x60])); assert_eq!(read("read part 4"), H256::from_slice(&padded[0x60..0x80])); } +#[test] +fn write_unbounded_bytes() { + let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\ + tempor incididunt ut labore et dolore magna aliqua."; + + let writer_output = EvmDataWriter::new() + .write(UnboundedBytes::from(&data[..])) + .build(); + + // We can read this "manualy" using simpler functions. + let mut reader = EvmDataReader::new(&writer_output); + + // We pad data to a multiple of 32 bytes. + let mut padded = data.to_vec(); + assert!(data.len() < 0x80); + padded.resize(0x80, 0); + + assert_eq!(reader.read::().expect("read offset"), 32.into()); + assert_eq!(reader.read::().expect("read size"), data.len().into()); + let mut read = |e| reader.read::().expect(e); // shorthand + assert_eq!(read("read part 1"), H256::from_slice(&padded[0x00..0x20])); + assert_eq!(read("read part 2"), H256::from_slice(&padded[0x20..0x40])); + assert_eq!(read("read part 3"), H256::from_slice(&padded[0x40..0x60])); + assert_eq!(read("read part 4"), H256::from_slice(&padded[0x60..0x80])); +} #[test] fn read_string() { @@ -618,7 +662,61 @@ fn write_vec_bytes() { assert_eq!(read("read part 3"), H256::from_slice(&padded[0x40..0x60])); assert_eq!(read("read part 4"), H256::from_slice(&padded[0x60..0x80])); } +#[test] +fn write_vec_unbounded_bytes() { + let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\ + tempor incididunt ut labore et dolore magna aliqua."; + let writer_output = EvmDataWriter::new() + .write(vec![ + UnboundedBytes::from(&data[..]), + UnboundedBytes::from(&data[..]), + ]) + .build(); + + writer_output + .chunks_exact(32) + .map(|chunk| H256::from_slice(chunk)) + .for_each(|hash| println!("{:?}", hash)); + + // We pad data to a multiple of 32 bytes. + let mut padded = data.to_vec(); + assert!(data.len() < 0x80); + padded.resize(0x80, 0); + + let mut reader = EvmDataReader::new(&writer_output); + + // Offset of vec + assert_eq!(reader.read::().expect("read offset"), 32.into()); + + // Length of vec + assert_eq!(reader.read::().expect("read offset"), 2.into()); + + // Relative offset of first bytgmes object + assert_eq!(reader.read::().expect("read offset"), 0x40.into()); + // Relative offset of second bytes object + assert_eq!(reader.read::().expect("read offset"), 0xe0.into()); + + // Length of first bytes object + assert_eq!(reader.read::().expect("read size"), data.len().into()); + + // First byte objects data + let mut read = |e| reader.read::().expect(e); // shorthand + assert_eq!(read("read part 1"), H256::from_slice(&padded[0x00..0x20])); + assert_eq!(read("read part 2"), H256::from_slice(&padded[0x20..0x40])); + assert_eq!(read("read part 3"), H256::from_slice(&padded[0x40..0x60])); + assert_eq!(read("read part 4"), H256::from_slice(&padded[0x60..0x80])); + + // Length of second bytes object + assert_eq!(reader.read::().expect("read size"), data.len().into()); + + // Second byte objects data + let mut read = |e| reader.read::().expect(e); // shorthand + assert_eq!(read("read part 1"), H256::from_slice(&padded[0x00..0x20])); + assert_eq!(read("read part 2"), H256::from_slice(&padded[0x20..0x40])); + assert_eq!(read("read part 3"), H256::from_slice(&padded[0x40..0x60])); + assert_eq!(read("read part 4"), H256::from_slice(&padded[0x60..0x80])); +} #[test] fn read_vec_of_bytes() { let data = b"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod\ @@ -744,3 +842,167 @@ fn read_complex_solidity_function() { // weight assert_eq!(reader.read::().unwrap(), 100u32.into()); } + +#[test] +fn read_dynamic_size_tuple() { + // (uint8, bytes[]) encoded by web3 + let data = hex!( + "0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000001 + 0100000000000000000000000000000000000000000000000000000000000000" + ); + + let mut reader = EvmDataReader::new(&data); + + assert_eq!( + reader.read::<(u8, Vec)>().unwrap(), + (1, vec![UnboundedBytes::from(vec![0x01])]) + ); +} +#[test] +fn junctions_decoder_works() { + let writer_output = EvmDataWriter::new() + .write(Junctions::X1(Junction::OnlyChild)) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junctions = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!(parsed, Junctions::X1(Junction::OnlyChild)); + + let writer_output = EvmDataWriter::new() + .write(Junctions::X2(Junction::OnlyChild, Junction::OnlyChild)) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junctions = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!( + parsed, + Junctions::X2(Junction::OnlyChild, Junction::OnlyChild) + ); + + let writer_output = EvmDataWriter::new() + .write(Junctions::X3( + Junction::OnlyChild, + Junction::OnlyChild, + Junction::OnlyChild, + )) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junctions = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!( + parsed, + Junctions::X3( + Junction::OnlyChild, + Junction::OnlyChild, + Junction::OnlyChild + ), + ); +} + +#[test] +fn junction_decoder_works() { + let writer_output = EvmDataWriter::new().write(Junction::Parachain(0)).build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junction = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!(parsed, Junction::Parachain(0)); + + let writer_output = EvmDataWriter::new() + .write(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junction = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!( + parsed, + Junction::AccountId32 { + network: None, + id: [1u8; 32], + } + ); + + let writer_output = EvmDataWriter::new() + .write(Junction::AccountIndex64 { + network: None, + index: u64::from_be_bytes([1u8; 8]), + }) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junction = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!( + parsed, + Junction::AccountIndex64 { + network: None, + index: u64::from_be_bytes([1u8; 8]), + } + ); + + let writer_output = EvmDataWriter::new() + .write(Junction::AccountKey20 { + network: None, + key: H160::repeat_byte(0xAA).as_bytes().try_into().unwrap(), + }) + .build(); + + let mut reader = EvmDataReader::new(&writer_output); + let parsed: Junction = reader + .read::() + .expect("to correctly parse Junctions"); + + assert_eq!( + parsed, + Junction::AccountKey20 { + network: None, + key: H160::repeat_byte(0xAA).as_bytes().try_into().unwrap(), + } + ); +} + +#[test] +fn network_id_decoder_works() { + assert_eq!(network_id_from_bytes(network_id_to_bytes(None)), Ok(None)); + + let mut name = [0u8; 32]; + name[0..6].copy_from_slice(b"myname"); + assert_eq!( + network_id_from_bytes(network_id_to_bytes(Some(NetworkId::ByGenesis(name)))), + Ok(Some(NetworkId::ByGenesis(name))) + ); + + assert_eq!( + network_id_from_bytes(network_id_to_bytes(Some(NetworkId::Kusama))), + Ok(Some(NetworkId::Kusama)) + ); + + assert_eq!( + network_id_from_bytes(network_id_to_bytes(Some(NetworkId::Polkadot))), + Ok(Some(NetworkId::Polkadot)) + ); +} diff --git a/precompiles/utils/src/xcm.rs b/precompiles/utils/src/xcm.rs new file mode 100644 index 00000000..96942a4b --- /dev/null +++ b/precompiles/utils/src/xcm.rs @@ -0,0 +1,428 @@ +// 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 { + crate::{bytes::*, revert, EvmData, EvmDataReader, EvmDataWriter, EvmResult}, + frame_support::{ensure, 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() + } +} + +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 b1ee2739..684ae0e4 100644 --- a/precompiles/xcm/Cargo.toml +++ b/precompiles/xcm/Cargo.toml @@ -28,6 +28,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 } @@ -61,5 +64,10 @@ std = [ "sp-io/std", "xcm/std", "xcm-executor/std", + "orml-xtokens/std", + "orml-xcm-support/std", + "orml-traits/std", +] +runtime-benchmarks = [ + "orml-xtokens/runtime-benchmarks", ] -runtime-benchmarks = [] diff --git a/precompiles/xcm/XCM.sol b/precompiles/xcm/XCM.sol index 19af7ae9..cc25f751 100644 --- a/precompiles/xcm/XCM.sol +++ b/precompiles/xcm/XCM.sol @@ -1,8 +1,13 @@ + /** + * DISCLAIMER: Please note that this file is deprecated and users are advised to use the XCM_v2.sol file instead. + */ + pragma solidity ^0.8.0; /** * @title XCM interface. */ + interface XCM { /** * @dev Withdraw assets using PalletXCM call. diff --git a/precompiles/xcm/XCM_v2.sol b/precompiles/xcm/XCM_v2.sol new file mode 100644 index 00000000..24233a75 --- /dev/null +++ b/precompiles/xcm/XCM_v2.sol @@ -0,0 +1,80 @@ +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; + } + + /** + * @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 beneficiary - Multilocation of beneficiary in respect to destination parachain + * @param destination - Multilocation of destination chain + * @param fee_index - index of asset_id item that should be used as a XCM fee + * @return bool confirmation whether the XCM message sent. + * + * How method check that assets list is valid: + * - all assets resolved to multi-location (on runtime level) + * - all assets has corresponded amount (lenght of assets list matched to amount list) + */ + function assets_withdraw( + address[] calldata asset_id, + uint256[] calldata asset_amount, + Multilocation memory beneficiary, + Multilocation memory destination, + uint256 fee_index + ) external returns (bool); + + /** + * @dev Execute a transaction on a remote chain. + * @param destination - Multilocation of destination chain + * @param payment_asset_id - ETH address of the local asset derivate used to pay for execution in the destination chain + * @param payment_amount - amount of payment asset to use for execution payment - should cover cost of XCM instructions + Transact call weight. + * @param call - encoded call data (must be decodable by remote chain) + * @param transact_weight - max weight that the encoded call is allowed to consume in the destination chain + * @return bool confirmation whether the XCM message sent. + */ + function remote_transact( + Multilocation memory destination, + address payment_asset_id, + uint256 payment_amount, + bytes calldata call, + uint64 transact_weight + ) 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 beneficiary - Multilocation of beneficiary in respect to destination parachain + * @param destination - Multilocation of destination chain + * @param fee_index - index of asset_id item that should be used as a XCM fee + * @return A boolean confirming whether the XCM message sent. + * How method check that assets list is valid: + * - all assets resolved to multi-location (on runtime level) + * - all assets has corresponded amount (lenght of assets list matched to amount list) + */ + function assets_reserve_transfer( + address[] calldata asset_id, + uint256[] calldata asset_amount, + Multilocation memory beneficiary, + Multilocation memory destination, + uint256 fee_index + ) external returns (bool); + + /** + * @dev send xcm using PalletXCM call. + * @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); +} \ No newline at end of file diff --git a/precompiles/xcm/Xtokens.sol b/precompiles/xcm/Xtokens.sol new file mode 100644 index 00000000..2d082890 --- /dev/null +++ b/precompiles/xcm/Xtokens.sol @@ -0,0 +1,114 @@ + +pragma solidity ^0.8.0; + + +/** + * @title Xtokens interface. + */ +interface Xtokens { + // A multilocation is defined by its number of parents and the encoded junctions (interior) + struct Multilocation { + uint8 parents; + bytes[] interior; + } + + // 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 currencyId + /// + /// @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 + function transfer( + address currencyAddress, + uint256 amount, + Multilocation memory destination, + uint64 weight + ) external returns (bool); + + /// Transfer a token through XCM based on its currencyId 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 destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain + function transfer_with_fee( + address currencyAddress, + uint256 amount, + uint256 fee, + Multilocation memory destination, + uint64 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 + function transfer_multiasset( + Multilocation memory asset, + uint256 amount, + Multilocation memory destination, + uint64 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 destination The Multilocation to which we want to send the tokens + /// @param weight The weight we want to buy in the destination chain + function transfer_multiasset_with_fee( + Multilocation memory asset, + uint256 amount, + uint256 fee, + Multilocation memory destination, + uint64 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 + function transfer_multi_currencies( + Currency[] memory currencies, + uint32 feeItem, + Multilocation memory destination, + uint64 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 + function transfer_multi_assets( + MultiAsset[] memory assets, + uint32 feeItem, + Multilocation memory destination, + uint64 weight + ) external returns (bool); +} diff --git a/precompiles/xcm/src/lib.rs b/precompiles/xcm/src/lib.rs index 985efcdf..016fa56c 100644 --- a/precompiles/xcm/src/lib.rs +++ b/precompiles/xcm/src/lib.rs @@ -23,22 +23,29 @@ use fp_evm::{PrecompileHandle, PrecompileOutput}; use frame_support::{ dispatch::{Dispatchable, GetDispatchInfo, PostDispatchInfo}, pallet_prelude::Weight, - traits::Get, + traits::{ConstU32, Get}, }; +pub const XCM_SIZE_LIMIT: u32 = 2u32.pow(16); +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}, + Address, Bytes, EvmDataWriter, EvmResult, FunctionModifier, PrecompileHandleExt, RuntimeHelper, }; - #[cfg(test)] mod mock; #[cfg(test)] @@ -49,30 +56,66 @@ mod tests; pub enum Action { AssetsWithdrawNative = "assets_withdraw(address[],uint256[],bytes32,bool,uint256,uint256)", AssetsWithdrawEvm = "assets_withdraw(address[],uint256[],address,bool,uint256,uint256)", - RemoteTransact = "remote_transact(uint256,bool,address,uint256,bytes,uint64)", + RemoteTransactOld = "remote_transact(uint256,bool,address,uint256,bytes,uint64)", AssetsReserveTransferNative = "assets_reserve_transfer(address[],uint256[],bytes32,bool,uint256,uint256)", AssetsReserveTransferEvm = "assets_reserve_transfer(address[],uint256[],address,bool,uint256,uint256)", + AssetsWithdraw = "assets_withdraw(address[],uint256[],(uint8,bytes[]),(uint8,bytes[]),uint256)", + RemoteTransactNew = "remote_transact((uint8,bytes[]),address,uint256,bytes,uint64)", + AssetsReserveTransfer = + "assets_reserve_transfer(address[],uint256[],(uint8,bytes[]),(uint8,bytes[]),uint256)", + SendXCM = "send_xcm((uint8,bytes[]),bytes)", + XtokensTransfer = "transfer(address,uint256,(uint8,bytes[]),uint64)", + XtokensTransferWithFee = "transfer_with_fee(address,uint256,uint256,(uint8,bytes[]),uint64)", + XtokensTransferMultiasset = + "transfer_multiasset((uint8,bytes[]),uint256,(uint8,bytes[]),uint64)", + XtokensTransferMultiassetWithFee = + "transfer_multiasset_with_fee((uint8,bytes[]),uint256,uint256,(uint8,bytes[]),uint64)", + XtokensTransferMulticurrencies = + "transfer_multi_currencies((address,uint256)[],uint32,(uint8,bytes[]),uint64)", + XtokensTransferMultiassets = + "transfet_multi_assets(((uint8,bytes[]),uint256)[],uint32,(uint8,bytes[]),uint64)", } /// Dummy H160 address representing native currency (e.g. ASTR or SDN) const NATIVE_ADDRESS: H160 = H160::zero(); +/// Dummy default 64KB +const DEFAULT_PROOF_SIZE: u64 = 1024 * 64; + +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)>); -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"); @@ -84,16 +127,28 @@ 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_v1(handle, BeneficiaryType::Account20) } - Action::AssetsWithdrawEvm => Self::assets_withdraw(handle, BeneficiaryType::Account20), - Action::RemoteTransact => Self::remote_transact(handle), + Action::RemoteTransactOld => 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::AssetsWithdraw => Self::assets_withdraw(handle), + Action::RemoteTransactNew => Self::remote_transact(handle), + Action::AssetsReserveTransfer => Self::assets_reserve_transfer(handle), + 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), } } } @@ -106,19 +161,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 { @@ -131,7 +193,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::>()?; @@ -189,8 +251,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()), @@ -198,12 +263,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)?; @@ -235,7 +300,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") @@ -248,7 +313,7 @@ where } let fee_amount = fee_amount.low_u128(); - let context = R::UniversalLocation::get(); + let context = ::UniversalLocation::get(); let fee_multilocation = MultiAsset { id: Concrete(fee_asset), fun: Fungible(fee_amount), @@ -274,19 +339,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 { @@ -305,7 +373,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(); @@ -366,8 +434,222 @@ 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()), + fee_asset_item, + }; + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn assets_withdraw(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(6)?; + + // Read arguments and check it + let assets: Vec = input + .read::>()? + .iter() + .cloned() + .filter_map(|address| { + Runtime::address_to_asset_id(address.into()).and_then(|x| C::reverse_ref(x).ok()) + }) + .collect(); + let amounts_raw = input.read::>()?; + if amounts_raw.iter().any(|x| *x > u128::MAX.into()) { + return Err(revert("Asset amount is too big")); + } + let amounts: Vec = amounts_raw.iter().map(|x| x.low_u128()).collect(); + + // Check that assets list is valid: + // * all assets resolved to multi-location + // * all assets has corresponded amount + if assets.len() != amounts.len() || assets.is_empty() { + return Err(revert("Assets resolution failure.")); + } + + let beneficiary: MultiLocation = input.read::()?; + let dest: MultiLocation = input.read::()?; + + let fee_asset_item: u32 = input.read::()?.low_u32(); + + log::trace!(target: "xcm-precompile::asset_withdraw", "Raw arguments: assets: {:?}, asset_amount: {:?} \ + beneficiart: {:?}, destination: {:?}, fee_index: {}", + assets, amounts_raw, beneficiary, dest, fee_asset_item); + + if fee_asset_item as usize > assets.len() { + return Err(revert("Bad fee index.")); + } + + let assets: MultiAssets = assets + .iter() + .cloned() + .zip(amounts.iter().cloned()) + .map(Into::into) + .collect::>() + .into(); + + // Build call with origin. + 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()), + fee_asset_item, + }; + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn remote_transact(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(6)?; + + let dest: MultiLocation = input.read::()?; + let fee_asset_addr = input.read::
()?; + let fee_amount = input.read::()?; + + let remote_call: Vec = input.read::()?.into(); + let transact_weight = input.read::()?; + + log::trace!(target: "xcm-precompile::remote_transact", "Raw arguments: dest: {:?}, fee_asset_addr: {:?} \ + fee_amount: {:?}, remote_call: {:?}, transact_weight: {}", + dest, fee_asset_addr, fee_amount, remote_call, transact_weight); + + let fee_asset = { + let address: H160 = fee_asset_addr.into(); + + // Special case where zero address maps to native token by convention. + if address == NATIVE_ADDRESS { + Here.into() + } else { + 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") + })? + } + }; + + if fee_amount > u128::MAX.into() { + return Err(revert("Fee amount is too big")); + } + let fee_amount = fee_amount.low_u128(); + + let context = ::UniversalLocation::get(); + let fee_multilocation = MultiAsset { + id: Concrete(fee_asset), + fun: Fungible(fee_amount), + }; + let fee_multilocation = fee_multilocation + .reanchored(&dest, context) + .map_err(|_| revert("Failed to reanchor fee asset"))?; + + // Prepare XCM + let xcm = Xcm(vec![ + WithdrawAsset(fee_multilocation.clone().into()), + BuyExecution { + fees: fee_multilocation.clone().into(), + weight_limit: WeightLimit::Unlimited, + }, + Transact { + origin_kind: OriginKind::SovereignAccount, + require_weight_at_most: Weight::from_ref_time(transact_weight), + call: remote_call.into(), + }, + ]); + + log::trace!(target: "xcm-precompile:remote_transact", "Processed arguments: dest: {:?}, fee asset: {:?}, XCM: {:?}", dest, fee_multilocation, xcm); + + // 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::VersionedXcm::V3(xcm)), + }; + + // Dispatch a call. + RuntimeHelper::::try_dispatch(handle, origin, call)?; + + Ok(succeed(EvmDataWriter::new().write(true).build())) + } + + fn assets_reserve_transfer(handle: &mut impl PrecompileHandle) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(6)?; + + // Read arguments and check it + let assets: Vec = input + .read::>()? + .iter() + .cloned() + .filter_map(|address| { + let address: H160 = address.into(); + + // Special case where zero address maps to native token by convention. + if address == NATIVE_ADDRESS { + Some(Here.into()) + } else { + Runtime::address_to_asset_id(address).and_then(|x| C::reverse_ref(x).ok()) + } + }) + .collect(); + let amounts_raw = input.read::>()?; + if amounts_raw.iter().any(|x| *x > u128::MAX.into()) { + return Err(revert("Asset amount is too big")); + } + let amounts: Vec = amounts_raw.iter().map(|x| x.low_u128()).collect(); + + log::trace!(target: "xcm-precompile:assets_reserve_transfer", "Processed arguments: assets {:?}, amounts: {:?}", assets, amounts); + + // Check that assets list is valid: + // * all assets resolved to multi-location + // * all assets has corresponded amount + if assets.len() != amounts.len() || assets.is_empty() { + return Err(revert("Assets resolution failure.")); + } + + let beneficiary: MultiLocation = input.read::()?; + let dest: MultiLocation = input.read::()?; + + let fee_asset_item: u32 = input.read::()?.low_u32(); + + if fee_asset_item as usize > assets.len() { + return Err(revert("Bad fee index.")); + } + + let assets: MultiAssets = assets + .iter() + .cloned() + .zip(amounts.iter().cloned()) + .map(Into::into) + .collect::>() + .into(); + + // Build call with origin. + 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()), @@ -375,7 +657,318 @@ 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(3)?; + + // 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(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 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 == u64::MAX { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE)) + }; + + 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(6)?; + + // 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 == u64::MAX { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE)) + }; + + 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(5)?; + + // Read call arguments + let asset_location = 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 dest_weight_limit = if weight == u64::MAX { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE)) + }; + + let call = orml_xtokens::Call::::transfer_multiasset { + asset: Box::new(VersionedMultiAsset::V3(MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(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_multiasset_with_fee( + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(6)?; + + // Read call arguments + let asset_location = 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 dest_weight_limit = if weight == u64::MAX { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE)) + }; + + let call = orml_xtokens::Call::::transfer_multiasset_with_fee { + asset: Box::new(VersionedMultiAsset::V3(MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(amount_of_tokens), + })), + fee: Box::new(VersionedMultiAsset::V3(MultiAsset { + id: AssetId::Concrete(asset_location), + fun: Fungibility::Fungible(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_multi_currencies( + handle: &mut impl PrecompileHandle, + ) -> EvmResult { + let mut input = handle.read_input()?; + input.expect_arguments(5)?; + + 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 == u64::MAX { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE)) + }; + + 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(5)?; + + 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 == u64::MAX { + WeightLimit::Unlimited + } else { + WeightLimit::Limited(Weight::from_parts(weight, DEFAULT_PROOF_SIZE)) + }; + + 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 839484ab..5c50ca51 100644 --- a/precompiles/xcm/src/mock.rs +++ b/precompiles/xcm/src/mock.rs @@ -46,6 +46,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; @@ -53,6 +56,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]; @@ -147,6 +166,49 @@ impl AddressToAssetId for Runtime { } } +pub struct CurrencyIdToMultiLocation; + +impl sp_runtime::traits::Convert> for CurrencyIdToMultiLocation { + fn convert(currency: CurrencyId) -> Option { + match currency { + a if a == 1u128 => Some(PARENT), + a if a == 2u128 => Some(PARACHAIN), + a if a == 3u128 => Some(GENERAL_INDEX), + a if a == 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; @@ -182,19 +244,19 @@ 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, } } @@ -312,9 +374,9 @@ impl pallet_evm::Config for Runtime { } 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; @@ -376,6 +438,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; @@ -448,6 +518,24 @@ impl pallet_xcm::Config for Runtime { type ReachableDest = ReachableDest; } +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. construct_runtime!( pub enum Runtime where @@ -461,6 +549,7 @@ construct_runtime!( Evm: pallet_evm, Timestamp: pallet_timestamp, XcmPallet: pallet_xcm, + Xtokens: orml_xtokens, } ); @@ -478,3 +567,9 @@ 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 8604dc44..75ca935a 100644 --- a/precompiles/xcm/src/tests.rs +++ b/precompiles/xcm/src/tests.rs @@ -20,263 +20,1069 @@ use core::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::RemoteTransactOld) + .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) + ); + } + } } +mod xcm_new_interface_test { + use super::*; + #[test] + fn wrong_assets_len_or_fee_index_reverts() { + ExtBuilder::default().build().execute_with(|| { + let dest: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::Parachain(2000u32)), + }; -#[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 { + let beneficiary: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(AccountId32 { + network: None, + id: H256::repeat_byte(0xF1).into(), + }), + }; + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdraw) + .write(vec![Address::from(H160::repeat_byte(0xF1))]) + .write(Vec::::new()) + .write(beneficiary) + .write(dest) + .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::AssetsWithdraw) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .write(U256::from(2_u64)) + .build(), + ) + .expect_no_logs() + .execute_reverts(|output| output == b"Bad fee index."); + }); + } + + #[test] + fn assets_withdraw_works() { + ExtBuilder::default().build().execute_with(|| { + let dest: MultiLocation = MultiLocation { parents: 1, - interior: Here - } - ); + interior: Junctions::Here, + }; - let non_native_asset = MultiAsset { - fun: Fungible(42000), - id: xcm::v3::AssetId::from(MultiLocation { + let beneficiary: MultiLocation = MultiLocation { parents: 0, - interior: Here, - }), - }; - - assert_matches!( - instructions.as_slice(), - [ - ReserveAssetDeposited(assets), - ClearOrigin, - BuyExecution { - fees, - .. - }, - DepositAsset { - beneficiary: MultiLocation { - parents: 0, - interior: X1(_), + interior: Junctions::X1(AccountId32 { + network: None, + id: H256::repeat_byte(0xF1).into(), + }), + }; + + // SS58 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdraw) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + // H160 + let beneficiary: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(AccountKey20 { + network: None, + key: H160::repeat_byte(0xDE).into(), + }), + }; + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsWithdraw) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .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(|| { + let multilocation = MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::Parachain(2000u32)), + }; + // SS58 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::RemoteTransactNew) + .write(multilocation) + .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(|| { + let dest: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::Here, + }; + + let beneficiary: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(AccountId32 { + network: None, + id: H256::repeat_byte(0xF1).into(), + }), + }; + // SS58 + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransfer) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + // H160 + let beneficiary: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(AccountKey20 { + network: None, + key: H160::repeat_byte(0xDE).into(), + }), + }; + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransfer) + .write(vec![Address::from(Runtime::asset_id_to_address(1u128))]) + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .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(|| { + let dest: MultiLocation = MultiLocation { + parents: 1, + interior: Junctions::Here, + }; + + let beneficiary: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(AccountId32 { + network: None, + id: H256::repeat_byte(0xF1).into(), + }), + }; + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransfer) + .write(vec![Address::from(H160::zero())]) // zero address by convention + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .write(U256::from(0_u64)) + .build(), + ) + .expect_no_logs() + .execute_returns(EvmDataWriter::new().write(true).build()); + + let beneficiary: MultiLocation = MultiLocation { + parents: 0, + interior: Junctions::X1(AccountKey20 { + network: None, + key: H160::repeat_byte(0xDE).into(), + }), + }; + precompiles() + .prepare_test( + TestAccount::Alice, + PRECOMPILE_ADDRESS, + EvmDataWriter::new_with_selector(Action::AssetsReserveTransfer) + .write(vec![Address::from(H160::zero())]) // zero address by convention + .write(vec![U256::from(42000u64)]) + .write(beneficiary) + .write(dest) + .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 } - ] + ); - if fees.contains(&non_native_asset) && assets.contains(&non_native_asset) - ); + 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_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 { +mod xtokens_interface_test { + use super::*; + #[test] + fn xtokens_transfer_works() { + ExtBuilder::default().build().execute_with(|| { + let parent_destination = MultiLocation { + parents: 1, + interior: Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), + }; + + let sibling_parachain_location = MultiLocation { parents: 1, - interior: Here - } + 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(U256::from(3_000_000_000u64)) + .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(U256::from(3_000_000_000u64)) + .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() { + 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(U256::from(3_000_000_000u64)) + .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_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() { + 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 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(U256::from(3_000_000_000u64)) + .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(U256::from(3_000_000_000u64)) + .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)); + }); + } + + #[test] + fn transfer_multi_currencies_works() { + let destination = MultiLocation::new( + 1, + Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], + }), ); + // 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(), + ]; - let native_asset = MultiAsset { - fun: Fungible(42000), - id: xcm::v3::AssetId::from(MultiLocation { - parents: 0, - interior: X1(OnlyChild), + 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(U256::from(3_000_000_000u64)) + .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 transfer_multi_currencies_cannot_insert_more_than_max() { + let destination = MultiLocation::new( + 1, + Junctions::X1(Junction::AccountId32 { + network: None, + id: [1u8; 32], }), - }; - - assert_matches!( - instructions.as_slice(), - [ - ReserveAssetDeposited(assets), - ClearOrigin, - BuyExecution { - fees, - .. + ); + // we only allow upto 2 currencies to be transfered + 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(), + ( + 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(U256::from(3_000_000_000u64)) + .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], }, - DepositAsset { - beneficiary: MultiLocation { - parents: 0, - interior: X1(_), - }, - .. - } - ] + ), + ); - 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 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(U256::from(3_000_000_000u64)) + .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], + }, + ), + ); + + 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(U256::from(3_000_000_000u64)) + .build(), + ) + .expect_no_logs() + .execute_reverts(|output| { + output == b"value too large : Array has more than max items allowed" + }); + }); } }