Alert Source Discuss
⚠️ Review Standards Track: ERC

ERC-5164: Cross-Chain Execution

Defines an interface that supports execution across EVM networks.

Authors Brendan Asselstine (@asselstine), Pierrick Turelier (@PierrickGT), Chris Whinfrey (@cwhinfrey)
Created 2022-06-14

Abstract

This specification defines a cross-chain execution interface for EVM-based blockchains. Implementations of this specification will allow contracts on one chain to call contracts on another by sending a cross-chain message.

The specification defines two components: the “Message Dispatcher” and the “Message Executor”. The Message Dispatcher lives on the calling side, and the executor lives on the receiving side. When a message is sent, a Message Dispatcher will move the message through a transport layer to a Message Executor, where they are executed. Implementations of this specification must implement both components.

Motivation

Many Ethereum protocols need to coordinate state changes across multiple EVM-based blockchains. These chains often have native or third-party bridges that allow Ethereum contracts to execute code. However, bridges have different APIs so bridge integrations are custom. Each one affords different properties; with varying degrees of security, speed, and control. Defining a simple, common specification will increase code re-use and allow us to use common bridge implementations.

Specification

The key words “MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, and “OPTIONAL” in this document are to be interpreted as described in RFC 2119.

This specification allows contracts on one chain to send messages to contracts on another chain. There are two key interfaces that needs to be implemented:

  • MessageDispatcher
  • MessageExecutor

The MessageDispatcher lives on the origin chain and dispatches messages to the MessageExecutor for execution. The MessageExecutor lives on the destination chain and executes dispatched messages.

There are also extensions of MessageDispatcher, each defining a method for initiating a message or message batch:

  • SingleMessageDispatcher
  • BatchMessageDispatcher

Alternatively, MessageDispatchers may implement a custom interface for initiating messages.

MessageDispatcher

The MessageDispatcher lives on the chain from which messages are sent. The Dispatcher’s job is to broadcast messages through a transport layer to one or more MessageExecutor contracts.

A unique messageId MUST be generated for each message or message batch.

To ensure uniqueness, it is RECOMMENDED that a monotonically increasing nonce is used in the calculation of the messageId.

MessageDispatcher Events

MessageDispatched

The MessageDispatched event MUST be emitted by the MessageDispatcher when an individual message is dispatched.

interface MessageDispatcher {
  event MessageDispatched(
    bytes32 indexed messageId,
    address indexed from,
    uint256 indexed toChainId,
    address to,
    bytes data,
  );
}
- name: MessageDispatched
  type: event
  inputs:
    - name: messageId
      indexed: true
      type: bytes32
    - name: from
      indexed: true
      type: address
    - name: toChainId
      indexed: true
      type: uint256
    - name: to
      type: address
    - name: data
      type: bytes

MessageBatchDispatched

The MessageBatchDispatched event MUST be emitted by the MessageDispatcher when a batch of messages is dispatched.

struct Message {
  address to;
  bytes data;
}

interface MessageDispatcher {
  event MessageBatchDispatched(
    bytes32 indexed messageId,
    address indexed from,
    uint256 indexed toChainId,
    Message[] messages
  );
}
- name: MessageBatchDispatched
  type: event
  inputs:
    - name: messageId
      indexed: true
      type: bytes32
    - name: from
      indexed: true
      type: address
    - name: toChainId
      indexed: true
      type: uint256
    - name: messages
      type: Message[]

SingleMessageDispatcher

The SingleMessageDispatcher is an extension of MessageDispatcher that defines a method, dispatchMessage, for dispatching an individual message to be executed on the toChainId.

SingleMessageDispatcher Methods

dispatchMessage

Will dispatch a message to be executed by the MessageExecutor on the destination chain specified by toChainId.

SingleMessageDispatchers MUST emit the MessageDispatched event when a message is dispatched.

SingleMessageDispatchers MUST revert if toChainId is not supported.

SingleMessageDispatchers MUST forward the message to a MessageExecutor on the toChainId.

SingleMessageDispatchers MUST use a unique messageId for each message.

SingleMessageDispatchers MUST return the messageId to allow the message sender to track the message.

SingleMessageDispatchers MAY require payment.

interface SingleMessageDispatcher is MessageDispatcher {
  function dispatchMessage(uint256 toChainId, address to, bytes calldata data) external payable returns (bytes32 messageId);
}
- name: dispatchMessage
  type: function
  stateMutability: payable
  inputs:
    - name: toChainId
      type: uint256
    - name: to
      type: address
    - name: data
      type: bytes
  outputs:
    - name: messageId
      type: bytes32

BatchedMessageDispatcher

The BatchedMessageDispatcher is an extension of MessageDispatcher that defines a method, dispatchMessageBatch, for dispatching a batch of messages to be executed on the toChainId.

BatchedMessageDispatcher Methods

dispatchMessageBatch

Will dispatch a batch of messages to be executed by the MessageExecutor on the destination chain specified by toChainId.

BatchedMessageDispatchers MUST emit the MessageBatchDispatched event when a message batch is dispatched.

BatchedMessageDispatchers MUST revert if toChainId is not supported.

BatchedMessageDispatchers MUST forward the message batch to the MessageExecutor on the toChainId.

BatchedMessageDispatchers MUST use a unique messageId for each batch of messages.

BatchedMessageDispatchers MUST return the messageId to allow the message sender to track the batch of messages.

BatchedMessageDispatchers MAY require payment.

interface BatchedMessageDispatcher is MessageDispatcher {
  function dispatchMessageBatch(uint256 toChainId, Message[] calldata messages) external payable returns (bytes32 messageId);
}
- name: dispatchMessageBatch
  type: function
  stateMutability: payable
  inputs:
    - name: toChainId
      type: uint256
    - name: messages
      type: Message[]
  outputs:
    - name: messageId
      type: bytes32

MessageExecutor

The MessageExecutor executes dispatched messages and message batches. Developers must implement a MessageExecutor in order to execute messages on the receiving chain.

The MessageExecutor will execute a messageId only once, but may execute messageIds in any order. This specification makes no ordering guarantees, because messages and message batches may travel non-sequentially through the transport layer.

Execution

MessageExecutors SHOULD verify all message data with the bridge transport layer.

MessageExecutors MUST NOT successfully execute a message more than once.

MessageExecutors MUST revert the transaction when a message fails to be executed allowing the message to be retried at a later time.

Calldata

MessageExecutors MUST append the ABI-packed (messageId, fromChainId, from) to the calldata for each message being executed. This allows the receiver of the message to verify the cross-chain sender and the chain that the message is coming from.

to.call(abi.encodePacked(data, messageId, fromChainId, from));
- name: calldata
  type: bytes
  inputs:
    - name: data
      type: bytes
    - name: messageId
      type: bytes32
    - name: fromChainId
      type: uint256
    - name: from
      type: address

Error handling

MessageAlreadyExecuted

MessageExecutors MUST revert if a messageId has already been executed and SHOULD emit a MessageIdAlreadyExecuted custom error.

interface MessageExecutor {
  error MessageIdAlreadyExecuted(
    bytes32 messageId
  );
}

MessageFailure

MessageExecutors MUST revert if an individual message fails and SHOULD emit a MessageFailure custom error.

interface MessageExecutor {
  error MessageFailure(
    bytes32 messageId,
    bytes errorData
  );
}

MessageBatchFailure

MessageExecutors MUST revert the entire batch if any message in a batch fails and SHOULD emit a MessageBatchFailure custom error.

interface MessageExecutor {
  error MessageBatchFailure(
    bytes32 messageId,
    uint256 messageIndex,
    bytes errorData
  );
}

MessageExecutor Events

MessageIdExecuted

MessageIdExecuted MUST be emitted once a message or message batch has been executed.

interface MessageExecutor {
  event MessageIdExecuted(
    uint256 indexed fromChainId,
    bytes32 indexed messageId
  );
}
- name: MessageIdExecuted
  type: event
  inputs:
    - name: fromChainId
      indexed: true
      type: uint256
    - name: messageId
      indexed: true
      type: bytes32

Rationale

The MessageDispatcher can be coupled to one or more MessageExecutor. It is up to bridges to decide how to couple the two. Users can easily bridge a message by calling dispatchMessage without being aware of the MessageExecutor address. Messages can also be traced by a client using the data logged by the MessageIdExecuted event.

Some bridges may require payment in the native currency, so the dispatchMessage function is payable.

Backwards Compatibility

This specification is compatible with existing governance systems as it offers simple cross-chain execution.

Security Considerations

Bridge trust profiles are variable, so users must understand that bridge security depends on the implementation.

Copyright and related rights waived via CC0.

Citation

Please cite this document as:

Brendan Asselstine (@asselstine), Pierrick Turelier (@PierrickGT), Chris Whinfrey (@cwhinfrey), "EIP-5164: Cross-Chain Execution [DRAFT]," Ethereum Improvement Proposals, no. 5164, June 2022. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-5164.