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:
- Withdrawal initiating transaction, which the user submits on L2.
- 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) - 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
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 ofL2CrossDomainMessenger
(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 theSafeCall
library, and if the minimum amount of gas cannot be met at the time of the external call from theOptimismPortal
->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 theL1CrossDomainMessenger.relayMessage
function's execution, extra gas will be added on top of the_minGasLimit
value by theCrossDomainMessenger.baseGas
function whensendMessage
is called on L2.
sendMessage
is a generic function that is used in both cross domain messengers. It calls_sendMessage
(opens new window), which is specific toL2CrossDomainMessenger
(opens new window)._sendMessage
callsinitiateWithdrawal
(opens new window) onL2ToL1MessagePasser
(opens new window). This function calculates the hash of the raw withdrawal fields. It then marks that hash as a sent message insentMessages
(opens new window) and emits the fields with the hash in aMessagePassed
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 valuesender
- The L2 address that initiated the transfer, typicallyL2CrossDomainMessenger
(opens new window)target
- The L1 target addressvalue
- The amount of WEI transferred by this transactiongasLimit
- 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
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 byop-node
. This new output root commits to the state of thesentMessages
mapping in theL2ToL1MessagePasser
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
A user calls the SDK's
CrossDomainMessenger.proveMessage()
(opens new window) with the hash of the L1 message. This function callsCrossDomainMessenger.populateTransaction.proveMessage()
(opens new window).To get from the L2 transaction hash to the raw withdrawal fields, the SDK uses
toLowLevelMessage
(opens new window). It gets them from theMessagePassed
event in the receipt.To get the proof, the SDK uses
getBedrockMessageProof
(opens new window).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
A user calls the SDK's
CrossDomainMessenger.finalizeMessage()
(opens new window) with the hash of the L1 message. This function callsCrossDomainMessenger.populateTransaction.finalizeMessage()
(opens new window).To get from the L2 transaction hash to the raw withdrawal fields, the SDK uses
toLowLevelMessage
(opens new window). It gets them from theMessagePassed
event in the receipt.Finally, the SDK calls
OptimismPortal.finalizeWithdrawalTransaction()
(opens new window) on L1.
# Onchain processing
OptimismPortal.finalizeWithdrawalTransaction()
(opens new window) runs several checks. The interesting ones are:- Verify the proof has already been submitted (opens new window).
- Verify the proof has been submitted long enough ago that the fault challenge period has already passed (opens new window).
- Verify that the proof applies to the current output root for that block (the output root for a block can be changed by the fault challenge process) (opens new window).
- Verify that the current output root for that block was proposed long enough ago that the fault challenge period has passed (opens new window).
- Verify that the transaction has not been finalized before to prevent replay attacks (opens new window).
If any of these checks fail, the transaction reverts.
Mark the withdrawal as finalized in
finalizedWithdrawals
.Run the actual withdrawal transaction (call the
target
contract with the calldata indata
).Emit a
WithdrawalFinalized
(opens new window) event.