Skip to content

Commit

Permalink
feat: cli command for verify warp route contracts (#4768)
Browse files Browse the repository at this point in the history
### Description
Uses warp artifacts to derive the verification artifacts from etherscan
and RPC api.

usage: `hyperlane warp verify --symbol`

### Drive-by
- Write to registry when blockexplorer api keys are added

### Backward compatibility
Yes

### Testing
Manual
  • Loading branch information
ltyu authored Nov 1, 2024
1 parent db0e735 commit db5875c
Show file tree
Hide file tree
Showing 9 changed files with 388 additions and 9 deletions.
6 changes: 6 additions & 0 deletions .changeset/shaggy-shrimps-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---

Add `hyperlane warp verify` to allow post-deployment verification.
25 changes: 25 additions & 0 deletions typescript/cli/src/commands/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
writeYamlOrJson,
} from '../utils/files.js';
import { getWarpCoreConfigOrExit } from '../utils/input.js';
import { selectRegistryWarpRoute } from '../utils/tokens.js';
import { runVerifyWarpRoute } from '../verify/warp.js';

import {
DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH,
Expand Down Expand Up @@ -54,6 +56,7 @@ export const warpCommand: CommandModule = {
.command(init)
.command(read)
.command(send)
.command(verify)
.version(false)
.demandCommand(),

Expand Down Expand Up @@ -334,3 +337,25 @@ export const check: CommandModuleWithContext<{
process.exit(0);
},
};

export const verify: CommandModuleWithWriteContext<{
symbol: string;
}> = {
command: 'verify',
describe: 'Verify deployed contracts on explorers',
builder: {
symbol: {
...symbolCommandOption,
demandOption: false,
},
},
handler: async ({ context, symbol }) => {
logCommandHeader('Hyperlane Warp Verify');
const warpCoreConfig = await selectRegistryWarpRoute(
context.registry,
symbol,
);

return runVerifyWarpRoute({ context, warpCoreConfig });
},
};
16 changes: 15 additions & 1 deletion typescript/cli/src/context/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,18 @@ async function getMultiProvider(registry: IRegistry, signer?: ethers.Signer) {
return multiProvider;
}

export async function getOrRequestApiKeys(
/**
* Requests and saves Block Explorer API keys for the specified chains, prompting the user if necessary.
*
* @param chains - The list of chain names to request API keys for.
* @param chainMetadata - The chain metadata, used to determine if an API key is already configured.
* @param registry - The registry used to update the chain metadata with the new API key.
* @returns A mapping of chain names to their API keys.
*/
export async function requestAndSaveApiKeys(
chains: ChainName[],
chainMetadata: ChainMap<ChainMetadata>,
registry: IRegistry,
): Promise<ChainMap<string>> {
const apiKeys: ChainMap<string> = {};

Expand Down Expand Up @@ -218,6 +227,11 @@ export async function getOrRequestApiKeys(
`${chain} api key`,
`${chain} metadata blockExplorers config`,
);
chainMetadata[chain].blockExplorers![0].apiKey = apiKeys[chain];
await registry.updateChain({
chainName: chain,
metadata: chainMetadata[chain],
});
}
}

Expand Down
4 changes: 2 additions & 2 deletions typescript/cli/src/deploy/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from '@hyperlane-xyz/sdk';

import { MINIMUM_CORE_DEPLOY_GAS } from '../consts.js';
import { getOrRequestApiKeys } from '../context/context.js';
import { requestAndSaveApiKeys } from '../context/context.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logBlue, logGray, logGreen } from '../logger.js';
import { runSingleChainSelectionStep } from '../utils/chains.js';
Expand Down Expand Up @@ -64,7 +64,7 @@ export async function runCoreDeploy(params: DeployParams) {

let apiKeys: ChainMap<string> = {};
if (!skipConfirmation)
apiKeys = await getOrRequestApiKeys([chain], chainMetadata);
apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry);

const deploymentParams: DeployParams = {
context,
Expand Down
12 changes: 8 additions & 4 deletions typescript/cli/src/deploy/warp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import {

import { readWarpRouteDeployConfig } from '../config/warp.js';
import { MINIMUM_WARP_DEPLOY_GAS } from '../consts.js';
import { getOrRequestApiKeys } from '../context/context.js';
import { requestAndSaveApiKeys } from '../context/context.js';
import { WriteCommandContext } from '../context/types.js';
import { log, logBlue, logGray, logGreen, logTable } from '../logger.js';
import { getSubmitterBuilder } from '../submit/submit.js';
Expand Down Expand Up @@ -100,7 +100,7 @@ export async function runWarpRouteDeploy({
context: WriteCommandContext;
warpRouteDeploymentConfigPath?: string;
}) {
const { signer, skipConfirmation, chainMetadata } = context;
const { signer, skipConfirmation, chainMetadata, registry } = context;

if (
!warpRouteDeploymentConfigPath ||
Expand All @@ -127,7 +127,7 @@ export async function runWarpRouteDeploy({

let apiKeys: ChainMap<string> = {};
if (!skipConfirmation)
apiKeys = await getOrRequestApiKeys(chains, chainMetadata);
apiKeys = await requestAndSaveApiKeys(chains, chainMetadata, registry);

const deploymentParams = {
context,
Expand Down Expand Up @@ -446,7 +446,11 @@ export async function runWarpRouteApply(

let apiKeys: ChainMap<string> = {};
if (!skipConfirmation)
apiKeys = await getOrRequestApiKeys(chains, chainMetadata);
apiKeys = await requestAndSaveApiKeys(
chains,
chainMetadata,
context.registry,
);

const transactions: AnnotatedEV5Transaction[] = [
...(await extendWarpRoute(
Expand Down
132 changes: 132 additions & 0 deletions typescript/cli/src/verify/warp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { ContractFactory } from 'ethers';

import { buildArtifact } from '@hyperlane-xyz/core/buildArtifact.js';
import {
ChainMap,
EvmERC20WarpRouteReader,
ExplorerLicenseType,
MultiProvider,
PostDeploymentContractVerifier,
TokenType,
VerificationInput,
WarpCoreConfig,
hypERC20contracts,
hypERC20factories,
isProxy,
proxyImplementation,
verificationUtils,
} from '@hyperlane-xyz/sdk';
import { Address, assert, objFilter } from '@hyperlane-xyz/utils';

import { requestAndSaveApiKeys } from '../context/context.js';
import { CommandContext } from '../context/types.js';
import { logBlue, logGray, logGreen } from '../logger.js';

// Zircuit does not have an external API: https://docs.zircuit.com/dev-tools/block-explorer
const UNSUPPORTED_CHAINS = ['zircuit'];

export async function runVerifyWarpRoute({
context,
warpCoreConfig,
}: {
context: CommandContext;
warpCoreConfig: WarpCoreConfig;
}) {
const { multiProvider, chainMetadata, registry, skipConfirmation } = context;

const verificationInputs: ChainMap<VerificationInput> = {};

let apiKeys: ChainMap<string> = {};
if (!skipConfirmation)
apiKeys = await requestAndSaveApiKeys(
warpCoreConfig.tokens.map((t) => t.chainName),
chainMetadata,
registry,
);

for (const token of warpCoreConfig.tokens) {
const { chainName } = token;
verificationInputs[chainName] = [];

if (UNSUPPORTED_CHAINS.includes(chainName)) {
logBlue(`Unsupported chain ${chainName}. Skipping.`);
continue;
}
assert(token.addressOrDenom, 'Invalid addressOrDenom');

const provider = multiProvider.getProvider(chainName);
const isProxyContract = await isProxy(provider, token.addressOrDenom);

logGray(`Getting constructor args for ${chainName} using explorer API`);

// Verify Implementation first because Proxy won't verify without it.
const deployedContractAddress = isProxyContract
? await proxyImplementation(provider, token.addressOrDenom)
: token.addressOrDenom;

const { factory, tokenType } = await getWarpRouteFactory(
multiProvider,
chainName,
deployedContractAddress,
);
const contractName = hypERC20contracts[tokenType];
const implementationInput = await verificationUtils.getImplementationInput({
chainName,
contractName,
multiProvider,
bytecode: factory.bytecode,
implementationAddress: deployedContractAddress,
});
verificationInputs[chainName].push(implementationInput);

// Verify Proxy and ProxyAdmin
if (isProxyContract) {
const { proxyAdminInput, transparentUpgradeableProxyInput } =
await verificationUtils.getProxyAndAdminInput({
chainName,
multiProvider,
proxyAddress: token.addressOrDenom,
});

verificationInputs[chainName].push(proxyAdminInput);
verificationInputs[chainName].push(transparentUpgradeableProxyInput);
}
}

logBlue(`All explorer constructor args successfully retrieved. Verifying...`);
const verifier = new PostDeploymentContractVerifier(
verificationInputs,
context.multiProvider,
apiKeys,
buildArtifact,
ExplorerLicenseType.MIT,
);

await verifier.verify();

return logGreen('Finished contract verification');
}

async function getWarpRouteFactory(
multiProvider: MultiProvider,
chainName: string,
warpRouteAddress: Address,
): Promise<{
factory: ContractFactory;
tokenType: Exclude<
TokenType,
TokenType.syntheticUri | TokenType.collateralUri
>;
}> {
const warpRouteReader = new EvmERC20WarpRouteReader(multiProvider, chainName);
const tokenType = (await warpRouteReader.deriveTokenType(
warpRouteAddress,
)) as Exclude<TokenType, TokenType.syntheticUri | TokenType.collateralUri>;

const factory = objFilter(
hypERC20factories,
(t, _contract): _contract is any => t === tokenType,
)[tokenType];

return { factory, tokenType };
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ export class PostDeploymentContractVerifier extends MultiGeneric<VerificationInp
this.logger.error(
{ name: input.name, address: input.address },
`Failed to verify contract on ${chain}`,
error,
);
}
}
Expand Down
Loading

0 comments on commit db5875c

Please sign in to comment.