Skip to content

Commit

Permalink
XCM wip
Browse files Browse the repository at this point in the history
  • Loading branch information
pgherveou committed Feb 1, 2024
1 parent 1b1cb29 commit 2baefa4
Show file tree
Hide file tree
Showing 11 changed files with 343 additions and 0 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
26 changes: 26 additions & 0 deletions crates/env/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,3 +787,29 @@ where
TypedEnvBackend::call_runtime::<E, _>(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<E, Call>(msg: &xcm::VersionedXcm<Call>) -> Result<()>
where
E: Environment,
Call: scale::Encode,
{
<EnvInstance as OnInstance>::on_instance(|instance| {
TypedEnvBackend::xcm_execute::<E, _>(instance, msg)
})
}
10 changes: 10 additions & 0 deletions crates/env/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<E, Call>(&mut self, msg: &xcm::VersionedXcm<Call>) -> Result<()>
where
E: Environment,
Call: scale::Encode;
}
7 changes: 7 additions & 0 deletions crates/env/src/engine/off_chain/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -547,4 +547,11 @@ impl TypedEnvBackend for EnvInstance {
{
unimplemented!("off-chain environment does not support `call_runtime`")
}

fn xcm_execute<E, Call>(&mut self, _msg: &xcm::VersionedXcm<Call>) -> Result<()>
where
E: Environment,
{
unimplemented!("off-chain environment does not support `xcm_execute`")
}
}
10 changes: 10 additions & 0 deletions crates/env/src/engine/on_chain/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<E, Call>(&mut self, msg: &VersionedXcm<Call>) -> 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)
}
}
3 changes: 3 additions & 0 deletions crates/ink/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 = [
Expand Down
7 changes: 7 additions & 0 deletions crates/ink/src/env_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1112,4 +1112,11 @@ where
pub fn call_runtime<Call: scale::Encode>(self, call: &Call) -> Result<()> {
ink_env::call_runtime::<E, _>(call)
}

pub fn xcm_execute<Call: scale::Encode>(
self,
msg: &xcm::VersionedXcm<Call>,
) -> Result<()> {
ink_env::xcm_execute::<E, _>(msg)
}
}
9 changes: 9 additions & 0 deletions integration-tests/xcm-execute/.gitignore
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions integration-tests/xcm-execute/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "xcm-execute"
version = "4.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
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 = []
22 changes: 22 additions & 0 deletions integration-tests/xcm-execute/README.md
Original file line number Diff line number Diff line change
@@ -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<Self>;
//
}
```
212 changes: 212 additions & 0 deletions integration-tests/xcm-execute/lib.rs
Original file line number Diff line number Diff line change
@@ -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<EnvError> 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<T> = Result<T, Box<dyn std::error::Error>>;

/// 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<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::<XcmExecute>();

let receiver: AccountId = default_accounts::<DefaultEnvironment>().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::<XcmExecute>();

let receiver: AccountId = default_accounts::<DefaultEnvironment>().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(())
}
}
}

0 comments on commit 2baefa4

Please sign in to comment.