From 7e620c9dfa5a8ca1a8130e04841a6d262f2e14fd Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Fri, 1 Dec 2023 15:32:26 -0500 Subject: [PATCH] feat:configure hooks in the CLI (#2964) ### Description - enable configuring hooks in the CLI (merkle, igp, protocolFee, aggregation, routing) - use preset by default without prompting ### Drive-by changes ### Related issues ### Backward compatibility ### Testing --- .changeset/odd-keys-pretend.md | 7 + typescript/cli/examples/hook-config.yaml | 18 - typescript/cli/examples/hooks.yaml | 66 ++++ typescript/cli/src/commands/config.ts | 8 +- typescript/cli/src/config/hooks.ts | 372 +++++++++++++----- typescript/cli/src/config/ism.ts | 32 +- typescript/cli/src/deploy/core.ts | 68 ++-- typescript/cli/src/tests/hooks.test.ts | 92 +++++ .../cli/src/tests/hooks/safe-parse-fail.yaml | 44 +++ .../config/environments/mainnet3/core.ts | 4 +- .../infra/config/environments/test/core.ts | 4 +- .../config/environments/testnet4/core.ts | 4 +- typescript/sdk/src/hook/types.ts | 11 +- typescript/sdk/src/index.ts | 1 + typescript/sdk/src/test/testUtils.ts | 4 +- 15 files changed, 539 insertions(+), 196 deletions(-) create mode 100644 .changeset/odd-keys-pretend.md delete mode 100644 typescript/cli/examples/hook-config.yaml create mode 100644 typescript/cli/examples/hooks.yaml create mode 100644 typescript/cli/src/tests/hooks.test.ts create mode 100644 typescript/cli/src/tests/hooks/safe-parse-fail.yaml diff --git a/.changeset/odd-keys-pretend.md b/.changeset/odd-keys-pretend.md new file mode 100644 index 0000000000..9d4233bf8d --- /dev/null +++ b/.changeset/odd-keys-pretend.md @@ -0,0 +1,7 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/infra': patch +'@hyperlane-xyz/sdk': patch +--- + +Allow CLI to accept hook as a config diff --git a/typescript/cli/examples/hook-config.yaml b/typescript/cli/examples/hook-config.yaml deleted file mode 100644 index 1b9b9e0936..0000000000 --- a/typescript/cli/examples/hook-config.yaml +++ /dev/null @@ -1,18 +0,0 @@ -anvil1: - required: - type: protocolFee - maxProtocolFee: '10000000000000000' - protocolFee: '10000000000' - beneficiary: '0xb1b4e269dD0D19d9D49f3a95bF6c2c15f13E7943' - owner: '0xb1b4e269dD0D19d9D49f3a95bF6c2c15f13E7943' - default: - type: merkleTreeHook -anvil2: - required: - type: protocolFee - maxProtocolFee: '10000000000000000' - protocolFee: '10000000000' - beneficiary: '0xb1b4e269dD0D19d9D49f3a95bF6c2c15f13E7943' - owner: '0xb1b4e269dD0D19d9D49f3a95bF6c2c15f13E7943' - default: - type: merkleTreeHook diff --git a/typescript/cli/examples/hooks.yaml b/typescript/cli/examples/hooks.yaml new file mode 100644 index 0000000000..9fc19433ab --- /dev/null +++ b/typescript/cli/examples/hooks.yaml @@ -0,0 +1,66 @@ +# A config to define the hooks for core contract deployments +# Ideally, use the `hyperlane config create hooks` command to generate this file +# but you we can refer to https://github.com/hyperlane-xyz/hyperlane-monorepo/blob/main/typescript/sdk/src/hook/types.ts for the matching types + +# HooksConfig: +# required: HookConfig +# default: HookConfig + +# HookConfig: +# type: HookType +# ... hook-specific config + +# HookType: +# - merkleTreeHook +# - domainRoutingHook +# - interchainGasPaymaster +# - protocolFee +# - aggregationHook +# - opStack (not yet supported) + +anvil1: + required: + type: protocolFee + maxProtocolFee: '1000000000000000000' # in wei (string) + protocolFee: '200000000000000' # in wei (string) + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + default: + type: domainRoutingHook + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + domains: + anvil2: + type: aggregationHook + hooks: + - type: merkleTreeHook + - type: interchainGasPaymaster + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + overhead: + anvil2: 50000 # gas amount (number) + gasOracleType: + anvil2: StorageGasOracle +anvil2: + required: + type: protocolFee + maxProtocolFee: '1000000000000000000' + protocolFee: '200000000000000' + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + default: + type: domainRoutingHook + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + domains: + anvil1: + type: aggregationHook + hooks: + - type: merkleTreeHook + - type: interchainGasPaymaster + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + overhead: + anvil1: 50000 + gasOracleType: + anvil1: StorageGasOracle diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index 64bfec5b1b..74b3c45646 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -2,7 +2,7 @@ import { CommandModule } from 'yargs'; import { log, logGreen } from '../../logger.js'; import { createChainConfig, readChainConfigs } from '../config/chain.js'; -import { createHookConfig } from '../config/hooks.js'; +import { createHooksConfigMap } from '../config/hooks.js'; import { createIsmConfigMap, readIsmConfig } from '../config/ism.js'; import { createMultisigConfig, @@ -96,8 +96,8 @@ const createIsmConfigCommand: CommandModule = { }; const createHookConfigCommand: CommandModule = { - command: 'hook', - describe: 'Create a new Hook config', + command: 'hooks', + describe: 'Create a new hooks config (required & default)', builder: (yargs) => yargs.options({ output: outputFileOption('./configs/hooks.yaml'), @@ -108,7 +108,7 @@ const createHookConfigCommand: CommandModule = { const format: FileFormat = argv.format; const outPath: string = argv.output; const chainConfigPath: string = argv.chains; - await createHookConfig({ format, outPath, chainConfigPath }); + await createHooksConfigMap({ format, outPath, chainConfigPath }); process.exit(0); }, }; diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index 4449e1948f..62f66e0fa6 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -1,5 +1,5 @@ import { confirm, input, select } from '@inquirer/prompts'; -import { BigNumber } from 'bignumber.js'; +import { BigNumber as BigNumberJs } from 'bignumber.js'; import { ethers } from 'ethers'; import { z } from 'zod'; @@ -7,12 +7,10 @@ import { ChainMap, ChainName, GasOracleContractType, - HookConfig, HookType, - IgpHookConfig, - MerkleTreeHookConfig, - MultisigIsmConfig, - ProtocolFeeHookConfig, + HooksConfig, + MultisigConfig, + chainMetadata, defaultMultisigConfigs, multisigIsmVerificationCost, } from '@hyperlane-xyz/sdk'; @@ -41,25 +39,56 @@ const MerkleTreeSchema = z.object({ type: z.literal(HookType.MERKLE_TREE), }); -const HookSchema = z.union([ProtocolFeeSchema, MerkleTreeSchema]); +const IGPSchema = z.object({ + type: z.literal(HookType.INTERCHAIN_GAS_PAYMASTER), + owner: z.string(), + beneficiary: z.string(), + overhead: z.record(z.number()), + gasOracleType: z.record(z.literal(GasOracleContractType.StorageGasOracle)), + oracleKey: z.string(), +}); + +const RoutingConfigSchema: z.ZodSchema = z.lazy(() => + z.object({ + type: z.literal(HookType.ROUTING), + owner: z.string(), + domains: z.record(HookConfigSchema), + }), +); + +const AggregationConfigSchema: z.ZodSchema = z.lazy(() => + z.object({ + type: z.literal(HookType.AGGREGATION), + hooks: z.array(HookConfigSchema), + }), +); + +const HookConfigSchema = z.union([ + ProtocolFeeSchema, + MerkleTreeSchema, + IGPSchema, + RoutingConfigSchema, + AggregationConfigSchema, +]); +export type HookConfig = z.infer; -const ConfigSchema = z.object({ - required: HookSchema, - default: HookSchema, +const HooksConfigSchema = z.object({ + required: HookConfigSchema, + default: HookConfigSchema, }); -const HookConfigMapSchema = z.object({}).catchall(ConfigSchema); -export type HookConfigMap = z.infer; +const HooksConfigMapSchema = z.record(HooksConfigSchema); +export type HooksConfigMap = z.infer; export function isValidHookConfigMap(config: any) { - return HookConfigMapSchema.safeParse(config).success; + return HooksConfigMapSchema.safeParse(config).success; } export function presetHookConfigs( owner: Address, local: ChainName, destinationChains: ChainName[], - ismConfig?: MultisigIsmConfig, -) { + multisigConfig?: MultisigConfig, +): HooksConfig { const gasOracleType = destinationChains.reduce< ChainMap >((acc, chain) => { @@ -69,14 +98,17 @@ export function presetHookConfigs( const overhead = destinationChains.reduce>((acc, chain) => { let validatorThreshold: number; let validatorCount: number; - if (ismConfig) { - validatorThreshold = ismConfig.threshold; - validatorCount = ismConfig.validators.length; + if (multisigConfig) { + validatorThreshold = multisigConfig.threshold; + validatorCount = multisigConfig.validators.length; } else if (local in defaultMultisigConfigs) { validatorThreshold = defaultMultisigConfigs[local].threshold; validatorCount = defaultMultisigConfigs[local].validators.length; } else { - throw new Error('Cannot estimate gas overhead for IGP hook'); + // default values + // fix here: https://github.com/hyperlane-xyz/issues/issues/773 + validatorThreshold = 2; + validatorCount = 3; } acc[chain] = multisigIsmVerificationCost( validatorThreshold, @@ -89,17 +121,17 @@ export function presetHookConfigs( return { required: { type: HookType.PROTOCOL_FEE, - maxProtocolFee: ethers.utils.parseUnits('1', 'gwei'), - protocolFee: ethers.utils.parseUnits('0', 'wei'), + maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), + protocolFee: ethers.utils.parseUnits('0', 'wei').toString(), beneficiary: owner, owner: owner, - } as ProtocolFeeHookConfig, + }, default: { type: HookType.AGGREGATION, hooks: [ { type: HookType.MERKLE_TREE, - } as MerkleTreeHookConfig, + }, { type: HookType.INTERCHAIN_GAS_PAYMASTER, owner: owner, @@ -107,19 +139,19 @@ export function presetHookConfigs( gasOracleType, overhead, oracleKey: owner, - } as IgpHookConfig, + }, ], }, }; } -export function readHookConfig(filePath: string) { +export function readHooksConfigMap(filePath: string) { const config = readYamlOrJson(filePath); if (!config) { logRed(`No hook config found at ${filePath}`); return; } - const result = HookConfigMapSchema.safeParse(config); + const result = HooksConfigMapSchema.safeParse(config); if (!result.success) { const firstIssue = result.error.issues[0]; throw new Error( @@ -127,21 +159,15 @@ export function readHookConfig(filePath: string) { ); } const parsedConfig = result.data; - const defaultHook: ChainMap = objMap( + const hooks: ChainMap = objMap( parsedConfig, - (_, config) => - ({ - type: config.default.type, - } as HookConfig), + (_, config) => config as HooksConfig, ); - logGreen(`All hook configs in ${filePath} are valid`); - return defaultHook; + logGreen(`All hook configs in ${filePath} are valid for ${hooks}`); + return hooks; } -// TODO: read different hook configs -// export async function readProtocolFeeHookConfig(config: {type: HookType.PROTOCOL_FEE, ...}) { - -export async function createHookConfig({ +export async function createHooksConfigMap({ format, outPath, chainConfigPath, @@ -154,70 +180,15 @@ export async function createHookConfig({ const customChains = readChainConfigsIfExists(chainConfigPath); const chains = await runMultiChainSelectionStep(customChains); - const result: HookConfigMap = {}; + const result: HooksConfigMap = {}; for (const chain of chains) { for (const hookRequirements of ['required', 'default']) { log(`Setting ${hookRequirements} hook for chain ${chain}`); - const hookType = await select({ - message: 'Select hook type', - choices: [ - { value: 'merkle_tree', name: 'MerkleTreeHook' }, - { value: 'protocol_fee', name: 'StaticProtocolFee' }, - ], - pageSize: 5, - }); - if (hookType === 'merkle_tree') { - result[chain] = { - ...result[chain], - [hookRequirements]: { type: HookType.MERKLE_TREE }, - }; - } else if (hookType === 'protocol_fee') { - const owner = await input({ - message: 'Enter owner address', - }); - const ownerAddress = normalizeAddressEvm(owner); - let beneficiary; - let sameAsOwner = false; - sameAsOwner = await confirm({ - message: 'Use this same address for the beneficiary?', - }); - if (sameAsOwner) { - beneficiary = ownerAddress; - } else { - beneficiary = await input({ - message: 'Enter beneficiary address', - }); - } - const beneficiaryAddress = normalizeAddressEvm(beneficiary); - // TODO: input in gwei, wei, etc - const maxProtocolFee = toWei( - await input({ - message: 'Enter max protocol fee in (e.g. 1.0)', - }), - ); - const protocolFee = toWei( - await input({ - message: 'Enter protocol fee (e.g. 1.0)', - }), - ); - if (BigNumber(protocolFee).gt(maxProtocolFee)) { - errorRed('Protocol fee cannot be greater than max protocol fee'); - throw new Error('Invalid protocol fee'); - } - - result[chain] = { - ...result[chain], - [hookRequirements]: { - type: HookType.PROTOCOL_FEE, - maxProtocolFee: maxProtocolFee.toString(), - protocolFee: protocolFee.toString(), - beneficiary: beneficiaryAddress, - owner: ownerAddress, - }, - }; - } else { - throw new Error(`Invalid hook type: ${hookType}}`); - } + const remotes = chains.filter((c) => c !== chain); + result[chain] = { + ...result[chain], + [hookRequirements]: await createHookConfig(chain, remotes), + }; } if (isValidHookConfigMap(result)) { logGreen(`Hook config is valid, writing to file ${outPath}`); @@ -230,3 +201,208 @@ export async function createHookConfig({ } } } + +export async function createHookConfig( + chain: ChainName, + remotes: ChainName[], +): Promise { + let lastConfig: HookConfig; + const hookType = await select({ + message: 'Select hook type', + choices: [ + { + value: HookType.MERKLE_TREE, + name: HookType.MERKLE_TREE, + description: + 'Add messages to the incremental merkle tree on origin chain (needed for the merkleRootMultisigIsm on the remote chain)', + }, + { + value: HookType.PROTOCOL_FEE, + name: HookType.PROTOCOL_FEE, + description: 'Charge fees for each message dispatch from this chain', + }, + { + value: HookType.INTERCHAIN_GAS_PAYMASTER, + name: HookType.INTERCHAIN_GAS_PAYMASTER, + description: + 'Allow for payments for expected gas to be paid by the relayer while delivering on remote chain', + }, + { + value: HookType.AGGREGATION, + name: HookType.AGGREGATION, + description: + 'Aggregate multiple hooks into a single hook (e.g. merkle tree + IGP) which will be called in sequence', + }, + { + value: HookType.ROUTING, + name: HookType.ROUTING, + description: + 'Each destination domain can have its own hook configured via DomainRoutingHook', + }, + ], + pageSize: 10, + }); + if (hookType === HookType.MERKLE_TREE) { + lastConfig = { type: HookType.MERKLE_TREE }; + } else if (hookType === HookType.PROTOCOL_FEE) { + lastConfig = await createProtocolFeeConfig(chain); + } else if (hookType === HookType.INTERCHAIN_GAS_PAYMASTER) { + lastConfig = await createIGPConfig(remotes); + } else if (hookType === HookType.AGGREGATION) { + lastConfig = await createAggregationConfig(chain, remotes); + } else if (hookType === HookType.ROUTING) { + lastConfig = await createRoutingConfig(chain, remotes); + } else { + throw new Error(`Invalid hook type: ${hookType}`); + } + return lastConfig; +} + +export async function createProtocolFeeConfig( + chain: ChainName, +): Promise { + const owner = await input({ + message: 'Enter owner address', + }); + const ownerAddress = normalizeAddressEvm(owner); + let beneficiary; + let sameAsOwner = false; + sameAsOwner = await confirm({ + message: 'Use this same address for the beneficiary?', + }); + if (sameAsOwner) { + beneficiary = ownerAddress; + } else { + beneficiary = await input({ + message: 'Enter beneficiary address', + }); + } + const beneficiaryAddress = normalizeAddressEvm(beneficiary); + // TODO: input in gwei, wei, etc + const maxProtocolFee = toWei( + await input({ + message: `Enter max protocol fee ${nativeTokenAndDecimals( + chain, + )} e.g. 1.0)`, + }), + ); + const protocolFee = toWei( + await input({ + message: `Enter protocol fee in ${nativeTokenAndDecimals( + chain, + )} e.g. 0.01)`, + }), + ); + if (BigNumberJs(protocolFee).gt(maxProtocolFee)) { + errorRed('Protocol fee cannot be greater than max protocol fee'); + throw new Error('Invalid protocol fee'); + } + + return { + type: HookType.PROTOCOL_FEE, + maxProtocolFee: maxProtocolFee.toString(), + protocolFee: protocolFee.toString(), + beneficiary: beneficiaryAddress, + owner: ownerAddress, + }; +} + +export async function createIGPConfig( + remotes: ChainName[], +): Promise { + const owner = await input({ + message: 'Enter owner address', + }); + const ownerAddress = normalizeAddressEvm(owner); + let beneficiary, oracleKey; + let sameAsOwner = false; + sameAsOwner = await confirm({ + message: 'Use this same address for the beneficiary and gasOracleKey?', + }); + if (sameAsOwner) { + beneficiary = ownerAddress; + oracleKey = ownerAddress; + } else { + beneficiary = await input({ + message: 'Enter beneficiary address', + }); + oracleKey = await input({ + message: 'Enter gasOracleKey address', + }); + } + const beneficiaryAddress = normalizeAddressEvm(beneficiary); + const oracleKeyAddress = normalizeAddressEvm(oracleKey); + const overheads: ChainMap = {}; + for (const chain of remotes) { + const overhead = parseInt( + await input({ + message: `Enter overhead for ${chain} (eg 75000)`, + }), + ); + overheads[chain] = overhead; + } + return { + type: HookType.INTERCHAIN_GAS_PAYMASTER, + beneficiary: beneficiaryAddress, + owner: ownerAddress, + oracleKey: oracleKeyAddress, + overhead: overheads, + gasOracleType: objMap( + overheads, + () => GasOracleContractType.StorageGasOracle, + ), + }; +} + +export async function createAggregationConfig( + chain: ChainName, + remotes: ChainName[], +): Promise { + const hooksNum = parseInt( + await input({ + message: 'Enter the number of hooks to aggregate (number)', + }), + 10, + ); + const hooks: Array = []; + for (let i = 0; i < hooksNum; i++) { + logBlue(`Creating hook ${i + 1} of ${hooksNum} ...`); + hooks.push(await createHookConfig(chain, remotes)); + } + return { + type: HookType.AGGREGATION, + hooks, + }; +} + +export async function createRoutingConfig( + origin: ChainName, + remotes: ChainName[], +): Promise { + const owner = await input({ + message: 'Enter owner address', + }); + const ownerAddress = owner; + + const domainsMap: ChainMap = {}; + for (const chain of remotes) { + await confirm({ + message: `You are about to configure hook for remote chain ${chain}. Continue?`, + }); + const config = await createHookConfig(origin, remotes); + domainsMap[chain] = config; + } + return { + type: HookType.ROUTING, + owner: ownerAddress, + domains: domainsMap, + }; +} + +function nativeTokenAndDecimals(chain: ChainName) { + return `10^${ + chainMetadata[chain].nativeToken?.decimals ?? '18' + } which you cannot exceed (in ${ + chainMetadata[chain].nativeToken?.symbol ?? 'eth' + }`; +} diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index ab75581d88..eb18006077 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -112,7 +112,7 @@ export async function createIsmConfigMap({ const result: ZodIsmConfigMap = {}; for (const chain of chains) { log(`Setting values for chain ${chain}`); - result[chain] = await createIsmConfig(chain, chainConfigPath); + result[chain] = await createIsmConfig(chain, chains); // TODO consider re-enabling. Disabling based on feedback from @nambrot for now. // repeat = await confirm({ @@ -132,8 +132,8 @@ export async function createIsmConfigMap({ } export async function createIsmConfig( - chain: ChainName, - chainConfigPath: string, + remote: ChainName, + origins: ChainName[], ): Promise { let lastConfig: ZodIsmConfig; const moduleType = await select({ @@ -177,9 +177,9 @@ export async function createIsmConfig( ) { lastConfig = await createMultisigConfig(moduleType); } else if (moduleType === IsmType.ROUTING) { - lastConfig = await createRoutingConfig(chain, chainConfigPath); + lastConfig = await createRoutingConfig(remote, origins); } else if (moduleType === IsmType.AGGREGATION) { - lastConfig = await createAggregationConfig(chain, chainConfigPath); + lastConfig = await createAggregationConfig(remote, origins); } else if (moduleType === IsmType.TEST_ISM) { lastConfig = { type: IsmType.TEST_ISM }; } else { @@ -208,8 +208,8 @@ export async function createMultisigConfig( } export async function createAggregationConfig( - chain: ChainName, - chainConfigPath: string, + remote: ChainName, + chains: ChainName[], ): Promise { const isms = parseInt( await input({ @@ -227,7 +227,7 @@ export async function createAggregationConfig( const modules: Array = []; for (let i = 0; i < isms; i++) { - modules.push(await createIsmConfig(chain, chainConfigPath)); + modules.push(await createIsmConfig(remote, chains)); } return { type: IsmType.AGGREGATION, @@ -237,27 +237,21 @@ export async function createAggregationConfig( } export async function createRoutingConfig( - chain: ChainName, - chainConfigPath: string, + remote: ChainName, + chains: ChainName[], ): Promise { const owner = await input({ message: 'Enter owner address', }); const ownerAddress = owner; - const customChains = readChainConfigsIfExists(chainConfigPath); - delete customChains[chain]; - const chains = await runMultiChainSelectionStep( - customChains, - `Select origin chains to be verified on ${chain}`, - [chain], - ); + const origins = chains.filter((chain) => chain !== remote); const domainsMap: ChainMap = {}; - for (const chain of chains) { + for (const chain of origins) { await confirm({ message: `You are about to configure ISM from source chain ${chain}. Continue?`, }); - const config = await createIsmConfig(chain, chainConfigPath); + const config = await createIsmConfig(chain, chains); domainsMap[chain] = config; } return { diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index b58a946901..bb9e5c7fef 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -7,7 +7,7 @@ import { CoreConfig, DeployedIsm, GasOracleContractType, - HookType, + HooksConfig, HyperlaneAddressesMap, HyperlaneContractsMap, HyperlaneCore, @@ -31,7 +31,7 @@ import { Address, objFilter, objMerge } from '@hyperlane-xyz/utils'; import { log, logBlue, logGray, logGreen, logRed } from '../../logger.js'; import { runDeploymentArtifactStep } from '../config/artifacts.js'; -import { readHookConfig } from '../config/hooks.js'; +import { presetHookConfigs, readHooksConfigMap } from '../config/hooks.js'; import { readIsmConfig } from '../config/ism.js'; import { readMultisigConfig } from '../config/multisig.js'; import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js'; @@ -95,8 +95,7 @@ export async function runCoreDeploy({ const multisigConfigs = isIsmConfig ? defaultMultisigConfigs : (result as ChainMap); - // TODO re-enable when hook config is actually used - await runHookStep(chains, hookConfigPath); + const hooksConfig = await runHookStep(chains, hookConfigPath); const deploymentParams: DeployParams = { chains, @@ -105,6 +104,7 @@ export async function runCoreDeploy({ artifacts, ismConfigs, multisigConfigs, + hooksConfig, outPath, skipConfirmation, }; @@ -194,24 +194,8 @@ async function runHookStep( _selectedChains: ChainName[], hookConfigPath?: string, ) { - if ('TODO: Skip this step for now as values are unused') return; - - // const presetConfigChains = Object.keys(presetHookConfigs); - - if (!hookConfigPath) { - logBlue( - '\n', - 'Hyperlane instances can take an Interchain Security Module (ISM).', - ); - hookConfigPath = await runFileSelectionStep( - './configs/', - 'Hook config', - 'hook', - ); - } - const configs = readHookConfig(hookConfigPath); - if (!configs) return; - log(`Found hook configs for chains: ${Object.keys(configs).join(', ')}`); + if (!hookConfigPath) return {}; + return readHooksConfigMap(hookConfigPath); } interface DeployParams { @@ -221,6 +205,7 @@ interface DeployParams { artifacts?: HyperlaneAddressesMap; ismConfigs?: ChainMap; multisigConfigs?: ChainMap; + hooksConfig?: ChainMap; outPath: string; skipConfirmation: boolean; } @@ -258,6 +243,7 @@ async function executeDeploy({ artifacts = {}, ismConfigs = {}, multisigConfigs = {}, + hooksConfig = {}, }: DeployParams) { logBlue('All systems ready, captain! Beginning deployment...'); @@ -320,7 +306,8 @@ async function executeDeploy({ owner, chains, defaultIsms, - multisigConfigs ?? defaultMultisigConfigs, // TODO: fix https://github.com/hyperlane-xyz/issues/issues/773 + hooksConfig, + multisigConfigs, ); const coreContracts = await coreDeployer.deploy(coreConfigs); artifacts = writeMergedAddresses(contractsFilePath, artifacts, coreContracts); @@ -372,32 +359,23 @@ function buildCoreConfigMap( owner: Address, chains: ChainName[], defaultIsms: ChainMap
, - multisigConfig: ChainMap, + hooksConfig: ChainMap, + multisigConfigs: ChainMap, ): ChainMap { return chains.reduce>((config, chain) => { - const igpConfig = buildIgpConfigMap(owner, chains, multisigConfig); + const hooks = + hooksConfig[chain] ?? + presetHookConfigs( + owner, + chain, + chains.filter((c) => c !== chain), + multisigConfigs[chain], // if no multisig config, uses default 2/3 + ); config[chain] = { owner, defaultIsm: defaultIsms[chain], - defaultHook: { - type: HookType.AGGREGATION, - hooks: [ - { - type: HookType.MERKLE_TREE, - }, - { - type: HookType.INTERCHAIN_GAS_PAYMASTER, - ...igpConfig[chain], - }, - ], - }, - requiredHook: { - type: HookType.PROTOCOL_FEE, - maxProtocolFee: ethers.utils.parseUnits('1', 'gwei'), // 1 gwei of native token - protocolFee: ethers.utils.parseUnits('0', 'wei'), // 1 wei - beneficiary: owner, - owner, - }, + defaultHook: hooks.default, + requiredHook: hooks.required, }; return config; }, {}); @@ -419,7 +397,7 @@ function buildTestRecipientConfigMap( }, {}); } -function buildIgpConfigMap( +export function buildIgpConfigMap( owner: Address, chains: ChainName[], multisigConfigs: ChainMap, diff --git a/typescript/cli/src/tests/hooks.test.ts b/typescript/cli/src/tests/hooks.test.ts new file mode 100644 index 0000000000..2848c0c061 --- /dev/null +++ b/typescript/cli/src/tests/hooks.test.ts @@ -0,0 +1,92 @@ +import { expect } from 'chai'; + +import { + ChainMap, + GasOracleContractType, + HookType, + HooksConfig, +} from '@hyperlane-xyz/sdk'; + +import { readHooksConfigMap } from '../config/hooks.js'; + +describe('readHooksConfigMap', () => { + it('parses and validates example correctly', () => { + const hooks = readHooksConfigMap('examples/hooks.yaml'); + + const exampleHooksConfig: ChainMap = { + anvil1: { + required: { + type: HookType.PROTOCOL_FEE, + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + maxProtocolFee: '1000000000000000000', + protocolFee: '200000000000000', + }, + default: { + type: HookType.ROUTING, + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + domains: { + anvil2: { + type: HookType.AGGREGATION, + hooks: [ + { + type: HookType.MERKLE_TREE, + }, + { + type: HookType.INTERCHAIN_GAS_PAYMASTER, + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + gasOracleType: { + anvil2: GasOracleContractType.StorageGasOracle, + }, + overhead: { anvil2: 50000 }, + }, + ], + }, + }, + }, + }, + anvil2: { + required: { + type: HookType.PROTOCOL_FEE, + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + maxProtocolFee: '1000000000000000000', + protocolFee: '200000000000000', + }, + default: { + type: HookType.ROUTING, + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + domains: { + anvil1: { + type: HookType.AGGREGATION, + hooks: [ + { + type: HookType.MERKLE_TREE, + }, + { + type: HookType.INTERCHAIN_GAS_PAYMASTER, + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + gasOracleType: { + anvil1: GasOracleContractType.StorageGasOracle, + }, + overhead: { anvil1: 50000 }, + }, + ], + }, + }, + }, + }, + }; + expect(hooks).to.deep.equal(exampleHooksConfig); + }); + + it('parsing failure, missing internal key "overhead"', () => { + expect(() => { + readHooksConfigMap('src/tests/hooks/safe-parse-fail.yaml'); + }).to.throw('Invalid hook config: anvil2,default => Invalid input'); + }); +}); diff --git a/typescript/cli/src/tests/hooks/safe-parse-fail.yaml b/typescript/cli/src/tests/hooks/safe-parse-fail.yaml new file mode 100644 index 0000000000..4a2a5cedbf --- /dev/null +++ b/typescript/cli/src/tests/hooks/safe-parse-fail.yaml @@ -0,0 +1,44 @@ +anvil1: + required: + type: protocolFee + maxProtocolFee: '1000000000000000000' + protocolFee: '200000000000000' + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + default: + type: domainRoutingHook + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + domains: + anvil2: + type: aggregationHook + hooks: + - type: merkleTreeHook + - type: interchainGasPaymaster + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + overhead: + anvil2: 50000 + gasOracleType: + anvil2: StorageGasOracle +anvil2: + required: + type: protocolFee + maxProtocolFee: '1000000000000000000' + protocolFee: '200000000000000' + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + default: + type: domainRoutingHook + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + domains: + anvil1: + type: aggregationHook + hooks: + - type: merkleTreeHook + - type: interchainGasPaymaster + beneficiary: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + owner: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + oracleKey: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' + gasOracleType: + anvil1: StorageGasOracle diff --git a/typescript/infra/config/environments/mainnet3/core.ts b/typescript/infra/config/environments/mainnet3/core.ts index 54405eb532..7fa6138334 100644 --- a/typescript/infra/config/environments/mainnet3/core.ts +++ b/typescript/infra/config/environments/mainnet3/core.ts @@ -36,8 +36,8 @@ export const core: ChainMap = objMap(owners, (local, owner) => { const requiredHook: ProtocolFeeHookConfig = { type: HookType.PROTOCOL_FEE, - maxProtocolFee: ethers.utils.parseUnits('1', 'gwei'), // 1 gwei of native token - protocolFee: BigNumber.from(0), // 0 wei + maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token + protocolFee: BigNumber.from(0).toString(), // 0 wei beneficiary: owner, owner, }; diff --git a/typescript/infra/config/environments/test/core.ts b/typescript/infra/config/environments/test/core.ts index 0c2f476e42..397059c52c 100644 --- a/typescript/infra/config/environments/test/core.ts +++ b/typescript/infra/config/environments/test/core.ts @@ -57,8 +57,8 @@ export const core: ChainMap = objMap(owners, (local, owner) => { const requiredHook: ProtocolFeeHookConfig = { type: HookType.PROTOCOL_FEE, - maxProtocolFee: ethers.utils.parseUnits('1', 'gwei'), // 1 gwei of native token - protocolFee: BigNumber.from(1), // 1 wei + maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token + protocolFee: BigNumber.from(1).toString(), // 1 wei beneficiary: owner, owner, }; diff --git a/typescript/infra/config/environments/testnet4/core.ts b/typescript/infra/config/environments/testnet4/core.ts index 6198c55799..f8747befa7 100644 --- a/typescript/infra/config/environments/testnet4/core.ts +++ b/typescript/infra/config/environments/testnet4/core.ts @@ -78,8 +78,8 @@ export const core: ChainMap = objMap(owners, (local, owner) => { const requiredHook: ProtocolFeeHookConfig = { type: HookType.PROTOCOL_FEE, - maxProtocolFee: ethers.utils.parseUnits('1', 'gwei'), // 1 gwei of native token - protocolFee: BigNumber.from(1), // 1 wei + maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token + protocolFee: BigNumber.from(1).toString(), // 1 wei of native token beneficiary: owner, owner, }; diff --git a/typescript/sdk/src/hook/types.ts b/typescript/sdk/src/hook/types.ts index a0e60a88b7..0cbedfb7a7 100644 --- a/typescript/sdk/src/hook/types.ts +++ b/typescript/sdk/src/hook/types.ts @@ -1,5 +1,3 @@ -import { BigNumber } from 'ethers'; - import { Address } from '@hyperlane-xyz/utils'; import { IgpConfig } from '../gas/types'; @@ -30,8 +28,8 @@ export type IgpHookConfig = IgpConfig & { export type ProtocolFeeHookConfig = { type: HookType.PROTOCOL_FEE; - maxProtocolFee: BigNumber; - protocolFee: BigNumber; + maxProtocolFee: string; + protocolFee: string; beneficiary: Address; owner: Address; }; @@ -64,3 +62,8 @@ export type HookConfig = | OpStackHookConfig | DomainRoutingHookConfig | FallbackRoutingHookConfig; + +export type HooksConfig = { + required: HookConfig; + default: HookConfig; +}; diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index f12ce8e06f..a5122765ce 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -116,6 +116,7 @@ export { FallbackRoutingHookConfig, HookConfig, HookType, + HooksConfig, IgpHookConfig, MerkleTreeHookConfig, OpStackHookConfig, diff --git a/typescript/sdk/src/test/testUtils.ts b/typescript/sdk/src/test/testUtils.ts index 0ce5da89dc..3d2c48e56e 100644 --- a/typescript/sdk/src/test/testUtils.ts +++ b/typescript/sdk/src/test/testUtils.ts @@ -58,8 +58,8 @@ export function testCoreConfig( }, requiredHook: { type: HookType.PROTOCOL_FEE, - maxProtocolFee: ethers.utils.parseUnits('1', 'gwei'), // 1 gwei of native token - protocolFee: BigNumber.from(1), // 1 wei + maxProtocolFee: ethers.utils.parseUnits('1', 'gwei').toString(), // 1 gwei of native token + protocolFee: BigNumber.from(1).toString(), // 1 wei beneficiary: nonZeroAddress, owner, },