Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Refactor XCM Contract: Enhanced Documentation, Error Handling, and Test Structure #2361

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
235 changes: 60 additions & 175 deletions integration-tests/public/contract-xcm/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,265 +7,150 @@ mod contract_xcm {
xcm::prelude::*,
};

/// A trivial contract used to exercise XCM API.
/// A smart contract example using the XCM API for cross-chain communication.
#[ink(storage)]
#[derive(Default)]
pub struct ContractXcm;

/// Enumeration of runtime errors for the contract.
#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
pub enum RuntimeError {
XcmExecuteFailed,
XcmSendFailed,
UnexpectedEnvError,
}

impl From<EnvError> 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`."),
EnvError::ReturnError(code) => match code {
ink::env::ReturnErrorCode::XcmExecutionFailed => RuntimeError::XcmExecuteFailed,
ink::env::ReturnErrorCode::XcmSendFailed => RuntimeError::XcmSendFailed,
_ => RuntimeError::UnexpectedEnvError,
},
_ => RuntimeError::UnexpectedEnvError,
}
}
}

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.
/// The constructor is `payable`, allowing the contract to receive initial tokens.
#[ink(constructor, payable)]
pub fn new() -> Self {
Default::default()
}

/// Tries to transfer `value` from the contract's balance to `receiver`.
/// Helper function to build an XCM message.
///
/// # Arguments
/// * `receiver` - The target account to receive the assets.
/// * `value` - The amount of tokens to transfer.
/// * `fee` - Optional fee for the XCM execution.
fn build_xcm_message(
&self,
receiver: AccountId32,
value: Balance,
fee: Option<Balance>,
) -> Xcm<()> {
let asset: Asset = (Here, value).into();
let mut builder = Xcm::builder()
.withdraw_asset(asset.clone())
.deposit_asset(asset.clone(), receiver);
if let Some(fee) = fee {
builder = builder.buy_execution((Here, fee), WeightLimit::Unlimited);
}
builder.build()
}

/// Transfers funds through XCM to the given 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)
/// - XCM execution fails.
/// - Insufficient funds.
/// - Unsupported environment or runtime configuration.
#[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())
.buy_execution(asset.clone(), Unlimited)
.deposit_asset(asset, beneficiary)
.build();
let message = self.build_xcm_message(beneficiary, value, None);

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.
/// Sends funds through XCM, paying a fee for execution.
///
/// 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)
/// - XCM execution fails.
/// - Insufficient funds or fees.
/// - Unsupported environment or runtime configuration.
#[ink(message)]
pub fn send_funds(
&mut self,
value: Balance,
fee: Balance,
) -> Result<XcmHash, RuntimeError> {
let destination: Location = Parent.into();
let asset: Asset = (Here, value).into();
let beneficiary = AccountId32 {
network: None,
id: *self.env().caller().as_ref(),
};
let destination: Location = Parent.into();
let message = self.build_xcm_message(beneficiary, value, Some(fee));

let message: Xcm<()> = Xcm::builder()
.withdraw_asset(asset.clone())
.buy_execution((Here, fee), WeightLimit::Unlimited)
.deposit_asset(asset, beneficiary)
.build();

let hash = self.env().xcm_send(
&VersionedLocation::V4(destination),
&VersionedXcm::V4(message),
)?;

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 super::*;
use ink::{
env::{
test::default_accounts,
DefaultEnvironment,
},
env::{test::default_accounts, DefaultEnvironment},
primitives::AccountId,
};
use ink_e2e::{
preset::mock_network::{
self,
primitives::{
CENTS,
UNITS,
},
MockNetworkSandbox,
},
preset::mock_network::{primitives::CENTS, 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;
/// Initial contract balance for testing.
pub const CONTRACT_BALANCE: u128 = 1_000_000;

type E2EResult<T> = Result<T, Box<dyn std::error::Error>>;

#[ink_e2e::test(backend(runtime_only(sandbox = MockNetworkSandbox)))]
async fn xcm_execute_works<Client: E2EBackend>(
async fn transfer_through_xcm_works<Client: E2EBackend>(
mut client: Client,
) -> E2EResult<()> {
// given
// Arrange: Instantiate the contract with a predefined balance.
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::<ContractXcm>();
.expect("instantiation failed");

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

// This will fail since we have insufficient balance
let transfer_message = call_builder.transfer_through_xcm(
default_accounts::<DefaultEnvironment>().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<Client: E2EBackend>(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::<ContractXcm>();
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);
});
// Act: Execute the transfer through XCM.
let transfer_message = contract
.call_builder()
.transfer_through_xcm(receiver, 1_000 * CENTS);

let result = client.call(&ink_e2e::alice(), &transfer_message).submit().await?;
assert!(result.return_value().is_ok());
Ok(())
}
}
Expand Down
Loading