feat(sdk,cli): Add hyperlane core apply with mailbox transfer ownership (#4215)

### Description
- Adds `hyperlane core apply` to the CLI
- Adds  mailbox transfer ownership to SDK

### Related issues
- https://github.com/hyperlane-xyz/issues/issues/1328
- https://github.com/hyperlane-xyz/issues/issues/1330

### Backward compatibility
Yes

### Testing
Manual/Unit Tests

---------

Co-authored-by: J M Rossy <jm.rossy@gmail.com>
noah/prompt
Lee 4 months ago committed by GitHub
parent 9edbc175cf
commit 5529d98d03
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/red-rabbits-dress.md
  2. 1
      Dockerfile
  3. 45
      typescript/cli/src/commands/core.ts
  4. 11
      typescript/cli/src/config/core.ts
  5. 47
      typescript/cli/src/deploy/core.ts
  6. 47
      typescript/sdk/src/core/AbstractHyperlaneModule.ts
  7. 44
      typescript/sdk/src/core/EvmCoreModule.hardhat-test.ts
  8. 84
      typescript/sdk/src/core/EvmCoreModule.ts
  9. 2
      typescript/sdk/src/index.ts
  10. 24
      typescript/sdk/src/token/EvmERC20WarpModule.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add hyperlane core apply with update ownership

@ -13,7 +13,6 @@ COPY .yarn/releases ./.yarn/releases
COPY .yarn/patches ./.yarn/patches
COPY typescript/utils/package.json ./typescript/utils/
COPY typescript/sdk/package.json ./typescript/sdk/
COPY typescript/widgets/package.json ./typescript/widgets/
COPY typescript/helloworld/package.json ./typescript/helloworld/
COPY typescript/cli/package.json ./typescript/cli/
COPY typescript/infra/package.json ./typescript/infra/

@ -2,12 +2,15 @@ import { CommandModule } from 'yargs';
import { EvmCoreReader } from '@hyperlane-xyz/sdk';
import { createCoreDeployConfig } from '../config/core.js';
import {
createCoreDeployConfig,
readCoreDeployConfigs,
} from '../config/core.js';
import {
CommandModuleWithContext,
CommandModuleWithWriteContext,
} from '../context/types.js';
import { runCoreDeploy } from '../deploy/core.js';
import { runCoreApply, runCoreDeploy } from '../deploy/core.js';
import { evaluateIfDryRunFailure } from '../deploy/dry-run.js';
import { errorRed, log, logGray, logGreen } from '../logger.js';
import {
@ -31,6 +34,7 @@ export const coreCommand: CommandModule = {
describe: 'Manage core Hyperlane contracts & configs',
builder: (yargs) =>
yargs
.command(apply)
.command(deploy)
.command(init)
.command(read)
@ -38,6 +42,43 @@ export const coreCommand: CommandModule = {
.demandCommand(),
handler: () => log('Command required'),
};
export const apply: CommandModuleWithWriteContext<{
chain: string;
mailbox: string;
config: string;
}> = {
command: 'apply',
describe:
'Applies onchain Core configuration updates for a given mailbox address',
builder: {
chain: {
...chainCommandOption,
demandOption: true,
},
mailbox: {
type: 'string',
description: 'Mailbox address used to derive the core config',
demandOption: true,
},
config: outputFileCommandOption(
'./configs/core-config.yaml',
true,
'The path to output a Core Config JSON or YAML file.',
),
},
handler: async ({ context, chain, mailbox, config: configFilePath }) => {
logGray(`Hyperlane Warp Apply`);
logGray('--------------------');
const expectedCoreConfig = await readCoreDeployConfigs(configFilePath);
await runCoreApply({
context,
chain,
mailbox,
config: expectedCoreConfig,
});
process.exit(0);
},
};
/**
* Generates a command module for deploying Hyperlane contracts, given a command

@ -4,7 +4,11 @@ import { CoreConfigSchema, HookConfig, IsmConfig } from '@hyperlane-xyz/sdk';
import { CommandContext } from '../context/types.js';
import { errorRed, log, logBlue, logGreen } from '../logger.js';
import { indentYamlOrJson, writeYamlOrJson } from '../utils/files.js';
import {
indentYamlOrJson,
readYamlOrJson,
writeYamlOrJson,
} from '../utils/files.js';
import { detectAndConfirmOrPrompt } from '../utils/input.js';
import {
@ -69,3 +73,8 @@ export async function createCoreDeployConfig({
throw e;
}
}
export async function readCoreDeployConfigs(filePath: string) {
const config = readYamlOrJson(filePath);
return CoreConfigSchema.parse(config);
}

@ -9,11 +9,12 @@ import {
EvmCoreModule,
ExplorerLicenseType,
} from '@hyperlane-xyz/sdk';
import { Address } from '@hyperlane-xyz/utils';
import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js';
import { getOrRequestApiKeys } from '../context/context.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logBlue, logGreen } from '../logger.js';
import { log, logBlue, logGray, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
import { indentYamlOrJson } from '../utils/files.js';
@ -29,18 +30,17 @@ interface DeployParams {
chain: ChainName;
config: CoreConfig;
}
interface ApplyParams extends DeployParams {
mailbox: Address;
}
/**
* Executes the core deploy command.
*/
export async function runCoreDeploy({
context,
chain,
config,
}: {
context: WriteCommandContext;
chain: ChainName;
config: CoreConfig;
}) {
export async function runCoreDeploy(params: DeployParams) {
const { context, config } = params;
let chain = params.chain;
const {
signer,
isDryRun,
@ -111,3 +111,30 @@ export async function runCoreDeploy({
logGreen('✅ Core contract deployments complete:\n');
log(indentYamlOrJson(yamlStringify(deployedAddresses, null, 2), 4));
}
export async function runCoreApply(params: ApplyParams) {
const { context, chain, mailbox, config } = params;
const { multiProvider } = context;
const evmCoreModule = new EvmCoreModule(multiProvider, {
chain,
config,
addresses: {
mailbox,
},
});
const transactions = await evmCoreModule.update(config);
if (transactions.length) {
logGray('Updating deployed core contracts');
for (const transaction of transactions) {
await multiProvider.sendTransaction(chain, transaction);
}
logGreen(`Core config updated on ${chain}.`);
} else {
logGreen(
`Core config on ${chain} is the same as target. No updates needed.`,
);
}
}

@ -1,8 +1,17 @@
import { Logger } from 'pino';
import { Annotated, ProtocolType } from '@hyperlane-xyz/utils';
import { Ownable__factory } from '@hyperlane-xyz/core';
import {
Address,
Annotated,
ProtocolType,
eqAddress,
} from '@hyperlane-xyz/utils';
import { ProtocolTypedTransaction } from '../providers/ProviderType.js';
import {
AnnotatedEV5Transaction,
ProtocolTypedTransaction,
} from '../providers/ProviderType.js';
import { ChainNameOrId } from '../types.js';
export type HyperlaneModuleParams<
@ -34,6 +43,40 @@ export abstract class HyperlaneModule<
config: TConfig,
): Promise<Annotated<ProtocolTypedTransaction<TProtocol>['transaction'][]>>;
/**
* Transfers ownership of a contract to a new owner.
*
* @param actualOwner - The current owner of the contract.
* @param expectedOwner - The expected new owner of the contract.
* @param deployedAddress - The address of the deployed contract.
* @param chainId - The chain ID of the network the contract is deployed on.
* @returns An array of annotated EV5 transactions that need to be executed to update the owner.
*/
static createTransferOwnershipTx(params: {
actualOwner: Address;
expectedOwner: Address;
deployedAddress: Address;
chainId: number;
}): AnnotatedEV5Transaction[] {
const { actualOwner, expectedOwner, deployedAddress, chainId } = params;
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (eqAddress(actualOwner, expectedOwner)) {
return [];
}
updateTransactions.push({
annotation: `Transferring ownership of ${deployedAddress} from current owner ${actualOwner} to new owner ${expectedOwner}`,
chainId,
to: deployedAddress,
data: Ownable__factory.createInterface().encodeFunctionData(
'transferOwnership(address)',
[expectedOwner],
),
});
return updateTransactions;
}
// /*
// Types and static methods can be challenging. Ensure each implementation includes a static create function.
// Currently, include TConfig to maintain the structure for ISM/Hook configurations.

@ -10,17 +10,18 @@ import {
TimelockController__factory,
ValidatorAnnounce__factory,
} from '@hyperlane-xyz/core';
import { objMap } from '@hyperlane-xyz/utils';
import { normalizeConfig, objMap } from '@hyperlane-xyz/utils';
import { TestChainName } from '../consts/testChains.js';
import { MultiProvider } from '../providers/MultiProvider.js';
import { testCoreConfig } from '../test/testUtils.js';
import { AnnotatedEV5Transaction } from '../providers/ProviderType.js';
import { randomAddress, testCoreConfig } from '../test/testUtils.js';
import { EvmCoreModule } from './EvmCoreModule.js';
import { CoreConfig } from './types.js';
describe('EvmCoreModule', async () => {
const CHAIN = TestChainName.test1;
const CHAIN = TestChainName.test4;
const DELAY = 1892391283182;
let config: CoreConfig;
let signer: SignerWithAddress;
@ -31,12 +32,17 @@ describe('EvmCoreModule', async () => {
let validatorAnnounceContract: any;
let testRecipientContract: any;
let timelockControllerContract: any;
async function sendTxs(txs: AnnotatedEV5Transaction[]) {
for (const tx of txs) {
await multiProvider.sendTransaction(CHAIN, tx);
}
}
before(async () => {
[signer] = await hre.ethers.getSigners();
multiProvider = MultiProvider.createTestMultiProvider({ signer });
config = {
...testCoreConfig([CHAIN])[CHAIN],
owner: signer.address,
upgrade: {
timelock: {
delay: DELAY,
@ -161,4 +167,34 @@ describe('EvmCoreModule', async () => {
expect(await timelockControllerContract.getMinDelay()).to.equal(DELAY);
});
});
describe('Update', async () => {
it('should update the owner only if they are different', async () => {
const evmCoreModule = await EvmCoreModule.create({
chain: CHAIN,
config,
multiProvider,
});
const newOwner = randomAddress();
let latestConfig = normalizeConfig(await evmCoreModule.read());
expect(latestConfig.owner).to.not.equal(newOwner);
await sendTxs(
await evmCoreModule.update({
...config,
owner: newOwner,
}),
);
latestConfig = normalizeConfig(await evmCoreModule.read());
expect(latestConfig.owner).to.equal(newOwner);
// No op if the same owner
const txs = await evmCoreModule.update({
...config,
owner: newOwner,
});
expect(txs.length).to.equal(0);
});
});
});

@ -1,5 +1,11 @@
import { Mailbox } from '@hyperlane-xyz/core';
import { Address, ProtocolType, rootLogger } from '@hyperlane-xyz/utils';
import {
Address,
Domain,
ProtocolType,
assert,
rootLogger,
} from '@hyperlane-xyz/utils';
import {
attachContractsMap,
@ -26,30 +32,38 @@ import { EvmCoreReader } from './EvmCoreReader.js';
import { EvmIcaModule } from './EvmIcaModule.js';
import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js';
import { CoreFactories } from './contracts.js';
import { CoreConfigSchema } from './schemas.js';
export type DeployedCoreAdresses = HyperlaneAddresses<CoreFactories> & {
testRecipient: Address;
timelockController?: Address; // Can be optional because it is only deployed if config.upgrade = true
interchainAccountRouter: Address;
interchainAccountIsm: Address;
} & HyperlaneAddresses<ProxyFactoryFactories>;
type DeployedCoreAddresses = Partial<
HyperlaneAddresses<CoreFactories> & {
testRecipient: Address;
timelockController: Address;
interchainAccountRouter: Address;
interchainAccountIsm: Address;
} & HyperlaneAddresses<ProxyFactoryFactories>
>;
export class EvmCoreModule extends HyperlaneModule<
ProtocolType.Ethereum,
CoreConfig,
DeployedCoreAdresses
DeployedCoreAddresses
> {
protected logger = rootLogger.child({ module: 'EvmCoreModule' });
protected coreReader: EvmCoreReader;
public readonly chainName: string;
protected constructor(
// We use domainId here because MultiProvider.getDomainId() will always
// return a number, and EVM the domainId and chainId are the same.
public readonly domainId: Domain;
constructor(
protected readonly multiProvider: MultiProvider,
args: HyperlaneModuleParams<CoreConfig, DeployedCoreAdresses>,
args: HyperlaneModuleParams<CoreConfig, DeployedCoreAddresses>,
) {
super(args);
this.coreReader = new EvmCoreReader(multiProvider, this.args.chain);
this.chainName = this.multiProvider.getChainName(this.args.chain);
this.domainId = multiProvider.getDomainId(args.chain);
}
/**
@ -57,11 +71,53 @@ export class EvmCoreModule extends HyperlaneModule<
* @returns The core config.
*/
public async read(): Promise<CoreConfig> {
assert(this.args.addresses.mailbox, 'Mailbox not provided for read');
return this.coreReader.deriveCoreConfig(this.args.addresses.mailbox);
}
public async update(_config: CoreConfig): Promise<AnnotatedEV5Transaction[]> {
throw new Error('Method not implemented.');
/**
* Updates the core contracts with the provided configuration.
*
* @param expectedConfig - The configuration for the core contracts to be updated.
* @returns An array of Ethereum transactions that were executed to update the contract.
*/
public async update(
expectedConfig: CoreConfig,
): Promise<AnnotatedEV5Transaction[]> {
CoreConfigSchema.parse(expectedConfig);
const actualConfig = await this.read();
const transactions: AnnotatedEV5Transaction[] = [];
transactions.push(
...this.createMailboxOwnershipTransferTx(actualConfig, expectedConfig),
);
return transactions;
}
/**
* Create a transaction to transfer ownership of an existing mailbox with a given config.
*
* @param actualConfig - The on-chain core configuration.
* @param expectedConfig - The expected token core configuration.
* @returns Ethereum transaction that need to be executed to update the owner.
*/
createMailboxOwnershipTransferTx(
actualConfig: CoreConfig,
expectedConfig: CoreConfig,
): AnnotatedEV5Transaction[] {
assert(
this.args.addresses.mailbox,
'Mailbox not provided for update ownership',
);
return EvmCoreModule.createTransferOwnershipTx({
actualOwner: actualConfig.owner,
expectedOwner: expectedConfig.owner,
deployedAddress: this.args.addresses.mailbox,
chainId: this.domainId,
});
}
/**
@ -102,7 +158,7 @@ export class EvmCoreModule extends HyperlaneModule<
multiProvider: MultiProvider;
chain: ChainNameOrId;
contractVerifier?: ContractVerifier;
}): Promise<DeployedCoreAdresses> {
}): Promise<DeployedCoreAddresses> {
const { config, multiProvider, chain, contractVerifier } = params;
const chainName = multiProvider.getChainName(chain);
@ -167,7 +223,7 @@ export class EvmCoreModule extends HyperlaneModule<
).address;
}
// Deploy Test Receipient
// Deploy Test Recipient
const testRecipient = (
await coreDeployer.deployTestRecipient(
chainName,

@ -505,7 +505,7 @@ export { isCompliant } from './utils/schemas.js';
// @ts-ignore
export { canProposeSafeTransactions, getSafe, getSafeDelegates, getSafeService } from './utils/gnosisSafe.js';
export { DeployedCoreAdresses, EvmCoreModule } from './core/EvmCoreModule.js';
export { EvmCoreModule } from './core/EvmCoreModule.js';
export { EvmIsmModule } from './ism/EvmIsmModule.js';
export { EvmERC20WarpModule } from './token/EvmERC20WarpModule.js';
export { ProxyFactoryFactoriesAddresses } from './deploy/schemas.js';

@ -1,6 +1,5 @@
import {
MailboxClient__factory,
Ownable__factory,
TokenRouter__factory,
} from '@hyperlane-xyz/core';
import { buildArtifact as coreBuildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
@ -12,7 +11,6 @@ import {
addressToBytes32,
assert,
deepEquals,
eqAddress,
isObjEmpty,
normalizeConfig,
rootLogger,
@ -216,22 +214,12 @@ export class EvmERC20WarpModule extends HyperlaneModule<
actualConfig: TokenRouterConfig,
expectedConfig: TokenRouterConfig,
): AnnotatedEV5Transaction[] {
const updateTransactions: AnnotatedEV5Transaction[] = [];
if (!eqAddress(actualConfig.owner, expectedConfig.owner)) {
const { deployedTokenRoute } = this.args.addresses;
const { owner: newOwner } = expectedConfig;
updateTransactions.push({
annotation: `Transferring ownership of Warp route ${deployedTokenRoute} to ${newOwner}`,
chainId: this.domainId,
to: deployedTokenRoute,
data: Ownable__factory.createInterface().encodeFunctionData(
'transferOwnership(address)',
[newOwner],
),
});
}
return updateTransactions;
return EvmERC20WarpModule.createTransferOwnershipTx({
actualOwner: actualConfig.owner,
expectedOwner: expectedConfig.owner,
deployedAddress: this.args.addresses.deployedTokenRoute,
chainId: this.domainId,
});
}
/**

Loading…
Cancel
Save