Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): Add hyperlane warp apply #4094

Merged
merged 16 commits into from
Jul 5, 2024
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: {
paulbalaji marked this conversation as resolved.
Show resolved Hide resolved
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]),
);
Comment on lines +341 to +345
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine but realistically if n < 1000-ish if really doesn't matter if it's O(1), O(n), or even O(n^2)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True that

Copy link
Contributor Author

@ltyu ltyu Jul 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mostly did it because i like reasoning with similar data shapes. I thought it would be hard to compare an array with an object


// 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is a cast needed here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because addresses[chain] returns a Record<string, string> which seems to be no specific enough

Type 'Record<string, string>' is missing the following properties from type '{ staticMerkleRootMultisigIsmFactory: string; staticMessageIdMultisigIsmFactory: string; staticAggregationIsmFactory: string; staticAggregationHookFactory: string; domainRoutingIsmFactory: string; }': staticMerkleRootMultisigIsmFactory, staticMessageIdMultisigIsmFactory, staticAggregationIsmFactory, staticAggregationHookFactory, domainRoutingIsmFactoryts(2739)

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} chain.`);
} else {
logGreen(
`Warp config on ${chain} chain is the same as target. No updates needed.`,
);
}
} catch (e) {
logRed(`Warp config on ${chain} chain failed to update.`, e);
ltyu marked this conversation as resolved.
Show resolved Hide resolved
}
}),
);
}
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
Loading