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 command for verify warp route contracts #4768

Merged
merged 21 commits into from
Nov 1, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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,
@@ -54,6 +56,7 @@ export const warpCommand: CommandModule = {
.command(init)
.command(read)
.command(send)
.command(verify)
.version(false)
.demandCommand(),

@@ -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
@@ -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> = {};

@@ -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],
});
}
}

4 changes: 2 additions & 2 deletions typescript/cli/src/deploy/core.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -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,
12 changes: 8 additions & 4 deletions typescript/cli/src/deploy/warp.ts
Original file line number Diff line number Diff line change
@@ -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';
@@ -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 ||
@@ -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,
@@ -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(
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
@@ -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,
);
}
}
193 changes: 192 additions & 1 deletion typescript/sdk/src/deploy/verify/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { ethers, utils } from 'ethers';

import { Address, eqAddress } from '@hyperlane-xyz/utils';
import {
ProxyAdmin__factory,
TransparentUpgradeableProxy__factory,
} from '@hyperlane-xyz/core';
import { Address, assert, eqAddress } from '@hyperlane-xyz/utils';

import { ExplorerFamily } from '../../metadata/chainMetadataTypes.js';
import { MultiProvider } from '../../providers/MultiProvider.js';
import { ChainMap, ChainName } from '../../types.js';
import { proxyAdmin, proxyImplementation } from '../proxy.js';

import { ContractVerificationInput } from './types.js';

@@ -84,3 +91,187 @@ export function shouldAddVerificationInput(
existingArtifact.isProxy === artifact.isProxy,
);
}

/**
* Retrieves the constructor args using their respective Explorer and/or RPC (eth_getTransactionByHash)
*/
export async function getConstructorArgumentsApi({
chainName,
contractAddress,
bytecode,
multiProvider,
}: {
bytecode: string;
chainName: string;
contractAddress: string;
multiProvider: MultiProvider;
}): Promise<string> {
const { family } = multiProvider.getExplorerApi(chainName);

let constructorArgs: string;
switch (family) {
case ExplorerFamily.Routescan:
case ExplorerFamily.Etherscan:
constructorArgs = await getEtherscanConstructorArgs({
chainName,
contractAddress,
bytecode,
multiProvider,
});
break;
case ExplorerFamily.Blockscout:
constructorArgs = await getBlockScoutConstructorArgs({
chainName,
contractAddress,
multiProvider,
});
break;
default:
throw new Error(`Explorer Family ${family} unsupported`);
}

return constructorArgs;
}

export async function getEtherscanConstructorArgs({
bytecode,
chainName,
contractAddress,
multiProvider,
}: {
bytecode: string;
chainName: string;
contractAddress: Address;
multiProvider: MultiProvider;
}): Promise<string> {
const { apiUrl: blockExplorerApiUrl, apiKey: blockExplorerApiKey } =
multiProvider.getExplorerApi(chainName);

const url = new URL(blockExplorerApiUrl);
url.searchParams.append('module', 'contract');
url.searchParams.append('action', 'getcontractcreation');
url.searchParams.append('contractaddresses', contractAddress);

if (blockExplorerApiKey)
url.searchParams.append('apikey', blockExplorerApiKey);

const explorerResp = await fetch(url);
const creationTx = (await explorerResp.json()).result[0].txHash;

// Fetch deployment bytecode (includes constructor args)
assert(creationTx, 'Contract creation transaction not found!');
const metadata = multiProvider.getChainMetadata(chainName);
const rpcUrl = metadata.rpcUrls[0].http;

const creationTxResp = await fetch(rpcUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
method: 'eth_getTransactionByHash',
params: [creationTx],
id: 1,
jsonrpc: '2.0',
}),
});

// Truncate the deployment bytecode
const creationInput: string = (await creationTxResp.json()).result.input;
return creationInput.substring(bytecode.length);
}

export async function getBlockScoutConstructorArgs({
chainName,
contractAddress,
multiProvider,
}: {
chainName: string;
contractAddress: Address;
multiProvider: MultiProvider;
}): Promise<string> {
const { apiUrl: blockExplorerApiUrl } =
multiProvider.getExplorerApi(chainName);
const url = new URL(
`/api/v2/smart-contracts/${contractAddress}`,
blockExplorerApiUrl,
);

const smartContractResp = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
});

return (await smartContractResp.json()).constructor_args;
}

export async function getProxyAndAdminInput({
chainName,
multiProvider,
proxyAddress,
}: {
chainName: string;
multiProvider: MultiProvider;
proxyAddress: Address;
}): Promise<{
proxyAdminInput: ContractVerificationInput;
transparentUpgradeableProxyInput: ContractVerificationInput;
}> {
const provider = multiProvider.getProvider(chainName);

const proxyAdminAddress = await proxyAdmin(provider, proxyAddress);
const proxyAdminConstructorArgs = await getConstructorArgumentsApi({
chainName,
multiProvider,
bytecode: ProxyAdmin__factory.bytecode,
contractAddress: proxyAdminAddress,
});
const proxyAdminInput = buildVerificationInput(
'ProxyAdmin',
proxyAdminAddress,
proxyAdminConstructorArgs,
);

const proxyConstructorArgs = await getConstructorArgumentsApi({
chainName,
multiProvider,
contractAddress: proxyAddress,
bytecode: TransparentUpgradeableProxy__factory.bytecode,
});
const transparentUpgradeableProxyInput = buildVerificationInput(
'TransparentUpgradeableProxy',
proxyAddress,
proxyConstructorArgs,
true,
await proxyImplementation(provider, proxyAddress),
);

return { proxyAdminInput, transparentUpgradeableProxyInput };
}

export async function getImplementationInput({
bytecode,
chainName,
contractName,
implementationAddress,
multiProvider,
}: {
bytecode: string;
chainName: string;
contractName: string;
implementationAddress: Address;
multiProvider: MultiProvider;
}): Promise<ContractVerificationInput> {
const implementationConstructorArgs = await getConstructorArgumentsApi({
bytecode,
chainName,
multiProvider,
contractAddress: implementationAddress,
});
return buildVerificationInput(
contractName,
implementationAddress,
implementationConstructorArgs,
);
}
8 changes: 7 additions & 1 deletion typescript/sdk/src/index.ts
Original file line number Diff line number Diff line change
@@ -447,6 +447,7 @@ export { HypERC20Checker } from './token/checker.js';
export { TokenType } from './token/config.js';
export {
HypERC20Factories,
hypERC20contracts,
HypERC721Factories,
TokenFactories,
hypERC20factories,
@@ -532,7 +533,12 @@ export {
} from './utils/gnosisSafe.js';

export { EvmCoreModule } from './core/EvmCoreModule.js';
export { proxyAdmin } from './deploy/proxy.js';
export {
proxyAdmin,
isProxy,
proxyConstructorArgs,
proxyImplementation,
} from './deploy/proxy.js';
export {
ProxyFactoryFactoriesAddresses,
ProxyFactoryFactoriesSchema,