refactor: dedupe HyperlaneIsmFactory and IsmModule.create (#4732)

### Description

- Uses HyperlaneIsmFactory in IsmModuleCreate for deduping redundant
code

### Backward compatibility

Yes

### Testing

Unit tests
pull/4772/head
Yorke Rhodes 1 month ago committed by GitHub
parent c622bfbcf5
commit e104cf6aa3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/dirty-swans-drum.md
  2. 31
      typescript/sdk/src/contracts/contracts.ts
  3. 15
      typescript/sdk/src/core/EvmCoreModule.ts
  4. 359
      typescript/sdk/src/deploy/EvmModuleDeployer.ts
  5. 5
      typescript/sdk/src/deploy/HyperlaneDeployer.ts
  6. 134
      typescript/sdk/src/hook/EvmHookModule.ts
  7. 411
      typescript/sdk/src/ism/EvmIsmModule.ts
  8. 1
      typescript/sdk/src/ism/HyperlaneIsmFactory.hardhat-test.ts
  9. 85
      typescript/sdk/src/ism/HyperlaneIsmFactory.ts
  10. 15
      typescript/sdk/src/token/EvmERC20WarpModule.ts
  11. 11
      typescript/utils/src/index.ts
  12. 10
      typescript/utils/src/sets.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/utils': patch
'@hyperlane-xyz/sdk': patch
---
Dedupe internals of hook and ISM module deploy code

@ -1,10 +1,11 @@
import { Contract } from 'ethers';
import { Ownable } from '@hyperlane-xyz/core';
import { Ownable, Ownable__factory } from '@hyperlane-xyz/core';
import {
Address,
ProtocolType,
ValueOf,
eqAddress,
hexOrBase58ToHex,
objFilter,
objMap,
@ -12,8 +13,10 @@ import {
promiseObjAll,
} from '@hyperlane-xyz/utils';
import { OwnableConfig } from '../deploy/types.js';
import { ChainMetadataManager } from '../metadata/ChainMetadataManager.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainMap, Connection } from '../types.js';
import {
@ -257,3 +260,29 @@ export function appFromAddressesMapHelper<F extends HyperlaneFactories>(
multiProvider,
};
}
export function transferOwnershipTransactions(
chainId: number,
contract: Address,
actual: OwnableConfig,
expected: OwnableConfig,
label?: string,
): AnnotatedEV5Transaction[] {
if (eqAddress(actual.owner, expected.owner)) {
return [];
}
return [
{
chainId,
annotation: `Transferring ownership of ${label ?? contract} from ${
actual.owner
} to ${expected.owner}`,
to: contract,
data: Ownable__factory.createInterface().encodeFunctionData(
'transferOwnership',
[expected.owner],
),
},
];
}

@ -10,6 +10,7 @@ import {
import {
attachContractsMap,
serializeContractsMap,
transferOwnershipTransactions,
} from '../contracts/contracts.js';
import {
HyperlaneAddresses,
@ -17,7 +18,6 @@ import {
} from '../contracts/types.js';
import { DeployedCoreAddresses } from '../core/schemas.js';
import { CoreConfig } from '../core/types.js';
import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import {
ProxyFactoryFactories,
@ -202,12 +202,13 @@ export class EvmCoreModule extends HyperlaneModule<
actualConfig: CoreConfig,
expectedConfig: CoreConfig,
): AnnotatedEV5Transaction[] {
return EvmModuleDeployer.createTransferOwnershipTx({
actualOwner: actualConfig.owner,
expectedOwner: expectedConfig.owner,
deployedAddress: this.args.addresses.mailbox,
chainId: this.domainId,
});
return transferOwnershipTransactions(
this.domainId,
this.args.addresses.mailbox,
actualConfig,
expectedConfig,
'Mailbox',
);
}
/**

@ -1,359 +0,0 @@
import { ethers } from 'ethers';
import { Logger } from 'pino';
import {
Ownable__factory,
StaticAddressSetFactory,
StaticThresholdAddressSetFactory,
TransparentUpgradeableProxy__factory,
} from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import {
Address,
addBufferToGasLimit,
eqAddress,
rootLogger,
} from '@hyperlane-xyz/utils';
import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainMap, ChainName } from '../types.js';
import { isProxy, proxyConstructorArgs } from './proxy.js';
import { ContractVerifier } from './verify/ContractVerifier.js';
import {
ContractVerificationInput,
ExplorerLicenseType,
} from './verify/types.js';
import { getContractVerificationInput } from './verify/utils.js';
export class EvmModuleDeployer<Factories extends HyperlaneFactories> {
public verificationInputs: ChainMap<ContractVerificationInput[]> = {};
constructor(
protected readonly multiProvider: MultiProvider,
protected readonly factories: Factories,
protected readonly logger = rootLogger.child({
module: 'EvmModuleDeployer',
}),
protected readonly contractVerifier?: ContractVerifier,
) {
this.contractVerifier ??= new ContractVerifier(
multiProvider,
{},
coreBuildArtifact,
ExplorerLicenseType.MIT,
);
}
// Deploys a contract from a factory
public async deployContractFromFactory<F extends ethers.ContractFactory>({
chain,
factory,
contractName,
constructorArgs,
initializeArgs,
implementationAddress,
}: {
chain: ChainName;
factory: F;
contractName: string;
constructorArgs: Parameters<F['deploy']>;
initializeArgs?: Parameters<Awaited<ReturnType<F['deploy']>>['initialize']>;
implementationAddress?: Address;
}): Promise<ReturnType<F['deploy']>> {
this.logger.info(
`Deploying ${contractName} on ${chain} with constructor args (${constructorArgs.join(
', ',
)})...`,
);
const contract = await this.multiProvider.handleDeploy(
chain,
factory,
constructorArgs,
);
if (initializeArgs) {
this.logger.debug(`Initialize ${contractName} on ${chain}`);
// Estimate gas for the initialize transaction
const estimatedGas = await contract.estimateGas.initialize(
...initializeArgs,
);
// deploy with buffer on gas limit
const overrides = this.multiProvider.getTransactionOverrides(chain);
const initTx = await contract.initialize(...initializeArgs, {
gasLimit: addBufferToGasLimit(estimatedGas),
...overrides,
});
await this.multiProvider.handleTx(chain, initTx);
}
const verificationInput = getContractVerificationInput({
name: contractName,
contract,
bytecode: factory.bytecode,
expectedimplementation: implementationAddress,
});
this.addVerificationArtifacts({ chain, artifacts: [verificationInput] });
// try verifying contract
try {
await this.contractVerifier?.verifyContract(chain, verificationInput);
} catch (error) {
// log error but keep deploying, can also verify post-deployment if needed
this.logger.debug(`Error verifying contract: ${error}`);
}
return contract;
}
/**
* Deploys a contract with a specified name.
*
* This function is capable of deploying any contract type defined within the `Factories` type to a specified chain.
*
* @param {ChainName} chain - The name of the chain on which the contract is to be deployed.
* @param {K} contractKey - The key identifying the factory to use for deployment.
* @param {string} contractName - The name of the contract to deploy. This must match the contract source code.
* @param {Parameters<Factories[K]['deploy']>} constructorArgs - Arguments for the contract's constructor.
* @param {Parameters<Awaited<ReturnType<Factories[K]['deploy']>>['initialize']>?} initializeArgs - Optional arguments for the contract's initialization function.
* @returns {Promise<HyperlaneContracts<Factories>[K]>} A promise that resolves to the deployed contract instance.
*/
public async deployContractWithName<K extends keyof Factories>({
chain,
contractKey,
contractName,
constructorArgs,
initializeArgs,
}: {
chain: ChainName;
contractKey: K;
contractName: string;
constructorArgs: Parameters<Factories[K]['deploy']>;
initializeArgs?: Parameters<
Awaited<ReturnType<Factories[K]['deploy']>>['initialize']
>;
}): Promise<HyperlaneContracts<Factories>[K]> {
const contract = await this.deployContractFromFactory({
chain,
factory: this.factories[contractKey],
contractName,
constructorArgs,
initializeArgs,
});
return contract;
}
// Deploys a contract with the same name as the contract key
public async deployContract<K extends keyof Factories>({
chain,
contractKey,
constructorArgs,
initializeArgs,
}: {
chain: ChainName;
contractKey: K;
constructorArgs: Parameters<Factories[K]['deploy']>;
initializeArgs?: Parameters<
Awaited<ReturnType<Factories[K]['deploy']>>['initialize']
>;
}): Promise<HyperlaneContracts<Factories>[K]> {
return this.deployContractWithName({
chain,
contractKey,
contractName: contractKey.toString(),
constructorArgs,
initializeArgs,
});
}
// Deploys the Implementation and Proxy for a given contract
public async deployProxiedContract<K extends keyof Factories>({
chain,
contractKey,
contractName,
proxyAdmin,
constructorArgs,
initializeArgs,
}: {
chain: ChainName;
contractKey: K;
contractName: string;
proxyAdmin: string;
constructorArgs: Parameters<Factories[K]['deploy']>;
initializeArgs?: Parameters<HyperlaneContracts<Factories>[K]['initialize']>;
}): Promise<HyperlaneContracts<Factories>[K]> {
// Try to initialize the implementation even though it may not be necessary
const implementation = await this.deployContractWithName({
chain,
contractKey,
contractName,
constructorArgs,
initializeArgs,
});
// Initialize the proxy the same way
return this.deployProxy({
chain,
implementation,
proxyAdmin,
initializeArgs,
});
}
// Deploys a proxy for a given implementation contract
protected async deployProxy<C extends ethers.Contract>({
chain,
implementation,
proxyAdmin,
initializeArgs,
}: {
chain: ChainName;
implementation: C;
proxyAdmin: string;
initializeArgs?: Parameters<C['initialize']>;
}): Promise<C> {
const isProxied = await isProxy(
this.multiProvider.getProvider(chain),
implementation.address,
);
if (isProxied) {
// if the implementation is already a proxy, do not deploy a new proxy
return implementation;
}
const constructorArgs = proxyConstructorArgs(
implementation,
proxyAdmin,
initializeArgs,
);
const proxy = await this.deployContractFromFactory({
chain,
factory: new TransparentUpgradeableProxy__factory(),
contractName: 'TransparentUpgradeableProxy',
constructorArgs,
implementationAddress: implementation.address,
});
return implementation.attach(proxy.address) as C;
}
// Adds verification artifacts to the verificationInputs map
protected addVerificationArtifacts({
chain,
artifacts,
}: {
chain: ChainName;
artifacts: ContractVerificationInput[];
}): void {
this.verificationInputs[chain] = this.verificationInputs[chain] || [];
artifacts.forEach((artifact) => {
this.verificationInputs[chain].push(artifact);
});
}
// Static deploy function used by Hook and ISM modules.
public static async deployStaticAddressSet({
chain,
factory,
values,
logger,
threshold = values.length,
multiProvider,
}: {
chain: ChainName;
factory: StaticThresholdAddressSetFactory | StaticAddressSetFactory;
values: Address[];
logger: Logger;
threshold?: number;
multiProvider: MultiProvider;
}): Promise<Address> {
const sortedValues = [...values].sort();
const address = await factory['getAddress(address[],uint8)'](
sortedValues,
threshold,
);
const code = await multiProvider.getProvider(chain).getCode(address);
if (code === '0x') {
logger.debug(
`Deploying new ${threshold} of ${sortedValues.length} address set to ${chain}`,
);
const overrides = multiProvider.getTransactionOverrides(chain);
// estimate gas
const estimatedGas = await factory.estimateGas['deploy(address[],uint8)'](
sortedValues,
threshold,
overrides,
);
// add gas buffer
const hash = await factory['deploy(address[],uint8)'](
sortedValues,
threshold,
{
gasLimit: addBufferToGasLimit(estimatedGas),
...overrides,
},
);
await multiProvider.handleTx(chain, hash);
} else {
logger.debug(
`Recovered ${threshold} of ${sortedValues.length} address set on ${chain}: ${address}`,
);
}
// TODO: figure out how to get the constructor arguments for manual deploy TXs
// const verificationInput = buildVerificationInput(
// NAME,
// ADDRESS,
// CONSTRUCTOR_ARGS,
// );
// await this.deployer.verifyContract(
// this.chainName,
// verificationInput,
// logger,
// );
return address;
}
/**
* Transfers ownership of a contract to a new owner.
*
* @param actualOwner - The current owner of the contract.
* @param expectedOwner - The expected new owner of the contract.
* @param deployedAddress - The address of the deployed contract.
* @param chainId - The chain ID of the network the contract is deployed on.
* @returns An array of annotated EV5 transactions that need to be executed to update the owner.
*/
public static createTransferOwnershipTx(params: {
actualOwner: Address;
expectedOwner: Address;
deployedAddress: Address;
chainId: number;
}): AnnotatedEV5Transaction[] {
const { actualOwner, expectedOwner, deployedAddress, chainId } = params;
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (eqAddress(actualOwner, expectedOwner)) {
return [];
}
updateTransactions.push({
annotation: `Transferring ownership of ${deployedAddress} from current owner ${actualOwner} to new owner ${expectedOwner}`,
chainId,
to: deployedAddress,
data: Ownable__factory.createInterface().encodeFunctionData(
'transferOwnership(address)',
[expectedOwner],
),
});
return updateTransactions;
}
}

@ -74,6 +74,8 @@ export abstract class HyperlaneDeployer<
public cachedAddresses: HyperlaneAddressesMap<any> = {};
public deployedContracts: HyperlaneContractsMap<Factories> = {};
protected cachingEnabled = true;
protected logger: Logger;
chainTimeoutMs: number;
@ -86,7 +88,6 @@ export abstract class HyperlaneDeployer<
) {
this.logger = options?.logger ?? rootLogger.child({ module: 'deployer' });
this.chainTimeoutMs = options?.chainTimeoutMs ?? 15 * 60 * 1000; // 15 minute timeout per chain
this.options.ismFactory?.setDeployer(this);
if (Object.keys(icaAddresses).length > 0) {
this.options.icaApp = InterchainAccount.fromAddressesMap(
icaAddresses,
@ -374,7 +375,7 @@ export abstract class HyperlaneDeployer<
shouldRecover = true,
implementationAddress?: Address,
): Promise<ReturnType<F['deploy']>> {
if (shouldRecover) {
if (this.cachingEnabled && shouldRecover) {
const cachedContract = this.readCache(chain, factory, contractName);
if (cachedContract) {
if (this.recoverVerificationInputs) {

@ -40,16 +40,16 @@ import {
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { CoreAddresses } from '../core/contracts.js';
import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
import { IgpFactories, igpFactories } from '../gas/contracts.js';
import { IgpConfig } from '../gas/types.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { ArbL2ToL1IsmConfig, IsmType, OpStackIsmConfig } from '../ism/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
import { ChainName, ChainNameOrId } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';
import { EvmHookReader } from './EvmHookReader.js';
@ -75,6 +75,14 @@ type HookModuleAddresses = {
proxyAdmin: Address;
};
class HookDeployer extends HyperlaneDeployer<{}, HookFactories> {
protected cachingEnabled = false;
deployContracts(_chain: ChainName, _config: {}): Promise<any> {
throw new Error('Method not implemented.');
}
}
export class EvmHookModule extends HyperlaneModule<
ProtocolType.Ethereum,
HookConfig,
@ -82,7 +90,9 @@ export class EvmHookModule extends HyperlaneModule<
> {
protected readonly logger = rootLogger.child({ module: 'EvmHookModule' });
protected readonly reader: EvmHookReader;
protected readonly deployer: EvmModuleDeployer<HookFactories & IgpFactories>;
// "ISM" Factory has aggregation hook factories too
protected readonly hookFactory: HyperlaneIsmFactory;
protected readonly deployer: HookDeployer;
// Adding these to reduce how often we need to grab from MultiProvider.
public readonly chain: string;
@ -105,15 +115,11 @@ export class EvmHookModule extends HyperlaneModule<
super(params);
this.reader = new EvmHookReader(multiProvider, this.args.chain);
this.deployer = new EvmModuleDeployer(
this.hookFactory = HyperlaneIsmFactory.fromAddressesMap(
{ [this.args.chain]: params.addresses },
multiProvider,
{
...hookFactories,
...igpFactories,
},
this.logger,
contractVerifier,
);
this.deployer = new HookDeployer(multiProvider, hookFactories);
this.chain = this.multiProvider.getChainName(this.args.chain);
this.domainId = this.multiProvider.getDomainId(this.chain);
@ -625,11 +631,9 @@ export class EvmHookModule extends HyperlaneModule<
switch (config.type) {
case HookType.MERKLE_TREE:
return this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.MERKLE_TREE,
constructorArgs: [this.args.addresses.mailbox],
});
return this.deployer.deployContract(this.chain, HookType.MERKLE_TREE, [
this.args.addresses.mailbox,
]);
case HookType.INTERCHAIN_GAS_PAYMASTER:
return this.deployIgpHook({ config });
case HookType.AGGREGATION:
@ -657,16 +661,13 @@ export class EvmHookModule extends HyperlaneModule<
config: ProtocolFeeHookConfig;
}): Promise<ProtocolFee> {
this.logger.debug('Deploying ProtocolFeeHook...');
return this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.PROTOCOL_FEE,
constructorArgs: [
const deployer = new HookDeployer(this.multiProvider, hookFactories);
return deployer.deployContract(this.chain, HookType.PROTOCOL_FEE, [
config.maxProtocolFee,
config.protocolFee,
config.beneficiary,
config.owner,
],
});
]);
}
protected async deployPausableHook({
@ -675,11 +676,12 @@ export class EvmHookModule extends HyperlaneModule<
config: PausableHookConfig;
}): Promise<PausableHook> {
this.logger.debug('Deploying PausableHook...');
const hook = await this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.PAUSABLE,
constructorArgs: [],
});
const deployer = new HookDeployer(this.multiProvider, hookFactories);
const hook = await deployer.deployContract(
this.chain,
HookType.PAUSABLE,
[],
);
// transfer ownership
await this.multiProvider.handleTx(
@ -715,13 +717,12 @@ export class EvmHookModule extends HyperlaneModule<
this.args.addresses.staticAggregationHookFactory,
signer,
);
const address = await EvmModuleDeployer.deployStaticAddressSet({
chain: this.chain,
const address = await this.hookFactory.deployStaticAddressSet(
this.chain,
factory,
values: aggregatedHooks,
logger: this.logger,
multiProvider: this.multiProvider,
});
aggregatedHooks,
this.logger,
);
// return aggregation hook
return StaticAggregationHook__factory.connect(address, signer);
@ -773,16 +774,12 @@ export class EvmHookModule extends HyperlaneModule<
);
// deploy opstack hook
const hook = await this.deployer.deployContract({
chain,
contractKey: HookType.OP_STACK,
constructorArgs: [
const hook = await this.deployer.deployContract(chain, HookType.OP_STACK, [
mailbox,
this.multiProvider.getDomainId(config.destinationChain),
addressToBytes32(opstackIsm.address),
config.nativeBridge,
],
});
]);
// set authorized hook on opstack ism
const authorizedHook = await opstackIsm.authorizedHook();
@ -866,17 +863,17 @@ export class EvmHookModule extends HyperlaneModule<
);
// deploy arbL1ToL1 hook
const hook = await this.deployer.deployContract({
const hook = await this.deployer.deployContract(
chain,
contractKey: HookType.ARB_L2_TO_L1,
constructorArgs: [
HookType.ARB_L2_TO_L1,
[
mailbox,
this.multiProvider.getDomainId(config.destinationChain),
addressToBytes32(arbL2ToL1IsmAddress),
config.arbSys,
BigNumber.from(200_000), // 2x estimate of executeTransaction call overhead
],
});
);
// set authorized hook on arbL2ToL1 ism
const authorizedHook = await arbL2ToL1Ism.authorizedHook();
if (authorizedHook === addressToBytes32(hook.address)) {
@ -928,22 +925,18 @@ export class EvmHookModule extends HyperlaneModule<
// deploy fallback hook
const fallbackHook = await this.deploy({ config: config.fallback });
// deploy routing hook with fallback
routingHook = await this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.FALLBACK_ROUTING,
constructorArgs: [
this.args.addresses.mailbox,
deployerAddress,
fallbackHook.address,
],
});
routingHook = await this.deployer.deployContract(
this.chain,
HookType.FALLBACK_ROUTING,
[this.args.addresses.mailbox, deployerAddress, fallbackHook.address],
);
} else {
// deploy routing hook
routingHook = await this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.ROUTING,
constructorArgs: [this.args.addresses.mailbox, deployerAddress],
});
routingHook = await this.deployer.deployContract(
this.chain,
HookType.ROUTING,
[this.args.addresses.mailbox, deployerAddress],
);
}
// compute the hooks that need to be set
@ -1002,14 +995,14 @@ export class EvmHookModule extends HyperlaneModule<
);
// Deploy the InterchainGasPaymaster
const igp = await this.deployer.deployProxiedContract({
chain: this.chain,
contractKey: HookType.INTERCHAIN_GAS_PAYMASTER,
contractName: HookType.INTERCHAIN_GAS_PAYMASTER,
proxyAdmin: this.args.addresses.proxyAdmin,
constructorArgs: [],
initializeArgs: [deployerAddress, config.beneficiary],
});
const igp = await this.deployer.deployProxiedContract(
this.chain,
HookType.INTERCHAIN_GAS_PAYMASTER,
HookType.INTERCHAIN_GAS_PAYMASTER,
this.args.addresses.proxyAdmin,
[],
[deployerAddress, config.beneficiary],
);
// Obtain the transactions to set the gas params for each remote
const configureTxs = await this.updateIgpRemoteGasParams({
@ -1038,11 +1031,12 @@ export class EvmHookModule extends HyperlaneModule<
config: IgpConfig;
}): Promise<StorageGasOracle> {
// Deploy the StorageGasOracle, by default msg.sender is the owner
const gasOracle = await this.deployer.deployContract({
chain: this.chain,
contractKey: 'storageGasOracle',
constructorArgs: [],
});
const gasOracle = await this.deployer.deployContractFromFactory(
this.chain,
new StorageGasOracle__factory(),
'storageGasOracle',
[],
);
// Obtain the transactions to set the gas params for each remote
const configureTxs = await this.updateStorageGasOracle({

@ -1,63 +1,38 @@
import { ethers } from 'ethers';
import { Logger } from 'pino';
import {
ArbL2ToL1Ism__factory,
DefaultFallbackRoutingIsm__factory,
DomainRoutingIsm,
DomainRoutingIsmFactory__factory,
DomainRoutingIsm__factory,
IAggregationIsm,
IAggregationIsm__factory,
IInterchainSecurityModule__factory,
IMultisigIsm,
IMultisigIsm__factory,
IRoutingIsm,
OPStackIsm__factory,
Ownable__factory,
PausableIsm__factory,
TestIsm__factory,
TrustedRelayerIsm__factory,
} from '@hyperlane-xyz/core';
import { DomainRoutingIsm__factory } from '@hyperlane-xyz/core';
import {
Address,
Domain,
ProtocolType,
addBufferToGasLimit,
assert,
deepEquals,
eqAddress,
objFilter,
intersection,
rootLogger,
} from '@hyperlane-xyz/utils';
import { attachAndConnectContracts } from '../contracts/contracts.js';
import { HyperlaneAddresses, HyperlaneContracts } from '../contracts/types.js';
import { transferOwnershipTransactions } from '../contracts/contracts.js';
import { HyperlaneAddresses } from '../contracts/types.js';
import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js';
import {
ProxyFactoryFactories,
proxyFactoryFactories,
} from '../deploy/contracts.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { ContractVerifier } from '../deploy/verify/ContractVerifier.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainName, ChainNameOrId } from '../types.js';
import { normalizeConfig } from '../utils/ism.js';
import { findMatchingLogEvents } from '../utils/logUtils.js';
import { EvmIsmReader } from './EvmIsmReader.js';
import { HyperlaneIsmFactory } from './HyperlaneIsmFactory.js';
import { IsmConfigSchema } from './schemas.js';
import {
AggregationIsmConfig,
DeployedIsm,
IsmConfig,
IsmType,
MUTABLE_ISM_TYPE,
MultisigIsmConfig,
RoutingIsmConfig,
} from './types.js';
import { calculateDomainRoutingDelta } from './utils.js';
@ -74,8 +49,8 @@ export class EvmIsmModule extends HyperlaneModule<
> {
protected readonly logger = rootLogger.child({ module: 'EvmIsmModule' });
protected readonly reader: EvmIsmReader;
protected readonly deployer: EvmModuleDeployer<any>;
protected readonly factories: HyperlaneContracts<ProxyFactoryFactories>;
protected readonly ismFactory: HyperlaneIsmFactory;
protected readonly mailbox: Address;
// Adding these to reduce how often we need to grab from MultiProvider.
public readonly chain: ChainName;
@ -95,33 +70,14 @@ export class EvmIsmModule extends HyperlaneModule<
super(params);
this.reader = new EvmIsmReader(multiProvider, params.chain);
this.deployer = new EvmModuleDeployer(
this.multiProvider,
{},
this.logger,
contractVerifier,
);
this.factories = attachAndConnectContracts(
{
staticMerkleRootMultisigIsmFactory:
params.addresses.staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory:
params.addresses.staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory:
params.addresses.staticAggregationIsmFactory,
staticAggregationHookFactory:
params.addresses.staticAggregationHookFactory,
domainRoutingIsmFactory: params.addresses.domainRoutingIsmFactory,
staticMerkleRootWeightedMultisigIsmFactory:
params.addresses.staticMerkleRootWeightedMultisigIsmFactory,
staticMessageIdWeightedMultisigIsmFactory:
params.addresses.staticMessageIdWeightedMultisigIsmFactory,
},
proxyFactoryFactories,
multiProvider.getSigner(params.chain),
this.ismFactory = HyperlaneIsmFactory.fromAddressesMap(
{ [params.chain]: params.addresses },
multiProvider,
);
this.mailbox = params.addresses.mailbox;
this.chain = this.multiProvider.getChainName(this.args.chain);
this.domainId = this.multiProvider.getDomainId(this.chain);
}
@ -211,24 +167,14 @@ export class EvmIsmModule extends HyperlaneModule<
}
// Lastly, check if the resolved owner is different from the current owner
const provider = this.multiProvider.getProvider(this.chain);
const owner = await Ownable__factory.connect(
updateTxs.push(
...transferOwnershipTransactions(
this.domainId,
this.args.addresses.deployedIsm,
provider,
).owner();
// Return an ownership transfer transaction if required
if (!eqAddress(targetConfig.owner, owner)) {
updateTxs.push({
annotation: 'Transferring ownership of ownable ISM...',
chainId: this.domainId,
to: this.args.addresses.deployedIsm,
data: Ownable__factory.createInterface().encodeFunctionData(
'transferOwnership(address)',
[targetConfig.owner],
currentConfig,
targetConfig,
),
});
}
);
return updateTxs;
}
@ -278,30 +224,24 @@ export class EvmIsmModule extends HyperlaneModule<
target: RoutingIsmConfig;
logger: Logger;
}): Promise<AnnotatedEV5Transaction[]> {
const routingIsmInterface = DomainRoutingIsm__factory.createInterface();
const updateTxs = [];
// filter out domains which are not part of the multiprovider
current = {
...current,
domains: this.filterRoutingIsmDomains({
config: current,
}).availableDomains,
};
target = {
...target,
domains: this.filterRoutingIsmDomains({
config: target,
}).availableDomains,
};
const contract = DomainRoutingIsm__factory.connect(
this.args.addresses.deployedIsm,
this.multiProvider.getProvider(this.chain),
);
const updateTxs: AnnotatedEV5Transaction[] = [];
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 domainsToEnroll) {
for (const origin of knownEnrolls) {
logger.debug(
`Reconfiguring preexisting routing ISM for origin ${origin}...`,
);
@ -310,27 +250,27 @@ export class EvmIsmModule extends HyperlaneModule<
});
const domainId = this.multiProvider.getDomainId(origin);
const tx = await contract.populateTransaction.set(domainId, ism.address);
updateTxs.push({
annotation: `Setting new ISM for origin ${origin}...`,
...tx,
chainId: this.domainId,
to: this.args.addresses.deployedIsm,
data: routingIsmInterface.encodeFunctionData('set(uint32,address)', [
domainId,
ism.address,
]),
});
}
const knownUnenrolls = intersection(
knownChains,
new Set(domainsToUnenroll),
);
// Unenroll domains
for (const origin of domainsToUnenroll) {
for (const origin of knownUnenrolls) {
const domainId = this.multiProvider.getDomainId(origin);
const tx = await contract.populateTransaction.remove(domainId);
updateTxs.push({
annotation: `Unenrolling originDomain ${domainId} from preexisting routing ISM at ${this.args.addresses.deployedIsm}...`,
...tx,
chainId: this.domainId,
to: this.args.addresses.deployedIsm,
data: routingIsmInterface.encodeFunctionData('remove(uint32)', [
domainId,
]),
});
}
@ -344,275 +284,10 @@ export class EvmIsmModule extends HyperlaneModule<
}): Promise<DeployedIsm> {
config = IsmConfigSchema.parse(config);
// If it's an address ISM, just return a base ISM
if (typeof config === 'string') {
// TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3773
// we can remove the ts-ignore once we have a proper type for address ISMs
// @ts-ignore
return IInterchainSecurityModule__factory.connect(
config,
this.multiProvider.getSignerOrProvider(this.args.chain),
);
}
const ismType = config.type;
const logger = rootLogger.child({ chainName: this.chain, ismType });
logger.debug(`Deploying ${ismType} to ${this.args.chain}`);
switch (ismType) {
case IsmType.MESSAGE_ID_MULTISIG:
case IsmType.MERKLE_ROOT_MULTISIG:
return this.deployMultisigIsm({
config,
logger,
});
case IsmType.ROUTING:
case IsmType.FALLBACK_ROUTING:
return this.deployRoutingIsm({
config,
logger,
});
case IsmType.AGGREGATION:
return this.deployAggregationIsm({
config,
logger,
});
case IsmType.OP_STACK:
return this.deployer.deployContractFromFactory({
chain: this.chain,
factory: new OPStackIsm__factory(),
contractName: IsmType.OP_STACK,
constructorArgs: [config.nativeBridge],
});
case IsmType.ARB_L2_TO_L1:
return this.deployer.deployContractFromFactory({
chain: this.chain,
factory: new ArbL2ToL1Ism__factory(),
contractName: IsmType.ARB_L2_TO_L1,
constructorArgs: [config.bridge],
});
case IsmType.PAUSABLE:
return this.deployer.deployContractFromFactory({
chain: this.chain,
factory: new PausableIsm__factory(),
contractName: IsmType.PAUSABLE,
constructorArgs: [config.owner],
});
case IsmType.TRUSTED_RELAYER:
assert(
this.args.addresses.mailbox,
`Mailbox address is required for deploying ${ismType}`,
);
return this.deployer.deployContractFromFactory({
chain: this.chain,
factory: new TrustedRelayerIsm__factory(),
contractName: IsmType.TRUSTED_RELAYER,
constructorArgs: [this.args.addresses.mailbox, config.relayer],
});
case IsmType.TEST_ISM:
return this.deployer.deployContractFromFactory({
chain: this.chain,
factory: new TestIsm__factory(),
contractName: IsmType.TEST_ISM,
constructorArgs: [],
});
default:
throw new Error(`Unsupported ISM type ${ismType}`);
}
}
protected async deployMultisigIsm({
config,
logger,
}: {
config: MultisigIsmConfig;
logger: Logger;
}): Promise<IMultisigIsm> {
const signer = this.multiProvider.getSigner(this.chain);
const factoryName =
config.type === IsmType.MERKLE_ROOT_MULTISIG
? 'staticMerkleRootMultisigIsmFactory'
: 'staticMessageIdMultisigIsmFactory';
const address = await EvmModuleDeployer.deployStaticAddressSet({
chain: this.chain,
factory: this.factories[factoryName],
values: config.validators,
logger,
threshold: config.threshold,
multiProvider: this.multiProvider,
});
return IMultisigIsm__factory.connect(address, signer);
}
protected async deployRoutingIsm({
config,
logger,
}: {
config: RoutingIsmConfig;
logger: Logger;
}): Promise<IRoutingIsm> {
// filter out domains which are not part of the multiprovider
const { availableDomains, availableDomainIds } =
this.filterRoutingIsmDomains({
config,
});
config = {
...config,
domains: availableDomains,
};
// deploy the submodules first
const submoduleAddresses: Address[] = [];
for (const origin of Object.keys(config.domains)) {
const { address } = await this.deploy({
config: config.domains[origin],
});
submoduleAddresses.push(address);
}
if (config.type === IsmType.FALLBACK_ROUTING) {
// deploy the fallback routing ISM
logger.debug('Deploying fallback routing ISM ...');
const ism = await this.multiProvider.handleDeploy(
this.chain,
new DefaultFallbackRoutingIsm__factory(),
[this.args.addresses.mailbox],
);
// initialize the fallback routing ISM
logger.debug('Initializing fallback routing ISM ...');
const tx = await ism['initialize(address,uint32[],address[])'](
config.owner,
availableDomainIds,
submoduleAddresses,
this.multiProvider.getTransactionOverrides(this.args.chain),
);
await this.multiProvider.handleTx(this.chain, tx);
// return the fallback routing ISM
return ism;
}
// then deploy the domain routing ISM
logger.debug('Deploying domain routing ISM ...');
return this.deployDomainRoutingIsm({
owner: config.owner,
domainIds: availableDomainIds,
submoduleAddresses,
});
}
protected async deployDomainRoutingIsm({
owner,
domainIds,
submoduleAddresses,
}: {
owner: string;
domainIds: number[];
submoduleAddresses: string[];
}): Promise<DomainRoutingIsm> {
const overrides = this.multiProvider.getTransactionOverrides(
this.args.chain,
);
const signer = this.multiProvider.getSigner(this.args.chain);
const domainRoutingIsmFactory = DomainRoutingIsmFactory__factory.connect(
this.args.addresses.domainRoutingIsmFactory,
signer,
);
// estimate gas
const estimatedGas = await domainRoutingIsmFactory.estimateGas.deploy(
owner,
domainIds,
submoduleAddresses,
overrides,
);
// deploying new domain routing ISM, add gas buffer
const tx = await domainRoutingIsmFactory.deploy(
owner,
domainIds,
submoduleAddresses,
{
gasLimit: addBufferToGasLimit(estimatedGas),
...overrides,
},
);
const receipt = await this.multiProvider.handleTx(this.args.chain, tx);
const dispatchLogs = findMatchingLogEvents(
receipt.logs,
domainRoutingIsmFactory.interface,
'ModuleDeployed',
);
if (dispatchLogs.length === 0) {
throw new Error('No ModuleDeployed event found');
}
const moduleAddress = dispatchLogs[0].args['module'];
return DomainRoutingIsm__factory.connect(moduleAddress, signer);
}
protected async deployAggregationIsm({
return this.ismFactory.deploy({
destination: this.chain,
config,
logger,
}: {
config: AggregationIsmConfig;
logger: Logger;
}): Promise<IAggregationIsm> {
const addresses: Address[] = [];
// Needs to be deployed sequentially because Ethers will throw `Error: replacement fee too low`
for (const module of config.modules) {
const submodule = await this.deploy({ config: module });
addresses.push(submodule.address);
}
const factoryName = 'staticAggregationIsmFactory';
const address = await EvmModuleDeployer.deployStaticAddressSet({
chain: this.chain,
factory: this.factories[factoryName],
values: addresses,
logger: logger,
threshold: config.threshold,
multiProvider: this.multiProvider,
mailbox: this.mailbox,
});
const signer = this.multiProvider.getSigner(this.args.chain);
return IAggregationIsm__factory.connect(address, signer);
}
// filtering out domains which are not part of the multiprovider
private filterRoutingIsmDomains({ config }: { config: RoutingIsmConfig }) {
const availableDomainIds: number[] = [];
const availableDomains = objFilter(
config.domains,
(domain, _): _ is IsmConfig => {
const domainId = this.multiProvider.tryGetDomainId(domain);
if (domainId === null) {
this.logger.warn(
`Domain ${domain} doesn't have chain metadata provided, skipping ...`,
);
return false;
}
availableDomainIds.push(domainId);
return true;
},
);
return { availableDomains, availableDomainIds };
}
}

@ -168,7 +168,6 @@ describe('HyperlaneIsmFactory', async () => {
ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
ismFactory = new HyperlaneIsmFactory(contractsMap, multiProvider);
ismFactory.setDeployer(new TestCoreDeployer(multiProvider, ismFactory));
exampleRoutingConfig = {
type: IsmType.ROUTING,

@ -2,6 +2,7 @@ import { ethers } from 'ethers';
import { Logger } from 'pino';
import {
ArbL2ToL1Ism__factory,
DefaultFallbackRoutingIsm,
DefaultFallbackRoutingIsm__factory,
DomainRoutingIsm,
@ -33,7 +34,10 @@ import {
import { HyperlaneApp } from '../app/HyperlaneApp.js';
import { appFromAddressesMapHelper } from '../contracts/contracts.js';
import { HyperlaneAddressesMap } from '../contracts/types.js';
import {
HyperlaneAddressesMap,
HyperlaneContractsMap,
} from '../contracts/types.js';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
import {
ProxyFactoryFactories,
@ -55,15 +59,39 @@ import {
} from './types.js';
import { 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(),
};
class IsmDeployer extends HyperlaneDeployer<{}, typeof ismFactories> {
protected readonly cachingEnabled = false;
deployContracts(_chain: ChainName, _config: any): Promise<any> {
throw new Error('Method not implemented.');
}
}
export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
// 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
public deployedIsms: ChainMap<any> = {};
protected readonly deployer: IsmDeployer;
protected deployer?: HyperlaneDeployer<any, any>;
setDeployer(deployer: HyperlaneDeployer<any, any>): void {
this.deployer = deployer;
constructor(
contractsMap: HyperlaneContractsMap<ProxyFactoryFactories>,
public readonly multiProvider: MultiProvider,
) {
super(
contractsMap,
multiProvider,
rootLogger.child({ module: 'ismFactoryApp' }),
);
this.deployer = new IsmDeployer(multiProvider, ismFactories);
}
static fromAddressesMap(
@ -75,11 +103,7 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
proxyFactoryFactories,
multiProvider,
);
return new HyperlaneIsmFactory(
helper.contractsMap,
multiProvider,
rootLogger.child({ module: 'ismFactoryApp' }),
);
return new HyperlaneIsmFactory(helper.contractsMap, multiProvider);
}
async deploy<C extends IsmConfig>(params: {
@ -142,56 +166,39 @@ export class HyperlaneIsmFactory extends HyperlaneApp<ProxyFactoryFactories> {
});
break;
case IsmType.OP_STACK:
assert(
this.deployer,
`HyperlaneDeployer must be set to deploy ${ismType}`,
);
contract = await this.deployer.deployContractFromFactory(
destination,
new OPStackIsm__factory(),
IsmType.OP_STACK,
[config.nativeBridge],
);
contract = await this.deployer.deployContract(destination, ismType, [
config.nativeBridge,
]);
break;
case IsmType.PAUSABLE:
assert(
this.deployer,
`HyperlaneDeployer must be set to deploy ${ismType}`,
);
contract = await this.deployer.deployContractFromFactory(
contract = await this.deployer.deployContract(
destination,
new PausableIsm__factory(),
IsmType.PAUSABLE,
[config.owner],
);
await this.deployer.transferOwnershipOfContracts(destination, config, {
[IsmType.PAUSABLE]: contract,
});
break;
case IsmType.TRUSTED_RELAYER:
assert(
this.deployer,
`HyperlaneDeployer must be set to deploy ${ismType}`,
);
assert(mailbox, `Mailbox address is required for deploying ${ismType}`);
contract = await this.deployer.deployContractFromFactory(
contract = await this.deployer.deployContract(
destination,
new TrustedRelayerIsm__factory(),
IsmType.TRUSTED_RELAYER,
[mailbox, config.relayer],
);
break;
case IsmType.TEST_ISM:
if (!this.deployer) {
throw new Error(`HyperlaneDeployer must be set to deploy ${ismType}`);
}
contract = await this.deployer.deployContractFromFactory(
contract = await this.deployer.deployContract(
destination,
new TestIsm__factory(),
IsmType.TEST_ISM,
[],
);
break;
case IsmType.ARB_L2_TO_L1:
contract = await this.deployer.deployContract(
destination,
IsmType.ARB_L2_TO_L1,
[config.bridge],
);
break;
default:
throw new Error(`Unsupported ISM type ${ismType}`);
}

@ -15,11 +15,11 @@ import {
rootLogger,
} from '@hyperlane-xyz/utils';
import { transferOwnershipTransactions } from '../contracts/contracts.js';
import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js';
import { EvmIsmModule } from '../ism/EvmIsmModule.js';
import { DerivedIsmConfig } from '../ism/EvmIsmReader.js';
import { MultiProvider } from '../providers/MultiProvider.js';
@ -215,12 +215,13 @@ export class EvmERC20WarpModule extends HyperlaneModule<
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): AnnotatedEV5Transaction[] {
return EvmModuleDeployer.createTransferOwnershipTx({
actualOwner: actualConfig.owner,
expectedOwner: expectedConfig.owner,
deployedAddress: this.args.addresses.deployedTokenRoute,
chainId: this.domainId,
});
return transferOwnershipTransactions(
this.multiProvider.getDomainId(this.args.chain),
this.args.addresses.deployedTokenRoute,
actualConfig,
expectedConfig,
`${expectedConfig.type} Warp Route`,
);
}
/**

@ -103,10 +103,12 @@ export {
parseLegacyMultisigIsmMetadata,
} from './multisig.js';
export {
ObjectDiff,
ValueOf,
arrayToObject,
deepCopy,
deepEquals,
diffObjMerge,
invertKeysAndValues,
isObjEmpty,
isObject,
@ -120,11 +122,14 @@ export {
pick,
promiseObjAll,
stringifyObject,
diffObjMerge,
ObjectDiff,
} from './objects.js';
export { Result, failure, success } from './result.js';
export { difference, setEquality, symmetricDifference } from './sets.js';
export {
difference,
intersection,
setEquality,
symmetricDifference,
} from './sets.js';
export {
errorToString,
fromHexString,

@ -22,3 +22,13 @@ export function symmetricDifference<T>(a: Set<T>, b: Set<T>) {
export function setEquality<T>(a: Set<T>, b: Set<T>) {
return symmetricDifference(a, b).size === 0;
}
export function intersection<T>(a: Set<T>, b: Set<T>) {
const _intersection = new Set<T>();
a.forEach((elem) => {
if (b.has(elem)) {
_intersection.add(elem);
}
});
return _intersection;
}

Loading…
Cancel
Save