diff --git a/package.json b/package.json index aa064cf90..e302a748e 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "redis4": "npm:redis@^4.1.0", "superstruct": "^1.0.3", "ts-node": "^10.9.1", - "viem": "^2.21.18", + "viem": "^2.21.37", "winston": "^3.10.0", "zksync-ethers": "^5.7.2" }, diff --git a/src/common/abi/OpStackPortalL1.json b/src/common/abi/OpStackPortalL1.json new file mode 100644 index 000000000..8da7b7e8c --- /dev/null +++ b/src/common/abi/OpStackPortalL1.json @@ -0,0 +1,172 @@ +[ + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "address", + "name": "_proofSubmitter", + "type": "address" + } + ], + "name": "finalizeWithdrawalTransactionExternalProof", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { "internalType": "uint256", "name": "nonce", "type": "uint256" }, + { "internalType": "address", "name": "sender", "type": "address" }, + { "internalType": "address", "name": "target", "type": "address" }, + { "internalType": "uint256", "name": "value", "type": "uint256" }, + { "internalType": "uint256", "name": "gasLimit", "type": "uint256" }, + { "internalType": "bytes", "name": "data", "type": "bytes" } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + } + ], + "name": "finalizeWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "gasLimit", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "internalType": "struct Types.WithdrawalTransaction", + "name": "_tx", + "type": "tuple" + }, + { + "internalType": "uint256", + "name": "_disputeGameIndex", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "bytes32", + "name": "version", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "stateRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "messagePasserStorageRoot", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "latestBlockhash", + "type": "bytes32" + } + ], + "internalType": "struct Types.OutputRootProof", + "name": "_outputRootProof", + "type": "tuple" + }, + { + "internalType": "bytes[]", + "name": "_withdrawalProof", + "type": "bytes[]" + } + ], + "name": "proveWithdrawalTransaction", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [{ "internalType": "bytes32", "name": "_withdrawalHash", "type": "bytes32" }], + "name": "numProofSubmitters", + "outputs": [{ "internalType": "uint256", "name": "", "type": "uint256" }], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { "internalType": "bytes32", "name": "", "type": "bytes32" }, + { "internalType": "uint256", "name": "", "type": "uint256" } + ], + "name": "proofSubmitters", + "outputs": [{ "internalType": "address", "name": "", "type": "address" }], + "stateMutability": "view", + "type": "function" + } +] diff --git a/src/finalizer/utils/opStack.ts b/src/finalizer/utils/opStack.ts index 288cf659a..ea84dc115 100644 --- a/src/finalizer/utils/opStack.ts +++ b/src/finalizer/utils/opStack.ts @@ -1,6 +1,16 @@ import assert from "assert"; -import { groupBy } from "lodash"; +import { groupBy, countBy } from "lodash"; import * as optimismSDK from "@eth-optimism/sdk"; +import * as viem from "viem"; +import * as viemChains from "viem/chains"; +import { + publicActionsL1, + publicActionsL2, + getWithdrawals, + GetWithdrawalStatusReturnType, + PublicActionsL1, + PublicActionsL2, +} from "viem/op-stack"; import { HubPoolClient, SpokePoolClient } from "../../clients"; import { TokensBridged } from "../../interfaces"; import { @@ -23,9 +33,11 @@ import { Contract, ethers, Multicall2Call, + mapAsync, paginatedEventQuery, } from "../../utils"; import { CONTRACT_ADDRESSES, OPSTACK_CONTRACT_OVERRIDES } from "../../common"; +import OPStackPortalL1 from "../../common/abi/OpStackPortalL1.json"; import { FinalizerPromise, CrossChainMessage } from "../types"; const { utils } = ethers; @@ -44,6 +56,23 @@ const OP_STACK_CHAINS = Object.values(CHAIN_IDs).filter((chainId) => chainIsOPSt * (typeof OP_STACK_CHAINS)[number] then takes all elements in this array and "unions" their type (i.e. 10 | 8453 | 3443 | ... ). * https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#keyof-and-lookup-types */ + +// We might want to export this mapping of chain ID to viem chain object out of a constant +// file once we start using Viem elsewhere in the repo: +const VIEM_OP_STACK_CHAINS = { + [CHAIN_IDs.OPTIMISM_SEPOLIA]: viemChains.optimismSepolia, + [CHAIN_IDs.OPTIMISM]: viemChains.optimism, + [CHAIN_IDs.BASE]: viemChains.base, + [CHAIN_IDs.REDSTONE]: viemChains.redstone, + [CHAIN_IDs.LISK]: viemChains.lisk, + [CHAIN_IDs.ZORA]: viemChains.zora, + [CHAIN_IDs.MODE]: viemChains.mode, + // @dev The following chains do not have "portal" contracts listed in the Viem chain definitions. + // They have non-standard interfaces for withdrawing from L2 to L1 + // [CHAIN_IDs.WORLD_CHAIN]: viemChains.worldchain, + // [CHAIN_IDs.BLAST]: viemChains.blast, +}; + type OVM_CHAIN_ID = (typeof OP_STACK_CHAINS)[number]; type OVM_CROSS_CHAIN_MESSENGER = optimismSDK.CrossChainMessenger; @@ -181,32 +210,174 @@ export async function opStackFinalizer( }); } - const proofs = await multicallOptimismL1Proofs( - chainId, - recentTokensBridgedEvents, - crossChainMessenger, - hubPoolClient, - logger - ); + let callData: Multicall2Call[]; + let crossChainTransfers: CrossChainMessage[]; + + // @dev Experimental: try using Viem if its available for this chain. Eventually we should + // fully migrate from SDK to Viem. Note, the Viem "provider" is not easily translateable from the ethers.js provider, + // so any RPC requests sent from the Viem client will likely not inherit benefits of our custom RetryProvider such + // as quorum, caching, fallbacks, etc. This is workable for now if we isolate viem usage. + const viemTxns: { + callData: Multicall2Call[]; + withdrawals: CrossChainMessage[]; + } = { + callData: [], + withdrawals: [], + }; + if (VIEM_OP_STACK_CHAINS[chainId]) { + const hubChainId = chainIsProd(chainId) ? CHAIN_IDs.MAINNET : CHAIN_IDs.SEPOLIA; + // TODO: can't figure out how to get these typecasts to work, despite this code being in the viem.opStack tutorials + const publicClientL1 = viem + .createPublicClient({ + chain: chainIsProd(chainId) ? viemChains.mainnet : viemChains.sepolia, + transport: viem.http(getCachedProvider(hubChainId, true).providers[0].connection.url), + }) + .extend(publicActionsL1() as any) as unknown as viem.PublicClient & PublicActionsL1; + const publicClientL2 = viem + .createPublicClient({ + chain: VIEM_OP_STACK_CHAINS[chainId], + transport: viem.http(getCachedProvider(chainId, true).providers[0].connection.url), + }) + .extend(publicActionsL2() as any) as unknown as viem.PublicClient & PublicActionsL2; + const uniqueTokenhashes = {}; + const logIndexesForMessage = []; + const events = recentTokensBridgedEvents.concat(olderTokensBridgedEvents); + for (const event of events) { + uniqueTokenhashes[event.transactionHash] = uniqueTokenhashes[event.transactionHash] ?? 0; + const logIndex = uniqueTokenhashes[event.transactionHash]; + logIndexesForMessage.push(logIndex); + uniqueTokenhashes[event.transactionHash] += 1; + } - // Next finalize withdrawals that have passed challenge period. - // Skip events that are likely not past the seven day challenge period. - logger.debug({ - at: "Finalizer", - message: `Earliest TokensBridged block to attempt to finalize for ${networkName}`, - earliestBlockToFinalize: latestBlockToProve, - }); + const crossChainMessenger = new Contract( + VIEM_OP_STACK_CHAINS[chainId].contracts.portal[hubChainId].address, + OPStackPortalL1, + signer + ); - const finalizations = await multicallOptimismFinalizations( - chainId, - olderTokensBridgedEvents, - crossChainMessenger, - hubPoolClient, - logger - ); + const withdrawalStatuses: string[] = []; + await mapAsync(events, async (event, i) => { + // Useful information for event: + const l1TokenInfo = getL1TokenInfo(event.l2TokenAddress, chainId); + const amountFromWei = convertFromWei(event.amountToReturn.toString(), l1TokenInfo.decimals); + + const receipt = await publicClientL2.getTransactionReceipt({ + hash: event.transactionHash as `0x${string}`, + }); + const withdrawal = getWithdrawals(receipt)[logIndexesForMessage[i]]; + const withdrawalStatus: GetWithdrawalStatusReturnType = await publicClientL1.getWithdrawalStatus({ + receipt, + chain: undefined, + targetChain: VIEM_OP_STACK_CHAINS[chainId], + logIndex: logIndexesForMessage[i], + }); + withdrawalStatuses.push(withdrawalStatus); + if (withdrawalStatus === "ready-to-prove") { + const l2Output = await publicClientL1.getL2Output({ + chain: undefined, + l2BlockNumber: BigInt(event.blockNumber), + targetChain: VIEM_OP_STACK_CHAINS[chainId], + }); + const { l2OutputIndex, outputRootProof, withdrawalProof } = await publicClientL2.buildProveWithdrawal({ + chain: undefined, + withdrawal, + output: l2Output, + }); + const proofArgs = [withdrawal, l2OutputIndex, outputRootProof, withdrawalProof]; + const callData = await crossChainMessenger.populateTransaction.proveWithdrawalTransaction(...proofArgs); + viemTxns.callData.push({ + callData: callData.data, + target: crossChainMessenger.address, + }); + viemTxns.withdrawals.push({ + originationChainId: chainId, + l1TokenSymbol: l1TokenInfo.symbol, + amount: amountFromWei, + type: "misc", + miscReason: "proof", + destinationChainId: hubPoolClient.chainId, + }); + } else if (withdrawalStatus === "waiting-to-finalize") { + const { seconds } = await publicClientL1.getTimeToFinalize({ + chain: undefined, + withdrawalHash: withdrawal.withdrawalHash, + targetChain: VIEM_OP_STACK_CHAINS[chainId], + }); + logger.debug({ + at: `${getNetworkName(chainId)}Finalizer`, + message: `Withdrawal ${event.transactionHash} for ${amountFromWei} of ${ + l1TokenInfo.symbol + } is in challenge period for ${seconds / 60 / 60} hours`, + }); + } else if (withdrawalStatus === "ready-to-finalize") { + // @dev Some OpStack chains use OptimismPortal instead of the newer OptimismPortal2, the latter of which + // requires that the msg.sender of the finalizeWithdrawalTransaction is equal to the address that + // submitted the proof. We try-catch both calls to handle this. + // See this comment in OptimismPortal2 for more context on why the new portal requires checking the + // proof submitter address: https://github.com/ethereum-optimism/optimism/blob/d6bda0339005d98c992c749c137938d515755029/packages/contracts-bedrock/src/L1/OptimismPortal2.sol#L132 + let callData: ethers.PopulatedTransaction; + try { + // Calling OptimismPortal2: https://github.com/ethereum-optimism/optimism/blob/d6bda0339005d98c992c749c137938d515755029/packages/contracts-bedrock/src/L1/OptimismPortal2.sol + const numProofSubmitters = await crossChainMessenger.numProofSubmitters(withdrawal.withdrawalHash); + const proofSubmitter = await crossChainMessenger.proofSubmitters( + withdrawal.withdrawalHash, + numProofSubmitters - 1 + ); + callData = await crossChainMessenger.populateTransaction.finalizeWithdrawalTransactionExternalProof( + withdrawal, + proofSubmitter + ); + } catch (e) { + // Calling OptimismPortal: https://github.com/ethereum-optimism/optimism/blob/d6bda0339005d98c992c749c137938d515755029/packages/contracts-bedrock/src/L1/OptimismPortal.sol + callData = await crossChainMessenger.populateTransaction.finalizeWithdrawalTransaction(withdrawal); + } + viemTxns.callData.push({ + callData: callData.data, + target: crossChainMessenger.address, + }); + viemTxns.withdrawals.push({ + originationChainId: chainId, + l1TokenSymbol: l1TokenInfo.symbol, + amount: amountFromWei, + type: "withdrawal", + destinationChainId: hubPoolClient.chainId, + }); + } + }); + logger.debug({ + at: `${getNetworkName(chainId)}Finalizer`, + message: `${getNetworkName(chainId)} message statuses`, + statusesGrouped: countBy(withdrawalStatuses), + }); + callData = viemTxns.callData; + crossChainTransfers = viemTxns.withdrawals; + } else { + const proofs = await multicallOptimismL1Proofs( + chainId, + recentTokensBridgedEvents, + crossChainMessenger, + hubPoolClient, + logger + ); - const callData = [...proofs.callData, ...finalizations.callData]; - const crossChainTransfers = [...proofs.withdrawals, ...finalizations.withdrawals]; + // Next finalize withdrawals that have passed challenge period. + // Skip events that are likely not past the seven day challenge period. + logger.debug({ + at: "Finalizer", + message: `Earliest TokensBridged block to attempt to finalize for ${networkName}`, + earliestBlockToFinalize: latestBlockToProve, + }); + + const finalizations = await multicallOptimismFinalizations( + chainId, + olderTokensBridgedEvents, + crossChainMessenger, + hubPoolClient, + logger + ); + callData = [...proofs.callData, ...finalizations.callData]; + crossChainTransfers = [...proofs.withdrawals, ...finalizations.withdrawals]; + } return { callData, crossChainMessages: crossChainTransfers }; } diff --git a/yarn.lock b/yarn.lock index c2e2d330e..fe0b93c7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1566,6 +1566,13 @@ dependencies: "@noble/hashes" "1.5.0" +"@noble/curves@1.7.0", "@noble/curves@~1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== + dependencies: + "@noble/hashes" "1.6.0" + "@noble/hashes@1.0.0", "@noble/hashes@~1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.0.0.tgz#d5e38bfbdaba174805a4e649f13be9a9ed3351ae" @@ -1576,6 +1583,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + +"@noble/hashes@1.6.1", "@noble/hashes@~1.6.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" + integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== + "@noble/secp256k1@1.5.5", "@noble/secp256k1@~1.5.2": version "1.5.5" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.5.5.tgz#315ab5745509d1a8c8e90d0bdf59823ccf9bcfc3" @@ -2451,6 +2468,11 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.8.tgz#8f23646c352f020c83bca750a82789e246d42b50" integrity sha512-6CyAclxj3Nb0XT7GHK6K4zK6k2xJm6E4Ft0Ohjt4WgegiFUHEtFb2CGzmPmGBwoIhrLsqNLYfLr04Y1GePrzZg== +"@scure/base@~1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" + integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== + "@scure/bip32@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.0.1.tgz#1409bdf9f07f0aec99006bb0d5827693418d3aa5" @@ -2469,6 +2491,15 @@ "@noble/hashes" "~1.5.0" "@scure/base" "~1.1.7" +"@scure/bip32@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.0.tgz#6dbc6b4af7c9101b351f41231a879d8da47e0891" + integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== + dependencies: + "@noble/curves" "~1.7.0" + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" + "@scure/bip39@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.0.0.tgz#47504e58de9a56a4bbed95159d2d6829fa491bb0" @@ -2485,6 +2516,14 @@ "@noble/hashes" "~1.5.0" "@scure/base" "~1.1.8" +"@scure/bip39@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.0.tgz#c8f9533dbd787641b047984356531d84485f19be" + integrity sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A== + dependencies: + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" + "@sentry/core@5.30.0": version "5.30.0" resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.30.0.tgz#6b203664f69e75106ee8b5a2fe1d717379b331f3" @@ -3771,6 +3810,11 @@ abitype@1.0.6, abitype@^1.0.6: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== +abitype@1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.7.tgz#876a0005d211e1c9132825d45bcee7b46416b284" + integrity sha512-ZfYYSktDQUwc2eduYu8C4wOs+RDPmnRYMh7zNfzeMtGGgb0U+6tLGjixUic6mXf5xKKCcgT5Qp6cv39tOARVFw== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -15278,24 +15322,24 @@ verror@1.10.0: extsprintf "^1.2.0" viem@^2.21.15: - version "2.21.48" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.48.tgz#f8f1d0bf5381282e22e6a1f8b72ebd6e64426480" - integrity sha512-/hBHyG1gdIIuiQv0z9YmzXl5eWJa0UCZGwkeuQzH2Bmg6FIEwZeEcxgiytXZydip+p2wMBFa1jdr7o5O1+mrIg== - dependencies: - "@noble/curves" "1.6.0" - "@noble/hashes" "1.5.0" - "@scure/bip32" "1.5.0" - "@scure/bip39" "1.4.0" - abitype "1.0.6" + version "2.21.55" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.55.tgz#a57ad31fcf2a0f6c011b1909f02c94421ec4f781" + integrity sha512-PgXew7C11cAuEtOSgRyQx2kJxEOPUwIwZA9dMglRByqJuFVA7wSGZZOOo/93iylAA8E15bEdqy9xulU3oKZ70Q== + dependencies: + "@noble/curves" "1.7.0" + "@noble/hashes" "1.6.1" + "@scure/bip32" "1.6.0" + "@scure/bip39" "1.5.0" + abitype "1.0.7" isows "1.0.6" ox "0.1.2" webauthn-p256 "0.0.10" ws "8.18.0" -viem@^2.21.18: - version "2.21.21" - resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.21.tgz#11a5001fa18c8a47548a4b20ae9ddd8cfb14de3f" - integrity sha512-KJPqpAXy8kyZQICx1nURUXqd8aABP9RweAZhfp27MzMPsAAxP450cWPlEffEAUrvsyyj5edVbIcHESE8DYVzFA== +viem@^2.21.37: + version "2.21.37" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.37.tgz#4d67bee8749321b0fd142c54df2021d5774403b1" + integrity sha512-JupwyttT4aJNnP9+kD7E8jorMS5VmgpC3hm3rl5zXsO8WNBTsP3JJqZUSg4AG6s2lTrmmpzS/qpmXMZu5gJw5Q== dependencies: "@adraffy/ens-normalize" "1.11.0" "@noble/curves" "1.6.0"