Predeploys

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.

CrossL2Inbox

ConstantValue
Address0x4200000000000000000000000000000000000022

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:

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

_msg

The message payload of the executing message.

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

_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.

_target

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.

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(msg.sender == tx.origin);
    require(_id.timestamp <= block.timestamp);
    require(L1Block.isInDependencySet(_id.chainid));

    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 = SafeCall.call({
      _target: _target,
      _value: msg.value,
      _calldata: _msg
    });

    require(success);
}

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.

L2ToL2CrossDomainMessenger

ConstantValue
Address0x4200000000000000000000000000000000000023
MESSAGE_VERSIONuint256(0)

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 chainid MUST be equal to the local chainid
  • The CrossL2Inbox cannot call itself

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.

Interfaces

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);
    nonce++;
}

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.

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 {
      tstore(CROSS_DOMAIN_MESSAGE_SENDER_SLOT, _sender)
      tstore(CROSS_DOMAIN_MESSAGE_SOURCE_SLOT, _source)
    }

    bool success = SafeCall.call({
       _target: _target,
       _value: msg.value,
       _calldata: _message
    });

    require(success);

    sentMessages[messageHash] = true;
}

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.

L1Block

ConstantValue
Address0x4200000000000000000000000000000000000015

The L1Block contract is updated to include the set of allowed chains. The L1 Attributes transaction sets the set of allowed chains. The L1Block contract 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 the chain's chain id is passed in as an argument.

The setL1BlockValuesInterop() function MUST be called on every block after the interop upgrade block. The interop upgrade block itself MUST include a call to setL1BlockValuesEcotone.

L1Attributes

The L1 Atrributes transaction is updated to include the dependency set. Since the dependency set is dynamically sized, a uint8 "interopSetSize" parameter prefixes tightly packed uint256 values that represent each chain id.

Input argTypeCalldata bytesSegment
{0x760ee04d}bytes40-3n/a
baseFeeScalaruint324-71
blobBaseFeeScalaruint328-11
sequenceNumberuint6412-19
l1BlockTimestampuint6420-27
l1BlockNumberuint6428-35
basefeeuint25636-672
blobBaseFeeuint25668-993
l1BlockHashbytes32100-1314
batcherHashbytes32132-1635
interopSetSizeuint8164-1656
chainIdsuint256[interopSetSize]165-(32*interopSetSize)6+

Security Considerations

TODO