Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Withdrawals (ethers)

A fast path to withdraw ETH / ERC-20 from ZKsync (L2) → Ethereum (L1) using the ethers adapter.

Withdrawals are a two-step process:

  1. Initiate on L2.
  2. Finalize on L1 to release funds.

Prerequisites

  • A funded L2 account to initiate the withdrawal.
  • A funded L1 account for finalization.
  • RPC URLs: L1_RPC_URL, L2_RPC_URL.
  • Installed: @dutterbutter/zksync-sdk + ethers.

Parameters (quick reference)

ParamRequiredMeaning
tokenYesETH_ADDRESS or ERC-20 address
amountYesBigInt/wei (e.g. parseEther('0.01'))
toYesL1 recipient address
refundRecipientNoL2 address to receive fee refunds (if applicable)

Fast path (one-shot)

// examples/withdrawals-eth.ts
import { JsonRpcProvider, Wallet, parseEther } from 'ethers';
import { createEthersClient, createEthersSdk } from '@dutterbutter/zksync-sdk/ethers';
import { ETH_ADDRESS } from '@dutterbutter/zksync-sdk/core';

const L1_RPC = 'http://localhost:8545'; // e.g. https://sepolia.infura.io/v3/XXX
const L2_RPC = 'http://localhost:3050'; // your L2 RPC
const PRIVATE_KEY = process.env.PRIVATE_KEY || '';

async function main() {
  const l1 = new JsonRpcProvider(L1_RPC);
  const l2 = new JsonRpcProvider(L2_RPC);
  const signer = new Wallet(PRIVATE_KEY, l1);

  const client = createEthersClient({ l1, l2, signer });
  const sdk = createEthersSdk(client);

  const me = (await signer.getAddress());

  // Withdraw params (ETH)
  const params = {
    token: ETH_ADDRESS,
    amount: parseEther('0.01'), // 0.001 ETH
    to: me,
    // l2GasLimit: 300_000n,
  } as const;

  // Quote (dry-run only)
  const quote = await sdk.withdrawals.quote(params);
  console.log('QUOTE: ', quote);

  const prepare = await sdk.withdrawals.prepare(params);
  console.log('PREPARE: ', prepare);

  const created = await sdk.withdrawals.create(params);
  console.log('CREATE:', created);

  // Quick status check
  console.log('STATUS (initial):', await sdk.withdrawals.status(created.l2TxHash));

  // wait for L2 inclusion
  const l2Receipt = await sdk.withdrawals.wait(created, { for: 'l2' });
  console.log(
    'L2 included: block=',
    l2Receipt?.blockNumber,
    'status=',
    l2Receipt?.status,
    'hash=',
    l2Receipt?.hash,
  );

  // Optional: check status again
  console.log('STATUS (post-L2):', await sdk.withdrawals.status(created.l2TxHash));

  // finalize on L1
  // Use tryFinalize to avoid throwing in an example script
  await sdk.withdrawals.wait(created.l2TxHash, { for: 'ready' });
  console.log('STATUS (ready):', await sdk.withdrawals.status(created.l2TxHash));

  const fin = await sdk.withdrawals.tryFinalize(created.l2TxHash);
  console.log('TRY FINALIZE: ', fin);

  const l1Receipt = await sdk.withdrawals.wait(created.l2TxHash, { for: 'finalized' });
  if (l1Receipt) {
    console.log('L1 finalize receipt:', l1Receipt.hash);
  } else {
    console.log('Finalized (no local L1 receipt available, possibly finalized by another actor).');
  }
}

main().catch((e) => {
  console.error(e);
  process.exit(1);
});
  • create() prepares and sends the L2 withdrawal.
  • wait(..., { for: 'l2' }) ⇒ included on L2.
  • wait(..., { for: 'ready' }) ⇒ ready for finalization.
  • finalize(l2TxHash) ⇒ required to release funds on L1.

Inspect & customize (quote → prepare → create)

1. Quote (no side-effects)

const quote = await sdk.withdrawals.quote(params);

2. Prepare (build txs, don’t send)

const plan = await sdk.withdrawals.prepare(params);

3. Create (send)

const handle = await sdk.withdrawals.create(params);

Track progress (status vs wait)

Non-blocking snapshot

const s = await sdk.withdrawals.status(handle /* or l2TxHash */);
// 'UNKNOWN' | 'L2_PENDING' | 'PENDING' | 'READY_TO_FINALIZE' | 'FINALIZED'

Block until checkpoint

const l2Receipt = await sdk.withdrawals.wait(handle, { for: 'l2' });
await sdk.withdrawals.wait(handle, { for: 'ready' });

Finalization (required step)

const result = await sdk.withdrawals.finalize(handle.l2TxHash);
console.log('Finalization result:', result);

Error handling patterns

Exceptions

try {
  const handle = await sdk.withdrawals.create(params);
} catch (e) {
  // normalized error envelope (type, operation, message, context, optional revert)
}

No-throw style

Use try* methods to avoid exceptions. They return { ok, value } or { ok, error }. Perfect for UIs or services that prefer explicit flow control.

const r = await sdk.withdrawals.tryCreate(params);

if (!r.ok) {
  console.error('Withdrawal failed:', r.error);
} else {
  const handle = r.value;
  const f = await sdk.withdrawals.tryFinalize(handle.l2TxHash);
  if (!f.ok) {
    console.error('Finalize failed:', f.error);
  } else {
    console.log('Withdrawal finalized on L1:', f.value.receipt?.transactionHash);
  }
}

Troubleshooting

  • Never reaches READY_TO_FINALIZE: proofs may not be available yet.
  • Finalize reverts: ensure enough L1 gas; inspect revert info.
  • Finalized but no receipt: wait(..., { for: 'finalized' }) may return null; retry or rely on finalize() result.

See also