XCM Support for contracts
The Vision
Contracts should be able to execute and send arbitrary XCM messages. This will allow contracts to participate in the wider ecosystem. A common use case would be to make use of assets on statemint.
The Plan
- Write a chain extension that allows sending XCM messages and receiving replies
- Add support in ink! for this chain extension
- Write example contracts with comon use cases
Open Questions
If you want to help us out and contribute to this issue, in this section you can find open questions and tasks where we would appreciate any input.
Here you can find the board with specific sub-tasks to this milestone: <Link to your project's view>
hi guys 👋
how will XCM over SCs work in terms of:
- XCMP's asynchronicity - How can SC logic "await"?
- XCMP irreversibility - How are cancelled SCs handled after assets have already been teleported away from the contract chain?
cc @KiChjang
We add a new host function that allows sending XCM functions to a MultiLocation. The contracts pallet decodes bytes and checks the XCM version and returns an error when the dest is not able to receive the used version. Apart from that we do not care at all about the contents of bytes.
ink_env::send_xcm(dest: MultiLocation, bytes: &[u8]) -> Result<(), ErrorCode>;
Whenever a contract is the target of an XCM message it gets called and the gas meter is set according to the BuyWeight instruction of the received XCM message. We add another exported function xcm_received that is called instead of call in this case. seal_input will return the XCM message and seal_caller the MultiLocation sender of the message.
XCMP's asynchronicity - How can SC logic "await"?
The send_xcm function will be fire and forget. If a reply is expected this needs to be modeled in terms of code in the xcm_received function. Specifying a callback at call side won't work because the XCM code is mostly opaque to pallet contracts for now.
I expect it to be done like this:
- Send xcm message
- Write to storage that there is a pending operation
- End current contract call
- XCM reply triggers
send_xcmwhich finalizes the operation in storage
XCMP irreversibility - How are cancelled SCs handled after assets have already been teleported away from the contract chain?
We can buffer all XCM messages and only emit them at the end if the contract call did not revert.
Integrate XCM: Discussion 2021-11-16

ink_env::send_xcm(location: MultiLocation, bytes: &[u8])
Example Usage in ink!:
// Contract developer is responsible for ensuring this
assert!(self.env().transferred_value >= execution_to_buy);
// It's the responsibility of the contract developer to ensure that the right amount of Weight is paid for.
// We may also need to assume that the `MultiAsset` being sent is initially just the Native asset of the chain
let xcm_msg: XcmMessage = {
…,
QueryId,
WithdrawAsset(MultiAsset),
BuyExecution(execution_to_buy),
RefundSurplus(…)
};
// TODO: XCM currently doesn't support a `SmartContract` `MultiLocation` destination, we'll need to add support for that
let location = MultiLocation(…);
ink_env::send_xcm(location, xcm_msg.encode()) -> Result<(), XcmSendError>;
enum XcmSendError {
XcmVersionNotSupported,
UnknownLocation,
}
pallet-contracts
seal_send_xcm(location: MultiLocation, msg: &[u8]) ➜ XcmSendError
- Implement routing for
MultiLocationto contracts. BuyExecutionhas to be reflected to contracts.- It is subtracted from contract balance.
- Contract developer must make sure
transferred_valueis sufficient, otherwise they sabotage their own contract.
- XCM-Origin is set to contract account ➜ The contract pays for XCM.
- Buffer XCM Message, send it only out if contract didn't revert.
- Add an RPC call for dry-running an XCM Message.
- This enables developers to get an estimate of how much value they should put into the
BuyExecution(value, …)for the callback into their contract.
- This enables developers to get an estimate of how much value they should put into the
Note
Currently, pallet-contracts enforces the free balance of a contract's account to be larger than the existential deposit. This is so no dust related accidents can happen. With these changes we can no longer enforce this invariant because we can't control the contents of the XCM (it could send away its own balance). I suggest:
pallet-contracts makes sure that the initial deposit that is subtracted from the caller in order to pay for the contract data structure is at least the existential deposit. It cannot be moved away by XCM and it can only be removed when the contract terminates.
This makes sure that a contract's acount always exist on-chain and no dust related accidents can happen.
XCM Receive
extern "C" {
/// Called by pallet_contracts when a xcm message is addressed
/// to a contract.
pub fn xcm_receive() {
xcm_receive(seal_caller(), seal_input());
}
}
// In `ink!`:
#[ink(xcm_receive)]
pub fn ink_xcm_receive(src: MultiLocation, msg: &[u8]) {
// Handle stuff
// Called by `xcm_receive`
}
Cross Chain Message Flow

Cross Chain Error Handling

Two problems here:
- Our contract updates some of its state, and a call to a remote chain fails. There's no way for us to revert the state of our smart contract
- We make a cross chain call which updates the state of the target chain, afterwards we resume execution and our smart contract fails. There's no way to revert the changes on the remote chain
We figured that maybe if a receive failed we could emit an event to notify external actors. However, a problem arises when we think about who actually ends up paying for the event. We aren't able to allocate any weight in WithdrawAsset or BuyExecution since that weight could be spent before we are able to emit an event.
// Event emitted by `pallet-contracts`
struct XcmReceiveFailure {
err: DispatchError,
location: MultiLocation,
}
Doing it anyway
We figured that we should provide xcm_receive anyway, even without a foolproof solution to reversion and error reporting. Sending out an event on receive failure might also be too complicated.
We basically provide this as a "no guards included" bare functionality and monitor what people build with it. This will give us more insight in howto provide more safe abstractions.
Persisting some of our discussions:
#[ink(xcm_receive, max_gas_consumed = 50_000)]
pub fn ink_xcm_receive(src: MultiLocation, msg: &[u8]) {
if msg[0] == 1 {
// expensive computation
} else {
// cheap computation
}
}
The max_gas_consumed value is put into the metadata, so that the calling parachain has information about which value they should put for BuyExecution: it would be weight_limit: Some(max_gas_consumed).
Note: Gas is a synonym for Weight.
How a contract would be called:
XCM {
BuyExecution(…),
Transact(…),
}
Necessary follow-up issues:
- [ ] Add a new RPC in
pallet-contractsfordry_run_xcm_receive. - [ ] Add a sub-command for
cargo-contractto executedry_run_xcm_receive.
Test use-cases for our MVP:
- [ ] Call Statemine, transer asset from one account to another, send reply to contract that this succeeded.
- [ ] Call contract on another chain and send reply.
Persisting this here: It's still unclear how the UI would display our MVP in a meaningful way.
We will do the MVP as a chain extension in a separate repository to get around the fact that we can't depend on polkadot from substrate. This will be useful for that: https://github.com/paritytech/substrate/issues/11751
Delayed to next month because it needs to be merged into polkadot and that takes more work than just an MVP: Docs, Tests, Review cycle.