UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

209 lines 9.89 kB
import { ethers } from 'ethers'; import { AbstractCcipReadIsm__factory, DomainRoutingIsm__factory, PausableIsm__factory, } from '@hyperlane-xyz/core'; import { arrayEqual, assert, deepEquals, intersection, isZeroishAddress, rootLogger, } from '@hyperlane-xyz/utils'; import { transferOwnershipTransactions } from '../contracts/contracts.js'; import { HyperlaneModule, } from '../core/AbstractHyperlaneModule.js'; import { normalizeConfig } from '../utils/ism.js'; import { EvmIsmReader } from './EvmIsmReader.js'; import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js'; import { IsmConfigSchema, IsmType, MUTABLE_ISM_TYPE, } from './types.js'; import { calculateDomainRoutingDelta } from './utils.js'; export class EvmIsmModule extends HyperlaneModule { multiProvider; contractVerifier; logger = rootLogger.child({ module: 'EvmIsmModule' }); reader; ismFactory; mailbox; // Adding these to reduce how often we need to grab from MultiProvider. chain; chainId; domainId; constructor(multiProvider, params, ccipContractCache, contractVerifier) { params.config = IsmConfigSchema.parse(params.config); super(params); this.multiProvider = multiProvider; this.contractVerifier = contractVerifier; this.reader = new EvmIsmReader(multiProvider, params.chain); this.ismFactory = HyperlaneIsmFactory.fromAddressesMap({ [params.chain]: params.addresses }, multiProvider, ccipContractCache, contractVerifier); this.mailbox = params.addresses.mailbox; this.chain = multiProvider.getChainName(this.args.chain); this.chainId = multiProvider.getEvmChainId(this.chain); this.domainId = multiProvider.getDomainId(this.chain); } async read() { return typeof this.args.config === 'string' ? this.args.addresses.deployedIsm : this.reader.deriveIsmConfig(this.args.addresses.deployedIsm); } // whoever calls update() needs to ensure that targetConfig has a valid owner async update(targetConfig) { targetConfig = IsmConfigSchema.parse(targetConfig); // Nothing to do if its the default ism if (typeof targetConfig === 'string' && isZeroishAddress(targetConfig)) { return []; } // We need to normalize the current and target configs to compare. const normalizedTargetConfig = normalizeConfig(await this.reader.deriveIsmConfig(targetConfig)); const normalizedCurrentConfig = normalizeConfig(await this.read()); // If configs match, no updates needed if (deepEquals(normalizedCurrentConfig, normalizedTargetConfig)) { return []; } // Update the module config to the target one as we are sure now that an update will be needed this.args.config = normalizedTargetConfig; // if the new config is an address just point the module to the new address if (typeof normalizedTargetConfig === 'string') { this.args.addresses.deployedIsm = normalizedTargetConfig; return []; } // Conditions for deploying a new ISM: // - If updating from an address/custom config to a proper ISM config. // - If updating a proper ISM config whose types are different. // - If it is not a mutable ISM. // Else, we have to figure out what an update for this ISM entails // Check if we need to deploy a new ISM if (typeof normalizedCurrentConfig === 'string' || normalizedCurrentConfig.type !== normalizedTargetConfig.type || !MUTABLE_ISM_TYPE.includes(normalizedTargetConfig.type)) { const contract = await this.deploy({ config: normalizedTargetConfig, }); this.args.addresses.deployedIsm = contract.address; return []; } // At this point, only the ownable/mutable ISM types should remain: PAUSABLE, ROUTING, FALLBACK_ROUTING, OFFCHAIN_LOOKUP return this.updateMutableIsm({ current: normalizedCurrentConfig, target: normalizedTargetConfig, }); } async updateMutableIsm({ current, target, }) { const updateTxs = []; assert(MUTABLE_ISM_TYPE.includes(current.type), `Expected mutable ISM type but got ${current.type}`); assert(current.type === target.type, `Updating Mutable ISMs requires both the expected and actual config to be of the same type`); const logger = this.logger.child({ destination: this.chain, ismType: target.type, }); logger.debug(`Updating ${target.type} on ${this.chain}`); if ((current.type === IsmType.ROUTING && target.type === IsmType.ROUTING) || (current.type === IsmType.FALLBACK_ROUTING && target.type === IsmType.FALLBACK_ROUTING)) { const txs = await this.updateRoutingIsm({ current, target, logger, }); updateTxs.push(...txs); } else if (current.type === IsmType.PAUSABLE && target.type === IsmType.PAUSABLE) { updateTxs.push(...this.updatePausableIsm({ current, target, })); } else if (current.type === IsmType.OFFCHAIN_LOOKUP && target.type === IsmType.OFFCHAIN_LOOKUP) { updateTxs.push(...this.updateOffchainLookupIsm({ current, target, })); } else { throw new Error(`Unsupported update to mutable ISM of type ${target.type}`); } // Lastly, check if the resolved owner is different from the current owner updateTxs.push(...transferOwnershipTransactions(this.chainId, this.args.addresses.deployedIsm, current, target)); return updateTxs; } // manually write static create function static async create({ chain, config, proxyFactoryFactories, mailbox, multiProvider, ccipContractCache, contractVerifier, }) { const module = new EvmIsmModule(multiProvider, { addresses: { ...proxyFactoryFactories, mailbox, deployedIsm: ethers.constants.AddressZero, }, chain, config, }, ccipContractCache, contractVerifier); const deployedIsm = await module.deploy({ config }); module.args.addresses.deployedIsm = deployedIsm.address; return module; } async updateRoutingIsm({ current, target, logger, }) { const contract = DomainRoutingIsm__factory.connect(this.args.addresses.deployedIsm, this.multiProvider.getProvider(this.chain)); const updateTxs = []; const knownChains = new Set(this.multiProvider.getKnownChainNames()); const { domainsToEnroll, domainsToUnenroll } = calculateDomainRoutingDelta(current, target); const knownEnrolls = intersection(knownChains, new Set(domainsToEnroll)); // Enroll domains for (const origin of knownEnrolls) { logger.debug(`Reconfiguring preexisting routing ISM for origin ${origin}...`); const ism = await this.deploy({ config: target.domains[origin], }); const domainId = this.multiProvider.getDomainId(origin); const tx = await contract.populateTransaction.set(domainId, ism.address); updateTxs.push({ chainId: this.chainId, annotation: `Setting new ISM for origin ${origin}...`, ...tx, }); } const knownUnenrolls = intersection(knownChains, new Set(domainsToUnenroll)); // Unenroll domains for (const origin of knownUnenrolls) { const domainId = this.multiProvider.getDomainId(origin); const tx = await contract.populateTransaction.remove(domainId); updateTxs.push({ chainId: this.chainId, annotation: `Unenrolling originDomain ${domainId} from preexisting routing ISM at ${this.args.addresses.deployedIsm}...`, ...tx, }); } return updateTxs; } updatePausableIsm({ current, target, }) { if (current.paused === target.paused) { return []; } const ismInterface = PausableIsm__factory.createInterface(); const data = target.paused ? ismInterface.encodeFunctionData('pause') : ismInterface.encodeFunctionData('unpause'); return [ { annotation: `${target.paused ? 'Pausing' : 'Unpausing'} Pausable ISM on chain "${this.chain}" and address "${this.args.addresses.deployedIsm}"`, chainId: this.multiProvider.getEvmChainId(this.chain), to: this.args.addresses.deployedIsm, data, }, ]; } updateOffchainLookupIsm({ current, target, }) { if (arrayEqual(target.urls, current.urls)) { return []; } return [ { annotation: `Setting urls to ${target.type} ISM on chain "${this.chain}" and address "${this.args.addresses.deployedIsm}"`, chainId: this.multiProvider.getEvmChainId(this.chain), to: this.args.addresses.deployedIsm, // The contract code just replaces the existing array with the new one data: AbstractCcipReadIsm__factory.createInterface().encodeFunctionData('setUrls(string[])', [target.urls]), }, ]; } async deploy({ config, }) { config = IsmConfigSchema.parse(config); return this.ismFactory.deploy({ destination: this.chain, config, mailbox: this.mailbox, }); } } //# sourceMappingURL=EvmIsmModule.js.map