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>
pull/4104/head
Lee 4 months ago committed by GitHub
parent cb225b824e
commit 7089994334
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/proud-days-flash.md
  2. 4
      typescript/cli/src/commands/options.ts
  3. 44
      typescript/cli/src/commands/warp.ts
  4. 96
      typescript/cli/src/deploy/warp.ts
  5. 4
      typescript/sdk/src/deploy/schemas.ts
  6. 1
      typescript/sdk/src/index.ts
  7. 46
      typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts
  8. 2
      typescript/sdk/src/ism/EvmIsmModule.ts
  9. 2
      typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts
  10. 3
      typescript/sdk/src/token/EvmERC20WarpModule.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Adds hyperlane warp apply

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

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

@ -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<TokenFactories>,
): Promise<WarpCoreConfig> {
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);
}
}),
);
}

@ -13,3 +13,7 @@ export const ProxyFactoryFactoriesSchema = z.object({
staticAggregationHookFactory: z.string(),
domainRoutingIsmFactory: z.string(),
});
export type ProxyFactoryFactoriesAddresses = z.infer<
typeof ProxyFactoryFactoriesSchema
>;

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

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

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

@ -356,7 +356,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
type: IsmType.ROUTING,
owner: randomAddress(),
domains: {
'2': ismAddress,
test2: { type: IsmType.TEST_ISM },
},
},
};

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

Loading…
Cancel
Save