From 708999433443eff675370374285b0f9bcac014c0 Mon Sep 17 00:00:00 2001 From: Lee <6251863+ltyu@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:59:50 -0400 Subject: [PATCH] feat(cli): Add hyperlane warp apply (#4094) ### Description - Adds `hyperlane warp apply` ### Related issues - Fixes https://github.com/hyperlane-xyz/issues/issues/1190 ### Backward compatibility Yes ### Testing Manual To test: `yarn hyperlane warp apply --warp` --------- Co-authored-by: pbio <10051819+paulbalaji@users.noreply.github.com> --- .changeset/proud-days-flash.md | 6 ++ typescript/cli/src/commands/options.ts | 4 +- typescript/cli/src/commands/warp.ts | 44 ++++++++- typescript/cli/src/deploy/warp.ts | 96 ++++++++++++++++--- typescript/sdk/src/deploy/schemas.ts | 4 + typescript/sdk/src/index.ts | 1 + .../sdk/src/ism/EvmIsmModule.hardhat-test.ts | 46 ++------- typescript/sdk/src/ism/EvmIsmModule.ts | 2 +- .../token/EvmERC20WarpModule.hardhat-test.ts | 2 +- .../sdk/src/token/EvmERC20WarpModule.ts | 3 + 10 files changed, 151 insertions(+), 57 deletions(-) create mode 100644 .changeset/proud-days-flash.md diff --git a/.changeset/proud-days-flash.md b/.changeset/proud-days-flash.md new file mode 100644 index 0000000000..99f1759813 --- /dev/null +++ b/.changeset/proud-days-flash.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Adds hyperlane warp apply diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 6d2bb46d03..00124952e2 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -89,13 +89,13 @@ export const warpDeploymentConfigCommandOption: Options = { description: 'A path to a JSON or YAML file with a warp route deployment config.', default: './configs/warp-route-deployment.yaml', - alias: 'w', + alias: 'wd', }; export const warpCoreConfigCommandOption: Options = { type: 'string', description: 'File path to Warp Route config', - alias: 'w', + alias: 'wc', }; export const agentConfigCommandOption = ( diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index 7a1367779e..c8bb7871e6 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -18,13 +18,14 @@ import { objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { createWarpRouteDeployConfig, readWarpCoreConfig, + readWarpRouteDeployConfig, } from '../config/warp.js'; import { CommandModuleWithContext, CommandModuleWithWriteContext, } from '../context/types.js'; import { evaluateIfDryRunFailure } from '../deploy/dry-run.js'; -import { runWarpRouteDeploy } from '../deploy/warp.js'; +import { runWarpRouteApply, runWarpRouteDeploy } from '../deploy/warp.js'; import { log, logGray, logGreen, logRed, logTable } from '../logger.js'; import { sendTestTransfer } from '../send/transfer.js'; import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js'; @@ -50,6 +51,7 @@ export const warpCommand: CommandModule = { describe: 'Manage Hyperlane warp routes', builder: (yargs) => yargs + .command(apply) .command(deploy) .command(init) .command(read) @@ -60,6 +62,46 @@ export const warpCommand: CommandModule = { handler: () => log('Command required'), }; +export const apply: CommandModuleWithWriteContext<{ + config: string; + symbol?: string; + warp: string; +}> = { + command: 'apply', + describe: 'Update Warp Route contracts', + builder: { + config: warpDeploymentConfigCommandOption, + symbol: { + ...symbolCommandOption, + demandOption: false, + }, + warp: { + ...warpCoreConfigCommandOption, + demandOption: false, + }, + }, + handler: async ({ context, config, symbol, warp }) => { + logGray(`Hyperlane Warp Apply`); + logGray('--------------------'); // @TODO consider creating a helper function for these dashes + let warpCoreConfig: WarpCoreConfig; + if (symbol) { + warpCoreConfig = await selectRegistryWarpRoute(context.registry, symbol); + } else if (warp) { + warpCoreConfig = readWarpCoreConfig(warp); + } else { + logRed(`Please specify either a symbol or warp config`); + process.exit(0); + } + const warpDeployConfig = await readWarpRouteDeployConfig(config); + await runWarpRouteApply({ + context, + warpDeployConfig, + warpCoreConfig, + }); + process.exit(0); + }, +}; + export const deploy: CommandModuleWithWriteContext<{ config: string; 'dry-run': string; diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 1b6e1878a7..d76453cd09 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -3,6 +3,7 @@ import { stringify as yamlStringify } from 'yaml'; import { IRegistry } from '@hyperlane-xyz/registry'; import { + EvmERC20WarpModule, EvmIsmModule, HypERC20Deployer, HypERC721Deployer, @@ -10,6 +11,7 @@ import { HyperlaneContractsMap, HyperlaneProxyFactoryDeployer, MultiProvider, + ProxyFactoryFactoriesAddresses, TOKEN_TYPE_TO_STANDARD, TokenFactories, TokenType, @@ -29,7 +31,14 @@ import { import { readWarpRouteDeployConfig } from '../config/warp.js'; import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js'; import { WriteCommandContext } from '../context/types.js'; -import { log, logBlue, logGray, logGreen, logTable } from '../logger.js'; +import { + log, + logBlue, + logGray, + logGreen, + logRed, + logTable, +} from '../logger.js'; import { indentYamlOrJson, isFile, @@ -44,7 +53,11 @@ import { interface DeployParams { context: WriteCommandContext; - configMap: WarpRouteDeployConfig; + warpDeployConfig: WarpRouteDeployConfig; +} + +interface ApplyParams extends DeployParams { + warpCoreConfig: WarpCoreConfig; } export async function runWarpRouteDeploy({ @@ -79,7 +92,7 @@ export async function runWarpRouteDeploy({ const deploymentParams = { context, - configMap: warpRouteConfig, + warpDeployConfig: warpRouteConfig, }; logBlue('Warp route deployment plan'); @@ -102,13 +115,13 @@ export async function runWarpRouteDeploy({ await completeDeploy(context, 'warp', initialBalances, userAddress, chains); } -async function runDeployPlanStep({ context, configMap }: DeployParams) { +async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { const { skipConfirmation } = context; logBlue('\nDeployment plan'); logGray('==============='); - log(`Using token standard ${configMap.isNft ? 'ERC721' : 'ERC20'}`); - logTable(configMap); + log(`Using token standard ${warpDeployConfig.isNft ? 'ERC721' : 'ERC20'}`); + logTable(warpDeployConfig); if (skipConfirmation || context.isDryRun) return; @@ -122,18 +135,18 @@ async function executeDeploy(params: DeployParams) { logBlue('All systems ready, captain! Beginning deployment...'); const { - configMap, + warpDeployConfig, context: { registry, multiProvider, isDryRun, dryRunChain }, } = params; - const deployer = configMap.isNft + const deployer = warpDeployConfig.isNft ? new HypERC721Deployer(multiProvider) : new HypERC20Deployer(multiProvider); const config: WarpRouteDeployConfig = isDryRun && dryRunChain - ? { [dryRunChain]: configMap[dryRunChain] } - : configMap; + ? { [dryRunChain]: warpDeployConfig[dryRunChain] } + : warpDeployConfig; const ismFactoryDeployer = new HyperlaneProxyFactoryDeployer(multiProvider); @@ -256,7 +269,7 @@ async function createWarpIsm( } async function getWarpCoreConfig( - { configMap, context }: DeployParams, + { warpDeployConfig, context }: DeployParams, contracts: HyperlaneContractsMap, ): Promise { const warpCoreConfig: WarpCoreConfig = { tokens: [] }; @@ -264,7 +277,7 @@ async function getWarpCoreConfig( // TODO: replace with warp read const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata( context.multiProvider, - configMap, + warpDeployConfig, ); assert( tokenMetadata && isTokenMetadata(tokenMetadata), @@ -275,7 +288,7 @@ async function getWarpCoreConfig( // First pass, create token configs for (const [chainName, contract] of Object.entries(contracts)) { - const config = configMap[chainName]; + const config = warpDeployConfig[chainName]; const collateralAddressOrDenom = config.type === TokenType.collateral ? config.token : undefined; warpCoreConfig.tokens.push({ @@ -285,7 +298,8 @@ async function getWarpCoreConfig( symbol, name, addressOrDenom: - contract[configMap[chainName].type as keyof TokenFactories].address, + contract[warpDeployConfig[chainName].type as keyof TokenFactories] + .address, collateralAddressOrDenom, }); } @@ -313,3 +327,57 @@ async function getWarpCoreConfig( return warpCoreConfig; } + +export async function runWarpRouteApply(params: ApplyParams) { + const { + warpDeployConfig, + warpCoreConfig, + context: { registry, multiProvider }, + } = params; + + // Addresses used to get static Ism factories + const addresses = await registry.getAddresses(); + + // Convert warpCoreConfig.tokens[] into a mapping of { [chainName]: Config } + // This allows O(1) reads within the loop + const warpCoreByChain = Object.fromEntries( + warpCoreConfig.tokens.map((token) => [token.chainName, token]), + ); + + // Attempt to update Warp Routes + // Can update existing or deploy new contracts + logGray(`Comparing target and onchain Warp configs`); + await promiseObjAll( + objMap(warpDeployConfig, async (chain, config) => { + try { + // Update Warp + config.ismFactoryAddresses = addresses[ + chain + ] as ProxyFactoryFactoriesAddresses; + const evmERC20WarpModule = new EvmERC20WarpModule(multiProvider, { + config, + chain, + addresses: { + deployedTokenRoute: warpCoreByChain[chain].addressOrDenom!, + }, + }); + const transactions = await evmERC20WarpModule.update(config); + + // Send Txs + if (transactions.length) { + for (const transaction of transactions) { + await multiProvider.sendTransaction(chain, transaction); + } + + logGreen(`Warp config updated on ${chain}.`); + } else { + logGreen( + `Warp config on ${chain} is the same as target. No updates needed.`, + ); + } + } catch (e) { + logRed(`Warp config on ${chain} failed to update.`, e); + } + }), + ); +} diff --git a/typescript/sdk/src/deploy/schemas.ts b/typescript/sdk/src/deploy/schemas.ts index 1f0f6c9798..8b50d46b1b 100644 --- a/typescript/sdk/src/deploy/schemas.ts +++ b/typescript/sdk/src/deploy/schemas.ts @@ -13,3 +13,7 @@ export const ProxyFactoryFactoriesSchema = z.object({ staticAggregationHookFactory: z.string(), domainRoutingIsmFactory: z.string(), }); + +export type ProxyFactoryFactoriesAddresses = z.infer< + typeof ProxyFactoryFactoriesSchema +>; diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 6e679b79bc..5e6de6201e 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -508,3 +508,4 @@ export { canProposeSafeTransactions, getSafe, getSafeDelegates, getSafeService } export { EvmCoreModule, DeployedCoreAdresses } from './core/EvmCoreModule.js'; export { EvmERC20WarpModule } from './token/EvmERC20WarpModule.js'; export { EvmIsmModule } from './ism/EvmIsmModule.js'; +export { ProxyFactoryFactoriesAddresses } from './deploy/schemas.js'; diff --git a/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts b/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts index 508891a58f..54ccbf040c 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts @@ -4,7 +4,6 @@ import { expect } from 'chai'; import { Signer } from 'ethers'; import hre from 'hardhat'; -import { FallbackDomainRoutingHook__factory } from '@hyperlane-xyz/core'; import { Address, eqAddress, normalizeConfig } from '@hyperlane-xyz/utils'; import { TestChainName, testChains } from '../consts/testChains.js'; @@ -93,7 +92,6 @@ describe('EvmIsmModule', async () => { let multiProvider: MultiProvider; let exampleRoutingConfig: RoutingIsmConfig; let mailboxAddress: Address; - let newMailboxAddress: Address; let fundingAccount: Signer; const chain = TestChainName.test4; @@ -128,11 +126,6 @@ describe('EvmIsmModule', async () => { await new TestCoreDeployer(multiProvider, legacyIsmFactory).deployApp() ).getContracts(chain).mailbox.address; - // new mailbox - newMailboxAddress = ( - await new TestCoreDeployer(multiProvider, legacyIsmFactory).deployApp() - ).getContracts(chain).mailbox.address; - // example routing config exampleRoutingConfig = { type: IsmType.ROUTING, @@ -242,15 +235,17 @@ describe('EvmIsmModule', async () => { // create a new ISM const { ism } = await createIsm(exampleRoutingConfig); - // add config for a domain the multiprovider doesn't have - exampleRoutingConfig.domains['test5'] = { - type: IsmType.MESSAGE_ID_MULTISIG, - threshold: 1, - validators: [randomAddress()], + // create an updated config with a domain the multiprovider doesn't have + const updatedRoutingConfig: IsmConfig = { + ...exampleRoutingConfig, + domains: { + ...exampleRoutingConfig.domains, + test5: randomMultisigIsmConfig(3, 5), + }, }; // expect 0 txs, as adding test5 domain is no-op - await expectTxsAndUpdate(ism, exampleRoutingConfig, 0); + await expectTxsAndUpdate(ism, updatedRoutingConfig, 0); }); it(`update route in an existing ${type}`, async () => { @@ -435,30 +430,5 @@ describe('EvmIsmModule', async () => { .true; }); } - - it(`redeploy same config if the mailbox address changes for defaultFallbackRoutingIsm`, async () => { - exampleRoutingConfig.type = IsmType.FALLBACK_ROUTING; - - // create a new ISM - const { ism, initialIsmAddress } = await createIsm(exampleRoutingConfig); - - // point to new mailbox - ism.setNewMailbox(newMailboxAddress); - - // expect a new ISM to be deployed, so no in-place updates to return - await expectTxsAndUpdate(ism, exampleRoutingConfig, 0); - - // expect the ISM address to be different - expect(eqAddress(initialIsmAddress, ism.serialize().deployedIsm)).to.be - .false; - - // expect that the ISM is configured with the new mailbox - const onchainIsm = FallbackDomainRoutingHook__factory.connect( - ism.serialize().deployedIsm, - multiProvider.getSigner(chain), - ); - const onchainMailbox = await onchainIsm['mailbox()'](); - expect(eqAddress(onchainMailbox, newMailboxAddress)).to.be.true; - }); }); }); diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts index d57d703bd8..ab492fd68e 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -132,9 +132,9 @@ export class EvmIsmModule extends HyperlaneModule< // save current config for comparison // normalize the config to ensure it's in a consistent format for comparison const currentConfig = normalizeConfig(await this.read()); - // Update the config this.args.config = targetConfig; + targetConfig = normalizeConfig(targetConfig); // moduleMatchesConfig expects any domain filtering to have been done already if ( diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index 214065be9b..24ab25f004 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -356,7 +356,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { type: IsmType.ROUTING, owner: randomAddress(), domains: { - '2': ismAddress, + test2: { type: IsmType.TEST_ISM }, }, }, }; diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.ts b/typescript/sdk/src/token/EvmERC20WarpModule.ts index 1944410ff3..77c71f2842 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.ts @@ -153,6 +153,9 @@ export class EvmERC20WarpModule extends HyperlaneModule< .address, }, }); + this.logger.info( + `Comparing target ISM config with ${this.args.chain} chain`, + ); const updateTransactions = await ismModule.update( expectedConfig.interchainSecurityModule, );