Guide: Withdrawing funds L2 to L1
This guide provides a complete walkthrough for withdrawing funds from an L2 (ZKsync) back to L1 (Ethereum). We will use the@dutterbutter/zksync-sdk to simplify the process.
The SDK intelligently handles the withdrawal flow, automatically selecting the correct route and approval requirements based on the token being used.
Prerequisites
Before you begin, ensure you have the following:- Node.js v20+ or Bun.sh installed.
- An account with sufficient L2 balance of the asset you want to withdraw and L2 gas for the withdrawal transaction.
- RPC endpoints for both the L2 you’re withdrawing from and the L1 you’re withdrawing to.
-
A project set up with
viemorethersand@dutterbutter/zksync-sdkinstalled. -
An
.envfile in your project’s root directory with the following variables:
The Withdrawal Process: Step-by-Step
Withdrawals generally follow two phases:- An L2 transaction that burns/transfers the token and emits the L2→L1 message.
- A finalization on L1 (after the message is ready) to release funds on L1.
The SDK exposes
wait(..., { for: 'ready' }),tryFinalize(...), andwait(..., { for: 'finalized' }).
Step 0: Setup and SDK Initialization
Connect to L1 and L2 using your preferred library (viem or ethers) and initialize the ZKsync SDK.
Step 1: Define Withdrawal Parameters
Next, define the parameters for your withdrawal in an object. You’ll specify the amount, the L2 token address (orETH_ADDRESS for native ETH on ETH-based L2s), and the recipient’s address on L1.
Advanced Parameters (Optional)
Advanced Parameters (Optional)
You can also specify advanced options for finer control over the transaction:
l2GasLimit: The gas limit for the L2 withdrawal transaction.operatorTip: A tip for the L2 operator (if supported).refundRecipient: An L1 address to receive any L1 finalize-phase refunds (chain-specific).
Step 2: Quote the Withdrawal
Before sending, get an estimate withsdk.withdrawals.quote. This returns expected fees and gas for the L2 step and any finalize phase hints.
You can skip straight to
create, but quoting helps avoid surprises.Step 3: Prepare the Transaction
Build the execution plan withsdk.withdrawals.prepare. For non-base ERC-20s or non-ETH L2-ETH, this may include L2 approvals.
Step 4: Create the Withdrawal
Execute the plan withsdk.withdrawals.create. You’ll get a handle you can use to track the withdrawal across phases.
Step 5: Track the Withdrawal Lifecycle
Usestatus and wait to track all phases:
wait(handle, { for: 'l2' })→ L2 inclusionwait(handle, { for: 'ready' })→ message proven/ready on L1tryFinalize(l2TxHash)→ submit finalize on L1 (no-op if already finalized)wait(l2TxHash, { for: 'finalized' })→ finalized on L1
finalized phase completes, your funds are available on L1 at the to address.
Full Code Example
Here is the complete, runnable script for your reference.Click to view the full script.
Click to view the full script.