diff --git a/.changeset/ten-spiders-trade.md b/.changeset/ten-spiders-trade.md new file mode 100644 index 0000000000..91eebf52f7 --- /dev/null +++ b/.changeset/ten-spiders-trade.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': patch +--- + +Added ZKSync signer support using zksync-ethers package diff --git a/solidity/.gitignore b/solidity/.gitignore index 6f01b8e770..1dce03d2e3 100644 --- a/solidity/.gitignore +++ b/solidity/.gitignore @@ -15,3 +15,14 @@ docs flattened/ buildArtifact.json fixtures/ + + +# zksync artifacts +artifacts-zk +cache-zk + +core-utils/zksync/artifacts/output + +.zksolc-libraries-cache/ +typechain-types/ +typechain/ \ No newline at end of file diff --git a/solidity/core-utils/index.ts b/solidity/core-utils/index.ts new file mode 100644 index 0000000000..0b8f5ba554 --- /dev/null +++ b/solidity/core-utils/index.ts @@ -0,0 +1,2 @@ +export * from './zksync/index.js'; +export * from './typechain/index.js'; diff --git a/solidity/core-utils/zksync/index.ts b/solidity/core-utils/zksync/index.ts new file mode 100644 index 0000000000..e6b8e03e9b --- /dev/null +++ b/solidity/core-utils/zksync/index.ts @@ -0,0 +1,96 @@ +import { readFileSync, readdirSync } from 'fs'; +import path, { join } from 'path'; +import { fileURLToPath } from 'url'; + +/** + * @dev Represents a ZkSync artifact. + */ +export type ZKSyncArtifact = { + contractName: string; + sourceName: string; + abi: any; + bytecode: string; + deployedBytecode: string; + factoryDeps?: Record; +}; + +/** + * @dev A mapping of artifact names to their corresponding ZkSync artifacts. + */ +export type ArtifactMap = { + [key: string]: ZKSyncArtifact; +}; + +// Get the resolved path to the current file +const currentFilePath = fileURLToPath(import.meta.url); +const currentDirectory = path.dirname(currentFilePath); + +/** + * @dev Reads artifact files from the specified directory. + * @param directory The directory to read artifact files from. + * @return An array of artifact file names that end with '.json'. + */ +function getArtifactFiles(directory: string): string[] { + return readdirSync(directory).filter((file) => file.endsWith('.json')); +} + +/** + * @dev Exports the list of artifact names without the .json extension. + * @return An array of artifact names without the .json extension. + */ +export const zksyncArtifactNames = getArtifactFiles( + join(currentDirectory, 'artifacts'), +).map((file) => file.replace('.json', '')); + +/** + * @dev Checks if a ZkSync artifact exists by its name. + * @param name The name of the artifact to check. + * @return True if the artifact exists, false otherwise. + */ +export function artifactExists(name: string): boolean { + return zksyncArtifactNames.includes(name); +} + +/** + * @dev Loads a ZkSync artifact by its name. + * @param name The name of the artifact to load. + * @return The loaded ZKSyncArtifact or undefined if it cannot be loaded. + */ +export function loadZKSyncArtifact(name: string): ZKSyncArtifact | undefined { + try { + const artifactPath = join(currentDirectory, 'artifacts', `${name}.json`); + const artifactContent = readFileSync(artifactPath, 'utf-8'); + return JSON.parse(artifactContent) as ZKSyncArtifact; + } catch (error) { + console.error(`Error loading artifact: ${name}`, error); + return undefined; + } +} + +/** + * @dev Loads all ZkSync artifacts into a map. + * @return A map of artifact names to their corresponding ZkSync artifacts. + */ +export function loadAllZKSyncArtifacts(): ArtifactMap { + const zkSyncArtifactMap: ArtifactMap = {}; + + for (const artifactName of zksyncArtifactNames) { + const artifact = loadZKSyncArtifact(artifactName); + if (artifact) { + zkSyncArtifactMap[artifactName] = artifact; + } + } + + return zkSyncArtifactMap; +} + +/** + * @dev Retrieves a specific ZkSync artifact by its file name. + * @param name The name of the artifact to retrieve. + * @return The loaded ZkSyncArtifact or undefined if it cannot be loaded. + */ +export function getZKSyncArtifactByName( + name: string, +): ZKSyncArtifact | undefined { + return loadZKSyncArtifact(name); +} diff --git a/solidity/exportBuildArtifact.sh b/solidity/exportBuildArtifact.sh index e6d5d503b8..b8b27c89dd 100755 --- a/solidity/exportBuildArtifact.sh +++ b/solidity/exportBuildArtifact.sh @@ -11,7 +11,7 @@ outputFileJs="./dist/buildArtifact.js" outputFileTsd="./dist/buildArtifact.d.ts" # log that we're in the script -echo 'Finding and processing hardhat build artifact...' +echo 'Finding and processing hardhat build EVM artifact...' # Find most recently modified JSON build artifact if [ "$(uname)" = "Darwin" ]; then @@ -37,3 +37,40 @@ else echo 'Failed to process build artifact with jq' exit 1 fi + +# ZKSYNC + +# Define the artifacts directory +artifactsDir="./artifacts-zk/build-info" +# Define the output file +outputFileJson="./dist/zksync/buildArtifact.json" +outputFileJs="./dist/zksync/buildArtifact.js" +outputFileTsd="./dist/zksync/buildArtifact.d.ts" + +# log that we're in the script +echo 'Finding and processing hardhat build ZKSync artifact...' + +# Find most recently modified JSON build artifact +if [ "$(uname)" = "Darwin" ]; then + # for local flow + jsonFiles=$(find "$artifactsDir" -type f -name "*.json" -exec stat -f "%m %N" {} \; | sort -rn | head -n 1 | cut -d' ' -f2-) +else + # for CI flow + jsonFiles=$(find "$artifactsDir" -type f -name "*.json" -exec stat -c "%Y %n" {} \; | sort -rn | head -n 1 | cut -d' ' -f2-) +fi + +if [ ! -f "$jsonFiles" ]; then + echo 'Failed to find build artifact' + exit 1 +fi + +# Extract required keys and write to outputFile +if jq -c '{input, solcLongVersion, zk_version: .output.zk_version}' "$jsonFiles" > "$outputFileJson"; then + echo "export const buildArtifact = " > "$outputFileJs" + cat "$outputFileJson" >> "$outputFileJs" + echo "export const buildArtifact: any" > "$outputFileTsd" + echo 'Finished processing build artifact.' +else + echo 'Failed to process build artifact with jq' + exit 1 +fi diff --git a/solidity/generate-artifact-exports.mjs b/solidity/generate-artifact-exports.mjs new file mode 100755 index 0000000000..be564a2bc9 --- /dev/null +++ b/solidity/generate-artifact-exports.mjs @@ -0,0 +1,39 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; +import { basename, dirname, join } from 'path'; +import { glob } from 'typechain'; +import { fileURLToPath } from 'url'; + +const cwd = process.cwd(); + +/** + * @dev Only includes primary JSON artifacts & excludes debug files and build-info directory + */ +const zksyncArtifacts = glob(cwd, [ + `!./artifacts-zk/!(build-info)/**/*.dbg.json`, + `./artifacts-zk/!(build-info)/**/+([a-zA-Z0-9_]).json`, +]); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const srcOutputDir = join(__dirname, 'core-utils/zksync/artifacts'); + +if (!existsSync(srcOutputDir)) { + mkdirSync(srcOutputDir, { recursive: true }); +} + +/** + * @dev Reads each artifact file and writes it to srcOutputDir + */ +zksyncArtifacts.forEach((file) => { + const fileContent = readFileSync(file, 'utf-8'); + let fileName = `${basename(file, '.json')}`; + + const outputFile = join(srcOutputDir, `${fileName}.json`); + + writeFileSync(outputFile, fileContent); +}); + +console.log( + `Generated ${zksyncArtifacts.length} individual JSON Artifacts in ${srcOutputDir}`, +); diff --git a/solidity/hardhat.config.cts b/solidity/hardhat.config.cts index 6d64d971e6..6fd31c3dc4 100644 --- a/solidity/hardhat.config.cts +++ b/solidity/hardhat.config.cts @@ -22,7 +22,7 @@ module.exports = { currency: 'USD', }, typechain: { - outDir: './types', + outDir: './core-utils/typechain', target: 'ethers-v5', alwaysGenerateOverloads: true, node16Modules: true, diff --git a/solidity/package.json b/solidity/package.json index b80646bd24..c2e8d572ac 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@layerzerolabs/solidity-examples": "^1.1.0", + "@matterlabs/hardhat-zksync-solc": "^1.2.4", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-waffle": "^2.0.6", "@typechain/ethers-v5": "^11.1.2", @@ -35,7 +36,8 @@ "ts-node": "^10.8.0", "tsx": "^4.19.1", "typechain": "patch:typechain@npm%3A8.3.2#~/.yarn/patches/typechain-npm-8.3.2-b02e27439e.patch", - "typescript": "5.3.3" + "typescript": "5.3.3", + "zksync-ethers": "^5.10.0" }, "directories": { "test": "test" @@ -43,9 +45,10 @@ "type": "module", "exports": { ".": "./dist/index.js", - "./mailbox": "./dist/contracts/Mailbox.js", + "./mailbox": "./dist/typechain/contracts/Mailbox.js", "./buildArtifact.js": "./dist/buildArtifact.js", "./buildArtifact.json": "./dist/buildArtifact.json", + "./buildArtifact-zksync.js": "./dist/zksync/buildArtifact.js", "./contracts": "./contracts" }, "types": "./dist/index.d.ts", @@ -64,13 +67,14 @@ ], "license": "Apache-2.0", "scripts": { - "build": "yarn version:update && yarn hardhat-esm compile && tsc && ./exportBuildArtifact.sh", + "build": "yarn version:update && yarn hardhat-esm compile && yarn hardhat-zk compile && ts-node generate-artifact-exports.mjs && tsc && ./exportBuildArtifact.sh && yarn copy-artifacts", "lint": "solhint contracts/**/*.sol", - "clean": "yarn hardhat-esm clean && rm -rf ./dist ./cache ./types ./coverage ./out ./forge-cache ./fixtures", + "clean": "yarn hardhat-esm clean && yarn hardhat-zk clean && rm -rf ./dist ./cache ./cache-zk ./types ./coverage ./out ./forge-cache ./fixtures", "coverage": "yarn fixtures && ./coverage.sh", "docs": "forge doc", "fixtures": "mkdir -p ./fixtures/aggregation ./fixtures/multisig", "hardhat-esm": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only --no-warnings=ExperimentalWarning' hardhat --config hardhat.config.cts", + "hardhat-zk": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only --no-warnings=ExperimentalWarning' hardhat --config zk-hardhat.config.cts", "prettier": "prettier --write ./contracts ./test", "test": "yarn version:exhaustive && yarn hardhat-esm test && yarn test:forge", "test:hardhat": "yarn hardhat-esm test", @@ -82,7 +86,8 @@ "storage": "./storage.sh", "version:update": "sh ./bytecodeversion.sh", "version:changed": "yarn version:update && git diff --exit-code", - "version:exhaustive": "yarn tsx ./test/exhaustiveversion.test.ts" + "version:exhaustive": "yarn tsx ./test/exhaustiveversion.test.ts", + "copy-artifacts": "mkdir -p dist/zksync/artifacts && cp core-utils/zksync/artifacts/*.json dist/zksync/artifacts/" }, "peerDependencies": { "@ethersproject/abi": "*", diff --git a/solidity/test/lib/mailboxes.ts b/solidity/test/lib/mailboxes.ts index 06ae211cf5..6466b7ae0e 100644 --- a/solidity/test/lib/mailboxes.ts +++ b/solidity/test/lib/mailboxes.ts @@ -18,8 +18,8 @@ import { LegacyMultisigIsm, TestMailbox, TestMerkleTreeHook, -} from '../../types'; -import { DispatchEvent } from '../../types/contracts/Mailbox'; +} from '../../core-utils/evm/types'; +import { DispatchEvent } from '../../core-utils/evm/types/contracts/Mailbox'; export type MessageAndProof = { proof: MerkleProof; diff --git a/solidity/test/merkle.test.ts b/solidity/test/merkle.test.ts index f2a3d5d399..1b22160d4c 100644 --- a/solidity/test/merkle.test.ts +++ b/solidity/test/merkle.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import { utils } from 'ethers'; import merkleTestCases from '../../vectors/merkle.json' assert { type: 'json' }; -import { TestMerkle, TestMerkle__factory } from '../types'; +import { TestMerkle, TestMerkle__factory } from '../core-utils/evm/types'; import { getSigner } from './signer'; diff --git a/solidity/test/message.test.ts b/solidity/test/message.test.ts index 83bde28b6f..430470dcae 100644 --- a/solidity/test/message.test.ts +++ b/solidity/test/message.test.ts @@ -8,7 +8,11 @@ import { } from '@hyperlane-xyz/utils'; import testCases from '../../vectors/message.json' assert { type: 'json' }; -import { Mailbox__factory, TestMessage, TestMessage__factory } from '../types'; +import { + Mailbox__factory, + TestMessage, + TestMessage__factory, +} from '../core-utils/evm/types'; import { getSigner, getSigners } from './signer'; diff --git a/solidity/test/mockMailbox.test.ts b/solidity/test/mockMailbox.test.ts index db49a7585e..bd621c7175 100644 --- a/solidity/test/mockMailbox.test.ts +++ b/solidity/test/mockMailbox.test.ts @@ -3,7 +3,10 @@ import { utils } from 'ethers'; import { addressToBytes32 } from '@hyperlane-xyz/utils'; -import { MockMailbox__factory, TestRecipient__factory } from '../types'; +import { + MockMailbox__factory, + TestRecipient__factory, +} from '../core-utils/evm/types'; import { getSigner } from './signer'; diff --git a/solidity/test/testrecipient.test.ts b/solidity/test/testrecipient.test.ts index acdbf573eb..8eb9cf611a 100644 --- a/solidity/test/testrecipient.test.ts +++ b/solidity/test/testrecipient.test.ts @@ -3,7 +3,7 @@ import { utils } from 'ethers'; import { addressToBytes32 } from '@hyperlane-xyz/utils'; -import { TestRecipient, TestRecipient__factory } from '../types'; +import { TestRecipient, TestRecipient__factory } from '../core-utils/evm/types'; import { getSigner } from './signer'; diff --git a/solidity/tsconfig.json b/solidity/tsconfig.json index cece6d7fc6..cf543ddbd1 100644 --- a/solidity/tsconfig.json +++ b/solidity/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../tsconfig.json", "compilerOptions": { "outDir": "./dist", - "rootDir": "./types" + "rootDir": "./core-utils" }, - "exclude": ["./test", "hardhat.config.cts", "./dist"] + "exclude": ["./test", "hardhat.config.cts", "zk-hardhat.config.cts", "./dist"] } diff --git a/solidity/zk-hardhat.config.cts b/solidity/zk-hardhat.config.cts new file mode 100644 index 0000000000..618fe65a5b --- /dev/null +++ b/solidity/zk-hardhat.config.cts @@ -0,0 +1,46 @@ +import '@matterlabs/hardhat-zksync-solc'; +import '@nomiclabs/hardhat-ethers'; +import 'hardhat-ignore-warnings'; + +/** + * @type import('hardhat/config').HardhatUserConfig + */ +module.exports = { + zksolc: { + version: '1.5.3', + compilerSource: 'binary', + enableEraVMExtensions: true, + }, + defaultNetwork: 'zkSyncNetwork', + networks: { + zkSyncNetwork: { + url: 'http://127.0.0.1:8011', + ethNetwork: '', + zksync: true, + }, + }, + solidity: { + version: '0.8.19', + settings: { + optimizer: { + enabled: true, + runs: 999_999, + }, + }, + }, + mocha: { + bail: true, + import: 'tsx', + }, + warnings: { + // turn off all warnings for libs: + 'fx-portal/**/*': { + default: 'off', + }, + }, + paths: { + sources: './contracts', + cache: './cache-zk', + artifacts: './artifacts-zk', + }, +}; diff --git a/typescript/cli/examples/core-config-zksync.yaml b/typescript/cli/examples/core-config-zksync.yaml new file mode 100644 index 0000000000..c04369d67d --- /dev/null +++ b/typescript/cli/examples/core-config-zksync.yaml @@ -0,0 +1,10 @@ +defaultHook: + type: merkleTreeHook +defaultIsm: + threshold: 1 + type: storageMessageIdMultisigIsm + validators: + - '0xBC989fDe9e54cAd2aB4392Af6dF60f04873A033A' +owner: '0xBC989fDe9e54cAd2aB4392Af6dF60f04873A033A' +requiredHook: + type: merkleTreeHook diff --git a/typescript/cli/examples/submit/strategy/json-rpc-chain-strategy.yaml b/typescript/cli/examples/submit/strategy/json-rpc-chain-strategy.yaml index 771153a67d..3251e73bbd 100644 --- a/typescript/cli/examples/submit/strategy/json-rpc-chain-strategy.yaml +++ b/typescript/cli/examples/submit/strategy/json-rpc-chain-strategy.yaml @@ -6,3 +6,9 @@ anvil3: submitter: chain: anvil3 type: jsonRpc +zksync1: + submitter: + type: jsonRpc +zksync2: + submitter: + type: jsonRpc diff --git a/typescript/cli/package.json b/typescript/cli/package.json index 48f7d80596..608fa3b1b7 100644 --- a/typescript/cli/package.json +++ b/typescript/cli/package.json @@ -43,7 +43,8 @@ "eslint-plugin-import": "^2.31.0", "mocha": "^10.2.0", "prettier": "^2.8.8", - "typescript": "5.3.3" + "typescript": "5.3.3", + "zksync-ethers": "^5.10.0" }, "scripts": { "hyperlane": "node ./dist/cli.js", diff --git a/typescript/cli/scripts/run-e2e-test.sh b/typescript/cli/scripts/run-e2e-test.sh index c98fc9158f..ec2bbc0f38 100755 --- a/typescript/cli/scripts/run-e2e-test.sh +++ b/typescript/cli/scripts/run-e2e-test.sh @@ -3,10 +3,16 @@ function cleanup() { set +e pkill -f anvil + rm -rf /tmp/anvil2 + rm -rf /tmp/anvil3 + rm -rf /tmp/zksync1 + rm -rf /tmp/zksync2 rm -rf ./tmp rm -f ./test-configs/anvil/chains/anvil2/addresses.yaml rm -f ./test-configs/anvil/chains/anvil3/addresses.yaml - set -e + rm -f ./test-configs/zksync/chains/zksync1/addresses.yaml + rm -f ./test-configs/zksync/chains/zksync2/addresses.yaml + set -e } cleanup diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index e710b6cf00..65536c15d7 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -5,6 +5,7 @@ import { stringify as yamlStringify } from 'yaml'; import { ChainMetadata, ChainMetadataSchema, + ChainTechnicalStack, EthJsonRpcBlockParameterTag, ExplorerFamily, ZChainName, @@ -69,6 +70,14 @@ export async function createChainConfig({ default: name[0].toUpperCase() + name.slice(1), }); + const technicalStack = (await select({ + choices: Object.entries(ChainTechnicalStack).map(([_, value]) => ({ + value, + })), + message: 'Select the correct chain technical stack', + pageSize: 10, + })) as ChainTechnicalStack; + const chainId = parseInt( await detectAndConfirmOrPrompt( async () => { @@ -93,6 +102,7 @@ export async function createChainConfig({ chainId, domainId: chainId, protocol: ProtocolType.Ethereum, + technicalStack, rpcUrls: [{ http: rpcUrl }], isTestnet, }; diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 570b233cde..51af7f8829 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -1,5 +1,5 @@ import { confirm } from '@inquirer/prompts'; -import { Signer, ethers } from 'ethers'; +import { Signer } from 'ethers'; import { DEFAULT_GITHUB_REGISTRY, @@ -229,10 +229,12 @@ function isCanonicalRepoUrl(url: string) { * @param customChains Custom chains specified by the user * @returns a new MultiProvider */ -async function getMultiProvider(registry: IRegistry, signer?: ethers.Signer) { +async function getMultiProvider(registry: IRegistry, signer?: Signer) { const chainMetadata = await registry.getMetadata(); const multiProvider = new MultiProvider(chainMetadata); + if (signer) multiProvider.setSharedSigner(signer); + return multiProvider; } diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts index 030f11b5f4..31c8dbe052 100644 --- a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -1,5 +1,6 @@ import { password } from '@inquirer/prompts'; import { Signer, Wallet } from 'ethers'; +import { Wallet as ZKSyncWallet } from 'zksync-ethers'; import { ChainName, @@ -26,7 +27,7 @@ export class MultiProtocolSignerFactory { switch (protocol) { case ProtocolType.Ethereum: - if (technicalStack === ChainTechnicalStack.ZkSync) + if (technicalStack === ChainTechnicalStack.ZKSync) return new ZKSyncSignerStrategy(strategyConfig); return new EthereumSignerStrategy(strategyConfig); default: @@ -57,7 +58,6 @@ class EthereumSignerStrategy extends BaseMultiProtocolSigner { } // 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean -// TODO: import ZKSync signer class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { async getSignerConfig(chain: ChainName): Promise { const submitter = this.config[chain]?.submitter as { @@ -74,6 +74,6 @@ class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { } getSigner(config: SignerConfig): Signer { - return new Wallet(config.privateKey); + return new ZKSyncWallet(config.privateKey); } } diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 7ce8a0247c..b42b3c029f 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -10,6 +10,7 @@ import { EvmCoreModule, ExplorerLicenseType, } from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js'; import { requestAndSaveApiKeys } from '../context/context.js'; @@ -20,6 +21,7 @@ import { indentYamlOrJson } from '../utils/files.js'; import { completeDeploy, + isIsmCompatible, prepareDeploy, runDeployPlanStep, runPreflightChecksForChains, @@ -82,6 +84,19 @@ export async function runCoreDeploy(params: DeployParams) { const userAddress = await signer.getAddress(); + const { technicalStack: chainTechnicalStack } = + context.multiProvider.getChainMetadata(chain); + + if (typeof config.defaultIsm !== 'string') { + assert( + isIsmCompatible({ + chainTechnicalStack, + ismType: config.defaultIsm?.type, + }), + `ERROR: Selected ISM of type ${config.defaultIsm?.type} is not compatible with the selected Chain Technical Stack of ${chainTechnicalStack}!`, + ); + } + const initialBalances = await prepareDeploy(context, userAddress, [chain]); const contractVerifier = new ContractVerifier( diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index f5ac01a175..e8c2750a49 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -5,9 +5,13 @@ import { ChainMap, ChainMetadata, ChainName, + ChainTechnicalStack, IsmConfig, + IsmType, MultisigConfig, getLocalProvider, + isIsmStatic, + isStaticDeploymentSupported, } from '@hyperlane-xyz/sdk'; import { Address, ProtocolType } from '@hyperlane-xyz/utils'; @@ -190,3 +194,22 @@ function transformChainMetadataForDisplay(chainMetadata: ChainMetadata) { 'Native Token: Decimals': chainMetadata.nativeToken?.decimals, }; } + +/** + * Checks if the given chain technical stack is compatible with the core configuration. + * + * @param {ChainTechnicalStack | undefined} params.chainTechnicalStack - The technical stack of the chain. + * @param {CoreConfig} params.config - The core configuration to check. + * @returns {boolean} True if the configuration is compatible, false otherwise. + */ +export function isIsmCompatible({ + chainTechnicalStack, + ismType, +}: { + chainTechnicalStack: ChainTechnicalStack | undefined; + ismType: IsmType; +}): boolean { + // Static deployment is not available on certain chains (e.g., ZKSync) for aggregation ISMs. + if (!isIsmStatic[ismType]) return true; + return isStaticDeploymentSupported(chainTechnicalStack); +} diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index e94bd709da..b4c72176e0 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -187,22 +187,21 @@ async function executeDeploy( context: { multiProvider, isDryRun, dryRunChain }, } = params; + const contractVerifier = new ContractVerifier( + multiProvider, + apiKeys, + coreBuildArtifact, + ExplorerLicenseType.MIT, + ); const deployer = warpDeployConfig.isNft ? new HypERC721Deployer(multiProvider) - : new HypERC20Deployer(multiProvider); // TODO: replace with EvmERC20WarpModule + : new HypERC20Deployer(multiProvider, undefined, contractVerifier); // TODO: replace with EvmERC20WarpModule const config: WarpRouteDeployConfig = isDryRun && dryRunChain ? { [dryRunChain]: warpDeployConfig[dryRunChain] } : warpDeployConfig; - const contractVerifier = new ContractVerifier( - multiProvider, - apiKeys, - coreBuildArtifact, - ExplorerLicenseType.MIT, - ); - const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer( multiProvider, contractVerifier, diff --git a/typescript/cli/src/tests/commands/core.ts b/typescript/cli/src/tests/commands/core.ts index 5feabbe2cb..e47db15e20 100644 --- a/typescript/cli/src/tests/commands/core.ts +++ b/typescript/cli/src/tests/commands/core.ts @@ -12,12 +12,14 @@ import { ANVIL_KEY, REGISTRY_PATH } from './helpers.js'; export async function hyperlaneCoreDeploy( chain: string, coreInputPath: string, + privateKey?: string, + registryPath?: string, ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane core deploy \ - --registry ${REGISTRY_PATH} \ + --registry ${registryPath ?? REGISTRY_PATH} \ --config ${coreInputPath} \ --chain ${chain} \ - --key ${ANVIL_KEY} \ + --key ${privateKey ?? ANVIL_KEY} \ --verbosity debug \ --yes`; } diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index 7853815459..7d21425886 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -26,6 +26,8 @@ export const TEMP_PATH = '/tmp'; // /temp gets removed at the end of all-test.sh export const ANVIL_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; +export const ZKSYNC_KEY = + '0x3d3cbc973389cb26f657686445bcc75662b415b656078503592ac8c1abb8810e'; export const E2E_TEST_BURN_ADDRESS = '0x0000000000000000000000000000000000000001'; @@ -62,11 +64,15 @@ export async function updateWarpOwnerConfig( owner: Address, warpCorePath: string, warpDeployPath: string, + key?: string, + registryPath?: string, ): Promise { const warpDeployConfig = await readWarpConfig( chain, warpCorePath, warpDeployPath, + key, + registryPath, ); warpDeployConfig[chain].owner = owner; await writeYamlOrJson(warpDeployPath, warpDeployConfig); @@ -82,9 +88,24 @@ export async function updateOwner( chain: string, warpConfigPath: string, warpCoreConfigPath: string, + key?: string, + registryPath?: string, ) { - await updateWarpOwnerConfig(chain, owner, warpCoreConfigPath, warpConfigPath); - return hyperlaneWarpApply(warpConfigPath, warpCoreConfigPath); + await updateWarpOwnerConfig( + chain, + owner, + warpCoreConfigPath, + warpConfigPath, + key, + registryPath, + ); + return hyperlaneWarpApply( + warpConfigPath, + warpCoreConfigPath, + undefined, + key, + registryPath, + ); } /** @@ -97,6 +118,8 @@ export async function extendWarpConfig(params: { warpCorePath: string; warpDeployPath: string; strategyUrl?: string; + key?: string; + registryPath?: string; }): Promise { const { chain, @@ -105,15 +128,25 @@ export async function extendWarpConfig(params: { warpCorePath, warpDeployPath, strategyUrl, + key, + registryPath, } = params; const warpDeployConfig = await readWarpConfig( chain, warpCorePath, warpDeployPath, + key, + registryPath, ); warpDeployConfig[chainToExtend] = extendedConfig; writeYamlOrJson(warpDeployPath, warpDeployConfig); - await hyperlaneWarpApply(warpDeployPath, warpCorePath, strategyUrl); + await hyperlaneWarpApply( + warpDeployPath, + warpCorePath, + strategyUrl, + key, + registryPath, + ); return warpDeployPath; } @@ -125,17 +158,28 @@ export async function deployOrUseExistingCore( chain: string, coreInputPath: string, key: string, + registryPath?: string, ) { const { registry } = await getContext({ - registryUri: REGISTRY_PATH, + registryUri: registryPath ?? REGISTRY_PATH, registryOverrideUri: '', key, }); const addresses = (await registry.getChainAddresses(chain)) as ChainAddresses; if (!addresses) { - await hyperlaneCoreDeploy(chain, coreInputPath); - return deployOrUseExistingCore(chain, coreInputPath, key); + await hyperlaneCoreDeploy( + chain, + coreInputPath, + key, + registryPath ?? REGISTRY_PATH, + ); + return deployOrUseExistingCore( + chain, + coreInputPath, + key, + registryPath ?? REGISTRY_PATH, + ); } return addresses; @@ -144,9 +188,10 @@ export async function deployOrUseExistingCore( export async function getDomainId( chainName: string, key: string, + registryPath?: string, ): Promise { const { registry } = await getContext({ - registryUri: REGISTRY_PATH, + registryUri: registryPath ?? REGISTRY_PATH, registryOverrideUri: '', key, }); diff --git a/typescript/cli/src/tests/commands/warp.ts b/typescript/cli/src/tests/commands/warp.ts index dc80b6ad24..60c5a5d9fd 100644 --- a/typescript/cli/src/tests/commands/warp.ts +++ b/typescript/cli/src/tests/commands/warp.ts @@ -11,13 +11,16 @@ $.verbose = true; /** * Deploys the Warp route to the specified chain using the provided config. */ -export async function hyperlaneWarpDeploy(warpCorePath: string) { - // --overrides is " " to allow local testing to work +export async function hyperlaneWarpDeploy( + warpCorePath: string, + key?: string, + registryPath?: string, +) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp deploy \ - --registry ${REGISTRY_PATH} \ + --registry ${registryPath ?? REGISTRY_PATH} \ --overrides " " \ --config ${warpCorePath} \ - --key ${ANVIL_KEY} \ + --key ${key ?? ANVIL_KEY} \ --verbosity debug \ --yes`; } @@ -29,13 +32,15 @@ export async function hyperlaneWarpApply( warpDeployPath: string, warpCorePath: string, strategyUrl = '', + key?: string, + registryPath?: string, ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp apply \ - --registry ${REGISTRY_PATH} \ + --registry ${registryPath ?? REGISTRY_PATH} \ --overrides " " \ --config ${warpDeployPath} \ --warp ${warpCorePath} \ - --key ${ANVIL_KEY} \ + --key ${key ?? ANVIL_KEY} \ --verbosity debug \ --strategy ${strategyUrl} \ --yes`; @@ -45,13 +50,15 @@ export async function hyperlaneWarpRead( chain: string, warpAddress: string, warpDeployPath: string, + key?: string, + registryPath?: string, ) { return $`yarn workspace @hyperlane-xyz/cli run hyperlane warp read \ - --registry ${REGISTRY_PATH} \ + --registry ${registryPath ?? REGISTRY_PATH} \ --overrides " " \ --address ${warpAddress} \ --chain ${chain} \ - --key ${ANVIL_KEY} \ + --key ${key ?? ANVIL_KEY} \ --verbosity debug \ --config ${warpDeployPath}`; } @@ -84,8 +91,16 @@ export async function readWarpConfig( chain: string, warpCorePath: string, warpDeployPath: string, + key?: string, + registryPath?: string, ): Promise { const warpAddress = getDeployedWarpAddress(chain, warpCorePath); - await hyperlaneWarpRead(chain, warpAddress!, warpDeployPath); + await hyperlaneWarpRead( + chain, + warpAddress!, + warpDeployPath, + key, + registryPath, + ); return readYamlOrJson(warpDeployPath); } diff --git a/typescript/cli/src/tests/warp-apply-zksync.e2e-test.ts b/typescript/cli/src/tests/warp-apply-zksync.e2e-test.ts new file mode 100644 index 0000000000..e17219b22e --- /dev/null +++ b/typescript/cli/src/tests/warp-apply-zksync.e2e-test.ts @@ -0,0 +1,337 @@ +import { expect } from 'chai'; +import { Wallet } from 'ethers'; + +import { ChainAddresses } from '@hyperlane-xyz/registry'; +import { + HypTokenRouterConfig, + TokenType, + WarpRouteDeployConfig, +} from '@hyperlane-xyz/sdk'; + +import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; + +import { + ZKSYNC_KEY, + deployOrUseExistingCore, + extendWarpConfig, + getDomainId, + updateOwner, +} from './commands/helpers.js'; +import { + hyperlaneWarpApply, + hyperlaneWarpDeploy, + readWarpConfig, +} from './commands/warp.js'; + +const CHAIN_NAME_ZK_2 = 'zksync1'; +const CHAIN_NAME_ZK_3 = 'zksync2'; + +export const TEST_CONFIGS_PATH = './test-configs'; +export const ZK_REGISTRY_PATH = `${TEST_CONFIGS_PATH}/zksync`; + +const BURN_ADDRESS = '0x0000000000000000000000000000000000000001'; +const EXAMPLES_PATH = './examples'; +const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config-zksync.yaml`; +const WARP_CONFIG_PATH_EXAMPLE = `${EXAMPLES_PATH}/warp-route-deployment.yaml`; + +const TEMP_PATH = '/tmp'; //temp gets removed at the end of all-test.sh +const WARP_CONFIG_PATH_2 = `${TEMP_PATH}/zksync/warp-route-deployment.yaml`; +const WARP_CORE_CONFIG_PATH_2 = `${ZK_REGISTRY_PATH}/deployments/warp_routes/ETH/${CHAIN_NAME_ZK_2}-config.yaml`; + +const TEST_TIMEOUT = 180_000; // Long timeout since these tests can take a while +describe.skip('WarpApply zkSync e2e tests', async function () { + let chain2Addresses: ChainAddresses = {}; + this.timeout(TEST_TIMEOUT); + + before(async function () { + await deployOrUseExistingCore( + CHAIN_NAME_ZK_2, + CORE_CONFIG_PATH, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + chain2Addresses = await deployOrUseExistingCore( + CHAIN_NAME_ZK_3, + CORE_CONFIG_PATH, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + // Create a new warp config using the example + const warpConfig: WarpRouteDeployConfig = readYamlOrJson( + WARP_CONFIG_PATH_EXAMPLE, + ); + const zksync2Config = { + zksync1: { ...warpConfig.anvil1 }, + }; + + writeYamlOrJson(WARP_CONFIG_PATH_2, zksync2Config); + }); + + beforeEach(async function () { + await hyperlaneWarpDeploy(WARP_CONFIG_PATH_2, ZKSYNC_KEY, ZK_REGISTRY_PATH); + }); + + it('should burn owner address', async function () { + const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + await updateOwner( + BURN_ADDRESS, + CHAIN_NAME_ZK_2, + warpConfigPath, + WARP_CORE_CONFIG_PATH_2, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const updatedWarpDeployConfig = await readWarpConfig( + CHAIN_NAME_ZK_2, + WARP_CORE_CONFIG_PATH_2, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + expect(updatedWarpDeployConfig.zksync1.owner).to.equal(BURN_ADDRESS); + }); + + it('should not update the same owner', async () => { + const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + await updateOwner( + BURN_ADDRESS, + CHAIN_NAME_ZK_2, + warpConfigPath, + WARP_CORE_CONFIG_PATH_2, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const { stdout } = await updateOwner( + BURN_ADDRESS, + CHAIN_NAME_ZK_2, + warpConfigPath, + WARP_CORE_CONFIG_PATH_2, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + expect(stdout).to.include( + 'Warp config is the same as target. No updates needed.', + ); + }); + + it('should extend an existing warp route', async () => { + // Read existing config into a file + const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-zksync-2.yaml`; + await readWarpConfig( + CHAIN_NAME_ZK_2, + WARP_CORE_CONFIG_PATH_2, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + // Extend with new config + const config: HypTokenRouterConfig = { + decimals: 18, + mailbox: chain2Addresses!.mailbox, + name: 'Ether', + owner: new Wallet(ZKSYNC_KEY).address, + symbol: 'ETH', + totalSupply: 0, + type: TokenType.native, + }; + + await extendWarpConfig({ + chain: CHAIN_NAME_ZK_2, + chainToExtend: CHAIN_NAME_ZK_3, + extendedConfig: config, + warpCorePath: WARP_CORE_CONFIG_PATH_2, + warpDeployPath: warpConfigPath, + key: ZKSYNC_KEY, + registryPath: ZK_REGISTRY_PATH, + }); + + const COMBINED_WARP_CORE_CONFIG_PATH = `${ZK_REGISTRY_PATH}/deployments/warp_routes/ETH/${CHAIN_NAME_ZK_2}-${CHAIN_NAME_ZK_3}-config.yaml`; + + // Check that chain2 is enrolled in chain1 + const updatedWarpDeployConfig1 = await readWarpConfig( + CHAIN_NAME_ZK_2, + COMBINED_WARP_CORE_CONFIG_PATH, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + const chain2Id = await getDomainId( + CHAIN_NAME_ZK_3, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const remoteRouterKeys1 = Object.keys( + updatedWarpDeployConfig1[CHAIN_NAME_ZK_2].remoteRouters!, + ); + expect(remoteRouterKeys1).to.include(chain2Id); + + // Check that chain1 is enrolled in chain2 + const updatedWarpDeployConfig2 = await readWarpConfig( + CHAIN_NAME_ZK_3, + COMBINED_WARP_CORE_CONFIG_PATH, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const remoteRouterKeys2 = Object.keys( + updatedWarpDeployConfig2[CHAIN_NAME_ZK_3].remoteRouters!, + ); + expect(remoteRouterKeys2).to.include( + await getDomainId(CHAIN_NAME_ZK_2, ZKSYNC_KEY, ZK_REGISTRY_PATH), + ); + }); + + it('should extend an existing warp route with json strategy', async () => { + // Read existing config into a file + const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + await readWarpConfig( + CHAIN_NAME_ZK_2, + WARP_CORE_CONFIG_PATH_2, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + // Extend with new config + const config: HypTokenRouterConfig = { + decimals: 18, + mailbox: chain2Addresses!.mailbox, + name: 'Ether', + owner: new Wallet(ZKSYNC_KEY).address, + symbol: 'ETH', + totalSupply: 0, + type: TokenType.native, + }; + + await extendWarpConfig({ + chain: CHAIN_NAME_ZK_2, + chainToExtend: CHAIN_NAME_ZK_3, + extendedConfig: config, + warpCorePath: WARP_CORE_CONFIG_PATH_2, + warpDeployPath: warpConfigPath, + strategyUrl: `${EXAMPLES_PATH}/submit/strategy/json-rpc-chain-strategy.yaml`, + key: ZKSYNC_KEY, + registryPath: ZK_REGISTRY_PATH, + }); + + const COMBINED_WARP_CORE_CONFIG_PATH = `${ZK_REGISTRY_PATH}/deployments/warp_routes/ETH/${CHAIN_NAME_ZK_2}-${CHAIN_NAME_ZK_3}-config.yaml`; + + // Check that chain2 is enrolled in chain1 + const updatedWarpDeployConfig1 = await readWarpConfig( + CHAIN_NAME_ZK_2, + COMBINED_WARP_CORE_CONFIG_PATH, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + const chain2Id = await getDomainId( + CHAIN_NAME_ZK_3, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const remoteRouterKeys1 = Object.keys( + updatedWarpDeployConfig1[CHAIN_NAME_ZK_2].remoteRouters!, + ); + expect(remoteRouterKeys1).to.include(chain2Id); + + // Check that chain1 is enrolled in chain2 + const updatedWarpDeployConfig2 = await readWarpConfig( + CHAIN_NAME_ZK_3, + COMBINED_WARP_CORE_CONFIG_PATH, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + const chain1Id = await getDomainId( + CHAIN_NAME_ZK_2, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const remoteRouterKeys2 = Object.keys( + updatedWarpDeployConfig2[CHAIN_NAME_ZK_3].remoteRouters!, + ); + expect(remoteRouterKeys2).to.include(chain1Id); + }); + + it('should extend an existing warp route and update the owner', async () => { + const warpDeployPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + // Burn zksync1 owner in config + const warpDeployConfig = await readWarpConfig( + CHAIN_NAME_ZK_2, + WARP_CORE_CONFIG_PATH_2, + warpDeployPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + warpDeployConfig[CHAIN_NAME_ZK_2].owner = BURN_ADDRESS; + + // Extend with new config + const randomOwner = new Wallet(ZKSYNC_KEY).address; + const extendedConfig: HypTokenRouterConfig = { + decimals: 18, + mailbox: chain2Addresses!.mailbox, + name: 'Ether', + owner: randomOwner, + symbol: 'ETH', + totalSupply: 0, + type: TokenType.native, + }; + + warpDeployConfig[CHAIN_NAME_ZK_3] = extendedConfig; + writeYamlOrJson(warpDeployPath, warpDeployConfig); + await hyperlaneWarpApply( + warpDeployPath, + WARP_CORE_CONFIG_PATH_2, + undefined, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + const COMBINED_WARP_CORE_CONFIG_PATH = `${ZK_REGISTRY_PATH}/deployments/warp_routes/ETH/${CHAIN_NAME_ZK_2}-${CHAIN_NAME_ZK_3}-config.yaml`; + + const updatedWarpDeployConfig_2 = await readWarpConfig( + CHAIN_NAME_ZK_2, + COMBINED_WARP_CORE_CONFIG_PATH, + warpDeployPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const updatedWarpDeployConfig_3 = await readWarpConfig( + CHAIN_NAME_ZK_3, + COMBINED_WARP_CORE_CONFIG_PATH, + warpDeployPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + // Check that zksync2 owner is burned + expect(updatedWarpDeployConfig_2.zksync1.owner).to.equal(BURN_ADDRESS); + + expect(updatedWarpDeployConfig_3.zksync2.owner).to.equal(randomOwner); + + // Check that both chains enrolled + const chain2Id = await getDomainId( + CHAIN_NAME_ZK_2, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + const chain3Id = await getDomainId( + CHAIN_NAME_ZK_3, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + const remoteRouterKeys2 = Object.keys( + updatedWarpDeployConfig_2[CHAIN_NAME_ZK_2].remoteRouters!, + ); + const remoteRouterKeys3 = Object.keys( + updatedWarpDeployConfig_3[CHAIN_NAME_ZK_3].remoteRouters!, + ); + expect(remoteRouterKeys2).to.include(chain3Id); + expect(remoteRouterKeys3).to.include(chain2Id); + }); +}); diff --git a/typescript/cli/src/tests/warp-read-zksync.e2e-test.ts b/typescript/cli/src/tests/warp-read-zksync.e2e-test.ts new file mode 100644 index 0000000000..3b14de6d6c --- /dev/null +++ b/typescript/cli/src/tests/warp-read-zksync.e2e-test.ts @@ -0,0 +1,60 @@ +import { expect } from 'chai'; + +import { WarpRouteDeployConfig } from '@hyperlane-xyz/sdk'; + +import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; + +import { ZKSYNC_KEY, deployOrUseExistingCore } from './commands/helpers.js'; +import { hyperlaneWarpDeploy, readWarpConfig } from './commands/warp.js'; + +const CHAIN_NAME_ZK_2 = 'zksync2'; + +export const TEST_CONFIGS_PATH = './test-configs'; +export const ZK_REGISTRY_PATH = `${TEST_CONFIGS_PATH}/zksync`; + +const EXAMPLES_PATH = './examples'; +const CORE_CONFIG_PATH = `${EXAMPLES_PATH}/core-config-zksync.yaml`; +const WARP_CONFIG_PATH_EXAMPLE = `${EXAMPLES_PATH}/warp-route-deployment.yaml`; + +const TEMP_PATH = '/tmp'; // temp gets removed at the end of all-test.sh +const WARP_CONFIG_PATH_2 = `${TEMP_PATH}/zksync/warp-route-deployment.yaml`; +const WARP_CORE_CONFIG_PATH_2 = `${ZK_REGISTRY_PATH}/deployments/warp_routes/ETH/${CHAIN_NAME_ZK_2}-config.yaml`; + +const TEST_TIMEOUT = 180_000; // Long timeout since these tests can take a while +describe.skip('WarpRead ZKSync e2e tests', async function () { + let zksync2WarpConfig: WarpRouteDeployConfig; + this.timeout(TEST_TIMEOUT); + before(async function () { + await deployOrUseExistingCore( + CHAIN_NAME_ZK_2, + CORE_CONFIG_PATH, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + + // Create a new warp config using the example + const exampleWarpConfig: WarpRouteDeployConfig = readYamlOrJson( + WARP_CONFIG_PATH_EXAMPLE, + ); + zksync2WarpConfig = { zksync2: { ...exampleWarpConfig.anvil1 } }; + writeYamlOrJson(WARP_CONFIG_PATH_2, zksync2WarpConfig); + }); + + beforeEach(async function () { + await hyperlaneWarpDeploy(WARP_CONFIG_PATH_2, ZKSYNC_KEY, ZK_REGISTRY_PATH); + }); + + it('should be able to read a warp route', async function () { + const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + const warpConfig = await readWarpConfig( + CHAIN_NAME_ZK_2, + WARP_CORE_CONFIG_PATH_2, + warpConfigPath, + ZKSYNC_KEY, + ZK_REGISTRY_PATH, + ); + expect(warpConfig[CHAIN_NAME_ZK_2].type).to.be.equal( + zksync2WarpConfig[CHAIN_NAME_ZK_2].type, + ); + }); +}); diff --git a/typescript/cli/src/utils/keys.ts b/typescript/cli/src/utils/keys.ts index 552b8d53d9..5ee086926e 100644 --- a/typescript/cli/src/utils/keys.ts +++ b/typescript/cli/src/utils/keys.ts @@ -1,5 +1,6 @@ import { input } from '@inquirer/prompts'; import { ethers, providers } from 'ethers'; +import { Wallet } from 'zksync-ethers'; import { impersonateAccount } from '@hyperlane-xyz/sdk'; import { Address, ensure0x } from '@hyperlane-xyz/utils'; @@ -16,7 +17,7 @@ export async function getSigner({ }: { key?: string; skipConfirmation?: boolean; -}) { +}): Promise<{ key?: string; signer: Wallet }> { key ||= await retrieveKey(skipConfirmation); const signer = privateKeyToSigner(key); return { key, signer }; @@ -77,14 +78,14 @@ async function addressToImpersonatedSigner( * @param key a private key * @returns a signer for the private key */ -function privateKeyToSigner(key: string): ethers.Wallet { +function privateKeyToSigner(key: string): Wallet { if (!key) throw new Error('No private key provided'); const formattedKey = key.trim().toLowerCase(); if (ethers.utils.isHexString(ensure0x(formattedKey))) - return new ethers.Wallet(ensure0x(formattedKey)); + return new Wallet(ensure0x(formattedKey)); else if (formattedKey.split(' ').length >= 6) - return ethers.Wallet.fromMnemonic(formattedKey); + return Wallet.fromMnemonic(formattedKey); else throw new Error('Invalid private key format'); } diff --git a/typescript/cli/test-configs/zksync/chains/zksync1/metadata.yaml b/typescript/cli/test-configs/zksync/chains/zksync1/metadata.yaml new file mode 100644 index 0000000000..c90e32e908 --- /dev/null +++ b/typescript/cli/test-configs/zksync/chains/zksync1/metadata.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=../schema.json +chainId: 260 +displayName: zksync1 +domainId: 260 +isTestnet: true +name: zksync1 +nativeToken: + decimals: 18 + name: Ether + symbol: ETH +protocol: ethereum +rpcUrls: + - http: http://127.0.0.1:8011 diff --git a/typescript/cli/test-configs/zksync/chains/zksync2/metadata.yaml b/typescript/cli/test-configs/zksync/chains/zksync2/metadata.yaml new file mode 100644 index 0000000000..613aecd658 --- /dev/null +++ b/typescript/cli/test-configs/zksync/chains/zksync2/metadata.yaml @@ -0,0 +1,13 @@ +# yaml-language-server: $schema=../schema.json +chainId: 270 +displayName: zksync2 +domainId: 270 +isTestnet: true +name: zksync2 +nativeToken: + decimals: 18 + name: Ether + symbol: ETH +protocol: ethereum +rpcUrls: + - http: http://127.0.0.1:3050 diff --git a/typescript/sdk/package.json b/typescript/sdk/package.json index 47bb713b13..6415bb0e2b 100644 --- a/typescript/sdk/package.json +++ b/typescript/sdk/package.json @@ -49,7 +49,8 @@ "ts-node": "^10.8.0", "tsx": "^4.19.1", "typescript": "5.3.3", - "yaml": "2.4.5" + "yaml": "2.4.5", + "zksync-ethers": "^5.10.0" }, "type": "module", "exports": { diff --git a/typescript/sdk/src/core/EvmCoreModule.ts b/typescript/sdk/src/core/EvmCoreModule.ts index 60dad3f523..6881ed771b 100644 --- a/typescript/sdk/src/core/EvmCoreModule.ts +++ b/typescript/sdk/src/core/EvmCoreModule.ts @@ -34,13 +34,17 @@ import { ProxyFactoryFactories, proxyFactoryFactories, } from '../deploy/contracts.js'; +import { isStaticDeploymentSupported } from '../deploy/protocolDeploymentConfig.js'; import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; +import { createDefaultProxyFactoryFactories } from '../deploy/proxyFactoryUtils.js'; +import { ProxyFactoryFactoriesAddresses } from '../deploy/types.js'; import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { HookFactories } from '../hook/contracts.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { IsmConfig } from '../ism/types.js'; +import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { ChainName, ChainNameOrId } from '../types.js'; @@ -294,14 +298,16 @@ export class EvmCoreModule extends HyperlaneModule< contractVerifier?: ContractVerifier; }): Promise { const { config, multiProvider, chain, contractVerifier } = params; - const chainName = multiProvider.getChainName(chain); + const { name: chainName, technicalStack } = + multiProvider.getChainMetadata(chain); - const ismFactoryFactories = await EvmCoreModule.deployIsmFactories({ - chainName, - config, - multiProvider, - contractVerifier, - }); + const ismFactoryFactories: ProxyFactoryFactoriesAddresses = + await this.getIsmFactoryFactories(technicalStack, { + chainName, + config, + multiProvider, + contractVerifier, + }); const ismFactory = new HyperlaneIsmFactory( attachContractsMap( @@ -396,7 +402,6 @@ export class EvmCoreModule extends HyperlaneModule< // Set Core & extra addresses return { ...ismFactoryFactories, - proxyAdmin: proxyAdmin.address, mailbox: mailbox.address, interchainAccountRouter, @@ -501,4 +506,31 @@ export class EvmCoreModule extends HyperlaneModule< ); return mailbox; } + + /** + * Retrieves the ISM factory factories based on the provided protocol and parameters. + * + * @param protocol - The protocol type to determine if static address set deployment should be skipped. + * @param params - An object containing the parameters needed for ISM factory deployment. + * @param params.chainName - The name of the chain for which the ISM factories are being deployed. + * @param params.config - The core configuration to be used during deployment. + * @param params.multiProvider - The multi-provider instance for interacting with the blockchain. + * @param params.contractVerifier - An optional contract verifier for validating contracts during deployment. + * @returns A promise that resolves to the addresses of the deployed ISM factory factories. + */ + private static async getIsmFactoryFactories( + technicalStack: ChainTechnicalStack | undefined, + params: { + chainName: string; + config: CoreConfig; + multiProvider: MultiProvider; + contractVerifier?: ContractVerifier; + }, + ): Promise { + // Check if we should skip static address set deployment + if (!isStaticDeploymentSupported(technicalStack)) { + return createDefaultProxyFactoryFactories(); + } + return EvmCoreModule.deployIsmFactories(params); + } } diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index a3130ecd1a..43940105db 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -32,6 +32,7 @@ import { } from '../contracts/types.js'; import { DerivedHookConfig, EvmHookReader } from '../hook/EvmHookReader.js'; import { DerivedIsmConfig, EvmIsmReader } from '../ism/EvmIsmReader.js'; +import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { RouterConfig } from '../router/types.js'; import { ChainMap, ChainName, OwnableConfig } from '../types.js'; @@ -149,6 +150,7 @@ export class HyperlaneCore extends HyperlaneApp { const mailbox = this.getContracts(origin).mailbox; const destinationDomain = this.multiProvider.getDomainId(destination); const recipientBytes32 = addressToBytes32(recipient); + const quote = await this.quoteGasPayment( origin, destination, @@ -251,6 +253,13 @@ export class HyperlaneCore extends HyperlaneApp { } async estimateHandle(message: DispatchedMessage): Promise { + // This estimation is not possible on zksync as it is overriding transaction.from + // transaction.from must be a signer on zksync + if ( + this.multiProvider.getChainMetadata(this.getDestination(message)) + .technicalStack === ChainTechnicalStack.ZKSync + ) + return '0'; return ( await this.getRecipient(message).estimateGas.handle( message.parsed.origin, diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index fb3262902b..74febabe91 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -32,10 +32,15 @@ import { HookConfig } from '../hook/types.js'; import type { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { IsmConfig } from '../ism/types.js'; import { moduleMatchesConfig } from '../ism/utils.js'; +import { + ChainTechnicalStack, + ExplorerFamily, +} from '../metadata/chainMetadataTypes.js'; import { InterchainAccount } from '../middleware/account/InterchainAccount.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { MailboxClientConfig } from '../router/types.js'; import { ChainMap, ChainName, OwnableConfig } from '../types.js'; +import { getZKSyncArtifactByContractName } from '../utils/zksync.js'; import { UpgradeConfig, @@ -46,6 +51,7 @@ import { proxyImplementation, } from './proxy.js'; import { ContractVerifier } from './verify/ContractVerifier.js'; +import { ZKSyncContractVerifier } from './verify/ZKSyncContractVerifier.js'; import { ContractVerificationInput, ExplorerLicenseType, @@ -53,6 +59,7 @@ import { import { buildVerificationInput, getContractVerificationInput, + getContractVerificationInputForZKSync, shouldAddVerificationInput, } from './verify/utils.js'; @@ -115,6 +122,15 @@ export abstract class HyperlaneDeployer< return this.options.contractVerifier?.verifyContract(chain, input, logger); } + async verifyContractForZKSync( + chain: ChainName, + input: ContractVerificationInput, + logger = this.logger, + ): Promise { + const verifier = new ZKSyncContractVerifier(this.multiProvider); + return verifier.verifyContract(chain, input, logger); + } + abstract deployContracts( chain: ChainName, config: Config, @@ -124,6 +140,7 @@ export abstract class HyperlaneDeployer< configMap: ChainMap, ): Promise> { const configChains = Object.keys(configMap); + const ethereumConfigChains = configChains.filter( (chain) => this.multiProvider.getChainMetadata(chain).protocol === @@ -263,6 +280,7 @@ export abstract class HyperlaneDeployer< setIsm: (contract: C, ism: Address) => Promise, ): Promise { const configuredIsm = await getIsm(contract); + let matches = false; let targetIsm: Address; if (typeof config === 'string') { @@ -291,10 +309,12 @@ export abstract class HyperlaneDeployer< if (!matches) { await this.runIfOwner(chain, contract, async () => { this.logger.debug(`Set ISM on ${chain} with address ${targetIsm}`); + await this.multiProvider.sendTransaction( chain, setIsm(contract, targetIsm), ); + if (!eqAddress(targetIsm, await getIsm(contract))) { throw new Error(`Set ISM failed on ${chain}`); } @@ -390,16 +410,24 @@ export abstract class HyperlaneDeployer< return cachedContract; } } - this.logger.info( `Deploying ${contractName} on ${chain} with constructor args (${constructorArgs.join( ', ', )})...`, ); + + const explorer = this.multiProvider.tryGetExplorerApi(chain); + const { technicalStack } = this.multiProvider.getChainMetadata(chain); + const isZKSyncExplorer = explorer?.family === ExplorerFamily.ZKSync; + const isZKSyncChain = technicalStack === ChainTechnicalStack.ZKSync; + const signer = this.multiProvider.getSigner(chain); + const artifact = await getZKSyncArtifactByContractName(contractName); + const contract = await this.multiProvider.handleDeploy( chain, factory, constructorArgs, + artifact, ); if (initializeArgs) { @@ -417,38 +445,56 @@ export abstract class HyperlaneDeployer< `Initializing ${contractName} (${contract.address}) on ${chain}...`, ); + const overrides = this.multiProvider.getTransactionOverrides(chain); + // Estimate gas for the initialize transaction - const estimatedGas = await contract.estimateGas.initialize( - ...initializeArgs, - ); + const estimatedGas = await contract + .connect(signer) + .estimateGas.initialize(...initializeArgs); - // deploy with buffer on gas limit - const overrides = this.multiProvider.getTransactionOverrides(chain); const initTx = await contract.initialize(...initializeArgs, { gasLimit: addBufferToGasLimit(estimatedGas), ...overrides, }); + this.logger.info(`Contract ${contractName} initialized`); const receipt = await this.multiProvider.handleTx(chain, initTx); + this.logger.debug( `Successfully initialized ${contractName} (${contract.address}) on ${chain}: ${receipt.transactionHash}`, ); } } - const verificationInput = getContractVerificationInput({ - name: contractName, - contract, - bytecode: factory.bytecode, - expectedimplementation: implementationAddress, - }); + let verificationInput: ContractVerificationInput; + if (isZKSyncChain) { + if (!artifact) { + throw new Error( + `No ZKSync artifact found for contract: ${contractName}`, + ); + } + verificationInput = await getContractVerificationInputForZKSync({ + name: contractName, + contract, + constructorArgs: constructorArgs, + artifact: artifact, + expectedimplementation: implementationAddress, + }); + } else { + verificationInput = getContractVerificationInput({ + name: contractName, + contract, + bytecode: factory.bytecode, + expectedimplementation: implementationAddress, + }); + } + this.addVerificationArtifacts(chain, [verificationInput]); // try verifying contract try { - await this.options.contractVerifier?.verifyContract( - chain, - verificationInput, - ); + await this[ + isZKSyncExplorer ? 'verifyContractForZKSync' : 'verifyContract' + ](chain, verificationInput); } catch (error) { // log error but keep deploying, can also verify post-deployment if needed this.logger.debug(`Error verifying contract: ${error}`); @@ -619,6 +665,10 @@ export abstract class HyperlaneDeployer< chain: ChainName, timelockConfig: UpgradeConfig['timelock'], ): Promise { + const TimelockZkArtifact = await getZKSyncArtifactByContractName( + 'TimelockController', + ); + return this.multiProvider.handleDeploy( chain, new TimelockController__factory(), @@ -629,6 +679,7 @@ export abstract class HyperlaneDeployer< [timelockConfig.roles.executor], ethers.constants.AddressZero, ], + TimelockZkArtifact, ); } diff --git a/typescript/sdk/src/deploy/protocolDeploymentConfig.ts b/typescript/sdk/src/deploy/protocolDeploymentConfig.ts new file mode 100644 index 0000000000..f8286e32c4 --- /dev/null +++ b/typescript/sdk/src/deploy/protocolDeploymentConfig.ts @@ -0,0 +1,43 @@ +import { IsmType } from '../ism/types.js'; +import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js'; + +/** + * @notice An array of chain technical stacks that are not supported for static deployment. + */ +export const skipStaticDeployment: ChainTechnicalStack[] = [ + ChainTechnicalStack.ZKSync, +]; + +export const isIsmStatic: Record = { + [IsmType.CUSTOM]: false, + [IsmType.OP_STACK]: false, + [IsmType.ROUTING]: false, + [IsmType.FALLBACK_ROUTING]: false, + [IsmType.AGGREGATION]: true, + [IsmType.MERKLE_ROOT_MULTISIG]: true, + [IsmType.MESSAGE_ID_MULTISIG]: true, + [IsmType.STORAGE_MERKLE_ROOT_MULTISIG]: false, + [IsmType.STORAGE_MESSAGE_ID_MULTISIG]: false, + [IsmType.TEST_ISM]: false, + [IsmType.PAUSABLE]: false, + [IsmType.TRUSTED_RELAYER]: false, + [IsmType.ARB_L2_TO_L1]: false, + [IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG]: true, + [IsmType.WEIGHTED_MESSAGE_ID_MULTISIG]: true, + [IsmType.STORAGE_AGGREGATION]: false, + [IsmType.ICA_ROUTING]: true, +} as const; + +/** + * @notice Checks if a static deployment is supported for a given chain technical stack. + * @param chainTechnicalStack The chain technical stack to check. + * @return True if the static deployment is supported, false otherwise. + */ +export function isStaticDeploymentSupported( + chainTechnicalStack: ChainTechnicalStack | undefined, +): boolean { + return ( + chainTechnicalStack === undefined || + !skipStaticDeployment.includes(chainTechnicalStack) + ); +} diff --git a/typescript/sdk/src/deploy/proxy.ts b/typescript/sdk/src/deploy/proxy.ts index 8749e433a9..dd895643bd 100644 --- a/typescript/sdk/src/deploy/proxy.ts +++ b/typescript/sdk/src/deploy/proxy.ts @@ -1,4 +1,5 @@ import { ethers } from 'ethers'; +import { Provider as ZKSyncProvider } from 'zksync-ethers'; import { ProxyAdmin__factory } from '@hyperlane-xyz/core'; import { Address, ChainId, eqAddress } from '@hyperlane-xyz/utils'; @@ -7,6 +8,8 @@ import { transferOwnershipTransactions } from '../contracts/contracts.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; import { DeployedOwnableConfig } from '../types.js'; +type Provider = ethers.providers.Provider | ZKSyncProvider; + export type UpgradeConfig = { timelock: { delay: number; @@ -19,7 +22,7 @@ export type UpgradeConfig = { }; export async function proxyImplementation( - provider: ethers.providers.Provider, + provider: Provider, proxy: Address, ): Promise
{ // Hardcoded storage slot for implementation per EIP-1967 @@ -31,7 +34,7 @@ export async function proxyImplementation( } export async function isInitialized( - provider: ethers.providers.Provider, + provider: Provider, contract: Address, ): Promise { // Using OZ's Initializable 4.9 which keeps it at the 0x0 slot @@ -43,7 +46,7 @@ export async function isInitialized( } export async function proxyAdmin( - provider: ethers.providers.Provider, + provider: Provider, proxy: Address, ): Promise
{ // Hardcoded storage slot for admin per EIP-1967 @@ -66,7 +69,7 @@ export function proxyConstructorArgs( } export async function isProxy( - provider: ethers.providers.Provider, + provider: Provider, proxy: Address, ): Promise { const admin = await proxyAdmin(provider, proxy); diff --git a/typescript/sdk/src/deploy/proxyFactoryUtils.ts b/typescript/sdk/src/deploy/proxyFactoryUtils.ts new file mode 100644 index 0000000000..ecdbd51dd1 --- /dev/null +++ b/typescript/sdk/src/deploy/proxyFactoryUtils.ts @@ -0,0 +1,16 @@ +import { ethers } from 'ethers'; + +import { proxyFactoryFactories } from './contracts.js'; +import { ProxyFactoryFactoriesAddresses } from './types.js'; + +/** + * Creates a default ProxyFactoryFactoriesAddresses object with all values set to ethers.constants.AddressZero. + * @returns {ProxyFactoryFactoriesAddresses} An object with all factory addresses set to AddressZero. + */ +export function createDefaultProxyFactoryFactories(): ProxyFactoryFactoriesAddresses { + const defaultAddress = ethers.constants.AddressZero; + return Object.keys(proxyFactoryFactories).reduce((acc, key) => { + acc[key as keyof ProxyFactoryFactoriesAddresses] = defaultAddress; // Type assertion added here + return acc; + }, {} as ProxyFactoryFactoriesAddresses); +} diff --git a/typescript/sdk/src/deploy/verify/BaseContractVerifier.ts b/typescript/sdk/src/deploy/verify/BaseContractVerifier.ts new file mode 100644 index 0000000000..da0b19b3d5 --- /dev/null +++ b/typescript/sdk/src/deploy/verify/BaseContractVerifier.ts @@ -0,0 +1,207 @@ +import { Logger } from 'pino'; + +import { isZeroishAddress, rootLogger, sleep } from '@hyperlane-xyz/utils'; + +import { ExplorerFamily } from '../../metadata/chainMetadataTypes.js'; +import { MultiProvider } from '../../providers/MultiProvider.js'; +import { ChainName } from '../../types.js'; + +import { + BuildArtifact, + ContractVerificationInput, + SolidityStandardJsonInput, +} from './types.js'; +import { FamilyVerificationDelay } from './utils.js'; + +export abstract class BaseContractVerifier { + protected logger = rootLogger.child({ module: this.constructor.name }); + protected contractSourceMap: { [contractName: string]: string } = {}; + protected readonly standardInputJson: SolidityStandardJsonInput; + + constructor( + protected readonly multiProvider: MultiProvider, + buildArtifact: BuildArtifact, + ) { + this.standardInputJson = buildArtifact.input; + this.createContractSourceMapFromBuildArtifacts(); + } + + protected createContractSourceMapFromBuildArtifacts(): void { + const contractRegex = /contract\s+([A-Z][a-zA-Z0-9]*)/g; + Object.entries(this.standardInputJson.sources).forEach( + ([sourceName, { content }]) => { + const matches = content.matchAll(contractRegex); + for (const match of matches) { + const contractName = match[1]; + if (contractName) { + this.contractSourceMap[contractName] = sourceName; + } + } + }, + ); + } + + public async verifyContract( + chain: ChainName, + input: ContractVerificationInput, + logger = this.logger, + ): Promise { + const verificationLogger = logger.child({ + chain, + name: input.name, + address: input.address, + }); + + if (!this.shouldVerifyContract(chain, input, verificationLogger)) { + return; + } + + const explorerApi = this.multiProvider.tryGetExplorerApi(chain); + + await sleep( + FamilyVerificationDelay[ + explorerApi?.family as keyof typeof FamilyVerificationDelay + ] ?? 0, + ); + await this.verify(chain, input, verificationLogger); + } + + protected shouldVerifyContract( + chain: ChainName, + input: ContractVerificationInput, + verificationLogger: Logger, + ): boolean { + const metadata = this.multiProvider.tryGetChainMetadata(chain); + const rpcUrl = metadata?.rpcUrls[0].http ?? ''; + if (rpcUrl.includes('localhost') || rpcUrl.includes('127.0.0.1')) { + verificationLogger.debug('Skipping verification for local endpoints'); + return false; + } + + const explorerApi = this.multiProvider.tryGetExplorerApi(chain); + if (!explorerApi) { + verificationLogger.debug('No explorer API set, skipping'); + return false; + } + + if (!explorerApi.family) { + verificationLogger.debug(`No explorer family set, skipping`); + return false; + } + + if (explorerApi.family === ExplorerFamily.Other) { + verificationLogger.debug(`Unsupported explorer family, skipping`); + return false; + } + + if (isZeroishAddress(input.address)) return false; + if (Array.isArray(input.constructorArguments)) { + verificationLogger.debug( + 'Constructor arguments in legacy format, skipping', + ); + return false; + } + + return true; + } + + protected abstract verify( + chain: ChainName, + input: ContractVerificationInput, + verificationLogger: Logger, + ): Promise; + + protected getImplementationData( + chain: ChainName, + input: ContractVerificationInput, + verificationLogger: Logger, + ) { + const sourceName = this.contractSourceMap[input.name]; + if (!sourceName) { + const errorMessage = `Contract '${input.name}' not found in provided build artifact`; + verificationLogger.error(errorMessage); + throw new Error(`[${chain}] ${errorMessage}`); + } + + const filteredStandardInputJson = + this.filterStandardInputJsonByContractName( + input.name, + this.standardInputJson, + verificationLogger, + ); + + return this.prepareImplementationData( + sourceName, + input, + filteredStandardInputJson, + ); + } + + protected abstract prepareImplementationData( + sourceName: string, + input: ContractVerificationInput, + filteredStandardInputJson: SolidityStandardJsonInput, + ): any; + + protected filterStandardInputJsonByContractName( + contractName: string, + input: SolidityStandardJsonInput, + verificationLogger: Logger, + ): SolidityStandardJsonInput { + verificationLogger.trace( + { contractName }, + 'Filtering unused contracts from solidity standard input JSON....', + ); + const filteredSources: SolidityStandardJsonInput['sources'] = {}; + const sourceFiles: string[] = Object.keys(input.sources); + const contractFile: string = this.contractSourceMap[contractName]; + const queue: string[] = [contractFile]; + const processed = new Set(); + + while (queue.length > 0) { + const file = queue.shift()!; + if (processed.has(file)) continue; + processed.add(file); + + filteredSources[file] = input.sources[file]; + + const content = input.sources[file].content; + const importStatements = this.getAllImportStatements(content); + + importStatements.forEach((importStatement) => { + const importPath = importStatement.match(/["']([^"']+)["']/)?.[1]; + if (importPath) { + const resolvedPath = this.resolveImportPath(file, importPath); + if (sourceFiles.includes(resolvedPath)) queue.push(resolvedPath); + } + }); + } + + return { + ...input, + sources: filteredSources, + }; + } + + protected getAllImportStatements(content: string): string[] { + const importRegex = + /import\s+(?:(?:(?:"[^"]+"|'[^']+')\s*;)|(?:{[^}]+}\s+from\s+(?:"[^"]+"|'[^']+')\s*;)|(?:\s*(?:"[^"]+"|'[^']+')\s*;))/g; + return content.match(importRegex) || []; + } + + protected resolveImportPath(currentFile: string, importPath: string): string { + if (importPath.startsWith('@') || importPath.startsWith('http')) { + return importPath; + } + const currentDir = currentFile.split('/').slice(0, -1).join('/'); + const resolvedPath = importPath.split('/').reduce((acc, part) => { + if (part === '..') { + acc.pop(); + } else if (part !== '.') { + acc.push(part); + } + return acc; + }, currentDir.split('/')); + return resolvedPath.join('/'); + } +} diff --git a/typescript/sdk/src/deploy/verify/ContractVerifier.ts b/typescript/sdk/src/deploy/verify/ContractVerifier.ts index 49ecd30980..978ff51721 100644 --- a/typescript/sdk/src/deploy/verify/ContractVerifier.ts +++ b/typescript/sdk/src/deploy/verify/ContractVerifier.ts @@ -1,13 +1,14 @@ import fetch from 'cross-fetch'; -import { ethers } from 'ethers'; import { Logger } from 'pino'; +import { buildArtifact as zksyncBuildArtifact } from '@hyperlane-xyz/core/buildArtifact-zksync.js'; import { rootLogger, sleep, strip0x } from '@hyperlane-xyz/utils'; import { ExplorerFamily } from '../../metadata/chainMetadataTypes.js'; import { MultiProvider } from '../../providers/MultiProvider.js'; import { ChainMap, ChainName } from '../../types.js'; +import { BaseContractVerifier } from './BaseContractVerifier.js'; import { BuildArtifact, CompilerOptions, @@ -19,12 +20,8 @@ import { SolidityStandardJsonInput, } from './types.js'; -export class ContractVerifier { +export class ContractVerifier extends BaseContractVerifier { protected logger = rootLogger.child({ module: 'ContractVerifier' }); - - protected contractSourceMap: { [contractName: string]: string } = {}; - - protected readonly standardInputJson: SolidityStandardJsonInput; protected readonly compilerOptions: CompilerOptions; constructor( @@ -33,86 +30,128 @@ export class ContractVerifier { buildArtifact: BuildArtifact, licenseType: CompilerOptions['licenseType'], ) { - this.standardInputJson = buildArtifact.input; - + super(multiProvider, buildArtifact); const compilerversion = `v${buildArtifact.solcLongVersion}`; - - // double check compiler version matches expected format const versionRegex = /v(\d.\d.\d+)\+commit.\w+/; const matches = versionRegex.exec(compilerversion); if (!matches) { throw new Error(`Invalid compiler version ${compilerversion}`); } - - // set compiler options - // only license type is configurable, empty if not provided this.compilerOptions = { codeformat: 'solidity-standard-json-input', compilerversion, licenseType, }; - - // process input to create mapping of contract names to source names - // this is required to construct the fully qualified contract name - const contractRegex = /contract\s+([A-Z][a-zA-Z0-9]*)/g; - Object.entries(buildArtifact.input.sources).forEach( - ([sourceName, { content }]) => { - const matches = content.matchAll(contractRegex); - for (const match of matches) { - const contractName = match[1]; - if (contractName) { - this.contractSourceMap[contractName] = sourceName; - } - } - }, - ); + if (zksyncBuildArtifact?.zk_version) + this.compilerOptions.zksolcversion = `v${zksyncBuildArtifact.zk_version}`; } - public async verifyContract( + protected async verify( chain: ChainName, input: ContractVerificationInput, - logger = this.logger, + verificationLogger: Logger, ): Promise { - const verificationLogger = logger.child({ - chain, - name: input.name, - address: input.address, - }); + const contractType: string = input.isProxy ? 'proxy' : 'implementation'; - const metadata = this.multiProvider.tryGetChainMetadata(chain); - const rpcUrl = metadata?.rpcUrls[0].http ?? ''; - if (rpcUrl.includes('localhost') || rpcUrl.includes('127.0.0.1')) { - verificationLogger.debug('Skipping verification for local endpoints'); - return; - } + verificationLogger.debug(`📝 Verifying ${contractType}...`); - const explorerApi = this.multiProvider.tryGetExplorerApi(chain); - if (!explorerApi) { - verificationLogger.debug('No explorer API set, skipping'); - return; - } + const data = input.isProxy + ? this.getProxyData(input) + : this.getImplementationData(chain, input, verificationLogger); - if (!explorerApi.family) { - verificationLogger.debug(`No explorer family set, skipping`); - return; - } + try { + const guid: string = await this.submitForm( + chain, + input.isProxy + ? ExplorerApiActions.VERIFY_PROXY + : ExplorerApiActions.VERIFY_IMPLEMENTATION, + verificationLogger, + data, + ); - if (explorerApi.family === ExplorerFamily.Other) { - verificationLogger.debug(`Unsupported explorer family, skipping`); - return; - } + verificationLogger.trace( + { guid }, + `Retrieved guid from verified ${contractType}.`, + ); + + await this.checkStatus( + chain, + input, + verificationLogger, + guid, + contractType, + ); + + const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl( + chain, + input.address, + ); - if (input.address === ethers.constants.AddressZero) return; - if (Array.isArray(input.constructorArguments)) { verificationLogger.debug( - 'Constructor arguments in legacy format, skipping', + { + addressUrl: addressUrl + ? `${addressUrl}#code` + : `Could not retrieve ${contractType} explorer URL.`, + }, + `✅ Successfully verified ${contractType}.`, + ); + } catch (error) { + verificationLogger.debug( + { error }, + `Verification of ${contractType} failed`, ); - return; + throw error; } + } - await this.verify(chain, input, verificationLogger); + private async checkStatus( + chain: ChainName, + input: ContractVerificationInput, + verificationLogger: Logger, + guid: string, + contractType: string, + ): Promise { + verificationLogger.trace({ guid }, `Checking ${contractType} status...`); + await this.submitForm( + chain, + input.isProxy + ? ExplorerApiActions.CHECK_PROXY_STATUS + : ExplorerApiActions.CHECK_IMPLEMENTATION_STATUS, + verificationLogger, + { + guid: guid, + }, + ); + } + + private getProxyData(input: ContractVerificationInput) { + return { + address: input.address, + expectedimplementation: input.expectedimplementation, + }; } + protected prepareImplementationData( + sourceName: string, + input: ContractVerificationInput, + filteredStandardInputJson: SolidityStandardJsonInput, + ) { + return { + sourceCode: JSON.stringify(filteredStandardInputJson), + contractname: `${sourceName}:${input.name}`, + contractaddress: input.address, + constructorArguements: strip0x(input.constructorArguments ?? ''), + ...this.compilerOptions, + }; + } + + /** + * @notice Submits the verification form to the explorer API + * @param chain The name of the chain where the contract is deployed + * @param verificationLogger A logger instance for verification-specific logging + * @param options Additional options for the API request + * @returns The response from the explorer API + */ private async submitForm( chain: ChainName, action: ExplorerApiActions, @@ -125,7 +164,6 @@ export class ContractVerifier { apiKey = this.apiKeys[chain], } = this.multiProvider.getExplorerApi(chain); const params = new URLSearchParams(); - params.set('module', 'contract'); params.set('action', action); if (apiKey) params.set('apikey', apiKey); @@ -256,203 +294,4 @@ export class ContractVerifier { await sleep(timeout); return responseJson.result; } - - private async verify( - chain: ChainName, - input: ContractVerificationInput, - verificationLogger: Logger, - ): Promise { - const contractType: string = input.isProxy ? 'proxy' : 'implementation'; - - verificationLogger.debug(`📝 Verifying ${contractType}...`); - - const data = input.isProxy - ? this.getProxyData(input) - : this.getImplementationData(chain, input, verificationLogger); - - try { - const guid: string = await this.submitForm( - chain, - input.isProxy - ? ExplorerApiActions.VERIFY_PROXY - : ExplorerApiActions.VERIFY_IMPLEMENTATION, - verificationLogger, - data, - ); - - verificationLogger.trace( - { guid }, - `Retrieved guid from verified ${contractType}.`, - ); - - await this.checkStatus( - chain, - input, - verificationLogger, - guid, - contractType, - ); - - const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl( - chain, - input.address, - ); - - verificationLogger.debug( - { - addressUrl: addressUrl - ? `${addressUrl}#code` - : `Could not retrieve ${contractType} explorer URL.`, - }, - `✅ Successfully verified ${contractType}.`, - ); - } catch (error) { - verificationLogger.debug( - { error }, - `Verification of ${contractType} failed`, - ); - throw error; - } - } - - private async checkStatus( - chain: ChainName, - input: ContractVerificationInput, - verificationLogger: Logger, - guid: string, - contractType: string, - ): Promise { - verificationLogger.trace({ guid }, `Checking ${contractType} status...`); - await this.submitForm( - chain, - input.isProxy - ? ExplorerApiActions.CHECK_PROXY_STATUS - : ExplorerApiActions.CHECK_IMPLEMENTATION_STATUS, - verificationLogger, - { - guid: guid, - }, - ); - } - - private getProxyData(input: ContractVerificationInput) { - return { - address: input.address, - expectedimplementation: input.expectedimplementation, - }; - } - - private getImplementationData( - chain: ChainName, - input: ContractVerificationInput, - verificationLogger: Logger, - ) { - const sourceName = this.contractSourceMap[input.name]; - if (!sourceName) { - const errorMessage = `Contract '${input.name}' not found in provided build artifact`; - verificationLogger.error(errorMessage); - throw new Error(`[${chain}] ${errorMessage}`); - } - - const filteredStandardInputJson = - this.filterStandardInputJsonByContractName( - input.name, - this.standardInputJson, - verificationLogger, - ); - - return { - sourceCode: JSON.stringify(filteredStandardInputJson), - contractname: `${sourceName}:${input.name}`, - contractaddress: input.address, - /* TYPO IS ENFORCED BY API */ - constructorArguements: strip0x(input.constructorArguments ?? ''), - ...this.compilerOptions, - }; - } - - /** - * Filters the solidity standard input for a specific contract name. - * - * This is a BFS impl to traverse the source input dependency graph. - * 1. Named contract file is set as root node. - * 2. The next level is formed by the direct imports of the contract file. - * 3. Each subsequent level's dependencies form the next level, etc. - * 4. The queue tracks the next files to process, and ensures the dependency graph explorered level by level. - */ - private filterStandardInputJsonByContractName( - contractName: string, - input: SolidityStandardJsonInput, - verificationLogger: Logger, - ): SolidityStandardJsonInput { - verificationLogger.trace( - { contractName }, - 'Filtering unused contracts from solidity standard input JSON....', - ); - const filteredSources: SolidityStandardJsonInput['sources'] = {}; - const sourceFiles: string[] = Object.keys(input.sources); - const contractFile: string = this.getContractFile( - contractName, - sourceFiles, - ); - const queue: string[] = [contractFile]; - const processed = new Set(); - - while (queue.length > 0) { - const file = queue.shift()!; - if (processed.has(file)) continue; - processed.add(file); - - filteredSources[file] = input.sources[file]; - - const content = input.sources[file].content; - const importStatements = this.getAllImportStatements(content); - - importStatements.forEach((importStatement) => { - const importPath = importStatement.match(/["']([^"']+)["']/)?.[1]; - if (importPath) { - const resolvedPath = this.resolveImportPath(file, importPath); - if (sourceFiles.includes(resolvedPath)) queue.push(resolvedPath); - } - }); - } - - return { - ...input, - sources: filteredSources, - }; - } - - private getContractFile(contractName: string, sourceFiles: string[]): string { - const contractFile = sourceFiles.find((file) => - file.endsWith(`/${contractName}.sol`), - ); - if (!contractFile) { - throw new Error(`Contract ${contractName} not found in sources.`); - } - return contractFile; - } - - private getAllImportStatements(content: string) { - const importRegex = - /import\s+(?:(?:(?:"[^"]+"|'[^']+')\s*;)|(?:{[^}]+}\s+from\s+(?:"[^"]+"|'[^']+')\s*;)|(?:\s*(?:"[^"]+"|'[^']+')\s*;))/g; - return content.match(importRegex) || []; - } - - private resolveImportPath(currentFile: string, importPath: string): string { - /* Use as-is for external dependencies and absolute imports */ - if (importPath.startsWith('@') || importPath.startsWith('http')) { - return importPath; - } - const currentDir = currentFile.split('/').slice(0, -1).join('/'); - const resolvedPath = importPath.split('/').reduce((acc, part) => { - if (part === '..') { - acc.pop(); - } else if (part !== '.') { - acc.push(part); - } - return acc; - }, currentDir.split('/')); - return resolvedPath.join('/'); - } } diff --git a/typescript/sdk/src/deploy/verify/ZKSyncContractVerifier.ts b/typescript/sdk/src/deploy/verify/ZKSyncContractVerifier.ts new file mode 100644 index 0000000000..b76c1d0111 --- /dev/null +++ b/typescript/sdk/src/deploy/verify/ZKSyncContractVerifier.ts @@ -0,0 +1,158 @@ +import fetch from 'cross-fetch'; +import { Logger } from 'pino'; + +import { buildArtifact } from '@hyperlane-xyz/core/buildArtifact-zksync.js'; +import { rootLogger } from '@hyperlane-xyz/utils'; + +import { MultiProvider } from '../../providers/MultiProvider.js'; +import { ChainName } from '../../types.js'; + +import { BaseContractVerifier } from './BaseContractVerifier.js'; +import { + BuildArtifact, + ContractVerificationInput, + SolidityStandardJsonInput, + ZKSyncCompilerOptions, +} from './types.js'; + +/** + * @title ZKSyncContractVerifier + * @notice Handles the verification of ZKSync contracts on block explorers + * @dev This class manages the process of verifying ZKSync contracts, including + * preparing verification data and submitting it to the appropriate explorer API + * Note: Etherscan verification is managed by the ContractVerifier class + * Blockscout verification is not currently supported on ZKSync + */ +export class ZKSyncContractVerifier extends BaseContractVerifier { + protected logger = rootLogger.child({ module: 'ZKSyncContractVerifier' }); + + protected readonly standardInputJson: SolidityStandardJsonInput; + protected readonly compilerOptions: ZKSyncCompilerOptions; + + /** + * @notice Creates a new ZKSyncContractVerifier instance + * @param multiProvider An instance of MultiProvider for interacting with multiple chains + */ + constructor(protected readonly multiProvider: MultiProvider) { + super(multiProvider, buildArtifact); + this.standardInputJson = (buildArtifact as BuildArtifact).input; + + const compilerZksolcVersion = `v${ + (buildArtifact as { zk_version: string }).zk_version + }`; + const compilerSolcVersion = (buildArtifact as BuildArtifact) + .solcLongVersion; + + this.compilerOptions = { + codeFormat: 'solidity-standard-json-input', + compilerSolcVersion, + compilerZksolcVersion, + optimizationUsed: true, + }; + } + + /** + * @notice Verifies a contract on the specified chain + * @param chain The name of the chain where the contract is deployed + * @param input The contract verification input data + * @param verificationLogger A logger instance for verification-specific logging + */ + protected async verify( + chain: ChainName, + input: ContractVerificationInput, + verificationLogger: Logger, + ): Promise { + const contractType: string = input.isProxy ? 'proxy' : 'implementation'; + + verificationLogger.debug(`📝 Verifying ${contractType}...`); + + const data = this.getImplementationData(chain, input, verificationLogger); + + try { + const verificationId: string = await this.submitForm( + chain, + verificationLogger, + data, + ); + + verificationLogger.trace( + { verificationId }, + `Retrieved verificationId from verified ${contractType}.`, + ); + } catch (error) { + verificationLogger.debug( + { error }, + `Verification of ${contractType} failed`, + ); + throw error; + } + } + + protected prepareImplementationData( + sourceName: string, + input: ContractVerificationInput, + filteredStandardInputJson: SolidityStandardJsonInput, + ) { + return { + sourceCode: filteredStandardInputJson, + contractName: `${sourceName}:${input.name}`, + contractAddress: input.address, + constructorArguments: `0x${input.constructorArguments || ''}`, + ...this.compilerOptions, + }; + } + + /** + * @notice Submits the verification form to the explorer API + * @param chain The name of the chain where the contract is deployed + * @param verificationLogger A logger instance for verification-specific logging + * @param options Additional options for the API request + * @returns The response from the explorer API + */ + private async submitForm( + chain: ChainName, + verificationLogger: Logger, + options?: Record, + ): Promise { + const { apiUrl, family } = this.multiProvider.getExplorerApi(chain); + + const url = new URL(apiUrl); + verificationLogger.trace( + { apiUrl, chain }, + 'Sending request to explorer...', + ); + + const response = await fetch(url.toString(), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(options), + }); + let responseJson; + try { + responseJson = await response.json(); + verificationLogger.trace( + { apiUrl, chain }, + 'Parsing response from explorer...', + ); + } catch (error) { + verificationLogger.trace( + { + error, + failure: response.statusText, + status: response.status, + chain, + apiUrl, + family, + }, + 'Failed to parse response from explorer.', + ); + throw new Error( + `Failed to parse response from explorer (${apiUrl}, ${chain}): ${ + response.statusText || 'UNKNOWN STATUS TEXT' + } (${response.status || 'UNKNOWN STATUS'})`, + ); + } + + return responseJson; + } +} diff --git a/typescript/sdk/src/deploy/verify/types.ts b/typescript/sdk/src/deploy/verify/types.ts index 3ef1dd9cd3..585359edae 100644 --- a/typescript/sdk/src/deploy/verify/types.ts +++ b/typescript/sdk/src/deploy/verify/types.ts @@ -27,6 +27,7 @@ export type SolidityStandardJsonInput = { export type BuildArtifact = { input: SolidityStandardJsonInput; solcLongVersion: string; + zk_version?: string; //only for zksync }; // see https://etherscan.io/contract-license-types @@ -51,6 +52,14 @@ export type CompilerOptions = { codeformat: 'solidity-standard-json-input'; compilerversion: string; // see https://etherscan.io/solcversions for list of support versions licenseType?: ExplorerLicenseType; + zksolcversion?: string; //only for zksync chains +}; + +export type ZKSyncCompilerOptions = { + codeFormat: 'solidity-standard-json-input'; + compilerSolcVersion: string; + compilerZksolcVersion: string; + optimizationUsed: boolean; }; export enum ExplorerApiActions { diff --git a/typescript/sdk/src/deploy/verify/utils.ts b/typescript/sdk/src/deploy/verify/utils.ts index 85f5877bfc..e82b3e1696 100644 --- a/typescript/sdk/src/deploy/verify/utils.ts +++ b/typescript/sdk/src/deploy/verify/utils.ts @@ -3,6 +3,7 @@ import { ethers, utils } from 'ethers'; import { ProxyAdmin__factory, TransparentUpgradeableProxy__factory, + ZKSyncArtifact, } from '@hyperlane-xyz/core'; import { Address, assert, eqAddress } from '@hyperlane-xyz/utils'; @@ -71,6 +72,36 @@ export function getContractVerificationInput({ ); } +export async function getContractVerificationInputForZKSync({ + name, + contract, + constructorArgs, + artifact, + isProxy, + expectedimplementation, +}: { + name: string; + contract: ethers.Contract; + constructorArgs: any[]; + artifact: ZKSyncArtifact; + isProxy?: boolean; + expectedimplementation?: Address; +}): Promise { + const args = encodeArguments(artifact.abi, constructorArgs); + return buildVerificationInput( + name, + contract.address, + args, + isProxy, + expectedimplementation, + ); +} + +export function encodeArguments(abi: any, constructorArgs: any[]): string { + const contractInterface = new utils.Interface(abi); + return contractInterface.encodeDeploy(constructorArgs).replace('0x', ''); +} + /** * Check if the artifact should be added to the verification inputs. * @param verificationInputs - The verification inputs for the chain. @@ -93,7 +124,14 @@ export function shouldAddVerificationInput( } /** - * Retrieves the constructor args using their respective Explorer and/or RPC (eth_getTransactionByHash) + * @notice Defines verification delay times for different blockchain explorer families. + * @dev This constant object associates explorer families with specific delay times (in milliseconds) + */ +export const FamilyVerificationDelay = { + [ExplorerFamily.Etherscan]: 40000, +} as const; + +/** Retrieves the constructor args using their respective Explorer and/or RPC (eth_getTransactionByHash) */ export async function getConstructorArgumentsApi({ chainName, diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index eddbb1c788..61175c1f60 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -64,6 +64,7 @@ import { HookConfig, HookConfigSchema, HookType, + HookTypeToContractNameMap, IgpHookConfig, MUTABLE_HOOK_TYPE, OpStackHookConfig, @@ -929,9 +930,10 @@ export class EvmHookModule extends HyperlaneModule< // deploy fallback hook const fallbackHook = await this.deploy({ config: config.fallback }); // deploy routing hook with fallback - routingHook = await this.deployer.deployContract( + routingHook = await this.deployer.deployContractWithName( this.chain, HookType.FALLBACK_ROUTING, + HookTypeToContractNameMap[HookType.FALLBACK_ROUTING], [this.args.addresses.mailbox, deployerAddress, fallbackHook.address], ); } else { diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index 768e93134b..51dbe48641 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -38,6 +38,21 @@ export enum HookType { ARB_L2_TO_L1 = 'arbL2ToL1Hook', } +export const HookTypeToContractNameMap: Record< + Exclude, + string +> = { + [HookType.MERKLE_TREE]: 'merkleTreeHook', + [HookType.INTERCHAIN_GAS_PAYMASTER]: 'interchainGasPaymaster', + [HookType.AGGREGATION]: 'staticAggregationHook', + [HookType.PROTOCOL_FEE]: 'protocolFee', + [HookType.OP_STACK]: 'opStackHook', + [HookType.ROUTING]: 'domainRoutingHook', + [HookType.FALLBACK_ROUTING]: 'fallbackDomainRoutingHook', + [HookType.PAUSABLE]: 'pausableHook', + [HookType.ARB_L2_TO_L1]: 'arbL2ToL1Hook', +}; + export type MerkleTreeHookConfig = z.infer; export type IgpHookConfig = z.infer; export type ProtocolFeeHookConfig = z.infer; diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index ad8c6c4870..936d89a14d 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -105,6 +105,7 @@ export { ViolationType, } from './deploy/types.js'; export { ContractVerifier } from './deploy/verify/ContractVerifier.js'; +export { ZKSyncContractVerifier } from './deploy/verify/ZKSyncContractVerifier.js'; export { PostDeploymentContractVerifier } from './deploy/verify/PostDeploymentContractVerifier.js'; export { BuildArtifact, @@ -625,4 +626,13 @@ export { WarpTypedTransaction, } from './warp/types.js'; export { WarpCore, WarpCoreOptions } from './warp/WarpCore.js'; + +export { MailboxClientConfigSchema as mailboxClientConfigSchema } from './router/types.js'; + +export { + isStaticDeploymentSupported, + isIsmStatic, + skipStaticDeployment, +} from './deploy/protocolDeploymentConfig.js'; + export { EvmHookModule } from './hook/EvmHookModule.js'; diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts index 310b6e1ee9..a3c8a9226a 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -74,6 +74,7 @@ export class EvmIsmModule extends HyperlaneModule< this.ismFactory = HyperlaneIsmFactory.fromAddressesMap( { [params.chain]: params.addresses }, multiProvider, + contractVerifier, ); this.mailbox = params.addresses.mailbox; diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index c8c987a5d9..8f3943e389 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -25,6 +25,7 @@ import { StorageMessageIdMultisigIsm__factory, TestIsm__factory, TrustedRelayerIsm__factory, + ZKSyncArtifact, } from '@hyperlane-xyz/core'; import { Address, @@ -47,8 +48,14 @@ import { ProxyFactoryFactories, proxyFactoryFactories, } from '../deploy/contracts.js'; +import { + isIsmStatic, + isStaticDeploymentSupported, +} from '../deploy/protocolDeploymentConfig.js'; +import { ContractVerifier } from '../deploy/verify/ContractVerifier.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap, ChainName } from '../types.js'; +import { getZKSyncArtifactByContractName } from '../utils/zksync.js'; import { AggregationIsmConfig, @@ -90,25 +97,33 @@ export class HyperlaneIsmFactory extends HyperlaneApp { constructor( contractsMap: HyperlaneContractsMap, public readonly multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, ) { super( contractsMap, multiProvider, rootLogger.child({ module: 'ismFactoryApp' }), ); - this.deployer = new IsmDeployer(multiProvider, ismFactories); + this.deployer = new IsmDeployer(multiProvider, ismFactories, { + contractVerifier, + }); } static fromAddressesMap( addressesMap: HyperlaneAddressesMap, multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, ): HyperlaneIsmFactory { const helper = appFromAddressesMapHelper( addressesMap, proxyFactoryFactories, multiProvider, ); - return new HyperlaneIsmFactory(helper.contractsMap, multiProvider); + return new HyperlaneIsmFactory( + helper.contractsMap, + multiProvider, + contractVerifier, + ); } async deploy(params: { @@ -136,6 +151,14 @@ export class HyperlaneIsmFactory extends HyperlaneApp { }`, ); + const { technicalStack } = this.multiProvider.getChainMetadata(destination); + + // For static ISM types it checks whether the technical stack supports static contract deployment + assert( + !isIsmStatic[ismType] || isStaticDeploymentSupported(technicalStack), + `Technical stack ${technicalStack} is not compatible with ${ismType}`, + ); + let contract: DeployedIsmType[typeof ismType]; switch (ismType) { case IsmType.MESSAGE_ID_MULTISIG: @@ -250,11 +273,13 @@ export class HyperlaneIsmFactory extends HyperlaneApp { factory: | StorageMerkleRootMultisigIsm__factory | StorageMessageIdMultisigIsm__factory, + artifact: ZKSyncArtifact | undefined, ) => { const contract = await this.multiProvider.handleDeploy( destination, factory, [config.validators, config.threshold], + artifact, ); return contract.address; }; @@ -275,11 +300,13 @@ export class HyperlaneIsmFactory extends HyperlaneApp { case IsmType.STORAGE_MERKLE_ROOT_MULTISIG: address = await deployStorage( new StorageMerkleRootMultisigIsm__factory(), + await getZKSyncArtifactByContractName('StorageMerkleRootMultisigIsm'), ); break; case IsmType.STORAGE_MESSAGE_ID_MULTISIG: address = await deployStorage( new StorageMessageIdMultisigIsm__factory(), + await getZKSyncArtifactByContractName('StorageMessageIdMultisigIsm'), ); break; default: @@ -467,6 +494,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { destination, new DefaultFallbackRoutingIsm__factory(), [mailbox], + await getZKSyncArtifactByContractName('DefaultFallbackRoutingIsm'), ); // TODO: Should verify contract here logger.debug('Initialising fallback routing ISM ...'); diff --git a/typescript/sdk/src/metadata/chainMetadataTypes.ts b/typescript/sdk/src/metadata/chainMetadataTypes.ts index f8972f9bdb..5e7b9fc100 100644 --- a/typescript/sdk/src/metadata/chainMetadataTypes.ts +++ b/typescript/sdk/src/metadata/chainMetadataTypes.ts @@ -22,6 +22,7 @@ export enum ExplorerFamily { Etherscan = 'etherscan', Blockscout = 'blockscout', Routescan = 'routescan', + ZKSync = 'zksync', Other = 'other', } @@ -30,7 +31,7 @@ export enum ChainTechnicalStack { OpStack = 'opstack', PolygonCDK = 'polygoncdk', PolkadotSubstrate = 'polkadotsubstrate', - ZkSync = 'zksync', + ZKSync = 'zksync', Other = 'other', } diff --git a/typescript/sdk/src/providers/MultiProvider.ts b/typescript/sdk/src/providers/MultiProvider.ts index 7b5f923061..719016ce39 100644 --- a/typescript/sdk/src/providers/MultiProvider.ts +++ b/typescript/sdk/src/providers/MultiProvider.ts @@ -8,7 +8,13 @@ import { providers, } from 'ethers'; import { Logger } from 'pino'; +import { + ContractFactory as ZKSyncContractFactory, + Provider as ZKSyncProvider, + Wallet as ZKSyncWallet, +} from 'zksync-ethers'; +import { ZKSyncArtifact } from '@hyperlane-xyz/core'; import { Address, addBufferToGasLimit, @@ -18,13 +24,18 @@ import { import { testChainMetadata, testChains } from '../consts/testChains.js'; import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; -import { ChainMetadata } from '../metadata/chainMetadataTypes.js'; +import { + ChainMetadata, + ChainTechnicalStack, +} from '../metadata/chainMetadataTypes.js'; import { ChainMap, ChainName, ChainNameOrId } from '../types.js'; +import { ZKSyncDeployer } from '../zksync/ZKSyncDeployer.js'; import { AnnotatedEV5Transaction } from './ProviderType.js'; import { ProviderBuilderFn, defaultProviderBuilder, + defaultZKProviderBuilder, } from './providerBuilders.js'; type Provider = providers.Provider; @@ -89,17 +100,25 @@ export class MultiProvider extends ChainMetadataManager { tryGetProvider(chainNameOrId: ChainNameOrId): Provider | null { const metadata = this.tryGetChainMetadata(chainNameOrId); if (!metadata) return null; - const { name, chainId, rpcUrls } = metadata; + const { name, chainId, rpcUrls, technicalStack } = metadata; if (this.providers[name]) return this.providers[name]; if (testChains.includes(name)) { - this.providers[name] = new providers.JsonRpcProvider( - 'http://127.0.0.1:8545', - 31337, - ); + if (technicalStack === ChainTechnicalStack.ZKSync) { + this.providers[name] = new ZKSyncProvider('http://127.0.0.1:8011', 260); + } else { + this.providers[name] = new providers.JsonRpcProvider( + 'http://127.0.0.1:8545', + 31337, + ); + } } else if (rpcUrls.length) { - this.providers[name] = this.providerBuilder(rpcUrls, chainId); + if (technicalStack === ChainTechnicalStack.ZKSync) { + this.providers[name] = defaultZKProviderBuilder(rpcUrls, chainId); + } else { + this.providers[name] = this.providerBuilder(rpcUrls, chainId); + } } else { return null; } @@ -155,7 +174,9 @@ export class MultiProvider extends ChainMetadataManager { if (signer.provider) return signer; // Auto-connect the signer for convenience const provider = this.tryGetProvider(chainName); - return provider ? signer.connect(provider) : signer; + if (!provider) return signer; + + return signer.connect(provider); } /** @@ -308,34 +329,53 @@ export class MultiProvider extends ChainMetadataManager { * Wait for deploy tx to be confirmed * @throws if chain's metadata or signer has not been set or tx fails */ - async handleDeploy( + async handleDeploy( chainNameOrId: ChainNameOrId, factory: F, params: Parameters, + artifact?: ZKSyncArtifact, ): Promise>> { - // setup contract factory + const metadata = this.getChainMetadata(chainNameOrId); + + const { technicalStack } = metadata; + + let contract; const overrides = this.getTransactionOverrides(chainNameOrId); const signer = this.getSigner(chainNameOrId); - const contractFactory = await factory.connect(signer); - // estimate gas - const deployTx = contractFactory.getDeployTransaction(...params); - const gasEstimated = await signer.estimateGas(deployTx); + if (technicalStack === ChainTechnicalStack.ZKSync) { + if (!artifact) throw new Error(`No ZKSync contract artifact provided!`); - // deploy with buffer on gas limit - const contract = await contractFactory.deploy(...params, { - gasLimit: addBufferToGasLimit(gasEstimated), - ...overrides, - }); + // Handle deployment for ZKSync protocol + const deployer = new ZKSyncDeployer(signer as ZKSyncWallet); - this.logger.trace( - `Deploying contract ${contract.address} on ${chainNameOrId}:`, - { transaction: deployTx }, - ); + const estimatedGas = await deployer.estimateDeployGas(artifact, params); + + contract = await deployer.deploy(artifact, params, { + gasLimit: addBufferToGasLimit(estimatedGas), + ...overrides, + }); + + this.logger.trace( + `Contract deployed at ${contract.address} on ${chainNameOrId}:`, + ); + } else { + const contractFactory = factory.connect(signer); + const deployTx = contractFactory.getDeployTransaction(...params); + const estimatedGas = await signer.estimateGas(deployTx); + contract = await contractFactory.deploy(...params, { + gasLimit: addBufferToGasLimit(estimatedGas), // 10% buffer + ...overrides, + }); - // wait for deploy tx to be confirmed - await this.handleTx(chainNameOrId, contract.deployTransaction); + // wait for deploy tx to be confirmed + await this.handleTx(chainNameOrId, contract.deployTransaction); + this.logger.trace( + `Contract deployed at ${contract.address} on ${chainNameOrId}:`, + { transaction: deployTx }, + ); + } // return deployed contract return contract as Awaited>; } @@ -413,6 +453,7 @@ export class MultiProvider extends ChainMetadataManager { } const txReq = await this.prepareTx(chainNameOrId, tx); const signer = this.getSigner(chainNameOrId); + const response = await signer.sendTransaction(txReq); this.logger.info(`Sent tx ${response.hash}`); return this.handleTx(chainNameOrId, response); @@ -422,7 +463,10 @@ export class MultiProvider extends ChainMetadataManager { * Creates a MultiProvider using the given signer for all test networks */ static createTestMultiProvider( - params: { signer?: Signer; provider?: Provider } = {}, + params: { + signer?: Signer; + provider?: Provider; + } = {}, chains: ChainName[] = testChains, ): MultiProvider { const { signer, provider } = params; diff --git a/typescript/sdk/src/providers/ProviderType.ts b/typescript/sdk/src/providers/ProviderType.ts index ce1740873d..d8330affdf 100644 --- a/typescript/sdk/src/providers/ProviderType.ts +++ b/typescript/sdk/src/providers/ProviderType.ts @@ -21,10 +21,16 @@ import type { Transaction as VTransaction, TransactionReceipt as VTransactionReceipt, } from 'viem'; +import { + Contract as ZKSyncBaseContract, + Provider as ZKSyncBaseProvider, + types as zkSyncTypes, +} from 'zksync-ethers'; import { Annotated, ProtocolType } from '@hyperlane-xyz/utils'; export enum ProviderType { + ZKSync = 'zksync', EthersV5 = 'ethers-v5', Viem = 'viem', SolanaWeb3 = 'solana-web3', @@ -96,6 +102,11 @@ interface TypedProviderBase { provider: T; } +export interface ZKSyncProvider extends TypedProviderBase { + type: ProviderType.ZKSync; + provider: ZKSyncBaseProvider; +} + export interface EthersV5Provider extends TypedProviderBase { type: ProviderType.EthersV5; @@ -130,7 +141,8 @@ export type TypedProvider = | ViemProvider | SolanaWeb3Provider | CosmJsProvider - | CosmJsWasmProvider; + | CosmJsWasmProvider + | ZKSyncProvider; /** * Contracts with discriminated union of provider type @@ -141,6 +153,10 @@ interface TypedContractBase { contract: T; } +export interface ZKSyncContract extends TypedContractBase { + type: ProviderType.ZKSync; + contract: ZKSyncBaseContract; +} export interface EthersV5Contract extends TypedContractBase { type: ProviderType.EthersV5; contract: EV5Contract; @@ -175,7 +191,8 @@ export type TypedContract = | ViemContract | SolanaWeb3Contract | CosmJsContract - | CosmJsWasmContract; + | CosmJsWasmContract + | ZKSyncBaseContract; /** * Transactions with discriminated union of provider type @@ -191,6 +208,11 @@ export interface EthersV5Transaction type: ProviderType.EthersV5; transaction: EV5Transaction; } +export interface ZKSyncTransaction + extends TypedTransactionBase { + type: ProviderType.ZKSync; + transaction: zkSyncTypes.TransactionRequest; +} export type AnnotatedEV5Transaction = Annotated; @@ -233,6 +255,11 @@ interface TypedTransactionReceiptBase { receipt: T; } +export interface ZKSyncTransactionReceipt + extends TypedTransactionReceiptBase { + type: ProviderType.ZKSync; + receipt: zkSyncTypes.TransactionReceipt; +} export interface EthersV5TransactionReceipt extends TypedTransactionReceiptBase { type: ProviderType.EthersV5; @@ -268,4 +295,5 @@ export type TypedTransactionReceipt = | ViemTransactionReceipt | SolanaWeb3TransactionReceipt | CosmJsTransactionReceipt - | CosmJsWasmTransactionReceipt; + | CosmJsWasmTransactionReceipt + | ZKSyncTransactionReceipt; diff --git a/typescript/sdk/src/providers/providerBuilders.ts b/typescript/sdk/src/providers/providerBuilders.ts index bc8051b1ba..1203c3e74d 100644 --- a/typescript/sdk/src/providers/providerBuilders.ts +++ b/typescript/sdk/src/providers/providerBuilders.ts @@ -3,8 +3,9 @@ import { StargateClient } from '@cosmjs/stargate'; import { Connection } from '@solana/web3.js'; import { providers } from 'ethers'; import { createPublicClient, http } from 'viem'; +import { Provider as ZKProvider } from 'zksync-ethers'; -import { ProtocolType, isNumeric } from '@hyperlane-xyz/utils'; +import { ProtocolType, assert, isNumeric } from '@hyperlane-xyz/utils'; import { ChainMetadata, RpcUrl } from '../metadata/chainMetadataTypes.js'; @@ -16,6 +17,7 @@ import { SolanaWeb3Provider, TypedProvider, ViemProvider, + ZKSyncProvider, } from './ProviderType.js'; import { HyperlaneSmartProvider } from './SmartProvider/SmartProvider.js'; import { ProviderRetryOptions } from './SmartProvider/types.js'; @@ -109,6 +111,16 @@ export function defaultCosmJsWasmProviderBuilder( }; } +export function defaultZKSyncProviderBuilder( + rpcUrls: RpcUrl[], + network: providers.Networkish, +): ZKSyncProvider { + assert(rpcUrls.length, 'No RPC URLs provided'); + const url = rpcUrls[0].http; + const provider = new ZKProvider(url, network); + return { type: ProviderType.ZKSync, provider }; +} + // Kept for backwards compatibility export function defaultProviderBuilder( rpcUrls: RpcUrl[], @@ -117,6 +129,13 @@ export function defaultProviderBuilder( return defaultEthersV5ProviderBuilder(rpcUrls, _network).provider; } +export function defaultZKProviderBuilder( + rpcUrls: RpcUrl[], + _network: number | string, +): ZKProvider { + return defaultZKSyncProviderBuilder(rpcUrls, _network).provider; +} + export type ProviderBuilderMap = Record< ProviderType, ProviderBuilderFn @@ -128,6 +147,7 @@ export const defaultProviderBuilderMap: ProviderBuilderMap = { [ProviderType.SolanaWeb3]: defaultSolProviderBuilder, [ProviderType.CosmJs]: defaultCosmJsProviderBuilder, [ProviderType.CosmJsWasm]: defaultCosmJsWasmProviderBuilder, + [ProviderType.ZKSync]: defaultZKSyncProviderBuilder, }; export const protocolToDefaultProviderBuilder: Record< diff --git a/typescript/sdk/src/utils/zksync.ts b/typescript/sdk/src/utils/zksync.ts new file mode 100644 index 0000000000..0b97e4330c --- /dev/null +++ b/typescript/sdk/src/utils/zksync.ts @@ -0,0 +1,32 @@ +import { ZKSyncArtifact, loadAllZKSyncArtifacts } from '@hyperlane-xyz/core'; + +/** + * @dev Retrieves a ZkSync artifact by its contract name or qualified name. + * @param name The name of the contract or qualified name in the format "sourceName:contractName". + * @return The corresponding ZKSyncArtifact if found, or undefined if not found. + */ +export const getZKSyncArtifactByContractName = async ( + name: string, +): Promise => { + // Load all ZkSync artifacts + const allArtifacts = loadAllZKSyncArtifacts(); + + // Find the artifact that matches the contract name or qualified name + const artifact = Object.values(allArtifacts).find( + ({ contractName, sourceName }: ZKSyncArtifact) => { + const lowerCaseContractName = contractName.toLowerCase(); + const lowerCaseName = name.toLowerCase(); + + // Check if the contract name matches + if (lowerCaseContractName === lowerCaseName) { + return true; + } + + // Check if the qualified name matches + const qualifiedName = `${sourceName}:${contractName}`; + return qualifiedName === name; // Return true if qualified name matches + }, + ); + + return artifact; +}; diff --git a/typescript/sdk/src/zksync/ZKSyncDeployer.ts b/typescript/sdk/src/zksync/ZKSyncDeployer.ts new file mode 100644 index 0000000000..fc41628150 --- /dev/null +++ b/typescript/sdk/src/zksync/ZKSyncDeployer.ts @@ -0,0 +1,195 @@ +import { BigNumber, BytesLike, Overrides, utils } from 'ethers'; +import { + Contract, + ContractFactory, + Provider, + Wallet, + types as zksyncTypes, +} from 'zksync-ethers'; + +import { ZKSyncArtifact, loadAllZKSyncArtifacts } from '@hyperlane-xyz/core'; +import { assert } from '@hyperlane-xyz/utils'; + +/** + * An entity capable of deploying contracts to the zkSync network. + */ +export class ZKSyncDeployer { + public zkWallet: Wallet; + public deploymentType?: zksyncTypes.DeploymentType; + + constructor(zkWallet: Wallet, deploymentType?: zksyncTypes.DeploymentType) { + this.deploymentType = deploymentType; + + const zkWeb3Provider = new Provider('http://127.0.0.1:8011', 260); + + const l2Provider = + zkWallet.provider === null ? zkWeb3Provider : zkWallet.provider; + + this.zkWallet = zkWallet.connect(l2Provider); + } + + public async loadArtifact(contractTitle: string): Promise { + const zksyncArtifacts = await loadAllZKSyncArtifacts(); + const artifact = (Object.values(zksyncArtifacts) as ZKSyncArtifact[]).find( + ({ contractName, sourceName }) => { + if (contractName === contractTitle) { + return true; + } + + const qualifiedName = `${sourceName}:${contractName}`; + if (contractTitle === qualifiedName) { + return true; + } + + return false; + }, + ); + + assert(artifact, `No ZKSync artifact for contract ${contractTitle} found!`); + + return artifact as any; + } + + /** + * Estimates the price of calling a deploy transaction in ETH. + * + * @param artifact The previously loaded artifact object. + * @param constructorArguments List of arguments to be passed to the contract constructor. + * + * @returns Calculated fee in ETH wei + */ + public async estimateDeployFee( + artifact: ZKSyncArtifact, + constructorArguments: any[], + ): Promise { + const gas = await this.estimateDeployGas(artifact, constructorArguments); + const gasPrice = await this.zkWallet.provider.getGasPrice(); + return gas.mul(gasPrice); + } + + /** + * Estimates the amount of gas needed to execute a deploy transaction. + * + * @param artifact The previously loaded artifact object. + * @param constructorArguments List of arguments to be passed to the contract constructor. + * + * @returns Calculated amount of gas. + */ + public async estimateDeployGas( + artifact: ZKSyncArtifact, + constructorArguments: any[], + ): Promise { + const factoryDeps = await this.extractFactoryDeps(artifact); + + const factory = new ContractFactory( + artifact.abi, + artifact.bytecode, + this.zkWallet, + this.deploymentType, + ); + + // Encode deploy transaction so it can be estimated. + const deployTx = factory.getDeployTransaction(...constructorArguments, { + customData: { + factoryDeps, + }, + }); + deployTx.from = this.zkWallet.address; + + return this.zkWallet.provider.estimateGas(deployTx); + } + + /** + * Sends a deploy transaction to the zkSync network. + * For now, it will use defaults for the transaction parameters: + * - fee amount is requested automatically from the zkSync server. + * + * @param artifact The previously loaded artifact object. + * @param constructorArguments List of arguments to be passed to the contract constructor. + * @param overrides Optional object with additional deploy transaction parameters. + * @param additionalFactoryDeps Additional contract bytecodes to be added to the factory dependencies list. + * + * @returns A contract object. + */ + public async deploy( + artifact: ZKSyncArtifact, + constructorArguments: any[] = [], + overrides?: Overrides, + additionalFactoryDeps?: BytesLike[], + ): Promise { + const baseDeps = await this.extractFactoryDeps(artifact); + const additionalDeps = additionalFactoryDeps + ? additionalFactoryDeps.map((val) => utils.hexlify(val)) + : []; + const factoryDeps = [...baseDeps, ...additionalDeps]; + + const factory = new ContractFactory( + artifact.abi, + artifact.bytecode, + this.zkWallet, + this.deploymentType, + ); + + const { customData, ..._overrides } = overrides ?? {}; + + // Encode and send the deploy transaction providing factory dependencies. + const contract = await factory.deploy(...constructorArguments, { + ..._overrides, + customData: { + ...customData, + factoryDeps, + }, + }); + + await contract.deployed(); + + return contract; + } + + /** + * Extracts factory dependencies from the artifact. + * + * @param artifact Artifact to extract dependencies from + * + * @returns Factory dependencies in the format expected by SDK. + */ + async extractFactoryDeps(artifact: ZKSyncArtifact): Promise { + const visited = new Set(); + + visited.add(`${artifact.sourceName}:${artifact.contractName}`); + return this.extractFactoryDepsRecursive(artifact, visited); + } + + private async extractFactoryDepsRecursive( + artifact: ZKSyncArtifact, + visited: Set, + ): Promise { + // Load all the dependency bytecodes. + // We transform it into an array of bytecodes. + const factoryDeps: string[] = []; + for (const dependencyHash in artifact.factoryDeps) { + if ( + Object.prototype.hasOwnProperty.call( + artifact.factoryDeps, + dependencyHash, + ) + ) { + const dependencyContract = artifact.factoryDeps[dependencyHash]; + if (!visited.has(dependencyContract)) { + const dependencyArtifact = await this.loadArtifact( + dependencyContract, + ); + factoryDeps.push(dependencyArtifact.bytecode); + visited.add(dependencyContract); + const transitiveDeps = await this.extractFactoryDepsRecursive( + dependencyArtifact, + visited, + ); + factoryDeps.push(...transitiveDeps); + } + } + } + + return factoryDeps; + } +} diff --git a/yarn.lock b/yarn.lock index 91d745e473..a50cd88b14 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4240,6 +4240,13 @@ __metadata: languageName: node linkType: hard +"@balena/dockerignore@npm:^1.0.2": + version: 1.0.2 + resolution: "@balena/dockerignore@npm:1.0.2" + checksum: 10/13d654fdd725008577d32e721c720275bdc48f72bce612326363d5bed449febbed856c517a0b23c7c40d87cb531e63432804550b4ecc13e365d26fee38fb6c8a + languageName: node + linkType: hard + "@base2/pretty-print-object@npm:1.0.1": version: 1.0.1 resolution: "@base2/pretty-print-object@npm:1.0.1" @@ -7398,6 +7405,7 @@ __metadata: typescript: "npm:5.3.3" yaml: "npm:2.4.5" yargs: "npm:^17.7.2" + zksync-ethers: "npm:^5.10.0" zod: "npm:^3.21.2" zod-validation-error: "npm:^3.3.0" zx: "npm:^8.1.4" @@ -7415,6 +7423,7 @@ __metadata: "@hyperlane-xyz/utils": "npm:7.3.0" "@layerzerolabs/lz-evm-oapp-v2": "npm:2.0.2" "@layerzerolabs/solidity-examples": "npm:^1.1.0" + "@matterlabs/hardhat-zksync-solc": "npm:^1.2.4" "@nomiclabs/hardhat-ethers": "npm:^2.2.3" "@nomiclabs/hardhat-waffle": "npm:^2.0.6" "@openzeppelin/contracts": "npm:^4.9.3" @@ -7441,6 +7450,7 @@ __metadata: tsx: "npm:^4.19.1" typechain: "patch:typechain@npm%3A8.3.2#~/.yarn/patches/typechain-npm-8.3.2-b02e27439e.patch" typescript: "npm:5.3.3" + zksync-ethers: "npm:^5.10.0" peerDependencies: "@ethersproject/abi": "*" "@ethersproject/providers": "*" @@ -7640,6 +7650,7 @@ __metadata: typescript: "npm:5.3.3" viem: "npm:^2.21.45" yaml: "npm:2.4.5" + zksync-ethers: "npm:^5.10.0" zod: "npm:^3.21.2" peerDependencies: "@ethersproject/abi": "*" @@ -8741,6 +8752,27 @@ __metadata: languageName: node linkType: hard +"@matterlabs/hardhat-zksync-solc@npm:^1.2.4": + version: 1.2.5 + resolution: "@matterlabs/hardhat-zksync-solc@npm:1.2.5" + dependencies: + "@nomiclabs/hardhat-docker": "npm:^2.0.2" + chai: "npm:^4.3.4" + chalk: "npm:^4.1.2" + debug: "npm:^4.3.5" + dockerode: "npm:^4.0.2" + fs-extra: "npm:^11.2.0" + proper-lockfile: "npm:^4.1.2" + semver: "npm:^7.6.2" + sinon: "npm:^18.0.0" + sinon-chai: "npm:^3.7.0" + undici: "npm:^6.18.2" + peerDependencies: + hardhat: ^2.22.5 + checksum: 10/0452ad5504258fad2f2d10be40cc79bb0e65d3470af75f70f60e4a94bb92f238031a13587cda51a0dfd77ef1cb028145088e2628a02db6d7a5ac721bf2816bf0 + languageName: node + linkType: hard + "@mdx-js/react@npm:^2.1.5": version: 2.3.0 resolution: "@mdx-js/react@npm:2.3.0" @@ -9626,6 +9658,17 @@ __metadata: languageName: node linkType: hard +"@nomiclabs/hardhat-docker@npm:^2.0.2": + version: 2.0.2 + resolution: "@nomiclabs/hardhat-docker@npm:2.0.2" + dependencies: + dockerode: "npm:^2.5.8" + fs-extra: "npm:^7.0.1" + node-fetch: "npm:^2.6.0" + checksum: 10/2b6601a7bcac115a24dc4d2ce35b76b1748ffaebd723afad17e8f506231e1d6c7e5c9df73b29d429c5eb01cb0e11ff92f10c746ca31343b0fd3ddc449c9ec8f3 + languageName: node + linkType: hard + "@nomiclabs/hardhat-ethers@npm:^2.2.3": version: 2.2.3 resolution: "@nomiclabs/hardhat-ethers@npm:2.2.3" @@ -12974,7 +13017,7 @@ __metadata: languageName: node linkType: hard -"@sinonjs/commons@npm:^3.0.0": +"@sinonjs/commons@npm:^3.0.0, @sinonjs/commons@npm:^3.0.1": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" dependencies: @@ -12983,6 +13026,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:11.2.2": + version: 11.2.2 + resolution: "@sinonjs/fake-timers@npm:11.2.2" + dependencies: + "@sinonjs/commons": "npm:^3.0.0" + checksum: 10/da7dfa677b2362bc5a321fc1563184755b5c62fbb1a72457fb9e901cd187ba9dc834f9e8a0fb5a4e1d1e6e6ad4c5b54e90900faa44dd6c82d3c49c92ec23ecd4 + languageName: node + linkType: hard + "@sinonjs/fake-timers@npm:>=5, @sinonjs/fake-timers@npm:^9.1.2": version: 9.1.2 resolution: "@sinonjs/fake-timers@npm:9.1.2" @@ -13001,6 +13053,15 @@ __metadata: languageName: node linkType: hard +"@sinonjs/fake-timers@npm:^13.0.1": + version: 13.0.5 + resolution: "@sinonjs/fake-timers@npm:13.0.5" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + checksum: 10/11ee417968fc4dce1896ab332ac13f353866075a9d2a88ed1f6258f17cc4f7d93e66031b51fcddb8c203aa4d53fd980b0ae18aba06269f4682164878a992ec3f + languageName: node + linkType: hard + "@sinonjs/samsam@npm:^6.1.1": version: 6.1.1 resolution: "@sinonjs/samsam@npm:6.1.1" @@ -13012,6 +13073,17 @@ __metadata: languageName: node linkType: hard +"@sinonjs/samsam@npm:^8.0.0": + version: 8.0.2 + resolution: "@sinonjs/samsam@npm:8.0.2" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + lodash.get: "npm:^4.4.2" + type-detect: "npm:^4.1.0" + checksum: 10/58ca9752e8e835a09ed275f8edf8da2720fe95c0c02f6bcb90ad7f86fdceb393f35f744194b705dd94216228646ec0aedbb814e245eb869b940dcf1266b7a533 + languageName: node + linkType: hard + "@sinonjs/text-encoding@npm:^0.7.1": version: 0.7.1 resolution: "@sinonjs/text-encoding@npm:0.7.1" @@ -13019,6 +13091,13 @@ __metadata: languageName: node linkType: hard +"@sinonjs/text-encoding@npm:^0.7.3": + version: 0.7.3 + resolution: "@sinonjs/text-encoding@npm:0.7.3" + checksum: 10/f0cc89bae36e7ce159187dece7800b78831288f1913e9ae8cf8a878da5388232d2049740f6f4a43ec4b43b8ad1beb55f919f45eb9a577adb4a2a6eacb27b25fc + languageName: node + linkType: hard + "@smithy/abort-controller@npm:^3.0.0": version: 3.0.0 resolution: "@smithy/abort-controller@npm:3.0.0" @@ -17078,6 +17157,18 @@ __metadata: languageName: node linkType: hard +"JSONStream@npm:1.3.2": + version: 1.3.2 + resolution: "JSONStream@npm:1.3.2" + dependencies: + jsonparse: "npm:^1.2.0" + through: "npm:>=2.2.7 <3" + bin: + JSONStream: ./bin.js + checksum: 10/3a1274f39e9b0369da5d5536906b527113326434a43b92923ac2d3c2d449009253b245055de2633b1d9ca7ae30054b6091d755e79f0cb1c7dab9b6b253871812 + languageName: node + linkType: hard + "JSONStream@npm:^1.3.5": version: 1.3.5 resolution: "JSONStream@npm:1.3.5" @@ -17909,7 +18000,7 @@ __metadata: languageName: node linkType: hard -"asn1@npm:~0.2.3": +"asn1@npm:^0.2.6, asn1@npm:~0.2.3": version: 0.2.6 resolution: "asn1@npm:0.2.6" dependencies: @@ -18323,7 +18414,7 @@ __metadata: languageName: node linkType: hard -"bcrypt-pbkdf@npm:^1.0.0": +"bcrypt-pbkdf@npm:^1.0.0, bcrypt-pbkdf@npm:^1.0.2": version: 1.0.2 resolution: "bcrypt-pbkdf@npm:1.0.2" dependencies: @@ -18491,6 +18582,16 @@ __metadata: languageName: node linkType: hard +"bl@npm:^1.0.0": + version: 1.2.3 + resolution: "bl@npm:1.2.3" + dependencies: + readable-stream: "npm:^2.3.5" + safe-buffer: "npm:^5.1.1" + checksum: 10/11d775b09ebd7d8c0df1ed7efd03cc8a2b1283c804a55153c81a0b586728a085fa24240647cac9a60163eb6f36a28cf8c45b80bf460a46336d4c84c40205faff + languageName: node + linkType: hard + "bl@npm:^4.0.3, bl@npm:^4.1.0": version: 4.1.0 resolution: "bl@npm:4.1.0" @@ -18815,6 +18916,23 @@ __metadata: languageName: node linkType: hard +"buffer-alloc-unsafe@npm:^1.1.0": + version: 1.1.0 + resolution: "buffer-alloc-unsafe@npm:1.1.0" + checksum: 10/c5e18bf51f67754ec843c9af3d4c005051aac5008a3992938dda1344e5cfec77c4b02b4ca303644d1e9a6e281765155ce6356d85c6f5ccc5cd21afc868def396 + languageName: node + linkType: hard + +"buffer-alloc@npm:^1.2.0": + version: 1.2.0 + resolution: "buffer-alloc@npm:1.2.0" + dependencies: + buffer-alloc-unsafe: "npm:^1.1.0" + buffer-fill: "npm:^1.0.0" + checksum: 10/560cd27f3cbe73c614867da373407d4506309c62fe18de45a1ce191f3785ec6ca2488d802ff82065798542422980ca25f903db078c57822218182c37c3576df5 + languageName: node + linkType: hard + "buffer-crc32@npm:~0.2.3": version: 0.2.13 resolution: "buffer-crc32@npm:0.2.13" @@ -18829,6 +18947,13 @@ __metadata: languageName: node linkType: hard +"buffer-fill@npm:^1.0.0": + version: 1.0.0 + resolution: "buffer-fill@npm:1.0.0" + checksum: 10/c29b4723ddeab01e74b5d3b982a0c6828f2ded49cef049ddca3dac661c874ecdbcecb5dd8380cf0f4adbeb8cff90a7de724126750a1f1e5ebd4eb6c59a1315b1 + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -18934,6 +19059,13 @@ __metadata: languageName: node linkType: hard +"buildcheck@npm:~0.0.6": + version: 0.0.6 + resolution: "buildcheck@npm:0.0.6" + checksum: 10/194ee8d3b0926fd6f3e799732130ad7ab194882c56900b8670ad43c81326f64871f49b7d9f1e9baad91ca3070eb4e8b678797fe9ae78cf87dde86d8916eb25d2 + languageName: node + linkType: hard + "busboy@npm:^1.6.0": version: 1.6.0 resolution: "busboy@npm:1.6.0" @@ -19392,7 +19524,7 @@ __metadata: languageName: node linkType: hard -"chownr@npm:^1.1.1, chownr@npm:^1.1.4": +"chownr@npm:^1.0.1, chownr@npm:^1.1.1, chownr@npm:^1.1.4": version: 1.1.4 resolution: "chownr@npm:1.1.4" checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d @@ -19927,7 +20059,7 @@ __metadata: languageName: node linkType: hard -"concat-stream@npm:^1.6.0, concat-stream@npm:^1.6.2": +"concat-stream@npm:^1.6.0, concat-stream@npm:^1.6.2, concat-stream@npm:~1.6.2": version: 1.6.2 resolution: "concat-stream@npm:1.6.2" dependencies: @@ -20156,6 +20288,17 @@ __metadata: languageName: node linkType: hard +"cpu-features@npm:~0.0.10": + version: 0.0.10 + resolution: "cpu-features@npm:0.0.10" + dependencies: + buildcheck: "npm:~0.0.6" + nan: "npm:^2.19.0" + node-gyp: "npm:latest" + checksum: 10/941b828ffe77582b2bdc03e894c913e2e2eeb5c6043ccb01338c34446d026f6888dc480ecb85e684809f9c3889d245f3648c7907eb61a92bdfc6aed039fcda8d + languageName: node + linkType: hard + "crc-32@npm:^1.2.0": version: 1.2.2 resolution: "crc-32@npm:1.2.2" @@ -20485,7 +20628,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:^3.1.0, debug@npm:^3.2.7": +"debug@npm:^3.1.0, debug@npm:^3.2.6, debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" dependencies: @@ -20947,6 +21090,13 @@ __metadata: languageName: node linkType: hard +"diff@npm:^5.2.0": + version: 5.2.0 + resolution: "diff@npm:5.2.0" + checksum: 10/01b7b440f83a997350a988e9d2f558366c0f90f15be19f4aa7f1bb3109a4e153dfc3b9fbf78e14ea725717017407eeaa2271e3896374a0181e8f52445740846d + languageName: node + linkType: hard + "difflib@npm:^0.2.4": version: 0.2.4 resolution: "difflib@npm:0.2.4" @@ -20979,6 +21129,52 @@ __metadata: languageName: node linkType: hard +"docker-modem@npm:^1.0.8": + version: 1.0.9 + resolution: "docker-modem@npm:1.0.9" + dependencies: + JSONStream: "npm:1.3.2" + debug: "npm:^3.2.6" + readable-stream: "npm:~1.0.26-4" + split-ca: "npm:^1.0.0" + checksum: 10/2ade3d9f1b25231a5ecadcbfb9401a397eff3de2eec7add8130de1c40004faaa58fe074e5110ccef12957973089e5911b711648c77944a4a15d908e9b9605549 + languageName: node + linkType: hard + +"docker-modem@npm:^5.0.3": + version: 5.0.3 + resolution: "docker-modem@npm:5.0.3" + dependencies: + debug: "npm:^4.1.1" + readable-stream: "npm:^3.5.0" + split-ca: "npm:^1.0.1" + ssh2: "npm:^1.15.0" + checksum: 10/fc4cc09f3aab0e17d32eb5a01974bed845a803e23352937aceab2e35c35bdcd283d1655c154b737063b037f164f57addbd447628d620f564a2da82e036543a5f + languageName: node + linkType: hard + +"dockerode@npm:^2.5.8": + version: 2.5.8 + resolution: "dockerode@npm:2.5.8" + dependencies: + concat-stream: "npm:~1.6.2" + docker-modem: "npm:^1.0.8" + tar-fs: "npm:~1.16.3" + checksum: 10/13111cfcaf47905cd2cd323a07cb5b79404ef5e9032e33ef3a6f71d1f72283d9b2921b6de955c8454b147bbf4db33822a80d960b2250e3e8aed62ffe0b43083f + languageName: node + linkType: hard + +"dockerode@npm:^4.0.2": + version: 4.0.2 + resolution: "dockerode@npm:4.0.2" + dependencies: + "@balena/dockerignore": "npm:^1.0.2" + docker-modem: "npm:^5.0.3" + tar-fs: "npm:~2.0.1" + checksum: 10/859279721553cc07d00f8e7ac55abb3bba3a8a42685c742f3651c46a996755c720005fedc8b6bb7ac0ca5dc9123536164099c93741d691c7779669ecde3bbc3d + languageName: node + linkType: hard + "doctrine@npm:^2.1.0": version: 2.1.0 resolution: "doctrine@npm:2.1.0" @@ -22700,7 +22896,7 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^5.1.0, ethers@npm:^5.3.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2": +"ethers@npm:^5.1.0, ethers@npm:^5.3.1, ethers@npm:^5.7.0, ethers@npm:^5.7.1, ethers@npm:^5.7.2, ethers@npm:~5.7.0": version: 5.7.2 resolution: "ethers@npm:5.7.2" dependencies: @@ -23678,7 +23874,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.0": +"fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: @@ -27335,6 +27531,13 @@ __metadata: languageName: node linkType: hard +"just-extend@npm:^6.2.0": + version: 6.2.0 + resolution: "just-extend@npm:6.2.0" + checksum: 10/1f487b074b9e5773befdd44dc5d1b446f01f24f7d4f1f255d51c0ef7f686e8eb5f95d983b792b9ca5c8b10cd7e60a924d64103725759eddbd7f18bcb22743f92 + languageName: node + linkType: hard + "jwa@npm:^2.0.0": version: 2.0.0 resolution: "jwa@npm:2.0.0" @@ -29156,7 +29359,7 @@ __metadata: languageName: node linkType: hard -"nan@npm:^2.13.2": +"nan@npm:^2.13.2, nan@npm:^2.19.0, nan@npm:^2.20.0": version: 2.22.0 resolution: "nan@npm:2.22.0" dependencies: @@ -29268,6 +29471,19 @@ __metadata: languageName: node linkType: hard +"nise@npm:^6.0.0": + version: 6.1.1 + resolution: "nise@npm:6.1.1" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + "@sinonjs/fake-timers": "npm:^13.0.1" + "@sinonjs/text-encoding": "npm:^0.7.3" + just-extend: "npm:^6.2.0" + path-to-regexp: "npm:^8.1.0" + checksum: 10/2d3175587cf0a351e2c91eb643fdc59d266de39f394a3ac0bace38571749d1e7f25341d763899245139b8f0d2ee048b2d3387d75ecf94c4897e947d5fc881eea + languageName: node + linkType: hard + "nock@npm:13.5.4": version: 13.5.4 resolution: "nock@npm:13.5.4" @@ -30406,6 +30622,13 @@ __metadata: languageName: node linkType: hard +"path-to-regexp@npm:^8.1.0": + version: 8.2.0 + resolution: "path-to-regexp@npm:8.2.0" + checksum: 10/23378276a172b8ba5f5fb824475d1818ca5ccee7bbdb4674701616470f23a14e536c1db11da9c9e6d82b82c556a817bbf4eee6e41b9ed20090ef9427cbb38e13 + languageName: node + linkType: hard + "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -31060,7 +31283,7 @@ __metadata: languageName: node linkType: hard -"proper-lockfile@npm:^4.1.1": +"proper-lockfile@npm:^4.1.1, proper-lockfile@npm:^4.1.2": version: 4.1.2 resolution: "proper-lockfile@npm:4.1.2" dependencies: @@ -31203,6 +31426,16 @@ __metadata: languageName: node linkType: hard +"pump@npm:^1.0.0": + version: 1.0.3 + resolution: "pump@npm:1.0.3" + dependencies: + end-of-stream: "npm:^1.1.0" + once: "npm:^1.3.1" + checksum: 10/61fe58694f9900020a5cf5bc765d74396891c201afecf06659df2f5874fd832be4e19e2f95cc72d8b9eb98ace0a4db3cebf7343f9fc893a930577be29e3ad8b5 + languageName: node + linkType: hard + "pump@npm:^2.0.0": version: 2.0.1 resolution: "pump@npm:2.0.1" @@ -31933,7 +32166,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.3.3, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.3.0, readable-stream@npm:^2.3.3, readable-stream@npm:^2.3.5, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -31963,7 +32196,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^3.1.0, readable-stream@npm:^3.6.2": +"readable-stream@npm:^3.1.0, readable-stream@npm:^3.5.0, readable-stream@npm:^3.6.2": version: 3.6.2 resolution: "readable-stream@npm:3.6.2" dependencies: @@ -31998,6 +32231,18 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:~1.0.26-4": + version: 1.0.34 + resolution: "readable-stream@npm:1.0.34" + dependencies: + core-util-is: "npm:~1.0.0" + inherits: "npm:~2.0.1" + isarray: "npm:0.0.1" + string_decoder: "npm:~0.10.x" + checksum: 10/20537fca5a8ffd4af0f483be1cce0e981ed8cbb1087e0c762e2e92ae77f1005627272cebed8422f28047b465056aa1961fefd24baf532ca6a3616afea6811ae0 + languageName: node + linkType: hard + "readdirp@npm:~3.2.0": version: 3.2.0 resolution: "readdirp@npm:3.2.0" @@ -33442,6 +33687,16 @@ __metadata: languageName: node linkType: hard +"sinon-chai@npm:^3.7.0": + version: 3.7.0 + resolution: "sinon-chai@npm:3.7.0" + peerDependencies: + chai: ^4.0.0 + sinon: ">=4.0.0" + checksum: 10/028853eb8a545ca613c6863014a40f07d1e6b81467e20939fefcd13f170206d24165b91099fb297aeb4d137745e321da25daa8e2d665cc0a78f90d5b877e8bbe + languageName: node + linkType: hard + "sinon@npm:^13.0.2": version: 13.0.2 resolution: "sinon@npm:13.0.2" @@ -33456,6 +33711,20 @@ __metadata: languageName: node linkType: hard +"sinon@npm:^18.0.0": + version: 18.0.1 + resolution: "sinon@npm:18.0.1" + dependencies: + "@sinonjs/commons": "npm:^3.0.1" + "@sinonjs/fake-timers": "npm:11.2.2" + "@sinonjs/samsam": "npm:^8.0.0" + diff: "npm:^5.2.0" + nise: "npm:^6.0.0" + supports-color: "npm:^7" + checksum: 10/65be65a7c5dbef7e9e315820bcd17fe0387fb07cb5dd41d3d6a89177e9a2ca463144676d8407348dc075758daa7cf37b9376d35bd1bc7e06717e87d03aa26111 + languageName: node + linkType: hard + "sisteransi@npm:^1.0.5": version: 1.0.5 resolution: "sisteransi@npm:1.0.5" @@ -33955,6 +34224,13 @@ __metadata: languageName: node linkType: hard +"split-ca@npm:^1.0.0, split-ca@npm:^1.0.1": + version: 1.0.1 + resolution: "split-ca@npm:1.0.1" + checksum: 10/1e7409938a95ee843fe2593156a5735e6ee63772748ee448ea8477a5a3e3abde193c3325b3696e56a5aff07c7dcf6b1f6a2f2a036895b4f3afe96abb366d893f + languageName: node + linkType: hard + "split-on-first@npm:^1.0.0": version: 1.1.0 resolution: "split-on-first@npm:1.1.0" @@ -33976,6 +34252,23 @@ __metadata: languageName: node linkType: hard +"ssh2@npm:^1.15.0": + version: 1.16.0 + resolution: "ssh2@npm:1.16.0" + dependencies: + asn1: "npm:^0.2.6" + bcrypt-pbkdf: "npm:^1.0.2" + cpu-features: "npm:~0.0.10" + nan: "npm:^2.20.0" + dependenciesMeta: + cpu-features: + optional: true + nan: + optional: true + checksum: 10/0951c22d9c5a0e3b89a8e5ae890ebcbce9f1f94dbed37d1490e4e48e26bc8b074fa81f202ee57b708e31b5f33033f4c870b92047f4f02b6bc26c32225b01d84c + languageName: node + linkType: hard + "sshpk@npm:^1.7.0": version: 1.17.0 resolution: "sshpk@npm:1.17.0" @@ -34374,6 +34667,13 @@ __metadata: languageName: node linkType: hard +"string_decoder@npm:~0.10.x": + version: 0.10.31 + resolution: "string_decoder@npm:0.10.31" + checksum: 10/cc43e6b1340d4c7843da0e37d4c87a4084c2342fc99dcf6563c3ec273bb082f0cbd4ebf25d5da19b04fb16400d393885fda830be5128e1c416c73b5a6165f175 + languageName: node + linkType: hard + "string_decoder@npm:~1.1.1": version: 1.1.1 resolution: "string_decoder@npm:1.1.1" @@ -34611,7 +34911,7 @@ __metadata: languageName: node linkType: hard -"supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": +"supports-color@npm:^7, supports-color@npm:^7.0.0, supports-color@npm:^7.1.0, supports-color@npm:^7.2.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" dependencies: @@ -34838,7 +35138,46 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^2.1.4": +"tar-fs@npm:~1.16.3": + version: 1.16.3 + resolution: "tar-fs@npm:1.16.3" + dependencies: + chownr: "npm:^1.0.1" + mkdirp: "npm:^0.5.1" + pump: "npm:^1.0.0" + tar-stream: "npm:^1.1.2" + checksum: 10/d467267093920afad14040d7d72454aa3ea4895958311e98a0f76e553a83da82d118928cab2b83af35c16e69f0456fa3830ec3e755b084510ede7c2b6248af45 + languageName: node + linkType: hard + +"tar-fs@npm:~2.0.1": + version: 2.0.1 + resolution: "tar-fs@npm:2.0.1" + dependencies: + chownr: "npm:^1.1.1" + mkdirp-classic: "npm:^0.5.2" + pump: "npm:^3.0.0" + tar-stream: "npm:^2.0.0" + checksum: 10/85ceac6fce0e9175b5b67c0eca8864b7d29a940cae8b7657c60b66e8a252319d701c3df12814162a6839e6120f9e1975757293bdeaf294ad5b15721d236c4d32 + languageName: node + linkType: hard + +"tar-stream@npm:^1.1.2": + version: 1.6.2 + resolution: "tar-stream@npm:1.6.2" + dependencies: + bl: "npm:^1.0.0" + buffer-alloc: "npm:^1.2.0" + end-of-stream: "npm:^1.0.0" + fs-constants: "npm:^1.0.0" + readable-stream: "npm:^2.3.0" + to-buffer: "npm:^1.1.1" + xtend: "npm:^4.0.0" + checksum: 10/ac9b850bd40e6d4b251abcf92613bafd9fc9e592c220c781ebcdbb0ba76da22a245d9ea3ea638ad7168910e7e1ae5079333866cd679d2f1ffadb99c403f99d7f + languageName: node + linkType: hard + +"tar-stream@npm:^2.0.0, tar-stream@npm:^2.1.4": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -35140,6 +35479,13 @@ __metadata: languageName: node linkType: hard +"to-buffer@npm:^1.1.1": + version: 1.1.1 + resolution: "to-buffer@npm:1.1.1" + checksum: 10/8ade59fe04239b281496b6067bc83ad0371a3657552276cbd09ffffaeb3ad0018a28306d61b854b83280eabe1829cbc53001ccd761e834c6062cbcc7fee2766a + languageName: node + linkType: hard + "to-fast-properties@npm:^2.0.0": version: 2.0.0 resolution: "to-fast-properties@npm:2.0.0" @@ -37993,6 +38339,17 @@ __metadata: languageName: node linkType: hard +"zksync-ethers@npm:^5.10.0": + version: 5.10.0 + resolution: "zksync-ethers@npm:5.10.0" + dependencies: + ethers: "npm:~5.7.0" + peerDependencies: + ethers: ~5.7.0 + checksum: 10/826719e2e40731e1104cf8a0c16c758526de6ca9e907d0483eb5bd80b635f02e3cce012115b75d68976a8dd746d63d4f83d576cc3bddc18a02a49d2bc023347f + languageName: node + linkType: hard + "zksync-web3@npm:^0.14.3": version: 0.14.4 resolution: "zksync-web3@npm:0.14.4"