UNPKG

@hyperlane-xyz/sdk

Version:

The official SDK for the Hyperlane Network

446 lines 21 kB
import { ethers } from 'ethers'; import { AbstractCcipReadIsm__factory, AbstractRoutingIsm__factory, AmountRoutingIsm__factory, ArbL2ToL1Ism__factory, CCIPIsm__factory, DefaultFallbackRoutingIsm__factory, IInterchainSecurityModule__factory, IMultisigIsm__factory, IOutbox__factory, InterchainAccountRouter__factory, OPStackIsm__factory, Ownable__factory, PausableIsm__factory, RateLimitedIsm__factory, StaticAggregationIsm__factory, TrustedRelayerIsm__factory, } from '@hyperlane-xyz/core'; import { assert, concurrentMap, getLogLevel, objMap, promiseObjAll, rootLogger, } from '@hyperlane-xyz/utils'; import { getChainNameFromCCIPSelector } from '../ccip/utils.js'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js'; import { HyperlaneReader } from '../utils/HyperlaneReader.js'; import { contractHasString, isMissingSelectorCallException, throwIfNotMissingSelector, } from '../utils/contract.js'; import { IsmType, ModuleType, } from './types.js'; const INCREMENTAL_REVERT_STRING = 'IncrementalDomainRoutingIsm: removal not supported'; // ISM types that cannot be deployed by HyperlaneIsmFactory — preserve as address strings. const NON_REDEPLOYABLE_ISM_TYPES = new Set([ IsmType.OFFCHAIN_LOOKUP, IsmType.INTERCHAIN_ACCOUNT_ROUTING, ]); export class EvmIsmReader extends HyperlaneReader { multiProvider; chain; concurrency; messageContext; logger = rootLogger.child({ module: 'EvmIsmReader' }); isZkSyncChain; constructor(multiProvider, chain, concurrency = multiProvider.tryGetRpcConcurrency(chain) ?? DEFAULT_CONTRACT_READ_CONCURRENCY, messageContext) { super(multiProvider, chain); this.multiProvider = multiProvider; this.chain = chain; this.concurrency = concurrency; this.messageContext = messageContext; // So we can distinguish between Storage/Static ISMs const chainTechnicalStack = this.multiProvider.getChainMetadata(this.chain).technicalStack; this.isZkSyncChain = chainTechnicalStack === ChainTechnicalStack.ZkSync; } async deriveIsmConfigFromAddress(address) { let moduleType = undefined; let derivedIsmConfig; try { const ism = IInterchainSecurityModule__factory.connect(address, this.provider); this.logger.debug('Deriving IsmConfig:', { address }); // Temporarily turn off SmartProvider logging // Provider errors are expected because deriving will call methods that may not exist in the Bytecode this.setSmartProviderLogLevel('silent'); moduleType = await ism.moduleType(); switch (moduleType) { case ModuleType.UNUSED: throw new Error('UNUSED does not have a corresponding IsmType'); case ModuleType.ROUTING: // IsmType is either ROUTING or FALLBACK_ROUTING, but that's determined inside deriveRoutingConfig derivedIsmConfig = await this.deriveRoutingConfig(address); break; case ModuleType.AGGREGATION: derivedIsmConfig = await this.deriveAggregationConfig(address); break; case ModuleType.LEGACY_MULTISIG: throw new Error('LEGACY_MULTISIG is deprecated and not supported'); case ModuleType.MERKLE_ROOT_MULTISIG: case ModuleType.MESSAGE_ID_MULTISIG: derivedIsmConfig = await this.deriveMultisigConfig(address); break; case ModuleType.NULL: derivedIsmConfig = await this.deriveNullConfig(address); break; case ModuleType.CCIP_READ: derivedIsmConfig = await this.deriveOffchainLookupConfig(address); break; case ModuleType.ARB_L2_TO_L1: return this.deriveArbL2ToL1Config(address); default: throw new Error(`Unknown ISM ModuleType: ${moduleType}`); } } catch (e) { const errorMessage = `Failed to derive ISM module type ${moduleType} on ${this.chain} (${address}) :\n\t${e}`; this.logger.debug(errorMessage); throw new Error(errorMessage); } finally { this.setSmartProviderLogLevel(getLogLevel()); // returns to original level defined by rootLogger } return derivedIsmConfig; } async deriveOffchainLookupConfig(address) { const ism = AbstractCcipReadIsm__factory.connect(address, this.provider); this.assertModuleType(await ism.moduleType(), ModuleType.CCIP_READ); const [urls, owner] = await Promise.all([ism.urls(), ism.owner()]); return { address, type: IsmType.OFFCHAIN_LOOKUP, urls, owner, }; } // expands ISM configs that are set as addresses by deriving the config // from the on-chain deployment async deriveIsmConfig(config) { if (typeof config === 'string') return this.deriveIsmConfigFromAddress(config); // Extend the inner isms switch (config.type) { case IsmType.FALLBACK_ROUTING: case IsmType.ROUTING: config.domains = await promiseObjAll(objMap(config.domains, async (_, ism) => { const derived = await this.deriveIsmConfig(ism); return this.preserveUnredeployableIsm(ism, derived); })); break; case IsmType.AGGREGATION: case IsmType.STORAGE_AGGREGATION: config.modules = await Promise.all(config.modules.map(async (ism) => { const derived = await this.deriveIsmConfig(ism); return this.preserveUnredeployableIsm(ism, derived); })); break; case IsmType.AMOUNT_ROUTING: { const lowerOrig = config.lowerIsm; const upperOrig = config.upperIsm; const [lowerDerived, upperDerived] = await Promise.all([ this.deriveIsmConfig(lowerOrig), this.deriveIsmConfig(upperOrig), ]); config.lowerIsm = this.preserveUnredeployableIsm(lowerOrig, lowerDerived); config.upperIsm = this.preserveUnredeployableIsm(upperOrig, upperDerived); break; } } return config; } // Returns the original IsmConfig for non-redeployable ISM types (e.g. OFFCHAIN_LOOKUP, // INTERCHAIN_ACCOUNT_ROUTING) so normalizeConfig and deploy() handle them correctly. // The original is typically a string address that survives normalizeConfig intact and // reaches deploy()'s string branch; an object config is also preserved via derived.address. preserveUnredeployableIsm(original, derived) { if (!NON_REDEPLOYABLE_ISM_TYPES.has(derived.type)) return derived; if (typeof original === 'string') return original; return derived.address; } async deriveRoutingConfig(address) { const abstractRoutingIsm = AbstractRoutingIsm__factory.connect(address, this.provider); this.assertModuleType(await abstractRoutingIsm.moduleType(), ModuleType.ROUTING); // OPTIMIZATION: When we have messageContext, we only need to derive // the specific ISM that will verify this message, not the full routing table. // Just call route(message) and derive that single ISM directly. if (this.messageContext) { const routedIsmAddress = await abstractRoutingIsm.route(this.messageContext.message); return this.deriveIsmConfig(routedIsmAddress); } const isIca = await this.isIcaRouter(address); let owner; const ownableIsm = Ownable__factory.connect(address, this.provider); try { owner = await ownableIsm.owner(); } catch (error) { throwIfNotMissingSelector(error); this.logger.debug('Error accessing owner property, implying that this is not a DefaultFallbackRoutingIsm.', address); } // If the current ISM does not have an owner then it is an Amount Router if (!owner) { if (isIca) { return { type: IsmType.INTERCHAIN_ACCOUNT_ROUTING, isms: {}, address, owner: ethers.constants.AddressZero, }; } return this.deriveNonOwnableRoutingConfig(address); } const defaultFallbackIsmInstance = DefaultFallbackRoutingIsm__factory.connect(address, this.provider); const domainIds = await defaultFallbackIsmInstance.domains(); // Detect ICA routing ISM (full or minimal router) if (isIca) { const icaRouter = InterchainAccountRouter__factory.connect(address, this.provider); return { type: IsmType.INTERCHAIN_ACCOUNT_ROUTING, isms: await this.deriveRemoteIsmConfigs(domainIds, abstractRoutingIsm, (domain) => icaRouter.isms(domain), // The isms here are deployed on remote chains and can't be derived false), address, owner, }; } const domains = await this.deriveRemoteIsmConfigs(domainIds, abstractRoutingIsm, (domain) => defaultFallbackIsmInstance.module(domain), true); // Fallback routing ISM extends from MailboxClient, default routing let ismType = IsmType.FALLBACK_ROUTING; try { await defaultFallbackIsmInstance.mailbox(); } catch (error) { throwIfNotMissingSelector(error); ismType = IsmType.ROUTING; this.logger.debug('Error accessing mailbox property, implying this is not a fallback routing ISM.', address); } // Check if it's an incremental routing ISM by looking for the unique error message in bytecode if (ismType === IsmType.ROUTING) { if (await contractHasString(this.provider, address, INCREMENTAL_REVERT_STRING)) { ismType = IsmType.INCREMENTAL_ROUTING; this.logger.debug({ address }, 'Detected incremental routing ISM'); } } return { owner, address, type: ismType, domains, }; } /** * Detects whether the contract at `address` is an ICA router * by probing for CCIP_READ_ISM() (full router) or bytecodeHash() (minimal router). */ async isIcaRouter(address) { const icaInstance = InterchainAccountRouter__factory.connect(address, this.provider); try { await icaInstance.CCIP_READ_ISM(); return true; } catch (error) { throwIfNotMissingSelector(error); try { await icaInstance.bytecodeHash(); return true; } catch (innerError) { throwIfNotMissingSelector(innerError); this.logger.debug('Not an ICA router (no CCIP_READ_ISM or bytecodeHash).', address); return false; } } } async deriveRemoteIsmConfigs(domainIds, contractInstance, addressDeriveFunc, deriveConfig) { const res = await concurrentMap(this.concurrency, domainIds, async (domainId) => { const chainName = this.multiProvider.tryGetChainName(domainId.toNumber()); if (!chainName) { this.logger.warn(`Unknown domain ID ${domainId.toString()}, skipping domain configuration`); return; } const moduleAddress = this.messageContext ? await contractInstance.route(this.messageContext.message) : await addressDeriveFunc(domainId); if (deriveConfig) { const derived = await this.deriveIsmConfigFromAddress(moduleAddress); return [ chainName, this.preserveUnredeployableIsm(moduleAddress, derived), ]; } return [chainName, moduleAddress]; }); return Object.fromEntries(res.filter((curr) => curr)); } async deriveNonOwnableRoutingConfig(address) { const ism = AmountRoutingIsm__factory.connect(address, this.provider); let lowerIsm; let upperIsm; let threshold; try { [lowerIsm, upperIsm, threshold] = await Promise.all([ ism.lower(), ism.upper(), ism.threshold(), ]); } catch (error) { throwIfNotMissingSelector(error); // If we fail to access AmountRoutingIsm properties, this is likely a legacy InterchainAccountIsm this.logger.debug('Error accessing AmountRoutingIsm properties, treating as legacy InterchainAccountIsm.', address); // return a basic ICA routing config for legacy contracts return { type: IsmType.INTERCHAIN_ACCOUNT_ROUTING, isms: {}, address, owner: ethers.constants.AddressZero, }; } const [lowerDerived, upperDerived] = await Promise.all([ this.deriveIsmConfigFromAddress(lowerIsm), this.deriveIsmConfigFromAddress(upperIsm), ]); return { type: IsmType.AMOUNT_ROUTING, address, lowerIsm: this.preserveUnredeployableIsm(lowerIsm, lowerDerived), upperIsm: this.preserveUnredeployableIsm(upperIsm, upperDerived), threshold: threshold.toNumber(), }; } async deriveAggregationConfig(address) { const ism = StaticAggregationIsm__factory.connect(address, this.provider); this.assertModuleType(await ism.moduleType(), ModuleType.AGGREGATION); const [modules, threshold] = await ism.modulesAndThreshold(ethers.constants.AddressZero); const ismConfigs = await concurrentMap(this.concurrency, modules, async (module) => { const derived = await this.deriveIsmConfigFromAddress(module); return this.preserveUnredeployableIsm(module, derived); }); // If it's a zkSync chain, it must be a StorageAggregationIsm const ismType = this.isZkSyncChain ? IsmType.STORAGE_AGGREGATION : IsmType.AGGREGATION; return { address, type: ismType, modules: ismConfigs, threshold, }; } async deriveMultisigConfig(address) { const ism = IMultisigIsm__factory.connect(address, this.provider); const moduleType = await ism.moduleType(); assert(moduleType === ModuleType.MERKLE_ROOT_MULTISIG || moduleType === ModuleType.MESSAGE_ID_MULTISIG, `expected module type to be ${ModuleType.MERKLE_ROOT_MULTISIG} or ${ModuleType.MESSAGE_ID_MULTISIG}, got ${moduleType}`); let ismType = moduleType === ModuleType.MERKLE_ROOT_MULTISIG ? IsmType.MERKLE_ROOT_MULTISIG : IsmType.MESSAGE_ID_MULTISIG; // If it's a zkSync chain, it must be a StorageMultisigIsm if (this.isZkSyncChain) { ismType = moduleType === ModuleType.MERKLE_ROOT_MULTISIG ? IsmType.STORAGE_MERKLE_ROOT_MULTISIG : IsmType.STORAGE_MESSAGE_ID_MULTISIG; } const [validators, threshold] = await ism.validatorsAndThreshold(ethers.constants.AddressZero); return { address, type: ismType, validators, threshold, }; } async deriveNullConfig(address) { const ism = IInterchainSecurityModule__factory.connect(address, this.provider); this.assertModuleType(await ism.moduleType(), ModuleType.NULL); // if it has trustedRelayer() property --> TRUSTED_RELAYER const trustedRelayerIsm = TrustedRelayerIsm__factory.connect(address, this.provider); try { const relayer = await trustedRelayerIsm.trustedRelayer(); return { address, relayer, type: IsmType.TRUSTED_RELAYER, }; } catch (error) { throwIfNotMissingSelector(error); this.logger.debug('Error accessing "trustedRelayer" property, implying this is not a Trusted Relayer ISM.', address); } // if it has paused() property --> PAUSABLE const pausableIsm = PausableIsm__factory.connect(address, this.provider); const [pausedResult, ownerResult] = await Promise.allSettled([ pausableIsm.paused(), pausableIsm.owner(), ]); const unexpectedError = [pausedResult, ownerResult].find((result) => result.status === 'rejected' && !isMissingSelectorCallException(result.reason)); if (unexpectedError?.status === 'rejected') { throw unexpectedError.reason; } if (pausedResult.status === 'fulfilled' && ownerResult.status === 'fulfilled') { return { address, owner: ownerResult.value, type: IsmType.PAUSABLE, paused: pausedResult.value, }; } else { this.logger.debug('Error accessing "paused" property, implying this is not a Pausable ISM.', address); } // if it has ccipOrigin property --> CCIP const ccipIsm = CCIPIsm__factory.connect(address, this.provider); try { const ccipOrigin = await ccipIsm.ccipOrigin(); const originChain = getChainNameFromCCIPSelector(ccipOrigin.toString()); if (!originChain) { throw new Error('Unknown CCIP origin chain'); } return { address, type: IsmType.CCIP, originChain, }; } catch (error) { throwIfNotMissingSelector(error); this.logger.debug('Error accessing "ccipOrigin" property, implying this is not a CCIP ISM.', address); } // if it has VERIFIED_MASK_INDEX, it's AbstractMessageIdAuthorizedIsm which means OPStackIsm const opStackIsm = OPStackIsm__factory.connect(address, this.provider); try { await opStackIsm.VERIFIED_MASK_INDEX(); return { address, type: IsmType.OP_STACK, origin: address, nativeBridge: '', // no way to extract native bridge from the ism }; } catch (error) { throwIfNotMissingSelector(error); this.logger.debug('Error accessing "VERIFIED_MASK_INDEX" property, implying this is not an OP Stack ISM.', address); } // Detect RateLimitedIsm by probing for its unique recipient() selector. // Safe today because no other NULL-type ISM in this repo exposes recipient(). // TODO: replace with bytecode or unique-selector matching if custom NULL ISMs // with the same interface are ever introduced. const rateLimitedIsm = RateLimitedIsm__factory.connect(address, this.provider); try { const recipient = await rateLimitedIsm.recipient(); const maxCapacity = (await rateLimitedIsm.maxCapacity()).toString(); const owner = await rateLimitedIsm.owner(); return { address, type: IsmType.RATE_LIMITED, recipient, maxCapacity, owner, }; } catch (error) { throwIfNotMissingSelector(error); this.logger.debug('Error accessing "recipient" property, implying this is not a Rate Limited ISM.', address); } // no specific properties, must be Test ISM return { address, type: IsmType.TEST_ISM, }; } async deriveArbL2ToL1Config(address) { const ism = ArbL2ToL1Ism__factory.connect(address, this.provider); const outbox = await ism.arbOutbox(); const outboxContract = IOutbox__factory.connect(outbox, this.provider); const bridge = await outboxContract.bridge(); return { address, type: IsmType.ARB_L2_TO_L1, bridge, }; } assertModuleType(moduleType, expectedModuleType) { assert(moduleType === expectedModuleType, `expected module type to be ${expectedModuleType}, got ${moduleType}`); } } //# sourceMappingURL=EvmIsmReader.js.map