From 170a0fc73922297b1163316825bd98ff352fe57f Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:00:37 -0500 Subject: [PATCH] feat: Add hook update into WarpModule.update() (#4891) ### Description This is a multi-part PR. This adds `createHookUpdateTxs()` to `WarpModule.update()` such that it updates a warp route _without_ a hook, and one with an existing hook. ### Related issues ### Backward compatibility Yes ### Testing Unit Tests --- .changeset/nice-oranges-glow.md | 5 + typescript/cli/src/deploy/warp.ts | 54 +++-- .../cli/src/tests/warp-apply.e2e-test.ts | 42 ++++ typescript/sdk/src/hook/EvmHookModule.ts | 4 +- .../token/EvmERC20WarpModule.hardhat-test.ts | 148 +++++++++++--- .../sdk/src/token/EvmERC20WarpModule.ts | 190 ++++++++++++++++-- 6 files changed, 387 insertions(+), 56 deletions(-) create mode 100644 .changeset/nice-oranges-glow.md diff --git a/.changeset/nice-oranges-glow.md b/.changeset/nice-oranges-glow.md new file mode 100644 index 000000000..2b844bcf9 --- /dev/null +++ b/.changeset/nice-oranges-glow.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/sdk': minor +--- + +Add `createHookUpdateTxs()` to `WarpModule.update()` such that it 1) deploys a hook for a warp route _without_ an existing hook, or 2) update an existing hook. diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 639d5d5c8..17e6c083d 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -3,7 +3,7 @@ import { groupBy } from 'lodash-es'; import { stringify as yamlStringify } from 'yaml'; import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js'; -import { IRegistry } from '@hyperlane-xyz/registry'; +import { ChainAddresses, IRegistry } from '@hyperlane-xyz/registry'; import { AggregationIsmConfig, AnnotatedEV5Transaction, @@ -30,7 +30,6 @@ import { MultisigIsmConfig, OpStackIsmConfig, PausableIsmConfig, - ProxyFactoryFactoriesAddresses, RemoteRouters, RoutingIsmConfig, SubmissionStrategy, @@ -519,7 +518,6 @@ async function extendWarpRoute( const newDeployedContracts = await executeDeploy( { - // TODO: use EvmERC20WarpModule when it's ready context: params.context, warpDeployConfig: extendedConfigs, }, @@ -549,7 +547,7 @@ async function updateExistingWarpRoute( logBlue('Updating deployed Warp Routes'); const { multiProvider, registry } = params.context; const registryAddresses = - (await registry.getAddresses()) as ChainMap; + (await registry.getAddresses()) as ChainMap; const contractVerifier = new ContractVerifier( multiProvider, apiKeys, @@ -568,14 +566,31 @@ async function updateExistingWarpRoute( `Missing artifacts for ${chain}. Probably new deployment. Skipping update...`, ); + const deployedTokenRoute = deployedConfig.addressOrDenom!; + const { + domainRoutingIsmFactory, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + } = registryAddresses[chain]; + const evmERC20WarpModule = new EvmERC20WarpModule( multiProvider, { config, chain, addresses: { - ...registryAddresses[chain], - deployedTokenRoute: deployedConfig.addressOrDenom!, + deployedTokenRoute, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + domainRoutingIsmFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, }, }, contractVerifier, @@ -660,8 +675,7 @@ async function enrollRemoteRouters( ): Promise { logBlue(`Enrolling deployed routers with each other...`); const { multiProvider, registry } = params.context; - const registryAddresses = - (await registry.getAddresses()) as ChainMap; + const registryAddresses = await registry.getAddresses(); const deployedRoutersAddresses: ChainMap
= objMap( deployedContractsMap, (_, contracts) => getRouter(contracts).address, @@ -678,20 +692,36 @@ async function enrollRemoteRouters( objMap(deployedContractsMap, async (chain, contracts) => { await retryAsync(async () => { const router = getRouter(contracts); // Assume deployedContract always has 1 value - + const deployedTokenRoute = router.address; // Mutate the config.remoteRouters by setting it to all other routers to update const warpRouteReader = new EvmERC20WarpRouteReader( multiProvider, chain, ); const mutatedWarpRouteConfig = - await warpRouteReader.deriveWarpRouteConfig(router.address); + await warpRouteReader.deriveWarpRouteConfig(deployedTokenRoute); + const { + domainRoutingIsmFactory, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + } = registryAddresses[chain]; + const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { config: mutatedWarpRouteConfig, chain, addresses: { - ...registryAddresses[chain], - deployedTokenRoute: router.address, + deployedTokenRoute, + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + domainRoutingIsmFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, }, }); diff --git a/typescript/cli/src/tests/warp-apply.e2e-test.ts b/typescript/cli/src/tests/warp-apply.e2e-test.ts index a97729c82..0703bce76 100644 --- a/typescript/cli/src/tests/warp-apply.e2e-test.ts +++ b/typescript/cli/src/tests/warp-apply.e2e-test.ts @@ -3,9 +3,12 @@ import { Wallet } from 'ethers'; import { ChainAddresses } from '@hyperlane-xyz/registry'; import { + HookType, TokenRouterConfig, TokenType, WarpRouteDeployConfig, + normalizeConfig, + randomAddress, } from '@hyperlane-xyz/sdk'; import { readYamlOrJson, writeYamlOrJson } from '../utils/files.js'; @@ -95,6 +98,45 @@ describe('WarpApply e2e tests', async function () { ); }); + it('should update hook configuration', async () => { + const warpDeployPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; + + // First read the existing config + const warpDeployConfig = await readWarpConfig( + CHAIN_NAME_2, + WARP_CORE_CONFIG_PATH_2, + warpDeployPath, + ); + + // Update with a new hook config + const owner = randomAddress(); + warpDeployConfig[CHAIN_NAME_2].hook = { + type: HookType.PROTOCOL_FEE, + beneficiary: owner, + maxProtocolFee: '1000000', + protocolFee: '100000', + owner, + }; + + // Write the updated config + await writeYamlOrJson(warpDeployPath, warpDeployConfig); + + // Apply the changes + await hyperlaneWarpApply(warpDeployPath, WARP_CORE_CONFIG_PATH_2); + + // Read back the config to verify changes + const updatedConfig = await readWarpConfig( + CHAIN_NAME_2, + WARP_CORE_CONFIG_PATH_2, + warpDeployPath, + ); + + // Verify the hook was updated with all properties + expect(normalizeConfig(updatedConfig[CHAIN_NAME_2].hook)).to.deep.equal( + normalizeConfig(warpDeployConfig[CHAIN_NAME_2].hook), + ); + }); + it('should extend an existing warp route', async () => { // Read existing config into a file const warpConfigPath = `${TEMP_PATH}/warp-route-deployment-2.yaml`; diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index a1e82177a..eddbb1c78 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -104,7 +104,7 @@ export class EvmHookModule extends HyperlaneModule< // Transaction overrides for the chain protected readonly txOverrides: Partial; - protected constructor( + constructor( protected readonly multiProvider: MultiProvider, params: HyperlaneModuleParams< HookConfig, @@ -243,7 +243,7 @@ export class EvmHookModule extends HyperlaneModule< chain: ChainNameOrId; config: HookConfig; proxyFactoryFactories: HyperlaneAddresses; - coreAddresses: CoreAddresses; + coreAddresses: Omit; multiProvider: MultiProvider; contractVerifier?: ContractVerifier; }): Promise { diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index 7ed443eb6..3acfacb88 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -1,6 +1,5 @@ import { SignerWithAddress } from '@nomiclabs/hardhat-ethers/signers.js'; import { expect } from 'chai'; -import { constants } from 'ethers'; import hre from 'hardhat'; import { @@ -12,16 +11,20 @@ import { HypERC4626Collateral__factory, HypNative__factory, Mailbox, + MailboxClient__factory, Mailbox__factory, } from '@hyperlane-xyz/core'; import { EvmIsmModule, + HookConfig, + HookType, HyperlaneAddresses, HyperlaneContractsMap, IsmConfig, IsmType, RouterConfig, TestChainName, + proxyAdmin, serializeContracts, } from '@hyperlane-xyz/sdk'; @@ -29,6 +32,7 @@ import { TestCoreApp } from '../core/TestCoreApp.js'; import { TestCoreDeployer } from '../core/TestCoreDeployer.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; +import { DerivedHookConfig } from '../hook/EvmHookReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { AnnotatedEV5Transaction } from '../providers/ProviderType.js'; @@ -55,7 +59,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const TOKEN_DECIMALS = 18; const chain = TestChainName.test4; let mailbox: Mailbox; - let hookAddress: string; let ismAddress: string; let ismFactory: HyperlaneIsmFactory; let factories: HyperlaneContractsMap; @@ -70,10 +73,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { async function validateCoreValues(deployedToken: GasRouter) { expect(await deployedToken.mailbox()).to.equal(mailbox.address); - expect(await deployedToken.hook()).to.equal(hookAddress); - expect(await deployedToken.interchainSecurityModule()).to.equal( - constants.AddressZero, - ); expect(await deployedToken.owner()).to.equal(signer.address); } @@ -106,7 +105,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { baseConfig = routerConfigMap[chain]; mailbox = Mailbox__factory.connect(baseConfig.mailbox, signer); - hookAddress = await mailbox.defaultHook(); ismAddress = await mailbox.defaultIsm(); }); @@ -115,7 +113,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { ...baseConfig, type: TokenType.collateral, token: token.address, - hook: hookAddress, }; // Deploy using WarpModule @@ -143,7 +140,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config: TokenRouterConfig = { type: TokenType.collateralVault, token: vault.address, - hook: hookAddress, ...baseConfig, }; @@ -175,13 +171,12 @@ describe('EvmERC20WarpHyperlaneModule', async () => { it('should create with a synthetic config', async () => { const config: TokenRouterConfig = { + ...baseConfig, type: TokenType.synthetic, - hook: hookAddress, name: TOKEN_NAME, symbol: TOKEN_NAME, decimals: TOKEN_DECIMALS, totalSupply: TOKEN_SUPPLY, - ...baseConfig, }; // Deploy using WarpModule @@ -213,7 +208,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { it('should create with a native config', async () => { const config = { type: TokenType.native, - hook: hookAddress, ...baseConfig, } as TokenRouterConfig; @@ -244,7 +238,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, remoteRouters: randomRemoteRouters(numOfRouters), } as TokenRouterConfig; @@ -260,28 +253,48 @@ describe('EvmERC20WarpHyperlaneModule', async () => { }); describe('Update', async () => { + const owner = randomAddress(); const ismConfigToUpdate: IsmConfig[] = [ { type: IsmType.TRUSTED_RELAYER, - relayer: randomAddress(), + relayer: owner, }, { type: IsmType.FALLBACK_ROUTING, - owner: randomAddress(), + owner: owner, domains: {}, }, { type: IsmType.PAUSABLE, - owner: randomAddress(), + owner: owner, paused: false, }, ]; + const hookConfigToUpdate: HookConfig[] = [ + { + type: HookType.PROTOCOL_FEE, + beneficiary: owner, + owner: owner, + maxProtocolFee: '1337', + protocolFee: '1337', + }, + { + type: HookType.INTERCHAIN_GAS_PAYMASTER, + owner: owner, + beneficiary: owner, + oracleKey: owner, + overhead: {}, + oracleConfig: {}, + }, + { + type: HookType.MERKLE_TREE, + }, + ]; it('should deploy and set a new Ism', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, interchainSecurityModule: ismAddress, } as TokenRouterConfig; @@ -297,7 +310,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { for (const interchainSecurityModule of ismConfigToUpdate) { const expectedConfig: TokenRouterConfig = { ...actualConfig, - interchainSecurityModule, }; await sendTxs(await evmERC20WarpModule.update(expectedConfig)); @@ -313,7 +325,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, interchainSecurityModule: ismAddress, } as TokenRouterConfig; @@ -351,6 +362,99 @@ describe('EvmERC20WarpHyperlaneModule', async () => { expect(txs.length).to.equal(0); }); + it('should update and set a new Hook based on config', async () => { + const config = { + ...baseConfig, + type: TokenType.native, + } as TokenRouterConfig; + + // Deploy using WarpModule + const evmERC20WarpModule = await EvmERC20WarpModule.create({ + chain, + config, + multiProvider, + proxyFactoryFactories: ismFactoryAddresses, + }); + const actualConfig = await evmERC20WarpModule.read(); + + for (const hook of hookConfigToUpdate) { + const expectedConfig: TokenRouterConfig = { + ...actualConfig, + hook, + }; + await sendTxs(await evmERC20WarpModule.update(expectedConfig)); + + const updatedConfig = (await evmERC20WarpModule.read()) + .hook as DerivedHookConfig; + expect(normalizeConfig(updatedConfig)).to.deep.equal(hook); + } + }); + + it('should set new deployed hook mailbox to WarpConfig.owner', async () => { + const config = { + ...baseConfig, + type: TokenType.native, + } as TokenRouterConfig; + + // Deploy using WarpModule + const evmERC20WarpModule = await EvmERC20WarpModule.create({ + chain, + config, + multiProvider, + proxyFactoryFactories: ismFactoryAddresses, + }); + const actualConfig = await evmERC20WarpModule.read(); + const expectedConfig: TokenRouterConfig = { + ...actualConfig, + hook: hookConfigToUpdate.find( + (c: any) => c.type === HookType.MERKLE_TREE, + ), + }; + await sendTxs(await evmERC20WarpModule.update(expectedConfig)); + + const updatedConfig = (await evmERC20WarpModule.read()) + .hook as DerivedHookConfig; + + const hook = MailboxClient__factory.connect( + updatedConfig.address, + multiProvider.getProvider(chain), + ); + expect(await hook.mailbox()).to.equal(expectedConfig.mailbox); + }); + + it("should set Proxied Hook's proxyAdmins to WarpConfig.proxyAdmin", async () => { + const config = { + ...baseConfig, + type: TokenType.native, + } as TokenRouterConfig; + + // Deploy using WarpModule + const evmERC20WarpModule = await EvmERC20WarpModule.create({ + chain, + config, + multiProvider, + proxyFactoryFactories: ismFactoryAddresses, + }); + const actualConfig = await evmERC20WarpModule.read(); + const expectedConfig: TokenRouterConfig = { + ...actualConfig, + hook: hookConfigToUpdate.find( + (c: any) => c.type === HookType.INTERCHAIN_GAS_PAYMASTER, + ), + }; + await sendTxs(await evmERC20WarpModule.update(expectedConfig)); + + const updatedConfig = (await evmERC20WarpModule.read()) + .hook as DerivedHookConfig; + + expect( + await proxyAdmin( + multiProvider.getProvider(chain), + updatedConfig.address, + ), + ).to.equal(expectedConfig.proxyAdmin?.address); + }); + it('should update a mutable Ism', async () => { const ismConfig: IsmConfig = { type: IsmType.ROUTING, @@ -372,7 +476,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, interchainSecurityModule: deployedIsm, } as TokenRouterConfig; @@ -409,7 +512,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, ismFactoryAddresses, } as TokenRouterConfig; @@ -441,7 +543,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, ismFactoryAddresses, } as TokenRouterConfig; @@ -494,7 +595,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config = { ...baseConfig, type: TokenType.native, - hook: hookAddress, ismFactoryAddresses, } as TokenRouterConfig; @@ -535,7 +635,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config: TokenRouterConfig = { ...baseConfig, type: TokenType.native, - hook: hookAddress, }; const owner = signer.address.toLowerCase(); @@ -579,7 +678,6 @@ describe('EvmERC20WarpHyperlaneModule', async () => { const config: TokenRouterConfig = { ...baseConfig, type: TokenType.native, - hook: hookAddress, remoteRouters: { [domain]: randomAddress(), }, diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts index 729842fca..7d1b84a51 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -19,6 +19,7 @@ import { addressToBytes32, assert, deepEquals, + eqAddress, isObjEmpty, objMap, rootLogger, @@ -31,6 +32,8 @@ import { } from '../core/AbstractHyperlaneModule.js'; import { ProxyFactoryFactories } from '../deploy/contracts.js'; import { proxyAdminUpdateTxs } from '../deploy/proxy.js'; +import { EvmHookModule } from '../hook/EvmHookModule.js'; +import { DerivedHookConfig } from '../hook/EvmHookReader.js'; import { EvmIsmModule } from '../ism/EvmIsmModule.js'; import { DerivedIsmConfig } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; @@ -42,12 +45,13 @@ import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; import { HypERC20Deployer } from './deploy.js'; import { TokenRouterConfig, TokenRouterConfigSchema } from './schemas.js'; +type WarpRouteAddresses = HyperlaneAddresses & { + deployedTokenRoute: Address; +}; export class EvmERC20WarpModule extends HyperlaneModule< ProtocolType.Ethereum, TokenRouterConfig, - HyperlaneAddresses & { - deployedTokenRoute: Address; - } + WarpRouteAddresses > { protected logger = rootLogger.child({ module: 'EvmERC20WarpModule', @@ -59,12 +63,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< constructor( protected readonly multiProvider: MultiProvider, - args: HyperlaneModuleParams< - TokenRouterConfig, - HyperlaneAddresses & { - deployedTokenRoute: Address; - } - >, + args: HyperlaneModuleParams, protected readonly contractVerifier?: ContractVerifier, ) { super(args); @@ -87,7 +86,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @param address - The address to derive the token router configuration from. * @returns A promise that resolves to the token router configuration. */ - public async read(): Promise { + async read(): Promise { return this.reader.deriveWarpRouteConfig( this.args.addresses.deployedTokenRoute, ); @@ -99,7 +98,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @param expectedConfig - The configuration for the token router to be updated. * @returns An array of Ethereum transactions that were executed to update the contract, or an error if the update failed. */ - public async update( + async update( expectedConfig: TokenRouterConfig, ): Promise { TokenRouterConfigSchema.parse(expectedConfig); @@ -115,6 +114,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< */ transactions.push( ...(await this.createIsmUpdateTxs(actualConfig, expectedConfig)), + ...(await this.createHookUpdateTxs(actualConfig, expectedConfig)), ...this.createRemoteRoutersUpdateTxs(actualConfig, expectedConfig), ...this.createSetDestinationGasUpdateTxs(actualConfig, expectedConfig), ...this.createOwnershipUpdateTxs(actualConfig, expectedConfig), @@ -280,6 +280,47 @@ export class EvmERC20WarpModule extends HyperlaneModule< return updateTransactions; } + async createHookUpdateTxs( + actualConfig: TokenRouterConfig, + expectedConfig: TokenRouterConfig, + ): Promise { + const updateTransactions: AnnotatedEV5Transaction[] = []; + + if (!expectedConfig.hook) { + return []; + } + + const actualDeployedHook = (actualConfig.hook as DerivedHookConfig) + ?.address; + + // Try to deploy or update Hook with the expected config + const { + deployedHook: expectedDeployedHook, + updateTransactions: hookUpdateTransactions, + } = await this.deployOrUpdateHook(actualConfig, expectedConfig); + + // If a Hook is updated in-place, push the update txs + updateTransactions.push(...hookUpdateTransactions); + + // If a new Hook is deployed, push the setHook tx + if (!eqAddress(actualDeployedHook, expectedDeployedHook)) { + const contractToUpdate = MailboxClient__factory.connect( + this.args.addresses.deployedTokenRoute, + this.multiProvider.getProvider(this.domainId), + ); + updateTransactions.push({ + chainId: this.chainId, + annotation: `Setting Hook for Warp Route to ${expectedDeployedHook}`, + to: contractToUpdate.address, + data: contractToUpdate.interface.encodeFunctionData('setHook', [ + expectedDeployedHook, + ]), + }); + } + + return updateTransactions; + } + /** * Transfer ownership of an existing Warp route with a given config. * @@ -305,17 +346,14 @@ export class EvmERC20WarpModule extends HyperlaneModule< * * @returns Object with deployedIsm address, and update Transactions */ - public async deployOrUpdateIsm( + async deployOrUpdateIsm( actualConfig: TokenRouterConfig, expectedConfig: TokenRouterConfig, ): Promise<{ deployedIsm: Address; updateTransactions: AnnotatedEV5Transaction[]; }> { - assert( - expectedConfig.interchainSecurityModule, - 'Ism not derived correctly', - ); + assert(expectedConfig.interchainSecurityModule, 'Ism derived incorrectly'); const ismModule = new EvmIsmModule( this.multiProvider, @@ -343,6 +381,124 @@ export class EvmERC20WarpModule extends HyperlaneModule< return { deployedIsm, updateTransactions }; } + /** + * Updates or deploys the hook using the provided configuration. + * + * @returns Object with deployedHook address, and update Transactions + */ + async deployOrUpdateHook( + actualConfig: TokenRouterConfig, + expectedConfig: TokenRouterConfig, + ): Promise<{ + deployedHook: Address; + updateTransactions: AnnotatedEV5Transaction[]; + }> { + assert(expectedConfig.hook, 'No hook config'); + + if (!actualConfig.hook) { + return this.deployNewHook(expectedConfig); + } + + return this.updateExistingHook(expectedConfig, actualConfig); + } + + async deployNewHook(expectedConfig: TokenRouterConfig): Promise<{ + deployedHook: Address; + updateTransactions: AnnotatedEV5Transaction[]; + }> { + this.logger.info( + `No hook deployed for warp route, deploying new hook on ${this.args.chain} chain`, + ); + + const { + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + domainRoutingIsmFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + } = this.args.addresses; + + assert(expectedConfig.hook, 'Hook is undefined'); + assert( + expectedConfig.proxyAdmin?.address, + 'ProxyAdmin address is undefined', + ); + + const hookModule = await EvmHookModule.create({ + chain: this.args.chain, + config: expectedConfig.hook, + proxyFactoryFactories: { + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + domainRoutingIsmFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + }, + coreAddresses: { + mailbox: expectedConfig.mailbox, + proxyAdmin: expectedConfig.proxyAdmin?.address, // Assume that a proxyAdmin is always deployed with a WarpRoute + }, + contractVerifier: this.contractVerifier, + multiProvider: this.multiProvider, + }); + const { deployedHook } = hookModule.serialize(); + return { deployedHook, updateTransactions: [] }; + } + + async updateExistingHook( + expectedConfig: TokenRouterConfig, + actualConfig: TokenRouterConfig, + ): Promise<{ + deployedHook: Address; + updateTransactions: AnnotatedEV5Transaction[]; + }> { + const { + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + domainRoutingIsmFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + } = this.args.addresses; + + assert(actualConfig.proxyAdmin?.address, 'ProxyAdmin address is undefined'); + assert(actualConfig.hook, 'Hook is undefined'); + + const hookModule = new EvmHookModule( + this.multiProvider, + { + chain: this.args.chain, + config: actualConfig.hook, + addresses: { + staticMerkleRootMultisigIsmFactory, + staticMessageIdMultisigIsmFactory, + staticAggregationIsmFactory, + staticAggregationHookFactory, + domainRoutingIsmFactory, + staticMerkleRootWeightedMultisigIsmFactory, + staticMessageIdWeightedMultisigIsmFactory, + mailbox: actualConfig.mailbox, + proxyAdmin: actualConfig.proxyAdmin?.address, + deployedHook: (actualConfig.hook as DerivedHookConfig).address, + }, + }, + this.contractVerifier, + ); + + this.logger.info( + `Comparing target Hook config with ${this.args.chain} chain`, + ); + const updateTransactions = await hookModule.update(expectedConfig.hook!); + const { deployedHook } = hookModule.serialize(); + + return { deployedHook, updateTransactions }; + } + /** * Deploys the Warp Route. * @@ -351,7 +507,7 @@ export class EvmERC20WarpModule extends HyperlaneModule< * @param multiProvider - The multi-provider instance to use. * @returns A new instance of the EvmERC20WarpHyperlaneModule. */ - public static async create(params: { + static async create(params: { chain: ChainNameOrId; config: TokenRouterConfig; multiProvider: MultiProvider;