Withdrawal Flow


In Optimism terminology, a withdrawal is a transaction sent from L2 (OP Mainnet, OP Goerli etc.) to L1 (Ethereum mainnet, Goerli, etc.). This withdrawal may or may not have assets attached to it.

Withdrawals require the user to submit three transactions:

  1. Withdrawal initiating transaction, which the user submits on L2.
  2. Withdrawal proving transaction, which the user submits on L1 to prove that the withdrawal is legitimate (based on a merkle patricia trie root that commits to the state of the L2ToL1MessagePasser's storage on L2)
  3. Withdrawal finalizing transaction, which the user submits on L1 after the fault challenge period has passed, to actually run the transaction on L1, claim any assets attached, etc.

You can see an example of how to do this in the tutorials (opens new window).

# Withdrawal initiating transaction

  1. On L2 somebody (either an externally owned account (EOA) directly or a contract acting on behalf of an EOA) calls the sendMessage (opens new window) function of L2CrossDomainMessenger (opens new window).

    This function accepts three parameters:

    • _target, target address on L1.
    • _message, the L1 transaction's calldata, formatted as per the ABI (opens new window) of the target account.
    • _minGasLimit, The minimum amount of gas that the withdrawal finalizing transaction can provide to the withdrawal transaction. This is enforced by the SafeCall library, and if the minimum amount of gas cannot be met at the time of the external call from the OptimismPortal -> L1CrossDomainMessenger, the finalization transaction will revert to allow for re-attempting with a higher gas limit. In order to account for the gas consumed in the L1CrossDomainMessenger.relayMessage function's execution, extra gas will be added on top of the _minGasLimit value by the CrossDomainMessenger.baseGas function when sendMessage is called on L2.
  2. sendMessage is a generic function that is used in both cross domain messengers. It calls _sendMessage (opens new window), which is specific to L2CrossDomainMessenger (opens new window).

  3. _sendMessage calls initiateWithdrawal (opens new window) on L2ToL1MessagePasser (opens new window). This function calculates the hash of the raw withdrawal fields. It then marks that hash as a sent message in sentMessages (opens new window) and emits the fields with the hash in a MessagePassed event (opens new window).

    The raw withdrawal fields are:

    • nonce - A single use value to prevent two otherwise identical withdrawals from hashing to the same value
    • sender - The L2 address that initiated the transfer, typically L2CrossDomainMessenger (opens new window)
    • target - The L1 target address
    • value - The amount of WEI transferred by this transaction
    • gasLimit - Gas limit for the transaction, the system guarantees that at least this amount of gas will be available to the transaction on L1. Note that if the gas limit is not enough, or if the L1 finalizing transaction does not have enough gas to provide that gas limit, the finalizing transaction returns a failure, it does not revert.
    • data - The calldata for the withdrawal transaction
  4. When op-proposer proposes a new output (opens new window), the output proposal includes the output root (opens new window), provided as part of the block by op-node. This new output root commits to the state of the sentMessages mapping in the L2ToL1MessagePasser contract's storage on L2, and it can be used to prove the presence of a pending withdrawal within it.

# Withdrawal proving transaction

Once an output root that includes the MessagePassed event is published to L1, the next step is to prove that the message hash really is in L2. Typically this is done by the SDK (opens new window).

# Offchain processing

  1. A user calls the SDK's CrossDomainMessenger.proveMessage() (opens new window) with the hash of the L1 message. This function calls CrossDomainMessenger.populateTransaction.proveMessage() (opens new window).

  2. To get from the L2 transaction hash to the raw withdrawal fields, the SDK uses toLowLevelMessage (opens new window). It gets them from the MessagePassed event in the receipt.

  3. To get the proof, the SDK uses getBedrockMessageProof (opens new window).

  4. Finally, the SDK calls OptimismPortal.proveWithdrawalTransaction() (opens new window) on L1.

# Onchain processing

OptimismPortal.proveWithdrawalTransaction() (opens new window) runs a few sanity checks. Then it verifies that in L2ToL1MessagePasser.sentMessages on L2 the hash for the withdrawal is turned on, and that this proof have not been submitted before. If everything checks out, it writes the output root, the timestamp, and the L2 output index to which it applies in provenWithdrawals and emits an event.

The next step is to wait the fault challenge period, to ensure that the L2 output root used in the proof is legitimate, and that the proof itself is legitimate and not a hack.

# Withdrawal finalizing transaction

Finally, once the fault challenge period passes, the withdrawal can be finalized and executed on L1.

# Offchain processing

  1. A user calls the SDK's CrossDomainMessenger.finalizeMessage() (opens new window) with the hash of the L1 message. This function calls CrossDomainMessenger.populateTransaction.finalizeMessage() (opens new window).

  2. To get from the L2 transaction hash to the raw withdrawal fields, the SDK uses toLowLevelMessage (opens new window). It gets them from the MessagePassed event in the receipt.

  3. Finally, the SDK calls OptimismPortal.finalizeWithdrawalTransaction() (opens new window) on L1.

# Onchain processing

  1. OptimismPortal.finalizeWithdrawalTransaction() (opens new window) runs several checks. The interesting ones are:

    If any of these checks fail, the transaction reverts.

  2. Mark the withdrawal as finalized in finalizedWithdrawals.

  3. Run the actual withdrawal transaction (call the target contract with the calldata in data).

  4. Emit a WithdrawalFinalized (opens new window) event.