Table of Contents

Two new system level predeploys are introduced for managing cross chain messaging along with an update to the L1Block contract with additional functionality.



The CrossL2Inbox is responsible for executing a cross chain message on the destination chain. It is permissionless to execute a cross chain message on behalf of any user.

To ensure safety of the protocol, the Message Invariants must be enforced.

Message execution arguments

The following fields are required for executing a cross chain message:

_msgbytesThe message payload, matching the initiating message.
_idIdentifierA Identifier pointing to the initiating message.
_targetaddressAccount that is called with _msg.


The message payload of the executing message.

This must match the emitted payload of the initiating message identified by _id.


A pointer to the _msg in a remote (or local) chain.

The message Identifier of the executing message. This is required to enforce the message executes an existing and valid initiating message.

By including the Identifier in the calldata, it makes static analysis much easier for block builders. It is impossible to check that the Identifier matches the cross chain message on chain. If the block builder includes a message that does not correspond to the Identifier, their block will be reorganized by the derivation pipeline.

A possible upgrade path to this contract would involve adding a new function. If any fields in the Identifier change, then a new 4byte selector will be generated by solc.


Messages are broadcast, not directed. Upon execution the caller can specify which address to target: there is no protocol enforcement on what this value is.

The _target is called with the _msg as input. In practice, the _target will be a contract that needs to know the schema of the _msg so that it can be decoded. It MAY call back to the CrossL2Inbox to authenticate properties about the _msg using the information in the Identifier.

ExecutingMessage Event

The ExecutingMessage event represents an executing message. It MUST be emitted on every call to executeMessage.

event ExecutingMessage(bytes,bytes);

The data encoded in the event contains the Identifier and the msg. The following pseudocode shows the deserialization:

(bytes memory identifier, bytes memory log) = abi.decode(, (bytes, bytes));
Identifier id = abi.decode(identifier, (Identifier));

It is not possible to use solidity structs directly in events, which is why it is ABI encoded into bytes first.

Reference implementation

A simple implementation of the executeMessage function is included below.

function executeMessage(Identifier calldata _id, address _target, bytes calldata _msg) public payable {
    require(_id.timestamp <= block.timestamp);

    assembly {
      tstore(ORIGIN_SLOT, _id.origin)
      tstore(BLOCKNUMBER_SLOT, _id.blocknumber)
      tstore(LOG_INDEX_SLOT, _id.logIndex)
      tstore(TIMESTAMP_SLOT, _id.timestamp)
      tstore(CHAINID_SLOT, _id.chainid)

    bool success ={
      _target: _target,
      _value: msg.value,
      _calldata: _msg


    emit ExecutingMessage(abi.encode(_id), _msg);

Note that the executeMessage function is payable to enable relayers to earn in the gas paying asset.

Identifier Getters

The Identifier MUST be exposed via public getters so that contracts can call back to authenticate properties about the _msg.



The L2ToL2CrossDomainMessenger is a higher level abstraction on top of the CrossL2Inbox that provides features necessary for secure transfers ERC20 tokens between L2 chains. Messages sent through the L2ToL2CrossDomainMessenger on the source chain receive both replay protection as well as domain binding, ie the executing transaction can only be valid on a single chain.

relayMessage Invariants

  • Only callable by the CrossL2Inbox
  • The Identifier.origin MUST be address(L2ToL2CrossDomainMessenger)
  • The _destination chain id MUST be equal to the local chain id
  • The CrossL2Inbox cannot call itself

sendExpire Invariants

  • The message MUST have not been successfully relayed
  • The EXPIRY_WINDOW MUST have elapsed since the message first failed to be relayed
  • The expired message MUST not have been previously sent back to source
  • The expired message MUST not be relayable after being sent back

relayExpire Invariants

  • Only callable by the CrossL2Inbox
  • The message source MUST be block.chainid
  • The Identifier.origin MUST be address(L2ToL2CrossDomainMessenger)
  • The expiredMessages mapping MUST only contain messages that originated in this chain and failed to be relayed on destination.
  • Already expired messages MUST NOT be relayed.

Message Versioning

Versioning is handled in the most significant bits of the nonce, similarly to how it is handled by the CrossDomainMessenger.

function messageNonce() public view returns (uint256) {
    return Encoding.encodeVersionedNonce(nonce, MESSAGE_VERSION);

No Native Support for Cross Chain Ether Sends

To enable interoperability between chains that use a custom gas token, there is no native support for sending ether between chains. ether must first be wrapped into WETH before sending between chains.


The L2ToL2CrossDomainMessenger uses a similar interface to the L2CrossDomainMessenger but the _minGasLimit is removed to prevent complexity around EVM gas introspection and the _destination chain is included instead.

Sending Messages

The initiating message is represented by the following event:

event SentMessage(bytes message) anonymous;

The bytes are an ABI encoded call to relayMessage. The event is defined as anonymous so that no topics are prefixed to the abi encoded call.

An explicit _destination chain and nonce are used to ensure that the message can only be played on a single remote chain a single time. The _destination is enforced to not be the local chain to avoid edge cases.

There is no need for address aliasing as the aliased address would need to commit to the source chain's chain id to create a unique alias that commits to a particular sender on a particular domain and it is far more simple to assert on both the address and the source chain's chain id rather than assert on an unaliased address. In both cases, the source chain's chain id is required for security. Executing messages will never be able to assume the identity of an account because msg.sender will never be the identity that initiated the message, it will be the L2ToL2CrossDomainMessenger and users will need to callback to get the initiator of the message.

function sendMessage(uint256 _destination, address _target, bytes calldata _message) external {
    require(_destination != block.chainid);

    bytes memory data = abi.encodeCall(L2ToL2CrossDomainMessenger.relayMessage, (_destination, block.chainid, messageNonce(), msg.sender, _target, _message));
    emit SentMessage(data);

Note that sendMessage is not payable.

Relaying Messages

When relaying a message through the L2ToL2CrossDomainMessenger, it is important to require that the _destination equal to block.chainid to ensure that the message is only valid on a single chain. The hash of the message is used for replay protection.

It is important to ensure that the source chain is in the dependency set of the destination chain, otherwise it is possible to send a message that is not playable.

When a message fails to be relayed, only the timestamp at which it first failed along with its source chain id are stored. This is needed for calculation of the failed message's expiry. The source chain id is also required to simplify the function signature of sendExpire.

function relayMessage(uint256 _destination, uint256 _source, uint256 _nonce, address _sender, address _target, bytes memory _message) external payable {
    require(msg.sender == address(CROSS_L2_INBOX));
    require(_destination == block.chainid);
    require(CROSS_L2_INBOX.origin() == address(this));

    bytes32 messageHash = keccak256(abi.encode(_destination, _source, _nonce, _sender, _target, _message));
    require(sentMessages[messageHash] == false);

    assembly {

    sentMessages[messageHash] = true;

    bool success ={
       _target: _target,
       _value: msg.value,
       _calldata: _message

    if (!success) {
      emit FailedRelayedMessage(messageHash);

      if (failedMessages[messageHash].timestamp == 0) {
        failedMessages[messageHash] = FailedMessage({timestamp: block.timestamp, sourceChainId: _source});


    successfulMessages[messageHash] = true;
    delete failedMessages[messageHash];
    emit RelayedMessage(messageHash);

Note that the relayMessage function is payable to enable relayers to earn in the gas paying asset.

To enable cross chain authorization patterns, both the _sender and the _source MUST be exposed via public getters.

Sending Expired Message Hashes

When expiring a message that failed to be relayed on the destination chain to the source chain, it's crucial to ensure the message can only be sent back to the L2ToL2CrossDomainMessenger contract in its source chain.

This function has no auth, which allows anyone to expire a given message hash. The EXPIRY_WINDOW variable is added to give the users enough time to replay their failed messages and to prevent malicious actors from performing a griefing attack by expiring messages upon arrival.

Once the expired message is sent to the source chain, the message on the local chain is set as successful in the successfulMessages mapping to ensure non-replayability and deleted from failedMessages. An initiating message is then emitted to relayExpire

function sendExpire(bytes32 _expiredHash) external nonReentrant {
    if (successfulMessages[_expiredHash]) revert MessageAlreadyRelayed();

    (uint256 messageTimestamp, uint256 messageSource) = failedMessages[_expiredHash];

    if (block.timestamp <  messageTimestamp + EXPIRY_WINDOW) revert ExpiryWindowHasNotEnsued();

    delete failedMessages[_expiredHash];
    successfulMessages[_expiredHash] = true;

    bytes memory data = abi.encodeCall(
        (_expiredHash, messageSource)
    emit SentMessage(data);

Relaying Expired Message Hashes

When relaying an expired message, only message hashes of actual failed messages should be stored, for this we must ensure the origin of the log, and caller are all expected contracts.

It's also important to ensure only the hashes of messages that were initiated in this chain are accepted.

If all checks have been successful, the message has is stored in the expiredMessages mapping. This enables smart contracts to read from it and check whether a message expired or not, and handle this case accordingly.

function relayExpire(bytes32 _expiredHash, uint256 _messageSource) external {
    if (_messageSource != block.chainid) revert IncorrectMessageSource();
    if (expiredMessages[_expiredHash] != 0) revert ExpiredMessageAlreadyRelayed();
    if (msg.sender != Predeploys.CROSS_L2_INBOX) revert ExpiredMessageCallerNotCrossL2Inbox();

    if (CrossL2Inbox(Predeploys.CROSS_L2_INBOX).origin() != Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER) {
        revert CrossL2InboxOriginNotL2ToL2CrossDomainMessenger();

    expiredMessages[_expiredHash] = block.timestamp;

    emit MessageHashExpired(_expiredHash);



Static Configuration

The L1Block contract MUST include method setConfig(ConfigType, bytes) for setting the system's static values, which are defined as values that only change based on the chain operator's input. This function serves to reduce the size of the L1 Attributes transaction, as well as to reduce the need to add specific one off functions. It can only be called by DEPOSITOR_ACCOUNT.

The ConfigType enum is defined as follows:

enum ConfigType {

The second argument to setConfig is a bytes value that is ABI encoded with the necessary values for the ConfigType.

SET_GAS_PAYING_TOKENabi.encode(token, decimals, name, symbol)


  • token is the gas paying token's address (type address)

  • decimals is the gas paying token's decimals (type uint8)

  • name is the gas paying token's name (type bytes32)

  • symbol is the gas paying token's symbol (type bytes32)

  • chainId is the chain id intended to be added or removed from the dependency set (type uint256)

Calls to setConfig MUST originate from SystemConfig and are forwarded to L1Block by OptimismPortal.

Dependency Set

L1Block is updated to include the set of allowed chains. These chains are added and removed through setConfig calls with ADD_DEPENDENCY or REMOVE_DEPENDENCY, respectively. The maximum size of the dependency set is type(uint8).max, and adding a chain id when the dependency set size is at its maximum MUST revert. If a chain id already in the dependency set, such as the chain's chain id, is attempted to be added, the call MUST revert. If a chain id that is not in the dependency set is attempted to be removed, the call MUST revert. If the chain's chain id is attempted to be removed, the call also MUST revert.

L1Block MUST provide a public getter to check if a particular chain is in the dependency set called isInDependencySet(uint256). This function MUST return true when a chain id in the dependency set, or the chain's chain id, is passed in as an argument, and false otherwise. Additionally, L1Block MUST provide a public getter to return the dependency set called dependencySet(). This function MUST return the array of chain ids that are in the dependency set. L1Block MUST also provide a public getter to get the dependency set size called dependencySetSize(). This function MUST return the length of the dependency set array.

Security Considerations