Skip to main content

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:
  1. Node.js v20+ or Bun.sh installed.
  2. An account with sufficient L2 balance of the asset you want to withdraw and L2 gas for the withdrawal transaction.
  3. RPC endpoints for both the L2 you’re withdrawing from and the L1 you’re withdrawing to.
  4. A project set up with viem or ethers and @dutterbutter/zksync-sdk installed.
  5. An .env file 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(...), and wait(..., { 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 (or ETH_ADDRESS for native ETH on ETH-based L2s), and the recipient’s address on L1.
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 with sdk.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 with sdk.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 with sdk.withdrawals.create. You’ll get a handle you can use to track the withdrawal across phases.

Step 5: Track the Withdrawal Lifecycle

Use status and wait to track all phases:
  • wait(handle, { for: 'l2' }) → L2 inclusion
  • wait(handle, { for: 'ready' }) → message proven/ready on L1
  • tryFinalize(l2TxHash) → submit finalize on L1 (no-op if already finalized)
  • wait(l2TxHash, { for: 'finalized' }) → finalized on L1
Once the 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.
For more detailed information on the methods used in this guide, see the official API reference: