Predeploys
Table of Contents
- Overview
- CrossL2Inbox
- L2ToL2CrossDomainMessenger
- OptimismSuperchainERC20Factory
- BeaconContract
- L1Block
- OptimismMintableERC20Factory
- L2StandardBridge
- SuperchainERC20Bridge
- Security Considerations
Overview
Four new system level predeploys are introduced for managing cross chain messaging and tokens, along with
an update to the L1Block
, OptimismMintableERC20Factory
and L2StandardBridge
contracts with additional functionalities.
CrossL2Inbox
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000022 |
The CrossL2Inbox
is the system predeploy for cross chain messaging. Anyone can trigger the execution or validation
of cross chain messages, on behalf of any user.
To ensure safety of the protocol, the Message Invariants must be enforced.
Functions
executeMessage
Executes a cross chain message and performs a CALL
with the payload to the provided target address, allowing
introspection of the data.
Signals the transaction has a cross chain message to validate by emitting the ExecutingMessage
event.
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 . |
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
.
function executeMessage(Identifier calldata _id, address _target, bytes memory _message)
validateMessage
A helper to enable contracts to provide their own public entrypoints for cross chain interactions.
Emits the ExecutingMessage
event to signal the transaction has a cross chain message to validate.
The following fields are required for validating a cross chain message:
Name | Type | Description |
---|---|---|
_id | Identifier | A Identifier pointing to the initiating message. |
_msgHash | bytes32 | The keccak256 hash of the message payload matching the initiating message. |
function validateMessage(Identifier calldata _id, bytes32 _msgHash)
Interop Start Timestamp
The Interop Start Timestamp represents the earliest timestamp which an initiating message (identifier) can have to be considered valid. This is important because OP Mainnet migrated from a legacy system that is not provable. We cannot allow for interop messages to come from unproven parts of the chain history, since interop is secured by fault proofs.
Interop Start Timestamp is stored in the storage of the CrossL2Inbox predeploy. During the Interop Network Upgrade,
each chain sets variable via a call to setInteropStart()
by the DEPOSITOR_ACCOUNT
which sets Interop Start Timestamp
to be the block.timestamp of the network upgrade block. Chains deployed after the network upgrade will have to enshrine
that timestamp into the pre-determined storage slot.
ExecutingMessage
Event
The ExecutingMessage
event represents an executing message. It MUST be emitted on every call
to executeMessage
and validateMessage
.
event ExecutingMessage(bytes32 indexed msgHash, Identifier identifier);
The data encoded in the event contains the keccak hash of the msg
and the Identifier
.
The following pseudocode shows the deserialization:
bytes32 msgHash = log.topics[1];
Identifier identifier = abi.decode(log.data, (Identifier));
Emitting the hash of the message is more efficient than emitting the message in its entirety. Equality with the initiating message can be handled off-chain through hash comparison.
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);
require(L1Block.isInDependencySet(_id.chainid));
require(_id.timestamp > interopStart());
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);
emit ExecutingMessage(keccak256(_msg), _id);
}
Note that the executeMessage
function is payable
to enable relayers to earn in the gas paying asset.
An example of encoding a cross chain call directly in an event. However realize the L2ToL2CrossDomainMessenger predeploy provides a cleaner and user friendly abstraction for cross chain calls.
contract MyCrossChainApp {
function sendMessage() external {
bytes memory data = abi.encodeCall(MyCrossChainApp.relayMessage, (1, address(0x20)));
// Encoded payload matches the required calldata by omission of an event topic
assembly {
log0(add(data, 0x20), mload(data))
}
}
function relayMessage(uint256 value, address recipient) external {
// Assert that this is only executed directly from the inbox
require(msg.sender == Predeploys.CrossL2Inbox);
}
}
An example of a custom entrypoint utilizing validateMessage
to consume a known
event. Note that in this example, the contract is consuming its own event
from another chain, however any event emitted from any contract is consumable!
contract MyCrossChainApp {
event MyCrossChainEvent();
function sendMessage() external {
emit MyCrossChainEvent();
}
function relayMessage(Identifier calldata _id, bytes calldata _msg) external {
// Example app-level validation
// - Expected event via the selector (first topic)
// - Assertion on the expected emitter of the event
require(MyCrossChainEvent.selector == _msg[:32]);
require(_id.origin == address(this));
// Authenticate this cross chain message
CrossL2Inbox.validateMessage(_id, keccak256(_msg));
// ABI decode the event message & perform actions.
// ...
}
}
Deposit Handling
Any call to the CrossL2Inbox
that would emit an ExecutingMessage
event will reverts
if the call is made in a deposit context.
The deposit context status can be determined by calling isDeposit
on the L1Block
contract.
In the future, deposit handling will be modified to be more permissive. It will revert only in specific cases where interop dependency resolution is not feasible.
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 general message passing, utilized 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
- The
Identifier.origin
MUST beaddress(L2ToL2CrossDomainMessenger)
- The
_destination
chain id MUST be equal to the local chain id - Messages MUST NOT be relayed more than once
sendMessage
Invariants
- Sent Messages MUST be uniquely identifiable
- It must emit the
SentMessage
event
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.
See SuperchainWETH for more information.
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 following function is used for sending messages between domains:
function sendMessage(uint256 _destination, address _target, bytes calldata _message) external returns (bytes32);
It returns the hash of the message being sent,
used to track whether the message has successfully been relayed.
It emits a SentMessage
event with the necessary metadata to execute when relayed on the destination chain.
event SentMessage(uint256 indexed destination, address indexed target, uint256 indexed messageNonce, address sender, bytes message);
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.
The _destination
MUST NOT be the chainid of the local chain and a locally defined nonce
MUST increment on
every call to sendMessage
.
Note that sendMessage
is not payable
.
Relaying Messages
The following diagram shows the flow for sending a cross chain message using the L2ToL2CrossDomainMessenger
.
Each subsequent call is labeled with a number.
flowchart LR user -->|"1#46; sendMessage"| al2tol2 user --> |"2#46; relayMessage"|bl2tol2 im{{SentMessage Event}} em{{ExecutingMessage Event}} direction TB al2tol2 --> im bcl2[CrossL2Inbox] al2tol2[L2ToL2CrossDomainMessenger] bl2tol2[L2ToL2CrossDomainMessenger] subgraph "Chain A" al2tol2 end subgraph "Chain B" bl2tol2 --> |"3#46; validateMessage"|bcl2 bcl2 --> em bl2tol2 --> |"4#46;"| Contract end
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.
A message is relayed by providing the identifier to a SentMessage
event and its corresponding message payload.
function relayMessage(ICrossL2Inbox.Identifier calldata _id, bytes calldata _sentMessage) external payable returns (bytes memory returnData_) {
require(_id.origin == Predeploys.L2_TO_L2_CROSS_DOMAIN_MESSENGER);
CrossL2Inbox(Predeploys.CROSS_L2_INBOX).validateMessage(_id, keccak256(_sentMessage));
// log topics
(bytes32 selector, uint256 _destination, address _target, uint256 _nonce) =
abi.decode(_sentMessage[:128], (bytes32,uint256,address,uint256));
require(selector == SentMessage.selector);
require(_destination == block.chainid);
// log data
(address _sender, bytes memory _message) = abi.decode(_sentMessage[128:], (address,bytes));
bool success;
(success, returnData_) = _target.call(_target, msg.value, _message);
require(success);
successfulMessages[messageHash] = true;
emit RelayedMessage(_source, _nonce, 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.
OptimismSuperchainERC20Factory
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000026 |
OptimismSuperchainERC20
The OptimismSuperchainERC20Factory
creates ERC20 contracts that implements the SuperchainERC20
standard,
grants mint-burn rights to the L2StandardBridge
(OptimismSuperchainERC20
)
and include a remoteToken
variable.
These ERC20s are called OptimismSuperchainERC20
and can be converted back and forth with OptimismMintableERC20
tokens.
The goal of the OptimismSuperchainERC20
is to extend functionalities
of the OptimismMintableERC20
so that they are interop compatible.
Overview
Anyone can deploy OptimismSuperchainERC20
contracts by using the OptimismSuperchainERC20Factory
.
Proxy
The OptimismSuperchainERC20Factory
MUST be a proxied predeploy.
It follows the
Proxy.sol
implementation
and delegatecall()
to the factory implementation address.
Beacon Pattern
It MUST deploy OptimismSuperchainERC20
as
BeaconProxies,
as this is the easiest way to upgrade multiple contracts simultaneously.
Each BeaconProxy delegatecalls to the implementation address provided by the Beacon Contract.
The implementation MUST include an initialize
function that
receives (address _remoteToken, string _name, string _symbol, uint8 _decimals)
and stores these in the BeaconProxy storage.
Deployment history
The L2StandardBridge
includes a convert()
function that allows anyone to convert
between any OptimismMintableERC20
and its corresponding OptimismSuperchainERC20
.
For this method to work, the OptimismSuperchainERC20Factory
MUST include a deployment history.
Functions
deploy
Creates an instance of the OptimismSuperchainERC20
contract with a set of metadata defined by:
_remoteToken
: address of the underlying token in its native chain._name
:OptimismSuperchainERC20
name_symbol
:OptimismSuperchainERC20
symbol_decimals
:OptimismSuperchainERC20
decimals
function deploy(address _remoteToken, string memory _name, string memory _symbol, uint8 _decimals) returns (address)
It returns the address of the deployed OptimismSuperchainERC20
.
The function MUST use CREATE3
to deploy its children.
This ensures the same address deployment across different chains,
which is necessary for the standard implementation.
The salt used for deployment MUST be computed by applying keccak256
to the abi.encode
of the input parameters (_remoteToken
, _name
, _symbol
, and _decimals
).
This implies that the same L1 token can have multiple OptimismSuperchainERC20
representations as long as the metadata changes.
The function MUST store the _remoteToken
address for each deployed OptimismSuperchainERC20
in a deployments
mapping.
Events
OptimismSuperchainERC20Created
It MUST trigger when deploy
is called.
event OptimismSuperchainERC20Created(address indexed superchainToken, address indexed remoteToken, address deployer);
where superchainToken
is the address of the newly deployed OptimismSuperchainERC20
,
remoteToken
is the address of the corresponding token in L1,
and deployeris the
msg.sender`.
Deployment Flow
sequenceDiagram participant Alice participant FactoryProxy participant FactoryImpl participant BeaconProxy as OptimismSuperchainERC20 BeaconProxy participant Beacon Contract participant Implementation Alice->>FactoryProxy: deploy(remoteToken, name, symbol, decimals) FactoryProxy->>FactoryImpl: delegatecall() FactoryProxy->>BeaconProxy: deploy with CREATE3 FactoryProxy-->FactoryProxy: deployments[superchainToken]=remoteToken FactoryProxy-->FactoryProxy: emit OptimismSuperchainERC20Created(superchainToken, remoteToken, Alice) BeaconProxy-->Beacon Contract: reads implementation() BeaconProxy->>Implementation: delegatecall() BeaconProxy->>Implementation: initialize()
BeaconContract
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000027 |
Overview
The BeaconContract
predeploy gets called by the OptimismSuperchainERC20
BeaconProxies deployed by the
SuperchainERC20Factory
The Beacon Contract implements the interface defined in EIP-1967.
The implementation address gets deduced similarly to the GasPriceOracle
address in Ecotone and Fjord updates.
L1Block
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000015 |
DEPOSITOR_ACCOUNT | 0xDeaDDEaDDeAdDeAdDEAdDEaddeAddEAdDEAd0001 |
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 {
SET_GAS_PAYING_TOKEN,
ADD_DEPENDENCY,
REMOVE_DEPENDENCY
}
The second argument to setConfig
is a bytes
value that is ABI encoded with the necessary values for the ConfigType
.
ConfigType | Value |
---|---|
SET_GAS_PAYING_TOKEN | abi.encode(token, decimals, name, symbol) |
ADD_DEPENDENCY | abi.encode(chainId) |
REMOVE_DEPENDENCY | abi.encode(chainId) |
where
-
token
is the gas paying token's address (typeaddress
) -
decimals
is the gas paying token's decimals (typeuint8
) -
name
is the gas paying token's name (typebytes32
) -
symbol
is the gas paying token's symbol (typebytes32
) -
chainId
is the chain id intended to be added or removed from the dependency set (typeuint256
)
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.
Deposit Context
New methods will be added on the L1Block
contract to interact with deposit contexts.
function isDeposit() public view returns (bool);
function depositsComplete() public;
isDeposit()
Returns true if the current execution occurs in a deposit context.
Only the CrossL2Inbox
is authorized to call isDeposit
.
This is done to prevent apps from easily detecting and censoring deposits.
depositsComplete()
Called after processing the first L1 Attributes transaction and user deposits to destroy the deposit context.
Only the DEPOSITOR_ACCOUNT
is authorized to call depositsComplete()
.
OptimismMintableERC20Factory
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000012 |
OptimismMintableERC20
The OptimismMintableERC20Factory
creates ERC20 contracts on L2 that can be used to deposit
native L1 tokens into (OptimismMintableERC20
). Anyone can deploy OptimismMintableERC20
contracts.
Each OptimismMintableERC20
contract created by the OptimismMintableERC20Factory
allows for the L2StandardBridge
to mint
and burn tokens, depending on whether the user is
depositing from L1 to L2 or withdrawing from L2 to L1.
Updates
The OptimismMintableERC20Factory
is updated to include a deployments
mapping
that stores the remoteToken
address for each deployed OptimismMintableERC20
.
This is essential for the liquidity migration process defined in the liquidity migration spec.
Functions
createOptimismMintableERC20WithDecimals
Creates an instance of the OptimismMintableERC20
contract with a set of metadata defined by:
_remoteToken
: address of the underlying token in its native chain._name
:OptimismMintableERC20
name_symbol
:OptimismMintableERC20
symbol_decimals
:OptimismMintableERC20
decimals
createOptimismMintableERC20WithDecimals(address _remoteToken, string memory _name, string memory _symbol, uint8 _decimals) returns (address)
Invariants
- The function MUST use
CREATE2
to deploy new contracts. - The salt MUST be computed by applying
keccak256
to theabi.encode
of the four input parameters (_remoteToken
,_name
,_symbol
, and_decimals
). This ensures a uniqueOptimismMintableERC20
for each set of ERC20 metadata. - The function MUST store the
_remoteToken
address for each deployedOptimismMintableERC20
in adeployments
mapping.
createOptimismMintableERC20
Creates an instance of the OptimismMintableERC20
contract with a set of metadata defined
by _remoteToken
, _name
and _symbol
and fixed decimals
to the standard value 18.
createOptimismMintableERC20(address _remoteToken, string memory _name, string memory _symbol) returns (address)
createStandardL2Token
Creates an instance of the OptimismMintableERC20
contract with a set of metadata defined
by _remoteToken
, _name
and _symbol
and fixed decimals
to the standard value 18.
createStandardL2Token(address _remoteToken, string memory _name, string memory _symbol) returns (address)
This function exists for backwards compatibility with the legacy version.
Events
OptimismMintableERC20Created
It MUST trigger when createOptimismMintableERC20WithDecimals
,
createOptimismMintableERC20
or createStandardL2Token
are called.
event OptimismMintableERC20Created(address indexed localToken, address indexed remoteToken, address deployer);
StandardL2TokenCreated
It MUST trigger when createOptimismMintableERC20WithDecimals
,
createOptimismMintableERC20
or createStandardL2Token
are called.
This event exists for backward compatibility with legacy version.
event StandardL2TokenCreated(address indexed remoteToken, address indexed localToken);
L2StandardBridge
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000010 |
Updates
The OptimismMintableERC20
and L2StandardToken
tokens (legacy tokens),
which correspond to locked liquidity in L1, are incompatible with interop.
Legacy token owners must convert into a OptimismSuperchainERC20
representation that implements the standard,
to move across the Superchain.
The conversion method uses the L2StandardBridge
mint/burn rights
over the legacy tokens to allow easy migration to and from the
corresponding OptimismSuperchainERC20
.
convert
The L2StandardBridge
SHOULD add a convert
public function that
converts _amount
of _from
token to _amount
of _to
token,
if and only if the token addresses are valid (as defined below).
function convert(address _from, address _to, uint256 _amount)
The function
- Checks that
_from
and_to
addresses are valid, paired and have the same amount of decimals. - Burns
_amount
of_from
frommsg.sender
. - Mints
_amount
of_to
tomsg.sender
.
Converted
The L2StandardBridge
SHOULD include a Converted
event
that MUST trigger when anyone converts tokens
with convert
.
event Converted(address indexed from, address indexed to, address indexed caller, uint256 amount);
where from
is the address of the input token, to
is the address of the output token,
caller
is the msg.sender
of the function call and amount
is the converted amount.
Invariants
The convert
function conserves the following invariants:
- Conservation of amount: The burnt amount should match the minted amount.
- Revert for non valid or non paired:
convert
SHOULD revert when called with:- Tokens with different decimals.
- Legacy tokens that are not in the
deployments
mapping from theOptimismMintableERC20Factory
. OptimismSuperchainERC20
that are not in thedeployments
mapping from theOptimismSuperchainERC20Factory
.- Legacy tokens and
OptimismSuperchainERC20s
s corresponding to different remote token addresses.
- Freedom of conversion for valid and paired tokens:
anyone can convert between allowed legacy representations and
valid
OptimismSuperchainERC20
corresponding to the same remote token.
Conversion Flow
sequenceDiagram participant Alice participant L2StandardBridge participant factory as OptimismMintableERC20Factory participant superfactory as OptimismSuperchainERC20Factory participant legacy as from Token participant SuperERC20 as to Token Alice->>L2StandardBridge: convert(from, to, amount) L2StandardBridge-->factory: check legacy token is allowed L2StandardBridge-->superfactory: check super token is allowed L2StandardBridge-->L2StandardBridge: checks matching remote and decimals L2StandardBridge->>legacy: IERC20(from).burn(Alice, amount) L2StandardBridge->>SuperERC20: IERC20(to).mint(Alice, amount) L2StandardBridge-->L2StandardBridge: emit Converted(from, to, Alice, amount)
SuperchainERC20Bridge
Constant | Value |
---|---|
Address | 0x4200000000000000000000000000000000000028 |
Overview
The SuperchainERC20Bridge
is an abstraction on top of the L2toL2CrossDomainMessenger
that facilitates token bridging using interop.
It has mint and burn rights over SuperchainERC20
tokens
as described in the token bridging spec.
Functions
sendERC20
Initializes a transfer of _amount
amount of tokens with address _tokenAddress
to target address _to
in chain _chainId
.
It SHOULD burn _amount
tokens with address _tokenAddress
and initialize a message to the
L2ToL2CrossChainMessenger
to mint the _amount
of the same token
in the target address _to
at _chainId
and emit the SentERC20
event including the msg.sender
as parameter.
To burn the token, the sendERC20
function
calls crosschainBurn
in the token contract,
which is included as part of the the
IERC7802
interface
implemented by the SuperchainERC20
standard.
Returns the msgHash_
crafted by the L2ToL2CrossChainMessenger
.
function sendERC20(address _tokenAddress, address _to, uint256 _amount, uint256 _chainId) returns (bytes32 msgHash_)
relayERC20
Process incoming messages IF AND ONLY IF initiated
by the same contract (bridge) address on a different chain
and relayed from the L2ToL2CrossChainMessenger
in the local chain.
It SHOULD mint _amount
of tokens with address _tokenAddress
to address _to
, as defined in sendERC20
and emit an event including the _tokenAddress
, the _from
and chain id from the
source
chain, where _from
is the msg.sender
of sendERC20
.
To mint the token, the relayERC20
function
calls crosschainMint
in the token contract,
which is included as part of the the
IERC7802
interface
implemented by the SuperchainERC20
standard.
function relayERC20(address _tokenAddress, address _from, address _to, uint256 _amount)
Events
SentERC20
MUST trigger when a cross-chain transfer is initiated using sendERC20
.
event SentERC20(address indexed tokenAddress, address indexed from, address indexed to, uint256 amount, uint256 destination)
RelayedERC20
MUST trigger when a cross-chain transfer is finalized using relayERC20
.
event RelayedERC20(address indexed tokenAddress, address indexed from, address indexed to, uint256 amount, uint256 source);
Diagram
The following diagram depicts a cross-chain transfer.
sequenceDiagram participant from participant L2SBA as SuperchainERC20Bridge (Chain A) participant SuperERC20_A as SuperchainERC20 (Chain A) participant Messenger_A as L2ToL2CrossDomainMessenger (Chain A) participant Inbox as CrossL2Inbox participant Messenger_B as L2ToL2CrossDomainMessenger (Chain B) participant L2SBB as SuperchainERC20Bridge (Chain B) participant SuperERC20_B as SuperchainERC20 (Chain B) from->>L2SBA: sendERC20(tokenAddr, to, amount, chainID) L2SBA->>SuperERC20_A: crosschainBurn(from, amount) SuperERC20_A-->SuperERC20_A: emit CrosschainBurn(from, amount) L2SBA->>Messenger_A: sendMessage(chainId, message) Messenger_A->>L2SBA: return msgHash_ L2SBA-->L2SBA: emit SentERC20(tokenAddr, from, to, amount, destination) L2SBA->>from: return msgHash_ Inbox->>Messenger_B: relayMessage() Messenger_B->>L2SBB: relayERC20(tokenAddr, from, to, amount) L2SBB->>SuperERC20_B: crosschainMint(to, amount) SuperERC20_B-->SuperERC20_B: emit CrosschainMint(to, amount) L2SBB-->L2SBB: emit RelayedERC20(tokenAddr, from, to, amount, source)
Invariants
The bridging of SuperchainERC20
using the SuperchainERC20Bridge
will require the following invariants:
- Conservation of bridged
amount
: The mintedamount
inrelayERC20()
should match theamount
that was burnt insendERC20()
, as long as target chain has the initiating chain in the dependency set.- Corollary 1: Finalized cross-chain transactions will conserve the sum of
totalSupply
and each user's balance for each chain in the Superchain. - Corollary 2: Each initiated but not finalized message (included in initiating chain but not yet in target chain)
will decrease the
totalSupply
and the initiating user balance precisely by the burntamount
. - Corollary 3:
SuperchainERC20s
should not charge a token fee or increase the balance when moving cross-chain. - Note: if the target chain is not in the initiating chain dependency set, funds will be locked, similar to sending funds to the wrong address. If the target chain includes it later, these could be unlocked.
- Corollary 1: Finalized cross-chain transactions will conserve the sum of
- Freedom of movement: Users should be able to send and receive tokens in any target
chain with the initiating chain in its dependency set
using
sendERC20()
andrelayERC20()
, respectively. - Unique Messenger: The
sendERC20()
function must exclusively use theL2toL2CrossDomainMessenger
for messaging. Similarly, therelayERC20()
function should only process messages originating from theL2toL2CrossDomainMessenger
. - Unique Address: The
sendERC20()
function must exclusively send a message to the same address on the target chain. Similarly, therelayERC20()
function should only process messages originating from the same address.- Note: The
Create2Deployer
preinstall and the custom Factory will ensure same address deployment.
- Note: The
- Locally initiated: The bridging action should be initialized
from the chain where funds are located only.
- This is because the same address might correspond to different users cross-chain. For example, two SAFEs with the same address in two chains might have different owners. With the prospects of a smart wallet future, it is impossible to assume there will be a way to distinguish EOAs from smart wallets.
- A way to allow for remotely initiated bridging is to include remote approval, i.e. approve a certain address in a certain chainId to spend local funds.
- Bridge Events:
sendERC20()
should emit aSentERC20
event. `relayERC20()
should emit aRelayedERC20
event.
Security Considerations
TODO