While Optimism has been operating since Nov of 2021 (see "An Introduction to Optimism’s Optimistic Rollup" by Kyle Charbonnet), the Optimism team is hard at work for next major release of the Optimism network called "Bedrock". As the release is planned for the last quarter of 2022, it is time to take a look at its solidity contracts and understand transaction trails as well as security measures around them.
From various articles describing how current Optimism works and past audits, it seems the current version of Optimism had gone through a few major iterations, and the final deployed version has a much smaller scope than Optimism set out to do. Most notably, order of transactions in CanonicalTransactionChain
is not guaranteed and fraud proof procedure is not currently in operation, despite its on-chain vestige. This is quite understandable, given the current highly centralized stage of the project.
A user of Optimism interacts with the system through three different types of transactions: L1 to L2, L2 to L1 and L2 transactions.
Based on current contracts, the following diagram depicts how messages are passed through different components.
The most common L1 to L2 transactions are user deposits.
Note although it is claimed "As of the OVM 2.0 update (Nov. 2021), the process of using ETH on L2 is identical to the process of using ETH in Ethereum.", the current version actually still represents ETH as an ERC20 token at this L2 address 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000
.
A user can only deposit ETH or ERC20 tokens through the canonical bridge route, starting with a call to the L1StandardBridge
proxy contract. The L1StandardBridge
proxy contract acts as an escrow account for both ETH and ERC20 tokens and sends a finalizeDeposit
message to the L1CrossDomainMessenger
contract. The L1CrossDomainMessenger
contract wraps the message inside relayMessage
, then enqueues the data to CanonicalTransactionChain
. The Optimism sequencer then picks up the data and sends it as calldata to appendSequencerBatch(...)
. The rollup node sees the emitted SequencerBatchAppended
event, finds the transaction in appendSequencerBatch
's calldata, unwraps the message and relays it to L2CrossDomainMessenger
as if from aliased L1CrossDomainMessenger
address. The L2CrossDomainMessenger
forwards it to L2StandardBridge
, which then mints new ERC20 tokens and finalizes the transfer to specified L2 user account.
To make things clear, let's follow one ETH deposit transaction with the corresponding L2 transaction and see how the L1StandardBridge
depositETH's calldata
0xb1a1a882 // L1StandardBridge: depositETH{value: 800000000000000000}(uint32,bytes)
0000000000000000000000000000000000000000000000000000000000030d40 // _l2Gas:
0000000000000000000000000000000000000000000000000000000000000040 // _data: start location, used as a tag for easy identification
0000000000000000000000000000000000000000000000000000000000000000 // _data: length
gets transformed into enqueued message through L1CrossDomainMessanger
and received by CanonicalTransactionChain
:
0x6fee07e0 // CanonicalTransactionChain: enqueue(address, uint256, bytes)
0000000000000000000000004200000000000000000000000000000000000007 // _target: L2_CROSS_DOMAIN_MESSENGER
0000000000000000000000000000000000000000000000000000000000030d40 // _gasLimit
0000000000000000000000000000000000000000000000000000000000000060 // _data: start location
00000000000000000000000000000000000000000000000000000000000001a4 // _data: length
cbd4ece9 // L2CrossDomainTransaction: relayMessage(address, address, bytes memory _message, uint256 _messageNonce)
0000000000000000000000004200000000000000000000000000000000000010 // _target: L2_STANDARD_BRIDGE
00000000000000000000000099c9fc46f92e8a1c0dec1b1747d010903e884be1 // _sender: Proxy__OVM_L1StandardBridge
0000000000000000000000000000000000000000000000000000000000000080 // _message: start position
0000000000000000000000000000000000000000000000000000000000024f73 // _messageNonce: 151411
00000000000000000000000000000000000000000000000000000000000000e4 _message: length
662a633a // L2StandardBridge: finalizeDeposit(address,address,address,address,uint256,bytes)
0000000000000000000000000000000000000000000000000000000000000000 // _l1Token
000000000000000000000000deaddeaddeaddeaddeaddeaddeaddeaddead0000 // _l2Token: OVM_ETH
0000000000000000000000003fea91ee988f1ca7a627ba758af6fa7b78c259e3 // _from
0000000000000000000000003fea91ee988f1ca7a627ba758af6fa7b78c259e3 // _to
0000000000000000000000000000000000000000000000000b1a2bc2ec500000 // _amount: 800000000000000000
00000000000000000000000000000000000000000000000000000000000000c0 // _extraData: start location
0000000000000000000000000000000000000000000000000000000000000000 // _extraData: length
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000
There are many checks along this route to ensure integrity of deposit transactions and prevent fake ones being inserted somewhere. Following money backwards from exit to initiation is a better way to discover any misses, so let's do just that (CD here stands for "Current version Deposit").
CD.1 L2StandardERC20
tokens can only be minted by L2StandardBridge
.
CD.2 Minting happens in finalizeDeposit( address _l1Token, address _l2Token, address _from, address _to, uint256 _amount, bytes calldata _data)
, which can only be called by L2CrossDomainMessanger
and requires xDomainMessageSender
to be L1StandardBridge
.
CD.3 In case there is a mismatch between L1 and L2 tokens, finalizeDeposit(...)
issues a withdraw transaction to refund users on L1.
Before passing the finalizeDeposit
along, relayMessage( address _target, address _sender, bytes memory _message, uint256 _messageNonce )
in L2CrossDomainMessanger
conducts a series of verifications:
CD.4.1 The msg.sender
needs to be aliasedL1CrossDomainMessenger
. This avoids contract address collision between L1 and L2. As an added benefit, it also prevents reentrancy.
CD.4.2 Ensure the same deposit has not been relayed before.
CD.4.3 _target
is not L2_TO_L1_MESSAGE_PASSER
, so the call is not a circular call back to L1.
CD.5 The relayMessage
is called as if from aliasedL1CrossDomainMessenger
by rollup node, controlled by Optimism until decentralization, which monitors SequencerBatchAppended(...)
emitted by appendSequencerBatch()
in CanonicalTransactionChain
for new deposits, then relays the message as if from the from
in the calldata.
CD.6 Enqueued messages are appended into sequencerBatch, by sequencer, an centralized entity controlled by Optimism, which monitors TransactionEnqueued(...)
emitted by enqueue(...)
.
CD.7 enqueue(...)
can only be called by sendMessage(...)
from L1CrossDomainMessenger
to fulfill CD.4.1.
CD.8 L1CrossDomainMessanger
has a replayMessage(...)
callable by anyone, in case not enough gas is supplied to the initial deposit. However, since it is required to reuse the old nonce, no double deposits will happen as result.
CD.9 sendMessage(...)
needs to be called by various deposit
functions from L1StandardBridge(...)
, as L1CrossDomainMessenger
fills in the msg.sender
, which is then checked by CD.2.
CD.10 The various deposit
functions fill in msg.sender
as _from
when crafting finalizeDeposit
message.
Users can also send arbitrary L1 to L2 transactions by enqueueing them into CanonicalTransactionChain
. However, enqueue(...)
is not payable and CD4.1 cannot be fulfilled on L2, so ETH deposits are not feasible through this route. As a matter of fact, this route is seldomly traversed, among the 18500+ enqueue calls since Optimism's first operation, there are only 9 direct user calls. Although these calls are experimental in nature, it is still interesting to see how an user can send arbitrary transactions to L2. As an example, this L1 transaction triggered a set(bool) on L2.
L2 to L1 transactions are the reverse actions of the above, where a user issues a withdrawal through L2StandardBridge
, which goes through L1CrossDomainMessenger
, and is passed to OVM_L2ToL1MessagePasser
. A sequencer then picks it up and adds it to a CanonicalTransactionChain
. After a pre-determined fraud-proof period and verifying with the StateCommitmentChain
, anyone will be able to relay the message and finalize the withdrawal on L1.
Similarly, let's see how a withdraw transaction started from L2StandardBridge
0x32b7006d // L2StandardBridge: withdraw(address _l2Token, uint256 _amount, uint32 _l1Gas, bytes _data)
0000000000000000000000009bcef72be871e61ed4fbbc7630889bee758eb81d // _l2Token
0000000000000000000000000000000000000000000000125f5ca99045274425 // _amount: 338912946895333049381
0000000000000000000000000000000000000000000000000000000000000000 // _l1Gas
0000000000000000000000000000000000000000000000000000000000000080 // _data: star location
0000000000000000000000000000000000000000000000000000000000000000 // _data: length
is transformed, wrapped and added to OVM_L2ToL1MessagePasser
through passMessageToL1
.
0xcbd4ece9 // L1CrossDomainMessager: relayMessage(address,address,bytes,uint256)
00000000000000000000000099c9fc46f92e8a1c0dec1b1747d010903e884be1 // _target: Proxy__OVM_L1StandardBridge
0000000000000000000000004200000000000000000000000000000000000010 // _sender: L2_STANDARD_BRIDGE
0000000000000000000000000000000000000000000000000000000000000080 // _message: start location
000000000000000000000000000000000000000000000000000000000001b2dc // _messageNonce: 111324
00000000000000000000000000000000000000000000000000000000000000e4
a9f9e675 // L1StandardBridge: finalizeERC20Withdrawal(address,address,address,address,uint256,bytes)
000000000000000000000000ae78736cd615f374d3085123a210448e74fc6393 // _l1Token
0000000000000000000000009bcef72be871e61ed4fbbc7630889bee758eb81d // _l2Token
0000000000000000000000002c362fd5bd900b73c4bf140b7cd6875a56b0e7b6 // _from
0000000000000000000000002c362fd5bd900b73c4bf140b7cd6875a56b0e7b6 // _to
0000000000000000000000000000000000000000000000125f5ca99045274425 // _amount: 338912946895333049381
00000000000000000000000000000000000000000000000000000000000000c0 // _data: star location
0000000000000000000000000000000000000000000000000000000000000000 // _data: length
00000000000000000000000000000000000000000000000000000000
Something interesting actually happens when a relayer wants to initiate the withdrawal finalization on L1, the relayer has to first transform the above message, since Proxy__OVM_L1CrossDomainMessenger
is expect a different relay signature:
relayMessage(
address _target,
address _sender,
bytes memory _message,
uint256 _messageNonce,
L2MessageInclusionProof memory _proof
)
This sounds hacky, but kind of makes sense since in crafting the message on L2 side, the proof at withdrawal time is not known yet.
Now, let's examine the various checks in place in the withdrawal process that mitigate security concerns (CW means Current version Withdrawal).
CW.1 Both ETH and ERC20 tokens are escrowed in L1StandardBridge.
CW.2 To withdraw, finalizeETHWithdrawal(...)
and finalizeERC20Withdrawal(...)
need to be triggered by L1CrossDomainMessager
and xDomainMessageSender
needs to be L2StandardBridge
.
In L1CrossDomainMessager
, relayMessage( address _target, address _sender, bytes memory _message, uint256 _messageNonce, L2MessageInclusionProof memory _proof )
conducts a series of checks before passing the withdrawal to L1StandardBridge
:
CW.3.1 relayMessage(...)
is callable by anyone, thus retriable.
CW.3.2 relayMessage(...)
has nonReentrant
guard, this prevents recursive withdrawals embedded in one withdrawal
CW.3.3 relayMessage(...)
is pausable (https://github.com/ethereum-optimism/optimism/blob/cdc1fa9846228121982605132b37f99df035e863/packages/contracts/contracts/L1/messaging/L1CrossDomainMessenger.sol#L171), this add extra security measure. However, an unpause()
function is missing in the contract.
CW.3.4 _proof
is outside of FRAUD_PROOF_WINDOW
CW.3.5 _proof.stateRootBatchHeader
exists in StateCommitmentChain
storage, thus is valid
CW.3.6 verifyStateCommitment()
verifies the consistency between _proof.stateRoot
, _proof.stateRootBatchHeader
, _proof.stateRootProof
CW.3.7 Verify the withdrawal message is sent by L2CrossDomainMessenger
, through L2_TO_L1_MESSAGE_PASSER
and is consistent with _proof
.
CW.3.8 Ensure the same withdrawal has not been relayed before.
CW.3.9 Ensure this withdrawal has not been blocked by Optimism.
CW.3.10 _target is not CanonicalTransactionChain
, so that the L2->L1 message cannot be sent back to L2
CW.4 Although currently no fraud-proof procedure is in operation, Optimism can be upgraded within the 7 day challenge window ("fast upgrade keys").
CW.5 The relayMessage(...)
is appended to SequencerBatch
by Optimism's sequencer by retrieving calldata from passMessageToL1()
in OVM_L2ToL1MessagePasser
CW.6 Withdrawal messages cannot be created through toOVM_L2ToL1MessagePasser
from L2CrossDomainMessenger
, since passMessageToL1
fills in msg.sender
, which is checked in CW.3.7.
CW.7 Withdrawal messages cannot be created through L2CrossDomainMessenger
, since sendMessage
fills in msg.sender
, which is checked in CW.2.
CW.8 The various withdraw
functions fill in msg.sender
as _from
when crafting finalizeETHWithdrawal
message and emit WithdrawalInitiated
.
CW.9 Burning token is a privilege belonging only to L2StandardBridge
.
With funds deposited, users can start transacting on L2. The sequencer accepts these transactions and builds blocks out of them. For each such block, the sequencer creates a corresponding sequencer batch, submits each batch to CanonicalTransactionChain
as calldata, which are then executed and recorded in StateCommitmentChain
.
Since only transactions sent to passMessageToL1
in OVM_L2ToL1MessagePasser
are passed to L1, there is no concern of mistreated regular L2 transactions.
As Bedrock is expected to hit mainnet toward the end of 2022, its contracts are under intense updates. While the diagram here is based on recent commit 1471911
, it is subject to further changes.
One immediately notices the similarity of transaction trails between Bedrock and the current version. As a matter of fact, the deposits and withdrawals input data go through pretty much the same transformation. One big difference is now Bedrock natively supports ETH. The same ETH deposit function is transformed to:
e9e05c42 // L1 OptimismPortal: depositTransaction(address,uint256,uint64,bool,bytes)
0000000000000000000000004200000000000000000000000000000000000007 // _target: Proxy_L2_CROSS_DOMAIN_MESSENGER
0000000000000000000000000000000000000000000000000b1a2bc2ec500000 // _value: 800000000000000000
0000000000000000000000000000000000000000000000000000000000030d40 // _gasLimit
0000000000000000000000000000000000000000000000000000000000000000 // _isCreation
00000000000000000000000000000000000000000000000000000000000000a0 // _data: start location
00000000000000000000000000000000000000000000000000000000000001a4 // _data: length
d764ad0b // L2 CrossDomainMessanger: relayMessage(uint256,address,address,uint256,uint256,bytes)
0000000000000000000000000000000000000000000000000000000000024f73 // _nonce: 151411
00000000000000000000000099c9fc46f92e8a1c0dec1b1747d010903e884be1 // _sender: Proxy__L1StandardBridge
0000000000000000000000004200000000000000000000000000000000000010 // _target: Proxy_L2_STANDARD_BRIDGE
0000000000000000000000000000000000000000000000000b1a2bc2ec500000 // _value: 800000000000000000
0000000000000000000000000000000000000000000000000000000000030d40 // _minGasLimit
00000000000000000000000000000000000000000000000000000000000000c0 // _message: start position
00000000000000000000000000000000000000000000000000000000000000e4 _message: length
1635f5fd // L2 StandardBridge: finalizeBridgeETH(address,address,uint256,bytes)
0000000000000000000000003fea91ee988f1ca7a627ba758af6fa7b78c259e3 // _from
0000000000000000000000003fea91ee988f1ca7a627ba758af6fa7b78c259e3 // _to
0000000000000000000000000000000000000000000000000b1a2bc2ec500000 // _amount: 800000000000000000
0000000000000000000000000000000000000000000000000000000000000080 // _extraData: start location
0000000000000000000000000000000000000000000000000000000000000000 // _extraData: length
0000000000000000000000000000000000000000000000000000000000000000
000000000000000000000000000000000000000000000000
The native support of ETH allows more symmetry between L1 and L2, from ETH transfer to gas consumption, thus having a huge impact on how the contracts can be structured and modularized:
A. In current version, L1 and L2 have clearly distinct business logic for their own StandardBridge
and CrossDomainMessenger
contracts respectively. Bedrock, on the other hand, unifies the logic under common abstract StandardBridge
and CrossDomainMessenger
contracts. The specific L1 and L2 StandardBridge
simply add legacy function interfaces (not shown in the diagram) to remain backward-compatible, while the differences between L1 and L2 CrossDomainMessenger
are kept minimal.
B. The most notable changes are the removals of CanonicalTransactionChain
, StateCommitmentChain
and their respective storage contracts. With CanonicalTransactionChain
replaced by an ordinary L1 account at 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0001
, and StateCommitmentChain
replaced by a much simpler L2OutputOracle
contract, Bedrock does very little bookkeeping, thus minimizing L1 gas expense.
C. Currently, predeployed accounts on L2 have fixed contract code. Under Bedrock, they are all proxies to upgradable implementations, thus making the upgrade process much more transparent.
D. To relay withdrawals on L1 side, Bedrock relayer needs to call finalizeWithdrawalTransaction
in OptimismPortal
, which checks inputs against L2 proposal retrieved through L2_ORACLE.getL2Output(_l2BlockNumber)
for correctness and finalization. This eliminates the need for the hacky, non-transparent transformation a relayer has to apply to the message as mentioned previously.
E. Since current version represents ETH as an ERC20 token internally, ETH is escrowed in L1StandardBridge
, while the token is minted from OVM_ETH 0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000
on L2 side. Bedrock handles ETH natively. Any ETH passed to Bedrock StandardBridge
is forwarded to and escrowed in OptimismPortal
on L1. On L2 side, withdrawn ETH is eventually burned by L2ToL1MessagePasser
.
F. Because ETH is native to Bedrock, user can now deposit or withdraw through either the canonical bridge route or direct calling to OptimismPortal
's depositTransaction(...)
, and L2ToL1MessagePasser
's initiateWithdrawal(...)
. These lower-level calls will not take advantage of CrossDomainMessenger
's retry mechanism, but allow for customized app-specific retry schemes.
G. In current version, when wrong l2token is passed in for a l1token deposits, an ERC20 withdrawal call is kicked off to refund the deposit to L1. Bedrock removed this functionality due to its rare usage and added complexity.
Now, let's examine the various security measures Bedrock implements for deposit transactions (BD here stands for "Bedrock Deposit").
BD.1 L2 tokens are only mintable by L2's StandardBridge
. Users receive ETH or ERC20 deposits with finalizeBridgeETH(...)
and finalizeBridgeERC20(...)
calls in L2's StandardBridge
. These functions need to be triggered by CrossDomainMessenger
on the same side, while the xDomainMessageSender
needs to be the other bridge.
BD.2 When l1 token and l2 token mismatch, these calls simply revert, without refunding l1 token, recognized by G. This means users need to make sure L1/L2 tokens match, otherwise L1 tokens will be lost.
relayMessage( uint256 _nonce, address _sender, address _target, uint256 _value, uint256 _minGasLimit, bytes calldata _message )
in CrossDomainMessenger
does checks below before making an external call, strictly following CEI pattern.
BD.3.1 Reentrancy not allowed.
BD.3.2 It is pausable.
BD.3.3 It is sent by aliased L1's CrossDomainMessenger
, in which case msg.value
needs to be the same as _value, or if it is a replay, msg.value needs to be 0 and it has been recorded by receivedMessages[versionedHash]
.
BD.3.4 _target
cannot be address(this)
nor address(L2_TO_L1_MESSAGE_PASSER)
, to prevent circular calls.
BD.3.5 ensure the same transfer has not succeeded before.
BD.3.6 enough gas provided.
BD.4 The relayMessage
is sent by a rollup node, which monitors TransactionDeposited(address indexed from, address indexed to, uint256 indexed version, bytes opaqueData)
events emitted in OptimismPortal
and sends the transaction as if from the from
in the log. Rollup nodes are controlled by Optimism until decentralization, where fraud-proof enforces the correctness.
BD.5 If sent by L1's CrossDomainMessenger
, depositTransaction
in OptimismPortal
aliases msg.sender
to satisfy BD.3.3.
BD.6 The deposit message needs to be passed into L1's CrossDomainMessenger
from L1's StandardBridge
, since sendMessage
fills in msg.sender
, which is checked in BD.1.
BD.7 The various deposit
functions fill in msg.sender
as _from
when crafting finalizeBridgeERC20
message.
depositTransaction
in OptimismPortal
does not require the msg.sender
to be L1's CrossDomainMessenger
. It can be initiated by sending ETH to OptimismPortal
or called directly. Per BD.4, this deposit will be picked up by rollup, since depositTransaction
emits TransactionDeposited
. Note the user needs to take care of retries on L2 in this case.
Withdrawals have similar checks in place (BW is for "Bedrock Withdraw").:
BW.1 On L1, ETH is escrowed in OptimismPortal
, while ERC20 is escrowed in L1's StandardBridge
.
Both ETH and ERC20 tokens can be withdrawn through the canonical bridge similar to current version:
BW.2 On L1, finalizeBridgeXXX(...)
in StandardBridge
needs to be triggered by CrossDomainMessenger
on the same side, while the xDomainMessageSender
needs to be the other bridge.
relayMessage( uint256 _nonce, address _sender, address _target, uint256 _value, uint256 _minGasLimit, bytes calldata _message )
in CrossDomainMessenger
checks that:
BW.3.1 Reentrancy not allowed.
BW.3.2 It is pausable.
BW.3.3 It is sent by OptimismPortal
and the l2Sender is L2's CrossDomainMessenger
, in which case msg.value
needs to be the same as _value
, or if it is a replay, msg.value needs to be 0 and it has been recorded by receivedMessages[versionedHash]
.
BW.3.4 _target
cannot be address(this)
nor address(portal)
, to prevent circular calls.
BW.3.5 ensure the same transfer has not succeeded before.
BW.3.6 enough gas provided.
Per BW.3.3 the first relayMessage
needs to be triggered by finalizeWithdrawalTransaction
callable by anyone in OptimismPortal
, which checks:
BW.4.1 No reentrancy, as require(l2Sender==DEFAULT_L2_SENDER)
ensures only one withdrawal per transaction.*
BW.4.2 No embedded withdrawal in current withdrawal with require(_tx.target!=address(this))
.
BW.4.3 Can retrieve proposal
from L2OutputOracle
, which can only be updated by the sequencer.
BW.4.4 Proposal has passed FINALIZATION_PERIOD_SECONDS
.
BW.4.5 Establish the validity of _outputRootProof
by checking against proposal
.
BW.4.6 Verify withdrawal is included in _outputRootProof
. Together with BW.4.5, this shows the authenticity of the current withdrawal.
BW.4.7 Check current withdrawal has not finalized.
BW.4.8 Enough gas provided.
BW.5 Sequencer, a centralized entity by Optimism, monitors MessagePassed
events emitted in L2ToL1MessagePasser
, creates and inserts L2 Blocks in L2OutputOracle
for BW.4.3.
BW.6 Withdrawn L2 ETH are accumulated in L2ToL1MessagePasser
and can be burned periodically.
BW.7 The withdrawal message needs to be passed into L2ToL1MessagePasser
from L2's CrossDomainMessenger
, since initiateWithdrawal
fills in msg.sender
, which is checked in BW.3.3.
BW.8 The withdrawal message needs to be passed into L2's CrossDomainMessenger
from L2's StandardBridge
, since sendMessage
fills in msg.sender
, which is checked in BW.2.
BW.9 The various withdraw
functions fill in msg.sender
as _from
when crafting finalizeBridgeERC20
message.
BW.10 Burning L2 ERC20 tokens is a privilege belonging to L2's StandardBridge
.
finalizeWithdrawalTransaction
in OptimismPortal
does not require the _tx.target
to be L1's CrossDomainMessenger
. Such calls can be initiated by sending ETH to L2_TO_L1_MESSAGE_PASSER
or calling initiateWithdrawal
directly in L2ToL1MessagePasser
. Per BW.5, this withdrawal will be picked up by Sequencer, since L2ToL1MessagePasser
emits MessagePassed
. Note the user needs to take care of retries in this case.
``
In their talk, Quantstamp classified common bridge attack surfaces into five common categories (see below for breakdown). Let's take a look at how these potential issues are addressed in Optimism's contracts through the use of different Solidity language features, such as access control modifier, function visibility/accessibility as well as require statements.
a. Incorrect asset amount released with respect to the burnt tokens
Current: Assets can only be released from L1StandardBridge
, through the canonical bridge, where correct tokens amounts are released and burnt. See CW.1, CW.2, CW.3.2, CW.3.4, CW.3.5, CW.3.6, CW.3.7, CW.3.8.
Bedrock: Both ETH and ERC20 can be released through the canonical bridge, whose correctness is ensured by BW.1, BW.2, BW.3.1, BW.3.3, BW.3.4, BW.3.5, BW.4, BW.5, BW.6, BW.7. ETH can also be released by direct calls to OptimismPortal
, with corresponding amount burnt in L2_TO_L1_MESSAGE_PASSER
. The latter requires users to take care of retries.
b. Assets released despite the debt token has not been burnt
Current: same as for a.
Bedrock: same as for a.
c. Asset transaction replay for a single burn transaction
Current: CW.3.2 avoid reentry, CW.3.8 prevents replay
Bedrock: BW.3.1 avoid reentry, BW.3.5 prevents replay
a. Incorrect amount of debt issued with respect to the deposited assets
Current: Assets can only be issued by L2StandardBridge
. The correct amount is ensured by the canonical bridge, through CD
Bedrock: Both ETH and ERC20 deposits can be finalized through the canonical bridge, whose correctness is ensured by BD. ETH can also be released by direct calls to OptimismPortal
, with corresponding amount issued on L2, users need to take care of retries.
b. Debt token issued although the actual verification did not take place
Current: Verification happens in CD.4
Bedrock: Verification happens in BD.3. An explicit trust on Optimism is needed due to BD.4
c. Anybody can issue debt tokens
Current: Tokens can only be issued in L2StandardBridge
, through canonical bridge route. CD.1, CD.2
Bedrock: BD.1. ETH can also be sent to L2 directly through depositTransaction
. Trusting Optimism rollup to transmit amount correctly.
Issues debt tokens although no assets have been deposited
Current: Minting on L2 can only be initiate by L1StandardBridge
, through CD trail
Bedrock: see BD.
Issues no debt tokens although assets have been deposited
Current: CD.5 allows for retries
Bedrock: BD.4 allows for retries
Accepts fraudulent messages from a fake custodian or a debt issuer
Current: see CD.1, CD.2, CW.1, CW.2
Bedrock: see BD.1, BW.1, BW.2
Does not relay messages
Current: see CD.5, CD.8, CW.3.1
Bedrock: see BD.3.3, BD.4, BW.3.3, BW.10
The source contract does not emit events upon deposit/withdrawal
Current: see CD.5, CD.6, CW.5 in place, events not depended for relay, CW.8
Bedrock: see BD.4, BW.5
Deposit from another account
Current: see CD.9, CD.10, CW.6, CW.7
Bedrock: see BD.6, BD.7, BW.7, BW.8
Execute any calls from any contract
L2->L1 transactions can be relayed by anyone, but there are enough checks in place.
Current: see CD.3, CW.3
Bedrock: see BD.3, BW.3, BW.4
51% attack
At current stage, sequencer is controlled by Optimism and requires explicit trust. Once fraud proof is in effect and decentralization is realized, a single honest fraud-proof verfier is all we need.
Current OP | Bedrock | |
---|---|---|
L2 Block Size | One tx per block | Multiple txs per block |
L2 Block Time | Variable | Fixed 2 sec per block, regardless of whether there are transactions or not |
Access to L1 Info | L1BlockNumber tracks blocknumber | L2Block, also contains timestamp, basefee, hash, and sequenceNumber |
Fees | Transactions submitted to CanonicalTransactionChain's queue lack a method for paying gas fees to the Sequencer. Gas is burned on L1 in an ad-hoc way to avoid spamming | A metering mechanism compliant with EIP1559 |
Deposit speed | 10-20 minutes | 2.5 minutes or less |
Withdrawal | Recorded in CanonicalTransactionChain and StateCommitmentChain in the same way as other transactions | A separate withdrawal root which allows withdrawal Merkle proofs to be 60% cheaper (in L1 gas) |
Data structure | In CanonicalTransactionChain, a batch consists of header, batch contexts and transactions, with sequencer submitted ones followed by enqueued txs | Within a block, the first tx is an L1 attributes deposit tx, which sets the parameters of L1Block contract, followed by user deposit transactions in the first block of an epoch (this first L2 block produced after an L1 block), and at last normal L2 transactions |
Fraud-proof | No fraud proof in operation | Cannon is half Geth and half MIPS, which conducts bisection game to identify single instruction for dispute based on pre-supplied preimage |
There has been a lot of excitement about Bedrock coming out of the Optimism team, since May 2022. The native support of ETH created a condition for more modularized and common functions across L1 and L2 contracts, thus reducing potential attack surface. With better security and cheaper fees, Optimism users will have a lot to look forward to.
If you’re interested in reading more on Optimism’s optimistic rollup, their documentation/code can be found in:
Optimism Documentation
Optimism Bedrock specs
Contracts for Current Optimism
Contracts for Bedrock