Skip to content

Commit 5814c80

Browse files
committed
fix: withdrawals and finalization
1 parent 70afd9b commit 5814c80

8 files changed

Lines changed: 91 additions & 234 deletions

File tree

composables/zksync/useFee.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1+
import { createEthersClient, createEthersSdk } from "@dutterbutter/zksync-sdk/ethers";
12
import { estimateGas } from "@wagmi/core";
23
import { AbiCoder } from "ethers";
3-
import { encodeFunctionData } from "viem";
4+
import { encodeFunctionData, type Address } from "viem";
45

56
import { wagmiConfig } from "@/data/wagmi";
67

78
import type { Token, TokenAmount } from "@/types";
89
import type { BigNumberish, ethers } from "ethers";
910
import type { Provider } from "zksync-ethers";
10-
import type { Address } from "zksync-ethers/build/types";
1111

1212
export type FeeEstimationParams = {
1313
type: "transfer" | "withdrawal";
14-
from: string;
15-
to: string;
16-
tokenAddress: string;
14+
from: Address;
15+
to: Address;
16+
tokenAddress: Address;
1717
isNativeToken: boolean | null;
1818
assetId?: string | null;
1919
amount: string;
@@ -25,6 +25,8 @@ export default (
2525
tokens: Ref<{ [tokenSymbol: string]: Token } | undefined>,
2626
balances: Ref<TokenAmount[]>
2727
) => {
28+
const { getL1VoidSigner } = useZkSyncWalletStore();
29+
2830
let params: FeeEstimationParams | undefined;
2931

3032
const gasLimit = ref<bigint | undefined>();
@@ -115,15 +117,15 @@ export default (
115117

116118
const [price, limit] = await Promise.all([
117119
retry(() => provider.getGasPrice()),
118-
retry(() => {
120+
retry(async () => {
119121
const isCustomBridgeToken = !!token?.l2BridgeAddress;
120122
if (isCustomBridgeToken) {
121123
return getCustomGasLimit({
122124
from: params!.from,
123125
to: params!.to,
124126
token: params!.tokenAddress,
125127
amount: tokenBalance,
126-
bridgeAddress: token?.l2BridgeAddress,
128+
bridgeAddress: token?.l2BridgeAddress as Address,
127129
});
128130
} else if (params!.isNativeToken && params!.assetId) {
129131
const assetData = AbiCoder.defaultAbiCoder().encode(
@@ -152,13 +154,24 @@ export default (
152154
args: [params!.assetId, assetData],
153155
}),
154156
});
155-
} else {
156-
return provider[params!.type === "transfer" ? "estimateGasTransfer" : "estimateGasWithdraw"]({
157+
} else if (params!.type === "transfer") {
158+
return provider.estimateGasTransfer({
157159
from: params!.from,
158160
to: params!.to,
159161
token: params!.tokenAddress,
160162
amount: tokenBalance,
161163
});
164+
} else {
165+
const signer = await getL1VoidSigner(true);
166+
const client = createEthersClient({ l1: signer.provider, l2: signer.providerL2, signer });
167+
const sdk = createEthersSdk(client);
168+
169+
const quote = await sdk.withdrawals.quote({
170+
to: params!.to,
171+
token: params!.tokenAddress,
172+
amount: 1n, // TODO: estimation fails if we pass actual user balance
173+
});
174+
return quote.fees.gasLimit;
162175
}
163176
}),
164177
]);

composables/zksync/useWithdrawalFinalization.ts

Lines changed: 19 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,22 @@
1-
import { useMemoize } from "@vueuse/core";
2-
import { Wallet, typechain } from "zksync-ethers";
3-
import IL1Nullifier from "zksync-ethers/abi/IL1Nullifier.json";
4-
5-
import { L1_BRIDGE_ABI } from "@/data/abis/l1BridgeAbi";
6-
import { customBridgeTokens } from "@/data/customBridgeTokens";
1+
import { createEthersClient, createEthersSdk } from "@dutterbutter/zksync-sdk/ethers";
72

83
import { useSentryLogger } from "../useSentryLogger";
94

10-
import type { Hash } from "@/types";
11-
import type { Address } from "viem";
12-
import type { FinalizeWithdrawalParams } from "zksync-ethers/build/types";
5+
import type { Hash } from "viem";
136

147
export default (transactionInfo: ComputedRef<TransactionInfo>) => {
158
const status = ref<"not-started" | "processing" | "waiting-for-signature" | "sending" | "done">("not-started");
169
const error = ref<Error | undefined>();
1710
const transactionHash = ref<Hash | undefined>();
1811
const onboardStore = useOnboardStore();
19-
const providerStore = useZkSyncProviderStore();
2012
const walletStore = useZkSyncWalletStore();
2113
const tokensStore = useZkSyncTokensStore();
2214
const { isCorrectNetworkSet } = storeToRefs(onboardStore);
2315
const { ethToken } = storeToRefs(tokensStore);
2416
const { captureException } = useSentryLogger();
2517

26-
const retrieveBridgeAddresses = useMemoize(() =>
27-
providerStore.requestProvider().then((provider) => provider.getDefaultBridgeAddresses())
28-
);
29-
const retrieveL1NullifierAddress = useMemoize(async () => {
30-
const providerL1 = await walletStore.getL1VoidSigner();
31-
return await typechain.IL1AssetRouter__factory.connect(
32-
(
33-
await retrieveBridgeAddresses()
34-
).sharedL1,
35-
providerL1
36-
).L1_NULLIFIER();
37-
});
38-
3918
const gasLimit = ref<bigint | undefined>();
4019
const gasPrice = ref<bigint | undefined>();
41-
const finalizeWithdrawalParams = ref<FinalizeWithdrawalParams | undefined>();
4220

4321
const totalFee = computed(() => {
4422
if (!gasLimit.value || !gasPrice.value) return undefined;
@@ -48,79 +26,6 @@ export default (transactionInfo: ComputedRef<TransactionInfo>) => {
4826
return ethToken.value;
4927
});
5028

51-
const getFinalizationParams = async () => {
52-
const provider = await providerStore.requestProvider();
53-
const wallet = new Wallet(
54-
// random private key cause we don't care about actual signer
55-
// finalizeWithdrawalParams method only exists on Wallet class
56-
"0x7726827caac94a7f9e1b160f7ea819f172f7b6f9d2a97f992c38edeab82d4110",
57-
provider
58-
);
59-
return await wallet.getFinalizeWithdrawalParams(transactionInfo.value.transactionHash);
60-
};
61-
62-
const getTransactionParams = async () => {
63-
finalizeWithdrawalParams.value = await getFinalizationParams();
64-
const provider = await providerStore.requestProvider();
65-
const chainId = BigInt(await provider.getNetwork().then((n) => n.chainId));
66-
const p = finalizeWithdrawalParams.value!;
67-
68-
// Check if this is a custom bridge withdrawal
69-
// First check if the token already has the bridge address stored
70-
let l1BridgeAddress = transactionInfo.value.token.l1BridgeAddress;
71-
72-
// If not, look it up from the custom bridge tokens configuration
73-
if (!l1BridgeAddress) {
74-
const { eraNetwork } = storeToRefs(providerStore);
75-
76-
const customBridgeToken = customBridgeTokens.find(
77-
(token) =>
78-
token.l2Address.toLowerCase() === transactionInfo.value.token.address.toLowerCase() &&
79-
token.chainId === eraNetwork.value.l1Network?.id
80-
);
81-
82-
l1BridgeAddress = customBridgeToken?.l1BridgeAddress;
83-
}
84-
85-
const isCustomBridge = !!l1BridgeAddress;
86-
87-
if (isCustomBridge) {
88-
// Use custom bridge finalization
89-
return {
90-
address: l1BridgeAddress as Address,
91-
abi: L1_BRIDGE_ABI,
92-
account: onboardStore.account.address!,
93-
functionName: "finalizeWithdrawal",
94-
args: [
95-
BigInt(p.l1BatchNumber ?? 0n),
96-
BigInt(p.l2MessageIndex),
97-
Number(p.l2TxNumberInBlock) as number,
98-
p.message as Hash,
99-
p.proof as Hash[],
100-
],
101-
} as const;
102-
} else {
103-
// Use standard bridge finalization through L1Nullifier
104-
const finalizeDepositParams = {
105-
chainId: BigInt(chainId),
106-
l2BatchNumber: BigInt(p.l1BatchNumber ?? 0n),
107-
l2MessageIndex: BigInt(p.l2MessageIndex),
108-
l2Sender: p.sender as Address,
109-
l2TxNumberInBatch: Number(p.l2TxNumberInBlock),
110-
message: p.message as Hash,
111-
merkleProof: p.proof as Hash[],
112-
};
113-
114-
return {
115-
address: (await retrieveL1NullifierAddress()) as Hash,
116-
abi: IL1Nullifier,
117-
account: onboardStore.account.address!,
118-
functionName: "finalizeDeposit",
119-
args: [finalizeDepositParams],
120-
} as const;
121-
}
122-
};
123-
12429
const {
12530
inProgress: estimationInProgress,
12631
error: estimationError,
@@ -130,19 +35,23 @@ export default (transactionInfo: ComputedRef<TransactionInfo>) => {
13035
tokensStore.requestTokens();
13136
const publicClient = onboardStore.getPublicClient();
13237

133-
const transactionParams = await getTransactionParams();
13438
const [price, limit] = await Promise.all([
13539
retry(async () => BigInt((await publicClient.getGasPrice()).toString())),
13640
retry(async () => {
137-
return BigInt((await publicClient.estimateContractGas(transactionParams as any)).toString());
41+
const signer = await walletStore.getL1VoidSigner(true);
42+
const client = createEthersClient({ l1: signer.provider, l2: signer.providerL2, signer });
43+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
44+
const sdk = createEthersSdk(client);
45+
// const estimation = await sdk.withdrawals.finalize
46+
// TODO: finish finalization gas estimation when available in SDK
47+
return BigInt(200_000_000); // Placeholder value
13848
}),
13949
]);
14050

14151
gasPrice.value = price;
14252
gasLimit.value = limit;
14353

14454
return {
145-
transactionParams,
14655
gasPrice: gasPrice.value,
14756
gasLimit: gasLimit.value,
14857
};
@@ -158,14 +67,15 @@ export default (transactionInfo: ComputedRef<TransactionInfo>) => {
15867
if (!isCorrectNetworkSet.value) {
15968
await onboardStore.setCorrectNetwork();
16069
}
161-
const wallet = await onboardStore.getWallet();
162-
const { transactionParams, gasLimit, gasPrice } = (await estimateFee())!;
16370
status.value = "waiting-for-signature";
164-
transactionHash.value = await wallet.writeContract({
165-
...(transactionParams as any),
166-
gasPrice: BigInt(gasPrice.toString()),
167-
gas: BigInt(gasLimit.toString()),
168-
});
71+
const signer = (await walletStore.getL1Signer())!;
72+
const client = createEthersClient({ l1: signer.provider, l2: signer.providerL2, signer });
73+
const sdk = createEthersSdk(client);
74+
const transaction = await sdk.withdrawals.finalize(transactionInfo.value!.transactionHash as Hash);
75+
if (!transaction.receipt) {
76+
throw new Error("Finalization transaction failed");
77+
}
78+
transactionHash.value = transaction.receipt?.hash as Hash;
16979

17080
status.value = "sending";
17181
const receipt = await retry(() =>
@@ -186,6 +96,8 @@ export default (transactionInfo: ComputedRef<TransactionInfo>) => {
18696
status.value = "done";
18797
return receipt;
18898
} catch (err) {
99+
// eslint-disable-next-line no-console
100+
console.error(err);
189101
error.value = formatError(err as Error);
190102
status.value = "not-started";
191103
captureException({

0 commit comments

Comments
 (0)