Skip to main content
When withdrawing from ZKsync (L2) back to Ethereum (L1), funds are not automatically released on L1 after your L2 tx is included. Withdrawals are a two-step process:
  1. Initiate on L2 — call withdraw() (via the SDK’s create) to start the withdrawal.
    This burns/locks funds on L2 and emits logs; funds are still unavailable on L1.
  2. Finalize on L1 — call finalize(l2TxHash) to release funds on L1.
    This submits an L1 tx; only then does your ETH or token balance increase on Ethereum.
If you never finalize, your funds remain locked: visible as “ready to withdraw,” but unavailable on L1. Anyone can finalize on your behalf, but you typically do it.

Why finalization matters

  • Funds remain locked until you (or anyone) finalizes.
  • Anyone can finalize — typically the withdrawer does.
  • Finalization costs L1 gas — budget for it.

Finalization methods

All methods accept either a handle (from create) or a raw L2 tx hash. If you only have the hash, you can still finalize.

Phases

Examples

Prefer the no-throw variants in UIs/services that want explicit flow control:
try-finalize.ts

Operational tips

  • Gate UX with phases: Display a Finalize button only when status.phase === 'READY_TO_FINALIZE'.
  • Polling cadence: wait(..., { for: 'ready' }) defaults to ~5500 ms. Adjust with pollMs if needed.
  • Timeouts: Use timeoutMs for long windows and fall back to status(...) to keep the UI responsive.
  • Receipts can be null: wait(..., { for: 'finalized' }) can resolve null if finalized but receipt isn’t retrievable; consider showing a link to the L1 explorer based on the tx hash you submitted.

Common errors

  • RPC/network hiccups: thrown ZKsyncError with kind RPC. Retry with backoff.
  • Internal decode issues: thrown ZKsyncError with kind INTERNAL. Capture logs and report.

See also