Skip to content

Commit

Permalink
feat(cli): Add hyperlane warp apply (#4094)
Browse files Browse the repository at this point in the history
### Description
- Adds `hyperlane warp apply`

### Related issues
- Fixes hyperlane-xyz/issues#1190

### Backward compatibility
Yes

### Testing
Manual


To test: `yarn hyperlane warp apply --warp`

---------

Co-authored-by: pbio <10051819+paulbalaji@users.noreply.github.com>
  • Loading branch information
ltyu and paulbalaji authored Jul 5, 2024
1 parent cb225b8 commit 7089994
Show file tree
Hide file tree
Showing 10 changed files with 151 additions and 57 deletions.
6 changes: 6 additions & 0 deletions .changeset/proud-days-flash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---

Adds hyperlane warp apply
4 changes: 2 additions & 2 deletions typescript/cli/src/commands/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
44 changes: 43 additions & 1 deletion typescript/cli/src/commands/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -50,6 +51,7 @@ export const warpCommand: CommandModule = {
describe: 'Manage Hyperlane warp routes',
builder: (yargs) =>
yargs
.command(apply)
.command(deploy)
.command(init)
.command(read)
Expand All @@ -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;
Expand Down
96 changes: 82 additions & 14 deletions typescript/cli/src/deploy/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import { stringify as yamlStringify } from 'yaml';

import { IRegistry } from '@hyperlane-xyz/registry';
import {
EvmERC20WarpModule,
EvmIsmModule,
HypERC20Deployer,
HypERC721Deployer,
HyperlaneAddresses,
HyperlaneContractsMap,
HyperlaneProxyFactoryDeployer,
MultiProvider,
ProxyFactoryFactoriesAddresses,
TOKEN_TYPE_TO_STANDARD,
TokenFactories,
TokenType,
Expand All @@ -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,
Expand All @@ -44,7 +53,11 @@ import {

interface DeployParams {
context: WriteCommandContext;
configMap: WarpRouteDeployConfig;
warpDeployConfig: WarpRouteDeployConfig;
}

interface ApplyParams extends DeployParams {
warpCoreConfig: WarpCoreConfig;
}

export async function runWarpRouteDeploy({
Expand Down Expand Up @@ -79,7 +92,7 @@ export async function runWarpRouteDeploy({

const deploymentParams = {
context,
configMap: warpRouteConfig,
warpDeployConfig: warpRouteConfig,
};

logBlue('Warp route deployment plan');
Expand All @@ -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;

Expand All @@ -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);

Expand Down Expand Up @@ -256,15 +269,15 @@ async function createWarpIsm(
}

async function getWarpCoreConfig(
{ configMap, context }: DeployParams,
{ warpDeployConfig, context }: DeployParams,
contracts: HyperlaneContractsMap<TokenFactories>,
): Promise<WarpCoreConfig> {
const warpCoreConfig: WarpCoreConfig = { tokens: [] };

// TODO: replace with warp read
const tokenMetadata = await HypERC20Deployer.deriveTokenMetadata(
context.multiProvider,
configMap,
warpDeployConfig,
);
assert(
tokenMetadata && isTokenMetadata(tokenMetadata),
Expand All @@ -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({
Expand All @@ -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,
});
}
Expand Down Expand Up @@ -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);
}
}),
);
}
4 changes: 4 additions & 0 deletions typescript/sdk/src/deploy/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ export const ProxyFactoryFactoriesSchema = z.object({
staticAggregationHookFactory: z.string(),
domainRoutingIsmFactory: z.string(),
});

export type ProxyFactoryFactoriesAddresses = z.infer<
typeof ProxyFactoryFactoriesSchema
>;
1 change: 1 addition & 0 deletions typescript/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
46 changes: 8 additions & 38 deletions typescript/sdk/src/ism/EvmIsmModule.hardhat-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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;
});
});
});
2 changes: 1 addition & 1 deletion typescript/sdk/src/ism/EvmIsmModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => {
type: IsmType.ROUTING,
owner: randomAddress(),
domains: {
'2': ismAddress,
test2: { type: IsmType.TEST_ISM },
},
},
};
Expand Down
Loading

0 comments on commit 7089994

Please sign in to comment.