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
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000022 |
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:
Name | Type | Description |
---|---|---|
_msg | bytes | The message payload, matching the initiating message. |
_id | Identifier | A Identifier pointing to the initiating message. |
_target | address | Account 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
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000023 |
MESSAGE_VERSION | uint256(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 beaddress(L2ToL2CrossDomainMessenger)
- The
_destination
chain id MUST be equal to the local chain id - 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
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000015 |
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 arg | Type | Calldata bytes | Segment |
---|---|---|---|
{0x760ee04d} | bytes4 | 0-3 | n/a |
baseFeeScalar | uint32 | 4-7 | 1 |
blobBaseFeeScalar | uint32 | 8-11 | |
sequenceNumber | uint64 | 12-19 | |
l1BlockTimestamp | uint64 | 20-27 | |
l1BlockNumber | uint64 | 28-35 | |
basefee | uint256 | 36-67 | 2 |
blobBaseFee | uint256 | 68-99 | 3 |
l1BlockHash | bytes32 | 100-131 | 4 |
batcherHash | bytes32 | 132-163 | 5 |
interopSetSize | uint8 | 164-165 | 6 |
chainIds | uint256[interopSetSize] | 165-(32*interopSetSize) | 6+ |
Security Considerations
TODO