|
| 1 | +import { |
| 2 | + TransactionReceipt, |
| 3 | + createPublicClient, |
| 4 | + encodeFunctionData, |
| 5 | + http, |
| 6 | + erc20Abi, |
| 7 | + PublicClient, |
| 8 | + fallback, |
| 9 | + parseEventLogs, |
| 10 | + parseAbi, |
| 11 | +} from 'viem'; |
| 12 | +import { BridgeAdapter, MemoizedTransactionRequest, RebalanceTransactionMemo } from '../../types'; |
| 13 | +import { SupportedBridge, ChainConfiguration, ILogger } from '@mark/core'; |
| 14 | +import { jsonifyError } from '@mark/logger'; |
| 15 | +import type { RebalanceRoute } from '@mark/core'; |
| 16 | + |
| 17 | +const ZKSYNC_L1_BRIDGE = '0x57891966931eb4bb6fb81430e6ce0a03aabde063'; |
| 18 | +const ZKSYNC_L2_BRIDGE = '0x11f943b2c77b743AB90f4A0Ae7d5A4e7FCA3E102'; |
| 19 | +const ETH_TOKEN_L2 = '0x000000000000000000000000000000000000800A'; |
| 20 | +const WITHDRAWAL_DELAY_HOURS = 24; |
| 21 | + |
| 22 | +const zkSyncL1BridgeAbi = parseAbi([ |
| 23 | + 'function deposit(address _l2Receiver, address _l1Token, uint256 _amount, uint256 _l2TxGasLimit, uint256 _l2TxGasPerPubdataByte, address _refundRecipient) payable', |
| 24 | + 'function finalizeWithdrawal(uint256 _l2BatchNumber, uint256 _l2MessageIndex, uint16 _l2TxNumberInBatch, bytes calldata _message, bytes32[] calldata _merkleProof)', |
| 25 | + 'event DepositInitiated(bytes32 indexed l2DepositTxHash, address indexed from, address indexed to, address l1Token, uint256 amount)', |
| 26 | +]); |
| 27 | + |
| 28 | +const zkSyncL2BridgeAbi = parseAbi([ |
| 29 | + 'function withdraw(address _l1Receiver, address _l2Token, uint256 _amount)', |
| 30 | + 'event WithdrawalInitiated(address indexed l2Sender, address indexed l1Receiver, address indexed l2Token, uint256 amount)', |
| 31 | +]); |
| 32 | + |
| 33 | + |
| 34 | +export class ZKSyncNativeBridgeAdapter implements BridgeAdapter { |
| 35 | + constructor( |
| 36 | + protected readonly chains: Record<string, ChainConfiguration>, |
| 37 | + protected readonly logger: ILogger, |
| 38 | + ) {} |
| 39 | + |
| 40 | + type(): SupportedBridge { |
| 41 | + return SupportedBridge.Zksync; |
| 42 | + } |
| 43 | + |
| 44 | + // https://docs.zksync.io/zk-stack/concepts/fee-mechanism |
| 45 | + async getReceivedAmount(amount: string, route: RebalanceRoute): Promise<string> { |
| 46 | + try { |
| 47 | + return amount; |
| 48 | + } catch (error) { |
| 49 | + this.handleError(error, 'calculate received amount', { amount, route }); |
| 50 | + } |
| 51 | + } |
| 52 | + |
| 53 | + async send( |
| 54 | + sender: string, |
| 55 | + recipient: string, |
| 56 | + amount: string, |
| 57 | + route: RebalanceRoute, |
| 58 | + ): Promise<MemoizedTransactionRequest[]> { |
| 59 | + try { |
| 60 | + const isL1ToL2 = route.origin === 1 && route.destination === 324; |
| 61 | + const isETH = route.asset.toLowerCase() === '0x0000000000000000000000000000000000000000'; |
| 62 | + const transactions: MemoizedTransactionRequest[] = []; |
| 63 | + |
| 64 | + if (isL1ToL2) { |
| 65 | + if (!isETH) { |
| 66 | + const client = await this.getClient(route.origin); |
| 67 | + const allowance = await client.readContract({ |
| 68 | + address: route.asset as `0x${string}`, |
| 69 | + abi: erc20Abi, |
| 70 | + functionName: 'allowance', |
| 71 | + args: [sender as `0x${string}`, ZKSYNC_L1_BRIDGE as `0x${string}`], |
| 72 | + }); |
| 73 | + |
| 74 | + if (allowance < BigInt(amount)) { |
| 75 | + transactions.push({ |
| 76 | + memo: RebalanceTransactionMemo.Approval, |
| 77 | + transaction: { |
| 78 | + to: route.asset as `0x${string}`, |
| 79 | + data: encodeFunctionData({ |
| 80 | + abi: erc20Abi, |
| 81 | + functionName: 'approve', |
| 82 | + args: [ZKSYNC_L1_BRIDGE as `0x${string}`, BigInt(amount)], |
| 83 | + }), |
| 84 | + value: BigInt(0), |
| 85 | + }, |
| 86 | + }); |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + transactions.push({ |
| 91 | + memo: RebalanceTransactionMemo.Rebalance, |
| 92 | + transaction: { |
| 93 | + to: ZKSYNC_L1_BRIDGE as `0x${string}`, |
| 94 | + data: encodeFunctionData({ |
| 95 | + abi: zkSyncL1BridgeAbi, |
| 96 | + functionName: 'deposit', |
| 97 | + args: [ |
| 98 | + recipient as `0x${string}`, |
| 99 | + route.asset as `0x${string}`, |
| 100 | + BigInt(amount), |
| 101 | + BigInt(200000), |
| 102 | + BigInt(800), |
| 103 | + sender as `0x${string}`, |
| 104 | + ], |
| 105 | + }), |
| 106 | + value: isETH ? BigInt(amount) : BigInt(0), |
| 107 | + }, |
| 108 | + }); |
| 109 | + } else { |
| 110 | + transactions.push({ |
| 111 | + memo: RebalanceTransactionMemo.Rebalance, |
| 112 | + transaction: { |
| 113 | + to: ZKSYNC_L2_BRIDGE as `0x${string}`, |
| 114 | + data: encodeFunctionData({ |
| 115 | + abi: zkSyncL2BridgeAbi, |
| 116 | + functionName: 'withdraw', |
| 117 | + args: [ |
| 118 | + recipient as `0x${string}`, |
| 119 | + route.asset === '0x0000000000000000000000000000000000000000' |
| 120 | + ? (ETH_TOKEN_L2 as `0x${string}`) |
| 121 | + : (route.asset as `0x${string}`), |
| 122 | + BigInt(amount), |
| 123 | + ], |
| 124 | + }), |
| 125 | + value: BigInt(0), |
| 126 | + }, |
| 127 | + }); |
| 128 | + } |
| 129 | + |
| 130 | + return transactions; |
| 131 | + } catch (error) { |
| 132 | + this.handleError(error, 'prepare bridge transactions', { sender, recipient, amount, route }); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + async readyOnDestination( |
| 137 | + amount: string, |
| 138 | + route: RebalanceRoute, |
| 139 | + originTransaction: TransactionReceipt, |
| 140 | + ): Promise<boolean> { |
| 141 | + try { |
| 142 | + const isL1ToL2 = route.origin === 1 && route.destination === 324; |
| 143 | + |
| 144 | + if (isL1ToL2) { |
| 145 | + return true; |
| 146 | + } else { |
| 147 | + this.logger.info('zkSync withdrawal delay check - 24-hour delay required', { |
| 148 | + txBlock: Number(originTransaction.blockNumber), |
| 149 | + txHash: originTransaction.transactionHash, |
| 150 | + requiredDelayHours: WITHDRAWAL_DELAY_HOURS, |
| 151 | + }); |
| 152 | + |
| 153 | + return true; |
| 154 | + } |
| 155 | + } catch (error) { |
| 156 | + this.handleError(error, 'check destination readiness', { amount, route, originTransaction }); |
| 157 | + } |
| 158 | + } |
| 159 | + |
| 160 | + async destinationCallback( |
| 161 | + route: RebalanceRoute, |
| 162 | + originTransaction: TransactionReceipt, |
| 163 | + ): Promise<MemoizedTransactionRequest | void> { |
| 164 | + try { |
| 165 | + const isL2ToL1 = route.origin === 324 && route.destination === 1; |
| 166 | + |
| 167 | + if (isL2ToL1) { |
| 168 | + const logs = parseEventLogs({ |
| 169 | + abi: zkSyncL2BridgeAbi, |
| 170 | + logs: originTransaction.logs, |
| 171 | + }); |
| 172 | + |
| 173 | + const withdrawalEvent = logs.find((log) => log.eventName === 'WithdrawalInitiated'); |
| 174 | + if (!withdrawalEvent) { |
| 175 | + this.logger.warn('No WithdrawalInitiated event found in transaction logs'); |
| 176 | + return; |
| 177 | + } |
| 178 | + |
| 179 | + this.logger.info('zkSync withdrawal requires manual finalization after 24-hour delay', { |
| 180 | + withdrawalTxHash: originTransaction.transactionHash, |
| 181 | + blockNumber: originTransaction.blockNumber, |
| 182 | + }); |
| 183 | + |
| 184 | + throw new Error('zkSync withdrawal finalization not yet implemented - requires batch proof integration'); |
| 185 | + } |
| 186 | + } catch (error) { |
| 187 | + this.handleError(error, 'prepare destination callback', { route, originTransaction }); |
| 188 | + } |
| 189 | + } |
| 190 | + |
| 191 | + private async getClient(chainId: number): Promise<PublicClient> { |
| 192 | + const providers = this.chains[chainId.toString()]?.providers ?? []; |
| 193 | + if (providers.length === 0) { |
| 194 | + throw new Error(`No providers configured for chain ${chainId}`); |
| 195 | + } |
| 196 | + |
| 197 | + return createPublicClient({ |
| 198 | + transport: fallback(providers.map((provider: string) => http(provider))), |
| 199 | + }); |
| 200 | + } |
| 201 | + |
| 202 | + private handleError(error: Error | unknown, context: string, metadata: Record<string, unknown>): never { |
| 203 | + this.logger.error(`Failed to ${context}`, { |
| 204 | + error: jsonifyError(error), |
| 205 | + ...metadata, |
| 206 | + }); |
| 207 | + throw new Error(`Failed to ${context}: ${(error as Error)?.message ?? ''}`); |
| 208 | + } |
| 209 | +} |
0 commit comments