ensure HYP_KEY & --key are only used for PKs & let user pass --from-address from dry-runs (#3743)

### Description

* This change ensures `HYP_KEY` and `--key` are only used for a
developer's private key, and allows a user to pass in a `--from-address`
for dry-runs (everything defaults to `HYP_KEY`, if sourced)

### Drive-by changes

- Improved DX by removing incorrect log statement after insufficient
balance check
```
Before:
   Signer is valid
  ? WARNING: 0x2d54EC30Be0D2583b0d781e10B31905c3c54616d has low balance on alfajores. At least 1.0 recommended but found 0.0 CELO Continue? yes
   Balances are sufficient

After:
   Signer is valid
  ? WARNING: 0x2d54EC30Be0D2583b0d781e10B31905c3c54616d has low balance on alfajores. At least 1.0 recommended but found 0.0 CELO Continue? yes
```

### Related issues

* Fixes: https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3736

### Backward compatibility

* Yes

### Testing

* Manual
pull/3785/head
Noah Bayindirli 🥂 6 months ago committed by GitHub
parent e37bd8abf9
commit ff221f66a0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 5
      .changeset/olive-apricots-develop.md
  2. 5
      typescript/cli/ci-test.sh
  3. 7
      typescript/cli/src/commands/deploy.ts
  4. 12
      typescript/cli/src/commands/options.ts
  5. 24
      typescript/cli/src/context/context.ts
  6. 1
      typescript/cli/src/context/types.ts
  7. 6
      typescript/cli/src/deploy/utils.ts
  8. 20
      typescript/cli/src/utils/balances.ts
  9. 26
      typescript/cli/src/utils/keys.ts

@ -0,0 +1,5 @@
---
'@hyperlane-xyz/cli': minor
---
Allows a developer to pass a private key or address to dry-run, and ensures HYP_KEY is only used for private keys.

@ -144,12 +144,11 @@ run_hyperlane_deploy_core_dry_run() {
echo -e "\nDry-running contract deployments to Alfajores"
yarn workspace @hyperlane-xyz/cli run hyperlane deploy core \
--dry-run alfajores \
--targets alfajores \
--registry ${TEST_CONFIGS_PATH}/dry-run \
--overrides " " \
$(if [ "$HOOK_FLAG" == "true" ]; then echo "--hook ${EXAMPLES_PATH}/hooks.yaml"; fi) \
--ism ${TEST_CONFIGS_PATH}/dry-run/ism.yaml \
--key 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--from-address 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
check_deployer_balance;
@ -167,7 +166,7 @@ run_hyperlane_deploy_warp_dry_run() {
--dry-run alfajores \
--overrides ${TEST_CONFIGS_PATH}/dry-run \
--config ${TEST_CONFIGS_PATH}/dry-run/warp-route-deployment.yaml \
--key 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--from-address 0xfaD1C94469700833717Fa8a3017278BC1cA8031C \
--yes
check_deployer_balance;

@ -15,6 +15,7 @@ import {
agentTargetsCommandOption,
coreTargetsCommandOption,
dryRunOption,
fromAddressCommandOption,
hookCommandOption,
ismCommandOption,
originCommandOption,
@ -73,6 +74,7 @@ const coreCommand: CommandModuleWithWriteContext<{
ism?: string;
hook?: string;
'dry-run': string;
'from-address': string;
agent: string;
}> = {
command: 'core',
@ -83,12 +85,13 @@ const coreCommand: CommandModuleWithWriteContext<{
hook: hookCommandOption,
agent: agentConfigCommandOption(false, './configs/agent.json'),
'dry-run': dryRunOption,
'from-address': fromAddressCommandOption,
},
handler: async ({ context, targets, ism, hook, agent, dryRun }) => {
logGray(
`Hyperlane permissionless core deployment${dryRun ? ' dry-run' : ''}`,
);
logGray('------------------------------------------------');
logGray(`------------------------------------------------`);
try {
const chains = targets?.split(',').map((r: string) => r.trim());
@ -113,12 +116,14 @@ const coreCommand: CommandModuleWithWriteContext<{
const warpCommand: CommandModuleWithWriteContext<{
config: string;
'dry-run': string;
'from-address': string;
}> = {
command: 'warp',
describe: 'Deploy Warp Route contracts',
builder: {
config: warpConfigCommandOption,
'dry-run': dryRunOption,
'from-address': fromAddressCommandOption,
},
handler: async ({ context, config, dryRun }) => {
logGray(`Hyperlane warp route deployment${dryRun ? ' dry-run' : ''}`);

@ -42,9 +42,9 @@ export const skipConfirmationOption: Options = {
export const keyCommandOption: Options = {
type: 'string',
description: `A hex private key or seed phrase for transaction signing, or use the HYP_KEY env var.
Dry-run: An address to simulate transaction signing on a forked network`,
alias: 'k',
description:
'A hex private key or seed phrase for transaction signing, or use the HYP_KEY env var.',
alias: ['k', 'private-key', 'seed-phrase'],
default: ENV.HYP_KEY,
defaultDescription: 'process.env.HYP_KEY',
};
@ -120,6 +120,12 @@ export const inputFileOption: Options = {
demandOption: true,
};
export const fromAddressCommandOption: Options = {
type: 'string',
description: `An address to simulate transaction signing on a forked network`,
alias: 'f',
};
export const dryRunOption: Options = {
type: 'string',
description:

@ -24,9 +24,14 @@ export async function contextMiddleware(argv: Record<string, any>) {
registryUri: argv.registry,
registryOverrideUri: argv.overrides,
key: argv.key,
fromAddress: argv.fromAddress,
requiresKey,
skipConfirmation: argv.yes,
};
if (!isDryRun && settings.fromAddress)
throw new Error(
"'--from-address' or '-f' should only be used for dry-runs",
);
const context = isDryRun
? await getDryRunContext(settings, argv.dryRun)
: await getContext(settings);
@ -67,7 +72,13 @@ export async function getContext({
* @returns dry-run context for the current command
*/
export async function getDryRunContext(
{ registryUri, registryOverrideUri, key, skipConfirmation }: ContextSettings,
{
registryUri,
registryOverrideUri,
key,
fromAddress,
skipConfirmation,
}: ContextSettings,
chain?: ChainName,
): Promise<CommandContext> {
const registry = getRegistry(registryUri, registryOverrideUri, true);
@ -86,11 +97,12 @@ export async function getDryRunContext(
const multiProvider = await getMultiProvider(registry);
await forkNetworkToMultiProvider(multiProvider, chain);
const { key: impersonatedKey, signer: impersonatedSigner } =
await getImpersonatedSigner({
key,
skipConfirmation,
});
const { impersonatedKey, impersonatedSigner } = await getImpersonatedSigner({
fromAddress,
key,
skipConfirmation,
});
multiProvider.setSharedSigner(impersonatedSigner);
return {

@ -12,6 +12,7 @@ export interface ContextSettings {
registryUri: string;
registryOverrideUri: string;
key?: string;
fromAddress?: string;
requiresKey?: boolean;
skipConfirmation?: boolean;
}

@ -12,7 +12,7 @@ import { Address, ProtocolType } from '@hyperlane-xyz/utils';
import { parseIsmConfig } from '../config/ism.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logGreen, logPink } from '../logger.js';
import { assertGasBalances } from '../utils/balances.js';
import { gasBalancesAreSufficient } from '../utils/balances.js';
import { ENV } from '../utils/env.js';
import { assertSigner } from '../utils/keys.js';
@ -76,13 +76,13 @@ export async function runPreflightChecksForChains({
assertSigner(signer);
logGreen('✅ Signer is valid');
await assertGasBalances(
const sufficient = await gasBalancesAreSufficient(
multiProvider,
signer,
chainsToGasCheck ?? chains,
minGas,
);
logGreen('✅ Balances are sufficient');
if (sufficient) logGreen('✅ Balances are sufficient');
}
// from parsed types

@ -3,14 +3,15 @@ import { ethers } from 'ethers';
import { ChainName, MultiProvider } from '@hyperlane-xyz/sdk';
export async function assertNativeBalances(
export async function nativeBalancesAreSufficient(
multiProvider: MultiProvider,
signer: ethers.Signer,
chains: ChainName[],
minBalanceWei: string,
) {
): Promise<boolean> {
const address = await signer.getAddress();
const minBalance = ethers.utils.formatEther(minBalanceWei.toString());
let sufficient = true;
await Promise.all(
chains.map(async (chain) => {
const balanceWei = await multiProvider
@ -25,23 +26,32 @@ export async function assertNativeBalances(
message: `WARNING: ${error} Continue?`,
});
if (!isResume) throw new Error(error);
sufficient = false;
}
}),
);
return sufficient;
}
export async function assertGasBalances(
export async function gasBalancesAreSufficient(
multiProvider: MultiProvider,
signer: ethers.Signer,
chains: ChainName[],
minGas: string,
) {
): Promise<boolean> {
let sufficient = true;
await Promise.all(
chains.map(async (chain) => {
const provider = multiProvider.getProvider(chain);
const gasPrice = await provider.getGasPrice();
const minBalanceWei = gasPrice.mul(minGas).toString();
await assertNativeBalances(multiProvider, signer, [chain], minBalanceWei);
sufficient = await nativeBalancesAreSufficient(
multiProvider,
signer,
[chain],
minBalanceWei,
);
}),
);
return sufficient;
}

@ -5,8 +5,6 @@ import { impersonateAccount } from '@hyperlane-xyz/sdk';
import { Address, ensure0x } from '@hyperlane-xyz/utils';
const ETHEREUM_ADDRESS_LENGTH = 42;
const DEFAULT_KEY_TYPE = 'private key';
const IMPERSONATED_KEY_TYPE = 'address';
/**
* Retrieves a signer for the current command-context.
@ -19,7 +17,7 @@ export async function getSigner({
key?: string;
skipConfirmation?: boolean;
}) {
key ||= await retrieveKey(DEFAULT_KEY_TYPE, skipConfirmation);
key ||= await retrieveKey(skipConfirmation);
const signer = privateKeyToSigner(key);
return { key, signer };
}
@ -29,15 +27,22 @@ export async function getSigner({
* @returns the impersonated signer
*/
export async function getImpersonatedSigner({
fromAddress,
key,
skipConfirmation,
}: {
fromAddress?: Address;
key?: string;
skipConfirmation?: boolean;
}) {
key ||= await retrieveKey(IMPERSONATED_KEY_TYPE, skipConfirmation);
const signer = await addressToImpersonatedSigner(key);
return { key, signer };
if (!fromAddress) {
const { signer } = await getSigner({ key, skipConfirmation });
fromAddress = signer.address;
}
return {
impersonatedKey: fromAddress,
impersonatedSigner: await addressToImpersonatedSigner(fromAddress),
};
}
/**
@ -61,9 +66,7 @@ async function addressToImpersonatedSigner(
const formattedKey = address.trim().toLowerCase();
if (address.length != ETHEREUM_ADDRESS_LENGTH)
throw new Error(
'Invalid address length. Please ensure you are passing an address and not a private key.',
);
throw new Error('Invalid address length.');
else if (ethers.utils.isHexString(ensure0x(formattedKey)))
return await impersonateAccount(address);
else throw new Error('Invalid address format');
@ -86,12 +89,11 @@ function privateKeyToSigner(key: string): ethers.Wallet {
}
async function retrieveKey(
keyType: string,
skipConfirmation: boolean | undefined,
): Promise<string> {
if (skipConfirmation) throw new Error(`No ${keyType} provided`);
if (skipConfirmation) throw new Error(`No private key provided`);
else
return await input({
message: `Please enter ${keyType} or use the HYP_KEY environment variable.`,
message: `Please enter private key or use the HYP_KEY environment variable.`,
});
}

Loading…
Cancel
Save