From 7d530fd4eb5d8256d1700713c54c3881d9d249c8 Mon Sep 17 00:00:00 2001 From: Paul Balaji Date: Thu, 22 Feb 2024 11:30:30 +0000 Subject: [PATCH] feat: verify with deployment (#3255) --- .changeset/green-pans-unite.md | 13 + Dockerfile | 2 +- solidity/.gitignore | 1 + solidity/exportBuildArtifact.sh | 34 ++ solidity/package.json | 3 +- solidity/tsconfig.json | 2 +- typescript/helloworld/src/deploy/deploy.ts | 7 +- typescript/infra/scripts/agent-utils.ts | 10 +- typescript/infra/scripts/deploy.ts | 62 ++- typescript/infra/scripts/verify.ts | 88 ++-- .../testcontracts/testquerysender.ts | 10 +- typescript/infra/src/deployment/verify.ts | 20 + .../sdk/src/core/HyperlaneCoreChecker.ts | 7 +- .../sdk/src/core/HyperlaneCoreDeployer.ts | 15 +- .../sdk/src/core/TestRecipientDeployer.ts | 7 +- .../sdk/src/deploy/HyperlaneDeployer.ts | 37 +- .../deploy/HyperlaneProxyFactoryDeployer.ts | 7 +- .../sdk/src/deploy/verify/ContractVerifier.ts | 366 +++++++------- .../verify/PostDeploymentContractVerifier.ts | 50 ++ typescript/sdk/src/deploy/verify/types.ts | 99 +++- .../sdk/src/gas/HyperlaneIgpDeployer.ts | 7 +- .../sdk/src/hook/HyperlaneHookDeployer.ts | 9 +- typescript/sdk/src/index.ts | 10 +- .../ism/HyperlaneIsmFactory.hardhat-test.ts | 7 +- typescript/sdk/src/ism/HyperlaneIsmFactory.ts | 463 ++---------------- typescript/sdk/src/ism/utils.ts | 415 ++++++++++++++++ .../sdk/src/metadata/ChainMetadataManager.ts | 6 +- .../account/InterchainAccountDeployer.ts | 10 +- .../LiquidityLayerRouterDeployer.ts | 10 +- .../query/InterchainQueryDeployer.ts | 10 +- .../sdk/src/router/HyperlaneRouterChecker.ts | 6 +- typescript/sdk/src/token/deploy.ts | 14 +- 32 files changed, 1077 insertions(+), 730 deletions(-) create mode 100644 .changeset/green-pans-unite.md create mode 100755 solidity/exportBuildArtifact.sh create mode 100644 typescript/infra/src/deployment/verify.ts create mode 100644 typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts create mode 100644 typescript/sdk/src/ism/utils.ts diff --git a/.changeset/green-pans-unite.md b/.changeset/green-pans-unite.md new file mode 100644 index 000000000..71791ee77 --- /dev/null +++ b/.changeset/green-pans-unite.md @@ -0,0 +1,13 @@ +--- +'@hyperlane-xyz/helloworld': minor +'@hyperlane-xyz/infra': minor +'@hyperlane-xyz/sdk': minor +'@hyperlane-xyz/core': minor +--- + +Enabled verification of contracts as part of the deployment flow. + +- Solidity build artifact is now included as part of the `@hyperlane-xyz/core` package. +- Updated the `HyperlaneDeployer` to perform contract verification immediately after deploying a contract. A default verifier is instantiated using the core build artifact. +- Updated the `HyperlaneIsmFactory` to re-use the `HyperlaneDeployer` for deployment where possible. +- Minor logging improvements throughout deployers. diff --git a/Dockerfile b/Dockerfile index a3e604961..446340248 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM node:18-alpine WORKDIR /hyperlane-monorepo -RUN apk add --update --no-cache git g++ make py3-pip +RUN apk add --update --no-cache git g++ make py3-pip jq RUN yarn set version 4.0.1 diff --git a/solidity/.gitignore b/solidity/.gitignore index fe81fe2bf..c3e113988 100644 --- a/solidity/.gitignore +++ b/solidity/.gitignore @@ -13,3 +13,4 @@ out forge-cache docs flattened/ +buildArtifact.json diff --git a/solidity/exportBuildArtifact.sh b/solidity/exportBuildArtifact.sh new file mode 100755 index 000000000..f7f64fca3 --- /dev/null +++ b/solidity/exportBuildArtifact.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +# set script location as working directory +cd "$(dirname "$0")" + +# Define the artifacts directory +artifactsDir="./artifacts/build-info" +# Define the output file +outputFile="./buildArtifact.json" + +# log that we're in the script +echo 'Finding and processing hardhat build artifact...' + +# Find most recently modified JSON build artifact +if [[ $OSTYPE == '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}' "$jsonFiles" > "$outputFile"; then + echo 'Finished processing build artifact.' +else + echo 'Failed to process build artifact with jq' + exit 1 +fi diff --git a/solidity/package.json b/solidity/package.json index 3e80f531b..a79f1d859 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -31,6 +31,7 @@ "test": "test" }, "files": [ + "/buildInfo.json", "/dist", "/contracts", "/interfaces", @@ -45,7 +46,7 @@ "main": "dist/index.js", "repository": "https://github.com/hyperlane-xyz/hyperlane-monorepo", "scripts": { - "build": "hardhat compile && tsc", + "build": "hardhat compile && ./exportBuildArtifact.sh && tsc", "lint": "solhint contracts/**/*.sol", "clean": "hardhat clean && rm -rf ./dist ./cache ./types ./coverage", "coverage": "./coverage.sh", diff --git a/solidity/tsconfig.json b/solidity/tsconfig.json index 3a0356c92..d5353f6c3 100644 --- a/solidity/tsconfig.json +++ b/solidity/tsconfig.json @@ -5,4 +5,4 @@ }, "exclude": ["./test", "hardhat.config.ts", "./dist"], "extends": "../tsconfig.json" -} \ No newline at end of file +} diff --git a/typescript/helloworld/src/deploy/deploy.ts b/typescript/helloworld/src/deploy/deploy.ts index 495b46cb5..8b9d67b89 100644 --- a/typescript/helloworld/src/deploy/deploy.ts +++ b/typescript/helloworld/src/deploy/deploy.ts @@ -2,6 +2,7 @@ import { ethers } from 'ethers'; import { ChainName, + ContractVerifier, HyperlaneContracts, HyperlaneIsmFactory, HyperlaneRouterDeployer, @@ -20,8 +21,12 @@ export class HelloWorldDeployer extends HyperlaneRouterDeployer< constructor( multiProvider: MultiProvider, readonly ismFactory?: HyperlaneIsmFactory, + readonly contractVerifier?: ContractVerifier, ) { - super(multiProvider, helloWorldFactories, { ismFactory }); + super(multiProvider, helloWorldFactories, { + ismFactory, + contractVerifier, + }); } router(contracts: HyperlaneContracts): HelloWorld { diff --git a/typescript/infra/scripts/agent-utils.ts b/typescript/infra/scripts/agent-utils.ts index 07f590be9..e41b19bf2 100644 --- a/typescript/infra/scripts/agent-utils.ts +++ b/typescript/infra/scripts/agent-utils.ts @@ -69,7 +69,7 @@ export function getArgs() { export function withModuleAndFork(args: yargs.Argv) { return args .choices('module', Object.values(Modules)) - .demandOption('module') + .demandOption('module', 'hyperlane module to deploy') .alias('m', 'module') .describe('fork', 'network to fork') .choices('fork', Object.values(Chains)) @@ -88,6 +88,7 @@ export function withContext(args: yargs.Argv) { .describe('context', 'deploy context') .default('context', Contexts.Hyperlane) .coerce('context', assertContext) + .alias('x', 'context') .demandOption('context'); } @@ -133,6 +134,13 @@ export function withMissingChains(args: yargs.Argv) { .alias('n', 'newChains'); } +export function withBuildArtifactPath(args: yargs.Argv) { + return args + .describe('buildArtifactPath', 'path to hardhat build artifact') + .string('buildArtifactPath') + .alias('b', 'buildArtifactPath'); +} + export function assertEnvironment(env: string): DeployEnvironment { if (EnvironmentNames.includes(env)) { return env as DeployEnvironment; diff --git a/typescript/infra/scripts/deploy.ts b/typescript/infra/scripts/deploy.ts index de9c932d6..b83d1019b 100644 --- a/typescript/infra/scripts/deploy.ts +++ b/typescript/infra/scripts/deploy.ts @@ -5,6 +5,8 @@ import { prompt } from 'prompts'; import { HelloWorldDeployer } from '@hyperlane-xyz/helloworld'; import { ChainMap, + ContractVerifier, + ExplorerLicenseType, HypERC20Deployer, HyperlaneCore, HyperlaneCoreDeployer, @@ -25,6 +27,10 @@ import { aggregationIsm } from '../config/routingIsm'; import { deployEnvToSdkEnv } from '../src/config/environment'; import { deployWithArtifacts } from '../src/deployment/deploy'; import { TestQuerySenderDeployer } from '../src/deployment/testcontracts/testquerysender'; +import { + extractBuildArtifact, + fetchExplorerApiKeys, +} from '../src/deployment/verify'; import { impersonateAccount, useLocalProvider } from '../src/utils/fork'; import { @@ -34,6 +40,7 @@ import { getArgs, getContractAddressesSdkFilepath, getModuleDirectory, + withBuildArtifactPath, withContext, withModuleAndFork, withNetwork, @@ -47,7 +54,10 @@ async function main() { fork, environment, network, - } = await withContext(withNetwork(withModuleAndFork(getArgs()))).argv; + buildArtifactPath, + } = await withContext( + withNetwork(withModuleAndFork(withBuildArtifactPath(getArgs()))), + ).argv; const envConfig = getEnvironmentConfig(environment); const env = deployEnvToSdkEnv[environment]; @@ -63,18 +73,40 @@ async function main() { multiProvider.setSharedSigner(signer); } + let contractVerifier; + if (buildArtifactPath) { + // fetch explorer API keys from GCP + const apiKeys = await fetchExplorerApiKeys(); + // extract build artifact contents + const buildArtifact = extractBuildArtifact(buildArtifactPath); + // instantiate verifier + contractVerifier = new ContractVerifier( + multiProvider, + apiKeys, + buildArtifact, + ExplorerLicenseType.MIT, + ); + } + let config: ChainMap = {}; let deployer: HyperlaneDeployer; if (module === Modules.PROXY_FACTORY) { config = objMap(envConfig.core, (_chain) => true); - deployer = new HyperlaneProxyFactoryDeployer(multiProvider); + deployer = new HyperlaneProxyFactoryDeployer( + multiProvider, + contractVerifier, + ); } else if (module === Modules.CORE) { config = envConfig.core; const ismFactory = HyperlaneIsmFactory.fromAddressesMap( getAddresses(environment, Modules.PROXY_FACTORY), multiProvider, ); - deployer = new HyperlaneCoreDeployer(multiProvider, ismFactory); + deployer = new HyperlaneCoreDeployer( + multiProvider, + ismFactory, + contractVerifier, + ); } else if (module === Modules.WARP) { const core = HyperlaneCore.fromEnvironment(env, multiProvider); const ismFactory = HyperlaneIsmFactory.fromAddressesMap( @@ -102,18 +134,22 @@ async function main() { plumetestnet, sepolia, }; - deployer = new HypERC20Deployer(multiProvider, ismFactory); + deployer = new HypERC20Deployer( + multiProvider, + ismFactory, + contractVerifier, + ); } else if (module === Modules.INTERCHAIN_GAS_PAYMASTER) { config = envConfig.igp; - deployer = new HyperlaneIgpDeployer(multiProvider); + deployer = new HyperlaneIgpDeployer(multiProvider, contractVerifier); } else if (module === Modules.INTERCHAIN_ACCOUNTS) { const core = HyperlaneCore.fromEnvironment(env, multiProvider); config = core.getRouterConfig(envConfig.owners); - deployer = new InterchainAccountDeployer(multiProvider); + deployer = new InterchainAccountDeployer(multiProvider, contractVerifier); } else if (module === Modules.INTERCHAIN_QUERY_SYSTEM) { const core = HyperlaneCore.fromEnvironment(env, multiProvider); config = core.getRouterConfig(envConfig.owners); - deployer = new InterchainQueryDeployer(multiProvider); + deployer = new InterchainQueryDeployer(multiProvider, contractVerifier); } else if (module === Modules.LIQUIDITY_LAYER) { const core = HyperlaneCore.fromEnvironment(env, multiProvider); const routerConfig = core.getRouterConfig(envConfig.owners); @@ -127,7 +163,7 @@ async function main() { ...routerConfig[chain], }), ); - deployer = new LiquidityLayerDeployer(multiProvider); + deployer = new LiquidityLayerDeployer(multiProvider, contractVerifier); } else if (module === Modules.TEST_RECIPIENT) { const addresses = getAddresses(environment, Modules.CORE); @@ -138,7 +174,7 @@ async function main() { ethers.constants.AddressZero, // ISM is required for the TestRecipientDeployer but onchain if the ISM is zero address, then it uses the mailbox's defaultISM }; } - deployer = new TestRecipientDeployer(multiProvider); + deployer = new TestRecipientDeployer(multiProvider, contractVerifier); } else if (module === Modules.TEST_QUERY_SENDER) { // Get query router addresses const queryAddresses = getAddresses( @@ -148,11 +184,15 @@ async function main() { config = objMap(queryAddresses, (_c, conf) => ({ queryRouterAddress: conf.router, })); - deployer = new TestQuerySenderDeployer(multiProvider); + deployer = new TestQuerySenderDeployer(multiProvider, contractVerifier); } else if (module === Modules.HELLO_WORLD) { const core = HyperlaneCore.fromEnvironment(env, multiProvider); config = core.getRouterConfig(envConfig.owners); - deployer = new HelloWorldDeployer(multiProvider); + deployer = new HelloWorldDeployer( + multiProvider, + undefined, + contractVerifier, + ); } else { console.log(`Skipping ${module}, deployer unimplemented`); return; diff --git a/typescript/infra/scripts/verify.ts b/typescript/infra/scripts/verify.ts index 56e020b16..4b75e6650 100644 --- a/typescript/infra/scripts/verify.ts +++ b/typescript/infra/scripts/verify.ts @@ -1,81 +1,67 @@ import { ChainMap, - CompilerOptions, - ContractVerifier, + ExplorerLicenseType, + PostDeploymentContractVerifier, VerificationInput, } from '@hyperlane-xyz/sdk'; -import { fetchGCPSecret } from '../src/utils/gcloud'; +import { + extractBuildArtifact, + fetchExplorerApiKeys, +} from '../src/deployment/verify'; import { readJSONAtPath } from '../src/utils/utils'; -import { assertEnvironment, getArgs } from './agent-utils'; +import { + assertEnvironment, + getArgs, + withBuildArtifactPath, + withNetwork, +} from './agent-utils'; import { getEnvironmentConfig } from './core-utils'; async function main() { - const argv = await getArgs() - .string('source') - .describe( - 'source', - 'Path to hardhat build artifact containing standard input JSON', - ) - .demandOption('source') - .string('artifacts') - .describe('artifacts', 'verification artifacts JSON file') - .demandOption('artifacts') - .string('network') - .describe('network', 'optional target network').argv; + const { environment, buildArtifactPath, verificationArtifactPath, network } = + await withNetwork(withBuildArtifactPath(getArgs())) + .string('verificationArtifactPath') + .describe( + 'verificationArtifactPath', + 'path to hyperlane verification artifact', + ) + .alias('v', 'verificationArtifactPath') + .demandOption('verificationArtifactPath') + .demandOption('buildArtifactPath').argv; - const environment = assertEnvironment(argv.e!); + // set up multiprovider + assertEnvironment(environment); const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); + // grab verification artifacts const verification: ChainMap = readJSONAtPath( - argv.artifacts!, + verificationArtifactPath, ); - const sourcePath = argv.source!; - if (!sourcePath.endsWith('.json')) { - throw new Error('Source must be a JSON file.'); - } - - const buildArtifactJson = readJSONAtPath(sourcePath); - const source = buildArtifactJson.input; - const solcLongVersion = buildArtifactJson.solcLongVersion; - - // codeformat always json - // compiler version inferred from build artifact - // always use MIT license - const compilerOptions: CompilerOptions = { - codeformat: 'solidity-standard-json-input', - compilerversion: `v${solcLongVersion}`, - licenseType: '3', - }; - - const versionRegex = /v(\d.\d.\d+)\+commit.\w+/; - const matches = versionRegex.exec(compilerOptions.compilerversion); - if (!matches) { - throw new Error( - `Invalid compiler version ${compilerOptions.compilerversion}`, - ); - } + // fetch explorer API keys from GCP + const apiKeys = await fetchExplorerApiKeys(); - const apiKeys: ChainMap = (await fetchGCPSecret( - 'explorer-api-keys', - true, - )) as any; + // extract build artifact contents + const buildArtifact = extractBuildArtifact(buildArtifactPath); - const verifier = new ContractVerifier( + // instantiate verifier + const verifier = new PostDeploymentContractVerifier( verification, multiProvider, apiKeys, - source, - compilerOptions, + buildArtifact, + ExplorerLicenseType.MIT, ); + // verify all the things const failedResults = ( - await verifier.verify(argv.network ? [argv.network] : undefined) + await verifier.verify(network ? [network] : undefined) ).filter((result) => result.status === 'rejected'); + // only log the failed verifications to console if (failedResults.length > 0) { console.error( 'Verification failed for the following contracts:', diff --git a/typescript/infra/src/deployment/testcontracts/testquerysender.ts b/typescript/infra/src/deployment/testcontracts/testquerysender.ts index 17c4c8b28..e7203856b 100644 --- a/typescript/infra/src/deployment/testcontracts/testquerysender.ts +++ b/typescript/infra/src/deployment/testcontracts/testquerysender.ts @@ -1,6 +1,7 @@ import { TestQuerySender__factory } from '@hyperlane-xyz/core'; import { ChainName, + ContractVerifier, HyperlaneDeployer, MultiProvider, } from '@hyperlane-xyz/sdk'; @@ -15,8 +16,13 @@ export class TestQuerySenderDeployer extends HyperlaneDeployer< TestQuerySenderConfig, typeof TEST_QUERY_SENDER_FACTORIES > { - constructor(multiProvider: MultiProvider) { - super(multiProvider, TEST_QUERY_SENDER_FACTORIES); + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { + super(multiProvider, TEST_QUERY_SENDER_FACTORIES, { + contractVerifier, + }); } async deployContracts(chain: ChainName, config: TestQuerySenderConfig) { diff --git a/typescript/infra/src/deployment/verify.ts b/typescript/infra/src/deployment/verify.ts new file mode 100644 index 000000000..7314c16fc --- /dev/null +++ b/typescript/infra/src/deployment/verify.ts @@ -0,0 +1,20 @@ +import { BuildArtifact, ChainMap } from '@hyperlane-xyz/sdk'; + +import { fetchGCPSecret } from '../utils/gcloud'; +import { readJSONAtPath } from '../utils/utils'; + +// read build artifact from given path +export function extractBuildArtifact(buildArtifactPath: string): BuildArtifact { + // check provided artifact is JSON + if (!buildArtifactPath.endsWith('.json')) { + throw new Error('Source must be a JSON file.'); + } + + // return as BuildArtifact + return readJSONAtPath(buildArtifactPath) as BuildArtifact; +} + +// fetch explorer API keys from GCP +export async function fetchExplorerApiKeys(): Promise> { + return (await fetchGCPSecret('explorer-api-keys', true)) as any; +} diff --git a/typescript/sdk/src/core/HyperlaneCoreChecker.ts b/typescript/sdk/src/core/HyperlaneCoreChecker.ts index 108e6361e..b1e3fabd6 100644 --- a/typescript/sdk/src/core/HyperlaneCoreChecker.ts +++ b/typescript/sdk/src/core/HyperlaneCoreChecker.ts @@ -5,11 +5,8 @@ import { Address, assert, eqAddress } from '@hyperlane-xyz/utils'; import { BytecodeHash } from '../consts/bytecode'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker'; import { proxyImplementation } from '../deploy/proxy'; -import { - HyperlaneIsmFactory, - collectValidators, - moduleMatchesConfig, -} from '../ism/HyperlaneIsmFactory'; +import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory'; +import { collectValidators, moduleMatchesConfig } from '../ism/utils'; import { MultiProvider } from '../providers/MultiProvider'; import { ChainMap, ChainName } from '../types'; diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts index 2e338188a..41bc9fef9 100644 --- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts +++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts @@ -10,13 +10,12 @@ import { Address } from '@hyperlane-xyz/utils'; import { HyperlaneContracts } from '../contracts/types'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; +import { ContractVerifier } from '../deploy/verify/ContractVerifier'; import { HyperlaneHookDeployer } from '../hook/HyperlaneHookDeployer'; import { HookConfig } from '../hook/types'; -import { - HyperlaneIsmFactory, - moduleMatchesConfig, -} from '../ism/HyperlaneIsmFactory'; +import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory'; import { IsmConfig } from '../ism/types'; +import { moduleMatchesConfig } from '../ism/utils'; import { MultiProvider } from '../providers/MultiProvider'; import { ChainMap, ChainName } from '../types'; @@ -37,18 +36,24 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< constructor( multiProvider: MultiProvider, readonly ismFactory: HyperlaneIsmFactory, + contractVerifier?: ContractVerifier, ) { super(multiProvider, coreFactories, { logger: debug('hyperlane:CoreDeployer'), chainTimeoutMs: 1000 * 60 * 10, // 10 minutes ismFactory, + contractVerifier, }); this.hookDeployer = new HyperlaneHookDeployer( multiProvider, {}, ismFactory, + contractVerifier, + ); + this.testRecipient = new TestRecipientDeployer( + multiProvider, + contractVerifier, ); - this.testRecipient = new TestRecipientDeployer(multiProvider); } cacheAddressesMap(addressesMap: ChainMap): void { diff --git a/typescript/sdk/src/core/TestRecipientDeployer.ts b/typescript/sdk/src/core/TestRecipientDeployer.ts index 6c5dc4246..90f7c12bb 100644 --- a/typescript/sdk/src/core/TestRecipientDeployer.ts +++ b/typescript/sdk/src/core/TestRecipientDeployer.ts @@ -4,6 +4,7 @@ import { TestRecipient, TestRecipient__factory } from '@hyperlane-xyz/core'; import { Address, eqAddress } from '@hyperlane-xyz/utils'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; +import { ContractVerifier } from '../deploy/verify/ContractVerifier'; import { MultiProvider } from '../providers/MultiProvider'; import { ChainName } from '../types'; @@ -28,9 +29,13 @@ export class TestRecipientDeployer extends HyperlaneDeployer< TestRecipientConfig, typeof testRecipientFactories > { - constructor(multiProvider: MultiProvider) { + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { super(multiProvider, testRecipientFactories, { logger: debug('hyperlane:TestRecipientDeployer'), + contractVerifier, }); } diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index 379bc2e93..c0370701f 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -13,6 +13,7 @@ import { TimelockController__factory, TransparentUpgradeableProxy__factory, } from '@hyperlane-xyz/core'; +import SdkBuildArtifact from '@hyperlane-xyz/core/buildArtifact.json'; import { Address, ProtocolType, @@ -26,11 +27,9 @@ import { HyperlaneContractsMap, HyperlaneFactories, } from '../contracts/types'; -import { - HyperlaneIsmFactory, - moduleMatchesConfig, -} from '../ism/HyperlaneIsmFactory'; +import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory'; import { IsmConfig } from '../ism/types'; +import { moduleMatchesConfig } from '../ism/utils'; import { MultiProvider } from '../providers/MultiProvider'; import { MailboxClientConfig } from '../router/types'; import { ChainMap, ChainName } from '../types'; @@ -43,7 +42,8 @@ import { proxyImplementation, } from './proxy'; import { OwnableConfig } from './types'; -import { ContractVerificationInput } from './verify/types'; +import { ContractVerifier } from './verify/ContractVerifier'; +import { ContractVerificationInput, ExplorerLicenseType } from './verify/types'; import { buildVerificationInput, getContractVerificationInput, @@ -53,6 +53,7 @@ export interface DeployerOptions { logger?: Debugger; chainTimeoutMs?: number; ismFactory?: HyperlaneIsmFactory; + contractVerifier?: ContractVerifier; } export abstract class HyperlaneDeployer< @@ -70,11 +71,20 @@ export abstract class HyperlaneDeployer< constructor( protected readonly multiProvider: MultiProvider, protected readonly factories: Factories, - protected readonly options?: DeployerOptions, + protected readonly options: DeployerOptions = {}, protected readonly recoverVerificationInputs = false, ) { this.logger = options?.logger ?? debug('hyperlane:deployer'); this.chainTimeoutMs = options?.chainTimeoutMs ?? 5 * 60 * 1000; // 5 minute timeout per chain + this.options.ismFactory?.setDeployer(this); + + // if none provided, instantiate a default verifier with SDK's included build artifact + this.options.contractVerifier ??= new ContractVerifier( + multiProvider, + {}, + SdkBuildArtifact, + ExplorerLicenseType.MIT, + ); } cacheAddressesMap(addressesMap: HyperlaneAddressesMap): void { @@ -211,7 +221,7 @@ export abstract class HyperlaneDeployer< } } else { const ismFactory = - this.options?.ismFactory ?? + this.options.ismFactory ?? (() => { throw new Error('No ISM factory provided'); })(); @@ -305,7 +315,7 @@ export abstract class HyperlaneDeployer< this.logger(`Mailbox client on ${local} initialized...`); } - protected async deployContractFromFactory( + public async deployContractFromFactory( chain: ChainName, factory: F, contractName: string, @@ -351,6 +361,17 @@ export abstract class HyperlaneDeployer< ); this.addVerificationArtifacts(chain, [verificationInput]); + // try verifying contract + try { + await this.options.contractVerifier?.verifyContract( + chain, + verificationInput, + ); + } catch (error) { + // log error but keep deploying, can also verify post-deployment if needed + this.logger(`Error verifying contract: ${error}`); + } + return contract; } diff --git a/typescript/sdk/src/deploy/HyperlaneProxyFactoryDeployer.ts b/typescript/sdk/src/deploy/HyperlaneProxyFactoryDeployer.ts index 37a0543a0..bf860a9ef 100644 --- a/typescript/sdk/src/deploy/HyperlaneProxyFactoryDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneProxyFactoryDeployer.ts @@ -10,14 +10,19 @@ import { proxyFactoryFactories, proxyFactoryImplementations, } from './contracts'; +import { ContractVerifier } from './verify/ContractVerifier'; export class HyperlaneProxyFactoryDeployer extends HyperlaneDeployer< {}, ProxyFactoryFactories > { - constructor(multiProvider: MultiProvider) { + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { super(multiProvider, proxyFactoryFactories, { logger: debug('hyperlane:IsmFactoryDeployer'), + contractVerifier, }); } diff --git a/typescript/sdk/src/deploy/verify/ContractVerifier.ts b/typescript/sdk/src/deploy/verify/ContractVerifier.ts index 2e408a67e..d958ae6ed 100644 --- a/typescript/sdk/src/deploy/verify/ContractVerifier.ts +++ b/typescript/sdk/src/deploy/verify/ContractVerifier.ts @@ -7,243 +7,238 @@ import { sleep, strip0x } from '@hyperlane-xyz/utils'; import { ExplorerFamily } from '../../metadata/chainMetadataTypes'; import { MultiProvider } from '../../providers/MultiProvider'; import { ChainMap, ChainName } from '../../types'; -import { MultiGeneric } from '../../utils/MultiGeneric'; import { + BuildArtifact, CompilerOptions, ContractVerificationInput, - VerificationInput, + EXPLORER_GET_ACTIONS, + ExplorerApiActions, + ExplorerApiErrors, + FormOptions, } from './types'; -enum ExplorerApiActions { - GETSOURCECODE = 'getsourcecode', - VERIFY_IMPLEMENTATION = 'verifysourcecode', - MARK_PROXY = 'verifyproxycontract', - CHECK_STATUS = 'checkverifystatus', - CHECK_PROXY_STATUS = 'checkproxyverification', -} +export class ContractVerifier { + private logger = debug(`hyperlane:ContractVerifier`); -enum ExplorerApiErrors { - ALREADY_VERIFIED = 'Contract source code already verified', - ALREADY_VERIFIED_ALT = 'Already Verified', - VERIFICATION_PENDING = 'Pending in queue', - PROXY_FAILED = 'A corresponding implementation contract was unfortunately not detected for the proxy address.', - BYTECODE_MISMATCH = 'Fail - Unable to verify. Compiled contract deployment bytecode does NOT match the transaction deployment bytecode.', -} + private contractSourceMap: { [contractName: string]: string } = {}; -export class ContractVerifier extends MultiGeneric { - protected logger: Debugger; + protected readonly standardInputJson: string; + protected readonly compilerOptions: CompilerOptions; constructor( - verificationInputs: ChainMap, protected readonly multiProvider: MultiProvider, protected readonly apiKeys: ChainMap, - protected readonly source: string, // solidity standard input json - protected readonly compilerOptions: CompilerOptions, + buildArtifact: BuildArtifact, + licenseType: CompilerOptions['licenseType'], ) { - super(verificationInputs); - this.logger = debug('hyperlane:ContractVerifier'); - } + // Extract the standard input json and compiler version from the build artifact + this.standardInputJson = JSON.stringify(buildArtifact.input); + 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}`); + } - verify(targets = this.chains()): Promise[]> { - return Promise.allSettled( - targets.map((chain) => { - const { family } = this.multiProvider.getExplorerApi(chain); - if (family === ExplorerFamily.Other) { - this.logger( - `Skipping verification for ${chain} due to unsupported explorer family.`, - ); - return Promise.resolve(); + // 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; + } } - return this.verifyChain(chain, this.get(chain)); - }), + }, ); } - async verifyChain( - chain: ChainName, - inputs: VerificationInput, - ): Promise { - this.logger(`Verifying ${chain}...`); - for (const input of inputs) { - await this.verifyContract(chain, input); - } - } - private async submitForm( chain: ChainName, action: ExplorerApiActions, - options?: Record, + verificationLogger: Debugger, + options?: FormOptions, ): Promise { const { apiUrl, family } = this.multiProvider.getExplorerApi(chain); - const isGetRequest = - action === ExplorerApiActions.CHECK_STATUS || - action === ExplorerApiActions.CHECK_PROXY_STATUS || - action === ExplorerApiActions.GETSOURCECODE; - const params = new URLSearchParams({ - ...(this.apiKeys[chain] ? { apikey: this.apiKeys[chain] } : {}), - module: 'contract', - action, - ...options, - }); - - let response: Response; - if (isGetRequest) { - response = await fetch(`${apiUrl}?${params}`); - } else { - // For Blockscout explorers, we need to ensure module and action are always query params - if (family === ExplorerFamily.Blockscout) { - const formData = new URLSearchParams(); - const urlWithParams = new URL(apiUrl); - urlWithParams.searchParams.append('module', 'contract'); - urlWithParams.searchParams.append('action', action); - - // remove any extraneous keys from body - for (const [key, value] of params) { - switch (key) { - case 'module': - case 'action': - case 'licenseType': - case 'apikey': - break; - default: - formData.append(key, value); - break; - } - } + const params = new URLSearchParams(); + params.set('module', 'contract'); + params.set('action', action); - response = await fetch(urlWithParams.toString(), { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: formData, - }); - } else { - response = await fetch(apiUrl, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - body: params, - }); - } + // no need to provide every argument for every request + for (const [key, value] of Object.entries(options ?? {})) { + params.set(key, value); } - let result; - let responseText; - try { - responseText = await response.text(); - result = JSON.parse(responseText); - } catch (e) { - this.logger( - `[${chain}] Failed to parse response from ${responseText}`, - e, - ); + // only include apikey if provided & not blockscout + if (family !== ExplorerFamily.Blockscout && this.apiKeys[chain]) { + params.set('apikey', this.apiKeys[chain]); + } + + const url = new URL(apiUrl); + const isGetRequest = EXPLORER_GET_ACTIONS.includes(action); + if (isGetRequest) { + url.search = params.toString(); + } else if (family === ExplorerFamily.Blockscout) { + // Blockscout requires module and action to be query params + url.searchParams.set('module', 'contract'); + url.searchParams.set('action', action); + } + + let response; + if (isGetRequest) { + response = await fetch(url.toString(), { + method: 'GET', + }); + } else { + response = await fetch(url.toString(), { + method: 'POST', + headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, + body: params, + }); } + + const responseText = await response.text(); + const result = JSON.parse(responseText); + if (result.message !== 'OK') { - const errorMessageBase = `[${chain}]`; let errorMessage; switch (result.result) { case ExplorerApiErrors.VERIFICATION_PENDING: await sleep(5000); // wait 5 seconds - return this.submitForm(chain, action, options); + return this.submitForm(chain, action, verificationLogger, options); case ExplorerApiErrors.ALREADY_VERIFIED: case ExplorerApiErrors.ALREADY_VERIFIED_ALT: return; case ExplorerApiErrors.PROXY_FAILED: - errorMessage = `${errorMessageBase} Proxy verification failed, try manually?`; + errorMessage = 'Proxy verification failed, try manually?'; break; case ExplorerApiErrors.BYTECODE_MISMATCH: - errorMessage = `${errorMessageBase} Compiled bytecode does not match deployed bytecode, check constructor arguments?`; + errorMessage = + 'Compiled bytecode does not match deployed bytecode, check constructor arguments?'; break; default: - errorMessage = `${errorMessageBase} Verification failed. ${ + errorMessage = `Verification failed. ${ result.result ?? response.statusText }`; break; } if (errorMessage) { - this.logger(errorMessage); - throw new Error(errorMessage); + verificationLogger(errorMessage); + throw new Error(`[${chain}] ${errorMessage}`); } } + if (result.result === ExplorerApiErrors.UNKNOWN_UID) { + await sleep(1000); // wait 1 second + return this.submitForm(chain, action, verificationLogger, options); + } + + if (result.result === ExplorerApiErrors.UNABLE_TO_VERIFY) { + const errorMessage = `Verification failed. ${ + result.result ?? response.statusText + }`; + verificationLogger(errorMessage); + throw new Error(`[${chain}] ${errorMessage}`); + } + return result.result; } private async isAlreadyVerified( chain: ChainName, input: ContractVerificationInput, - ) { + verificationLogger: Debugger, + ): Promise { try { const result = await this.submitForm( chain, ExplorerApiActions.GETSOURCECODE, + verificationLogger, { - ...this.compilerOptions, address: input.address, }, ); return !!result[0]?.SourceCode; } catch (error) { - this.logger( - `[${chain}] [${input.name}] Error checking if contract is already verified: ${error}`, + verificationLogger( + `Error checking if contract is already verified: ${error}`, ); return false; } } - async verifyProxy( + private async verifyProxy( chain: ChainName, input: ContractVerificationInput, + verificationLogger: Debugger, ): Promise { - if (input.isProxy) { - try { - const proxyGuid = await this.submitForm( - chain, - ExplorerApiActions.MARK_PROXY, - { - address: input.address, - }, - ); - - const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl( - chain, - input.address, - ); - - // poll for verified proxy status - if (proxyGuid) { - await this.submitForm(chain, ExplorerApiActions.CHECK_PROXY_STATUS, { - guid: proxyGuid, - }); - this.logger( - `[${chain}] [${input.name}] Successfully verified proxy ${addressUrl}#readProxyContract`, - ); - } - } catch (error) { - console.error( - `[${chain}] [${input.name}] Verification of proxy at ${input.address} failed`, - ); - throw error; - } + if (!input.isProxy) return; + + try { + const proxyGuid = await this.submitForm( + chain, + ExplorerApiActions.MARK_PROXY, + verificationLogger, + { address: input.address }, + ); + if (!proxyGuid) return; + + await this.submitForm( + chain, + ExplorerApiActions.CHECK_PROXY_STATUS, + verificationLogger, + { + guid: proxyGuid, + }, + ); + const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl( + chain, + input.address, + ); + verificationLogger( + `Successfully verified proxy ${addressUrl}#readProxyContract`, + ); + } catch (error) { + verificationLogger( + `Verification of proxy at ${input.address} failed: ${error}`, + ); + throw error; } } - async verifyImplementation( + private async verifyImplementation( chain: ChainName, input: ContractVerificationInput, + verificationLogger: Debugger, ): Promise { - this.logger( - `[${chain}] [${input.name}] Verifying implementation at ${input.address}`, - ); + verificationLogger(`Verifying implementation at ${input.address}`); + + const sourceName = this.contractSourceMap[input.name]; + if (!sourceName) { + const errorMessage = `Contract '${input.name}' not found in provided build artifact`; + verificationLogger(errorMessage); + throw new Error(`[${chain}] ${errorMessage}`); + } const data = { - sourceCode: this.source, - contractname: input.name, + sourceCode: this.standardInputJson, + contractname: `${sourceName}:${input.name}`, contractaddress: input.address, // TYPO IS ENFORCED BY API constructorArguements: strip0x(input.constructorArguments ?? ''), @@ -253,59 +248,64 @@ export class ContractVerifier extends MultiGeneric { const guid = await this.submitForm( chain, ExplorerApiActions.VERIFY_IMPLEMENTATION, + verificationLogger, data, ); + if (!guid) return; + await this.submitForm( + chain, + ExplorerApiActions.CHECK_STATUS, + verificationLogger, + { guid }, + ); const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl( chain, input.address, ); - - // poll for verified status - if (guid) { - try { - await this.submitForm(chain, ExplorerApiActions.CHECK_STATUS, { guid }); - this.logger( - `[${chain}] [${input.name}] Successfully verified ${addressUrl}#code`, - ); - } catch (error) { - console.error( - `[${chain}] [${input.name}] Verifying implementation at ${input.address} failed`, - ); - throw error; - } - } + verificationLogger(`Successfully verified ${addressUrl}#code`); } async verifyContract( chain: ChainName, input: ContractVerificationInput, + logger = this.logger, ): Promise { - if (input.address === ethers.constants.AddressZero) { + const verificationLogger = logger.extend(`${chain}:${input.name}`); + + const explorerApi = this.multiProvider.tryGetExplorerApi(chain); + if (!explorerApi) { + verificationLogger('No explorer API set, skipping'); return; } + if (!explorerApi.family) { + verificationLogger(`No explorer family set, skipping`); + return; + } + + if (explorerApi.family === ExplorerFamily.Other) { + verificationLogger(`Unsupported explorer family, skipping`); + return; + } + + if (input.address === ethers.constants.AddressZero) return; if (Array.isArray(input.constructorArguments)) { - this.logger( - `[${chain}] [${input.name}] Constructor arguments in legacy format, skipping`, - ); + verificationLogger('Constructor arguments in legacy format, skipping'); return; } - if (await this.isAlreadyVerified(chain, input)) { + if (await this.isAlreadyVerified(chain, input, verificationLogger)) { const addressUrl = await this.multiProvider.tryGetExplorerAddressUrl( chain, input.address, ); - this.logger( - `[${chain}] [${input.name}] Contract already verified at ${addressUrl}#code`, - ); - // There is a rate limit of 5 requests per second - await sleep(200); + verificationLogger(`Contract already verified at ${addressUrl}#code`); + await sleep(200); // There is a rate limit of 5 requests per second return; - } else { - await this.verifyImplementation(chain, input); } - await this.verifyProxy(chain, input); + + await this.verifyImplementation(chain, input, verificationLogger); + await this.verifyProxy(chain, input, verificationLogger); } } diff --git a/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts b/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts new file mode 100644 index 000000000..22cb991ab --- /dev/null +++ b/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts @@ -0,0 +1,50 @@ +import { debug } from 'debug'; + +import { ExplorerFamily } from '../../metadata/chainMetadataTypes'; +import { MultiProvider } from '../../providers/MultiProvider'; +import { ChainMap } from '../../types'; +import { MultiGeneric } from '../../utils/MultiGeneric'; + +import { ContractVerifier } from './ContractVerifier'; +import { BuildArtifact, CompilerOptions, VerificationInput } from './types'; + +export class PostDeploymentContractVerifier extends MultiGeneric { + protected logger = debug('hyperlane:PostDeploymentContractVerifier'); + protected readonly contractVerifier: ContractVerifier; + + constructor( + verificationInputs: ChainMap, + protected readonly multiProvider: MultiProvider, + apiKeys: ChainMap, + buildArtifact: BuildArtifact, + licenseType: CompilerOptions['licenseType'], + ) { + super(verificationInputs); + this.contractVerifier = new ContractVerifier( + multiProvider, + apiKeys, + buildArtifact, + licenseType, + ); + } + + verify(targets = this.chains()): Promise[]> { + return Promise.allSettled( + targets.map(async (chain) => { + // can check explorer family here to avoid doing these checks for each input in verifier + const { family } = this.multiProvider.getExplorerApi(chain); + if (family === ExplorerFamily.Other) { + this.logger( + `Skipping verification for ${chain} due to unsupported explorer family.`, + ); + return; + } + + this.logger(`Verifying ${chain}...`); + for (const input of this.get(chain)) { + await this.contractVerifier.verifyContract(chain, input, this.logger); + } + }), + ); + } +} diff --git a/typescript/sdk/src/deploy/verify/types.ts b/typescript/sdk/src/deploy/verify/types.ts index 27bd5c7ab..442ad5e37 100644 --- a/typescript/sdk/src/deploy/verify/types.ts +++ b/typescript/sdk/src/deploy/verify/types.ts @@ -7,22 +7,89 @@ export type ContractVerificationInput = { export type VerificationInput = ContractVerificationInput[]; +export type SolidityStandardJsonInput = { + sources: { + [sourceName: string]: { + content: string; + }; + }; +}; + +export type BuildArtifact = { + input: SolidityStandardJsonInput; + solcLongVersion: string; +}; + +// see https://etherscan.io/contract-license-types +export enum ExplorerLicenseType { + NO_LICENSE = '1', + UNLICENSED = '2', + MIT = '3', + GPL2 = '4', + GPL3 = '5', + LGPL2 = '6', + LGPL3 = '7', + BSD2 = '8', + BSD3 = '9', + MPL2 = '10', + OSL3 = '11', + APACHE2 = '12', + AGPL3 = '13', + BSL = '14', +} + export type CompilerOptions = { codeformat: 'solidity-standard-json-input'; - compilerversion: string; // see https://etherscan.io/solcversions for list of support versions, inferred from build artifact - licenseType: - | '1' - | '2' - | '3' - | '4' - | '5' - | '6' - | '7' - | '8' - | '9' - | '10' - | '11' - | '12' - | '13' - | '14'; // integer from 1-14, see https://etherscan.io/contract-license-types + compilerversion: string; // see https://etherscan.io/solcversions for list of support versions + licenseType?: ExplorerLicenseType; }; + +export enum ExplorerApiActions { + GETSOURCECODE = 'getsourcecode', + VERIFY_IMPLEMENTATION = 'verifysourcecode', + MARK_PROXY = 'verifyproxycontract', + CHECK_STATUS = 'checkverifystatus', + CHECK_PROXY_STATUS = 'checkproxyverification', +} + +export const EXPLORER_GET_ACTIONS = [ + ExplorerApiActions.CHECK_STATUS, + ExplorerApiActions.CHECK_PROXY_STATUS, + ExplorerApiActions.GETSOURCECODE, +]; + +export enum ExplorerApiErrors { + ALREADY_VERIFIED = 'Contract source code already verified', + ALREADY_VERIFIED_ALT = 'Already Verified', + VERIFICATION_PENDING = 'Pending in queue', + PROXY_FAILED = 'A corresponding implementation contract was unfortunately not detected for the proxy address.', + BYTECODE_MISMATCH = 'Fail - Unable to verify. Compiled contract deployment bytecode does NOT match the transaction deployment bytecode.', + UNABLE_TO_VERIFY = 'Fail - Unable to verify', + UNKNOWN_UID = 'Unknown UID', +} + +export type FormOptions = + Action extends ExplorerApiActions.GETSOURCECODE + ? { + address: string; + } + : Action extends ExplorerApiActions.VERIFY_IMPLEMENTATION + ? CompilerOptions & { + contractaddress: string; + sourceCode: string; + contractname: string; + constructorArguements?: string; // TYPO IS ENFORCED BY API + } + : Action extends ExplorerApiActions.MARK_PROXY + ? { + address: string; + } + : Action extends ExplorerApiActions.CHECK_STATUS + ? { + guid: string; + } + : Action extends ExplorerApiActions.CHECK_PROXY_STATUS + ? { + guid: string; + } + : never; diff --git a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts index fd82c9046..3840a0912 100644 --- a/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts +++ b/typescript/sdk/src/gas/HyperlaneIgpDeployer.ts @@ -9,6 +9,7 @@ import { eqAddress } from '@hyperlane-xyz/utils'; import { HyperlaneContracts } from '../contracts/types'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; +import { ContractVerifier } from '../deploy/verify/ContractVerifier'; import { MultiProvider } from '../providers/MultiProvider'; import { ChainName } from '../types'; @@ -20,9 +21,13 @@ export class HyperlaneIgpDeployer extends HyperlaneDeployer< IgpConfig, IgpFactories > { - constructor(multiProvider: MultiProvider) { + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { super(multiProvider, igpFactories, { logger: debug('hyperlane:IgpDeployer'), + contractVerifier, }); } diff --git a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts index 8a92ae66b..30e055c0b 100644 --- a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts +++ b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts @@ -15,6 +15,7 @@ import { Address, addressToBytes32 } from '@hyperlane-xyz/utils'; import { HyperlaneContracts } from '../contracts/types'; import { CoreAddresses } from '../core/contracts'; import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; +import { ContractVerifier } from '../deploy/verify/ContractVerifier'; import { HyperlaneIgpDeployer } from '../gas/HyperlaneIgpDeployer'; import { IgpFactories } from '../gas/contracts'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory'; @@ -42,10 +43,15 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< multiProvider: MultiProvider, readonly core: ChainMap>, readonly ismFactory: HyperlaneIsmFactory, - readonly igpDeployer = new HyperlaneIgpDeployer(multiProvider), + contractVerifier?: ContractVerifier, + readonly igpDeployer = new HyperlaneIgpDeployer( + multiProvider, + contractVerifier, + ), ) { super(multiProvider, hookFactories, { logger: debug('hyperlane:HookDeployer'), + contractVerifier, }); } @@ -161,6 +167,7 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< chain, this.ismFactory.getContracts(chain).aggregationHookFactory, aggregatedHooks, + this.logger, ); hooks[HookType.AGGREGATION] = StaticAggregationHook__factory.connect( address, diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index e9db2dac1..e8592a1bf 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -86,11 +86,14 @@ export { OwnerViolation, ViolationType, } from './deploy/types'; +export { PostDeploymentContractVerifier } from './deploy/verify/PostDeploymentContractVerifier'; export { ContractVerifier } from './deploy/verify/ContractVerifier'; export { CompilerOptions, ContractVerificationInput, VerificationInput, + ExplorerLicenseType, + BuildArtifact, } from './deploy/verify/types'; export * as verificationUtils from './deploy/verify/utils'; export { HyperlaneIgp } from './gas/HyperlaneIgp'; @@ -132,11 +135,8 @@ export { PausableHookConfig, ProtocolFeeHookConfig, } from './hook/types'; -export { - HyperlaneIsmFactory, - collectValidators, - moduleCanCertainlyVerify, -} from './ism/HyperlaneIsmFactory'; +export { HyperlaneIsmFactory } from './ism/HyperlaneIsmFactory'; +export { collectValidators, moduleCanCertainlyVerify } from './ism/utils'; export { buildAggregationIsmConfigs, buildMultisigIsmConfigs, diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts index 0541d7d75..79c4b30a5 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts @@ -11,10 +11,7 @@ import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDe import { MultiProvider } from '../providers/MultiProvider'; import { randomAddress, randomInt } from '../test/testUtils'; -import { - HyperlaneIsmFactory, - moduleMatchesConfig, -} from './HyperlaneIsmFactory'; +import { HyperlaneIsmFactory } from './HyperlaneIsmFactory'; import { AggregationIsmConfig, IsmConfig, @@ -23,6 +20,7 @@ import { MultisigIsmConfig, RoutingIsmConfig, } from './types'; +import { moduleMatchesConfig } from './utils'; function randomModuleType(): ModuleType { const choices = [ @@ -319,6 +317,7 @@ describe('HyperlaneIsmFactory', async () => { ), multiProvider, ); + new TestCoreDeployer(multiProvider, ismFactory); ism = await ismFactory.deploy({ destination: chain, config: exampleRoutingConfig, diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index bcfc2a4b6..b51c1131f 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -1,4 +1,4 @@ -import { debug } from 'debug'; +import debug, { Debugger } from 'debug'; import { ethers } from 'ethers'; import { @@ -12,13 +12,9 @@ import { IMultisigIsm, IMultisigIsm__factory, IRoutingIsm, - IRoutingIsm__factory, - MailboxClient__factory, - OPStackIsm, OPStackIsm__factory, PausableIsm__factory, StaticAddressSetFactory, - StaticAggregationIsm__factory, StaticThresholdAddressSetFactory, TestIsm__factory, } from '@hyperlane-xyz/core'; @@ -26,10 +22,7 @@ import { Address, Domain, eqAddress, - formatMessage, - normalizeAddress, objFilter, - objMap, warn, } from '@hyperlane-xyz/utils'; @@ -39,12 +32,12 @@ import { hyperlaneEnvironments, } from '../consts/environments'; import { appFromAddressesMapHelper } from '../contracts/contracts'; -import { HyperlaneAddressesMap, HyperlaneContracts } from '../contracts/types'; +import { HyperlaneAddressesMap } from '../contracts/types'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer'; import { ProxyFactoryFactories, proxyFactoryFactories, } from '../deploy/contracts'; -import { logger } from '../logger'; import { MultiProvider } from '../providers/MultiProvider'; import { ChainMap, ChainName } from '../types'; @@ -54,13 +47,11 @@ import { DeployedIsmType, IsmConfig, IsmType, - ModuleType, MultisigIsmConfig, - OpStackIsmConfig, RoutingIsmConfig, RoutingIsmDelta, - ismTypeToModuleType, } from './types'; +import { routingModuleDelta } from './utils'; export class HyperlaneIsmFactory extends HyperlaneApp { // The shape of this object is `ChainMap
`, @@ -68,6 +59,11 @@ export class HyperlaneIsmFactory extends HyperlaneApp { // TODO: fix this in the next refactoring public deployedIsms: ChainMap = {}; + protected deployer?: HyperlaneDeployer; + setDeployer(deployer: HyperlaneDeployer): void { + this.deployer = deployer; + } + static fromEnvironment( env: Env, multiProvider: MultiProvider, @@ -113,7 +109,9 @@ export class HyperlaneIsmFactory extends HyperlaneApp { } const ismType = config.type; - this.logger( + const logger = this.logger.extend(`${destination}:${ismType}`); + + logger( `Deploying ${ismType} to ${destination} ${ origin ? `(for verifying ${origin})` : '' }`, @@ -123,7 +121,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { switch (ismType) { case IsmType.MESSAGE_ID_MULTISIG: case IsmType.MERKLE_ROOT_MULTISIG: - contract = await this.deployMultisigIsm(destination, config); + contract = await this.deployMultisigIsm(destination, config, logger); break; case IsmType.ROUTING: case IsmType.FALLBACK_ROUTING: @@ -133,6 +131,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { origin, mailbox, existingIsmAddress, + logger, }); break; case IsmType.AGGREGATION: @@ -141,22 +140,39 @@ export class HyperlaneIsmFactory extends HyperlaneApp { config, origin, mailbox, + logger, }); break; case IsmType.OP_STACK: - contract = await this.deployOpStackIsm(destination, config); + if (!this.deployer) { + throw new Error(`HyperlaneDeployer must be set to deploy ${ismType}`); + } + contract = await this.deployer?.deployContractFromFactory( + destination, + new OPStackIsm__factory(), + IsmType.OP_STACK, + [config.nativeBridge], + ); break; case IsmType.PAUSABLE: - contract = await this.multiProvider.handleDeploy( + if (!this.deployer) { + throw new Error(`HyperlaneDeployer must be set to deploy ${ismType}`); + } + contract = await this.deployer?.deployContractFromFactory( destination, new PausableIsm__factory(), + IsmType.PAUSABLE, [config.owner], ); break; case IsmType.TEST_ISM: - contract = await this.multiProvider.handleDeploy( + if (!this.deployer) { + throw new Error(`HyperlaneDeployer must be set to deploy ${ismType}`); + } + contract = await this.deployer?.deployContractFromFactory( destination, new TestIsm__factory(), + IsmType.TEST_ISM, [], ); break; @@ -185,6 +201,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { protected async deployMultisigIsm( destination: ChainName, config: MultisigIsmConfig, + logger: Debugger, ): Promise { const signer = this.multiProvider.getSigner(destination); const multisigIsmFactory = @@ -196,6 +213,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { destination, multisigIsmFactory, config.validators, + logger, config.threshold, ); @@ -208,6 +226,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { origin?: ChainName; mailbox?: Address; existingIsmAddress?: Address; + logger: Debugger; }): Promise { const { destination, config, mailbox, existingIsmAddress } = params; const overrides = this.multiProvider.getTransactionOverrides(destination); @@ -263,7 +282,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { // deploying all the ISMs which have to be updated for (const originDomain of delta.domainsToEnroll) { const origin = this.multiProvider.getChainName(originDomain); // already filtered to only include domains in the multiprovider - logger( + params.logger( `Reconfiguring preexisting routing ISM at for origin ${origin}...`, ); const ism = await this.deploy({ @@ -282,7 +301,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { } // unenrolling domains if needed for (const originDomain of delta.domainsToUnenroll) { - logger( + params.logger( `Unenrolling originDomain ${originDomain} from preexisting routing ISM at ${existingIsmAddress}...`, ); const tx = await routingIsm.remove(originDomain, overrides); @@ -290,7 +309,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { } // transfer ownership if needed if (delta.owner) { - logger(`Transferring ownership of routing ISM...`); + params.logger(`Transferring ownership of routing ISM...`); const tx = await routingIsm.transferOwnership(delta.owner, overrides); await this.multiProvider.handleTx(destination, tx); } @@ -314,13 +333,13 @@ export class HyperlaneIsmFactory extends HyperlaneApp { 'Mailbox address is required for deploying fallback routing ISM', ); } - logger('Deploying fallback routing ISM ...'); + params.logger('Deploying fallback routing ISM ...'); routingIsm = await this.multiProvider.handleDeploy( destination, new DefaultFallbackRoutingIsm__factory(), [mailbox], ); - logger('Initialising fallback routing ISM ...'); + params.logger('Initialising fallback routing ISM ...'); receipt = await this.multiProvider.handleTx( destination, routingIsm['initialize(address,uint32[],address[])']( @@ -371,6 +390,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { config: AggregationIsmConfig; origin?: ChainName; mailbox?: Address; + logger: Debugger; }): Promise { const { destination, config, origin, mailbox } = params; const signer = this.multiProvider.getSigner(destination); @@ -390,26 +410,17 @@ export class HyperlaneIsmFactory extends HyperlaneApp { destination, aggregationIsmFactory, addresses, + params.logger, config.threshold, ); return IAggregationIsm__factory.connect(address, signer); } - protected async deployOpStackIsm( - chain: ChainName, - config: OpStackIsmConfig, - ): Promise { - return await this.multiProvider.handleDeploy( - chain, - new OPStackIsm__factory(), - [config.nativeBridge], - ); - } - async deployStaticAddressSet( chain: ChainName, factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory, values: Address[], + logger: Debugger, threshold = values.length, ): Promise
{ const sorted = [...values].sort(); @@ -420,7 +431,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp { ); const code = await this.multiProvider.getProvider(chain).getCode(address); if (code === '0x') { - this.logger( + logger( `Deploying new ${threshold} of ${values.length} address set to ${chain}`, ); const overrides = this.multiProvider.getTransactionOverrides(chain); @@ -432,390 +443,10 @@ export class HyperlaneIsmFactory extends HyperlaneApp { await this.multiProvider.handleTx(chain, hash); // TODO: add proxy verification artifact? } else { - this.logger( + logger( `Recovered ${threshold} of ${values.length} address set on ${chain}`, ); } return address; } } - -// Note that this function may return false negatives, but should -// not return false positives. -// This can happen if, for example, the module has sender, recipient, or -// body specific logic, as the sample message used when querying the ISM -// sets all of these to zero. -export async function moduleCanCertainlyVerify( - destModule: Address | IsmConfig, - multiProvider: MultiProvider, - origin: ChainName, - destination: ChainName, -): Promise { - const originDomainId = multiProvider.tryGetDomainId(origin); - const destinationDomainId = multiProvider.tryGetDomainId(destination); - if (!originDomainId || !destinationDomainId) { - return false; - } - const message = formatMessage( - 0, - 0, - originDomainId, - ethers.constants.AddressZero, - destinationDomainId, - ethers.constants.AddressZero, - '0x', - ); - const provider = multiProvider.getSignerOrProvider(destination); - - if (typeof destModule === 'string') { - const module = IInterchainSecurityModule__factory.connect( - destModule, - provider, - ); - - try { - const moduleType = await module.moduleType(); - if ( - moduleType === ModuleType.MERKLE_ROOT_MULTISIG || - moduleType === ModuleType.MESSAGE_ID_MULTISIG - ) { - const multisigModule = IMultisigIsm__factory.connect( - destModule, - provider, - ); - - const [, threshold] = await multisigModule.validatorsAndThreshold( - message, - ); - return threshold > 0; - } else if (moduleType === ModuleType.ROUTING) { - const routingIsm = IRoutingIsm__factory.connect(destModule, provider); - const subModule = await routingIsm.route(message); - return moduleCanCertainlyVerify( - subModule, - multiProvider, - origin, - destination, - ); - } else if (moduleType === ModuleType.AGGREGATION) { - const aggregationIsm = IAggregationIsm__factory.connect( - destModule, - provider, - ); - const [subModules, threshold] = - await aggregationIsm.modulesAndThreshold(message); - let verified = 0; - for (const subModule of subModules) { - const canVerify = await moduleCanCertainlyVerify( - subModule, - multiProvider, - origin, - destination, - ); - if (canVerify) { - verified += 1; - } - } - return verified >= threshold; - } else { - throw new Error(`Unsupported module type: ${moduleType}`); - } - } catch (e) { - logger(`Error checking module ${destModule}: ${e}`); - return false; - } - } else { - // destModule is an IsmConfig - switch (destModule.type) { - case IsmType.MERKLE_ROOT_MULTISIG: - case IsmType.MESSAGE_ID_MULTISIG: - return destModule.threshold > 0; - case IsmType.ROUTING: { - const checking = moduleCanCertainlyVerify( - destModule.domains[destination], - multiProvider, - origin, - destination, - ); - return checking; - } - case IsmType.AGGREGATION: { - let verified = 0; - for (const subModule of destModule.modules) { - const canVerify = await moduleCanCertainlyVerify( - subModule, - multiProvider, - origin, - destination, - ); - if (canVerify) { - verified += 1; - } - } - return verified >= destModule.threshold; - } - case IsmType.OP_STACK: - return destModule.nativeBridge !== ethers.constants.AddressZero; - case IsmType.TEST_ISM: { - return true; - } - default: - throw new Error(`Unsupported module type: ${(destModule as any).type}`); - } - } -} - -export async function moduleMatchesConfig( - chain: ChainName, - moduleAddress: Address, - config: IsmConfig, - multiProvider: MultiProvider, - contracts: HyperlaneContracts, - mailbox?: Address, -): Promise { - if (typeof config === 'string') { - return eqAddress(moduleAddress, config); - } - - // If the module address is zero, it can't match any object-based config. - // The subsequent check of what moduleType it is will throw, so we fail here. - if (eqAddress(moduleAddress, ethers.constants.AddressZero)) { - return false; - } - - const provider = multiProvider.getProvider(chain); - const module = IInterchainSecurityModule__factory.connect( - moduleAddress, - provider, - ); - const actualType = await module.moduleType(); - if (actualType !== ismTypeToModuleType(config.type)) return false; - let matches = true; - switch (config.type) { - case IsmType.MERKLE_ROOT_MULTISIG: { - // A MerkleRootMultisigIsm matches if validators and threshold match the config - const expectedAddress = - await contracts.merkleRootMultisigIsmFactory.getAddress( - config.validators.sort(), - config.threshold, - ); - matches = eqAddress(expectedAddress, module.address); - break; - } - case IsmType.MESSAGE_ID_MULTISIG: { - // A MessageIdMultisigIsm matches if validators and threshold match the config - const expectedAddress = - await contracts.messageIdMultisigIsmFactory.getAddress( - config.validators.sort(), - config.threshold, - ); - matches = eqAddress(expectedAddress, module.address); - break; - } - case IsmType.FALLBACK_ROUTING: - case IsmType.ROUTING: { - // A RoutingIsm matches if: - // 1. The set of domains in the config equals those on-chain - // 2. The modules for each domain match the config - // TODO: Check (1) - const routingIsm = DomainRoutingIsm__factory.connect( - moduleAddress, - provider, - ); - // Check that the RoutingISM owner matches the config - const owner = await routingIsm.owner(); - matches &&= eqAddress(owner, config.owner); - // check if the mailbox matches the config for fallback routing - if (config.type === IsmType.FALLBACK_ROUTING) { - const client = MailboxClient__factory.connect(moduleAddress, provider); - const mailboxAddress = await client.mailbox(); - matches = - matches && - mailbox !== undefined && - eqAddress(mailboxAddress, mailbox); - } - const delta = await routingModuleDelta( - chain, - moduleAddress, - config, - multiProvider, - contracts, - mailbox, - ); - matches = - matches && - delta.domainsToEnroll.length === 0 && - delta.domainsToUnenroll.length === 0 && - !delta.mailbox && - !delta.owner; - break; - } - case IsmType.AGGREGATION: { - // An AggregationIsm matches if: - // 1. The threshold matches the config - // 2. There is a bijection between on and off-chain configured modules - const aggregationIsm = StaticAggregationIsm__factory.connect( - moduleAddress, - provider, - ); - const [subModules, threshold] = await aggregationIsm.modulesAndThreshold( - '0x', - ); - matches &&= threshold === config.threshold; - matches &&= subModules.length === config.modules.length; - - const configIndexMatched = new Map(); - for (const subModule of subModules) { - const subModuleMatchesConfig = await Promise.all( - config.modules.map((c) => - moduleMatchesConfig(chain, subModule, c, multiProvider, contracts), - ), - ); - // The submodule returned by the ISM must match exactly one - // entry in the config. - const count = subModuleMatchesConfig.filter(Boolean).length; - matches &&= count === 1; - - // That entry in the config should not have been matched already. - subModuleMatchesConfig.forEach((matched, index) => { - if (matched) { - matches &&= !configIndexMatched.has(index); - configIndexMatched.set(index, true); - } - }); - } - break; - } - case IsmType.OP_STACK: { - const opStackIsm = OPStackIsm__factory.connect(moduleAddress, provider); - const type = await opStackIsm.moduleType(); - matches &&= type === ModuleType.NULL; - break; - } - case IsmType.TEST_ISM: { - // This is just a TestISM - matches = true; - break; - } - case IsmType.PAUSABLE: { - const pausableIsm = PausableIsm__factory.connect(moduleAddress, provider); - const owner = await pausableIsm.owner(); - matches &&= eqAddress(owner, config.owner); - - if (config.paused) { - const isPaused = await pausableIsm.paused(); - matches &&= config.paused === isPaused; - } - break; - } - default: { - throw new Error('Unsupported ModuleType'); - } - } - - return matches; -} - -export async function routingModuleDelta( - destination: ChainName, - moduleAddress: Address, - config: RoutingIsmConfig, - multiProvider: MultiProvider, - contracts: HyperlaneContracts, - mailbox?: Address, -): Promise { - const provider = multiProvider.getProvider(destination); - const routingIsm = DomainRoutingIsm__factory.connect(moduleAddress, provider); - const owner = await routingIsm.owner(); - const deployedDomains = (await routingIsm.domains()).map((domain) => - domain.toNumber(), - ); - // config.domains is already filtered to only include domains in the multiprovider - const safeConfigDomains = objMap(config.domains, (domain) => - multiProvider.getDomainId(domain), - ); - - const delta: RoutingIsmDelta = { - domainsToUnenroll: [], - domainsToEnroll: [], - }; - - // if owners don't match, we need to transfer ownership - if (!eqAddress(owner, normalizeAddress(config.owner))) - delta.owner = config.owner; - if (config.type === IsmType.FALLBACK_ROUTING) { - const client = MailboxClient__factory.connect(moduleAddress, provider); - const mailboxAddress = await client.mailbox(); - if (mailbox && !eqAddress(mailboxAddress, mailbox)) delta.mailbox = mailbox; - } - // check for exclusion of domains in the config - delta.domainsToUnenroll = deployedDomains.filter( - (domain) => !Object.values(safeConfigDomains).includes(domain), - ); - // check for inclusion of domains in the config - for (const [origin, subConfig] of Object.entries(config.domains)) { - const originDomain = safeConfigDomains[origin]; - if (!deployedDomains.includes(originDomain)) { - delta.domainsToEnroll.push(originDomain); - } else { - const subModule = await routingIsm.module(originDomain); - // Recursively check that the submodule for each configured - // domain matches the submodule config. - const subModuleMatches = await moduleMatchesConfig( - destination, - subModule, - subConfig, - multiProvider, - contracts, - mailbox, - ); - if (!subModuleMatches) delta.domainsToEnroll.push(originDomain); - } - } - return delta; -} - -export function collectValidators( - origin: ChainName, - config: IsmConfig, -): Set { - // TODO: support address configurations in collectValidators - if (typeof config === 'string') { - debug('hyperlane:IsmFactory')( - 'Address config unimplemented in collectValidators', - ); - return new Set([]); - } - - let validators: string[] = []; - if ( - config.type === IsmType.MERKLE_ROOT_MULTISIG || - config.type === IsmType.MESSAGE_ID_MULTISIG - ) { - validators = config.validators; - } else if (config.type === IsmType.ROUTING) { - if (Object.keys(config.domains).includes(origin)) { - const domainValidators = collectValidators( - origin, - config.domains[origin], - ); - validators = [...domainValidators]; - } - } else if (config.type === IsmType.AGGREGATION) { - const aggregatedValidators = config.modules.map((c) => - collectValidators(origin, c), - ); - aggregatedValidators.forEach((set) => { - validators = validators.concat([...set]); - }); - } else if ( - config.type === IsmType.TEST_ISM || - config.type === IsmType.PAUSABLE - ) { - return new Set([]); - } else { - throw new Error('Unsupported ModuleType'); - } - - return new Set(validators); -} diff --git a/typescript/sdk/src/ism/utils.ts b/typescript/sdk/src/ism/utils.ts new file mode 100644 index 000000000..d9c18092b --- /dev/null +++ b/typescript/sdk/src/ism/utils.ts @@ -0,0 +1,415 @@ +import debug from 'debug'; +import { ethers } from 'ethers'; + +import { + DomainRoutingIsm__factory, + IAggregationIsm__factory, + IInterchainSecurityModule__factory, + IMultisigIsm__factory, + IRoutingIsm__factory, + MailboxClient__factory, + OPStackIsm__factory, + PausableIsm__factory, + StaticAggregationIsm__factory, +} from '@hyperlane-xyz/core'; +import { + Address, + eqAddress, + formatMessage, + normalizeAddress, + objMap, +} from '@hyperlane-xyz/utils'; + +import { HyperlaneContracts } from '../contracts/types'; +import { ProxyFactoryFactories } from '../deploy/contracts'; +import { MultiProvider } from '../providers/MultiProvider'; +import { ChainName } from '../types'; + +import { + IsmConfig, + IsmType, + ModuleType, + RoutingIsmConfig, + RoutingIsmDelta, + ismTypeToModuleType, +} from './types'; + +const logger = debug('hyperlane:IsmUtils'); + +// Note that this function may return false negatives, but should +// not return false positives. +// This can happen if, for example, the module has sender, recipient, or +// body specific logic, as the sample message used when querying the ISM +// sets all of these to zero. +export async function moduleCanCertainlyVerify( + destModule: Address | IsmConfig, + multiProvider: MultiProvider, + origin: ChainName, + destination: ChainName, +): Promise { + const originDomainId = multiProvider.tryGetDomainId(origin); + const destinationDomainId = multiProvider.tryGetDomainId(destination); + if (!originDomainId || !destinationDomainId) { + return false; + } + const message = formatMessage( + 0, + 0, + originDomainId, + ethers.constants.AddressZero, + destinationDomainId, + ethers.constants.AddressZero, + '0x', + ); + const provider = multiProvider.getSignerOrProvider(destination); + + if (typeof destModule === 'string') { + const module = IInterchainSecurityModule__factory.connect( + destModule, + provider, + ); + + try { + const moduleType = await module.moduleType(); + if ( + moduleType === ModuleType.MERKLE_ROOT_MULTISIG || + moduleType === ModuleType.MESSAGE_ID_MULTISIG + ) { + const multisigModule = IMultisigIsm__factory.connect( + destModule, + provider, + ); + + const [, threshold] = await multisigModule.validatorsAndThreshold( + message, + ); + return threshold > 0; + } else if (moduleType === ModuleType.ROUTING) { + const routingIsm = IRoutingIsm__factory.connect(destModule, provider); + const subModule = await routingIsm.route(message); + return moduleCanCertainlyVerify( + subModule, + multiProvider, + origin, + destination, + ); + } else if (moduleType === ModuleType.AGGREGATION) { + const aggregationIsm = IAggregationIsm__factory.connect( + destModule, + provider, + ); + const [subModules, threshold] = + await aggregationIsm.modulesAndThreshold(message); + let verified = 0; + for (const subModule of subModules) { + const canVerify = await moduleCanCertainlyVerify( + subModule, + multiProvider, + origin, + destination, + ); + if (canVerify) { + verified += 1; + } + } + return verified >= threshold; + } else { + throw new Error(`Unsupported module type: ${moduleType}`); + } + } catch (e) { + logger(`Error checking module ${destModule}: ${e}`); + return false; + } + } else { + // destModule is an IsmConfig + switch (destModule.type) { + case IsmType.MERKLE_ROOT_MULTISIG: + case IsmType.MESSAGE_ID_MULTISIG: + return destModule.threshold > 0; + case IsmType.ROUTING: { + const checking = moduleCanCertainlyVerify( + destModule.domains[destination], + multiProvider, + origin, + destination, + ); + return checking; + } + case IsmType.AGGREGATION: { + let verified = 0; + for (const subModule of destModule.modules) { + const canVerify = await moduleCanCertainlyVerify( + subModule, + multiProvider, + origin, + destination, + ); + if (canVerify) { + verified += 1; + } + } + return verified >= destModule.threshold; + } + case IsmType.OP_STACK: + return destModule.nativeBridge !== ethers.constants.AddressZero; + case IsmType.TEST_ISM: { + return true; + } + default: + throw new Error(`Unsupported module type: ${(destModule as any).type}`); + } + } +} + +export async function moduleMatchesConfig( + chain: ChainName, + moduleAddress: Address, + config: IsmConfig, + multiProvider: MultiProvider, + contracts: HyperlaneContracts, + mailbox?: Address, +): Promise { + if (typeof config === 'string') { + return eqAddress(moduleAddress, config); + } + + // If the module address is zero, it can't match any object-based config. + // The subsequent check of what moduleType it is will throw, so we fail here. + if (eqAddress(moduleAddress, ethers.constants.AddressZero)) { + return false; + } + + const provider = multiProvider.getProvider(chain); + const module = IInterchainSecurityModule__factory.connect( + moduleAddress, + provider, + ); + const actualType = await module.moduleType(); + if (actualType !== ismTypeToModuleType(config.type)) return false; + let matches = true; + switch (config.type) { + case IsmType.MERKLE_ROOT_MULTISIG: { + // A MerkleRootMultisigIsm matches if validators and threshold match the config + const expectedAddress = + await contracts.merkleRootMultisigIsmFactory.getAddress( + config.validators.sort(), + config.threshold, + ); + matches = eqAddress(expectedAddress, module.address); + break; + } + case IsmType.MESSAGE_ID_MULTISIG: { + // A MessageIdMultisigIsm matches if validators and threshold match the config + const expectedAddress = + await contracts.messageIdMultisigIsmFactory.getAddress( + config.validators.sort(), + config.threshold, + ); + matches = eqAddress(expectedAddress, module.address); + break; + } + case IsmType.FALLBACK_ROUTING: + case IsmType.ROUTING: { + // A RoutingIsm matches if: + // 1. The set of domains in the config equals those on-chain + // 2. The modules for each domain match the config + // TODO: Check (1) + const routingIsm = DomainRoutingIsm__factory.connect( + moduleAddress, + provider, + ); + // Check that the RoutingISM owner matches the config + const owner = await routingIsm.owner(); + matches &&= eqAddress(owner, config.owner); + // check if the mailbox matches the config for fallback routing + if (config.type === IsmType.FALLBACK_ROUTING) { + const client = MailboxClient__factory.connect(moduleAddress, provider); + const mailboxAddress = await client.mailbox(); + matches = + matches && + mailbox !== undefined && + eqAddress(mailboxAddress, mailbox); + } + const delta = await routingModuleDelta( + chain, + moduleAddress, + config, + multiProvider, + contracts, + mailbox, + ); + matches = + matches && + delta.domainsToEnroll.length === 0 && + delta.domainsToUnenroll.length === 0 && + !delta.mailbox && + !delta.owner; + break; + } + case IsmType.AGGREGATION: { + // An AggregationIsm matches if: + // 1. The threshold matches the config + // 2. There is a bijection between on and off-chain configured modules + const aggregationIsm = StaticAggregationIsm__factory.connect( + moduleAddress, + provider, + ); + const [subModules, threshold] = await aggregationIsm.modulesAndThreshold( + '0x', + ); + matches &&= threshold === config.threshold; + matches &&= subModules.length === config.modules.length; + + const configIndexMatched = new Map(); + for (const subModule of subModules) { + const subModuleMatchesConfig = await Promise.all( + config.modules.map((c) => + moduleMatchesConfig(chain, subModule, c, multiProvider, contracts), + ), + ); + // The submodule returned by the ISM must match exactly one + // entry in the config. + const count = subModuleMatchesConfig.filter(Boolean).length; + matches &&= count === 1; + + // That entry in the config should not have been matched already. + subModuleMatchesConfig.forEach((matched, index) => { + if (matched) { + matches &&= !configIndexMatched.has(index); + configIndexMatched.set(index, true); + } + }); + } + break; + } + case IsmType.OP_STACK: { + const opStackIsm = OPStackIsm__factory.connect(moduleAddress, provider); + const type = await opStackIsm.moduleType(); + matches &&= type === ModuleType.NULL; + break; + } + case IsmType.TEST_ISM: { + // This is just a TestISM + matches = true; + break; + } + case IsmType.PAUSABLE: { + const pausableIsm = PausableIsm__factory.connect(moduleAddress, provider); + const owner = await pausableIsm.owner(); + matches &&= eqAddress(owner, config.owner); + + if (config.paused) { + const isPaused = await pausableIsm.paused(); + matches &&= config.paused === isPaused; + } + break; + } + default: { + throw new Error('Unsupported ModuleType'); + } + } + + return matches; +} + +export async function routingModuleDelta( + destination: ChainName, + moduleAddress: Address, + config: RoutingIsmConfig, + multiProvider: MultiProvider, + contracts: HyperlaneContracts, + mailbox?: Address, +): Promise { + const provider = multiProvider.getProvider(destination); + const routingIsm = DomainRoutingIsm__factory.connect(moduleAddress, provider); + const owner = await routingIsm.owner(); + const deployedDomains = (await routingIsm.domains()).map((domain) => + domain.toNumber(), + ); + // config.domains is already filtered to only include domains in the multiprovider + const safeConfigDomains = objMap(config.domains, (domain) => + multiProvider.getDomainId(domain), + ); + + const delta: RoutingIsmDelta = { + domainsToUnenroll: [], + domainsToEnroll: [], + }; + + // if owners don't match, we need to transfer ownership + if (!eqAddress(owner, normalizeAddress(config.owner))) + delta.owner = config.owner; + if (config.type === IsmType.FALLBACK_ROUTING) { + const client = MailboxClient__factory.connect(moduleAddress, provider); + const mailboxAddress = await client.mailbox(); + if (mailbox && !eqAddress(mailboxAddress, mailbox)) delta.mailbox = mailbox; + } + // check for exclusion of domains in the config + delta.domainsToUnenroll = deployedDomains.filter( + (domain) => !Object.values(safeConfigDomains).includes(domain), + ); + // check for inclusion of domains in the config + for (const [origin, subConfig] of Object.entries(config.domains)) { + const originDomain = safeConfigDomains[origin]; + if (!deployedDomains.includes(originDomain)) { + delta.domainsToEnroll.push(originDomain); + } else { + const subModule = await routingIsm.module(originDomain); + // Recursively check that the submodule for each configured + // domain matches the submodule config. + const subModuleMatches = await moduleMatchesConfig( + destination, + subModule, + subConfig, + multiProvider, + contracts, + mailbox, + ); + if (!subModuleMatches) delta.domainsToEnroll.push(originDomain); + } + } + return delta; +} + +export function collectValidators( + origin: ChainName, + config: IsmConfig, +): Set { + // TODO: support address configurations in collectValidators + if (typeof config === 'string') { + logger.extend(origin)('Address config unimplemented in collectValidators'); + return new Set([]); + } + + let validators: string[] = []; + if ( + config.type === IsmType.MERKLE_ROOT_MULTISIG || + config.type === IsmType.MESSAGE_ID_MULTISIG + ) { + validators = config.validators; + } else if (config.type === IsmType.ROUTING) { + if (Object.keys(config.domains).includes(origin)) { + const domainValidators = collectValidators( + origin, + config.domains[origin], + ); + validators = [...domainValidators]; + } + } else if (config.type === IsmType.AGGREGATION) { + const aggregatedValidators = config.modules.map((c) => + collectValidators(origin, c), + ); + aggregatedValidators.forEach((set) => { + validators = validators.concat([...set]); + }); + } else if ( + config.type === IsmType.TEST_ISM || + config.type === IsmType.PAUSABLE + ) { + return new Set([]); + } else { + throw new Error('Unsupported ModuleType'); + } + + return new Set(validators); +} diff --git a/typescript/sdk/src/metadata/ChainMetadataManager.ts b/typescript/sdk/src/metadata/ChainMetadataManager.ts index 5508f1c47..eb55defde 100644 --- a/typescript/sdk/src/metadata/ChainMetadataManager.ts +++ b/typescript/sdk/src/metadata/ChainMetadataManager.ts @@ -275,10 +275,10 @@ export class ChainMetadataManager { apiKey?: string; family?: ExplorerFamily; } { - const url = this.tryGetExplorerApi(chainNameOrId); - if (!url) + const explorerApi = this.tryGetExplorerApi(chainNameOrId); + if (!explorerApi) throw new Error(`No supported explorer api set for ${chainNameOrId}`); - return url; + return explorerApi; } /** diff --git a/typescript/sdk/src/middleware/account/InterchainAccountDeployer.ts b/typescript/sdk/src/middleware/account/InterchainAccountDeployer.ts index f564342de..329539150 100644 --- a/typescript/sdk/src/middleware/account/InterchainAccountDeployer.ts +++ b/typescript/sdk/src/middleware/account/InterchainAccountDeployer.ts @@ -1,6 +1,7 @@ import { ethers } from 'ethers'; import { HyperlaneContracts } from '../../contracts/types'; +import { ContractVerifier } from '../../deploy/verify/ContractVerifier'; import { MultiProvider } from '../../providers/MultiProvider'; import { ProxiedRouterDeployer } from '../../router/ProxiedRouterDeployer'; import { ProxiedRouterConfig, RouterConfig } from '../../router/types'; @@ -20,8 +21,13 @@ export class InterchainAccountDeployer extends ProxiedRouterDeployer< > { readonly routerContractName = 'interchainAccountRouter'; - constructor(multiProvider: MultiProvider) { - super(multiProvider, interchainAccountFactories); + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { + super(multiProvider, interchainAccountFactories, { + contractVerifier, + }); } async constructorArgs(_: string, config: RouterConfig): Promise<[string]> { diff --git a/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts b/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts index 6e338f116..1662fc440 100644 --- a/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts +++ b/typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts @@ -11,6 +11,7 @@ import { HyperlaneContracts, HyperlaneContractsMap, } from '../../contracts/types'; +import { ContractVerifier } from '../../deploy/verify/ContractVerifier'; import { MultiProvider } from '../../providers/MultiProvider'; import { ProxiedRouterDeployer } from '../../router/ProxiedRouterDeployer'; import { RouterConfig } from '../../router/types'; @@ -57,8 +58,13 @@ export class LiquidityLayerDeployer extends ProxiedRouterDeployer< > { readonly routerContractName = 'liquidityLayerRouter'; - constructor(multiProvider: MultiProvider) { - super(multiProvider, liquidityLayerFactories); + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { + super(multiProvider, liquidityLayerFactories, { + contractVerifier, + }); } async constructorArgs( diff --git a/typescript/sdk/src/middleware/query/InterchainQueryDeployer.ts b/typescript/sdk/src/middleware/query/InterchainQueryDeployer.ts index 588890c33..51af12bb9 100644 --- a/typescript/sdk/src/middleware/query/InterchainQueryDeployer.ts +++ b/typescript/sdk/src/middleware/query/InterchainQueryDeployer.ts @@ -1,5 +1,6 @@ import { ethers } from 'ethers'; +import { ContractVerifier } from '../../deploy/verify/ContractVerifier'; import { MultiProvider } from '../../providers/MultiProvider'; import { ProxiedRouterDeployer } from '../../router/ProxiedRouterDeployer'; import { RouterConfig } from '../../router/types'; @@ -18,8 +19,13 @@ export class InterchainQueryDeployer extends ProxiedRouterDeployer< > { readonly routerContractName = 'interchainQueryRouter'; - constructor(multiProvider: MultiProvider) { - super(multiProvider, interchainQueryFactories); + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { + super(multiProvider, interchainQueryFactories, { + contractVerifier, + }); } async constructorArgs(_: string, config: RouterConfig): Promise<[string]> { diff --git a/typescript/sdk/src/router/HyperlaneRouterChecker.ts b/typescript/sdk/src/router/HyperlaneRouterChecker.ts index d56e29c77..9fbe4fa55 100644 --- a/typescript/sdk/src/router/HyperlaneRouterChecker.ts +++ b/typescript/sdk/src/router/HyperlaneRouterChecker.ts @@ -7,10 +7,8 @@ import { addressToBytes32, eqAddress } from '@hyperlane-xyz/utils'; import { HyperlaneFactories } from '../contracts/types'; import { HyperlaneAppChecker } from '../deploy/HyperlaneAppChecker'; -import { - HyperlaneIsmFactory, - moduleMatchesConfig, -} from '../ism/HyperlaneIsmFactory'; +import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory'; +import { moduleMatchesConfig } from '../ism/utils'; import { MultiProvider } from '../providers/MultiProvider'; import { ChainMap, ChainName } from '../types'; diff --git a/typescript/sdk/src/token/deploy.ts b/typescript/sdk/src/token/deploy.ts index df32fcb45..a158d03c0 100644 --- a/typescript/sdk/src/token/deploy.ts +++ b/typescript/sdk/src/token/deploy.ts @@ -24,6 +24,7 @@ import { import { objMap } from '@hyperlane-xyz/utils'; import { HyperlaneContracts } from '../contracts/types'; +import { ContractVerifier } from '../deploy/verify/ContractVerifier'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory'; import { MultiProvider } from '../providers/MultiProvider'; import { GasRouterDeployer } from '../router/GasRouterDeployer'; @@ -56,10 +57,15 @@ export class HypERC20Deployer extends GasRouterDeployer< ERC20RouterConfig, HypERC20Factories > { - constructor(multiProvider: MultiProvider, ismFactory?: HyperlaneIsmFactory) { + constructor( + multiProvider: MultiProvider, + ismFactory?: HyperlaneIsmFactory, + contractVerifier?: ContractVerifier, + ) { super(multiProvider, {} as HypERC20Factories, { logger: debug('hyperlane:HypERC20Deployer'), ismFactory, + contractVerifier, }); // factories not used in deploy } @@ -280,9 +286,13 @@ export class HypERC721Deployer extends GasRouterDeployer< ERC721RouterConfig, HypERC721Factories > { - constructor(multiProvider: MultiProvider) { + constructor( + multiProvider: MultiProvider, + contractVerifier?: ContractVerifier, + ) { super(multiProvider, {} as HypERC721Factories, { logger: debug('hyperlane:HypERC721Deployer'), + contractVerifier, }); // factories not used in deploy }