@hyperlane-xyz/sdk
Version:
The official SDK for the Hyperlane Network
411 lines • 21.9 kB
JavaScript
import { AmountRoutingIsm__factory, ArbL2ToL1Ism__factory, CCIPIsm__factory, DefaultFallbackRoutingIsm__factory, DomainRoutingIsm__factory, IAggregationIsm__factory, IInterchainSecurityModule__factory, IMultisigIsm__factory, OPStackIsm__factory, PausableIsm__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(),
};
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}`);
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.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.CCIP:
contract = await this.deployCCIPIsm(destination, config);
break;
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 domainRoutingIsmFactory = this.getContracts(destination).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 routingIsm = await this.deployer?.deployContractFromFactory(destination, new DomainRoutingIsm__factory(), IsmType.ROUTING, []);
await routingIsm['initialize(address,uint32[],address[])'](owner, safeConfigDomains, submoduleAddresses, overrides);
return routingIsm;
}
// estimate gas
const estimatedGas = await domainRoutingIsmFactory.estimateGas.deploy(owner, safeConfigDomains, submoduleAddresses, overrides);
// add gas buffer
const tx = await domainRoutingIsmFactory.deploy(owner, safeConfigDomains, submoduleAddresses, {
gasLimit: addBufferToGasLimit(estimatedGas),
...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'];
routingIsm = DomainRoutingIsm__factory.connect(moduleAddress, this.multiProvider.getSigner(destination));
}
}
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 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);
// 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),
...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 address = await factory['getAddress((address,uint96)[],uint96)'](sorted, thresholdWeight);
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),
...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;
}
}
//# sourceMappingURL=HyperlaneIsmFactory.js.map