Skip to content
This repository was archived by the owner on Apr 6, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 81 additions & 70 deletions pages/stack/transactions/withdrawal-flow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,110 +33,121 @@ Withdrawals require the user to submit three transactions:
3. **Withdrawal finalizing transaction**, which the user submits on L1 after the fault challenge period has passed, to actually run the transaction on L1.

<Callout type="info">
You can see an example of how to do this [in the bridging tutorials](/app-developers/tutorials/bridging/cross-dom-bridge-erc20).
You can see an example of how to do this [in the bridging tutorials](/app-developers/tutorials/bridging/cross-dom-bridge-erc20).
</Callout>

## Withdrawal initiating transaction

1. On L2, a user, either an externally owned account (EOA) directly or a contract acting on behalf of an EOA, calls the [`sendMessage`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol#L249-L289) function of the [`L2CrossDomainMessenger`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/universal/CrossDomainMessenger.sol) contract.

This function accepts three parameters:

* `_target`, target address on L1.
* `_message`, the L1 transaction's calldata, formatted as per the [ABI](https://docs.soliditylang.org/en/v0.8.19/abi-spec.html) of the target address.
* `_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`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2CrossDomainMessenger.sol#L48-L60), which is specific to [`L2CrossDomainMessenger`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2CrossDomainMessenger.sol).
## Withdrawal flow overview

3. `_sendMessage` calls [`initiateWithdrawal`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2ToL1MessagePasser.sol#L91-L129) on [`L2ToL1MessagePasser`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2ToL1MessagePasser.sol).
This function calculates the hash of the raw withdrawal fields.
It then [marks that hash as a sent message in `sentMessages`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2ToL1MessagePasser.sol#L114) and [emits the fields with the hash in a `MessagePassed` event](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2ToL1MessagePasser.sol#L116-L124).
The complete withdrawal process using viem has the following steps:

The raw withdrawal fields are:
1. Initiate the withdrawal transaction on L2
2. Wait for the L2 output to be proposed (typically takes up to one hour)
3. Prove the withdrawal transaction on L1
4. Wait for the fault challenge period to pass (7 days on mainnet)
5. Finalize the withdrawal transaction on L1

* `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`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L2/L2CrossDomainMessenger.sol)
* `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
## Setting up the viem clients

4. When `op-proposer` [proposes a new output](https://github.com/ethereum-optimism/optimism/blob/4a3d3fb444f50bed6a6991785ea5634e0efa07a4/op-proposer/proposer/driver.go#L311), the output proposal includes [the output root](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/op-proposer/proposer/l2_output_submitter.go#L316), 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.
Before working with withdrawals, you need to set up viem clients for both L1 and L2 chains with the appropriate OP Stack extensions:

## Withdrawal proving transaction
```js file=<rootDir>/public/tutorials/withdrawal-flow.js#L3-L41 hash=9d315e38bbf5f0f6cebcab266a675657
```

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](https://sdk.optimism.io/classes/crosschainmessenger#proveMessage-2).
## Withdrawal initiating transaction

### Offchain processing
The first step is to initiate a withdrawal on L2 by using the `buildInitiateWithdrawal` and `initiateWithdrawal` functions:

1. A user calls the SDK's [`CrossDomainMessenger.proveMessage()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L1452-L1471) with the hash of the L2 message.
This function calls [`CrossDomainMessenger.populateTransaction.proveMessage()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L1746-L1798).
```js file=<rootDir>/public/tutorials/withdrawal-flow.js#L43-L47 hash=a5958f7bf103652287e81582bd217a3f
```

2. To get from the L2 transaction hash to the raw withdrawal fields, the SDK uses [`toLowLevelMessage`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L368-L450).
It gets them from the `MessagePassed` event in the receipt.
Behind the scenes, this process triggers the following:

3. To get the proof, the SDK uses [`getBedrockMessageProof`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L1348-L1395).
1. When you call `initiateWithdrawal`, the transaction interacts with the [`L2ToL1MessagePasser`](https://github.com/ethereum-optimism/optimism/blob/1a8fe18c4989bfd0852a8873f30422542ad4f44d/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol#L19) contract on L2.
2. The transaction calculates a hash of the withdrawal fields and marks it as a sent message in the contract's storage.
3. A [`MessagePassed`](https://github.com/ethereum-optimism/optimism/blob/1a8fe18c4989bfd0852a8873f30422542ad4f44d/packages/contracts-bedrock/src/L2/L2ToL1MessagePasser.sol#L40) event is emitted with the withdrawal details and hash.
4. When [`op-proposer`](/operators/chain-operators/architecture#op-proposer) proposes a new output, it includes the output root which commits to the state of the `L2ToL1MessagePasser` contract's storage, allowing your withdrawal to be proven on L1.

4. Finally, the SDK calls [`OptimismPortal.proveWithdrawalTransaction()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L234-L318) on L1.
The raw withdrawal fields included in this process are:

### Onchain processing
* `nonce` - A single-use value to prevent identical withdrawals from hashing to the same value
* `sender` - The L2 address that initiated the transfer
* `target` - The L1 target address
* `value` - The amount of WEI transferred
* `gasLimit` - Gas limit for the transaction execution on L1
* `data` - The calldata for the withdrawal transaction

[`OptimismPortal.proveWithdrawalTransaction()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L234-L318) 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 has 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.
## Withdrawal proving transaction

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.
Once an output root that includes your withdrawal is published to L1, you need to prove that the withdrawal is legitimate:

## Withdrawal finalizing transaction
```js file=<rootDir>/public/tutorials/withdrawal-flow.js#L49-L73 hash=e8674293cd269e10b9fcefe2a101b225
```

Finally, once the fault challenge period passes, the withdrawal can be finalized and executed on L1.
The proving process works as follows:

To do so, a user, either an externally owned account (EOA) directly or a contract acting on behalf of an EOA, calls the [`finalizeWithdrawalTransaction`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L320-L420) function of the [`OptimismPortal`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol) contract.
1. The `waitToProve` function waits until the L2 output containing your withdrawal has been proposed on L1.
2. The `buildProveWithdrawal` function prepares the necessary parameters for proving, including the withdrawal details and the output proof.
3. The `proveWithdrawal` function calls [`OptimismPortal.proveWithdrawalTransaction()`](https://github.com/ethereum-optimism/optimism/blob/1a8fe18c4989bfd0852a8873f30422542ad4f44d/packages/contracts-bedrock/interfaces/L1/IOptimismPortal2.sol#L97) on L1.
4. The portal contract verifies that the withdrawal hash exists in `L2ToL1MessagePasser.sentMessages` on L2, and that this proof hasn't been submitted before.
5. If verification passes, the contract writes the output root, timestamp, and L2 output index to `provenWithdrawals` and emits an event.

## Expected internal reverts in withdrawal transactions
You can also check a withdrawal's status at any time using:

During the withdrawal process, users may observe internal reverts when viewing the transaction on **Etherscan**. This is a common point of confusion but is expected behavior.
```javascript
const status = await publicClientL1.getWithdrawalStatus({
receipt,
targetChain: walletClientL2.chain
})

These internal reverts often show up in yellow on the Etherscan UI and may cause concern that something went wrong with the transaction. However, these reverts occur due to the non-standard proxy used in Optimism, specifically the **Chugsplash Proxy**. The Chugsplash Proxy sometimes triggers internal calls that revert as part of the designed flow of the withdrawal process.
if (status === 'ready-to-prove') {
// Ready to prove the withdrawal
}
```

### Why do these reverts happen?
## Withdrawal finalizing transaction

The Chugsplash Proxy operates differently than standard proxies. During a withdrawal transaction, it may trigger internal contract calls that result in reverts, but these reverts do not indicate that the withdrawal has failed. Instead, they are part of the internal logic of the system and are expected in certain scenarios.
After the fault challenge period passes (7 days on mainnet), you can finalize and execute the withdrawal on L1:

### Key takeaways:
```js file=<rootDir>/public/tutorials/withdrawal-flow.js#L75-L92 hash=d983519f2c3465bea63c1f27e4aa7b5f
```

* **Internal Reverts Are Expected**: These reverts are part of the normal operation of the Chugsplash Proxy during withdrawal transactions and do not represent an error.
* **No Cause for Concern**: Although Etherscan highlights these reverts, they do not affect the final success of the transaction.
* **User Assurance**: If you encounter these reverts during a withdrawal transaction, rest assured that the withdrawal will still finalize as expected.
The finalization process:

### Offchain processing
1. `waitToFinalize` waits until the fault challenge period has passed.
2. `finalizeWithdrawal` calls `OptimismPortal.finalizeWithdrawalTransaction()` on L1.
3. The portal contract runs several checks:
* Verifies the proof has been submitted
* Confirms the challenge period has passed
* Verifies the proof applies to the current output root
* Checks that the transaction hasn't been finalized before
4. If all checks pass, the contract:
* Marks the withdrawal as finalized in `finalizedWithdrawals`
* Executes the actual withdrawal transaction (calls the target contract with the calldata in `data`)
* Emits a `WithdrawalFinalized` event

1. A user calls the SDK's [`CrossDomainMessenger.finalizeMessage()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L1473-L1493) with the hash of the L1 message.
This function calls [`CrossDomainMessenger.populateTransaction.finalizeMessage()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L1800-L1853).
You can track the time until finalization using:

2. To get from the L2 transaction hash to the raw withdrawal fields, the SDK uses [`toLowLevelMessage`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/sdk/src/cross-chain-messenger.ts#L368-L450).
It gets them from the `MessagePassed` event in the receipt.
```javascript
const { seconds } = await publicClientL1.getTimeToFinalize({
withdrawalHash: withdrawal.withdrawalHash,
targetChain: walletClientL2.chain
})
console.log(`Time until finalization: ${seconds} seconds`)
```

3. Finally, the SDK calls [`OptimismPortal.finalizeWithdrawalTransaction()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L320-L420) on L1.
## Complete example

### Onchain processing
Here's a complete example of the withdrawal flow:

1. [`OptimismPortal.finalizeWithdrawalTransaction()`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L320-L420) runs several checks. The interesting ones are:
```js file=<rootDir>/public/tutorials/withdrawal-flow.js#L3-L92 hash=35ea41209638633005a1023433cce7b7
```

* [Verify the proof has already been submitted](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L341-L347).
* [Verify the proof has been submitted long enough ago that the fault challenge period has already passed](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L357-L364).
* [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)](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L366-L378).
* [Verify that the current output root for that block was proposed long enough ago that the fault challenge period has passed](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L380-L384).
* [Verify that the transaction has not been finalized before to prevent replay attacks](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L386-L390).
## Expected internal reverts in withdrawal transactions

If any of these checks fail, the transaction reverts.
During the withdrawal process, users may observe internal reverts when viewing the transaction on **Etherscan**. This is a common point of confusion but is expected behavior.

2. Mark the withdrawal as finalized in `finalizedWithdrawals`.
These internal reverts often show up in yellow on the Etherscan UI and may cause concern that something went wrong with the transaction. However, these reverts occur due to the non-standard proxy used in Optimism, specifically the **Chugsplash Proxy**. The Chugsplash Proxy sometimes triggers internal calls that revert as part of the designed flow of the withdrawal process.

3. Run the actual withdrawal transaction (call the `target` contract with the calldata in `data`).
### Why do these reverts happen?

4. Emit a [`WithdrawalFinalized`](https://github.com/ethereum-optimism/optimism/blob/62c7f3b05a70027b30054d4c8974f44000606fb7/packages/contracts-bedrock/contracts/L1/OptimismPortal.sol#L118) event.
The Chugsplash Proxy operates differently than standard proxies. During a withdrawal transaction, it may trigger internal contract calls that result in reverts, but these reverts do not indicate that the withdrawal has failed. Instead, they are part of the internal logic of the system and are expected in certain scenarios.
1 change: 0 additions & 1 deletion public/tutorials/app.mts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
createWalletClient,
http,
defineChain,
publicActions,
getContract,
Address,
Expand Down
Loading