diff --git a/contract-xcm/Cargo.toml b/contract-xcm/Cargo.toml new file mode 100644 index 00000000..a1eebb59 --- /dev/null +++ b/contract-xcm/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "contract-xcm" +version = "5.1.0" +authors = ["Use Ink "] +edition = "2021" +publish = false + +[dependencies] +ink = { version = "5.1.0", 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/contract-xcm/lib.rs b/contract-xcm/lib.rs new file mode 100644 index 00000000..93cf0486 --- /dev/null +++ b/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(()) + } + } +} diff --git a/runtime-call-contract/Cargo.toml b/runtime-call-contract/Cargo.toml new file mode 100644 index 00000000..9572a253 --- /dev/null +++ b/runtime-call-contract/Cargo.toml @@ -0,0 +1,49 @@ +[workspace] +members = ["sandbox-runtime", "traits"] + +[workspace.package] +authors = ["Use Ink "] +edition = "2021" +homepage = "https://use.ink" +keywords = ["wasm", "ink", "webassembly", "blockchain", "edsl"] +license = "Apache-2.0" +repository = "https://github.com/use-ink/ink" + +[workspace.dependencies] +frame-support = { version = "32.0.0", default-features = false } +frame-system = { version = "32.0.0", default-features = false } +pallet-contracts = { version = "31.0.0", default-features = false } +sp-runtime = { version = "35.0.0", default-features = false } +codec = { package = "parity-scale-codec", version = "3.6.9", default-features = false } +scale-info = { version = "2.11.1", default-features = false } + +[package] +name = "runtime-call-contract" +version = "5.1.0" +authors = ["Use Ink "] +edition = "2021" +publish = false + +[dependencies] +ink = { version = "5.1.0", default-features = false } +flipper-traits = { path = "traits", default-features = false } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e", features = ["sandbox"] } +sandbox-runtime = { path = "sandbox-runtime", default-features = false } +scale-value = "0.14.1" +# can't use workspace dependency because of `cargo-contract` build not +# working with workspace dependencies +frame-support = { version = "32.0.0", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "sandbox-runtime/std", + "flipper-traits/std", +] +ink-as-dependency = [] diff --git a/runtime-call-contract/e2e_tests.rs b/runtime-call-contract/e2e_tests.rs new file mode 100644 index 00000000..f4b73396 --- /dev/null +++ b/runtime-call-contract/e2e_tests.rs @@ -0,0 +1,61 @@ +use super::{ + Flipper, + FlipperRef, +}; +use ink_e2e::{ + ChainBackend, + ContractsBackend, +}; + +type E2EResult = Result>; + +/// Just instantiate a contract using non-default runtime. +#[ink_e2e::test(backend(runtime_only(sandbox = sandbox_runtime::ContractCallerSandbox)))] +async fn instantiate_and_get(mut client: Client) -> E2EResult<()> { + use flipper_traits::Flip; + + let initial_value = false; + let mut constructor = FlipperRef::new(initial_value); + + let contract = client + .instantiate("runtime-call-contract", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + + let mut call_builder = contract.call_builder::(); + let flip_dry_run = client + .call(&ink_e2e::bob(), &call_builder.flip()) + .dry_run() + .await?; + let gas_required = flip_dry_run.exec_result.gas_required; + + // call pallet dispatchable + client + .runtime_call( + &ink_e2e::alice(), + "ContractCaller", + "contract_call_flip", + vec![ + scale_value::Value::from_bytes(contract.account_id), + scale_value::serde::to_value(frame_support::weights::Weight::from_parts( + gas_required.ref_time(), + gas_required.proof_size(), + )) + .unwrap(), + scale_value::serde::to_value(None::).unwrap(), + ], + ) + .await + .expect("runtime call failed"); + + // now check that the flip was executed via the pallet + let get_result = client + .call(&ink_e2e::alice(), &call_builder.get()) + .dry_run() + .await?; + + assert_eq!(get_result.return_value(), !initial_value); + + Ok(()) +} diff --git a/runtime-call-contract/lib.rs b/runtime-call-contract/lib.rs new file mode 100644 index 00000000..58435745 --- /dev/null +++ b/runtime-call-contract/lib.rs @@ -0,0 +1,36 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +pub use flipper::{ + Flipper, + FlipperRef, +}; + +#[ink::contract] +mod flipper { + #[ink(storage)] + pub struct Flipper { + value: bool, + } + + impl Flipper { + #[ink(constructor)] + pub fn new(init_value: bool) -> Self { + Self { value: init_value } + } + } + + impl flipper_traits::Flip for Flipper { + #[ink(message)] + fn flip(&mut self) { + self.value = !self.value; + } + + #[ink(message)] + fn get(&self) -> bool { + self.value + } + } +} + +#[cfg(test)] +mod e2e_tests; diff --git a/runtime-call-contract/sandbox-runtime/Cargo.toml b/runtime-call-contract/sandbox-runtime/Cargo.toml new file mode 100644 index 00000000..fabffd81 --- /dev/null +++ b/runtime-call-contract/sandbox-runtime/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "sandbox-runtime" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +ink_sandbox = { path = "../../../../crates/e2e/sandbox" } +pallet-contract-caller = { path = "pallet-contract-caller", default-features = false } +frame-support = { workspace = true } +frame-system = { workspace = true } +codec = { workspace = true } +scale-info = { workspace = true } + +[features] +default = ["std"] +std = [ + "pallet-contract-caller/std", +] diff --git a/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml b/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml new file mode 100644 index 00000000..2fd03162 --- /dev/null +++ b/runtime-call-contract/sandbox-runtime/pallet-contract-caller/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "pallet-contract-caller" +version = "0.1.0" +description = "Demonstrate calling an ink! contract from a pallet" +authors = ["Use Ink "] +homepage = "https://substrate.io" +edition.workspace = true +license = "MIT-0" +publish = false +repository = "https://github.com/use-ink/ink" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true, default-features = false, features = ["derive"] } +scale-info = { workspace = true, default-features = false, features = ["derive"] } +frame-support = { workspace = true, default-features = false } +frame-system = { workspace = true, default-features = false } +sp-runtime = { workspace = true, default-features = false } + +pallet-contracts = { workspace = true, default-features = false } +flipper-traits = { path = "../../traits", default-features = false } +ink = { version = "5.1.0", features = ["no-panic-handler", "no-allocator"] } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "frame-system/std", + "sp-runtime/std", + "scale-info/std", + "pallet-contracts/std", + "ink/std" +] +try-runtime = [] diff --git a/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/executor.rs b/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/executor.rs new file mode 100644 index 00000000..214cf5fd --- /dev/null +++ b/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/executor.rs @@ -0,0 +1,59 @@ +use crate::{ + AccountIdOf, + BalanceOf, +}; +use frame_support::pallet_prelude::Weight; +use ink::env::{ + call::{ + ExecutionInput, + Executor, + }, + Environment, +}; + +pub struct PalletContractsExecutor { + pub origin: AccountIdOf, + pub contract: AccountIdOf, + pub value: BalanceOf, + pub gas_limit: Weight, + pub storage_deposit_limit: Option>, + pub marker: core::marker::PhantomData, +} + +impl Executor for PalletContractsExecutor +where + E: Environment, + R: pallet_contracts::Config, +{ + type Error = sp_runtime::DispatchError; + + fn exec( + &self, + input: &ExecutionInput, + ) -> Result, Self::Error> + where + Args: codec::Encode, + Output: codec::Decode, + { + let data = codec::Encode::encode(&input); + + let result = pallet_contracts::Pallet::::bare_call( + self.origin.clone(), + self.contract.clone(), + self.value, + self.gas_limit, + self.storage_deposit_limit, + data, + pallet_contracts::DebugInfo::UnsafeDebug, + pallet_contracts::CollectEvents::Skip, + pallet_contracts::Determinism::Enforced, + ); + + let output = result.result?.data; + let result = codec::Decode::decode(&mut &output[..]).map_err(|_| { + sp_runtime::DispatchError::Other("Failed to decode contract output") + })?; + + Ok(result) + } +} diff --git a/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs b/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs new file mode 100644 index 00000000..cf7abdd4 --- /dev/null +++ b/runtime-call-contract/sandbox-runtime/pallet-contract-caller/src/lib.rs @@ -0,0 +1,76 @@ +//! # Contract Caller +//! +//! Demonstrates calling into an `ink!` contract from a pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +mod executor; + +use frame_support::{ + pallet_prelude::Weight, + traits::fungible::Inspect, +}; +pub use pallet::*; + +type AccountIdOf = ::AccountId; +type BalanceOf = <::Currency as Inspect< + ::AccountId, +>>::Balance; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use flipper_traits::Flip; + use frame_support::{ + pallet_prelude::*, + traits::fungible::Inspect, + }; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_contracts::Config {} + + #[pallet::error] + pub enum Error {} + + #[pallet::call] + impl Pallet + where + [u8; 32]: From<::AccountId>, + <::Currency as Inspect< + ::AccountId, + >>::Balance: From, + { + /// Call the flip method on the contract at the given `contract` account. + #[pallet::call_index(0)] + #[pallet::weight(::call().saturating_add(*gas_limit))] + pub fn contract_call_flip( + origin: OriginFor, + contract: AccountIdOf, + gas_limit: Weight, + storage_deposit_limit: Option>, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + let executor = + executor::PalletContractsExecutor:: { + origin: who.clone(), + contract: contract.clone(), + value: 0.into(), + gas_limit, + storage_deposit_limit, + marker: Default::default(), + }; + + let mut flipper = ink::message_builder!(Flip); + let result = flipper.flip().exec(&executor)?; + + assert!(result.is_ok()); + + Ok(()) + } + } +} diff --git a/runtime-call-contract/sandbox-runtime/src/lib.rs b/runtime-call-contract/sandbox-runtime/src/lib.rs new file mode 100644 index 00000000..78a39d89 --- /dev/null +++ b/runtime-call-contract/sandbox-runtime/src/lib.rs @@ -0,0 +1,11 @@ +//! # Contract Caller +//! +//! Demonstrates calling into an `ink!` contract from a pallet. + +#![cfg_attr(not(feature = "std"), no_std)] + +ink_sandbox::create_sandbox!(ContractCallerSandbox, ContractCallerSandboxRuntime, (), (), { + ContractCaller: pallet_contract_caller, +}); + +impl pallet_contract_caller::Config for ContractCallerSandboxRuntime {} diff --git a/runtime-call-contract/traits/Cargo.toml b/runtime-call-contract/traits/Cargo.toml new file mode 100644 index 00000000..101fd590 --- /dev/null +++ b/runtime-call-contract/traits/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "flipper-traits" +version = "5.1.0" +authors = ["Use Ink "] +edition = "2021" +publish = false + +[dependencies] +ink = { version = "5.1.0", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] diff --git a/runtime-call-contract/traits/lib.rs b/runtime-call-contract/traits/lib.rs new file mode 100644 index 00000000..294077a1 --- /dev/null +++ b/runtime-call-contract/traits/lib.rs @@ -0,0 +1,14 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +/// Allows to flip and get a bool value. +#[ink::trait_definition] +pub trait Flip { + /// Flip the value of the stored `bool` from `true` + /// to `false` and vice versa. + #[ink(message)] + fn flip(&mut self); + + /// Returns the current value of our `bool`. + #[ink(message)] + fn get(&self) -> bool; +}