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

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->
pull/3014/head
Kunal Arora 12 months ago committed by GitHub
parent 9fc0866b29
commit 7e620c9dfa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      .changeset/odd-keys-pretend.md
  2. 18
      typescript/cli/examples/hook-config.yaml
  3. 66
      typescript/cli/examples/hooks.yaml
  4. 8
      typescript/cli/src/commands/config.ts
  5. 372
      typescript/cli/src/config/hooks.ts
  6. 32
      typescript/cli/src/config/ism.ts
  7. 68
      typescript/cli/src/deploy/core.ts
  8. 92
      typescript/cli/src/tests/hooks.test.ts
  9. 44
      typescript/cli/src/tests/hooks/safe-parse-fail.yaml
  10. 4
      typescript/infra/config/environments/mainnet3/core.ts
  11. 4
      typescript/infra/config/environments/test/core.ts
  12. 4
      typescript/infra/config/environments/testnet4/core.ts
  13. 11
      typescript/sdk/src/hook/types.ts
  14. 1
      typescript/sdk/src/index.ts
  15. 4
      typescript/sdk/src/test/testUtils.ts

@ -0,0 +1,7 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/infra': patch
'@hyperlane-xyz/sdk': patch
---
Allow CLI to accept hook as a config

@ -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

@ -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

@ -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);
},
};

@ -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<any> = z.lazy(() =>
z.object({
type: z.literal(HookType.ROUTING),
owner: z.string(),
domains: z.record(HookConfigSchema),
}),
);
const AggregationConfigSchema: z.ZodSchema<any> = 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<typeof HookConfigSchema>;
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<typeof HookConfigMapSchema>;
const HooksConfigMapSchema = z.record(HooksConfigSchema);
export type HooksConfigMap = z.infer<typeof HooksConfigMapSchema>;
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<GasOracleContractType>
>((acc, chain) => {
@ -69,14 +98,17 @@ export function presetHookConfigs(
const overhead = destinationChains.reduce<ChainMap<number>>((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<HookConfig> = objMap(
const hooks: ChainMap<HooksConfig> = 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<HookConfig> {
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<HookConfig> {
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<HookConfig> {
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<number> = {};
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<HookConfig> {
const hooksNum = parseInt(
await input({
message: 'Enter the number of hooks to aggregate (number)',
}),
10,
);
const hooks: Array<HookConfig> = [];
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<HookConfig> {
const owner = await input({
message: 'Enter owner address',
});
const ownerAddress = owner;
const domainsMap: ChainMap<HookConfig> = {};
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'
}`;
}

@ -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<ZodIsmConfig> {
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<ZodIsmConfig> {
const isms = parseInt(
await input({
@ -227,7 +227,7 @@ export async function createAggregationConfig(
const modules: Array<ZodIsmConfig> = [];
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<ZodIsmConfig> {
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<ZodIsmConfig> = {};
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 {

@ -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<MultisigConfig>);
// 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<any>;
ismConfigs?: ChainMap<IsmConfig>;
multisigConfigs?: ChainMap<MultisigConfig>;
hooksConfig?: ChainMap<HooksConfig>;
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<Address>,
multisigConfig: ChainMap<MultisigConfig>,
hooksConfig: ChainMap<HooksConfig>,
multisigConfigs: ChainMap<MultisigConfig>,
): ChainMap<CoreConfig> {
return chains.reduce<ChainMap<CoreConfig>>((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<MultisigConfig>,

@ -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<HooksConfig> = {
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');
});
});

@ -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

@ -36,8 +36,8 @@ export const core: ChainMap<CoreConfig> = 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,
};

@ -57,8 +57,8 @@ export const core: ChainMap<CoreConfig> = 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,
};

@ -78,8 +78,8 @@ export const core: ChainMap<CoreConfig> = 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,
};

@ -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;
};

@ -116,6 +116,7 @@ export {
FallbackRoutingHookConfig,
HookConfig,
HookType,
HooksConfig,
IgpHookConfig,
MerkleTreeHookConfig,
OpStackHookConfig,

@ -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,
},

Loading…
Cancel
Save