feat: implement `create()` for `EvmHookModule` (#3861)

resolves https://github.com/hyperlane-xyz/issues/issues/1153

- enables creation of new Hooks through the EvmHookModule
- introduce `EvmModuleDeployer`
	- separate from `HyperlaneDeployer`
	- contains some basic methods to deploy a contract/proxy
	- reduces module necessity HyperlaneDeployer

IN PROGRESS:
- [x] tests
- [x] figure out why randomly generated routing/fallbackrouting hooks
fail
	- [x] figure out why protocol fee hooks fail

![image](https://github.com/hyperlane-xyz/hyperlane-monorepo/assets/10051819/4cba7af3-4e72-49f6-8f98-fd7fea147282)

---------

Signed-off-by: Paul Balaji <paul@hyperlane.xyz>
pull/4011/head
Paul Balaji 5 months ago committed by GitHub
parent cd419c98a3
commit e0f226806e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/shy-countries-heal.md
  2. 6
      typescript/cli/src/deploy/warp.ts
  3. 287
      typescript/sdk/src/deploy/EvmModuleDeployer.ts
  4. 488
      typescript/sdk/src/hook/EvmHookModule.hardhat-test.ts
  5. 627
      typescript/sdk/src/hook/EvmHookModule.ts
  6. 78
      typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts
  7. 165
      typescript/sdk/src/ism/EvmIsmModule.ts
  8. 5
      typescript/utils/src/objects.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/sdk': minor
---
- Enables creation of new Hooks through the `EvmHookModule`.
- Introduces an `EvmModuleDeployer` to perform the barebones tasks of deploying contracts/proxies.

@ -8,7 +8,6 @@ import {
HypERC721Deployer,
HyperlaneAddresses,
HyperlaneContractsMap,
HyperlaneDeployer,
HyperlaneProxyFactoryDeployer,
MultiProvider,
TOKEN_TYPE_TO_STANDARD,
@ -196,7 +195,6 @@ async function deployAndResolveWarpIsm(
chain,
warpConfig,
multiProvider,
ismFactoryDeployer,
{
domainRoutingIsmFactory: chainAddresses.domainRoutingIsmFactory,
staticAggregationHookFactory:
@ -227,7 +225,6 @@ async function createWarpIsm(
chain: string,
warpConfig: WarpRouteDeployConfig,
multiProvider: MultiProvider,
ismFactoryDeployer: HyperlaneDeployer<any, any>,
factoryAddresses: HyperlaneAddresses<any>,
): Promise<string> {
const {
@ -240,9 +237,8 @@ async function createWarpIsm(
const evmIsmModule = await EvmIsmModule.create({
chain,
multiProvider,
deployer: ismFactoryDeployer,
mailbox: warpConfig[chain].mailbox,
factories: {
proxyFactoryFactories: {
domainRoutingIsmFactory,
staticAggregationHookFactory,
staticAggregationIsmFactory,

@ -0,0 +1,287 @@
import { ethers } from 'ethers';
import { Logger } from 'pino';
import {
StaticAddressSetFactory,
StaticThresholdAddressSetFactory,
TransparentUpgradeableProxy__factory,
} from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import { Address, rootLogger } from '@hyperlane-xyz/utils';
import { HyperlaneContracts, HyperlaneFactories } from '../contracts/types.js';
import { MultiProvider } from '../providers/MultiProvider.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 = new ContractVerifier(
multiProvider,
{},
coreBuildArtifact,
ExplorerLicenseType.MIT,
),
) {}
// Deploys a contract from a factory
public async deployContractFromFactory<F extends ethers.ContractFactory>({
chain,
factory,
contractName,
constructorArgs,
initializeArgs,
}: {
chain: ChainName;
factory: F;
contractName: string;
constructorArgs: Parameters<F['deploy']>;
initializeArgs?: Parameters<Awaited<ReturnType<F['deploy']>>['initialize']>;
}): Promise<ReturnType<F['deploy']>> {
this.logger.info(
`Deploy ${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}`);
const overrides = this.multiProvider.getTransactionOverrides(chain);
const initTx = await contract.initialize(...initializeArgs, overrides);
await this.multiProvider.handleTx(chain, initTx);
}
const verificationInput = getContractVerificationInput(
contractName,
contract,
factory.bytecode,
);
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,
});
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 address = await factory['getAddress(address[],uint8)'](
values,
threshold,
);
const code = await multiProvider.getProvider(chain).getCode(address);
if (code === '0x') {
logger.debug(
`Deploying new ${threshold} of ${values.length} address set to ${chain}`,
);
const overrides = multiProvider.getTransactionOverrides(chain);
const hash = await factory['deploy(address[],uint8)'](
values,
threshold,
overrides,
);
await multiProvider.handleTx(chain, hash);
} else {
logger.debug(
`Recovered ${threshold} of ${values.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;
}
}

@ -0,0 +1,488 @@
/* eslint-disable no-console */
import { expect } from 'chai';
import hre from 'hardhat';
import {
Address,
configDeepEquals,
normalizeConfig,
stringifyObject,
} from '@hyperlane-xyz/utils';
import { TestChainName, testChains } from '../consts/testChains.js';
import { HyperlaneAddresses, HyperlaneContracts } from '../contracts/types.js';
import { TestCoreDeployer } from '../core/TestCoreDeployer.js';
import { CoreAddresses } from '../core/contracts.js';
import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js';
import { ProxyFactoryFactories } from '../deploy/contracts.js';
import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { randomAddress, randomInt } from '../test/testUtils.js';
import { EvmHookModule } from './EvmHookModule.js';
import {
AggregationHookConfig,
DomainRoutingHookConfig,
FallbackRoutingHookConfig,
HookConfig,
HookType,
IgpHookConfig,
MerkleTreeHookConfig,
PausableHookConfig,
ProtocolFeeHookConfig,
} from './types.js';
const hookTypes = Object.values(HookType);
function randomHookType(): HookType {
// OP_STACK filtering is temporary until we have a way to deploy the required contracts
const filteredHookTypes = hookTypes.filter(
(type) => type !== HookType.OP_STACK && type !== HookType.CUSTOM,
);
return filteredHookTypes[
Math.floor(Math.random() * filteredHookTypes.length)
];
}
function randomProtocolFee(): { maxProtocolFee: string; protocolFee: string } {
const maxProtocolFee = Math.random() * 100000000000000;
const protocolFee = (Math.random() * maxProtocolFee) / 1000;
return {
maxProtocolFee: Math.floor(maxProtocolFee).toString(),
protocolFee: Math.floor(protocolFee).toString(),
};
}
function randomHookConfig(
depth = 0,
maxDepth = 2,
providedHookType?: HookType,
): HookConfig {
const hookType: HookType = providedHookType ?? randomHookType();
if (depth >= maxDepth) {
if (
hookType === HookType.AGGREGATION ||
hookType === HookType.ROUTING ||
hookType === HookType.FALLBACK_ROUTING
) {
return { type: HookType.MERKLE_TREE };
}
}
switch (hookType) {
case HookType.MERKLE_TREE:
return { type: hookType };
case HookType.AGGREGATION:
return {
type: hookType,
hooks: [
randomHookConfig(depth + 1, maxDepth),
randomHookConfig(depth + 1, maxDepth),
],
};
case HookType.INTERCHAIN_GAS_PAYMASTER: {
const owner = randomAddress();
return {
owner,
type: hookType,
beneficiary: randomAddress(),
oracleKey: owner,
overhead: Object.fromEntries(
testChains.map((c) => [c, Math.floor(Math.random() * 100)]),
),
oracleConfig: Object.fromEntries(
testChains.map((c) => [
c,
{
tokenExchangeRate: randomInt(1234567891234).toString(),
gasPrice: randomInt(1234567891234).toString(),
},
]),
),
};
}
case HookType.PROTOCOL_FEE: {
const { maxProtocolFee, protocolFee } = randomProtocolFee();
return {
owner: randomAddress(),
type: hookType,
maxProtocolFee,
protocolFee,
beneficiary: randomAddress(),
};
}
case HookType.OP_STACK:
return {
owner: randomAddress(),
type: hookType,
nativeBridge: randomAddress(),
destinationChain: 'testChain',
};
case HookType.ROUTING:
return {
owner: randomAddress(),
type: hookType,
domains: Object.fromEntries(
testChains.map((c) => [c, randomHookConfig(depth + 1, maxDepth)]),
),
};
case HookType.FALLBACK_ROUTING:
return {
owner: randomAddress(),
type: hookType,
fallback: randomHookConfig(depth + 1, maxDepth),
domains: Object.fromEntries(
testChains.map((c) => [c, randomHookConfig(depth + 1, maxDepth)]),
),
};
case HookType.PAUSABLE:
return {
owner: randomAddress(),
type: hookType,
paused: false,
};
default:
throw new Error(`Unsupported Hook type: ${hookType}`);
}
}
describe('EvmHookModule', async () => {
let multiProvider: MultiProvider;
let coreAddresses: CoreAddresses;
const chain = TestChainName.test4;
let proxyFactoryAddresses: HyperlaneAddresses<ProxyFactoryFactories>;
let factoryContracts: HyperlaneContracts<ProxyFactoryFactories>;
beforeEach(async () => {
const [signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
const contractsMap = await ismFactoryDeployer.deploy(
multiProvider.mapKnownChains(() => ({})),
);
// get addresses of factories for the chain
factoryContracts = contractsMap[chain];
proxyFactoryAddresses = Object.keys(factoryContracts).reduce((acc, key) => {
acc[key] =
contractsMap[chain][key as keyof ProxyFactoryFactories].address;
return acc;
}, {} as Record<string, Address>) as HyperlaneAddresses<ProxyFactoryFactories>;
// legacy HyperlaneIsmFactory is required to do a core deploy
const legacyIsmFactory = new HyperlaneIsmFactory(
contractsMap,
multiProvider,
);
// core deployer for tests
const testCoreDeployer = new TestCoreDeployer(
multiProvider,
legacyIsmFactory,
);
// mailbox and proxy admin for the core deploy
const { mailbox, proxyAdmin, validatorAnnounce } = (
await testCoreDeployer.deployApp()
).getContracts(chain);
coreAddresses = {
mailbox: mailbox.address,
proxyAdmin: proxyAdmin.address,
validatorAnnounce: validatorAnnounce.address,
};
});
// Helper method for checking whether Hook module matches a given config
async function hookModuleMatchesConfig({
hook,
config,
}: {
hook: EvmHookModule;
config: HookConfig;
}): Promise<boolean> {
const normalizedDerivedConfig = normalizeConfig(await hook.read());
const normalizedConfig = normalizeConfig(config);
const matches = configDeepEquals(normalizedDerivedConfig, normalizedConfig);
if (!matches) {
console.error(
'Derived config:\n',
stringifyObject(normalizedDerivedConfig),
);
console.error('Expected config:\n', stringifyObject(normalizedConfig));
}
return matches;
}
// hook module and config for testing
let testHook: EvmHookModule;
let testConfig: HookConfig;
// expect that the hook matches the config after all tests
afterEach(async () => {
expect(
await hookModuleMatchesConfig({ hook: testHook, config: testConfig }),
).to.be.true;
});
// create a new Hook and verify that it matches the config
async function createHook(
config: HookConfig,
): Promise<{ hook: EvmHookModule; initialHookAddress: Address }> {
console.log('Creating hook with config: ', stringifyObject(config));
const hook = await EvmHookModule.create({
chain,
config,
proxyFactoryFactories: proxyFactoryAddresses,
coreAddresses,
multiProvider,
});
testConfig = config;
testHook = hook;
return { hook, initialHookAddress: hook.serialize().deployedHook };
}
describe('create', async () => {
it('deploys a hook of type CUSTOM', async () => {
const config: HookConfig = randomAddress();
await createHook(config);
});
it('deploys a hook of type MERKLE_TREE', async () => {
const config: MerkleTreeHookConfig = {
type: HookType.MERKLE_TREE,
};
await createHook(config);
});
it('deploys a hook of type INTERCHAIN_GAS_PAYMASTER', async () => {
const owner = randomAddress();
const config: IgpHookConfig = {
owner,
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: randomAddress(),
oracleKey: owner,
overhead: Object.fromEntries(
testChains.map((c) => [c, Math.floor(Math.random() * 100)]),
),
oracleConfig: Object.fromEntries(
testChains.map((c) => [
c,
{
tokenExchangeRate: randomInt(1234567891234).toString(),
gasPrice: randomInt(1234567891234).toString(),
},
]),
),
};
await createHook(config);
});
it('deploys a hook of type PROTOCOL_FEE', async () => {
const { maxProtocolFee, protocolFee } = randomProtocolFee();
const config: ProtocolFeeHookConfig = {
owner: randomAddress(),
type: HookType.PROTOCOL_FEE,
maxProtocolFee,
protocolFee,
beneficiary: randomAddress(),
};
await createHook(config);
});
it('deploys a hook of type ROUTING', async () => {
const config: DomainRoutingHookConfig = {
owner: randomAddress(),
type: HookType.ROUTING,
domains: Object.fromEntries(
testChains
.filter((c) => c !== TestChainName.test4)
.map((c) => [
c,
{
type: HookType.MERKLE_TREE,
},
]),
),
};
await createHook(config);
});
it('deploys a hook of type FALLBACK_ROUTING', async () => {
const config: FallbackRoutingHookConfig = {
owner: randomAddress(),
type: HookType.FALLBACK_ROUTING,
fallback: { type: HookType.MERKLE_TREE },
domains: Object.fromEntries(
testChains
.filter((c) => c !== TestChainName.test4)
.map((c) => [
c,
{
type: HookType.MERKLE_TREE,
},
]),
),
};
await createHook(config);
});
it('deploys a hook of type AGGREGATION', async () => {
const config: AggregationHookConfig = {
type: HookType.AGGREGATION,
hooks: [{ type: HookType.MERKLE_TREE }, { type: HookType.MERKLE_TREE }],
};
await createHook(config);
});
it('deploys a hook of type PAUSABLE', async () => {
const config: PausableHookConfig = {
owner: randomAddress(),
type: HookType.PAUSABLE,
paused: false,
};
await createHook(config);
});
// it('deploys a hook of type OP_STACK', async () => {
// need to setup deploying/mocking IL1CrossDomainMessenger before this test can be enabled
// const config: OpStackHookConfig = {
// owner: randomAddress(),
// type: HookType.OP_STACK,
// nativeBridge: randomAddress(),
// destinationChain: 'testChain',
// };
// await createHook(config);
// });
for (let i = 0; i < 16; i++) {
it(`deploys a random hook config #${i}`, async () => {
// random config with depth 0-2
const config = randomHookConfig();
await createHook(config);
});
}
it('regression test #1', async () => {
const config: HookConfig = {
type: HookType.AGGREGATION,
hooks: [
{
owner: '0xebe67f0a423fd1c4af21debac756e3238897c665',
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: '0xfe3be5940327305aded56f20359761ef85317554',
oracleKey: '0xebe67f0a423fd1c4af21debac756e3238897c665',
overhead: {
test1: 18,
test2: 85,
test3: 23,
test4: 69,
},
oracleConfig: {
test1: {
tokenExchangeRate: '1032586497157',
gasPrice: '1026942205817',
},
test2: {
tokenExchangeRate: '81451154935',
gasPrice: '1231220057593',
},
test3: {
tokenExchangeRate: '31347320275',
gasPrice: '21944956734',
},
test4: {
tokenExchangeRate: '1018619796544',
gasPrice: '1124484183261',
},
},
},
{
owner: '0xcc803fc9e6551b9eaaebfabbdd5af3eccea252ff',
type: HookType.ROUTING,
domains: {
test1: {
type: HookType.MERKLE_TREE,
},
test2: {
owner: '0x7e43dfa88c4a5d29a8fcd69883b7f6843d465ca3',
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: '0x762e71a849a3825613cf5cbe70bfff27d0fe7766',
oracleKey: '0x7e43dfa88c4a5d29a8fcd69883b7f6843d465ca3',
overhead: {
test1: 46,
test2: 34,
test3: 47,
test4: 24,
},
oracleConfig: {
test1: {
tokenExchangeRate: '1132883204938',
gasPrice: '1219466305935',
},
test2: {
tokenExchangeRate: '938422264723',
gasPrice: '229134538568',
},
test3: {
tokenExchangeRate: '69699594189',
gasPrice: '475781234236',
},
test4: {
tokenExchangeRate: '1027245678936',
gasPrice: '502686418976',
},
},
},
test3: {
type: HookType.MERKLE_TREE,
},
test4: {
owner: '0xa1ce72b70566f2cba6000bfe6af50f0f358f49d7',
type: HookType.INTERCHAIN_GAS_PAYMASTER,
beneficiary: '0x9796c0c49c61fe01eb1a8ba56d09b831f6da8603',
oracleKey: '0xa1ce72b70566f2cba6000bfe6af50f0f358f49d7',
overhead: {
test1: 71,
test2: 16,
test3: 37,
test4: 13,
},
oracleConfig: {
test1: {
tokenExchangeRate: '443874625350',
gasPrice: '799154764503',
},
test2: {
tokenExchangeRate: '915348561750',
gasPrice: '1124345797215',
},
test3: {
tokenExchangeRate: '930832717805',
gasPrice: '621743941770',
},
test4: {
tokenExchangeRate: '147394981623',
gasPrice: '766494385983',
},
},
},
},
},
],
};
await createHook(config);
});
});
});

@ -1,45 +1,116 @@
import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
import { BigNumber, ethers } from 'ethers';
import {
DomainRoutingHook,
DomainRoutingHook__factory,
FallbackDomainRoutingHook,
IL1CrossDomainMessenger__factory,
IPostDispatchHook__factory,
InterchainGasPaymaster,
OPStackHook,
OPStackIsm__factory,
PausableHook,
ProtocolFee,
StaticAggregationHook,
StaticAggregationHookFactory__factory,
StaticAggregationHook__factory,
StorageGasOracle,
} from '@hyperlane-xyz/core';
import {
Address,
ProtocolType,
addressToBytes32,
configDeepEquals,
rootLogger,
} from '@hyperlane-xyz/utils';
import { TOKEN_EXCHANGE_RATE_SCALE } from '../consts/igp.js';
import { HyperlaneAddresses } from '../contracts/types.js';
import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
import { CoreAddresses } from '../core/contracts.js';
import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.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 { IsmType, OpStackIsmConfig } from '../ism/types.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
import { EvmHookReader } from './EvmHookReader.js';
import { HookFactories } from './contracts.js';
import { HookConfig } from './types.js';
import { DeployedHook, HookFactories, hookFactories } from './contracts.js';
import {
AggregationHookConfig,
DomainRoutingHookConfig,
FallbackRoutingHookConfig,
HookConfig,
HookType,
IgpHookConfig,
OpStackHookConfig,
PausableHookConfig,
ProtocolFeeHookConfig,
} from './types.js';
type HookModuleAddresses = {
deployedHook: Address;
mailbox: Address;
proxyAdmin: Address;
};
// WIP example implementation of EvmHookModule
export class EvmHookModule extends HyperlaneModule<
ProtocolType.Ethereum,
HookConfig,
HyperlaneAddresses<HookFactories> & {
deployedHook: Address;
}
HyperlaneAddresses<ProxyFactoryFactories> & HookModuleAddresses
> {
protected logger = rootLogger.child({ module: 'EvmHookModule' });
protected reader: EvmHookReader;
protected readonly logger = rootLogger.child({ module: 'EvmHookModule' });
protected readonly reader: EvmHookReader;
protected readonly deployer: EvmModuleDeployer<HookFactories & IgpFactories>;
// Adding these to reduce how often we need to grab from MultiProvider.
public readonly chain: string;
// We use domainId here because MultiProvider.getDomainId() will always
// return a number, and EVM the domainId and chainId are the same.
public readonly domainId: number;
// Transaction overrides for the chain
protected readonly txOverrides: Partial<ethers.providers.TransactionRequest>;
protected constructor(
protected readonly multiProvider: MultiProvider,
protected readonly deployer: HyperlaneDeployer<any, any>,
args: HyperlaneModuleParams<
HookConfig,
HyperlaneAddresses<HookFactories> & {
deployedHook: Address;
}
HyperlaneAddresses<ProxyFactoryFactories> & HookModuleAddresses
>,
contractVerifier?: ContractVerifier,
) {
super(args);
this.reader = new EvmHookReader(multiProvider, args.chain);
this.reader = new EvmHookReader(multiProvider, this.args.chain);
this.deployer = new EvmModuleDeployer(
multiProvider,
{
...hookFactories,
...igpFactories,
},
this.logger,
contractVerifier,
);
this.chain = this.multiProvider.getChainName(this.args.chain);
this.domainId = this.multiProvider.getDomainId(this.chain);
this.txOverrides = this.multiProvider.getTransactionOverrides(this.chain);
}
public async read(): Promise<HookConfig> {
return this.reader.deriveHookConfig(this.args.addresses.deployedHook);
return typeof this.args.config === 'string'
? this.args.addresses.deployedHook
: this.reader.deriveHookConfig(this.args.addresses.deployedHook);
}
public async update(_config: HookConfig): Promise<AnnotatedEV5Transaction[]> {
@ -47,7 +118,527 @@ export class EvmHookModule extends HyperlaneModule<
}
// manually write static create function
public static create(_config: HookConfig): Promise<EvmHookModule> {
throw new Error('not implemented');
public static async create({
chain,
config,
proxyFactoryFactories,
coreAddresses,
multiProvider,
}: {
chain: ChainNameOrId;
config: HookConfig;
proxyFactoryFactories: HyperlaneAddresses<ProxyFactoryFactories>;
coreAddresses: CoreAddresses;
multiProvider: MultiProvider;
}): Promise<EvmHookModule> {
// instantiate new EvmHookModule
const module = new EvmHookModule(multiProvider, {
addresses: {
...proxyFactoryFactories,
...coreAddresses,
deployedHook: ethers.constants.AddressZero,
},
chain,
config,
});
// deploy hook and assign address to module
const deployedHook = await module.deploy({ config });
module.args.addresses.deployedHook = deployedHook.address;
return module;
}
// Compute delta between current and target domain configurations
protected async computeRoutingHooksToSet({
currentDomains,
targetDomains,
}: {
currentDomains: DomainRoutingHookConfig['domains'];
targetDomains: DomainRoutingHookConfig['domains'];
}): Promise<DomainRoutingHook.HookConfigStruct[]> {
const routingHookUpdates: DomainRoutingHook.HookConfigStruct[] = [];
// Iterate over the target domains and compare with the current configuration
for (const [dest, targetDomainConfig] of Object.entries(targetDomains)) {
const destDomain = this.multiProvider.tryGetDomainId(dest);
if (!destDomain) {
this.logger.warn(`Domain not found in MultiProvider: ${dest}`);
continue;
}
// If the domain is not in the current config or the config has changed, deploy a new hook
// TODO: in-place updates per domain as a future optimization
if (!configDeepEquals(currentDomains[dest], targetDomainConfig)) {
const domainHook = await this.deploy({
config: targetDomainConfig,
});
routingHookUpdates.push({
destination: destDomain,
hook: domainHook.address,
});
}
}
return routingHookUpdates;
}
// Updates a routing hook
protected async updateRoutingHook({
current,
target,
}: {
current: DomainRoutingHookConfig | FallbackRoutingHookConfig;
target: DomainRoutingHookConfig | FallbackRoutingHookConfig;
}): Promise<AnnotatedEV5Transaction[]> {
// Deploy a new fallback hook if the fallback config has changed
if (
target.type === HookType.FALLBACK_ROUTING &&
!configDeepEquals(
target.fallback,
(current as FallbackRoutingHookConfig).fallback,
)
) {
const hook = await this.deploy({ config: target });
this.args.addresses.deployedHook = hook.address;
}
const routingUpdates = await this.computeRoutingHooksToSet({
currentDomains: current.domains,
targetDomains: target.domains,
});
// Return if no updates are required
if (routingUpdates.length === 0) {
return [];
}
// Create tx for setting hooks
return [
{
annotation: 'Updating routing hooks...',
chainId: this.domainId,
to: this.args.addresses.deployedHook,
data: DomainRoutingHook__factory.createInterface().encodeFunctionData(
'setHooks',
[routingUpdates],
),
},
];
}
protected async deploy({
config,
}: {
config: HookConfig;
}): Promise<DeployedHook> {
// If it's an address, just return a base Hook
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 Hooks
// @ts-ignore
return IPostDispatchHook__factory.connect(
config,
this.multiProvider.getSignerOrProvider(this.args.chain),
);
}
switch (config.type) {
case HookType.MERKLE_TREE:
return this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.MERKLE_TREE,
constructorArgs: [this.args.addresses.mailbox],
});
case HookType.INTERCHAIN_GAS_PAYMASTER:
return this.deployIgpHook({ config });
case HookType.AGGREGATION:
return this.deployAggregationHook({ config });
case HookType.PROTOCOL_FEE:
return this.deployProtocolFeeHook({ config });
case HookType.OP_STACK:
return this.deployOpStackHook({ config });
case HookType.ROUTING:
case HookType.FALLBACK_ROUTING:
return this.deployRoutingHook({ config });
case HookType.PAUSABLE: {
return this.deployPausableHook({ config });
}
default:
throw new Error(`Unsupported hook config: ${config}`);
}
}
protected async deployProtocolFeeHook({
config,
}: {
config: ProtocolFeeHookConfig;
}): Promise<ProtocolFee> {
this.logger.debug('Deploying ProtocolFeeHook...');
return this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.PROTOCOL_FEE,
constructorArgs: [
config.maxProtocolFee,
config.protocolFee,
config.beneficiary,
config.owner,
],
});
}
protected async deployPausableHook({
config,
}: {
config: PausableHookConfig;
}): Promise<PausableHook> {
this.logger.debug('Deploying PausableHook...');
const hook = await this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.PAUSABLE,
constructorArgs: [],
});
// transfer ownership
await this.multiProvider.handleTx(
this.chain,
hook.transferOwnership(config.owner, this.txOverrides),
);
return hook;
}
protected async deployAggregationHook({
config,
}: {
config: AggregationHookConfig;
}): Promise<StaticAggregationHook> {
this.logger.debug('Deploying AggregationHook...');
// deploy subhooks
const aggregatedHooks = [];
for (const hookConfig of config.hooks) {
const { address } = await this.deploy({ config: hookConfig });
aggregatedHooks.push(address);
}
// deploy aggregation hook
this.logger.debug(
`Deploying aggregation hook of type ${config.hooks.map((h) =>
typeof h === 'string' ? h : h.type,
)}...`,
);
const signer = this.multiProvider.getSigner(this.chain);
const factory = StaticAggregationHookFactory__factory.connect(
this.args.addresses.staticAggregationHookFactory,
signer,
);
const address = await EvmModuleDeployer.deployStaticAddressSet({
chain: this.chain,
factory,
values: aggregatedHooks,
logger: this.logger,
multiProvider: this.multiProvider,
});
// return aggregation hook
return StaticAggregationHook__factory.connect(address, signer);
}
protected async deployOpStackHook({
config,
}: {
config: OpStackHookConfig;
}): Promise<OPStackHook> {
const chain = this.chain;
const mailbox = this.args.addresses.mailbox;
this.logger.debug(
'Deploying OPStackHook for %s to %s...',
chain,
config.destinationChain,
);
// fetch l2 messenger address from l1 messenger
const l1Messenger = IL1CrossDomainMessenger__factory.connect(
config.nativeBridge,
this.multiProvider.getSignerOrProvider(chain),
);
const l2Messenger: Address = await l1Messenger.OTHER_MESSENGER();
// deploy opstack ism
const ismConfig: OpStackIsmConfig = {
type: IsmType.OP_STACK,
origin: chain,
nativeBridge: l2Messenger,
};
// deploy opstack ism
const opStackIsmAddress = (
await EvmIsmModule.create({
chain: config.destinationChain,
config: ismConfig,
proxyFactoryFactories: this.args.addresses,
mailbox: mailbox,
multiProvider: this.multiProvider,
})
).serialize().deployedIsm;
// connect to ISM
const opstackIsm = OPStackIsm__factory.connect(
opStackIsmAddress,
this.multiProvider.getSignerOrProvider(config.destinationChain),
);
// deploy opstack hook
const hook = await this.deployer.deployContract({
chain,
contractKey: HookType.OP_STACK,
constructorArgs: [
mailbox,
this.multiProvider.getDomainId(config.destinationChain),
addressToBytes32(opstackIsm.address),
config.nativeBridge,
],
});
// set authorized hook on opstack ism
const authorizedHook = await opstackIsm.authorizedHook();
if (authorizedHook === addressToBytes32(hook.address)) {
this.logger.debug(
'Authorized hook already set on ism %s',
opstackIsm.address,
);
return hook;
} else if (
authorizedHook !== addressToBytes32(ethers.constants.AddressZero)
) {
this.logger.debug(
'Authorized hook mismatch on ism %s, expected %s, got %s',
opstackIsm.address,
addressToBytes32(hook.address),
authorizedHook,
);
throw new Error('Authorized hook mismatch');
}
// check if mismatch and redeploy hook
this.logger.debug(
'Setting authorized hook %s on ism % on destination %s',
hook.address,
opstackIsm.address,
config.destinationChain,
);
await this.multiProvider.handleTx(
config.destinationChain,
opstackIsm.setAuthorizedHook(
addressToBytes32(hook.address),
this.multiProvider.getTransactionOverrides(config.destinationChain),
),
);
return hook;
}
protected async deployRoutingHook({
config,
}: {
config: DomainRoutingHookConfig | FallbackRoutingHookConfig;
}): Promise<DomainRoutingHook> {
// originally set owner to deployer so we can set hooks
const deployerAddress = await this.multiProvider.getSignerAddress(
this.chain,
);
let routingHook: DomainRoutingHook | FallbackDomainRoutingHook;
if (config.type === HookType.FALLBACK_ROUTING) {
// 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,
],
});
} else {
// deploy routing hook
routingHook = await this.deployer.deployContract({
chain: this.chain,
contractKey: HookType.ROUTING,
constructorArgs: [this.args.addresses.mailbox, deployerAddress],
});
}
// compute the hooks that need to be set
const hooksToSet = await this.computeRoutingHooksToSet({
currentDomains: {},
targetDomains: config.domains,
});
// set hooks
await this.multiProvider.handleTx(
this.chain,
routingHook.setHooks(hooksToSet, this.txOverrides),
);
// transfer ownership
await this.multiProvider.handleTx(
this.chain,
routingHook.transferOwnership(config.owner, this.txOverrides),
);
// return a fully configured routing hook
return routingHook;
}
protected async deployIgpHook({
config,
}: {
config: IgpHookConfig;
}): Promise<InterchainGasPaymaster> {
this.logger.debug('Deploying IGP as hook...');
// Deploy the StorageGasOracle
const storageGasOracle = await this.deployStorageGasOracle({
config,
});
// Deploy the InterchainGasPaymaster
const interchainGasPaymaster = await this.deployInterchainGasPaymaster({
storageGasOracle,
config,
});
return interchainGasPaymaster;
}
protected async deployInterchainGasPaymaster({
storageGasOracle,
config,
}: {
storageGasOracle: StorageGasOracle;
config: IgpConfig;
}): Promise<InterchainGasPaymaster> {
const deployerAddress = await this.multiProvider.getSignerAddress(
this.chain,
);
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 gasParamsToSet: InterchainGasPaymaster.GasParamStruct[] = [];
for (const [remote, gasOverhead] of Object.entries(config.overhead)) {
// Note: non-EVM remotes actually *are* supported, provided that the remote domain is in the MultiProvider.
// Previously would check core metadata for non EVMs and fallback to multiprovider for custom EVMs
const remoteDomain = this.multiProvider.tryGetDomainId(remote);
if (!remoteDomain) {
this.logger.warn(
`Skipping overhead ${this.chain} -> ${remote}. Expected if the remote is a non-EVM chain.`,
);
continue;
}
this.logger.debug(
`Setting gas params for ${this.chain} -> ${remote}: gasOverhead = ${gasOverhead} gasOracle = ${storageGasOracle.address}`,
);
gasParamsToSet.push({
remoteDomain,
config: {
gasOverhead,
gasOracle: storageGasOracle.address,
},
});
}
if (gasParamsToSet.length > 0) {
await this.multiProvider.handleTx(
this.chain,
igp.setDestinationGasConfigs(gasParamsToSet, this.txOverrides),
);
}
// Transfer igp to the configured owner
await this.multiProvider.handleTx(
this.chain,
igp.transferOwnership(config.owner, this.txOverrides),
);
return igp;
}
protected async deployStorageGasOracle({
config,
}: {
config: IgpConfig;
}): Promise<StorageGasOracle> {
const gasOracle = await this.deployer.deployContract({
chain: this.chain,
contractKey: 'storageGasOracle',
constructorArgs: [],
});
if (!config.oracleConfig) {
this.logger.debug('No oracle config provided, skipping...');
return gasOracle;
}
this.logger.info(`Configuring gas oracle from ${this.chain}...`);
const configsToSet: Array<StorageGasOracle.RemoteGasDataConfigStruct> = [];
for (const [remote, desired] of Object.entries(config.oracleConfig)) {
// Note: non-EVM remotes actually *are* supported, provided that the remote domain is in the MultiProvider.
// Previously would check core metadata for non EVMs and fallback to multiprovider for custom EVMs
const remoteDomain = this.multiProvider.tryGetDomainId(remote);
if (!remoteDomain) {
this.logger.warn(
`Skipping gas oracle ${this.chain} -> ${remote}.` +
' Expected if the remote is a non-EVM chain or the remote domain is not the in the MultiProvider.',
);
continue;
}
configsToSet.push({
remoteDomain,
...desired,
});
// Log an example remote gas cost
const exampleRemoteGas = (config.overhead[remote] ?? 200_000) + 50_000;
const exampleRemoteGasCost = BigNumber.from(desired.tokenExchangeRate)
.mul(desired.gasPrice)
.mul(exampleRemoteGas)
.div(TOKEN_EXCHANGE_RATE_SCALE);
this.logger.info(
`${
this.chain
} -> ${remote}: ${exampleRemoteGas} remote gas cost: ${ethers.utils.formatEther(
exampleRemoteGasCost,
)}`,
);
}
if (configsToSet.length > 0) {
await this.multiProvider.handleTx(
this.chain,
gasOracle.setRemoteGasDataConfigs(configsToSet, this.txOverrides),
);
}
// Transfer gas oracle to the configured owner
await this.multiProvider.handleTx(
this.chain,
gasOracle.transferOwnership(config.oracleKey, this.txOverrides),
);
return gasOracle;
}
}

@ -50,43 +50,47 @@ function randomModuleType(): ModuleType {
const randomIsmConfig = (depth = 0, maxDepth = 2): IsmConfig => {
const moduleType =
depth == maxDepth ? ModuleType.MERKLE_ROOT_MULTISIG : randomModuleType();
if (moduleType === ModuleType.MERKLE_ROOT_MULTISIG) {
const n = randomInt(5, 1);
return randomMultisigIsmConfig(randomInt(n, 1), n);
} else if (moduleType === ModuleType.ROUTING) {
const config: RoutingIsmConfig = {
type: IsmType.ROUTING,
owner: randomAddress(),
domains: Object.fromEntries(
testChains.map((c) => [c, randomIsmConfig(depth + 1)]),
),
};
return config;
} else if (moduleType === ModuleType.AGGREGATION) {
const n = randomInt(5, 1);
const modules = new Array<number>(n)
.fill(0)
.map(() => randomIsmConfig(depth + 1));
const config: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
threshold: randomInt(n, 1),
modules,
};
return config;
} else if (moduleType === ModuleType.NULL) {
const config: TrustedRelayerIsmConfig = {
type: IsmType.TRUSTED_RELAYER,
relayer: randomAddress(),
};
return config;
} else {
throw new Error(`Unsupported ISM type: ${moduleType}`);
switch (moduleType) {
case ModuleType.MERKLE_ROOT_MULTISIG: {
const n = randomInt(5, 1);
return randomMultisigIsmConfig(randomInt(n, 1), n);
}
case ModuleType.ROUTING: {
const config: RoutingIsmConfig = {
type: IsmType.ROUTING,
owner: randomAddress(),
domains: Object.fromEntries(
testChains.map((c) => [c, randomIsmConfig(depth + 1)]),
),
};
return config;
}
case ModuleType.AGGREGATION: {
const n = randomInt(5, 1);
const modules = new Array<number>(n)
.fill(0)
.map(() => randomIsmConfig(depth + 1));
const config: AggregationIsmConfig = {
type: IsmType.AGGREGATION,
threshold: randomInt(n, 1),
modules,
};
return config;
}
case ModuleType.NULL: {
const config: TrustedRelayerIsmConfig = {
type: IsmType.TRUSTED_RELAYER,
relayer: randomAddress(),
};
return config;
}
default:
throw new Error(`Unsupported ISM type: ${moduleType}`);
}
};
describe('EvmIsmModule', async () => {
let multiProvider: MultiProvider;
let ismFactoryDeployer: HyperlaneProxyFactoryDeployer;
let exampleRoutingConfig: RoutingIsmConfig;
let mailboxAddress: Address;
let newMailboxAddress: Address;
@ -101,10 +105,9 @@ describe('EvmIsmModule', async () => {
fundingAccount = funder;
multiProvider = MultiProvider.createTestMultiProvider({ signer });
ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider);
const contractsMap = await ismFactoryDeployer.deploy(
multiProvider.mapKnownChains(() => ({})),
);
const contractsMap = await new HyperlaneProxyFactoryDeployer(
multiProvider,
).deploy(multiProvider.mapKnownChains(() => ({})));
// get addresses of factories for the chain
factoryContracts = contractsMap[chain];
@ -186,8 +189,7 @@ describe('EvmIsmModule', async () => {
const ism = await EvmIsmModule.create({
chain,
config,
deployer: ismFactoryDeployer,
factories: factoryAddresses,
proxyFactoryFactories: factoryAddresses,
mailbox: mailboxAddress,
multiProvider,
});

@ -16,8 +16,6 @@ import {
OPStackIsm__factory,
Ownable__factory,
PausableIsm__factory,
StaticAddressSetFactory,
StaticThresholdAddressSetFactory,
TestIsm__factory,
TrustedRelayerIsm__factory,
} from '@hyperlane-xyz/core';
@ -39,11 +37,12 @@ import {
HyperlaneModule,
HyperlaneModuleParams,
} from '../core/AbstractHyperlaneModule.js';
import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js';
import { EvmModuleDeployer } from '../deploy/EvmModuleDeployer.js';
import {
ProxyFactoryFactories,
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';
@ -73,6 +72,7 @@ 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>;
// Adding these to reduce how often we need to grab from MultiProvider.
@ -83,18 +83,34 @@ export class EvmIsmModule extends HyperlaneModule<
protected constructor(
protected readonly multiProvider: MultiProvider,
protected readonly deployer: HyperlaneDeployer<any, any>,
params: HyperlaneModuleParams<
IsmConfig,
HyperlaneAddresses<ProxyFactoryFactories> & IsmModuleAddresses
>,
contractVerifier?: ContractVerifier,
) {
super(params);
this.reader = new EvmIsmReader(multiProvider, params.chain);
const { mailbox: _, deployedIsm: __, ...addresses } = params.addresses;
this.deployer = new EvmModuleDeployer(
this.multiProvider,
{},
this.logger,
contractVerifier,
);
this.factories = attachAndConnectContracts(
addresses,
{
staticMerkleRootMultisigIsmFactory:
params.addresses.staticMerkleRootMultisigIsmFactory,
staticMessageIdMultisigIsmFactory:
params.addresses.staticMessageIdMultisigIsmFactory,
staticAggregationIsmFactory:
params.addresses.staticAggregationIsmFactory,
staticAggregationHookFactory:
params.addresses.staticAggregationHookFactory,
domainRoutingIsmFactory: params.addresses.domainRoutingIsmFactory,
},
proxyFactoryFactories,
multiProvider.getSigner(params.chain),
);
@ -140,9 +156,9 @@ export class EvmIsmModule extends HyperlaneModule<
// Else, we have to figure out what an update for this ISM entails
// If target config is a custom ISM, just update the address
// if config -> custom ISM, update address
// if custom ISM -> custom ISM, update address
// If target config is an address ISM, just update the address
// if config -> address ISM, update address
// if address ISM -> address ISM, update address
if (typeof targetConfig === 'string') {
// TODO: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3773
this.args.addresses.deployedIsm = targetConfig;
@ -151,7 +167,7 @@ export class EvmIsmModule extends HyperlaneModule<
// Check if we need to deploy a new ISM
if (
// if custom ISM -> config, do a new deploy
// if address ISM -> config, do a new deploy
typeof currentConfig === 'string' ||
// if config -> config, AND types are different, do a new deploy
currentConfig.type !== targetConfig.type ||
@ -242,21 +258,23 @@ export class EvmIsmModule extends HyperlaneModule<
}
// manually write static create function
public static async create(params: {
public static async create({
chain,
config,
proxyFactoryFactories,
mailbox,
multiProvider,
}: {
chain: ChainNameOrId;
config: IsmConfig;
deployer: HyperlaneDeployer<any, any>;
factories: HyperlaneAddresses<ProxyFactoryFactories>;
proxyFactoryFactories: HyperlaneAddresses<ProxyFactoryFactories>;
mailbox: Address;
multiProvider: MultiProvider;
}): Promise<EvmIsmModule> {
const { chain, config, deployer, factories, mailbox, multiProvider } =
params;
// instantiate new EvmIsmModule
const module = new EvmIsmModule(multiProvider, deployer, {
const module = new EvmIsmModule(multiProvider, {
addresses: {
...factories,
...proxyFactoryFactories,
mailbox,
deployedIsm: ethers.constants.AddressZero,
},
@ -330,10 +348,10 @@ export class EvmIsmModule extends HyperlaneModule<
}: {
config: C;
}): Promise<DeployedIsm> {
// If it's a custom ISM, just return a base ISM
// 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 custom ISMs
// we can remove the ts-ignore once we have a proper type for address ISMs
// @ts-ignore
return IInterchainSecurityModule__factory.connect(
config,
@ -368,40 +386,40 @@ export class EvmIsmModule extends HyperlaneModule<
});
case IsmType.OP_STACK:
return this.deployer.deployContractFromFactory(
this.chain,
new OPStackIsm__factory(),
IsmType.OP_STACK,
[config.nativeBridge],
);
return this.deployer.deployContractFromFactory({
chain: this.chain,
factory: new OPStackIsm__factory(),
contractName: IsmType.OP_STACK,
constructorArgs: [config.nativeBridge],
});
case IsmType.PAUSABLE:
return this.deployer.deployContractFromFactory(
this.chain,
new PausableIsm__factory(),
IsmType.PAUSABLE,
[config.owner],
);
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(
this.chain,
new TrustedRelayerIsm__factory(),
IsmType.TRUSTED_RELAYER,
[this.args.addresses.mailbox, config.relayer],
);
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(
this.chain,
new TestIsm__factory(),
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}`);
@ -421,7 +439,7 @@ export class EvmIsmModule extends HyperlaneModule<
? 'staticMerkleRootMultisigIsmFactory'
: 'staticMessageIdMultisigIsmFactory';
const address = await EvmIsmModule.deployStaticAddressSet({
const address = await EvmModuleDeployer.deployStaticAddressSet({
chain: this.chain,
factory: this.factories[factoryName],
values: config.validators,
@ -545,7 +563,7 @@ export class EvmIsmModule extends HyperlaneModule<
}
const factoryName = 'staticAggregationIsmFactory';
const address = await EvmIsmModule.deployStaticAddressSet({
const address = await EvmModuleDeployer.deployStaticAddressSet({
chain: this.chain,
factory: this.factories[factoryName],
values: addresses,
@ -578,61 +596,6 @@ export class EvmIsmModule extends HyperlaneModule<
this.args.addresses.mailbox = newMailboxAddress;
}
// Public so it can be reused by the hook module.
// Caller of this function is responsible for verifying the contract
// because they know exactly which factory is being called.
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 address = await factory['getAddress(address[],uint8)'](
values,
threshold,
);
const code = await multiProvider.getProvider(chain).getCode(address);
if (code === '0x') {
logger.debug(
`Deploying new ${threshold} of ${values.length} address set to ${chain}`,
);
const overrides = multiProvider.getTransactionOverrides(chain);
const hash = await factory['deploy(address[],uint8)'](
values,
threshold,
overrides,
);
await multiProvider.handleTx(chain, hash);
} else {
logger.debug(
`Recovered ${threshold} of ${values.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;
}
// filtering out domains which are not part of the multiprovider
private filterRoutingIsmDomains({ config }: { config: RoutingIsmConfig }) {
const availableDomainIds: number[] = [];

@ -2,6 +2,7 @@ import { deepStrictEqual } from 'node:assert/strict';
import { stringify as yamlStringify } from 'yaml';
import { ethersBigNumberSerializer, rootLogger } from './logging.js';
import { WithAddress } from './types.js';
import { assert } from './validation.js';
export function isObject(item: any) {
@ -143,7 +144,7 @@ export function arrayToObject(keys: Array<string>, val = true) {
}
export function stringifyObject(
object: object,
object: any,
format: 'json' | 'yaml' = 'yaml',
space?: number,
): string {
@ -157,7 +158,7 @@ export function stringifyObject(
}
// Function to recursively remove 'address' properties and lowercase string properties
export function normalizeConfig(obj: any): any {
export function normalizeConfig(obj: WithAddress<any>): any {
if (Array.isArray(obj)) {
return obj.map(normalizeConfig);
} else if (obj !== null && typeof obj === 'object') {

Loading…
Cancel
Save