This is a high-level overview of the bridging process.
For a step-by-step tutorial on how to send data between L1 and L2, check out the Solidity tutorial.
Understanding contract calls
It can be easier to understand bridging if you first have a basic understanding of how contracts on EVM-based blockchains like OP Mainnet and Ethereum communicate within the same network. The interface for sending messages between Ethereum and OP Mainnet is designed to mimic the standard contract communication interface as much as possible. Here’s how a contract on Ethereum might trigger a function within another contract on Ethereum:MyContract.doTheThing
triggers a call to MyOtherContract.doSomething
.
Under the hood, Solidity is triggering the code for MyOtherContract
by sending an ABI encoded call for the doSomething
function.
A lot of this complexity is abstracted away to simplify the developer experience.
Solidity also has manual encoding tools that allow us to demonstrate the same process in a more verbose way.
Here’s how you might manually encode the same call:
Basics of communication between layers
At a high level, the process for sending data between L1 and L2 is pretty similar to the process for sending data between two contracts on Ethereum (with a few caveats). Communication between L1 and L2 is made possible by a pair of special smart contracts called the “messenger” contracts. Each layer has its own messenger contract, which serves to abstract away some lower-level communication details, a lot like how HTTP libraries abstract away physical network connections. We won’t get into too much detail about these contracts here. The most important thing that you need to know is that each messenger contract has asendMessage
function that allows you to send a message to a contract on the other layer.
sendMessage
function has three parameters:
- The
address _target
of the contract to call on the other layer. - The
bytes memory _message
calldata to send to the contract on the other layer. - The
uint32 _minGasLimit
minimum gas limit that can be used when executing the message on the other layer.
You can find the addresses of the
L1CrossDomainMessenger
and the L2CrossDomainMessenger
contracts on OP Mainnet and OP Sepolia on the Contract Addresses page.Communication speed
Unlike calls between contracts on the same blockchain, calls between Ethereum and OP Mainnet are not instantaneous. The speed of a cross-chain transaction depends on the direction in which the transaction is sent.For L1 to L2 transactions
Transactions sent from L1 to L2 take approximately 1-3 minutes to get from Ethereum to OP Mainnet, or from Sepolia to OP Sepolia. This is because the Sequencer waits for a certain number of L1 blocks to be created before including L1 to L2 transactions to avoid potentially annoying reorgs.For L2 to L1 transactions
Transactions sent from L2 to L1 take approximately 7 days to get from OP Mainnet to Ethereum, or from OP Sepolia to Sepolia. This is because the bridge contract on L1 must wait for the L2 state to be proven to the L1 chain before it can relay the message. The process of sending transactions from L2 to L1 involves four distinct steps:- The L2 transaction that sends a message to L1 is sent to the Sequencer. This is just like any other L2 transaction and takes just a few seconds to be confirmed by the Sequencer.
- The block containing the L2 transaction is proposed to the L1. This typically takes approximately 20 minutes.
-
A proof of the transaction is submitted to the
OptimismPortal
contract on L1. This can be done any time after step 2 is complete. - The transaction is finalized on L1. This can only be done after the fault challenge period has elapsed. This period is 7 days on Ethereum and a few seconds on Sepolia. This waiting period is a core part of the security model of the OP Stack and cannot be circumvented.
Accessing msg.sender
Contracts frequently make use of msg.sender
to make decisions based on the calling address.
For example, many contracts will use the Ownable pattern to selectively restrict access to certain functions.
Because messages are essentially shuttled between L1 and L2 by the messenger contracts, the msg.sender
you’ll see when receiving one of these messages will be the messenger contract corresponding to the layer you’re on.
In order to get around this, you can find a xDomainMessageSender
function to each messenger:
onlyOwner
modifier on L2:
Fees for sending data between L1 and L2
For L1 to L2 transactions
The majority of the cost of an L1 to L2 transaction comes from the smart contract execution on L1. When sending an L1 to L2 transaction, you send to theL1CrossDomainMessenger
contract, which then sends a call to the OptimismPortal
contract.
This involves some execution on L1, which costs gas.
The total cost is ultimately determined by gas prices on Ethereum when you’re sending the cross-chain transaction.
L1 to L2 execution also triggers contract execution on L2.
The OptimismPortal
contract charges you for this L2 execution by burning a dynamic amount of L1 gas during your L1 to L2 transaction, depending on the gas limit you requested on L2.
The amount of L1 gas charged increases when more people are sending L1 to L2 transactions (and decreases when fewer people are sending L1 to L2 transactions).
Since the gas amount charged is dynamic, the gas burn can change from block to block.
You should always add a buffer of at least 20% to the gas limit for your L1 to L2 transaction to avoid running out of gas.
For L2 to L1 transactions
Each message from L2 to L1 requires three transactions:- An L2 transaction that initiates the transaction, which is priced the same as any other transaction made on OP Mainnet.
- An L1 transaction that proves the transaction. This transaction can only be submitted after L2 block, including your L2 transaction, is proposed on L1. This transaction is expensive because it includes verifying a Merkle trie inclusion proof on L1.
- An L1 transaction that finalizes the transaction. This transaction can only be submitted after the transaction challenge period (7 days on mainnet) has passed.