paritytech / polkadot-sdk

The Parity Polkadot Blockchain SDK
https://polkadot.network/
1.78k stars 640 forks source link

contracts: XCM support #121

Open HCastano opened 2 years ago

HCastano commented 2 years ago

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

A while ago I created a chain extension to support executing, sending and receiving XCM messages from a contract: https://github.com/paritytech/pallet-contracts-xcm

This is a prototype but already outlines all the features we need:

It doesn't do this in a secure way, though. It is a prototype. For example, it doesn't charge storage deposits for the reply id and reply buffer.

It is a chain extension rather than a core API solely because of technical reasons: It depends on the XCM crates which are part of the polkadot repo. However, pallet-contracts is in the substrate repository on which polkadot depends. Therefore pallet-contracts can't depend on XCM as this would be a circle. As a workaround we placed the code into another repository. The only way to do that is a chain extension. The real implementation should be bundled with pallet-contracts once we have the monorepo. It still needs to be optional as some chains might decide not to implement it. Hence it might make sense to keep it a chain extension rather than a core API.

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: [](https://github.com/orgs/paritytech/projects/29/views/2)
bernardoaraujor commented 2 years ago

hi guys 👋

how will XCM over SCs work in terms of:

cc @KiChjang

athei commented 2 years ago

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:

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.

athei commented 2 years ago

Integrate XCM: Discussion 2021-11-16

Whiteboard Mess

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

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-message-flow

Cross Chain Error Handling

Cross-Chain Messages

Two problems here:

  1. 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
  2. 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.

cmichi commented 2 years ago

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:

Test use-cases for our MVP:

cmichi commented 2 years ago

Persisting this here: It's still unclear how the UI would display our MVP in a meaningful way.

athei commented 2 years ago

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

athei commented 2 years ago

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.

ashutoshvarma commented 1 year ago

@athei Can you elaborate on why the callback design was discarded in favor of polling? pallet-xcm's OnResponse already provides the dispatch callbacks for queries (notify).

athei commented 1 year ago

Because it is unclear who pays for for the execution when there is no extrinsic attached to the execution. Even without the problem of fees we have the problem that the callback will be called from on_initialize which we want to avoid. Additionally it introduces difficult error states: What if the max_weight supplied to the call back is too low. You have no way of recovering. The contract state will be in limbo waiting for the the reply. Attaching this to an extrinsic will allow for re-try with higher weight.

It seems to me as a much simpler design.