feat: cli command for verify warp route contracts (#4768)

### 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
pull/4805/head
Lee 3 weeks ago committed by GitHub
parent db0e735029
commit db5875cc22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 6
      .changeset/shaggy-shrimps-sneeze.md
  2. 25
      typescript/cli/src/commands/warp.ts
  3. 16
      typescript/cli/src/context/context.ts
  4. 4
      typescript/cli/src/deploy/core.ts
  5. 12
      typescript/cli/src/deploy/warp.ts
  6. 132
      typescript/cli/src/verify/warp.ts
  7. 1
      typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts
  8. 193
      typescript/sdk/src/deploy/verify/utils.ts
  9. 8
      typescript/sdk/src/index.ts

@ -0,0 +1,6 @@
---
'@hyperlane-xyz/cli': minor
'@hyperlane-xyz/sdk': minor
---
Add `hyperlane warp verify` to allow post-deployment verification.

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

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

@ -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,

@ -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(

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

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

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

@ -448,6 +448,7 @@ export { HypERC20Checker } from './token/checker.js';
export { TokenType } from './token/config.js';
export {
HypERC20Factories,
hypERC20contracts,
HypERC721Factories,
TokenFactories,
hypERC20factories,
@ -533,7 +534,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,

Loading…
Cancel
Save