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:
- Initiate on L2.
- 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)
Param | Required | Meaning |
---|---|---|
token | Yes | ETH_ADDRESS or ERC-20 address |
amount | Yes | BigInt/wei (e.g. parseEther('0.01') ) |
to | Yes | L1 recipient address |
refundRecipient | No | L2 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 returnnull
; retry or rely onfinalize()
result.