diff --git a/Cargo.toml b/Cargo.toml index f1be4619b35..64bd73ee2f1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -90,6 +90,8 @@ sp-keyring = { version = "31.0.0", default-features = false } sp-runtime = { version = "31.0.1", default-features = false } sp-weights = { version = "27.0.0", default-features = false } +xcm = { package = "staging-xcm", version = "1.0.0", default-features = false} + # Local dependencies ink = { version = "=5.0.0-rc", path = "crates/ink", default-features = false } ink_allocator = { version = "=5.0.0-rc", path = "crates/allocator", default-features = false } diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 61682408185..a942c692d16 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -787,3 +787,29 @@ where TypedEnvBackend::call_runtime::(instance, call) }) } + +/// Execute an XCM message locally, using the contract's address as the origin. +/// +/// `call` (after SCALE encoding) should be decodable to a valid instance of `RuntimeCall` +/// enum. +/// +/// For more details consult +/// [host function documentation](https://paritytech.github.io/substrate/master/pallet_contracts/api_doc/trait.Current.html#tymethod.xcm_execute). +/// +/// # Errors +/// +/// - If the call cannot be properly decoded on the pallet contracts side. +/// - If the runtime doesn't allow for the contract unstable feature. +/// +/// # Panics +/// +/// Panics in the off-chain environment. +pub fn xcm_execute(msg: &xcm::VersionedXcm) -> Result<()> +where + E: Environment, + Call: scale::Encode, +{ + ::on_instance(|instance| { + TypedEnvBackend::xcm_execute::(instance, msg) + }) +} diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index d5da075d807..8de31acb485 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -399,4 +399,14 @@ pub trait TypedEnvBackend: EnvBackend { where E: Environment, Call: scale::Encode; + + /// Execute an XCM message locally, using the contract's address as the origin. + /// + /// # Note + /// + /// For more details visit: [`xcm_execute`][`crate::xcm_execute`] + fn xcm_execute(&mut self, msg: &xcm::VersionedXcm) -> Result<()> + where + E: Environment, + Call: scale::Encode; } diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index df1a3321028..9edcbcb9890 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -547,4 +547,11 @@ impl TypedEnvBackend for EnvInstance { { unimplemented!("off-chain environment does not support `call_runtime`") } + + fn xcm_execute(&mut self, _msg: &xcm::VersionedXcm) -> Result<()> + where + E: Environment, + { + unimplemented!("off-chain environment does not support `xcm_execute`") + } } diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index b1c39c37794..593555cd42b 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -612,4 +612,14 @@ impl TypedEnvBackend for EnvInstance { let enc_call = scope.take_encoded(call); ext::call_runtime(enc_call).map_err(Into::into) } + + fn xcm_execute(&mut self, msg: &VersionedXcm) -> Result<()> + where + E: Environment, + Call: scale::Encode, + { + let mut scope = self.scoped_buffer(); + let enc_msg = scope.take_encoded(msg); + ext::xcm_execute(enc_msg).map_err(Into::into) + } } diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index 7d863739339..8858c75026c 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -27,6 +27,8 @@ scale = { workspace = true } scale-info = { workspace = true, default-features = false, features = ["derive"], optional = true } derive_more = { workspace = true, features = ["from"] } +xcm = { workspace = true} + [dev-dependencies] ink_ir = { workspace = true, default-features = true } ink_metadata = { workspace = true } @@ -45,6 +47,7 @@ std = [ "ink_macro/std", "scale/std", "scale-info/std", + "xcm/std", ] # Enable contract debug messages via `debug_print!` and `debug_println!`. ink-debug = [ diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index a7a705882d8..db81fdc176c 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -1112,4 +1112,11 @@ where pub fn call_runtime(self, call: &Call) -> Result<()> { ink_env::call_runtime::(call) } + + pub fn xcm_execute( + self, + msg: &xcm::VersionedXcm, + ) -> Result<()> { + ink_env::xcm_execute::(msg) + } } diff --git a/integration-tests/xcm-execute/.gitignore b/integration-tests/xcm-execute/.gitignore new file mode 100644 index 00000000000..bf910de10af --- /dev/null +++ b/integration-tests/xcm-execute/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/xcm-execute/Cargo.toml b/integration-tests/xcm-execute/Cargo.toml new file mode 100644 index 00000000000..1c0c1f27050 --- /dev/null +++ b/integration-tests/xcm-execute/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "xcm-execute" +version = "4.0.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } +xcm = { package = "staging-xcm", version = "1.0.0", default-features = false} + +# Substrate +# +# We need to explicitly turn off some of the `sp-io` features, to avoid conflicts +# (especially for global allocator). +# +# See also: https://substrate.stackexchange.com/questions/4733/error-when-compiling-a-contract-using-the-xcm-chain-extension. +sp-io = { version = "23.0.0", default-features = false, features = ["disable_panic_handler", "disable_oom", "disable_allocator"] } +sp-runtime = { version = "24.0.0", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "sp-runtime/std", + "sp-io/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/xcm-execute/README.md b/integration-tests/xcm-execute/README.md new file mode 100644 index 00000000000..d4758995767 --- /dev/null +++ b/integration-tests/xcm-execute/README.md @@ -0,0 +1,22 @@ +# `xcm-execute` example + +## What is this example about? + +It demonstrates how to use XCM from an ink! contract. + +## Chain-side configuration + +To integrate this example into Substrate you need to implement `pallet-xcm` and configure the `Xcm` trait of `pallet-contracts` + + ```rust +// In your node's runtime configuration file (runtime.rs) +impl pallet_xcm::Config for Runtime { + // ... +} + +impl pallet_contracts::Config for Runtime { + // … + type Xcm = PalletXCMAdapter; + // … + } + ``` diff --git a/integration-tests/xcm-execute/lib.rs b/integration-tests/xcm-execute/lib.rs new file mode 100644 index 00000000000..29630f4d0ba --- /dev/null +++ b/integration-tests/xcm-execute/lib.rs @@ -0,0 +1,212 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod xcm_execute { + // use xcm::VersionedXcm; + use xcm::{ + v3::prelude::*, + VersionedXcm, + }; + + use ink::{ + env::Error as EnvError, + prelude::*, + }; + + /// A trivial contract with a single message, that uses `xcm_execute` API for + /// performing native token transfer. + #[ink(storage)] + #[derive(Default)] + pub struct XcmExecute; + + #[derive(Debug, PartialEq, Eq)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum RuntimeError { + CallRuntimeFailed, + } + + impl From for RuntimeError { + fn from(e: EnvError) -> Self { + use ink::env::ReturnErrorCode; + match e { + EnvError::ReturnError(ReturnErrorCode::CallRuntimeFailed) => { + RuntimeError::CallRuntimeFailed + } + _ => panic!("Unexpected error from `pallet-contracts`."), + } + } + } + + impl XcmExecute { + /// The constructor is `payable`, so that during instantiation it can be given + /// some tokens that will be further transferred when transferring funds through + /// XCM. + #[ink(constructor, payable)] + pub fn new() -> Self { + Default::default() + } + + /// Tries to transfer `value` from the contract's balance to `receiver`. + /// + /// Fails if: + /// - called in the off-chain environment + /// - the chain is not configured with Xcm + /// - the XCM program executed failed (e.g contract doesn't have enough balance) + #[ink(message)] + pub fn transfer_through_xcm( + &mut self, + receiver: AccountId, + value: Balance, + ) -> Result<(), RuntimeError> { + self.env() + .xcm_execute(&VersionedXcm::V3(Xcm::<()>(vec![ + WithdrawAsset(vec![(Here, value).into()].into()), + DepositAsset { + assets: All.into(), + beneficiary: AccountId32 { + network: None, + id: *receiver.as_ref(), + } + .into(), + }, + ]))) + .map_err(Into::into) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use super::*; + use ink_e2e::{ + ChainBackend, + ContractsBackend, + }; + + use ink::{ + env::{ + test::default_accounts, + DefaultEnvironment, + }, + primitives::AccountId, + }; + + type E2EResult = Result>; + + /// The base number of indivisible units for balances on the + /// `substrate-contracts-node`. + const UNIT: Balance = 1_000_000_000_000; + + /// The contract will be given 1000 tokens during instantiation. + const CONTRACT_BALANCE: Balance = 1_000 * UNIT; + + /// The receiver will get enough funds to have the required existential deposit. + /// + /// If your chain has this threshold higher, increase the transfer value. + const TRANSFER_VALUE: Balance = 1 / 10 * UNIT; + + /// An amount that is below the existential deposit, so that a transfer to an + /// empty account fails. + /// + /// Must not be zero, because such an operation would be a successful no-op. + const INSUFFICIENT_TRANSFER_VALUE: Balance = 1; + + /// Positive case scenario: + #[ink_e2e::test] + async fn transfer_with_xcm_execute_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let constructor = XcmExecuteRef::new(); + let contract = client + .instantiate( + "xcm-execute", + &ink_e2e::alice(), + constructor, + CONTRACT_BALANCE, + None, + ) + .await + .expect("instantiate failed"); + let mut call = contract.call::(); + + let receiver: AccountId = default_accounts::().bob; + + let contract_balance_before = client + .balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_before = client + .balance(receiver) + .await + .expect("Failed to get account balance"); + + // when + let transfer_message = call.transfer_through_xcm(receiver, TRANSFER_VALUE); + + let call_res = client + .call(&ink_e2e::alice(), &transfer_message, 0, None) + .await + .expect("call failed"); + + assert!(call_res.return_value().is_ok()); + + // then + let contract_balance_after = client + .balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_after = client + .balance(receiver) + .await + .expect("Failed to get account balance"); + + assert_eq!( + contract_balance_before, + contract_balance_after + TRANSFER_VALUE + ); + assert_eq!( + receiver_balance_before, + receiver_balance_after - TRANSFER_VALUE + ); + + Ok(()) + } + + /// Negative case scenario: + #[ink_e2e::test] + async fn transfer_with_xcm_execute_fails_when_execution_fails< + Client: E2EBackend, + >( + mut client: Client, + ) -> E2EResult<()> { + // given + let constructor = XcmExecuteRef::new(); + let contract = client + .instantiate( + "xcm-execute", + &ink_e2e::alice(), + constructor, + CONTRACT_BALANCE, + None, + ) + .await + .expect("instantiate failed"); + let mut call = contract.call::(); + + let receiver: AccountId = default_accounts::().bob; + + // when + let transfer_message = call.transfer_through_xcm(receiver, TRANSFER_VALUE); + + let call_res = client + .call_dry_run(&ink_e2e::alice(), &transfer_message, 0, None) + .await + .return_value(); + + // then + assert!(matches!(call_res, Err(RuntimeError::CallRuntimeFailed))); + + Ok(()) + } + } +}