Written in collaboration with Ben Jones.
The proliferation of L2s and sidechains means that assets must be represented and moved between different environments. This post outlines how we at Optimism are defining interfaces for
- looking up where to go, and
- interacting with the contracts which will allow assets to be moved between chains.
This post outlines Optimism’s current work, and requests feedback from the community on how to correctly bridge L2 assets to L1.
Terminology note:
- we use the term cross-domain transfer to refer to the action of moving an asset between two asynchronous state spaces, such as L1 and Optimistic Ethereum.
- a gateway contract is one that receives and dispenses a token and facilitates the transfer to the cross-domain.
Motivation
Multiple layer 2 solutions are currently being developed in parallel. There are also bridges to completely distinct layer 1 sidechains. Many projects currently operating on L1 will wish to operate on L2, or at least allow their token to be transacted on L2.
We are seeking to establish two simple standard interfaces for:
- A pairing of gateway contracts (one on each domain) which will accept a “canonical” token address (e.g. the main L1 contract) on one domain, and express the L2 representation of the same token on the other domain.
- A single registry, which maps a token address to the correct gateway contract that should be used for a cross-domain transfer.
The standard we are aiming for does not need to satisfy all use cases, nor include every possible feature. The simplicity and extensibility of EIP-20 itself should be used as a guiding reference.
Important properties we are aiming for include the canonicality of token pairings, and cross-chain composability.
Canonicality of token pairings
Given a token on L1, there should be a clear way to define the source of truth about the address of the token on L2. By extension, the total supply of the token on L2 should be equivalent to the amount of the token deposited to the Gateway contract on L1.
We want to avoid situations where there is confusion about the correct address for the L2 counterpart to an L1 token. However, we also want to avoid situations where a single contract handles deposits for all token types in the same way, as some tokens require custom L2 implementations which cannot be solved with any one bridge model. Thus, we define a standard registry interface as opposed to a monolith contract.
Composability
Projects and users are interested in more interesting actions than simply transferring funds between accounts. Proposals have included using a transferToAndCall
pattern, which would execute a call to the receiver’s address on the cross-domain (ie. receiver.call(data)
) upon finalization of the transfer.
For simplicity and safety, we have elected for a more conservative approach. Instead of having a contract on the receiving domain execute a call, we pass an address from
and bytes data
field through deposits. Subsequently, the data
and from
fields of a deposit may be verified against the state of the recipient domain in a separate transaction. We believe this affords the extensibility of having calldata to pass around, without the security risk of unexpected external calls.
Specification
Registry Interface
To define a standard way to list tokens and their corresponding gateways, without enshrining a single implementation of the bridge itself, we define a getter interface which can retrieve the address of the cross-domain gateway for a given ERC20 asset.
contract Registry {
/// returns the chain ID of the xDomain
function crossDomainID() returns(uint256)
/// returns the address of the token's counterpart on the xDomain
function tokenToGateway(address token) external view returns (address)
/// returns the address on this domain of an xDomain token
function gatewayToToken(address counterpartToken) external view returns (address)
}
Note: this interface has been expressed above in Solidity, but some community members have expressed that this might be better as an off-chain data source, similar to Uniswap’s TokenLists. We would especially love input on this from the community!
Gateway Interface
Gateways live on each domain, and together form a pair which communicate to each other across domains. One gateway (e.g. an L1 deposit contract) will normally interact with the “real” token, locking and unlocking funds as they are transferred away and returned, respectively. The other gateway (e.g. the L2 contract) holds special privilege over an L2 ERC20 address, minting and burning tokens as they are received and transferred.
Note that the terminology here is domain-agnostic, ie. it avoids terms like ‘deposit’ and ‘withdrawal’, so that we can use the same interface on L1 and L2 for transferring back and forth. We hope that this is more future proof, for a world where assets are transferred not only between L1 and L2, but between different L2s.
interface TokenGateway {
/**********
* Events *
**********/
/**
* @dev This emits when a token transfer to the cross-domain is initiated.
* @param _from Address tokens are sent from on this domain.
* @param _to Address that will receive the tokens on the cross-domain.
* @param _amount Amount of the token to transfer.
* @param _data Data provided to the cross-domain.
*/
event OutboundTransferInitiated(
address indexed _from,
address indexed _to,
uint256 _amount,
bytes _data
);
/**
* @dev This emits when a token transfer from the cross-domain is paid out on this domain.
* ie. in finalizeInboundTransfer().
* @param _from Address tokens were sent from on the cross-domain.
* @param _to Address that received the tokens on this domain.
* @param _amount Amount of the token to transfer.
* @param _data Data provided from the cross-domain.
*/
event InboundTransferFinalized(
address indexed _from,
address indexed _to,
uint256 _amount,
bytes _data
);
/********************
* Public Functions *
********************/
/**
* @returns The address the corresponding gateway on the xDomain
*/
function counterpartGateway() returns(address);
/**
* @notice Transfers a token to the same address as msg.sender on the cross-domain.
* emits an OutboundTransferInitiated event.
* @param _amount Amount of the ERC20 to deposit.
* @param _data Data provided to the cross-domain.
*/
function outboundTransfer(
uint _amount,
bytes calldata _data
)
external;
/**
* @notice Transfers a token to another address on the cross-domain
* emits a TransferredOver event
* @param _to Address on cross domain to transfer to.
* @param _amount Amount of the ERC20 to transfer.
* @param _data Arbitrary data with additional information for use on the cross-domain.
*/
function outboundTransferTo(
address _to,
uint _amount,
bytes calldata _data
)
external;
/*****************************
* Cross-chain Functions *
*****************************/
/**
* @notice Finalizes one or more transfers initiated on the cross-domain
* emits a FinalizedReturn event.
* @param _to Address to transfer the token to.
* @param _amount Amount of the ERC20 to transfer.
* @returns _from Address of the sender on the cross-domain.
* @returns _data Data with additional information for use on the cross-domain.
*/
function finalizeInboundTransfer(
address _to,
uint _amount
)
external
returns
(
address _from,
bytes memory _data
)
}
A reference implementation of the current proposal can be found here.
Community Request
We hope that this post can serve as a starting point for community discussion on token bridging interfaces and standards. As more and more L2s appear, it is important that we get this right. So, please tear it apart!