@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
409 lines • 19.7 kB
JavaScript
import { ethers } from 'ethers';
import { AbstractStorageMultisigIsm__factory, AmountRoutingIsm__factory, CCIPIsm__factory, DomainRoutingIsm__factory, IAggregationIsm__factory, IInterchainSecurityModule__factory, IMultisigIsm__factory, IRoutingIsm__factory, MailboxClient__factory, OPStackIsm__factory, PausableIsm__factory, StaticAggregationIsm__factory, TrustedRelayerIsm__factory, } from '@hyperlane-xyz/core';
import { deepEquals, eqAddress, formatMessage, normalizeAddress, objMap, rootLogger, } from '@hyperlane-xyz/utils';
import { getChainNameFromCCIPSelector } from '../ccip/utils.js';
import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js';
import { normalizeConfig } from '../utils/ism.js';
import { IsmType, ModuleType, STATIC_ISM_TYPES, ismTypeToModuleType, } from './types.js';
const logger = rootLogger.child({ module: 'IsmUtils' });
// Determines the domains to enroll and unenroll to update the current ISM config
// to match the target ISM config.
export function calculateDomainRoutingDelta(current, target) {
const domainsToEnroll = [];
for (const origin of Object.keys(target.domains)) {
if (!current.domains[origin]) {
domainsToEnroll.push(origin);
}
else {
const subModuleMatches = deepEquals(current.domains[origin], target.domains[origin]);
if (!subModuleMatches)
domainsToEnroll.push(origin);
}
}
const domainsToUnenroll = Object.keys(current.domains).reduce((acc, origin) => {
if (!Object.keys(target.domains).includes(origin)) {
acc.push(origin);
}
return acc;
}, []);
return {
domainsToEnroll,
domainsToUnenroll,
};
}
/*
* The following functions are considered legacy and are deprecated. DO NOT USE.
* -----------------------------------------------------------------------------
*/
// Note that this function may return false negatives, but should
// not return false positives.
// This can happen if, for example, the module has sender, recipient, or
// body specific logic, as the sample message used when querying the ISM
// sets all of these to zero.
export async function moduleCanCertainlyVerify(destModule, multiProvider, origin, destination) {
const originDomainId = multiProvider.tryGetDomainId(origin);
const destinationDomainId = multiProvider.tryGetDomainId(destination);
if (!originDomainId || !destinationDomainId) {
return false;
}
const message = formatMessage(0, 0, originDomainId, ethers.constants.AddressZero, destinationDomainId, ethers.constants.AddressZero, '0x');
const provider = multiProvider.getSignerOrProvider(destination);
if (typeof destModule === 'string') {
const module = IInterchainSecurityModule__factory.connect(destModule, provider);
try {
const moduleType = await module.moduleType();
if (moduleType === ModuleType.MERKLE_ROOT_MULTISIG ||
moduleType === ModuleType.MESSAGE_ID_MULTISIG) {
const multisigModule = IMultisigIsm__factory.connect(destModule, provider);
const [, threshold] = await multisigModule.validatorsAndThreshold(message);
return threshold > 0;
}
else if (moduleType === ModuleType.ROUTING) {
const routingIsm = IRoutingIsm__factory.connect(destModule, provider);
const subModule = await routingIsm.route(message);
return moduleCanCertainlyVerify(subModule, multiProvider, origin, destination);
}
else if (moduleType === ModuleType.AGGREGATION) {
const aggregationIsm = IAggregationIsm__factory.connect(destModule, provider);
const [subModules, threshold] = await aggregationIsm.modulesAndThreshold(message);
let verified = 0;
for (const subModule of subModules) {
const canVerify = await moduleCanCertainlyVerify(subModule, multiProvider, origin, destination);
if (canVerify) {
verified += 1;
}
}
return verified >= threshold;
}
else {
throw new Error(`Unsupported module type: ${moduleType}`);
}
}
catch (err) {
logger.error(`Error checking module ${destModule}`, err);
return false;
}
}
else {
// destModule is an IsmConfig
switch (destModule.type) {
case IsmType.MERKLE_ROOT_MULTISIG:
case IsmType.MESSAGE_ID_MULTISIG:
return destModule.threshold > 0;
case IsmType.ROUTING: {
const checking = moduleCanCertainlyVerify(destModule.domains[destination], multiProvider, origin, destination);
return checking;
}
case IsmType.AGGREGATION: {
let verified = 0;
for (const subModule of destModule.modules) {
const canVerify = await moduleCanCertainlyVerify(subModule, multiProvider, origin, destination);
if (canVerify) {
verified += 1;
}
}
return verified >= destModule.threshold;
}
case IsmType.OP_STACK:
return destModule.nativeBridge !== ethers.constants.AddressZero;
case IsmType.TEST_ISM: {
return true;
}
default:
throw new Error(`Unsupported module type: ${destModule.type}`);
}
}
}
export async function moduleMatchesConfig(chain, moduleAddress, config, multiProvider, contracts, mailbox) {
if (typeof config === 'string') {
return eqAddress(moduleAddress, config);
}
// If the module address is zero, it can't match any object-based config.
// The subsequent check of what moduleType it is will throw, so we fail here.
if (eqAddress(moduleAddress, ethers.constants.AddressZero)) {
return false;
}
const provider = multiProvider.getProvider(chain);
const module = IInterchainSecurityModule__factory.connect(moduleAddress, provider);
const actualType = await module.moduleType();
if (actualType !== ismTypeToModuleType(config.type))
return false;
let matches = true;
switch (config.type) {
case IsmType.STORAGE_MERKLE_ROOT_MULTISIG:
case IsmType.STORAGE_MESSAGE_ID_MULTISIG: {
// A storage multisig ism matches if validators and threshold match the config
const storageMerkleRootMultisigIsm = AbstractStorageMultisigIsm__factory.connect(moduleAddress, provider);
const [validators, threshold] = await storageMerkleRootMultisigIsm.validatorsAndThreshold(ethers.constants.AddressZero);
matches = deepEquals(normalizeConfig({ validators, threshold }), normalizeConfig({
validators: config.validators,
threshold: config.threshold,
}));
break;
}
case IsmType.MERKLE_ROOT_MULTISIG: {
// A MerkleRootMultisigIsm matches if validators and threshold match the config
const expectedAddress = await contracts.staticMerkleRootMultisigIsmFactory.getAddress(config.validators.sort(), config.threshold);
matches = eqAddress(expectedAddress, module.address);
break;
}
case IsmType.MESSAGE_ID_MULTISIG: {
// A MessageIdMultisigIsm matches if validators and threshold match the config
const expectedAddress = await contracts.staticMessageIdMultisigIsmFactory.getAddress(config.validators.sort(), config.threshold);
matches = eqAddress(expectedAddress, module.address);
break;
}
case IsmType.AMOUNT_ROUTING: {
const amountRoutingIsm = AmountRoutingIsm__factory.connect(moduleAddress, provider);
const [lowerIsmAddress, upperIsmAddress, threshold] = await Promise.all([
amountRoutingIsm.lower(),
amountRoutingIsm.upper(),
amountRoutingIsm.threshold(),
]);
const subModuleMatchesConfig = await Promise.all([
[lowerIsmAddress, config.lowerIsm],
[upperIsmAddress, config.upperIsm],
].map(([ismAddress, ismConfig]) => moduleMatchesConfig(chain, ismAddress, ismConfig, multiProvider, contracts, mailbox)));
matches &&= threshold.eq(config.threshold);
matches &&= subModuleMatchesConfig.every(Boolean);
break;
}
case IsmType.FALLBACK_ROUTING:
case IsmType.ROUTING: {
// A RoutingIsm matches if:
// 1. The set of domains in the config equals those on-chain
// 2. The modules for each domain match the config
// TODO: Check (1)
const routingIsm = DomainRoutingIsm__factory.connect(moduleAddress, provider);
// Check that the RoutingISM owner matches the config
const owner = await routingIsm.owner();
const expectedOwner = config.owner;
matches &&= eqAddress(owner, expectedOwner);
// check if the mailbox matches the config for fallback routing
if (config.type === IsmType.FALLBACK_ROUTING) {
const client = MailboxClient__factory.connect(moduleAddress, provider);
let mailboxAddress;
try {
mailboxAddress = await client.mailbox();
}
catch {
matches = false;
break;
}
matches =
matches &&
mailbox !== undefined &&
eqAddress(mailboxAddress, mailbox);
}
const delta = await routingModuleDelta(chain, moduleAddress, config, multiProvider, contracts, mailbox);
matches =
matches &&
delta.domainsToEnroll.length === 0 &&
delta.domainsToUnenroll.length === 0 &&
!delta.mailbox &&
!delta.owner;
break;
}
case IsmType.AGGREGATION: {
// An AggregationIsm matches if:
// 1. The threshold matches the config
// 2. There is a bijection between on and off-chain configured modules
const aggregationIsm = StaticAggregationIsm__factory.connect(moduleAddress, provider);
const [subModules, threshold] = await aggregationIsm.modulesAndThreshold('0x');
matches &&= threshold === config.threshold;
matches &&= subModules.length === config.modules.length;
const configIndexMatched = new Map();
for (const subModule of subModules) {
const subModuleMatchesConfig = await Promise.all(config.modules.map((c) => moduleMatchesConfig(chain, subModule, c, multiProvider, contracts, mailbox)));
// The submodule returned by the ISM must match exactly one
// entry in the config.
const count = subModuleMatchesConfig.filter(Boolean).length;
matches &&= count === 1;
// That entry in the config should not have been matched already.
subModuleMatchesConfig.forEach((matched, index) => {
if (matched) {
matches &&= !configIndexMatched.has(index);
configIndexMatched.set(index, true);
}
});
}
break;
}
case IsmType.OP_STACK: {
const opStackIsm = OPStackIsm__factory.connect(moduleAddress, provider);
const type = await opStackIsm.moduleType();
matches &&= type === ModuleType.NULL;
break;
}
case IsmType.TEST_ISM: {
// This is just a TestISM
matches = true;
break;
}
case IsmType.TRUSTED_RELAYER: {
const trustedRelayerIsm = TrustedRelayerIsm__factory.connect(moduleAddress, provider);
const type = await trustedRelayerIsm.moduleType();
matches &&= type === ModuleType.NULL;
const relayer = await trustedRelayerIsm.trustedRelayer();
matches &&= eqAddress(relayer, config.relayer);
break;
}
case IsmType.CCIP: {
const ccipIsm = CCIPIsm__factory.connect(moduleAddress, provider);
const type = await ccipIsm.moduleType();
matches &&= type === ModuleType.NULL;
// Check that the origin chain selector matches the config
const originCcipChainSelector = await ccipIsm.ccipOrigin();
const chainName = getChainNameFromCCIPSelector(originCcipChainSelector.toString());
matches &&= chainName === config.originChain;
break;
}
case IsmType.PAUSABLE: {
const pausableIsm = PausableIsm__factory.connect(moduleAddress, provider);
const owner = await pausableIsm.owner();
const expectedOwner = config.owner;
matches &&= eqAddress(owner, expectedOwner);
if (config.paused) {
const isPaused = await pausableIsm.paused();
matches &&= config.paused === isPaused;
}
break;
}
case IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG: {
const expectedAddress = await contracts.staticMerkleRootWeightedMultisigIsmFactory.getAddress(config.validators.sort(), config.thresholdWeight);
matches = eqAddress(expectedAddress, module.address);
break;
}
case IsmType.WEIGHTED_MESSAGE_ID_MULTISIG: {
const expectedAddress = await contracts.staticMessageIdWeightedMultisigIsmFactory.getAddress(config.validators.sort(), config.thresholdWeight);
matches = eqAddress(expectedAddress, module.address);
break;
}
default: {
throw new Error('Unsupported ModuleType');
}
}
return matches;
}
export async function routingModuleDelta(destination, moduleAddress, config, multiProvider, contracts, mailbox) {
if (config.type === IsmType.FALLBACK_ROUTING ||
config.type === IsmType.ROUTING) {
return domainRoutingModuleDelta(destination, moduleAddress, config, multiProvider, contracts, mailbox);
}
return {
domainsToEnroll: [],
domainsToUnenroll: [],
};
}
async function domainRoutingModuleDelta(destination, moduleAddress, config, multiProvider, contracts, mailbox) {
const provider = multiProvider.getProvider(destination);
const routingIsm = DomainRoutingIsm__factory.connect(moduleAddress, provider);
const owner = await routingIsm.owner();
const deployedDomains = (await routingIsm.domains()).map((domain) => domain.toNumber());
const delta = {
domainsToUnenroll: [],
domainsToEnroll: [],
};
// if owners don't match, we need to transfer ownership
if (!eqAddress(owner, normalizeAddress(config.owner))) {
delta.owner = config.owner;
}
if (config.type === IsmType.FALLBACK_ROUTING) {
const client = MailboxClient__factory.connect(moduleAddress, provider);
const mailboxAddress = await client.mailbox();
if (mailbox && !eqAddress(mailboxAddress, mailbox))
delta.mailbox = mailbox;
}
const ismByDomainName = config.type === IsmType.INTERCHAIN_ACCOUNT_ROUTING
? config.isms
: config.domains;
// config.domains is already filtered to only include domains in the multiprovider
const safeConfigDomains = objMap(ismByDomainName, (chainName) => multiProvider.getDomainId(chainName));
// check for exclusion of domains in the config
delta.domainsToUnenroll = deployedDomains.filter((domain) => !Object.values(safeConfigDomains).includes(domain));
// check for inclusion of domains in the config
for (const [origin, subConfig] of Object.entries(ismByDomainName)) {
const originDomain = safeConfigDomains[origin];
if (!deployedDomains.includes(originDomain)) {
delta.domainsToEnroll.push(originDomain);
}
else {
const subModule = await routingIsm.module(originDomain);
// Recursively check that the submodule for each configured
// domain matches the submodule config.
const subModuleMatches = await moduleMatchesConfig(destination, subModule, subConfig, multiProvider, contracts, mailbox);
if (!subModuleMatches) {
delta.domainsToEnroll.push(originDomain);
}
}
}
return delta;
}
export function collectValidators(origin, config) {
// TODO: support address configurations in collectValidators
if (typeof config === 'string') {
logger
.child({ origin })
.debug('Address config unimplemented in collectValidators');
return new Set([]);
}
let validators = [];
if (config.type === IsmType.STORAGE_MERKLE_ROOT_MULTISIG ||
config.type === IsmType.STORAGE_MESSAGE_ID_MULTISIG ||
config.type === IsmType.MERKLE_ROOT_MULTISIG ||
config.type === IsmType.MESSAGE_ID_MULTISIG) {
validators = config.validators;
}
else if (config.type === IsmType.ROUTING) {
if (Object.keys(config.domains).includes(origin)) {
const domainValidators = collectValidators(origin, config.domains[origin]);
validators = [...domainValidators];
}
}
else if (config.type === IsmType.AGGREGATION) {
const aggregatedValidators = config.modules.map((c) => collectValidators(origin, c));
aggregatedValidators.forEach((set) => {
validators = validators.concat([...set]);
});
}
else if (config.type === IsmType.TEST_ISM ||
config.type === IsmType.PAUSABLE) {
return new Set([]);
}
else {
throw new Error('Unsupported ModuleType');
}
return new Set(validators);
}
/**
* Checks if the given ISM type requires static deployment
*
* @param {IsmType} ismType - The type of Interchain Security Module (ISM)
* @returns {boolean} True if the ISM type requires static deployment, false otherwise
*/
export function isStaticIsm(ismType) {
return STATIC_ISM_TYPES.includes(ismType);
}
/**
* Determines if static ISM deployment is supported on a given chain's technical stack
* @dev Currently, only ZkSync does not support static deployments
* @param chainTechnicalStack - The technical stack of the target chain
* @returns boolean - true if static deployment is supported, false for ZkSync
*/
export function isStaticDeploymentSupported(chainTechnicalStack) {
return chainTechnicalStack !== ChainTechnicalStack.ZkSync;
}
/**
* Checks if the given ISM type is compatible with the chain's technical stack.
*
* @param {IsmType} params.ismType - The type of Interchain Security Module (ISM)
* @param {ChainTechnicalStack | undefined} params.chainTechnicalStack - The technical stack of the chain
* @returns {boolean} True if the ISM type is compatible with the chain, false otherwise
*/
export function isIsmCompatible({ chainTechnicalStack, ismType, }) {
// Skip compatibility check for non-static ISMs as they're always supported
if (!isStaticIsm(ismType))
return true;
return isStaticDeploymentSupported(chainTechnicalStack);
}
//# sourceMappingURL=utils.js.map