diff --git a/.changeset/shaggy-shrimps-sneeze.md b/.changeset/shaggy-shrimps-sneeze.md new file mode 100644 index 0000000000..18ea34ecd1 --- /dev/null +++ b/.changeset/shaggy-shrimps-sneeze.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/cli': minor +'@hyperlane-xyz/sdk': minor +--- + +Add `hyperlane warp verify` to allow post-deployment verification. diff --git a/typescript/cli/src/commands/warp.ts b/typescript/cli/src/commands/warp.ts index b7fb456243..c2f4916b01 100644 --- a/typescript/cli/src/commands/warp.ts +++ b/typescript/cli/src/commands/warp.ts @@ -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 }); + }, +}; diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index 30390f4b40..f9dfb34ab9 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -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, + registry: IRegistry, ): Promise> { const apiKeys: ChainMap = {}; @@ -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], + }); } } diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index f0848458fc..ec1d6b2718 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -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 = {}; if (!skipConfirmation) - apiKeys = await getOrRequestApiKeys([chain], chainMetadata); + apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry); const deploymentParams: DeployParams = { context, diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 38a5e14f27..984d46235b 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -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 = {}; 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 = {}; if (!skipConfirmation) - apiKeys = await getOrRequestApiKeys(chains, chainMetadata); + apiKeys = await requestAndSaveApiKeys( + chains, + chainMetadata, + context.registry, + ); const transactions: AnnotatedEV5Transaction[] = [ ...(await extendWarpRoute( diff --git a/typescript/cli/src/verify/warp.ts b/typescript/cli/src/verify/warp.ts new file mode 100644 index 0000000000..f350e2aae6 --- /dev/null +++ b/typescript/cli/src/verify/warp.ts @@ -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 = {}; + + let apiKeys: ChainMap = {}; + 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; + + const factory = objFilter( + hypERC20factories, + (t, _contract): _contract is any => t === tokenType, + )[tokenType]; + + return { factory, tokenType }; +} diff --git a/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts b/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts index ab739e8cfc..b657b8e7e6 100644 --- a/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts +++ b/typescript/sdk/src/deploy/verify/PostDeploymentContractVerifier.ts @@ -54,6 +54,7 @@ export class PostDeploymentContractVerifier extends MultiGeneric { + 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 { + 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 { + 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 { + const implementationConstructorArgs = await getConstructorArgumentsApi({ + bytecode, + chainName, + multiProvider, + contractAddress: implementationAddress, + }); + return buildVerificationInput( + contractName, + implementationAddress, + implementationConstructorArgs, + ); +} diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 7fb3c1729d..4688acd28c 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -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,