From e06fe0b327168b189ba6f9767ae3ebf49f6ed717 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:29:26 -0500 Subject: [PATCH] fix:enable `DefaultFallbackRoutingIsm` through non-factory deployment (#3009) ### Description - `DefaultFallbackRoutingIsm` needs the mailbox address which meant `DefaultFallbackRoutingIsmFactory` needed the mailbox address which is not ideal. So, I switched to non-factory deployments for this specific ISM type. - CLI does the ISM deployment inside core deployment instead of ISM first and core then. - Using the coreDeployer's `cachedAddresses` to store the latest ie toplevel ISM. ### Drive-by changes - fixed bug in `ismFactory.fromAddressMap` to not use the multiprovider filtered by contract map. This is undesirable as CLI user can provide chain selection and artifacts which doesn't include all the chains in the configs (and when we call `multiprovider.getDomainId` for the origin which may now be missing from the filtered MP, we get an error). ### Related issues - fixes https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895 ### Backward compatibility Yes ### Testing Manual through CLI --- .changeset/large-guests-jump.md | 8 + .../isms/routing/DomainRoutingIsmFactory.sol | 20 +- solidity/test/isms/DomainRoutingIsm.t.sol | 2 +- typescript/cli/examples/ism-advanced.yaml | 2 +- typescript/cli/src/config/ism.ts | 27 ++- typescript/cli/src/config/multisig.ts | 4 +- typescript/cli/src/config/warp.ts | 10 +- typescript/cli/src/deploy/core.ts | 29 +-- typescript/cli/src/tests/ism.test.ts | 2 +- typescript/cli/src/tests/multisig.test.ts | 2 +- .../infra/src/govern/HyperlaneCoreGovernor.ts | 8 +- typescript/sdk/hardhat.config.ts | 2 +- .../sdk/src/core/HyperlaneCoreDeployer.ts | 24 +- .../sdk/src/deploy/HyperlaneDeployer.ts | 4 +- typescript/sdk/src/deploy/contracts.ts | 4 - .../sdk/src/hook/HyperlaneHookDeployer.ts | 10 +- typescript/sdk/src/index.ts | 1 + .../ism/HyperlaneIsmFactory.hardhat-test.ts | 72 +++++- typescript/sdk/src/ism/HyperlaneIsmFactory.ts | 223 ++++++++++++------ typescript/sdk/src/ism/types.ts | 8 +- 20 files changed, 301 insertions(+), 161 deletions(-) create mode 100644 .changeset/large-guests-jump.md diff --git a/.changeset/large-guests-jump.md b/.changeset/large-guests-jump.md new file mode 100644 index 000000000..6276c4c08 --- /dev/null +++ b/.changeset/large-guests-jump.md @@ -0,0 +1,8 @@ +--- +'@hyperlane-xyz/infra': patch +'@hyperlane-xyz/cli': patch +'@hyperlane-xyz/sdk': patch +'@hyperlane-xyz/core': patch +--- + +Supporting DefaultFallbackRoutingIsm through non-factory deployments diff --git a/solidity/contracts/isms/routing/DomainRoutingIsmFactory.sol b/solidity/contracts/isms/routing/DomainRoutingIsmFactory.sol index d0681723e..fc8b20383 100644 --- a/solidity/contracts/isms/routing/DomainRoutingIsmFactory.sol +++ b/solidity/contracts/isms/routing/DomainRoutingIsmFactory.sol @@ -18,10 +18,12 @@ abstract contract AbstractDomainRoutingIsmFactory { /** * @notice Deploys and initializes a DomainRoutingIsm using a minimal proxy + * @param _owner The owner to set on the ISM * @param _domains The origin domains * @param _modules The ISMs to use to verify messages */ function deploy( + address _owner, uint32[] calldata _domains, IInterchainSecurityModule[] calldata _modules ) external returns (DomainRoutingIsm) { @@ -29,7 +31,7 @@ abstract contract AbstractDomainRoutingIsmFactory { MinimalProxy.create(implementation()) ); emit ModuleDeployed(_ism); - _ism.initialize(msg.sender, _domains, _modules); + _ism.initialize(_owner, _domains, _modules); return _ism; } @@ -51,19 +53,3 @@ contract DomainRoutingIsmFactory is AbstractDomainRoutingIsmFactory { return _implementation; } } - -/** - * @title DefaultFallbackRoutingIsmFactory - */ -contract DefaultFallbackRoutingIsmFactory is AbstractDomainRoutingIsmFactory { - // ============ Immutables ============ - address internal immutable _implementation; - - constructor(address mailbox) { - _implementation = address(new DefaultFallbackRoutingIsm(mailbox)); - } - - function implementation() public view override returns (address) { - return _implementation; - } -} diff --git a/solidity/test/isms/DomainRoutingIsm.t.sol b/solidity/test/isms/DomainRoutingIsm.t.sol index 58166d064..88785d361 100644 --- a/solidity/test/isms/DomainRoutingIsm.t.sol +++ b/solidity/test/isms/DomainRoutingIsm.t.sol @@ -5,7 +5,7 @@ import "forge-std/Test.sol"; import {DomainRoutingIsm} from "../../contracts/isms/routing/DomainRoutingIsm.sol"; import {DefaultFallbackRoutingIsm} from "../../contracts/isms/routing/DefaultFallbackRoutingIsm.sol"; -import {DefaultFallbackRoutingIsmFactory, DomainRoutingIsmFactory} from "../../contracts/isms/routing/DomainRoutingIsmFactory.sol"; +import {DomainRoutingIsmFactory} from "../../contracts/isms/routing/DomainRoutingIsmFactory.sol"; import {IInterchainSecurityModule} from "../../contracts/interfaces/IInterchainSecurityModule.sol"; import {MessageUtils, TestIsm} from "./IsmTestUtils.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; diff --git a/typescript/cli/examples/ism-advanced.yaml b/typescript/cli/examples/ism-advanced.yaml index 8124102a5..544781108 100644 --- a/typescript/cli/examples/ism-advanced.yaml +++ b/typescript/cli/examples/ism-advanced.yaml @@ -1,5 +1,5 @@ anvil1: - type: domainRoutingIsm + type: defaultFallbackRoutingIsm owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' domains: anvil2: diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index 9f4846ce8..45a111ee2 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -1,7 +1,7 @@ import { confirm, input, select } from '@inquirer/prompts'; import { z } from 'zod'; -import { ChainMap, ChainName, IsmType } from '@hyperlane-xyz/sdk'; +import { ChainMap, ChainName, IsmType, ZHash } from '@hyperlane-xyz/sdk'; import { errorRed, log, logBlue, logGreen } from '../../logger.js'; import { runMultiChainSelectionStep } from '../utils/chains.js'; @@ -15,13 +15,16 @@ const MultisigIsmConfigSchema = z.object({ z.literal(IsmType.MESSAGE_ID_MULTISIG), ]), threshold: z.number(), - validators: z.array(z.string()), + validators: z.array(ZHash), }); const RoutingIsmConfigSchema: z.ZodSchema = z.lazy(() => z.object({ - type: z.literal(IsmType.ROUTING), - owner: z.string(), + type: z.union([ + z.literal(IsmType.ROUTING), + z.literal(IsmType.FALLBACK_ROUTING), + ]), + owner: ZHash, domains: z.record(IsmConfigSchema), }), ); @@ -160,6 +163,12 @@ export async function createIsmConfig( description: 'Each origin chain can be verified by the specified ISM type via RoutingISM', }, + { + value: IsmType.FALLBACK_ROUTING, + name: IsmType.FALLBACK_ROUTING, + description: + "You can specify ISM type for specific chains you like and fallback to mailbox's default ISM for other chains via DefaultFallbackRoutingISM", + }, { value: IsmType.AGGREGATION, name: IsmType.AGGREGATION, @@ -180,8 +189,11 @@ export async function createIsmConfig( moduleType === IsmType.MERKLE_ROOT_MULTISIG ) { lastConfig = await createMultisigConfig(moduleType); - } else if (moduleType === IsmType.ROUTING) { - lastConfig = await createRoutingConfig(remote, origins); + } else if ( + moduleType === IsmType.ROUTING || + moduleType === IsmType.FALLBACK_ROUTING + ) { + lastConfig = await createRoutingConfig(moduleType, remote, origins); } else if (moduleType === IsmType.AGGREGATION) { lastConfig = await createAggregationConfig(remote, origins); } else if (moduleType === IsmType.TEST_ISM) { @@ -241,6 +253,7 @@ export async function createAggregationConfig( } export async function createRoutingConfig( + type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING, remote: ChainName, chains: ChainName[], ): Promise { @@ -259,7 +272,7 @@ export async function createRoutingConfig( domainsMap[chain] = config; } return { - type: IsmType.ROUTING, + type, owner: ownerAddress, domains: domainsMap, }; diff --git a/typescript/cli/src/config/multisig.ts b/typescript/cli/src/config/multisig.ts index 19ef2ad5a..ba59111ab 100644 --- a/typescript/cli/src/config/multisig.ts +++ b/typescript/cli/src/config/multisig.ts @@ -1,7 +1,7 @@ import { confirm, input } from '@inquirer/prompts'; import { z } from 'zod'; -import { ChainMap, MultisigConfig } from '@hyperlane-xyz/sdk'; +import { ChainMap, MultisigConfig, ZHash } from '@hyperlane-xyz/sdk'; import { Address, isValidAddress, @@ -19,7 +19,7 @@ import { readChainConfigsIfExists } from './chain.js'; const MultisigConfigMapSchema = z.object({}).catchall( z.object({ threshold: z.number(), - validators: z.array(z.string()), + validators: z.array(ZHash), }), ); export type MultisigConfigMap = z.infer; diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index f793959d9..d187e9b9e 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -2,7 +2,7 @@ import { confirm, input } from '@inquirer/prompts'; import { ethers } from 'ethers'; import { z } from 'zod'; -import { TokenType } from '@hyperlane-xyz/sdk'; +import { TokenType, ZHash } from '@hyperlane-xyz/sdk'; import { errorRed, logBlue, logGreen } from '../../logger.js'; import { @@ -14,9 +14,9 @@ import { FileFormat, readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; import { readChainConfigsIfExists } from './chain.js'; const ConnectionConfigSchema = { - mailbox: z.string().optional(), - interchainGasPaymaster: z.string().optional(), - interchainSecurityModule: z.string().optional(), + mailbox: ZHash.optional(), + interchainGasPaymaster: ZHash.optional(), + interchainSecurityModule: ZHash.optional(), foreignDeployment: z.string().optional(), }; @@ -24,7 +24,7 @@ export const WarpRouteConfigSchema = z.object({ base: z.object({ type: z.literal(TokenType.native).or(z.literal(TokenType.collateral)), chainName: z.string(), - address: z.string().optional(), + address: ZHash.optional(), isNft: z.boolean().optional(), name: z.string().optional(), symbol: z.string().optional(), diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index f4b670145..c49875ba2 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -279,24 +279,15 @@ async function executeDeploy({ mergedContractAddrs, multiProvider, ); - - // 3. Deploy ISM contracts to remote deployable chains - logBlue('Deploying ISMs'); + // 3. Construct ISM configs for all deployable chains const ismContracts: ChainMap<{ interchainSecurityModule: DeployedIsm }> = {}; - const defaultIsms: ChainMap
= {}; + const defaultIsms: ChainMap = {}; for (const ismOrigin of chains) { - logBlue(`Deploying ISM to ${ismOrigin}`); - const ismConfig = + defaultIsms[ismOrigin] = ismConfigs[ismOrigin] ?? buildIsmConfig(owner, ismOrigin, chains, multisigConfigs); - ismContracts[ismOrigin] = { - interchainSecurityModule: await ismFactory.deploy(ismOrigin, ismConfig), - }; - defaultIsms[ismOrigin] = - ismContracts[ismOrigin].interchainSecurityModule.address; } artifacts = writeMergedAddresses(contractsFilePath, artifacts, ismContracts); - logGreen('ISM contracts deployed'); // 4. Deploy core contracts to chains logBlue(`Deploying core contracts to ${chains.join(', ')}`); @@ -310,6 +301,16 @@ async function executeDeploy({ multisigConfigs, ); const coreContracts = await coreDeployer.deploy(coreConfigs); + + // 4.5 recover the toplevel ISM address + const isms: HyperlaneAddressesMap = {}; + for (const chain of chains) { + isms[chain] = { + interchainSecurityModule: + coreDeployer.cachedAddresses[chain].interchainSecurityModule, + }; + } + artifacts = objMerge(artifacts, isms); artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts); logGreen('Core contracts deployed'); @@ -358,7 +359,7 @@ function buildIsmConfig( function buildCoreConfigMap( owner: Address, chains: ChainName[], - defaultIsms: ChainMap
, + defaultIsms: ChainMap, hooksConfig: ChainMap, multisigConfigs: ChainMap, ): ChainMap { @@ -381,7 +382,7 @@ function buildCoreConfigMap( }, {}); } -function buildTestRecipientConfigMap( +export function buildTestRecipientConfigMap( chains: ChainName[], addressesMap: HyperlaneAddressesMap, ): ChainMap { diff --git a/typescript/cli/src/tests/ism.test.ts b/typescript/cli/src/tests/ism.test.ts index a0b16d558..4942963cf 100644 --- a/typescript/cli/src/tests/ism.test.ts +++ b/typescript/cli/src/tests/ism.test.ts @@ -10,7 +10,7 @@ describe('readIsmConfig', () => { const exampleIsmConfig: ChainMap = { anvil1: { - type: IsmType.ROUTING, + type: IsmType.FALLBACK_ROUTING, owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720', domains: { anvil2: { diff --git a/typescript/cli/src/tests/multisig.test.ts b/typescript/cli/src/tests/multisig.test.ts index f54bfd5b2..f56b3e7bd 100644 --- a/typescript/cli/src/tests/multisig.test.ts +++ b/typescript/cli/src/tests/multisig.test.ts @@ -36,6 +36,6 @@ describe('readMultisigConfig', () => { it('invalid address', () => { expect(function () { readMultisigConfig('src/tests/multisig/invalid-address-fail.yaml'); - }).to.throw('Invalid address 0xa0ee7a142d267c1n36714e4a8f7561f20a79720'); + }).to.throw('Invalid multisig config: anvil2,validators,0 => Invalid'); }); }); diff --git a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts index 019c7321a..25547381c 100644 --- a/typescript/infra/src/govern/HyperlaneCoreGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneCoreGovernor.ts @@ -29,10 +29,10 @@ export class HyperlaneCoreGovernor extends HyperlaneAppGovernor< case MailboxViolationType.DefaultIsm: { let ismAddress: string; if (typeof violation.expected === 'object') { - const ism = await this.checker.ismFactory.deploy( - violation.chain, - violation.expected, - ); + const ism = await this.checker.ismFactory.deploy({ + destination: violation.chain, + config: violation.expected, + }); ismAddress = ism.address; } else if (typeof violation.expected === 'string') { ismAddress = violation.expected; diff --git a/typescript/sdk/hardhat.config.ts b/typescript/sdk/hardhat.config.ts index fc40a3931..e65a4508a 100644 --- a/typescript/sdk/hardhat.config.ts +++ b/typescript/sdk/hardhat.config.ts @@ -7,7 +7,7 @@ import '@typechain/hardhat'; */ module.exports = { solidity: { - version: '0.7.6', + version: '0.8.19', settings: { optimizer: { enabled: true, diff --git a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts index a1ee91456..4432073a7 100644 --- a/typescript/sdk/src/core/HyperlaneCoreDeployer.ts +++ b/typescript/sdk/src/core/HyperlaneCoreDeployer.ts @@ -22,7 +22,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< CoreConfig, CoreFactories > { - startingBlockNumbers: ChainMap = {}; hookDeployer: HyperlaneHookDeployer; constructor( @@ -69,8 +68,13 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< ); if (!matches) { this.logger('Deploying default ISM'); - defaultIsm = await this.deployIsm(chain, config.defaultIsm); + defaultIsm = await this.deployIsm( + chain, + config.defaultIsm, + mailbox.address, + ); } + this.cachedAddresses[chain].interchainSecurityModule = defaultIsm; const hookAddresses = { mailbox: mailbox.address, proxyAdmin }; @@ -168,8 +172,16 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< return hooks[config.type].address; } - async deployIsm(chain: ChainName, config: IsmConfig): Promise
{ - const ism = await this.ismFactory.deploy(chain, config); + async deployIsm( + chain: ChainName, + config: IsmConfig, + mailbox: Address, + ): Promise
{ + const ism = await this.ismFactory.deploy({ + destination: chain, + config, + mailbox, + }); this.addDeployedContracts(chain, this.ismFactory.deployedIsms[chain]); return ism.address; } @@ -187,10 +199,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer< const mailbox = await this.deployMailbox(chain, config, proxyAdmin.address); - // TODO: remove once agents fetch deployedBlock from mailbox - const deployedBlock = await mailbox.deployedBlock(); - this.startingBlockNumbers[chain] = deployedBlock.toNumber(); - const validatorAnnounce = await this.deployValidatorAnnounce( chain, mailbox.address, diff --git a/typescript/sdk/src/deploy/HyperlaneDeployer.ts b/typescript/sdk/src/deploy/HyperlaneDeployer.ts index fb3cc16e7..0daf8998e 100644 --- a/typescript/sdk/src/deploy/HyperlaneDeployer.ts +++ b/typescript/sdk/src/deploy/HyperlaneDeployer.ts @@ -220,7 +220,8 @@ export abstract class HyperlaneDeployer< this.multiProvider, ismFactory.getContracts(chain), ); - targetIsm = (await ismFactory.deploy(chain, config)).address; + targetIsm = (await ismFactory.deploy({ destination: chain, config })) + .address; } if (!matches) { await this.runIfOwner(chain, contract, async () => { @@ -269,7 +270,6 @@ export abstract class HyperlaneDeployer< config: MailboxClientConfig, ): Promise { this.logger(`Initializing mailbox client (if not already) on ${local}...`); - this.logger(`MailboxClient Config: ${JSON.stringify(config)}`); if (config.hook) { await this.configureHook( local, diff --git a/typescript/sdk/src/deploy/contracts.ts b/typescript/sdk/src/deploy/contracts.ts index ad4e42c7c..542c245c1 100644 --- a/typescript/sdk/src/deploy/contracts.ts +++ b/typescript/sdk/src/deploy/contracts.ts @@ -13,9 +13,6 @@ export const proxyFactoryFactories = { aggregationIsmFactory: new StaticAggregationIsmFactory__factory(), aggregationHookFactory: new StaticAggregationHookFactory__factory(), routingIsmFactory: new DomainRoutingIsmFactory__factory(), - // TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895 - // defaultFallbackRoutingIsmFactory: - // new DefaultFallbackRoutingIsmFactory__factory(), }; export type ProxyFactoryFactories = typeof proxyFactoryFactories; @@ -29,5 +26,4 @@ export const proxyFactoryImplementations: ProxyFactoryImplementations = { aggregationIsmFactory: 'StaticAggregationIsm', aggregationHookFactory: 'StaticAggregationHook', routingIsmFactory: 'DomaingRoutingIsm', - // defaultFallbackRoutingIsmFactory: 'DefaultFallbackRoutingIsm', }; diff --git a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts index 783bbe7ee..5eb961cf9 100644 --- a/typescript/sdk/src/hook/HyperlaneHookDeployer.ts +++ b/typescript/sdk/src/hook/HyperlaneHookDeployer.ts @@ -182,11 +182,11 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer< origin: chain, nativeBridge: l2Messenger, }; - const opstackIsm = (await this.ismFactory.deploy( - config.destinationChain, - ismConfig, - chain, - )) as OPStackIsm; + const opstackIsm = (await this.ismFactory.deploy({ + destination: config.destinationChain, + config: ismConfig, + origin: chain, + })) as OPStackIsm; // deploy opstack hook const hook = await this.deployContract(chain, HookType.OP_STACK, [ mailbox, diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index a5122765c..2e76f6977 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -178,6 +178,7 @@ export { getDomainId, isValidChainMetadata, } from './metadata/chainMetadataTypes'; +export { ZHash } from './metadata/customZodTypes'; export { HyperlaneDeploymentArtifacts, HyperlaneDeploymentArtifactsSchema, diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts index cdb0ba9ed..c5061e08d 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts @@ -4,6 +4,8 @@ import { ethers } from 'hardhat'; import { error } from '@hyperlane-xyz/utils'; import { TestChains } from '../consts/chains'; +import { TestCoreApp } from '../core/TestCoreApp'; +import { TestCoreDeployer } from '../core/TestCoreDeployer'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer'; import { MultiProvider } from '../providers/MultiProvider'; import { randomAddress, randomInt } from '../test/testUtils'; @@ -72,7 +74,9 @@ const randomIsmConfig = (depth = 0, maxDepth = 2): IsmConfig => { }; describe('HyperlaneIsmFactory', async () => { - let factory: HyperlaneIsmFactory; + let ismFactory: HyperlaneIsmFactory; + let coreApp: TestCoreApp; + const chain = 'test1'; before(async () => { @@ -80,20 +84,24 @@ describe('HyperlaneIsmFactory', async () => { const multiProvider = MultiProvider.createTestMultiProvider({ signer }); - const deployer = new HyperlaneProxyFactoryDeployer(multiProvider); - const contracts = await deployer.deploy({ [chain]: {} }); - factory = new HyperlaneIsmFactory(contracts, multiProvider); + const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); + ismFactory = new HyperlaneIsmFactory( + await ismFactoryDeployer.deploy(multiProvider.mapKnownChains(() => ({}))), + multiProvider, + ); + const coreDeployer = new TestCoreDeployer(multiProvider, ismFactory); + coreApp = await coreDeployer.deployApp(); }); it('deploys a simple ism', async () => { const config = randomMultisigIsmConfig(3, 5); - const ism = await factory.deploy(chain, config); + const ism = await ismFactory.deploy({ destination: chain, config }); const matches = await moduleMatchesConfig( chain, ism.address, config, - factory.multiProvider, - factory.getContracts(chain), + ismFactory.multiProvider, + ismFactory.getContracts(chain), ); expect(matches).to.be.true; }); @@ -103,7 +111,7 @@ describe('HyperlaneIsmFactory', async () => { const config = randomIsmConfig(); let ismAddress: string; try { - const ism = await factory.deploy(chain, config); + const ism = await ismFactory.deploy({ destination: chain, config }); ismAddress = ism.address; } catch (e) { error('Failed to deploy random ism config', e); @@ -116,8 +124,8 @@ describe('HyperlaneIsmFactory', async () => { chain, ismAddress, config, - factory.multiProvider, - factory.getContracts(chain), + ismFactory.multiProvider, + ismFactory.getContracts(chain), ); expect(matches).to.be.true; } catch (e) { @@ -127,4 +135,48 @@ describe('HyperlaneIsmFactory', async () => { } }); } + + it('deploys routingIsm with correct routes', async () => { + const config: RoutingIsmConfig = { + type: IsmType.ROUTING, + owner: randomAddress(), + domains: Object.fromEntries( + TestChains.map((c) => [c, randomIsmConfig()]), + ), + }; + const ism = await ismFactory.deploy({ destination: chain, config }); + const matches = await moduleMatchesConfig( + chain, + ism.address, + config, + ismFactory.multiProvider, + ismFactory.getContracts(chain), + ); + expect(matches).to.be.true; + }); + + it('deploys defaultFallbackRoutingIsm with correct routes and fallback to mailbox', async () => { + const config: RoutingIsmConfig = { + type: IsmType.FALLBACK_ROUTING, + owner: randomAddress(), + domains: Object.fromEntries( + TestChains.map((c) => [c, randomIsmConfig()]), + ), + }; + const mailbox = await coreApp.getContracts(chain).mailbox; + const ism = await ismFactory.deploy({ + destination: chain, + config, + mailbox: mailbox.address, + }); // not through an actual factory just for maintaining consistency in naming + const matches = await moduleMatchesConfig( + chain, + ism.address, + config, + ismFactory.multiProvider, + ismFactory.getContracts(chain), + mailbox.address, + ); + expect(matches).to.be.true; + }); }); diff --git a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts index 0f1dbba52..b7ada8b58 100644 --- a/typescript/sdk/src/ism/HyperlaneIsmFactory.ts +++ b/typescript/sdk/src/ism/HyperlaneIsmFactory.ts @@ -2,11 +2,19 @@ import { debug } from 'debug'; import { ethers } from 'ethers'; import { + DefaultFallbackRoutingIsm, + DefaultFallbackRoutingIsm__factory, + DomainRoutingIsm, DomainRoutingIsm__factory, + IAggregationIsm, IAggregationIsm__factory, IInterchainSecurityModule__factory, + IMultisigIsm, IMultisigIsm__factory, + IRoutingIsm, IRoutingIsm__factory, + MailboxClient__factory, + OPStackIsm, OPStackIsm__factory, StaticAddressSetFactory, StaticAggregationIsm__factory, @@ -71,27 +79,29 @@ export class HyperlaneIsmFactory extends HyperlaneApp { ); return new HyperlaneIsmFactory( helper.contractsMap, - helper.multiProvider, + multiProvider, debug('hyperlane:IsmFactoryApp'), ); } - async deploy( - chain: ChainName, - config: C, - origin?: ChainName, - ): Promise { + async deploy(params: { + destination: ChainName; + config: C; + origin?: ChainName; + mailbox?: Address; + }): Promise { + const { destination, config, origin, mailbox } = params; if (typeof config === 'string') { // @ts-ignore return IInterchainSecurityModule__factory.connect( config, - this.multiProvider.getSignerOrProvider(chain), + this.multiProvider.getSignerOrProvider(destination), ); } const ismType = config.type; this.logger( - `Deploying ${ismType} to ${chain} ${ + `Deploying ${ismType} to ${destination} ${ origin ? `(for verifying ${origin})` : '' }`, ); @@ -100,20 +110,31 @@ export class HyperlaneIsmFactory extends HyperlaneApp { switch (ismType) { case IsmType.MESSAGE_ID_MULTISIG: case IsmType.MERKLE_ROOT_MULTISIG: - contract = await this.deployMultisigIsm(chain, config); + contract = await this.deployMultisigIsm(destination, config); break; case IsmType.ROUTING: - contract = await this.deployRoutingIsm(chain, config); + case IsmType.FALLBACK_ROUTING: + contract = await this.deployRoutingIsm({ + destination, + config, + origin, + mailbox, + }); break; case IsmType.AGGREGATION: - contract = await this.deployAggregationIsm(chain, config, origin); + contract = await this.deployAggregationIsm({ + destination, + config, + origin, + mailbox, + }); break; case IsmType.OP_STACK: - contract = await this.deployOpStackIsm(chain, config); + contract = await this.deployOpStackIsm(destination, config); break; case IsmType.TEST_ISM: contract = await this.multiProvider.handleDeploy( - chain, + destination, new TestIsm__factory(), [], ); @@ -122,33 +143,36 @@ export class HyperlaneIsmFactory extends HyperlaneApp { throw new Error(`Unsupported ISM type ${ismType}`); } - if (!this.deployedIsms[chain]) { - this.deployedIsms[chain] = {}; + if (!this.deployedIsms[destination]) { + this.deployedIsms[destination] = {}; } if (origin) { // if we're deploying network-specific contracts (e.g. ISMs), store them as sub-entry // under that network's key (`origin`) - if (!this.deployedIsms[chain][origin]) { - this.deployedIsms[chain][origin] = {}; + if (!this.deployedIsms[destination][origin]) { + this.deployedIsms[destination][origin] = {}; } - this.deployedIsms[chain][origin][ismType] = contract; + this.deployedIsms[destination][origin][ismType] = contract; } else { // otherwise store the entry directly - this.deployedIsms[chain][ismType] = contract; + this.deployedIsms[destination][ismType] = contract; } return contract; } - private async deployMultisigIsm(chain: ChainName, config: MultisigIsmConfig) { - const signer = this.multiProvider.getSigner(chain); + protected async deployMultisigIsm( + destination: ChainName, + config: MultisigIsmConfig, + ): Promise { + const signer = this.multiProvider.getSigner(destination); const multisigIsmFactory = config.type === IsmType.MERKLE_ROOT_MULTISIG - ? this.getContracts(chain).merkleRootMultisigIsmFactory - : this.getContracts(chain).messageIdMultisigIsmFactory; + ? this.getContracts(destination).merkleRootMultisigIsmFactory + : this.getContracts(destination).messageIdMultisigIsmFactory; const address = await this.deployStaticAddressSet( - chain, + destination, multisigIsmFactory, config.validators, config.threshold, @@ -157,71 +181,107 @@ export class HyperlaneIsmFactory extends HyperlaneApp { return IMultisigIsm__factory.connect(address, signer); } - private async deployRoutingIsm(chain: ChainName, config: RoutingIsmConfig) { - const signer = this.multiProvider.getSigner(chain); - const routingIsmFactory = this.getContracts(chain).routingIsmFactory; - // TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895 - // config.defaultFallback - // ? this.getContracts(chain).defaultFallbackRoutingIsmFactory - // : this.getContracts(chain).routingIsmFactory; + protected async deployRoutingIsm(params: { + destination: ChainName; + config: RoutingIsmConfig; + origin?: ChainName; + mailbox?: Address; + }): Promise { + const { destination, config, mailbox } = params; + const routingIsmFactory = this.getContracts(destination).routingIsmFactory; const isms: ChainMap
= {}; for (const origin of Object.keys(config.domains)) { - const ism = await this.deploy(chain, config.domains[origin], origin); + const ism = await this.deploy({ + destination, + config: config.domains[origin], + origin, + mailbox, + }); isms[origin] = ism.address; } const domains = Object.keys(isms).map((chain) => this.multiProvider.getDomainId(chain), ); const submoduleAddresses = Object.values(isms); - const overrides = this.multiProvider.getTransactionOverrides(chain); - const tx = await routingIsmFactory.deploy( - domains, - submoduleAddresses, - overrides, - ); - const receipt = await this.multiProvider.handleTx(chain, tx); - // TODO: Break this out into a generalized function - const dispatchLogs = receipt.logs - .map((log) => { - try { - return routingIsmFactory.interface.parseLog(log); - } catch (e) { - return undefined; - } - }) - .filter( - (log): log is ethers.utils.LogDescription => - !!log && log.name === 'ModuleDeployed', + const overrides = this.multiProvider.getTransactionOverrides(destination); + let receipt: ethers.providers.TransactionReceipt; + let routingIsm: DomainRoutingIsm | DefaultFallbackRoutingIsm; + if (config.type === IsmType.FALLBACK_ROUTING) { + if (!mailbox) { + throw new Error( + 'Mailbox address is required for deploying fallback routing ISM', + ); + } + debug('Deploying fallback routing ISM ...'); + routingIsm = await this.multiProvider.handleDeploy( + destination, + new DefaultFallbackRoutingIsm__factory(), + [mailbox], ); - const moduleAddress = dispatchLogs[0].args['module']; - const routingIsm = DomainRoutingIsm__factory.connect( - moduleAddress, - this.multiProvider.getSigner(chain), - ); - this.logger(`Transferring ownership of routing ISM to ${config.owner}`); - await this.multiProvider.handleTx( - chain, - await routingIsm.transferOwnership(config.owner, overrides), - ); - const address = dispatchLogs[0].args['module']; - return IRoutingIsm__factory.connect(address, signer); + debug('Initialising fallback routing ISM ...'); + receipt = await this.multiProvider.handleTx( + destination, + routingIsm['initialize(address,uint32[],address[])']( + config.owner, + domains, + submoduleAddresses, + overrides, + ), + ); + } else { + const tx = await routingIsmFactory.deploy( + config.owner, + domains, + submoduleAddresses, + overrides, + ); + receipt = await this.multiProvider.handleTx(destination, tx); + + // TODO: Break this out into a generalized function + const dispatchLogs = receipt.logs + .map((log) => { + try { + return routingIsmFactory.interface.parseLog(log); + } catch (e) { + return undefined; + } + }) + .filter( + (log): log is ethers.utils.LogDescription => + !!log && log.name === 'ModuleDeployed', + ); + const moduleAddress = dispatchLogs[0].args['module']; + routingIsm = DomainRoutingIsm__factory.connect( + moduleAddress, + this.multiProvider.getSigner(destination), + ); + } + + return routingIsm; } - private async deployAggregationIsm( - chain: ChainName, - config: AggregationIsmConfig, - origin?: ChainName, - ) { - const signer = this.multiProvider.getSigner(chain); + protected async deployAggregationIsm(params: { + destination: ChainName; + config: AggregationIsmConfig; + origin?: ChainName; + mailbox?: Address; + }): Promise { + const { destination, config, origin, mailbox } = params; + const signer = this.multiProvider.getSigner(destination); const aggregationIsmFactory = - this.getContracts(chain).aggregationIsmFactory; + this.getContracts(destination).aggregationIsmFactory; const addresses: Address[] = []; for (const module of config.modules) { - const submodule = await this.deploy(chain, module, origin); + const submodule = await this.deploy({ + destination, + config: module, + origin, + mailbox, + }); addresses.push(submodule.address); } const address = await this.deployStaticAddressSet( - chain, + destination, aggregationIsmFactory, addresses, config.threshold, @@ -229,7 +289,10 @@ export class HyperlaneIsmFactory extends HyperlaneApp { return IAggregationIsm__factory.connect(address, signer); } - private async deployOpStackIsm(chain: ChainName, config: OpStackIsmConfig) { + protected async deployOpStackIsm( + chain: ChainName, + config: OpStackIsmConfig, + ): Promise { return await this.multiProvider.handleDeploy( chain, new OPStackIsm__factory(), @@ -397,7 +460,7 @@ export async function moduleMatchesConfig( config: IsmConfig, multiProvider: MultiProvider, contracts: HyperlaneContracts, - _origin?: ChainName, + mailbox?: Address, ): Promise { if (typeof config === 'string') { return eqAddress(moduleAddress, config); @@ -438,6 +501,7 @@ export async function moduleMatchesConfig( 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 @@ -450,6 +514,15 @@ export async function moduleMatchesConfig( // Check that the RoutingISM owner matches the config const owner = await routingIsm.owner(); matches = 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); + } // Recursively check that the submodule for each configured // domain matches the submodule config. for (const [origin, subConfig] of Object.entries(config.domains)) { @@ -462,7 +535,7 @@ export async function moduleMatchesConfig( subConfig, multiProvider, contracts, - origin, + mailbox, ); matches = matches && subModuleMatches; } diff --git a/typescript/sdk/src/ism/types.ts b/typescript/sdk/src/ism/types.ts index 0f7e17cd8..e9d222bfc 100644 --- a/typescript/sdk/src/ism/types.ts +++ b/typescript/sdk/src/ism/types.ts @@ -26,6 +26,7 @@ export enum ModuleType { export enum IsmType { OP_STACK = 'opStackIsm', ROUTING = 'domainRoutingIsm', + FALLBACK_ROUTING = 'defaultFallbackRoutingIsm', AGGREGATION = 'staticAggregationIsm', MERKLE_ROOT_MULTISIG = 'merkleRootMultisigIsm', MESSAGE_ID_MULTISIG = 'messageIdMultisigIsm', @@ -39,6 +40,8 @@ export function ismTypeToModuleType(ismType: IsmType): ModuleType { return ModuleType.NULL; case IsmType.ROUTING: return ModuleType.ROUTING; + case IsmType.FALLBACK_ROUTING: + return ModuleType.ROUTING; case IsmType.AGGREGATION: return ModuleType.AGGREGATION; case IsmType.MERKLE_ROOT_MULTISIG: @@ -64,11 +67,9 @@ export type TestIsmConfig = { }; export type RoutingIsmConfig = { - type: IsmType.ROUTING; + type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING; owner: Address; domains: ChainMap; - // TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895 - // defaultFallback?: boolean; }; export type AggregationIsmConfig = { @@ -93,6 +94,7 @@ export type IsmConfig = export type DeployedIsmType = { [IsmType.ROUTING]: IRoutingIsm; + [IsmType.FALLBACK_ROUTING]: IRoutingIsm; [IsmType.AGGREGATION]: IAggregationIsm; [IsmType.MERKLE_ROOT_MULTISIG]: IMultisigIsm; [IsmType.MESSAGE_ID_MULTISIG]: IMultisigIsm;