Optimism standard bridge contract walkthrough
Optimism(opens in a new tab) is an Optimistic Rollup. Optimistic rollups can process transactions for a much lower price than Ethereum Mainnet (also known as layer 1 or L1) because transactions are only processed by a few nodes, instead of every node on the network. At the same time, the data is all written to L1 so everything can be proved and reconstructed with all the integrity and availability guarantees of Mainnet.
To use L1 assets on Optimism (or any other L2), the assets need to be bridged. One way to achieve this is for users to lock assets (ETH and ERC-20 tokens(opens in a new tab) are the most common ones) on L1, and receive equivalent assets to use on L2. Eventually, whoever ends up with them might want to bridge them back to L1. When doing this, the assets are burned on L2 and then released back to the user on L1.
This is the way the Optimism standard bridge(opens in a new tab) works. In this article we go over the source code for that bridge to see how it works and study it as an example of well written Solidity code.
Control flows
The bridge has two main flows:
- Deposit (from L1 to L2)
- Withdrawal (from L2 to L1)
Deposit flow
Layer 1
- If depositing an ERC-20, the depositor gives the bridge an allowance to spend the amount being deposited
- The depositor calls the L1 bridge (
depositERC20
,depositERC20To
,depositETH
, ordepositETHTo
) - The L1 bridge takes possession of the bridged asset
- ETH: The asset is transferred by the depositor as part of the call
- ERC-20: The asset is transferred by the bridge to itself using the allowance provided by the depositor
- The L1 bridge uses the cross-domain message mechanism to call
finalizeDeposit
on the L2 bridge
Layer 2
- The L2 bridge verifies the call to
finalizeDeposit
is legitimate:- Came from the cross domain message contract
- Was originally from the bridge on L1
- The L2 bridge checks if the ERC-20 token contract on L2 is the correct one:
- The L2 contract reports that its L1 counterpart is the same as the one the tokens came from on L1
- The L2 contract reports that it supports the correct interface (using ERC-165(opens in a new tab)).
- If the L2 contract is the correct one, call it to mint the appropriate number of tokens to the appropriate address. If not, start a withdrawal process to allow the user to claim the tokens on L1.
Withdrawal flow
Layer 2
- The withdrawer calls the L2 bridge (
withdraw
orwithdrawTo
) - The L2 bridge burns the appropriate number of tokens belonging to
msg.sender
- The L2 bridge uses the cross-domain message mechanism to call
finalizeETHWithdrawal
orfinalizeERC20Withdrawal
on the L1 bridge
Layer 1
- The L1 bridge verifies the call to
finalizeETHWithdrawal
orfinalizeERC20Withdrawal
is legitimate:- Came from the cross domain message mechanism
- Was originally from the bridge on L2
- The L1 bridge transfers the appropriate asset (ETH or ERC-20) to the appropriate address
Layer 1 code
This is the code that runs on L1, the Ethereum Mainnet.
IL1ERC20Bridge
This interface is defined here(opens in a new tab). It includes functions and definitions required for bridging ERC-20 tokens.
1// SPDX-License-Identifier: MIT2નકલ કરો
Most of Optimism's code is released under the MIT license(opens in a new tab).
1pragma solidity >0.5.0 <0.9.0;2નકલ કરો
At writing the latest version of Solidity is 0.8.12. Until version 0.9.0 is released, we don't know if this code is compatible with it or not.
1/**2 * @title IL1ERC20Bridge3 */4interface IL1ERC20Bridge {5 /**********6 * Events *7 **********/89 event ERC20DepositInitiated(10બધું બતાવોનકલ કરો
In Optimism bridge terminology deposit means transfer from L1 to L2, and withdrawal means a transfer from L2 to L1.
1 address indexed _l1Token,2 address indexed _l2Token,3નકલ કરો
In most cases the address of an ERC-20 on L1 is not the same the address of the equivalent ERC-20 on L2.
You can see the list of token addresses here(opens in a new tab).
The address with chainId
1 is on L1 (Mainnet) and the address with chainId
10 is on L2 (Optimism).
The other two chainId
values are for the Kovan test network (42) and the Optimistic Kovan test network (69).
1 address indexed _from,2 address _to,3 uint256 _amount,4 bytes _data5 );6નકલ કરો
It is possible to add notes to transfers, in which case they are added to the events that report them.
1 event ERC20WithdrawalFinalized(2 address indexed _l1Token,3 address indexed _l2Token,4 address indexed _from,5 address _to,6 uint256 _amount,7 bytes _data8 );9નકલ કરો
The same bridge contract handles transfers in both directions. In the case of the L1 bridge, this means initialization of deposits and finalization of withdrawals.
12 /********************3 * Public Functions *4 ********************/56 /**7 * @dev get the address of the corresponding L2 bridge contract.8 * @return Address of the corresponding L2 bridge contract.9 */10 function l2TokenBridge() external returns (address);11બધું બતાવોનકલ કરો
This function is not really needed, because on L2 it is a predeployed contract, so it is always at address 0x4200000000000000000000000000000000000010
.
It is here for symmetry with the L2 bridge, because the address of the L1 bridge is not trivial to know.
1 /**2 * @dev deposit an amount of the ERC20 to the caller's balance on L2.3 * @param _l1Token Address of the L1 ERC20 we are depositing4 * @param _l2Token Address of the L1 respective L2 ERC205 * @param _amount Amount of the ERC20 to deposit6 * @param _l2Gas Gas limit required to complete the deposit on L2.7 * @param _data Optional data to forward to L2. This data is provided8 * solely as a convenience for external contracts. Aside from enforcing a maximum9 * length, these contracts provide no guarantees about its content.10 */11 function depositERC20(12 address _l1Token,13 address _l2Token,14 uint256 _amount,15 uint32 _l2Gas,16 bytes calldata _data17 ) external;18બધું બતાવોનકલ કરો
The _l2Gas
parameter is the amount of L2 gas the transaction is allowed to spend.
Up to a certain (high) limit, this is free(opens in a new tab), so unless the ERC-20 contract does something really strange when minting, it should not be an issue.
This function takes care of the common scenario, where a user bridges assets to the same address on a different blockchain.
1 /**2 * @dev deposit an amount of ERC20 to a recipient's balance on L2.3 * @param _l1Token Address of the L1 ERC20 we are depositing4 * @param _l2Token Address of the L1 respective L2 ERC205 * @param _to L2 address to credit the withdrawal to.6 * @param _amount Amount of the ERC20 to deposit.7 * @param _l2Gas Gas limit required to complete the deposit on L2.8 * @param _data Optional data to forward to L2. This data is provided9 * solely as a convenience for external contracts. Aside from enforcing a maximum10 * length, these contracts provide no guarantees about its content.11 */12 function depositERC20To(13 address _l1Token,14 address _l2Token,15 address _to,16 uint256 _amount,17 uint32 _l2Gas,18 bytes calldata _data19 ) external;20બધું બતાવોનકલ કરો
This function is almost identical to depositERC20
, but it lets you send the ERC-20 to a different address.
1 /*************************2 * Cross-chain Functions *3 *************************/45 /**6 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the7 * L1 ERC20 token.8 * This call will fail if the initialized withdrawal from L2 has not been finalized.9 *10 * @param _l1Token Address of L1 token to finalizeWithdrawal for.11 * @param _l2Token Address of L2 token where withdrawal was initiated.12 * @param _from L2 address initiating the transfer.13 * @param _to L1 address to credit the withdrawal to.14 * @param _amount Amount of the ERC20 to deposit.15 * @param _data Data provided by the sender on L2. This data is provided16 * solely as a convenience for external contracts. Aside from enforcing a maximum17 * length, these contracts provide no guarantees about its content.18 */19 function finalizeERC20Withdrawal(20 address _l1Token,21 address _l2Token,22 address _from,23 address _to,24 uint256 _amount,25 bytes calldata _data26 ) external;27}28બધું બતાવોનકલ કરો
Withdrawals (and other messages from L2 to L1) in Optimism are a two step process:
- An initiating transaction on L2.
- A finalizing or claiming transaction on L1. This transaction needs to happen after the fault challenge period(opens in a new tab) for the L2 transaction ends.
IL1StandardBridge
This interface is defined here(opens in a new tab).
This file contains event and function definitions for ETH.
These definitions are very similar to those defined in IL1ERC20Bridge
above for ERC-20.
The bridge interface is divided between two files because some ERC-20 tokens require custom processing and cannot be handled by the standard bridge.
This way the custom bridge that handles such a token can implement IL1ERC20Bridge
and not have to also bridge ETH.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34import "./IL1ERC20Bridge.sol";56/**7 * @title IL1StandardBridge8 */9interface IL1StandardBridge is IL1ERC20Bridge {10 /**********11 * Events *12 **********/13 event ETHDepositInitiated(14 address indexed _from,15 address indexed _to,16 uint256 _amount,17 bytes _data18 );19બધું બતાવોનકલ કરો
This event is nearly identical to the ERC-20 version (ERC20DepositInitiated
), except without the L1 and L2 token addresses.
The same is true for the other events and the functions.
1 event ETHWithdrawalFinalized(2 .3 .4 .5 );67 /********************8 * Public Functions *9 ********************/1011 /**12 * @dev Deposit an amount of the ETH to the caller's balance on L2.13 .14 .15 .16 */17 function depositETH(uint32 _l2Gas, bytes calldata _data) external payable;1819 /**20 * @dev Deposit an amount of ETH to a recipient's balance on L2.21 .22 .23 .24 */25 function depositETHTo(26 address _to,27 uint32 _l2Gas,28 bytes calldata _data29 ) external payable;3031 /*************************32 * Cross-chain Functions *33 *************************/3435 /**36 * @dev Complete a withdrawal from L2 to L1, and credit funds to the recipient's balance of the37 * L1 ETH token. Since only the xDomainMessenger can call this function, it will never be called38 * before the withdrawal is finalized.39 .40 .41 .42 */43 function finalizeETHWithdrawal(44 address _from,45 address _to,46 uint256 _amount,47 bytes calldata _data48 ) external;49}50બધું બતાવોનકલ કરો
CrossDomainEnabled
This contract(opens in a new tab) is inherited by both bridges (L1 and L2) to send messages to the other layer.
1// SPDX-License-Identifier: MIT2pragma solidity >0.5.0 <0.9.0;34/* Interface Imports */5import { ICrossDomainMessenger } from "./ICrossDomainMessenger.sol";6નકલ કરો
This interface(opens in a new tab) tells the contract how to send messages to the other layer, using the cross domain messenger. This cross domain messenger is a whole other system, and deserves its own article, which I hope to write in the future.
1/**2 * @title CrossDomainEnabled3 * @dev Helper contract for contracts performing cross-domain communications4 *5 * Compiler used: defined by inheriting contract6 */7contract CrossDomainEnabled {8 /*************9 * Variables *10 *************/1112 // Messenger contract used to send and receive messages from the other domain.13 address public messenger;1415 /***************16 * Constructor *17 ***************/1819 /**20 * @param _messenger Address of the CrossDomainMessenger on the current layer.21 */22 constructor(address _messenger) {23 messenger = _messenger;24 }25બધું બતાવોનકલ કરો
The one parameter that the contract needs to know, the address of the cross domain messenger on this layer. This parameter is set once, in the constructor, and never changes.
12 /**********************3 * Function Modifiers *4 **********************/56 /**7 * Enforces that the modified function is only callable by a specific cross-domain account.8 * @param _sourceDomainAccount The only account on the originating domain which is9 * authenticated to call this function.10 */11 modifier onlyFromCrossDomainAccount(address _sourceDomainAccount) {12બધું બતાવોનકલ કરો
The cross domain messaging is accessible by any contract on the blockchain where it is running (either Ethereum mainnet or Optimism). But we need the bridge on each side to only trust certain messages if they come from the bridge on the other side.
1 require(2 msg.sender == address(getCrossDomainMessenger()),3 "OVM_XCHAIN: messenger contract unauthenticated"4 );5નકલ કરો
Only messages from the appropriate cross domain messenger (messenger
, as you see below) can be trusted.
12 require(3 getCrossDomainMessenger().xDomainMessageSender() == _sourceDomainAccount,4 "OVM_XCHAIN: wrong sender of cross-domain message"5 );6નકલ કરો
The way the cross domain messenger provides the address that sent a message with the other layer is the .xDomainMessageSender()
function(opens in a new tab).
As long as it is called in the transaction that was initiated by the message it can provide this information.
We need to make sure that the message we received came from the other bridge.
12 _;3 }45 /**********************6 * Internal Functions *7 **********************/89 /**10 * Gets the messenger, usually from storage. This function is exposed in case a child contract11 * needs to override.12 * @return The address of the cross-domain messenger contract which should be used.13 */14 function getCrossDomainMessenger() internal virtual returns (ICrossDomainMessenger) {15 return ICrossDomainMessenger(messenger);16 }17બધું બતાવોનકલ કરો
This function returns the cross domain messenger.
We use a function rather than the variable messenger
to allow contracts that inherit from this one to use an algorithm to specify which cross domain messenger to use.
12 /**3 * Sends a message to an account on another domain4 * @param _crossDomainTarget The intended recipient on the destination domain5 * @param _message The data to send to the target (usually calldata to a function with6 * `onlyFromCrossDomainAccount()`)7 * @param _gasLimit The gasLimit for the receipt of the message on the target domain.8 */9 function sendCrossDomainMessage(10 address _crossDomainTarget,11 uint32 _gasLimit,12 bytes memory _message13બધું બતાવોનકલ કરો
Finally, the function that sends a message to the other layer.
1 ) internal {2 // slither-disable-next-line reentrancy-events, reentrancy-benign3નકલ કરો
Slither(opens in a new tab) is a static analyzer Optimism runs on every contract to look for vulnerabilities and other potential problems. In this case, the following line triggers two vulnerabilities:
1 getCrossDomainMessenger().sendMessage(_crossDomainTarget, _message, _gasLimit);2 }3}4નકલ કર ો
In this case we are not worried about reentrancy we know getCrossDomainMessenger()
returns a trustworthy address, even if Slither has no way to know that.
The L1 bridge contract
The source code for this contract is here(opens in a new tab).
1// SPDX-License-Identifier: MIT2pragma solidity ^0.8.9;3નકલ કરો
The interfaces can be part of other contracts, so they have to support a wide range of Solidity versions. But the bridge itself is our contract, and we can be strict about what Solidity version it uses.
1/* Interface Imports */2import { IL1StandardBridge } from "./IL1StandardBridge.sol";3import { IL1ERC20Bridge } from "./IL1ERC20Bridge.sol";4નકલ કરો
IL1ERC20Bridge and IL1StandardBridge are explained above.
1import { IL2ERC20Bridge } from "../../L2/messaging/IL2ERC20Bridge.sol";2નકલ કરો
This interface(opens in a new tab) lets us create messages to control the standard bridge on L2.
1import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol";2નકલ કરો
This interface(opens in a new tab) lets us control ERC-20 contracts. You can read more about it here.
1/* Library Imports */2import { CrossDomainEnabled } from "../../libraries/bridge/CrossDomainEnabled.sol";3નકલ કરો
As explained above, this contract is used for interlayer messaging.
1import { Lib_PredeployAddresses } from "../../libraries/constants/Lib_PredeployAddresses.sol";2નકલ કરો
Lib_PredeployAddresses
(opens in a new tab) has the addresses for the L2 contracts that always have the same address. This includes the standard bridge on L2.
1import { Address } from "@openzeppelin/contracts/utils/Address.sol";2નકલ કરો
OpenZeppelin's Address utilities(opens in a new tab). It is used to distinguish between contract addresses and those belonging to externally owned accounts (EOA).
Note that this isn't a perfect solution, because there is no way to distinguish between direct calls and calls made from a contract's constructor, but at least this lets us identify and prevent some common user errors.
1import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";2નકલ કરો
The ERC-20 standard(opens in a new tab) supports two ways for a contract to report failure:
- Revert
- Return
false
Handling both cases would make our code more complicated, so instead we use OpenZeppelin's SafeERC20
(opens in a new tab), which makes sure all failures result in a revert(opens in a new tab).
1/**2 * @title L1StandardBridge3 * @dev The L1 ETH and ERC20 Bridge is a contract which stores deposited L1 funds and standard4 * tokens that are in use on L2. It synchronizes a corresponding L2 Bridge, informing it of deposits5 * and listening to it for newly finalized withdrawals.6 *7 */8contract L1StandardBridge is IL1StandardBridge, CrossDomainEnabled {9 using SafeERC20 for IERC20;10બધું બતાવોનકલ કરો
This line is how we specify to use the SafeERC20
wrapper every time we use the IERC20
interface.
12 /********************************3 * External Contract References *4 ********************************/56 address public l2TokenBridge;7![]()