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
cli-figlet
Kunal Arora 12 months ago committed by GitHub
parent 8b16adee48
commit e06fe0b327
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      .changeset/large-guests-jump.md
  2. 20
      solidity/contracts/isms/routing/DomainRoutingIsmFactory.sol
  3. 2
      solidity/test/isms/DomainRoutingIsm.t.sol
  4. 2
      typescript/cli/examples/ism-advanced.yaml
  5. 27
      typescript/cli/src/config/ism.ts
  6. 4
      typescript/cli/src/config/multisig.ts
  7. 10
      typescript/cli/src/config/warp.ts
  8. 29
      typescript/cli/src/deploy/core.ts
  9. 2
      typescript/cli/src/tests/ism.test.ts
  10. 2
      typescript/cli/src/tests/multisig.test.ts
  11. 8
      typescript/infra/src/govern/HyperlaneCoreGovernor.ts
  12. 2
      typescript/sdk/hardhat.config.ts
  13. 24
      typescript/sdk/src/core/HyperlaneCoreDeployer.ts
  14. 4
      typescript/sdk/src/deploy/HyperlaneDeployer.ts
  15. 4
      typescript/sdk/src/deploy/contracts.ts
  16. 10
      typescript/sdk/src/hook/HyperlaneHookDeployer.ts
  17. 1
      typescript/sdk/src/index.ts
  18. 72
      typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts
  19. 223
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  20. 8
      typescript/sdk/src/ism/types.ts

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

@ -18,10 +18,12 @@ abstract contract AbstractDomainRoutingIsmFactory {
/** /**
* @notice Deploys and initializes a DomainRoutingIsm using a minimal proxy * @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 _domains The origin domains
* @param _modules The ISMs to use to verify messages * @param _modules The ISMs to use to verify messages
*/ */
function deploy( function deploy(
address _owner,
uint32[] calldata _domains, uint32[] calldata _domains,
IInterchainSecurityModule[] calldata _modules IInterchainSecurityModule[] calldata _modules
) external returns (DomainRoutingIsm) { ) external returns (DomainRoutingIsm) {
@ -29,7 +31,7 @@ abstract contract AbstractDomainRoutingIsmFactory {
MinimalProxy.create(implementation()) MinimalProxy.create(implementation())
); );
emit ModuleDeployed(_ism); emit ModuleDeployed(_ism);
_ism.initialize(msg.sender, _domains, _modules); _ism.initialize(_owner, _domains, _modules);
return _ism; return _ism;
} }
@ -51,19 +53,3 @@ contract DomainRoutingIsmFactory is AbstractDomainRoutingIsmFactory {
return _implementation; 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;
}
}

@ -5,7 +5,7 @@ import "forge-std/Test.sol";
import {DomainRoutingIsm} from "../../contracts/isms/routing/DomainRoutingIsm.sol"; import {DomainRoutingIsm} from "../../contracts/isms/routing/DomainRoutingIsm.sol";
import {DefaultFallbackRoutingIsm} from "../../contracts/isms/routing/DefaultFallbackRoutingIsm.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 {IInterchainSecurityModule} from "../../contracts/interfaces/IInterchainSecurityModule.sol";
import {MessageUtils, TestIsm} from "./IsmTestUtils.sol"; import {MessageUtils, TestIsm} from "./IsmTestUtils.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol";

@ -1,5 +1,5 @@
anvil1: anvil1:
type: domainRoutingIsm type: defaultFallbackRoutingIsm
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720' owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720'
domains: domains:
anvil2: anvil2:

@ -1,7 +1,7 @@
import { confirm, input, select } from '@inquirer/prompts'; import { confirm, input, select } from '@inquirer/prompts';
import { z } from 'zod'; 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 { errorRed, log, logBlue, logGreen } from '../../logger.js';
import { runMultiChainSelectionStep } from '../utils/chains.js'; import { runMultiChainSelectionStep } from '../utils/chains.js';
@ -15,13 +15,16 @@ const MultisigIsmConfigSchema = z.object({
z.literal(IsmType.MESSAGE_ID_MULTISIG), z.literal(IsmType.MESSAGE_ID_MULTISIG),
]), ]),
threshold: z.number(), threshold: z.number(),
validators: z.array(z.string()), validators: z.array(ZHash),
}); });
const RoutingIsmConfigSchema: z.ZodSchema<any> = z.lazy(() => const RoutingIsmConfigSchema: z.ZodSchema<any> = z.lazy(() =>
z.object({ z.object({
type: z.literal(IsmType.ROUTING), type: z.union([
owner: z.string(), z.literal(IsmType.ROUTING),
z.literal(IsmType.FALLBACK_ROUTING),
]),
owner: ZHash,
domains: z.record(IsmConfigSchema), domains: z.record(IsmConfigSchema),
}), }),
); );
@ -160,6 +163,12 @@ export async function createIsmConfig(
description: description:
'Each origin chain can be verified by the specified ISM type via RoutingISM', '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, value: IsmType.AGGREGATION,
name: IsmType.AGGREGATION, name: IsmType.AGGREGATION,
@ -180,8 +189,11 @@ export async function createIsmConfig(
moduleType === IsmType.MERKLE_ROOT_MULTISIG moduleType === IsmType.MERKLE_ROOT_MULTISIG
) { ) {
lastConfig = await createMultisigConfig(moduleType); lastConfig = await createMultisigConfig(moduleType);
} else if (moduleType === IsmType.ROUTING) { } else if (
lastConfig = await createRoutingConfig(remote, origins); moduleType === IsmType.ROUTING ||
moduleType === IsmType.FALLBACK_ROUTING
) {
lastConfig = await createRoutingConfig(moduleType, remote, origins);
} else if (moduleType === IsmType.AGGREGATION) { } else if (moduleType === IsmType.AGGREGATION) {
lastConfig = await createAggregationConfig(remote, origins); lastConfig = await createAggregationConfig(remote, origins);
} else if (moduleType === IsmType.TEST_ISM) { } else if (moduleType === IsmType.TEST_ISM) {
@ -241,6 +253,7 @@ export async function createAggregationConfig(
} }
export async function createRoutingConfig( export async function createRoutingConfig(
type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING,
remote: ChainName, remote: ChainName,
chains: ChainName[], chains: ChainName[],
): Promise<ZodIsmConfig> { ): Promise<ZodIsmConfig> {
@ -259,7 +272,7 @@ export async function createRoutingConfig(
domainsMap[chain] = config; domainsMap[chain] = config;
} }
return { return {
type: IsmType.ROUTING, type,
owner: ownerAddress, owner: ownerAddress,
domains: domainsMap, domains: domainsMap,
}; };

@ -1,7 +1,7 @@
import { confirm, input } from '@inquirer/prompts'; import { confirm, input } from '@inquirer/prompts';
import { z } from 'zod'; import { z } from 'zod';
import { ChainMap, MultisigConfig } from '@hyperlane-xyz/sdk'; import { ChainMap, MultisigConfig, ZHash } from '@hyperlane-xyz/sdk';
import { import {
Address, Address,
isValidAddress, isValidAddress,
@ -19,7 +19,7 @@ import { readChainConfigsIfExists } from './chain.js';
const MultisigConfigMapSchema = z.object({}).catchall( const MultisigConfigMapSchema = z.object({}).catchall(
z.object({ z.object({
threshold: z.number(), threshold: z.number(),
validators: z.array(z.string()), validators: z.array(ZHash),
}), }),
); );
export type MultisigConfigMap = z.infer<typeof MultisigConfigMapSchema>; export type MultisigConfigMap = z.infer<typeof MultisigConfigMapSchema>;

@ -2,7 +2,7 @@ import { confirm, input } from '@inquirer/prompts';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { z } from 'zod'; 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 { errorRed, logBlue, logGreen } from '../../logger.js';
import { import {
@ -14,9 +14,9 @@ import { FileFormat, readYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import { readChainConfigsIfExists } from './chain.js'; import { readChainConfigsIfExists } from './chain.js';
const ConnectionConfigSchema = { const ConnectionConfigSchema = {
mailbox: z.string().optional(), mailbox: ZHash.optional(),
interchainGasPaymaster: z.string().optional(), interchainGasPaymaster: ZHash.optional(),
interchainSecurityModule: z.string().optional(), interchainSecurityModule: ZHash.optional(),
foreignDeployment: z.string().optional(), foreignDeployment: z.string().optional(),
}; };
@ -24,7 +24,7 @@ export const WarpRouteConfigSchema = z.object({
base: z.object({ base: z.object({
type: z.literal(TokenType.native).or(z.literal(TokenType.collateral)), type: z.literal(TokenType.native).or(z.literal(TokenType.collateral)),
chainName: z.string(), chainName: z.string(),
address: z.string().optional(), address: ZHash.optional(),
isNft: z.boolean().optional(), isNft: z.boolean().optional(),
name: z.string().optional(), name: z.string().optional(),
symbol: z.string().optional(), symbol: z.string().optional(),

@ -279,24 +279,15 @@ async function executeDeploy({
mergedContractAddrs, mergedContractAddrs,
multiProvider, multiProvider,
); );
// 3. Construct ISM configs for all deployable chains
// 3. Deploy ISM contracts to remote deployable chains
logBlue('Deploying ISMs');
const ismContracts: ChainMap<{ interchainSecurityModule: DeployedIsm }> = {}; const ismContracts: ChainMap<{ interchainSecurityModule: DeployedIsm }> = {};
const defaultIsms: ChainMap<Address> = {}; const defaultIsms: ChainMap<IsmConfig> = {};
for (const ismOrigin of chains) { for (const ismOrigin of chains) {
logBlue(`Deploying ISM to ${ismOrigin}`); defaultIsms[ismOrigin] =
const ismConfig =
ismConfigs[ismOrigin] ?? ismConfigs[ismOrigin] ??
buildIsmConfig(owner, ismOrigin, chains, multisigConfigs); buildIsmConfig(owner, ismOrigin, chains, multisigConfigs);
ismContracts[ismOrigin] = {
interchainSecurityModule: await ismFactory.deploy(ismOrigin, ismConfig),
};
defaultIsms[ismOrigin] =
ismContracts[ismOrigin].interchainSecurityModule.address;
} }
artifacts = writeMergedAddresses(contractsFilePath, artifacts, ismContracts); artifacts = writeMergedAddresses(contractsFilePath, artifacts, ismContracts);
logGreen('ISM contracts deployed');
// 4. Deploy core contracts to chains // 4. Deploy core contracts to chains
logBlue(`Deploying core contracts to ${chains.join(', ')}`); logBlue(`Deploying core contracts to ${chains.join(', ')}`);
@ -310,6 +301,16 @@ async function executeDeploy({
multisigConfigs, multisigConfigs,
); );
const coreContracts = await coreDeployer.deploy(coreConfigs); const coreContracts = await coreDeployer.deploy(coreConfigs);
// 4.5 recover the toplevel ISM address
const isms: HyperlaneAddressesMap<any> = {};
for (const chain of chains) {
isms[chain] = {
interchainSecurityModule:
coreDeployer.cachedAddresses[chain].interchainSecurityModule,
};
}
artifacts = objMerge(artifacts, isms);
artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts); artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts);
logGreen('Core contracts deployed'); logGreen('Core contracts deployed');
@ -358,7 +359,7 @@ function buildIsmConfig(
function buildCoreConfigMap( function buildCoreConfigMap(
owner: Address, owner: Address,
chains: ChainName[], chains: ChainName[],
defaultIsms: ChainMap<Address>, defaultIsms: ChainMap<IsmConfig>,
hooksConfig: ChainMap<HooksConfig>, hooksConfig: ChainMap<HooksConfig>,
multisigConfigs: ChainMap<MultisigConfig>, multisigConfigs: ChainMap<MultisigConfig>,
): ChainMap<CoreConfig> { ): ChainMap<CoreConfig> {
@ -381,7 +382,7 @@ function buildCoreConfigMap(
}, {}); }, {});
} }
function buildTestRecipientConfigMap( export function buildTestRecipientConfigMap(
chains: ChainName[], chains: ChainName[],
addressesMap: HyperlaneAddressesMap<any>, addressesMap: HyperlaneAddressesMap<any>,
): ChainMap<TestRecipientConfig> { ): ChainMap<TestRecipientConfig> {

@ -10,7 +10,7 @@ describe('readIsmConfig', () => {
const exampleIsmConfig: ChainMap<IsmConfig> = { const exampleIsmConfig: ChainMap<IsmConfig> = {
anvil1: { anvil1: {
type: IsmType.ROUTING, type: IsmType.FALLBACK_ROUTING,
owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720', owner: '0xa0ee7a142d267c1f36714e4a8f75612f20a79720',
domains: { domains: {
anvil2: { anvil2: {

@ -36,6 +36,6 @@ describe('readMultisigConfig', () => {
it('invalid address', () => { it('invalid address', () => {
expect(function () { expect(function () {
readMultisigConfig('src/tests/multisig/invalid-address-fail.yaml'); readMultisigConfig('src/tests/multisig/invalid-address-fail.yaml');
}).to.throw('Invalid address 0xa0ee7a142d267c1n36714e4a8f7561f20a79720'); }).to.throw('Invalid multisig config: anvil2,validators,0 => Invalid');
}); });
}); });

@ -29,10 +29,10 @@ export class HyperlaneCoreGovernor extends HyperlaneAppGovernor<
case MailboxViolationType.DefaultIsm: { case MailboxViolationType.DefaultIsm: {
let ismAddress: string; let ismAddress: string;
if (typeof violation.expected === 'object') { if (typeof violation.expected === 'object') {
const ism = await this.checker.ismFactory.deploy( const ism = await this.checker.ismFactory.deploy({
violation.chain, destination: violation.chain,
violation.expected, config: violation.expected,
); });
ismAddress = ism.address; ismAddress = ism.address;
} else if (typeof violation.expected === 'string') { } else if (typeof violation.expected === 'string') {
ismAddress = violation.expected; ismAddress = violation.expected;

@ -7,7 +7,7 @@ import '@typechain/hardhat';
*/ */
module.exports = { module.exports = {
solidity: { solidity: {
version: '0.7.6', version: '0.8.19',
settings: { settings: {
optimizer: { optimizer: {
enabled: true, enabled: true,

@ -22,7 +22,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
CoreConfig, CoreConfig,
CoreFactories CoreFactories
> { > {
startingBlockNumbers: ChainMap<number | undefined> = {};
hookDeployer: HyperlaneHookDeployer; hookDeployer: HyperlaneHookDeployer;
constructor( constructor(
@ -69,8 +68,13 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
); );
if (!matches) { if (!matches) {
this.logger('Deploying default ISM'); 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 }; const hookAddresses = { mailbox: mailbox.address, proxyAdmin };
@ -168,8 +172,16 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
return hooks[config.type].address; return hooks[config.type].address;
} }
async deployIsm(chain: ChainName, config: IsmConfig): Promise<Address> { async deployIsm(
const ism = await this.ismFactory.deploy(chain, config); chain: ChainName,
config: IsmConfig,
mailbox: Address,
): Promise<Address> {
const ism = await this.ismFactory.deploy({
destination: chain,
config,
mailbox,
});
this.addDeployedContracts(chain, this.ismFactory.deployedIsms[chain]); this.addDeployedContracts(chain, this.ismFactory.deployedIsms[chain]);
return ism.address; return ism.address;
} }
@ -187,10 +199,6 @@ export class HyperlaneCoreDeployer extends HyperlaneDeployer<
const mailbox = await this.deployMailbox(chain, config, proxyAdmin.address); 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( const validatorAnnounce = await this.deployValidatorAnnounce(
chain, chain,
mailbox.address, mailbox.address,

@ -220,7 +220,8 @@ export abstract class HyperlaneDeployer<
this.multiProvider, this.multiProvider,
ismFactory.getContracts(chain), ismFactory.getContracts(chain),
); );
targetIsm = (await ismFactory.deploy(chain, config)).address; targetIsm = (await ismFactory.deploy({ destination: chain, config }))
.address;
} }
if (!matches) { if (!matches) {
await this.runIfOwner(chain, contract, async () => { await this.runIfOwner(chain, contract, async () => {
@ -269,7 +270,6 @@ export abstract class HyperlaneDeployer<
config: MailboxClientConfig, config: MailboxClientConfig,
): Promise<void> { ): Promise<void> {
this.logger(`Initializing mailbox client (if not already) on ${local}...`); this.logger(`Initializing mailbox client (if not already) on ${local}...`);
this.logger(`MailboxClient Config: ${JSON.stringify(config)}`);
if (config.hook) { if (config.hook) {
await this.configureHook( await this.configureHook(
local, local,

@ -13,9 +13,6 @@ export const proxyFactoryFactories = {
aggregationIsmFactory: new StaticAggregationIsmFactory__factory(), aggregationIsmFactory: new StaticAggregationIsmFactory__factory(),
aggregationHookFactory: new StaticAggregationHookFactory__factory(), aggregationHookFactory: new StaticAggregationHookFactory__factory(),
routingIsmFactory: new DomainRoutingIsmFactory__factory(), routingIsmFactory: new DomainRoutingIsmFactory__factory(),
// TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895
// defaultFallbackRoutingIsmFactory:
// new DefaultFallbackRoutingIsmFactory__factory(),
}; };
export type ProxyFactoryFactories = typeof proxyFactoryFactories; export type ProxyFactoryFactories = typeof proxyFactoryFactories;
@ -29,5 +26,4 @@ export const proxyFactoryImplementations: ProxyFactoryImplementations = {
aggregationIsmFactory: 'StaticAggregationIsm', aggregationIsmFactory: 'StaticAggregationIsm',
aggregationHookFactory: 'StaticAggregationHook', aggregationHookFactory: 'StaticAggregationHook',
routingIsmFactory: 'DomaingRoutingIsm', routingIsmFactory: 'DomaingRoutingIsm',
// defaultFallbackRoutingIsmFactory: 'DefaultFallbackRoutingIsm',
}; };

@ -182,11 +182,11 @@ export class HyperlaneHookDeployer extends HyperlaneDeployer<
origin: chain, origin: chain,
nativeBridge: l2Messenger, nativeBridge: l2Messenger,
}; };
const opstackIsm = (await this.ismFactory.deploy( const opstackIsm = (await this.ismFactory.deploy({
config.destinationChain, destination: config.destinationChain,
ismConfig, config: ismConfig,
chain, origin: chain,
)) as OPStackIsm; })) as OPStackIsm;
// deploy opstack hook // deploy opstack hook
const hook = await this.deployContract(chain, HookType.OP_STACK, [ const hook = await this.deployContract(chain, HookType.OP_STACK, [
mailbox, mailbox,

@ -178,6 +178,7 @@ export {
getDomainId, getDomainId,
isValidChainMetadata, isValidChainMetadata,
} from './metadata/chainMetadataTypes'; } from './metadata/chainMetadataTypes';
export { ZHash } from './metadata/customZodTypes';
export { export {
HyperlaneDeploymentArtifacts, HyperlaneDeploymentArtifacts,
HyperlaneDeploymentArtifactsSchema, HyperlaneDeploymentArtifactsSchema,

@ -4,6 +4,8 @@ import { ethers } from 'hardhat';
import { error } from '@hyperlane-xyz/utils'; import { error } from '@hyperlane-xyz/utils';
import { TestChains } from '../consts/chains'; import { TestChains } from '../consts/chains';
import { TestCoreApp } from '../core/TestCoreApp';
import { TestCoreDeployer } from '../core/TestCoreDeployer';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer';
import { MultiProvider } from '../providers/MultiProvider'; import { MultiProvider } from '../providers/MultiProvider';
import { randomAddress, randomInt } from '../test/testUtils'; import { randomAddress, randomInt } from '../test/testUtils';
@ -72,7 +74,9 @@ const randomIsmConfig = (depth = 0, maxDepth = 2): IsmConfig => {
}; };
describe('HyperlaneIsmFactory', async () => { describe('HyperlaneIsmFactory', async () => {
let factory: HyperlaneIsmFactory; let ismFactory: HyperlaneIsmFactory;
let coreApp: TestCoreApp;
const chain = 'test1'; const chain = 'test1';
before(async () => { before(async () => {
@ -80,20 +84,24 @@ describe('HyperlaneIsmFactory', async () => {
const multiProvider = MultiProvider.createTestMultiProvider({ signer }); const multiProvider = MultiProvider.createTestMultiProvider({ signer });
const deployer = new HyperlaneProxyFactoryDeployer(multiProvider); const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
const contracts = await deployer.deploy({ [chain]: {} }); ismFactory = new HyperlaneIsmFactory(
factory = new HyperlaneIsmFactory(contracts, multiProvider); await ismFactoryDeployer.deploy(multiProvider.mapKnownChains(() => ({}))),
multiProvider,
);
const coreDeployer = new TestCoreDeployer(multiProvider, ismFactory);
coreApp = await coreDeployer.deployApp();
}); });
it('deploys a simple ism', async () => { it('deploys a simple ism', async () => {
const config = randomMultisigIsmConfig(3, 5); const config = randomMultisigIsmConfig(3, 5);
const ism = await factory.deploy(chain, config); const ism = await ismFactory.deploy({ destination: chain, config });
const matches = await moduleMatchesConfig( const matches = await moduleMatchesConfig(
chain, chain,
ism.address, ism.address,
config, config,
factory.multiProvider, ismFactory.multiProvider,
factory.getContracts(chain), ismFactory.getContracts(chain),
); );
expect(matches).to.be.true; expect(matches).to.be.true;
}); });
@ -103,7 +111,7 @@ describe('HyperlaneIsmFactory', async () => {
const config = randomIsmConfig(); const config = randomIsmConfig();
let ismAddress: string; let ismAddress: string;
try { try {
const ism = await factory.deploy(chain, config); const ism = await ismFactory.deploy({ destination: chain, config });
ismAddress = ism.address; ismAddress = ism.address;
} catch (e) { } catch (e) {
error('Failed to deploy random ism config', e); error('Failed to deploy random ism config', e);
@ -116,8 +124,8 @@ describe('HyperlaneIsmFactory', async () => {
chain, chain,
ismAddress, ismAddress,
config, config,
factory.multiProvider, ismFactory.multiProvider,
factory.getContracts(chain), ismFactory.getContracts(chain),
); );
expect(matches).to.be.true; expect(matches).to.be.true;
} catch (e) { } 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;
});
}); });

@ -2,11 +2,19 @@ import { debug } from 'debug';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { import {
DefaultFallbackRoutingIsm,
DefaultFallbackRoutingIsm__factory,
DomainRoutingIsm,
DomainRoutingIsm__factory, DomainRoutingIsm__factory,
IAggregationIsm,
IAggregationIsm__factory, IAggregationIsm__factory,
IInterchainSecurityModule__factory, IInterchainSecurityModule__factory,
IMultisigIsm,
IMultisigIsm__factory, IMultisigIsm__factory,
IRoutingIsm,
IRoutingIsm__factory, IRoutingIsm__factory,
MailboxClient__factory,
OPStackIsm,
OPStackIsm__factory, OPStackIsm__factory,
StaticAddressSetFactory, StaticAddressSetFactory,
StaticAggregationIsm__factory, StaticAggregationIsm__factory,
@ -71,27 +79,29 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
); );
return new HyperlaneIsmFactory( return new HyperlaneIsmFactory(
helper.contractsMap, helper.contractsMap,
helper.multiProvider, multiProvider,
debug('hyperlane:IsmFactoryApp'), debug('hyperlane:IsmFactoryApp'),
); );
} }
async deploy<C extends IsmConfig>( async deploy<C extends IsmConfig>(params: {
chain: ChainName, destination: ChainName;
config: C, config: C;
origin?: ChainName, origin?: ChainName;
): Promise<DeployedIsm> { mailbox?: Address;
}): Promise<DeployedIsm> {
const { destination, config, origin, mailbox } = params;
if (typeof config === 'string') { if (typeof config === 'string') {
// @ts-ignore // @ts-ignore
return IInterchainSecurityModule__factory.connect( return IInterchainSecurityModule__factory.connect(
config, config,
this.multiProvider.getSignerOrProvider(chain), this.multiProvider.getSignerOrProvider(destination),
); );
} }
const ismType = config.type; const ismType = config.type;
this.logger( this.logger(
`Deploying ${ismType} to ${chain} ${ `Deploying ${ismType} to ${destination} ${
origin ? `(for verifying ${origin})` : '' origin ? `(for verifying ${origin})` : ''
}`, }`,
); );
@ -100,20 +110,31 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
switch (ismType) { switch (ismType) {
case IsmType.MESSAGE_ID_MULTISIG: case IsmType.MESSAGE_ID_MULTISIG:
case IsmType.MERKLE_ROOT_MULTISIG: case IsmType.MERKLE_ROOT_MULTISIG:
contract = await this.deployMultisigIsm(chain, config); contract = await this.deployMultisigIsm(destination, config);
break; break;
case IsmType.ROUTING: case IsmType.ROUTING:
contract = await this.deployRoutingIsm(chain, config); case IsmType.FALLBACK_ROUTING:
contract = await this.deployRoutingIsm({
destination,
config,
origin,
mailbox,
});
break; break;
case IsmType.AGGREGATION: case IsmType.AGGREGATION:
contract = await this.deployAggregationIsm(chain, config, origin); contract = await this.deployAggregationIsm({
destination,
config,
origin,
mailbox,
});
break; break;
case IsmType.OP_STACK: case IsmType.OP_STACK:
contract = await this.deployOpStackIsm(chain, config); contract = await this.deployOpStackIsm(destination, config);
break; break;
case IsmType.TEST_ISM: case IsmType.TEST_ISM:
contract = await this.multiProvider.handleDeploy( contract = await this.multiProvider.handleDeploy(
chain, destination,
new TestIsm__factory(), new TestIsm__factory(),
[], [],
); );
@ -122,33 +143,36 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
throw new Error(`Unsupported ISM type ${ismType}`); throw new Error(`Unsupported ISM type ${ismType}`);
} }
if (!this.deployedIsms[chain]) { if (!this.deployedIsms[destination]) {
this.deployedIsms[chain] = {}; this.deployedIsms[destination] = {};
} }
if (origin) { if (origin) {
// if we're deploying network-specific contracts (e.g. ISMs), store them as sub-entry // if we're deploying network-specific contracts (e.g. ISMs), store them as sub-entry
// under that network's key (`origin`) // under that network's key (`origin`)
if (!this.deployedIsms[chain][origin]) { if (!this.deployedIsms[destination][origin]) {
this.deployedIsms[chain][origin] = {}; this.deployedIsms[destination][origin] = {};
} }
this.deployedIsms[chain][origin][ismType] = contract; this.deployedIsms[destination][origin][ismType] = contract;
} else { } else {
// otherwise store the entry directly // otherwise store the entry directly
this.deployedIsms[chain][ismType] = contract; this.deployedIsms[destination][ismType] = contract;
} }
return contract; return contract;
} }
private async deployMultisigIsm(chain: ChainName, config: MultisigIsmConfig) { protected async deployMultisigIsm(
const signer = this.multiProvider.getSigner(chain); destination: ChainName,
config: MultisigIsmConfig,
): Promise<IMultisigIsm> {
const signer = this.multiProvider.getSigner(destination);
const multisigIsmFactory = const multisigIsmFactory =
config.type === IsmType.MERKLE_ROOT_MULTISIG config.type === IsmType.MERKLE_ROOT_MULTISIG
? this.getContracts(chain).merkleRootMultisigIsmFactory ? this.getContracts(destination).merkleRootMultisigIsmFactory
: this.getContracts(chain).messageIdMultisigIsmFactory; : this.getContracts(destination).messageIdMultisigIsmFactory;
const address = await this.deployStaticAddressSet( const address = await this.deployStaticAddressSet(
chain, destination,
multisigIsmFactory, multisigIsmFactory,
config.validators, config.validators,
config.threshold, config.threshold,
@ -157,71 +181,107 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
return IMultisigIsm__factory.connect(address, signer); return IMultisigIsm__factory.connect(address, signer);
} }
private async deployRoutingIsm(chain: ChainName, config: RoutingIsmConfig) { protected async deployRoutingIsm(params: {
const signer = this.multiProvider.getSigner(chain); destination: ChainName;
const routingIsmFactory = this.getContracts(chain).routingIsmFactory; config: RoutingIsmConfig;
// TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895 origin?: ChainName;
// config.defaultFallback mailbox?: Address;
// ? this.getContracts(chain).defaultFallbackRoutingIsmFactory }): Promise<IRoutingIsm> {
// : this.getContracts(chain).routingIsmFactory; const { destination, config, mailbox } = params;
const routingIsmFactory = this.getContracts(destination).routingIsmFactory;
const isms: ChainMap<Address> = {}; const isms: ChainMap<Address> = {};
for (const origin of Object.keys(config.domains)) { 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; isms[origin] = ism.address;
} }
const domains = Object.keys(isms).map((chain) => const domains = Object.keys(isms).map((chain) =>
this.multiProvider.getDomainId(chain), this.multiProvider.getDomainId(chain),
); );
const submoduleAddresses = Object.values(isms); const submoduleAddresses = Object.values(isms);
const overrides = this.multiProvider.getTransactionOverrides(chain); const overrides = this.multiProvider.getTransactionOverrides(destination);
const tx = await routingIsmFactory.deploy( let receipt: ethers.providers.TransactionReceipt;
domains, let routingIsm: DomainRoutingIsm | DefaultFallbackRoutingIsm;
submoduleAddresses, if (config.type === IsmType.FALLBACK_ROUTING) {
overrides, if (!mailbox) {
); throw new Error(
const receipt = await this.multiProvider.handleTx(chain, tx); 'Mailbox address is required for deploying fallback routing ISM',
// TODO: Break this out into a generalized function );
const dispatchLogs = receipt.logs }
.map((log) => { debug('Deploying fallback routing ISM ...');
try { routingIsm = await this.multiProvider.handleDeploy(
return routingIsmFactory.interface.parseLog(log); destination,
} catch (e) { new DefaultFallbackRoutingIsm__factory(),
return undefined; [mailbox],
}
})
.filter(
(log): log is ethers.utils.LogDescription =>
!!log && log.name === 'ModuleDeployed',
); );
const moduleAddress = dispatchLogs[0].args['module']; debug('Initialising fallback routing ISM ...');
const routingIsm = DomainRoutingIsm__factory.connect( receipt = await this.multiProvider.handleTx(
moduleAddress, destination,
this.multiProvider.getSigner(chain), routingIsm['initialize(address,uint32[],address[])'](
); config.owner,
this.logger(`Transferring ownership of routing ISM to ${config.owner}`); domains,
await this.multiProvider.handleTx( submoduleAddresses,
chain, overrides,
await routingIsm.transferOwnership(config.owner, overrides), ),
); );
const address = dispatchLogs[0].args['module']; } else {
return IRoutingIsm__factory.connect(address, signer); 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( protected async deployAggregationIsm(params: {
chain: ChainName, destination: ChainName;
config: AggregationIsmConfig, config: AggregationIsmConfig;
origin?: ChainName, origin?: ChainName;
) { mailbox?: Address;
const signer = this.multiProvider.getSigner(chain); }): Promise<IAggregationIsm> {
const { destination, config, origin, mailbox } = params;
const signer = this.multiProvider.getSigner(destination);
const aggregationIsmFactory = const aggregationIsmFactory =
this.getContracts(chain).aggregationIsmFactory; this.getContracts(destination).aggregationIsmFactory;
const addresses: Address[] = []; const addresses: Address[] = [];
for (const module of config.modules) { 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); addresses.push(submodule.address);
} }
const address = await this.deployStaticAddressSet( const address = await this.deployStaticAddressSet(
chain, destination,
aggregationIsmFactory, aggregationIsmFactory,
addresses, addresses,
config.threshold, config.threshold,
@ -229,7 +289,10 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
return IAggregationIsm__factory.connect(address, signer); return IAggregationIsm__factory.connect(address, signer);
} }
private async deployOpStackIsm(chain: ChainName, config: OpStackIsmConfig) { protected async deployOpStackIsm(
chain: ChainName,
config: OpStackIsmConfig,
): Promise<OPStackIsm> {
return await this.multiProvider.handleDeploy( return await this.multiProvider.handleDeploy(
chain, chain,
new OPStackIsm__factory(), new OPStackIsm__factory(),
@ -397,7 +460,7 @@ export async function moduleMatchesConfig(
config: IsmConfig, config: IsmConfig,
multiProvider: MultiProvider, multiProvider: MultiProvider,
contracts: HyperlaneContracts<ProxyFactoryFactories>, contracts: HyperlaneContracts<ProxyFactoryFactories>,
_origin?: ChainName, mailbox?: Address,
): Promise<boolean> { ): Promise<boolean> {
if (typeof config === 'string') { if (typeof config === 'string') {
return eqAddress(moduleAddress, config); return eqAddress(moduleAddress, config);
@ -438,6 +501,7 @@ export async function moduleMatchesConfig(
matches = eqAddress(expectedAddress, module.address); matches = eqAddress(expectedAddress, module.address);
break; break;
} }
case IsmType.FALLBACK_ROUTING:
case IsmType.ROUTING: { case IsmType.ROUTING: {
// A RoutingIsm matches if: // A RoutingIsm matches if:
// 1. The set of domains in the config equals those on-chain // 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 // Check that the RoutingISM owner matches the config
const owner = await routingIsm.owner(); const owner = await routingIsm.owner();
matches = matches && eqAddress(owner, config.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 // Recursively check that the submodule for each configured
// domain matches the submodule config. // domain matches the submodule config.
for (const [origin, subConfig] of Object.entries(config.domains)) { for (const [origin, subConfig] of Object.entries(config.domains)) {
@ -462,7 +535,7 @@ export async function moduleMatchesConfig(
subConfig, subConfig,
multiProvider, multiProvider,
contracts, contracts,
origin, mailbox,
); );
matches = matches && subModuleMatches; matches = matches && subModuleMatches;
} }

@ -26,6 +26,7 @@ export enum ModuleType {
export enum IsmType { export enum IsmType {
OP_STACK = 'opStackIsm', OP_STACK = 'opStackIsm',
ROUTING = 'domainRoutingIsm', ROUTING = 'domainRoutingIsm',
FALLBACK_ROUTING = 'defaultFallbackRoutingIsm',
AGGREGATION = 'staticAggregationIsm', AGGREGATION = 'staticAggregationIsm',
MERKLE_ROOT_MULTISIG = 'merkleRootMultisigIsm', MERKLE_ROOT_MULTISIG = 'merkleRootMultisigIsm',
MESSAGE_ID_MULTISIG = 'messageIdMultisigIsm', MESSAGE_ID_MULTISIG = 'messageIdMultisigIsm',
@ -39,6 +40,8 @@ export function ismTypeToModuleType(ismType: IsmType): ModuleType {
return ModuleType.NULL; return ModuleType.NULL;
case IsmType.ROUTING: case IsmType.ROUTING:
return ModuleType.ROUTING; return ModuleType.ROUTING;
case IsmType.FALLBACK_ROUTING:
return ModuleType.ROUTING;
case IsmType.AGGREGATION: case IsmType.AGGREGATION:
return ModuleType.AGGREGATION; return ModuleType.AGGREGATION;
case IsmType.MERKLE_ROOT_MULTISIG: case IsmType.MERKLE_ROOT_MULTISIG:
@ -64,11 +67,9 @@ export type TestIsmConfig = {
}; };
export type RoutingIsmConfig = { export type RoutingIsmConfig = {
type: IsmType.ROUTING; type: IsmType.ROUTING | IsmType.FALLBACK_ROUTING;
owner: Address; owner: Address;
domains: ChainMap<IsmConfig>; domains: ChainMap<IsmConfig>;
// TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/2895
// defaultFallback?: boolean;
}; };
export type AggregationIsmConfig = { export type AggregationIsmConfig = {
@ -93,6 +94,7 @@ export type IsmConfig =
export type DeployedIsmType = { export type DeployedIsmType = {
[IsmType.ROUTING]: IRoutingIsm; [IsmType.ROUTING]: IRoutingIsm;
[IsmType.FALLBACK_ROUTING]: IRoutingIsm;
[IsmType.AGGREGATION]: IAggregationIsm; [IsmType.AGGREGATION]: IAggregationIsm;
[IsmType.MERKLE_ROOT_MULTISIG]: IMultisigIsm; [IsmType.MERKLE_ROOT_MULTISIG]: IMultisigIsm;
[IsmType.MESSAGE_ID_MULTISIG]: IMultisigIsm; [IsmType.MESSAGE_ID_MULTISIG]: IMultisigIsm;

Loading…
Cancel
Save