diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index fafdff77c7b..b232f7a22c4 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -132,3 +132,4 @@ DRink ^ externalities sandbox_client +xcm diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8446532080c..0fb05d907ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -622,7 +622,7 @@ jobs: # - custom_allocator # Pulls in sp-std which needlessly requires atomic pointers (TODO: Fix sp-std and enable this example) # - call-runtime - scripts/for_all_contracts_exec.sh --path integration-tests --ignore public/custom-allocator --ignore public/call-runtime \ + scripts/for_all_contracts_exec.sh --path integration-tests --ignore public/custom-allocator --ignore public/call-runtime --ignore public/contract-xcm \ -- cargo build --manifest-path {} --no-default-features --target $RISCV_TARGET -Zbuild-std="core,alloc" examples-docs: diff --git a/CHANGELOG.md b/CHANGELOG.md index 50bd37087ae..9d05bcc7b63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [Linter] Add links to detailed lint description ‒ [#2170](https://github.com/use-ink/ink/pull/2170) +- Add `xcm_execute` and `xcm_send` support - [#1912](https://github.com/paritytech/ink/pull/1912) - Environment agnostic contract invocation API ‒ [#219](https://github.com/use-ink/ink/pull/2219) ### Changed diff --git a/Cargo.lock b/Cargo.lock index 69d72290a5b..eedf1c64b70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2551,6 +2551,7 @@ dependencies = [ "pallet-contracts-uapi", "parity-scale-codec", "scale-info", + "staging-xcm", "trybuild", ] @@ -2676,6 +2677,7 @@ dependencies = [ "secp256k1", "sha2 0.10.8", "sha3", + "staging-xcm", "static_assertions", ] diff --git a/Cargo.toml b/Cargo.toml index 5ee0bac0a15..0a4a802984e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,6 +99,7 @@ sp-core = { version = "32.0.0", default-features = false } sp-keyring = { version = "35.0.0", default-features = false } sp-runtime = { version = "35.0.0", default-features = false } sp-weights = { version = "31.0.0", default-features = false } +xcm = { package = "staging-xcm", version = "11.0.0", default-features = false } # Local dependencies ink = { version = "=5.0.0", path = "crates/ink", default-features = false } diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index 4e5f1c1e0dd..9d5d185e526 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -68,7 +68,7 @@ std = [ "ink_e2e_macro/std", "ink_sandbox/std", "frame-support/std", - "pallet-contracts-mock-network?/std" + "pallet-contracts-mock-network?/std", ] sandbox = [ diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 82ead6181d7..61cfada3a84 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -56,7 +56,10 @@ pub use node_proc::{ TestNodeProcessBuilder, }; #[cfg(feature = "sandbox")] -pub use sandbox_client::Client as SandboxClient; +pub use sandbox_client::{ + preset, + Client as SandboxClient, +}; pub use sp_core::H256; pub use sp_keyring::AccountKeyring; pub use subxt::{ diff --git a/crates/e2e/src/sandbox_client.rs b/crates/e2e/src/sandbox_client.rs index ac27c240a46..91f7f9b2d1c 100644 --- a/crates/e2e/src/sandbox_client.rs +++ b/crates/e2e/src/sandbox_client.rs @@ -483,18 +483,27 @@ pub mod preset { /// } /// ``` #[derive(Default)] - pub struct MockNetworkSandbox; + pub struct MockNetworkSandbox { + dry_run: bool, + } + impl Sandbox for MockNetworkSandbox { type Runtime = parachain::Runtime; fn execute_with(&mut self, execute: impl FnOnce() -> T) -> T { - ParaA::execute_with(execute) + if self.dry_run { + ParaA::execute_with(execute) + } else { + ParaA::execute_without_dispatch(execute) + } } fn dry_run(&mut self, action: impl FnOnce(&mut Self) -> T) -> T { EXT_PARAA.with(|v| { let backend_backup = v.borrow_mut().as_backend(); + self.dry_run = true; let result = action(self); + self.dry_run = false; let mut v = v.borrow_mut(); v.commit_all().expect("Failed to commit changes"); diff --git a/crates/env/Cargo.toml b/crates/env/Cargo.toml index 268aa0a8532..065d8cb2334 100644 --- a/crates/env/Cargo.toml +++ b/crates/env/Cargo.toml @@ -29,6 +29,7 @@ cfg-if = { workspace = true } paste = { workspace = true } static_assertions = { workspace = true } const_env = { workspace = true } +xcm = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] rlibc = "1" @@ -63,22 +64,23 @@ default = [ "std" ] std = [ "blake2", "ink_allocator/std", - "ink_engine/std", "ink_prelude/std", "ink_primitives/std", "ink_storage_traits/std", - "num-traits/std", + "ink_engine/std", + "scale/std", "scale-decode", "scale-encode", "scale-info/std", - "scale/std", - "schnorrkel", "secp256k1", - # Enables hashing crates for off-chain environment. + "schnorrkel", + "num-traits/std", + # Enables hashing crates for off-chain environment. "sha2", "sha3", "scale-decode?/std", - "scale-encode?/std" + "scale-encode?/std", + "xcm/std" ] # Enable contract debug messages via `debug_print!` and `debug_println!`. diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 4ef8a7d0472..47ee8c696cb 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -903,3 +903,54 @@ where instance.unlock_delegate_dependency::(code_hash) }) } + +/// Execute an XCM message locally, using the contract's address as the origin. +/// +/// For more details consult the +/// [host function documentation](https://paritytech.github.io/substrate/master/pallet_contracts/api_doc/trait.Current.html#tymethod.xcm_execute). +/// +/// # Errors +/// +/// - If the message cannot be properly decoded on the `pallet-contracts` side. +/// - If the XCM execution fails because of the runtime's XCM configuration. +/// +/// # 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) + }) +} + +/// Send an XCM message, using the contract's address as the origin. +/// +/// The `msg` argument has to be SCALE encoded, it needs to be decodable to a valid +/// instance of the `RuntimeCall` enum. +/// +/// For more details consult +/// [host function documentation](https://paritytech.github.io/substrate/master/pallet_contracts/api_doc/trait.Current.html#tymethod.xcm_send). +/// +/// # Errors +/// +/// - If the message cannot be properly decoded on the `pallet-contracts` side. +/// +/// # Panics +/// +/// Panics in the off-chain environment. +pub fn xcm_send( + dest: &xcm::VersionedLocation, + msg: &xcm::VersionedXcm, +) -> Result +where + E: Environment, + Call: scale::Encode, +{ + ::on_instance(|instance| { + TypedEnvBackend::xcm_send::(instance, dest, msg) + }) +} diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index b2d819f204e..1ace832dfaf 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -455,4 +455,28 @@ pub trait TypedEnvBackend: EnvBackend { fn unlock_delegate_dependency(&mut self, code_hash: &E::Hash) where E: Environment; + + /// Execute an XCM message locally, using the contract's address as the origin. + /// + /// # Note + /// + /// For more details visit: [`xcm`][`crate::xcm_execute`]. + fn xcm_execute(&mut self, msg: &xcm::VersionedXcm) -> Result<()> + where + E: Environment, + Call: scale::Encode; + + /// Send an XCM message, using the contract's address as the origin. + /// + /// # Note + /// + /// For more details visit: [`xcm`][`crate::xcm_send`]. + fn xcm_send( + &mut self, + dest: &xcm::VersionedLocation, + 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 f212a64b525..727bee025aa 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -595,6 +595,24 @@ impl TypedEnvBackend for EnvInstance { unimplemented!("off-chain environment does not support delegate dependencies") } + fn xcm_execute(&mut self, _msg: &xcm::VersionedXcm) -> Result<()> + where + E: Environment, + { + unimplemented!("off-chain environment does not support `xcm_execute`") + } + + fn xcm_send( + &mut self, + _dest: &xcm::VersionedLocation, + _msg: &xcm::VersionedXcm, + ) -> Result + where + E: Environment, + { + unimplemented!("off-chain environment does not support `xcm_send`") + } + fn unlock_delegate_dependency(&mut self, _code_hash: &E::Hash) where E: Environment, diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index 5a870fc719e..aaf35f50e1b 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -58,6 +58,7 @@ use pallet_contracts_uapi::{ ReturnErrorCode, ReturnFlags, }; +use xcm::VersionedXcm; impl CryptoHash for Blake2x128 { fn hash(input: &[u8], output: &mut ::Type) { @@ -726,4 +727,40 @@ impl TypedEnvBackend for EnvInstance { let enc_code_hash = scope.take_encoded(code_hash); ext::unlock_delegate_dependency(enc_code_hash) } + + fn xcm_execute(&mut self, msg: &VersionedXcm) -> Result<()> + where + E: Environment, + Call: scale::Encode, + { + let mut scope = self.scoped_buffer(); + + // Double encoding the message as the host fn expects an encoded message. + let enc_msg = scope.take_encoded(&scale::Encode::encode(msg)); + #[allow(deprecated)] + ext::xcm_execute(enc_msg).map_err(Into::into) + } + + fn xcm_send( + &mut self, + dest: &xcm::VersionedLocation, + msg: &VersionedXcm, + ) -> Result + where + E: Environment, + Call: scale::Encode, + { + let mut scope = self.scoped_buffer(); + let output = scope.take(32); + scope.append_encoded(dest); + let enc_dest = scope.take_appended(); + + // Double encoding the message as the host fn expects an encoded message. + scope.append_encoded(&scale::Encode::encode(msg)); + let enc_msg = scope.take_appended(); + #[allow(deprecated)] + ext::xcm_send(enc_dest, enc_msg, output.try_into().unwrap())?; + let hash: xcm::v4::XcmHash = scale::Decode::decode(&mut &output[..])?; + Ok(hash) + } } diff --git a/crates/ink/Cargo.toml b/crates/ink/Cargo.toml index ece158eb5c4..8bf8c1e9190 100644 --- a/crates/ink/Cargo.toml +++ b/crates/ink/Cargo.toml @@ -26,11 +26,11 @@ pallet-contracts-uapi = { workspace = true } 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 } - trybuild = { workspace = true, features = ["diff"] } @@ -45,6 +45,7 @@ std = [ "ink_storage/std", "scale-info/std", "scale/std", + "xcm/std" ] # Enable contract debug messages via `debug_print!` and `debug_println!`. ink-debug = [ "ink_env/ink-debug" ] diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index ea6460d6e03..106a015860e 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -1341,4 +1341,19 @@ where pub fn unlock_delegate_dependency(self, code_hash: &E::Hash) { ink_env::unlock_delegate_dependency::(code_hash) } + + pub fn xcm_execute( + self, + msg: &xcm::VersionedXcm, + ) -> Result<()> { + ink_env::xcm_execute::(msg) + } + + pub fn xcm_send( + self, + dest: &xcm::VersionedLocation, + msg: &xcm::VersionedXcm, + ) -> Result { + ink_env::xcm_send::(dest, msg) + } } diff --git a/crates/ink/src/lib.rs b/crates/ink/src/lib.rs index 578a2a93cab..01698cd17e7 100644 --- a/crates/ink/src/lib.rs +++ b/crates/ink/src/lib.rs @@ -44,6 +44,7 @@ pub use ink_primitives as primitives; pub use scale; #[cfg(feature = "std")] pub use scale_info; +pub use xcm; pub mod storage { pub mod traits { diff --git a/crates/primitives/src/types.rs b/crates/primitives/src/types.rs index 4185f34e8a7..ffb4a073af9 100644 --- a/crates/primitives/src/types.rs +++ b/crates/primitives/src/types.rs @@ -47,7 +47,7 @@ use { From, )] #[cfg_attr(feature = "std", derive(TypeInfo, DecodeAsType, EncodeAsType))] -pub struct AccountId([u8; 32]); +pub struct AccountId(pub [u8; 32]); impl AsRef<[u8; 32]> for AccountId { #[inline] diff --git a/integration-tests/public/contract-xcm/Cargo.toml b/integration-tests/public/contract-xcm/Cargo.toml new file mode 100644 index 00000000000..5e865bb84ca --- /dev/null +++ b/integration-tests/public/contract-xcm/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "contract-xcm" +version = "4.0.0" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } +frame-support = { version = "32.0.0", default-features = false } +pallet-balances = { version = "33.0.0", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e", features = ["sandbox"] } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "pallet-balances/std", + "frame-support/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/public/contract-xcm/lib.rs b/integration-tests/public/contract-xcm/lib.rs new file mode 100644 index 00000000000..93cf048645c --- /dev/null +++ b/integration-tests/public/contract-xcm/lib.rs @@ -0,0 +1,272 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +mod contract_xcm { + use ink::{ + env::Error as EnvError, + xcm::prelude::*, + }; + + /// A trivial contract used to exercise XCM API. + #[ink(storage)] + #[derive(Default)] + pub struct ContractXcm; + + #[derive(Debug, PartialEq, Eq)] + #[ink::scale_derive(Encode, Decode, TypeInfo)] + pub enum RuntimeError { + XcmExecuteFailed, + XcmSendFailed, + } + + impl From for RuntimeError { + fn from(e: EnvError) -> Self { + use ink::env::ReturnErrorCode; + match e { + EnvError::ReturnError(ReturnErrorCode::XcmExecutionFailed) => { + RuntimeError::XcmExecuteFailed + } + EnvError::ReturnError(ReturnErrorCode::XcmSendFailed) => { + RuntimeError::XcmSendFailed + } + _ => panic!("Unexpected error from `pallet-contracts`."), + } + } + } + + impl ContractXcm { + /// 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 to support 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> { + let asset: Asset = (Here, value).into(); + let beneficiary = AccountId32 { + network: None, + id: *receiver.as_ref(), + }; + + let message: Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone().into()) + .buy_execution(asset.clone(), Unlimited) + .deposit_asset(asset.into(), beneficiary.into()) + .build(); + + self.env() + .xcm_execute(&VersionedXcm::V4(message)) + .map_err(Into::into) + } + + /// Transfer some funds on the relay chain via XCM from the contract's derivative + /// account to the caller's account. + /// + /// Fails if: + /// - called in the off-chain environment + /// - the chain is not configured to support XCM + /// - the XCM program executed failed (e.g contract doesn't have enough balance) + #[ink(message)] + pub fn send_funds( + &mut self, + value: Balance, + fee: Balance, + ) -> Result { + let destination: Location = Parent.into(); + let asset: Asset = (Here, value).into(); + let beneficiary = AccountId32 { + network: None, + id: *self.env().caller().as_ref(), + }; + + let message: Xcm<()> = Xcm::builder() + .withdraw_asset(asset.clone().into()) + .buy_execution((Here, fee).into(), WeightLimit::Unlimited) + .deposit_asset(asset.into(), beneficiary.into()) + .build(); + + let hash = self.env().xcm_send( + &VersionedLocation::V4(destination), + &VersionedXcm::V4(message), + )?; + + Ok(hash) + } + } + + #[cfg(all(test, feature = "e2e-tests"))] + mod e2e_tests { + use frame_support::{ + sp_runtime::AccountId32, + traits::tokens::currency::Currency, + }; + use ink::{ + env::{ + test::default_accounts, + DefaultEnvironment, + }, + primitives::AccountId, + }; + use ink_e2e::{ + preset::mock_network::{ + self, + primitives::{ + CENTS, + UNITS, + }, + MockNetworkSandbox, + }, + ChainBackend, + ContractsBackend, + }; + use mock_network::{ + parachain::estimate_message_fee, + parachain_account_sovereign_account_id, + relay_chain, + Relay, + TestExt, + }; + + use super::*; + + /// The contract will be given 1000 tokens during instantiation. + pub const CONTRACT_BALANCE: u128 = 1_000 * UNITS; + + type E2EResult = Result>; + + #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))] + async fn xcm_execute_works( + mut client: Client, + ) -> E2EResult<()> { + // given + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(CONTRACT_BALANCE) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let receiver: AccountId = default_accounts::().bob; + + let contract_balance_before = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_before = client + .free_balance(receiver) + .await + .expect("Failed to get account balance"); + + // when + let amount = 1000 * CENTS; + let transfer_message = call_builder.transfer_through_xcm(receiver, amount); + + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await + .expect("call failed"); + + assert!(call_res.return_value().is_ok()); + + // then + let contract_balance_after = client + .free_balance(contract.account_id) + .await + .expect("Failed to get account balance"); + let receiver_balance_after = client + .free_balance(receiver) + .await + .expect("Failed to get account balance"); + + assert_eq!(contract_balance_before, contract_balance_after + amount); + assert_eq!(receiver_balance_before, receiver_balance_after - amount); + + Ok(()) + } + + #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))] + async fn incomplete_xcm_execute_works( + mut client: Client, + ) -> E2EResult<()> { + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(CONTRACT_BALANCE) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + // This will fail since we have insufficient balance + let transfer_message = call_builder.transfer_through_xcm( + default_accounts::().bob, + CONTRACT_BALANCE + 1, + ); + + let call_res = client + .call(&ink_e2e::alice(), &transfer_message) + .submit() + .await? + .return_value(); + + assert!(matches!(call_res, Err(RuntimeError::XcmExecuteFailed))); + Ok(()) + } + + #[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))] + async fn xcm_send_works(mut client: Client) -> E2EResult<()> { + let mut constructor = ContractXcmRef::new(); + let contract = client + .instantiate("contract_xcm", &ink_e2e::alice(), &mut constructor) + .value(CONTRACT_BALANCE) + .submit() + .await + .expect("instantiate failed"); + + Relay::execute_with(|| { + let sovereign_account = parachain_account_sovereign_account_id( + 1u32, + AccountId32::from(contract.account_id.0), + ); + + // Fund the contract's derivative account, so we can use it as a sink, to + // transfer funds to the caller. + relay_chain::Balances::make_free_balance_be( + &sovereign_account, + CONTRACT_BALANCE, + ); + }); + + let amount = 1000 * CENTS; + let fee = estimate_message_fee(4); + + let mut call_builder = contract.call_builder::(); + let message = call_builder.send_funds(amount, fee); + let call_res = client.call(&ink_e2e::alice(), &message).submit().await?; + assert!(call_res.return_value().is_ok()); + + Relay::execute_with(|| { + let alice = AccountId32::from(ink_e2e::alice().public_key().0); + assert_eq!(relay_chain::Balances::free_balance(&alice), amount - fee); + }); + + Ok(()) + } + } +}