feat: verify with deployment (#3255)

pull/3307/head
Paul Balaji 8 months ago committed by GitHub
parent 3bd520e105
commit 7d530fd4eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 13
      .changeset/green-pans-unite.md
  2. 2
      Dockerfile
  3. 1
      solidity/.gitignore
  4. 34
      solidity/exportBuildArtifact.sh
  5. 3
      solidity/package.json
  6. 2
      solidity/tsconfig.json
  7. 7
      typescript/helloworld/src/deploy/deploy.ts
  8. 10
      typescript/infra/scripts/agent-utils.ts
  9. 62
      typescript/infra/scripts/deploy.ts
  10. 88
      typescript/infra/scripts/verify.ts
  11. 10
      typescript/infra/src/deployment/testcontracts/testquerysender.ts
  12. 20
      typescript/infra/src/deployment/verify.ts
  13. 7
      typescript/sdk/src/core/HyperlaneCoreChecker.ts
  14. 15
      typescript/sdk/src/core/HyperlaneCoreDeployer.ts
  15. 7
      typescript/sdk/src/core/TestRecipientDeployer.ts
  16. 37
      typescript/sdk/src/deploy/HyperlaneDeployer.ts
  17. 7
      typescript/sdk/src/deploy/HyperlaneProxyFactoryDeployer.ts
  18. 366
      typescript/sdk/src/deploy/verify/ContractVerifier.ts
  19. 50
      typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts
  20. 99
      typescript/sdk/src/deploy/verify/types.ts
  21. 7
      typescript/sdk/src/gas/HyperlaneIgpDeployer.ts
  22. 9
      typescript/sdk/src/hook/HyperlaneHookDeployer.ts
  23. 10
      typescript/sdk/src/index.ts
  24. 7
      typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts
  25. 463
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  26. 415
      typescript/sdk/src/ism/utils.ts
  27. 6
      typescript/sdk/src/metadata/ChainMetadataManager.ts
  28. 10
      typescript/sdk/src/middleware/account/InterchainAccountDeployer.ts
  29. 10
      typescript/sdk/src/middleware/liquidity-layer/LiquidityLayerRouterDeployer.ts
  30. 10
      typescript/sdk/src/middleware/query/InterchainQueryDeployer.ts
  31. 6
      typescript/sdk/src/router/HyperlaneRouterChecker.ts
  32. 14
      typescript/sdk/src/token/deploy.ts

@ -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.

@ -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

@ -13,3 +13,4 @@ out
forge-cache
docs
flattened/
buildArtifact.json

@ -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

@ -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",

@ -5,4 +5,4 @@
},
"exclude": ["./test", "hardhat.config.ts", "./dist"],
"extends": "../tsconfig.json"
}
}

@ -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<HelloWorldFactories>): HelloWorld {

@ -69,7 +69,7 @@ export function getArgs() {
export function withModuleAndFork<T>(args: yargs.Argv<T>) {
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<T>(args: yargs.Argv<T>) {
.describe('context', 'deploy context')
.default('context', Contexts.Hyperlane)
.coerce('context', assertContext)
.alias('x', 'context')
.demandOption('context');
}
@ -133,6 +134,13 @@ export function withMissingChains<T>(args: yargs.Argv<T>) {
.alias('n', 'newChains');
}
export function withBuildArtifactPath<T>(args: yargs.Argv<T>) {
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;

@ -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<unknown> = {};
let deployer: HyperlaneDeployer<any, any>;
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;

@ -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<VerificationInput> = 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<string> = (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:',

@ -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) {

@ -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<ChainMap<string>> {
return (await fetchGCPSecret('explorer-api-keys', true)) as any;
}

@ -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';

@ -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<CoreAddresses>): void {

@ -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,
});
}

@ -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<any>): 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<F extends ethers.ContractFactory>(
public async deployContractFromFactory<F extends ethers.ContractFactory>(
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;
}

@ -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,
});
}

@ -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<VerificationInput> {
protected logger: Debugger;
protected readonly standardInputJson: string;
protected readonly compilerOptions: CompilerOptions;
constructor(
verificationInputs: ChainMap<VerificationInput>,
protected readonly multiProvider: MultiProvider,
protected readonly apiKeys: ChainMap<string>,
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<PromiseSettledResult<void>[]> {
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<void> {
this.logger(`Verifying ${chain}...`);
for (const input of inputs) {
await this.verifyContract(chain, input);
}
}
private async submitForm(
chain: ChainName,
action: ExplorerApiActions,
options?: Record<string, string>,
verificationLogger: Debugger,
options?: FormOptions<typeof action>,
): Promise<any> {
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<boolean> {
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<void> {
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<void> {
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<VerificationInput> {
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<void> {
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);
}
}

@ -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<VerificationInput> {
protected logger = debug('hyperlane:PostDeploymentContractVerifier');
protected readonly contractVerifier: ContractVerifier;
constructor(
verificationInputs: ChainMap<VerificationInput>,
protected readonly multiProvider: MultiProvider,
apiKeys: ChainMap<string>,
buildArtifact: BuildArtifact,
licenseType: CompilerOptions['licenseType'],
) {
super(verificationInputs);
this.contractVerifier = new ContractVerifier(
multiProvider,
apiKeys,
buildArtifact,
licenseType,
);
}
verify(targets = this.chains()): Promise<PromiseSettledResult<void>[]> {
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);
}
}),
);
}
}

@ -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> =
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;

@ -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,
});
}

@ -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<Partial<CoreAddresses>>,
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,

@ -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,

@ -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,

@ -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<ProxyFactoryFactories> {
// The shape of this object is `ChainMap<Address | ChainMap<Address>`,
@ -68,6 +59,11 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
// TODO: fix this in the next refactoring
public deployedIsms: ChainMap<any> = {};
protected deployer?: HyperlaneDeployer<any, any>;
setDeployer(deployer: HyperlaneDeployer<any, any>): void {
this.deployer = deployer;
}
static fromEnvironment<Env extends HyperlaneEnvironment>(
env: Env,
multiProvider: MultiProvider,
@ -113,7 +109,9 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
}
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<ProxyFactoryFactories> {
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<ProxyFactoryFactories> {
origin,
mailbox,
existingIsmAddress,
logger,
});
break;
case IsmType.AGGREGATION:
@ -141,22 +140,39 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
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<ProxyFactoryFactories> {
protected async deployMultisigIsm(
destination: ChainName,
config: MultisigIsmConfig,
logger: Debugger,
): Promise<IMultisigIsm> {
const signer = this.multiProvider.getSigner(destination);
const multisigIsmFactory =
@ -196,6 +213,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
destination,
multisigIsmFactory,
config.validators,
logger,
config.threshold,
);
@ -208,6 +226,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
origin?: ChainName;
mailbox?: Address;
existingIsmAddress?: Address;
logger: Debugger;
}): Promise<IRoutingIsm> {
const { destination, config, mailbox, existingIsmAddress } = params;
const overrides = this.multiProvider.getTransactionOverrides(destination);
@ -263,7 +282,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
// 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<ProxyFactoryFactories> {
}
// 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<ProxyFactoryFactories> {
}
// 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<ProxyFactoryFactories> {
'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<ProxyFactoryFactories> {
config: AggregationIsmConfig;
origin?: ChainName;
mailbox?: Address;
logger: Debugger;
}): Promise<IAggregationIsm> {
const { destination, config, origin, mailbox } = params;
const signer = this.multiProvider.getSigner(destination);
@ -390,26 +410,17 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
destination,
aggregationIsmFactory,
addresses,
params.logger,
config.threshold,
);
return IAggregationIsm__factory.connect(address, signer);
}
protected async deployOpStackIsm(
chain: ChainName,
config: OpStackIsmConfig,
): Promise<OPStackIsm> {
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<Address> {
const sorted = [...values].sort();
@ -420,7 +431,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
);
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<ProxyFactoryFactories> {
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<boolean> {
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<ProxyFactoryFactories>,
mailbox?: Address,
): Promise<boolean> {
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<ProxyFactoryFactories>,
mailbox?: Address,
): Promise<RoutingIsmDelta> {
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<string> {
// 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);
}

@ -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<boolean> {
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<ProxyFactoryFactories>,
mailbox?: Address,
): Promise<boolean> {
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<ProxyFactoryFactories>,
mailbox?: Address,
): Promise<RoutingIsmDelta> {
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<string> {
// 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);
}

@ -275,10 +275,10 @@ export class ChainMetadataManager<MetaExt = {}> {
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;
}
/**

@ -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]> {

@ -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(

@ -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]> {

@ -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';

@ -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
}

Loading…
Cancel
Save