Withdrawals
This document contains a detailed explanation of how asset withdrawals work.
Native ETH withdrawals
This section explains step by step how native ETH withdrawals work.
On L2:
-
The user sends a transaction calling
withdraw(address _receiverOnL1)on theCommonBridgeL2contract, along with the amount of ETH to be withdrawn. -
The bridge sends the withdrawn amount to the burn address.
-
The bridge calls
sendMessageToL1(bytes32 data)on theL2ToL1Messengercontract, withdatabeing:bytes32 data = keccak256(abi.encodePacked(ETH_ADDRESS, ETH_ADDRESS, _receiverOnL1, msg.value))The
ETH_ADDRESSis an arbitrary address we use, meaning the "token" to transfer is ETH. -
L2ToL1Messengeremits anL1Messageevent, with the address of the L2 bridge contract anddataas topics, along with a unique message ID.
Off-chain:
- On each L2 node, the L1 watcher extracts
L1Messageevents, generating a merkle tree with the hashed messages as leaves. The merkle tree format is explained in the "L1MessageMerkle tree" section below.
On L1:
- A sequencer commits the batch on L1, publishing the merkle tree's root with
publishWithdrawalson the L1CommonBridge. - The user submits a withdrawal proof when calling
claimWithdrawalon the L1CommonBridge. The proof can be obtained by callingethrex_getWithdrawalProofin any L2 node, after the batch containing the withdrawal transaction was verified in the L1. - The bridge asserts the proof is valid and wasn't previously claimed.
- The bridge sends the locked funds specified in the
L1Messageto the user.
---
title: User makes an ETH withdrawal
---
sequenceDiagram
box rgb(139, 63, 63) L2
actor L2Alice as Alice
participant CommonBridgeL2
participant L2ToL1Messenger
end
actor Sequencer
box rgb(33,66,99) L1
participant OnChainProposer
participant CommonBridge
actor L1Alice as Alice
end
L2Alice->>CommonBridgeL2: withdraws 42 ETH
CommonBridgeL2->>CommonBridgeL2: burns 42 ETH
CommonBridgeL2->>L2ToL1Messenger: calls sendMessageToL1
L2ToL1Messenger->>L2ToL1Messenger: emits L1Message event
L2ToL1Messenger-->>Sequencer: receives event
Sequencer->>OnChainProposer: publishes batch
OnChainProposer->>CommonBridge: publishes L1 message root
L1Alice->>CommonBridge: submits withdrawal proof
CommonBridge-->>CommonBridge: asserts proof is valid
CommonBridge->>L1Alice: sends 42 ETH
ERC20 withdrawals through the native bridge
This section explains step by step how native ERC20 withdrawals work.
On L2:
-
The user calls
approveon the L2 tokens to allow the bridge to transfer the asset. -
The user sends a transaction calling
withdrawERC20(address _token, address _receiverOnL1, uint256 _value)on theCommonBridgeL2contract. -
The bridge calls
crosschainBurnon the L2 token, burning the amount to be withdrawn by the user. -
The bridge fetches the address of the L1 token by calling
l1Address()on the L2 token contract. -
The bridge calls
sendMessageToL1(bytes32 data)on theL2ToL1Messengercontract, withdatabeing:bytes32 data = keccak256(abi.encodePacked(_token.l1Address(), _token, _receiverOnL1, _value)) -
L2ToL1Messengeremits anL1Messageevent, with the address of the L2 bridge contract anddataas topics, along with a unique message ID.
Off-chain:
- On each L2 node, the L1 watcher extracts
L1Messageevents, generating a merkle tree with the hashed messages as leaves. The merkle tree format is explained in the "L1MessageMerkle tree" section below.
On L1:
- A sequencer commits the batch on L1, publishing the
L1MessagewithpublishWithdrawalson the L1CommonBridge. - The user submits a withdrawal proof when calling
claimWithdrawalERC20on the L1CommonBridge. The proof can be obtained by callingethrex_getWithdrawalProofin any L2 node, after the batch containing the withdrawal transaction was verified in the L1. - The bridge asserts the proof is valid and wasn't previously claimed, and that the locked tokens mapping contains enough balance for the L1 and L2 token pair to cover the transfer.
- The bridge transfers the locked tokens specified in the
L1Messageto the user and discounts the transferred amount from the L1 and L2 token pair in the mapping.
---
title: User makes an ERC20 withdrawal
---
sequenceDiagram
box rgb(139, 63, 63) L2
actor L2Alice as Alice
participant L2Token
participant CommonBridgeL2
participant L2ToL1Messenger
end
actor Sequencer
box rgb(33,66,99) L1
participant OnChainProposer
participant CommonBridge
participant L1Token
actor L1Alice as Alice
end
L2Alice->>L2Token: approves token transfer
L2Alice->>CommonBridgeL2: withdraws 42 of L2Token
CommonBridgeL2->>L2Token: burns the 42 tokens
CommonBridgeL2->>L2ToL1Messenger: calls sendMessageToL1
L2ToL1Messenger->>L2ToL1Messenger: emits L1Message event
L2ToL1Messenger-->>Sequencer: receives event
Sequencer->>OnChainProposer: publishes batch
OnChainProposer->>CommonBridge: publishes L1 message root
L1Alice->>CommonBridge: submits withdrawal proof
CommonBridge->>L1Token: transfers tokens
L1Token-->>L1Alice: sends 42 tokens
Generic L2->L1 messaging
First, we need to understand the generic mechanism behind it:
L1Message
To allow generic L2->L1 messages, a system contract is added which allows sending arbitrary data. This data is emitted as L1Message events, which nodes automatically extract from blocks.
#![allow(unused)] fn main() { struct L1Message { tx_hash: H256, // L2 transaction where it was included from: Address, // Who sent the message in L2 data_hash: H256, // Hashed payload message_id: U256, // Unique message ID } }
L1Message Merkle tree
When sequencers commit a new batch, they include the merkle root of all the L1Messages inside the batch.
That way, L1 contracts can verify some data was sent from a specific L2 sender.
---
title: L1Message Merkle tree
---
flowchart TD
Msg2[L1Message<sub>2</sub>]
Root([Root])
Node1([Node<sub>1</sub>])
Node2([Node<sub>2</sub>])
Root --- Node1
Root --- Node2
subgraph Msg1["L1Message<sub>1</sub>"]
direction LR
txHash1["txHash<sub>1</sub>"]
from1["from<sub>1</sub>"]
dataHash1["hash(data<sub>1</sub>)"]
messageId1["messageId<sub>1</sub>"]
txHash1 --- from1
from1 --- dataHash1
dataHash1 --- messageId1
end
Node1 --- Msg1
Node2 --- Msg2
As shown in the diagram, the leaves of the tree are the hash of each encoded L1Message.
Messages are encoded by packing, in order:
- the transaction hash that generated it in the L2
- the address of the L2 sender
- the hashed data attached to the message
- the unique message ID
Bridging
On the L2 side, for the case of asset bridging, a contract burns some assets. It then sends a message to the L1 containing the details of this operation:
- From: L2 token address that was burnt
- To: L1 token address that will be withdrawn
- Destination: L1 address that can claim the deposit
- Amount: how much was burnt
When the batch is committed on the L1, the OnChainProposer notifies the bridge which saves the message tree root.
Once the batch containing this transaction is verified, the user can claim their funds on the L1.
To do this, they compute a merkle proof for the included batch and call the L1 CommonBridge contract.
This contract then:
- Checks that the batch is verified
- Ensures the withdrawal wasn't already claimed
- Computes the expected leaf
- Validates that the proof leads from the leaf to the root of the message tree
- Gives the funds to the user
- Marks the withdrawal as claimed