diff --git a/typescript/sdk/src/crud/AbstractCrudModule.ts b/typescript/sdk/src/crud/AbstractCrudModule.ts index 255258449f7..2c697dd64ad 100644 --- a/typescript/sdk/src/crud/AbstractCrudModule.ts +++ b/typescript/sdk/src/crud/AbstractCrudModule.ts @@ -2,34 +2,24 @@ import { Logger } from 'pino'; import { Address, Annotated, ProtocolType } from '@hyperlane-xyz/utils'; -import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js'; -import { - ProtocolTypedProvider, - ProtocolTypedTransaction, -} from '../providers/ProviderType.js'; +import { ProtocolTypedTransaction } from '../providers/ProviderType.js'; import { ChainNameOrId } from '../types.js'; -export type CrudModuleArgs< - TProtocol extends ProtocolType, - TConfig, - TAddressMap extends Record, -> = { +export type CrudModuleArgs> = { addresses: TAddressMap; chain: ChainNameOrId; - chainMetadataManager: ChainMetadataManager; config: TConfig; - provider: ProtocolTypedProvider['provider']; }; export abstract class CrudModule< TProtocol extends ProtocolType, TConfig, - TAddressMap extends Record, + TAddressMap extends Record, > { protected abstract readonly logger: Logger; protected constructor( - protected readonly args: CrudModuleArgs, + protected readonly args: CrudModuleArgs, ) {} public serialize(): TAddressMap { diff --git a/typescript/sdk/src/crud/EvmHookModule.ts b/typescript/sdk/src/crud/EvmHookModule.ts index 727135090e9..b0b88b945a6 100644 --- a/typescript/sdk/src/crud/EvmHookModule.ts +++ b/typescript/sdk/src/crud/EvmHookModule.ts @@ -20,19 +20,9 @@ export class EvmHookModule extends CrudModule< protected constructor( protected readonly multiProvider: MultiProvider, - args: Omit< - CrudModuleArgs< - ProtocolType.Ethereum, - HookConfig, - HyperlaneAddresses - >, - 'provider' - >, + args: CrudModuleArgs>, ) { - super({ - ...args, - provider: multiProvider.getProvider(args.chain), - }); + super(args); this.reader = new EvmHookReader(multiProvider, args.chain); } diff --git a/typescript/sdk/src/crud/EvmIsmModule.ts b/typescript/sdk/src/crud/EvmIsmModule.ts index 820c4bc84cf..a9ccef1ab94 100644 --- a/typescript/sdk/src/crud/EvmIsmModule.ts +++ b/typescript/sdk/src/crud/EvmIsmModule.ts @@ -1,11 +1,14 @@ import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils'; -import { HyperlaneAddresses } from '../contracts/types.js'; +import { HyperlaneContracts } from '../contracts/types.js'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { EvmIsmCreator } from '../ism/EvmIsmCreator.js'; import { EvmIsmReader } from '../ism/read.js'; import { IsmConfig } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { EthersV5Transaction } from '../providers/ProviderType.js'; +import { ChainNameOrId } from '../types.js'; import { CrudModule, CrudModuleArgs } from './AbstractCrudModule.js'; @@ -13,32 +16,29 @@ import { CrudModule, CrudModuleArgs } from './AbstractCrudModule.js'; export class EvmIsmModule extends CrudModule< ProtocolType.Ethereum, IsmConfig, - HyperlaneAddresses + HyperlaneContracts & { + deployedIsm: Address; + } > { protected logger = rootLogger.child({ module: 'EvmIsmModule' }); protected reader: EvmIsmReader; protected constructor( protected readonly multiProvider: MultiProvider, - args: Omit< - CrudModuleArgs< - ProtocolType.Ethereum, - IsmConfig, - HyperlaneAddresses - >, - 'provider' + args: CrudModuleArgs< + IsmConfig, + HyperlaneContracts & { + deployedIsm: Address; + } >, ) { - super({ - ...args, - provider: multiProvider.getProvider(args.chain), - }); + super(args); this.reader = new EvmIsmReader(multiProvider, args.chain); } - public async read(address: Address): Promise { - return await this.reader.deriveIsmConfig(address); + public async read(): Promise { + return await this.reader.deriveIsmConfig(this.args.addresses.deployedIsm); } public async update(_config: IsmConfig): Promise { @@ -46,7 +46,32 @@ export class EvmIsmModule extends CrudModule< } // manually write static create function - public static create(_config: IsmConfig): Promise { - throw new Error('not implemented'); + public static async create({ + chain, + config, + deployer, + factories, + multiProvider, + }: { + chain: ChainNameOrId; + config: IsmConfig; + deployer: HyperlaneDeployer; + factories: HyperlaneContracts; + multiProvider: MultiProvider; + }): Promise { + const destination = multiProvider.getChainName(chain); + const ismCreator = new EvmIsmCreator(deployer, multiProvider, factories); + const deployedIsm = await ismCreator.deploy({ + config, + destination, + }); + return new EvmIsmModule(multiProvider, { + addresses: { + ...factories, + deployedIsm: deployedIsm.address, + }, + chain, + config, + }); } } diff --git a/typescript/sdk/src/ism/EvmIsmCreator.ts b/typescript/sdk/src/ism/EvmIsmCreator.ts new file mode 100644 index 00000000000..ac55fbafa31 --- /dev/null +++ b/typescript/sdk/src/ism/EvmIsmCreator.ts @@ -0,0 +1,462 @@ +import { ethers } from 'ethers'; +import { Logger } from 'pino'; + +import { + DefaultFallbackRoutingIsm, + DefaultFallbackRoutingIsm__factory, + DomainRoutingIsm, + DomainRoutingIsm__factory, + IAggregationIsm, + IAggregationIsm__factory, + IInterchainSecurityModule__factory, + IMultisigIsm, + IMultisigIsm__factory, + IRoutingIsm, + OPStackIsm__factory, + PausableIsm__factory, + StaticAddressSetFactory, + StaticThresholdAddressSetFactory, + TestIsm__factory, + TrustedRelayerIsm__factory, +} from '@hyperlane-xyz/core'; +import { + Address, + Domain, + assert, + eqAddress, + objFilter, + rootLogger, +} from '@hyperlane-xyz/utils'; + +import { chainMetadata } from '../consts/chainMetadata.js'; +import { HyperlaneContracts } from '../contracts/types.js'; +import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; +import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { resolveOrDeployAccountOwner } from '../deploy/types.js'; +import { MultiProvider } from '../providers/MultiProvider.js'; +import { ChainMap, ChainName } from '../types.js'; + +import { + AggregationIsmConfig, + DeployedIsm, + DeployedIsmType, + IsmConfig, + IsmType, + MultisigIsmConfig, + RoutingIsmConfig, + RoutingIsmDelta, +} from './types.js'; +import { routingModuleDelta } from './utils.js'; + +// extends HyperlaneApp + +export class EvmIsmCreator { + // The shape of this object is `ChainMap
`, + // although `any` is use here because that type breaks a lot of signatures. + // TODO: fix this in the next refactoring + public deployedIsms: ChainMap = {}; + + protected readonly logger = rootLogger.child({ module: 'EvmIsmCreator' }); + + constructor( + protected readonly deployer: HyperlaneDeployer, + protected readonly multiProvider: MultiProvider, + protected readonly factories: HyperlaneContracts, + ) {} + + async deploy(params: { + destination: ChainName; + config: C; + origin?: ChainName; + mailbox?: Address; + existingIsmAddress?: Address; + }): Promise { + const { destination, config, origin, mailbox, existingIsmAddress } = params; + if (typeof config === 'string') { + // @ts-ignore + return IInterchainSecurityModule__factory.connect( + config, + this.multiProvider.getSignerOrProvider(destination), + ); + } + + const ismType = config.type; + const logger = this.logger.child({ destination, ismType }); + + logger.debug( + `Deploying ${ismType} to ${destination} ${ + origin ? `(for verifying ${origin})` : '' + }`, + ); + + let contract: DeployedIsmType[typeof ismType]; + switch (ismType) { + case IsmType.MESSAGE_ID_MULTISIG: + case IsmType.MERKLE_ROOT_MULTISIG: + contract = await this.deployMultisigIsm(destination, config, logger); + break; + case IsmType.ROUTING: + case IsmType.FALLBACK_ROUTING: + contract = await this.deployRoutingIsm({ + destination, + config, + origin, + mailbox, + existingIsmAddress, + logger, + }); + break; + case IsmType.AGGREGATION: + contract = await this.deployAggregationIsm({ + destination, + config, + origin, + mailbox, + logger, + }); + break; + case IsmType.OP_STACK: + assert( + this.deployer, + `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: + assert( + this.deployer, + `HyperlaneDeployer must be set to deploy ${ismType}`, + ); + contract = await this.deployer.deployContractFromFactory( + destination, + new PausableIsm__factory(), + IsmType.PAUSABLE, + [ + await resolveOrDeployAccountOwner( + this.multiProvider, + destination, + config.owner, + ), + ], + ); + await this.deployer.transferOwnershipOfContracts(destination, config, { + [IsmType.PAUSABLE]: contract, + }); + break; + case IsmType.TRUSTED_RELAYER: + assert( + this.deployer, + `HyperlaneDeployer must be set to deploy ${ismType}`, + ); + assert(mailbox, `Mailbox address is required for deploying ${ismType}`); + contract = await this.deployer.deployContractFromFactory( + destination, + new TrustedRelayerIsm__factory(), + IsmType.TRUSTED_RELAYER, + [mailbox, config.relayer], + ); + break; + case IsmType.TEST_ISM: + 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; + default: + throw new Error(`Unsupported ISM type ${ismType}`); + } + + 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[destination][origin]) { + this.deployedIsms[destination][origin] = {}; + } + this.deployedIsms[destination][origin][ismType] = contract; + } else { + // otherwise store the entry directly + this.deployedIsms[destination][ismType] = contract; + } + + return contract; + } + + protected async deployMultisigIsm( + destination: ChainName, + config: MultisigIsmConfig, + logger: Logger, + ): Promise { + const signer = this.multiProvider.getSigner(destination); + const multisigIsmFactory = + config.type === IsmType.MERKLE_ROOT_MULTISIG + ? this.factories.staticMerkleRootMultisigIsmFactory + : this.factories.staticMessageIdMultisigIsmFactory; + + const address = await this.deployStaticAddressSet( + destination, + multisigIsmFactory, + config.validators, + logger, + config.threshold, + ); + + return IMultisigIsm__factory.connect(address, signer); + } + + protected async deployRoutingIsm(params: { + destination: ChainName; + config: RoutingIsmConfig; + origin?: ChainName; + mailbox?: Address; + existingIsmAddress?: Address; + logger: Logger; + }): Promise { + const { destination, config, mailbox, existingIsmAddress, logger } = params; + const overrides = this.multiProvider.getTransactionOverrides(destination); + const domainRoutingIsmFactory = this.factories.domainRoutingIsmFactory; + let routingIsm: DomainRoutingIsm | DefaultFallbackRoutingIsm; + // filtering out domains which are not part of the multiprovider + config.domains = objFilter( + config.domains, + (domain, config): config is IsmConfig => { + const domainId = + chainMetadata[domain]?.domainId ?? + this.multiProvider.tryGetDomainId(domain); + if (domainId === null) { + logger.warn( + `Domain ${domain} doesn't have chain metadata provided, skipping ...`, + ); + } + return domainId !== null; + }, + ); + const safeConfigDomains = Object.keys(config.domains).map( + (domain) => + chainMetadata[domain]?.domainId ?? + this.multiProvider.getDomainId(domain), + ); + const delta: RoutingIsmDelta = existingIsmAddress + ? await routingModuleDelta( + destination, + existingIsmAddress, + config, + this.multiProvider, + this.factories, + mailbox, + ) + : { + domainsToUnenroll: [], + domainsToEnroll: safeConfigDomains, + }; + + const signer = this.multiProvider.getSigner(destination); + const provider = this.multiProvider.getProvider(destination); + let isOwner = false; + if (existingIsmAddress) { + const owner = await DomainRoutingIsm__factory.connect( + existingIsmAddress, + provider, + ).owner(); + isOwner = eqAddress(await signer.getAddress(), owner); + } + // reconfiguring existing routing ISM + if (existingIsmAddress && isOwner && !delta.mailbox) { + const isms: Record = {}; + routingIsm = DomainRoutingIsm__factory.connect( + existingIsmAddress, + this.multiProvider.getSigner(destination), + ); + // 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.debug( + `Reconfiguring preexisting routing ISM at for origin ${origin}...`, + ); + const ism = await this.deploy({ + destination, + config: config.domains[origin], + origin, + mailbox, + }); + isms[originDomain] = ism.address; + const tx = await routingIsm.set( + originDomain, + isms[originDomain], + overrides, + ); + await this.multiProvider.handleTx(destination, tx); + } + // unenrolling domains if needed + for (const originDomain of delta.domainsToUnenroll) { + logger.debug( + `Unenrolling originDomain ${originDomain} from preexisting routing ISM at ${existingIsmAddress}...`, + ); + const tx = await routingIsm.remove(originDomain, overrides); + await this.multiProvider.handleTx(destination, tx); + } + // transfer ownership if needed + if (delta.owner) { + logger.debug(`Transferring ownership of routing ISM...`); + const tx = await routingIsm.transferOwnership(delta.owner, overrides); + await this.multiProvider.handleTx(destination, tx); + } + } else { + const isms: ChainMap
= {}; + const owner = await resolveOrDeployAccountOwner( + this.multiProvider, + destination, + config.owner, + ); + for (const origin of Object.keys(config.domains)) { + const ism = await this.deploy({ + destination, + config: config.domains[origin], + origin, + mailbox, + }); + isms[origin] = ism.address; + } + const submoduleAddresses = Object.values(isms); + let receipt: ethers.providers.TransactionReceipt; + if (config.type === IsmType.FALLBACK_ROUTING) { + // deploying new fallback routing ISM + if (!mailbox) { + throw new Error( + 'Mailbox address is required for deploying fallback routing ISM', + ); + } + logger.debug('Deploying fallback routing ISM ...'); + routingIsm = await this.multiProvider.handleDeploy( + destination, + new DefaultFallbackRoutingIsm__factory(), + [mailbox], + ); + logger.debug('Initialising fallback routing ISM ...'); + receipt = await this.multiProvider.handleTx( + destination, + routingIsm['initialize(address,uint32[],address[])']( + owner, + safeConfigDomains, + submoduleAddresses, + overrides, + ), + ); + } else { + // deploying new domain routing ISM + const owner = await resolveOrDeployAccountOwner( + this.multiProvider, + destination, + config.owner, + ); + const tx = await domainRoutingIsmFactory.deploy( + owner, + safeConfigDomains, + 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 domainRoutingIsmFactory.interface.parseLog(log); + } catch (e) { + return undefined; + } + }) + .filter( + (log): log is ethers.utils.LogDescription => + !!log && log.name === 'ModuleDeployed', + ); + if (dispatchLogs.length === 0) { + throw new Error('No ModuleDeployed event found'); + } + const moduleAddress = dispatchLogs[0].args['module']; + routingIsm = DomainRoutingIsm__factory.connect( + moduleAddress, + this.multiProvider.getSigner(destination), + ); + } + } + return routingIsm; + } + + protected async deployAggregationIsm(params: { + destination: ChainName; + config: AggregationIsmConfig; + origin?: ChainName; + mailbox?: Address; + logger: Logger; + }): Promise { + const { destination, config, origin, mailbox } = params; + const signer = this.multiProvider.getSigner(destination); + const staticAggregationIsmFactory = + this.factories.staticAggregationIsmFactory; + const addresses: Address[] = []; + for (const module of config.modules) { + const submodule = await this.deploy({ + destination, + config: module, + origin, + mailbox, + }); + addresses.push(submodule.address); + } + const address = await this.deployStaticAddressSet( + destination, + staticAggregationIsmFactory, + addresses, + params.logger, + config.threshold, + ); + return IAggregationIsm__factory.connect(address, signer); + } + + async deployStaticAddressSet( + chain: ChainName, + factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory, + values: Address[], + logger: Logger, + threshold = values.length, + ): Promise
{ + const sorted = [...values].sort(); + + const address = await factory['getAddress(address[],uint8)']( + sorted, + threshold, + ); + const code = await this.multiProvider.getProvider(chain).getCode(address); + if (code === '0x') { + logger.debug( + `Deploying new ${threshold} of ${values.length} address set to ${chain}`, + ); + const overrides = this.multiProvider.getTransactionOverrides(chain); + const hash = await factory['deploy(address[],uint8)']( + sorted, + threshold, + overrides, + ); + await this.multiProvider.handleTx(chain, hash); + // TODO: add proxy verification artifact? + } else { + logger.debug( + `Recovered ${threshold} of ${values.length} address set on ${chain}: ${address}`, + ); + } + return address; + } +}