Optimism Bedrock Wrap-Up Series 3 [ENG]

Flow of the deposit/withdrawal process

Aaron Lee
Tokamak Network
19 min readJan 5, 2024

--

Onther aims to deliver valuable information for current developers interested in Optimism and the evolution of the Ethereum ecosystem.

Special Thanks to Theo Lee, Austin Oh, Justin Gee, Ethan.K, and Max Lee for their contributions to this post.

You can check the Korean version of this article here.

Fig. Illustration of an Ethereum coin next to Optimism (Source: unplash)

This post is the third installment of the ‘Optimism Bedrock Wrap-Up Series,’ comprising five planned articles by Onther. It systematically breaks down the deposit and withdrawal process, revealing the underlying code logic layer by layer through a step-by-step analysis.

Given the interconnected nature of the series, we recommend reading the articles sequentially for a cohesive understanding.

Series 1. Overview of the Bedrock Upgrade: It provides an overview of the Bedrock upgrade, its components, and the smart contracts deployed within its layers.

Series 2. Key Changes Since the Bedrock Upgrade: In this section, we aim to unravel the substantial changes introduced by the Bedrock upgrade, laying the groundwork for a comprehensive understanding to navigate the upcoming segments of the series.

Series 4. Block Derivation: Once blocks are generated on the Layer 2(Optimism mainnet), the system initiates a process to roll-up these blocks to Layer 1. Subsequently, in the Block Derivation phase, L2 blocks are reconstructed using exclusively the data that has been rolled up. We will provide detailed guidance through each step of the block derivation process, offering code examination along the way.

Series 5. Roles and Behavior of Optimism Bedrock Components: We comprehensively examine the roles and operational logic of Op-Batcher and Op-Proposer as the final components of this series.

Deposit & Withdrawal Flow

The deposit and withdrawal process within Optimism constitute fundamental and pivotal procedures, serving as the linchpin connecting L1 and L2. Deposits, being the initial step in leveraging Optimism, empower users to transfer assets from L1 to L2, making these assets accessible within the L2 environment. On the other hand, withdrawal ensures the secure return of one’s assets from the L2 environment back to L1, supported by coherent and compelling proof. Throughout this series, we will meticulously examine the code governing the interaction between layers in the deposit and withdrawal processes.

Deposit Flow

In the Bedrock version, the term “Deposit Transaction” encompasses all L2 transactions and contract calls triggered by L1.

Fig. L1 to L2 deposit flow

In the above diagram, the essential contracts and functions invoked during the deposit process are structured based on the deposit flow.

①. The deposit process is initiated and concluded by StandardBridges, supporting the Lock & Mint function for the actual asset movement between L1 and L2.

②. The L1CrossDomainMessenger receives the transaction, invoking the depositTransaction function of the OptimismPortal contract, which triggers the TransactionDeposited event.

③. The Op-node component monitors L1 transaction information, parsing and delivering details to the execution engine (Op-geth) for the execution of the deposit transaction in L2 when a deposit transaction occurs in OptimismPortal.

④. The parsed information is transmitted to the L2CrossDomainMessenger, calling the relayMessage function, ultimately delivering the asset to the L2 user through the L2StandardBridge.

Now, let’s delve into the intricacies of this process through code.

L1

①. The L1StandardBridge contract identifies the assets for deposit, locks them, and utilizes the sendMessage function of the L1CrossDomainMessenger contract to relay the deposit transaction to L2.

Fig. L1StandardBridge.sol/depositERC20, depositETH (Source: github link)

The categorization of assets stems from the distinction in handling the deposit process for ERC20 and ETH. Each path involves the invocation of unique functions. This divergence begins with the asset locking process, where ERC20 assets are locked on the L1StandardBridge, while ETH assets are locked in the OptimismPortal. (Note that Optimism supports the deposit of specific ERC20 tokens classified as MINTABLE_ERC20, and you can verify the supported tokens through Link).

Parameters for depositERC20 / depositETH:

  • _l1Token (ERC20): The token contract address of the L1 ERC20 token being deposited.
  • l2Token (ERC20): The token contract address of the L2 ERC20 to be deposited.
  • _amount (ERC20): The amount of ERC20 to deposit.
  • minGasLimit (ERC20, ETH): The minimum gas cost for the deposit.
  • _extraData (ERC20, ETH): Extra data to attach to the deposit.

As the flow of depositERC20 and depositETH function calls is quite similar, we will not distinguish between the two assets and assume the deposit involves ETH.

Following this, the _initiateETHDeposit function calls _initiateBridgeETH in the L1StandardBridge.sol contract.

Fig.StandardBridge.sol/_initiateBridgeETH (Source: github link)

messenger.sendMessage parameters

  • address(OTHER_BRIDGE): Since it is currently called from L1, OTHER_BRIDGE will be the contract address of the L2StandardBridge.
  • abi.encodeWithSelector: Perform ABI encoding with finalizeBridgeETH.selector, _from, _to, _amount, and _extraData as one parameter.
Fig. CrossDomainMessenger.sol/sendMessage (Source: github link)

sendMessage parameters

  • _target: The L2 target address.
  • _message: The result of ABI encoding in abi.encodeWithSelector above will be _message.

②. The L1CrossDomainMessenger invokes the depositTransaction function of the OptimismPortal contract using the _sendMessage function, triggering the TransactionDeposited event.

Fig. L1CrossDomainMessenger.sol/_sendMessage (Source: github link)

_sendMessage parameters

  • _to: The destination address of the message, which will always be the L2CrossDomainMessenger address (0x42000000000000000000000000000000000000000007) on deposit.
  • _gasLimit: The gas limit, calculated according to the logic of the baseGas function.
  • _value: The amount of ETH to send to L2.
  • _data: An encoded value of all six parameters (this.relayMessage.selector, messageNonce(), msg.sender, _target, msg.value, and _minGasLimit, _message) passed as parameters when calling _sendMessage from sendMessage above. This becomes the calldata needed to relay the message. (The _data is generated according to the ABI format of the relayMessage).

The following describes the depositTransaction of the OptimismPortal contract.

Fig. OptimismPortal.sol/depositTransaction (Source: github link)

The depositTransaction function is raised by OptimismPortal to pass a deposit message to L2.

depositTransaction parameters

  • _to: The destination address.
  • _value: The amount of ETH to send to L2.
  • _gasLimit: Gas limit, calculated according to the logic of the baseGas function.
  • _isCreation: Checks if this is a CA creation transaction or a normal transaction.
  • _data: Same as the _data passed in the _sendMessage parameter above.

Now, to finalize the deposit process on L1, OptimismPortal fires the TransactionDeposited event.

The preceding steps concentrated on transmitting data from L1 to execute a transaction in L2. In the subsequent phase, the op-node and the execution engine take charge of handling the deposit transaction. In this stage, the op-node constructs the attributes of the L2 block through the deposit transaction initiates the creation of a new L2 block, and facilitates its transmission.

Op-node

③ Op-node is responsible for parsing the deposit transaction to L2, listening to all L1 transaction information and parsing it as soon as the transaction occurs in OptimismPortal.

In this course, it would be too much to analyze every function or method that occurs at the code level, so I will summarize it according to the flow of key logic.

Fig. state.go/Driver (Source: github link)

The Driver, operating within its eventLoop(), receives events that exert control and command over the op-node’s overarching behavior. These directives encompass tasks such as initiating or halting L2 block generation, fetching new block header information for L1 blocks, or ascertaining the block type.

Fig. pipeline.go/Step (Source: github link), engine_queue.go/Step (Source: github link)

The Step() function iterates through the functions in pipeline.go and engine_queue.go. It adds new L1 block data through the pipeline while maintaining synchronization with the execution engine queue.

Subsequently, we proceed to add NextAttributes to the attributes queue, situated between the batch queue and the engine queue. This queue is responsible for converting batches, comprising multiple L1 transactions, into payload attributes. The transformed payload attributes are then forwarded to the engine queue. This process serves as the creation of a template for payload attributes, facilitating the generation of L2 blocks from L1 transactions during a specific epoch.

Fig. sequencer.go/StartBuildingBlock (Source: github link)

Preparations for creating the L2 block begin with the StartBuildingBlock method, which gets the L1 block information from the l1OriginSelector object.

Following this, a context.Context object named “fetchCtx” is created, serving the purpose of invoking the PreparePayloadAttributes method within the AttributesBuilder object. At this juncture, we undertake the preparation of payload attributes, resembling the following:

Fig. attributes_queue.go/AttributesBuilder(Source: github link), attributes.go/PreparePayloadAttributes(Source: github link)

The PreparePayloadAttributes method defined in the AttributesBuilder interface is called with three parameters returned: ctx context.Context, l2Parent eth.L2BlockRef, and epoch eth.BlockID. Several functions are called to extract the data contained in each of these parameters, with the key function being the DeriveDeposits function.

Fig. deposit.go/DeriveDeposits (Source: github link)

The DeriveDeposits function takes as parameters a slice of the *types.Receipt pointer called receipts and a common.Address called depositContractAddr. To do this, the DeriveDeposit function first calls the UserDeposits function with receipts and depositContractAddr as arguments, as shown below.

Fig. deposit.go/UserDeposits (Source: github link)

UserDeposits iterates over the log field of L1’s transaction receipt array, and if it finds a log matching the depositContractAddr, it unmarshals (decodes) that receipt and adds it to *types.DepositTx as an output slice.

  • receipts: The result of executing a transaction in the EVM, which contains the actual gas cost, status code, and logs generated when calling the contract. L1 receipts encode the specific information of the executed transaction and are written to the Merkle Patricia Trie root with an index key, and Optimism receives the deposit transaction based on that information. (For more information on receipts, see section 4.3.1 of the Ethereum yellow paper.)
  • depositContractAddr: The address of the specified L1 deposit contract, in this case the address of the OptimismPortal.

In this way, the deposit transactions originating from OptimismPortal are identified and extracted from the transactions originating from L1.

  • UnmarshalDepositLogEvent(log): The purpose of the function is to unmarshal the deposit log and add log to the *types.DepositTx pointer.
  • When we refer to “unmarshaling,” it implies the extraction of fields that serve as parameters for the depositTransaction, such as _to, _value, _gasLimit, _isCreation, _data, etc.

Once we have returned *types.DepositTx, we return to the DeriveDeposit function to perform the remaining steps.

Fig. deposit.go/DeriveDeposits (Source: github link)

encodedTxs: uses a for loop to continuously get the *types.DepositTx values and encodes them into a byte array with the types.NewTx(tx).MarshalBinary() method. If this process is successful, all the byte arrays are stored in opaqueTx, and any errors that occur are logged in err.

Having obtained all the necessary return values from the DeriveDeposits function, we return to the PreparePayloadAttributes function, concluding it by returning a pointer (*eth.PayloadAttributes) that encapsulates the payload attributes essential for crafting a new L2 block.

Following this, in the createNextAttributes method, it is generating the payload attributes for the subsequent L2 block to be created.

Fig. attributes_queue.go/createNextAttributes (Source: github link)

The generation of new payload attributes is synonymous with appending a new queue to an AttributesQueue. The ‘builder’ object within the AttributesQueue employs the ‘PreparePayloadAttributes’ method to acquire the payload attributes for the subsequent L2 block. Subsequently, it records the number of transactions and the timestamp of the batch to the respective PayloadAttributes object, culminating in the creation and return of a new PayloadAttributes object.

Once the PayloadAttributes are created, let’s return to the sequencer’s BuildingBlock method to complete the block creation.

Fig. sequencer.go/StartBuildingBlock (Source: github link)

Utilize the ConfirmPayload method to identify any errors in constructing the L2 block. If no errors are detected, proceed to record the finalized L2 block in the *eth.ExecutionPayload pointer.

It’s important to note that the sequencer does not directly generate the block; instead, the sequencer and execution engine collaborate through the engine API to bring about the block’s creation.

Fig. engine_update.go/StartPayload (Source: github link)

StartPayload() starts a build for the given payload in the execution engine. This function runs ForkchoiceUpdate(), which processes the transaction in the execution engine (op-geth) via the engine_forkchoiceUpdatedV1 API and creates the L2 block. We'll cover this in more detail in part 4 of the series, as it's becoming too much to cover in the current series.

L2

④. As the execution engine processes the deposit transaction, the relayMessage of the CrossDomainMessenger is called.

Fig. CrossDomainMessenger.sol/relayMessage (Source: github link)

relayMessage Parameters

  • _nonce: The nonce of the message being relayed.
  • _sender: The address of the user who sent the message.
  • _target: The L2 target address to deposit to (L2StandardBridge address).
  • _value: Ethereum amount to send.
  • _minGasLimit: The minimum gas limit to run the message.
  • _message: The message to send to the target address.

If there is no error in relaying the message, the relayed calldata is used to deliver the funds to the actual target address.

Fig. CrossDomainMessenger.sol/relayMessage (Source: github link)

xDomainMsgSender = _sender

  • Set _sender to the xDomainMsgSender variable to keep track of the sender’s information.

bool success = SafeCall.call(_target, gasleft() — RELAY_RESERVED_GAS, _value, _message)

  • Using the SafeCall.sol contract, call RELAY_RESERVED_GAS, _value, and _message to the address _target, and get success as the return value.

Finally, we call L2StandardBridge with the relayed deposit information and pass it to the account to be deposited.

Fig. L2StandardBridge.sol/finalizeDeposit (Source: github link)

The finalizeDeposit function takes the above six parameters and finalizes the deposit on L2. The function first checks whether the asset being deposited is ETH or ERC20, and if it is ETH, it calls the finalizeBridgeETH function with _from, _to, _amount, and _extraData to finalize the deposit, and if it is ERC20 token, it calls the finalizeBridgeERC20 function with _l2Token, _l1Token, _from, _to, _amount, and _extraData to finalize the deposit.

This is how the deposit process works, and the next step is to analyze the withdrawal process.

Withdrawal Flow

Fig. Optimism Bridge withdrawal progress capture screen. (Source: optimism bridge)

When a user makes a withdrawal through Optimism Bridge, the withdrawal screen looks like the one above, and the user needs to submit three transactions to make the withdrawal.

①. Withdrawal initiating transaction: This is the transaction where the user sends the first withdrawal request in L2.

②. Withdrawal proving transaction: This is the first step in Two-Phase Withdrawals, where the user submits a ‘proveWithdrawalTransaction’ to the OptimismPortal in L1 to prove that the withdrawal is valid.

③. Withdrawal finalizing transaction: This corresponds to the ‘finalizeWithdrawalTransaction’ submitted by the user to reclaim the asset after the completion of the finalization period (previously known as the Challenge Period in the Legacy version). It marks the conclusive step in Two-Phase Withdrawals, where the user asserts their withdrawal.

In Series 2, we conducted a brief overview of Two-Phase Withdrawals. The current series delves into a detailed code-level analysis of the withdrawal process from the viewpoint of the party initiating the withdrawal.

L2

①. Withdrawal initiating transaction

Fig. Function path for a withdrawal initiation transaction

The diagram above illustrates the functional pathway for a withdrawal transaction originating from L2, just before it is relayed to L1.

Fig. L2StandardBridge.sol/withdraw (Source: github link)

The withdrawal request commences with the withdraw function of L2StandardBridge.sol deployed in L2. This function is exclusively available for withdrawing mintable ERC20 tokens (OptimismMintableERC20) or ETH from L2.

Withdrawal parameters.

  • _l2Token: The address (sender) of the L2 token to be withdrawn.
  • _amount: The amount of L2 tokens to withdraw.
  • _minGasLimit: The minimum gas limit to use for the transaction.
  • _extraData: extra data to attach to the withdrawal.

The withdraw function then calls the _initiateWithdrawal function.

Fig. L2StandardBridge.sol/_initiateWithdrawal (Source: github link)

It is crucial to highlight that ETH generated before the Bedrock upgrade was managed by a pre-deployment contract named LEGACY_ERC20_ETH. However, post the Bedrock upgrade, all ETH was migrated to the native concept. Consequently, ETH generated before and after the Bedrock upgrade should be handled distinctly.

LEGACY_ERC20_ETH(0xDeadDeAddeAddEAddeadDEaDDEAdDeaDDeAD0000)

  • The contract that managed ETH in the legacy version, which is now deprecated.

OPTIMISM_MINTABLE_ERC20_FACTORY(0x4200000000000000000000000000000000000012;)

  • This contract allows L2StandardBridge to mint and burn tokens when depositing or withdrawing ERC20 or ETH..

If the ETH was created before the Legacy version, the withdrawal is performed by calling the _emitETHBridgeInitiated function through the _initiateBridgeETH function in StandardBridge.sol. If the ETH or ERC20 token was created after the Bedrock upgrade, the withdrawal is performed by calling the _emitERC20BridgeInitiated function through the _initiateBridgeERC20 function in StandardBridge.sol.

As LEGACY_ERC20_ETH is deprecated, we’ll assume that we are withdrawing ETH generated after the Bedrock upgrade.

Fig. StandardBridge.sol/_initiateBridgeERC20 (Source: github link)

Following this, the _initiateBridgeERC20 function prepares to call L2CrossdomainMessenger’s sendMessage function.

_initiateBridgeERC20 parameters.

  • _localToken: The L2 address where the token to be withdrawn is located.
  • _remoteToken: The L1 address where the token is to be withdrawn.

And there are a few checks:

  • _isCorrectTokenPair: Verifies whether the types of tokens intended for withdrawal from L2 to L1 are matching.
  • OptimismMintableERC20(_localToken).burn: If the token types match, it proceeds to burn a specified amount of _localToken. If not, the function returns to the entity that initiated the withdrawal(_from address).
  • Finally, upon the generation of the withdrawal, an ERC20BridgeInitiated event is triggered through the _emitERC20BridgeInitiate function.

Once the ERC20BridgeInitiated event is emitted, the system prepares to invoke the sendMessage function by calling MESSENGER.sendMessage.

Fig. StandardBridge.sol/MESSENGER.sendMessage (Source: github link)

A closer examination of MESSENGER.sendMessage reveals the following details:

  • value: _amount: Represents the amount of ETH to be included in the message.
  • address(OTHER_BRIDGE): Given that the transaction originated on L2, OTHER_BRIDGE corresponds to the L1StandardBridge.
  • address.abi.encodeWithSelector: Involves the ABI information of the function slated for execution on L1StandardBridge. This ABI is encoded and dispatched within a function named finalizedBrideETH, encompassing details such as _from, _to, _amount, and _extraData.

Subsequently, the sendMessage function is invoked to relay the withdrawal message to L1.

Fig. CrossDomainMessenger.sol/sendMessage (Source: github link)

The sendMessage function takes three parameters.

  • _target: The target address on L1.
  • _message: The call data for the L1 transaction.
  • _minGasLimit: The minimum amount of gas required for the entire process until the withdrawal finalizing transaction.

As _sendMessage serves as a universal function employed by both L1 and L2 cross-domain messengers, we subsequently invoke L2CrossDomainMessenger’s _sendMessage once again. Following this, _sendMessage triggers the initiateWithdrawal function within L2ToL1MessagePasser, which retrieves the withdrawalHash value for the raw withdrawal field from the current state.

Fig.L2ToL1MessagePasser.sol/initiateWithdrawal (Source: github link)

The raw withdrawal field within the initiateWithdrawal function comprises the following:

  • messageNonce: A unique number assigned to the transaction to prevent the hashing of two identical withdrawals to the same value.
  • msg.sender: The L2 address that initiated the transfer.
  • _target: The L1 address where the assets withdrawn from L2 will be deposited.
  • msg.value: The amount of ETH to be transferred.
  • _gasLimit: The gas limit for the transaction.
  • _data: The calldata of the withdrawal transaction.

At this stage, the withdrawal process on L2 concludes by triggering the MessagePassed event, and the withdrawal message is forwarded to L1.

Fig. Withdrawal flow diagram (Source: Optimism Docs link)

After the withdrawal message is transmitted to L1, a brief ‘waiting period’ ensues. This is the time required for the Proposer to generate the output root encompassing the withdrawal transactions processed thus far in L2. The generated output root is subsequently transmitted to the L2OutputOracle, a process that typically takes about an hour on mainnet.

L1

②. Withdrawal proving transaction:

Fig. Withdrawal proving transaction: Required data collection and creation process to generate a ‘proof’

Now that the withdrawal transaction initiated in L2 has been proposed to L2OutputOracle by the proposer, the subsequent step involves submitting the ‘proof’ alongside the message to OptimismPortal. Here, the user conveys the necessary input values essential for proof generation to the relayer from L2. The relayer then generates the proof and submits it to OptimismPortal.

Fig. OptimismPortal.sol/proveWithdrawalTransaction (Source: github link), Types.sol/WithdrawalTransaction, OutputRootProof (Source: github link)

proveWithdrawalTransaction() or ‘prove’ a withdrawal, comprises four parameters:

  • _tx: Information about the transaction.
  • _l2OutputIndex: The index of the L2 output.
  • _outputRootProof: An inclusion proof validating that the storage root of the L2ToL1MessagePasser contract is included in the state root.
  • _withdrawalProof: An inclusion proof verifying that the storage of the L2ToL1MessagePasser contract contains the corresponding withdrawal.

While we have outlined that a ‘proof’ consists of these four parameters, the question arises: How is the information obtained from different locations? To address this, Optimism provides the capability to directly call proveMessage() through the bridge service after the L2 output has been rolled up into L1.

Fig. sdk/src/cross-chain-messenger.ts/proveMessage (Source: github link)

The proveMessage function receives and stores the necessary data for proof creation, including the following components:

  • withdrawal: Crafted by extracting a withdrawal event, such as MessagePassed, generated during the course of the withdrawal.
  • proof: Proof is generated and stored here, and the method of its creation will be explained in detail below.
Fig. cross-chain-messenger.ts/proveMessage, getBedrockMessageProof (Source: github link)

① To generate the proof, we utilize getMessageBedrockOutput to query and retrieve the output root that was proposed to the L2OutputOracle.

② Following this, we commence the creation of a Withdrawal. This involves generating a stateTrieProof with several parameters. These parameters include the slot hash value corresponding to the slot location of the withdrawal, the messagePasserStorageRoot (the storage root of the L2ToL1MessagePasser contract), and the withdrawal hash.”

Once all the necessary withdrawal information is obtained in the manner described above, the following parameter groups are assembled.

Fig. cross-chain-messenger.ts/proveMessage (Source: github link), OptimismPortal.sol/proveWithdrawalTransaction (Source: github link)

Consequently, we conclude the process by invoking the proveWithdrawalTransaction function from OptimismPortal, providing the aforementioned four parameter groups.

Fig. OptimismPortal.sol/getL2Output (Source: github link)

In the subsequent step of the proveWithdrawalTransaction() function, a procedure is executed to validate the output root of the prepared proof. To obtain the output root for the withdrawal transaction, the L2OutputIndex function is employed to retrieve the specific output root from the L2OutputOracle contract. The system then verifies whether this output root aligns with the one received as a parameter, signaling the completion of the verification process.

Fig. OutputRootProof parameters colored in Types.sol. (Source: github link)

Taking a moment to examine the OutputRootProof, the output root is essentially a hash encompassing information about the version and payload. The payload, in turn, includes components such as state_root, withdrawal_storage_root, and latest_block_hash.

Fig. OptimismPortal.sol/provenWithdrawal (Source: github link)

Ultimately, the proof is added to a mapping variable named provenWithdrawal, triggering an event indicating that proof has been submitted for the corresponding withdrawal

Fig. Optimism bridge withdrawal progress capture screen (Source: optimism bridge)

Upon the submission of the withdrawal proof, a 7-day finalization period begins. Following this period, the user is required to initiate the withdrawal claim personally. In this scenario, the user can finalize their withdrawal by executing the finalizeMessage() function through the bridge service. Internally, finalizeMessage() calls finalizeWithdrawalTransaction() in OptimismPortal.

③. Withdrawal finalizing transaction

Fig. Withdrawal confirmation transaction: the process of claiming a withdrawal after the confirmation period.

This is the final step to finalize the withdrawal. finalizeWithdrawalTransaction fetches the ProvenWithdrawal created during the proof process, using the withdrawal hash as the identifier.

Fig. OptimismPortal.sol/finalizeWithdrawalTransaction (Source: github link)

Following this, the code checks the provenWithdrawal.timestamp to determine whether the finalization period (which is 7 days on the mainnet) for that particular proof has concluded. A value of 0 indicates that the finalization period is still ongoing, while a timestamp is assigned once the finalization period has passed.

Several additional checks are also performed, including verifying the match between provenWithdrawal.outputroot in the current proof and the output root for the withdrawal in L2OutputOracle. If all these checks pass successfully, the withdrawal of ETH can be executed by invoking the relayMessage function of the CrossDomainMessenger.

Fig. OptimismPortal.sol/SafeCall.callWithMinGas (Source: github link), CrossDomainMessenger.sol/relayMessage (Source: github link), StandardBridge.sol//finalizeBridgeETH (Source: github link)

The relayMessage takes the sender’s address, the address to withdraw ETH, and the amount of ETH to be withdrawn as parameters, and calls StandardBridge’s finalizeBridgeETH, which is the target function for the withdrawal message generated by L2. Then, in the finalizeBridgeETH function, mint the amount burned by L2 and send it to the L1 user address to complete the withdrawal process.

Closing thoughts

Up to this point, we’ve explored the flow of the deposit-withdrawal process after the Bedrock upgrade as much as possible at the code level. In particular, the steps involved in retrieving and cross-verifying components necessary for generating proofs in the withdrawal process, such as withdrawal information, block details, and output root value, presented a challenging journey. Understanding the implemented code was a challenge in itself, and I can’t help but admire the effort put into researching and developing such a complex process.

In the upcoming series, we will analyze the block derivation process where a batcher transaction submitted to L1 is transformed back into L2 blocks.

Reference:

https://static.optimism.io/optimism.tokenlist.json

--

--