- Initiate on L2 — call
withdraw()(via the SDK’screate) to start the withdrawal.
This burns/locks funds on L2 and emits logs; funds are still unavailable on L1. - 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.
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
| Method | Purpose | Returns |
|---|---|---|
withdrawals.status(h | l2TxHash) | Snapshot phase (UNKNOWN → FINALIZED) | WithdrawalStatus |
withdrawals.wait(h | l2TxHash, { for }) | Block until a checkpoint ('l2' | 'ready' | 'finalized') | Receipt or null |
withdrawals.finalize(l2TxHash) | Send the L1 finalize tx | { status, receipt } |
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
| Phase | Meaning |
|---|---|
UNKNOWN | Handle doesn’t contain an L2 hash yet. |
L2_PENDING | L2 tx not yet included. |
PENDING | L2 included, but not yet ready to finalize on L1. |
READY_TO_FINALIZE | Finalization on L1 would succeed now. |
FINALIZED | Finalized on L1; funds released. |
Examples
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 withpollMsif needed. - Timeouts: Use
timeoutMsfor long windows and fall back tostatus(...)to keep the UI responsive. - Receipts can be
null:wait(..., { for: 'finalized' })can resolvenullif 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
ZKsyncErrorwith kindRPC. Retry with backoff. - Internal decode issues: thrown
ZKsyncErrorwith kindINTERNAL. Capture logs and report.