@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
530 lines • 28.9 kB
JavaScript
import { AmountRoutingIsm__factory, ArbL2ToL1Ism__factory, CCIPIsm__factory, DefaultFallbackRoutingIsm__factory, DomainRoutingIsm__factory, IAggregationIsm__factory, IInterchainSecurityModule__factory, IMultisigIsm__factory, IncrementalDomainRoutingIsm__factory, OPStackIsm__factory, PausableIsm__factory, RateLimitedIsm__factory, StorageAggregationIsm__factory, StorageMerkleRootMultisigIsm__factory, StorageMessageIdMultisigIsm__factory, TestIsm__factory, TrustedRelayerIsm__factory, } from '@hyperlane-xyz/core';
import { addBufferToGasLimit, assert, eqAddress, objFilter, rootLogger, } from '@hyperlane-xyz/utils';
import { HyperlaneApp } from '../app/HyperlaneApp.js';
import { CCIPContractCache } from '../ccip/utils.js';
import { appFromAddressesMapHelper } from '../contracts/contracts.js';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
import { proxyFactoryFactories, } from '../deploy/contracts.js';
import { ChainTechnicalStack } from '../metadata/chainMetadataTypes.js';
import { getZKSyncArtifactByContractName } from '../utils/zksync.js';
import { IsmType, } from './types.js';
import { isIsmCompatible, routingModuleDelta } from './utils.js';
const ismFactories = {
[IsmType.PAUSABLE]: new PausableIsm__factory(),
[IsmType.TRUSTED_RELAYER]: new TrustedRelayerIsm__factory(),
[IsmType.TEST_ISM]: new TestIsm__factory(),
[IsmType.OP_STACK]: new OPStackIsm__factory(),
[IsmType.ARB_L2_TO_L1]: new ArbL2ToL1Ism__factory(),
[IsmType.CCIP]: new CCIPIsm__factory(),
[IsmType.RATE_LIMITED]: new RateLimitedIsm__factory(),
};
const domainRoutingInitializationSize = (destination) => {
if (destination === 'tempo') {
return 30;
}
if (destination === 'mitosis') {
return 30;
}
if (destination === 'shibarium' || destination === 'citrea') {
return 50;
}
if (destination === 'flare') {
return 90;
}
if (destination === 'sei' ||
destination === 'scroll' ||
destination === 'cyber' ||
destination === 'xlayer' ||
destination === 'zircuit' ||
destination === 'flowmainnet' ||
destination === 'nibiru' ||
destination === 'eni' ||
destination === 'merlin' ||
destination === 'megaeth' ||
destination === 'pulsechain') {
return 120;
}
return 300;
};
class IsmDeployer extends HyperlaneDeployer {
cachingEnabled = false;
deployContracts(_chain, _config) {
throw new Error('Method not implemented.');
}
}
export class HyperlaneIsmFactory extends HyperlaneApp {
multiProvider;
ccipContractCache;
// The shape of this object is `ChainMap<Address | ChainMap<Address>`,
// although `any` is use here because that type breaks a lot of signatures.
// TODO: fix this in the next refactoring
deployedIsms = {};
deployer;
constructor(contractsMap, multiProvider, ccipContractCache = new CCIPContractCache(), contractVerifier) {
super(contractsMap, multiProvider, rootLogger.child({ module: 'ismFactoryApp' }));
this.multiProvider = multiProvider;
this.ccipContractCache = ccipContractCache;
this.deployer = new IsmDeployer(multiProvider, ismFactories, {
contractVerifier,
});
}
static fromAddressesMap(addressesMap, multiProvider, ccipContractCache, contractVerifier) {
const helper = appFromAddressesMapHelper(addressesMap, proxyFactoryFactories, multiProvider);
return new HyperlaneIsmFactory(helper.contractsMap, multiProvider, ccipContractCache, contractVerifier);
}
async deploy(params) {
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 ISM of type ${ismType} to ${destination} ${origin ? `(for verifying ${origin})` : ''}`);
const { technicalStack } = this.multiProvider.getChainMetadata(destination);
// For static ISM types it checks whether the technical stack supports static contract deployment
assert(isIsmCompatible({ ismType, chainTechnicalStack: technicalStack }), `Technical stack ${technicalStack} is not compatible with ${ismType}`);
// Explicit check for unknown ISM types from newer registry versions
assert(ismType !== IsmType.UNKNOWN, `Cannot deploy unknown ISM type. Registry contains ISM type not supported by this SDK version.`);
let contract;
switch (ismType) {
case IsmType.MESSAGE_ID_MULTISIG:
case IsmType.MERKLE_ROOT_MULTISIG:
case IsmType.STORAGE_MESSAGE_ID_MULTISIG:
case IsmType.STORAGE_MERKLE_ROOT_MULTISIG:
contract = await this.deployMultisigIsm(destination, config, logger);
break;
case IsmType.WEIGHTED_MESSAGE_ID_MULTISIG:
case IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG:
contract = await this.deployWeightedMultisigIsm(destination, config, logger);
break;
case IsmType.ROUTING:
case IsmType.FALLBACK_ROUTING:
case IsmType.INCREMENTAL_ROUTING:
case IsmType.AMOUNT_ROUTING:
contract = await this.deployRoutingIsm({
destination,
config,
origin,
mailbox,
existingIsmAddress,
logger,
});
break;
case IsmType.AGGREGATION:
case IsmType.STORAGE_AGGREGATION:
contract = await this.deployAggregationIsm({
destination,
config,
origin,
mailbox,
logger,
});
break;
case IsmType.OP_STACK:
contract = await this.deployer.deployContract(destination, ismType, [
config.nativeBridge,
]);
break;
case IsmType.PAUSABLE:
contract = await this.deployer.deployContract(destination, IsmType.PAUSABLE, [config.owner]);
break;
case IsmType.TRUSTED_RELAYER:
assert(mailbox, `Mailbox address is required for deploying ${ismType}`);
contract = await this.deployer.deployContract(destination, IsmType.TRUSTED_RELAYER, [mailbox, config.relayer]);
break;
case IsmType.TEST_ISM:
contract = await this.deployer.deployContract(destination, IsmType.TEST_ISM, []);
break;
case IsmType.ARB_L2_TO_L1:
contract = await this.deployer.deployContract(destination, IsmType.ARB_L2_TO_L1, [config.bridge]);
break;
case IsmType.RATE_LIMITED: {
const rateLimitedConfig = config;
assert(mailbox, `Mailbox address is required for deploying ${ismType}`);
assert(rateLimitedConfig.recipient, `Recipient address is required for deploying ${ismType}`);
contract = await this.deployer.deployContract(destination, IsmType.RATE_LIMITED, [mailbox, rateLimitedConfig.maxCapacity, rateLimitedConfig.recipient]);
if (rateLimitedConfig.owner) {
const signer = this.multiProvider.getSigner(destination);
const signerAddress = await signer.getAddress();
if (!eqAddress(signerAddress, rateLimitedConfig.owner)) {
const overrides = this.multiProvider.getTransactionOverrides(destination);
const rateLimitedIsm = RateLimitedIsm__factory.connect(contract.address, signer);
const tx = await rateLimitedIsm.transferOwnership(rateLimitedConfig.owner, overrides);
await this.multiProvider.handleTx(destination, tx);
}
}
break;
}
case IsmType.CCIP:
contract = await this.deployCCIPIsm(destination, config);
break;
case IsmType.OFFCHAIN_LOOKUP:
throw new Error(`OFFCHAIN_LOOKUP ISM cannot be deployed — it must already be deployed. ` +
`Pass its contract address as a string instead of a config object.`);
case IsmType.INTERCHAIN_ACCOUNT_ROUTING:
throw new Error('Interchain Account ISM is not supported in this context');
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;
}
async deployCCIPIsm(destination, config) {
const ism = this.ccipContractCache.getIsm(config.originChain, destination);
if (!ism) {
this.logger.error(`CCIP ISM not found for ${config.originChain} -> ${destination}`);
throw new Error(`CCIP ISM not found for ${config.originChain} -> ${destination}`);
}
return CCIPIsm__factory.connect(ism, this.multiProvider.getSigner(destination));
}
async deployMultisigIsm(destination, config, logger) {
const signer = this.multiProvider.getSigner(destination);
const deployStatic = (factory) => this.deployStaticAddressSet(destination, factory, config.validators, logger, config.threshold);
const deployStorage = async (factory, artifact) => {
const contract = await this.multiProvider.handleDeploy(destination, factory, [config.validators, config.threshold], artifact);
return contract.address;
};
let address;
switch (config.type) {
case IsmType.MERKLE_ROOT_MULTISIG:
address = await deployStatic(this.getContracts(destination).staticMerkleRootMultisigIsmFactory);
break;
case IsmType.MESSAGE_ID_MULTISIG:
address = await deployStatic(this.getContracts(destination).staticMessageIdMultisigIsmFactory);
break;
// TODO: support using minimal proxy factories for storage multisig ISMs too
case IsmType.STORAGE_MERKLE_ROOT_MULTISIG:
address = await deployStorage(new StorageMerkleRootMultisigIsm__factory(), await getZKSyncArtifactByContractName(config.type));
break;
case IsmType.STORAGE_MESSAGE_ID_MULTISIG:
address = await deployStorage(new StorageMessageIdMultisigIsm__factory(), await getZKSyncArtifactByContractName(config.type));
break;
default:
throw new Error(`Unsupported multisig ISM type ${config.type}`);
}
return IMultisigIsm__factory.connect(address, signer);
}
async deployWeightedMultisigIsm(destination, config, logger) {
const signer = this.multiProvider.getSigner(destination);
const weightedmultisigIsmFactory = config.type === IsmType.WEIGHTED_MERKLE_ROOT_MULTISIG
? this.getContracts(destination)
.staticMerkleRootWeightedMultisigIsmFactory
: this.getContracts(destination)
.staticMessageIdWeightedMultisigIsmFactory;
const address = await this.deployStaticWeightedValidatorSet(destination, weightedmultisigIsmFactory, config.validators, config.thresholdWeight, logger);
return IMultisigIsm__factory.connect(address, signer);
}
async deployRoutingIsm(params) {
const { config } = params;
if (config.type === IsmType.AMOUNT_ROUTING) {
return this.deployAmountRoutingIsm({
config: config,
destination: params.destination,
origin: params.origin,
mailbox: params.mailbox,
});
}
if (config.type === IsmType.INTERCHAIN_ACCOUNT_ROUTING) {
throw new Error(`${IsmType.INTERCHAIN_ACCOUNT_ROUTING} deployment not supported for now in the HyperlaneIsmFactory class`);
}
return this.deployOwnableRoutingIsm({
...params,
// Can't pass params directly because ts will complain that the types do not match
config,
});
}
async deployAmountRoutingIsm(params) {
const { threshold, lowerIsm, upperIsm } = params.config;
const addresses = [];
for (const module of [lowerIsm, upperIsm]) {
const submodule = await this.deploy({
destination: params.destination,
config: module,
origin: params.origin,
mailbox: params.mailbox,
});
addresses.push(submodule.address);
}
const [lowerIsmAddress, upperIsmAddress] = addresses;
return this.multiProvider.handleDeploy(params.destination, new AmountRoutingIsm__factory(), [lowerIsmAddress, upperIsmAddress, threshold]);
}
async deployOwnableRoutingIsm(params) {
const { destination, config, mailbox, existingIsmAddress, logger } = params;
const overrides = this.multiProvider.getTransactionOverrides(destination);
const contracts = this.getContracts(destination);
const domainRoutingIsmFactory = config.type === IsmType.INCREMENTAL_ROUTING
? contracts.incrementalDomainRoutingIsmFactory
: contracts.domainRoutingIsmFactory;
let routingIsm;
// filtering out domains which are not part of the multiprovider
config.domains = objFilter(config.domains, (domain, _) => {
const 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) => this.multiProvider.getDomainId(domain));
const delta = existingIsmAddress
? await routingModuleDelta(destination, existingIsmAddress, config, this.multiProvider, this.getContracts(destination), 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 = {};
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 = {};
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;
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], await getZKSyncArtifactByContractName(config.type));
// TODO: Should verify contract here
logger.debug('Initialising fallback routing ISM ...');
receipt = await this.multiProvider.handleTx(destination, routingIsm['initialize(address,uint32[],address[])'](config.owner, safeConfigDomains, submoduleAddresses, overrides));
}
else {
// deploying new domain routing ISM
const owner = config.owner;
// if zksync we can't use the proxy factories, so we need to deploy directly
const isZksync = this.multiProvider.getChainMetadata(destination).technicalStack ===
ChainTechnicalStack.ZkSync;
if (isZksync) {
assert(this.deployer, 'HyperlaneDeployer must be set to deploy routing ISM');
const factory = config.type === IsmType.INCREMENTAL_ROUTING
? new IncrementalDomainRoutingIsm__factory()
: new DomainRoutingIsm__factory();
const routingIsm = await this.deployer?.deployContractFromFactory(destination, factory, config.type, []);
await routingIsm['initialize(address,uint32[],address[])'](owner, safeConfigDomains, submoduleAddresses, overrides);
return routingIsm;
}
// estimate gas
const signerAddress = await signer.getAddress();
const batchSize = domainRoutingInitializationSize(destination);
// Deploy initial batch of domains
const initialBatchSize = Math.min(batchSize, safeConfigDomains.length);
const initialDomains = safeConfigDomains.slice(0, initialBatchSize);
const initialAddresses = submoduleAddresses.slice(0, initialBatchSize);
const estimatedGas = await domainRoutingIsmFactory.estimateGas.deploy(signerAddress, initialDomains, initialAddresses, overrides);
this.logger.debug(`Deploying routing ISM with initial ${initialBatchSize} domains on ${destination}`);
// add gas buffer
const tx = await domainRoutingIsmFactory.deploy(signerAddress, initialDomains, initialAddresses, {
gasLimit: addBufferToGasLimit(estimatedGas, 15),
...overrides,
});
// TODO: Should verify contract here
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 {
return undefined;
}
})
.filter((log) => !!log && log.name === 'ModuleDeployed');
if (dispatchLogs.length === 0) {
throw new Error('No ModuleDeployed event found');
}
const moduleAddress = dispatchLogs[0].args['module'];
const factory = config.type === IsmType.INCREMENTAL_ROUTING
? IncrementalDomainRoutingIsm__factory
: DomainRoutingIsm__factory;
routingIsm = factory.connect(moduleAddress, this.multiProvider.getSigner(destination));
// Enroll remaining domains and addresses
// If all domains are enrolled already, this is a no-op
for (let i = initialBatchSize; i < safeConfigDomains.length; i++) {
const estimatedGas = await routingIsm.estimateGas.set(safeConfigDomains[i], submoduleAddresses[i], overrides);
const chainName = this.multiProvider.getChainName(safeConfigDomains[i]);
this.logger.debug(`Enrolling ${chainName} (${safeConfigDomains[i]}) ISM at ${submoduleAddresses[i]} on Domain Routing ISM ${moduleAddress}`);
const enrollTx = await routingIsm.set(safeConfigDomains[i], submoduleAddresses[i], {
gasLimit: addBufferToGasLimit(estimatedGas, 15),
...overrides,
});
await this.multiProvider.handleTx(destination, enrollTx);
}
// Transfer ownership after all enrollments are complete, unless the
// signer is already the target owner (common for self-owned deploys).
if (!eqAddress(signerAddress, config.owner)) {
const transferTxEstimatedGas = await routingIsm.estimateGas.transferOwnership(config.owner, overrides);
const transferTx = await routingIsm.transferOwnership(config.owner, {
gasLimit: addBufferToGasLimit(transferTxEstimatedGas, 15),
...overrides,
});
await this.multiProvider.handleTx(destination, transferTx);
}
}
}
return routingIsm;
}
async deployAggregationIsm(params) {
const { destination, config, origin, mailbox } = params;
const signer = this.multiProvider.getSigner(destination);
const addresses = [];
for (const module of config.modules) {
const submodule = await this.deploy({
destination,
config: module,
origin,
mailbox,
});
addresses.push(submodule.address);
}
let ismAddress;
if (config.type === IsmType.STORAGE_AGGREGATION) {
// TODO: support using minimal proxy factories for storage aggregation ISMs too
const factory = new StorageAggregationIsm__factory().connect(signer);
const ism = await this.multiProvider.handleDeploy(destination, factory, [
addresses,
config.threshold,
]);
ismAddress = ism.address;
}
else {
const staticAggregationIsmFactory = this.getContracts(destination).staticAggregationIsmFactory;
ismAddress = await this.deployStaticAddressSet(destination, staticAggregationIsmFactory, addresses, params.logger, config.threshold);
}
return IAggregationIsm__factory.connect(ismAddress, signer);
}
async deployStaticAddressSet(chain, factory, values, logger, threshold = values.length) {
const sorted = [...values].sort();
const getAddressResult = await factory['getAddress(address[],uint8)'](sorted, threshold);
const address = (await this.previewFactoryDeployAddress(chain, factory, 'deploy(address[],uint8)', [sorted, threshold])) ?? getAddressResult;
if (!eqAddress(address, getAddressResult)) {
logger.debug(`Factory getAddress mismatch on ${chain}, using deploy simulation address ${address}`);
}
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);
// estimate gas
const estimatedGas = await factory.estimateGas['deploy(address[],uint8)'](sorted, threshold, overrides);
// add gas buffer
const hash = await factory['deploy(address[],uint8)'](sorted, threshold, {
gasLimit: addBufferToGasLimit(estimatedGas, 15),
...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;
}
async deployStaticWeightedValidatorSet(chain, factory, values, thresholdWeight = 66e8, logger) {
const sorted = [...values].sort();
const getAddressResult = await factory['getAddress((address,uint96)[],uint96)'](sorted, thresholdWeight);
const address = (await this.previewFactoryDeployAddress(chain, factory, 'deploy((address,uint96)[],uint96)', [sorted, thresholdWeight])) ?? getAddressResult;
if (!eqAddress(address, getAddressResult)) {
logger.debug(`Weighted factory getAddress mismatch on ${chain}, using deploy simulation address ${address}`);
}
const code = await this.multiProvider.getProvider(chain).getCode(address);
if (code === '0x') {
logger.debug(`Deploying new weighted set of ${values.length} validators with a threshold weight ${thresholdWeight} on ${chain} `);
const overrides = this.multiProvider.getTransactionOverrides(chain);
// estimate gas
const estimatedGas = await factory.estimateGas['deploy((address,uint96)[],uint96)'](sorted, thresholdWeight, overrides);
// add gas buffer
const hash = await factory['deploy((address,uint96)[],uint96)'](sorted, thresholdWeight, {
gasLimit: addBufferToGasLimit(estimatedGas, 15),
...overrides,
});
await this.multiProvider.handleTx(chain, hash);
// TODO: add proxy verification artifact?
}
else {
logger.debug(`Recovered weighted set of ${values.length} validators on ${chain} with a threshold weight ${thresholdWeight}: ${address}`);
}
return address;
}
async previewFactoryDeployAddress(chain, factory, signature, args) {
try {
const data = factory.interface.encodeFunctionData(signature, args);
const result = await this.multiProvider.getProvider(chain).call({
to: factory.address,
data,
});
const decoded = factory.interface.decodeFunctionResult(signature, result);
if (Array.isArray(decoded) && typeof decoded[0] === 'string') {
return decoded[0];
}
if (typeof decoded === 'string') {
return decoded;
}
return undefined;
}
catch (e) {
this.logger.warn(`Failed to preview factory deploy address on ${chain} (factory=${factory.address}, fn=${signature}): ${e instanceof Error ? e.message : String(e)}`);
return undefined;
}
}
}
//# sourceMappingURL=HyperlaneIsmFactory.js.map