@nomicfoundation/hardhat-verify
Version:
Hardhat plugin for verifying contracts
157 lines • 8.14 kB
JavaScript
import { assertHardhatInvariant, HardhatError, } from "@nomicfoundation/hardhat-errors";
import { getFullyQualifiedName, parseFullyQualifiedName, } from "hardhat/utils/contract-names";
import { getBuildInfoAndOutput } from "./artifacts.js";
import { formatInferredSolcVersion } from "./metadata.js";
/**
* Resolves on-chain bytecode back to a locally compiled contract,
* either by explicit FQN or by scanning all artifacts.
*
* Throws if:
* - no build info is found;
* - the compiler versions are incompatible;
* - the deployed bytecode doesn’t match;
* - zero or multiple matches in inference mode.
*/
// TODO: add tests once the todos in getBuildInfoAndOutput are resolved.
export class ContractInformationResolver {
#artifacts;
#compatibleSolcVersions;
#networkName;
constructor(artifacts, compatibleSolcVersions, networkName) {
this.#artifacts = artifacts;
this.#compatibleSolcVersions = compatibleSolcVersions;
this.#networkName = networkName;
}
async resolve(contract, deployedBytecode) {
if (contract !== undefined) {
return await this.#resolveByFqn(contract, deployedBytecode);
}
else {
return await this.#resolveByBytecodeLookup(deployedBytecode);
}
}
/**
* Resolves a contract by its fully qualified name by comparing its compiled
* build info against the on-chain bytecode.
*
* @param contract The fully qualified contract name (e.g. "contracts/Token.sol:Token").
* @param deployedBytecode The on-chain bytecode wrapped in a Bytecode instance.
* @returns The matching ContractInformation.
* @throws {HardhatError} with the descriptor:
* - CONTRACT_NOT_FOUND if the artifact for the contract does not exist.
* - BUILD_INFO_NOT_FOUND if no build info is found for the contract.
* - BUILD_INFO_SOLC_VERSION_MISMATCH if the build info’s solc version
* is incompatible with the deployed bytecode.
* - DEPLOYED_BYTECODE_MISMATCH if the compiled and deployed bytecodes
* do not match.
*/
async #resolveByFqn(contract, deployedBytecode) {
const artifactExists = await this.#artifacts.artifactExists(contract);
if (!artifactExists) {
// TODO: we could use HardhatError.ERRORS.CORE.ARTIFACTS.NOT_FOUND
// but we need to build the "suggestion" string, like in #throwNotFoundError
// within the artifacts manager
throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.CONTRACT_NOT_FOUND, {
contract,
});
}
const buildInfoAndOutput = await getBuildInfoAndOutput(this.#artifacts, contract);
if (buildInfoAndOutput === undefined) {
throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.BUILD_INFO_NOT_FOUND, {
contract,
});
}
const isSolcVersionCompatible = this.#compatibleSolcVersions.includes(buildInfoAndOutput.buildInfo.solcVersion);
if (!isSolcVersionCompatible) {
const formattedSolcVersion = formatInferredSolcVersion(deployedBytecode.solcVersion);
const versionDetails = deployedBytecode.hasVersionRange()
? `a Solidity version in the range ${formattedSolcVersion}`
: `the Solidity version ${formattedSolcVersion}`;
throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.BUILD_INFO_SOLC_VERSION_MISMATCH, {
contract,
buildInfoSolcVersion: buildInfoAndOutput.buildInfo.solcVersion,
networkName: this.#networkName,
versionDetails,
});
}
const contractInformation = this.#matchAndBuild(contract, buildInfoAndOutput, deployedBytecode);
if (contractInformation === null) {
throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.DEPLOYED_BYTECODE_MISMATCH, { contractDescription: `the contract "${contract}"` });
}
return contractInformation;
}
/**
* Infers a contract by scanning all artifacts and matching their compiled
* bytecode against the on-chain bytecode.
*
* @param deployedBytecode The on-chain bytecode wrapped in a Bytecode instance.
* @returns The matching ContractInformation.
* @throws {HardhatError} with the descriptor:
* - DEPLOYED_BYTECODE_MISMATCH if no matching contracts are found.
* - DEPLOYED_BYTECODE_MULTIPLE_MATCHES if more than one matching contract
* is found.
*/
async #resolveByBytecodeLookup(deployedBytecode) {
const candidates = await this.#artifacts.getAllFullyQualifiedNames();
const matches = [];
for (const contract of candidates) {
const buildInfoAndOutput = await getBuildInfoAndOutput(this.#artifacts, contract);
if (buildInfoAndOutput === undefined) {
// TODO: can this happen? should we throw an error?
continue;
}
const isSolcVersionCompatible = this.#compatibleSolcVersions.includes(buildInfoAndOutput.buildInfo.solcVersion);
if (!isSolcVersionCompatible) {
continue;
}
const contractInformation = this.#matchAndBuild(contract, buildInfoAndOutput, deployedBytecode);
if (contractInformation !== null) {
matches.push(contractInformation);
}
}
if (matches.length === 0) {
throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.DEPLOYED_BYTECODE_MISMATCH, { contractDescription: "any of your local contracts" });
}
if (matches.length > 1) {
const fqnList = matches.map((c) => ` * ${c.userFqn}`).join("\n");
throw new HardhatError(HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.DEPLOYED_BYTECODE_MULTIPLE_MATCHES, { fqnList });
}
return matches[0];
}
/**
* Compares on-chain bytecode against the compiled deployedBytecode in the
* build output, and assembles a ContractInformation object if they match.
*
* @param contract The fully qualified contract name (e.g. "src/A.sol:MyA").
* @param buildInfoAndOutput An object containing the compiler’s BuildInfo
* and its Output.
* @param deployedBytecode The on-chain bytecode wrapped in a Bytecode instance.
* @returns A ContractInformation object when the compiled and deployed bytecodes
* match, or `null` otherwise.
* @throws {HardhatError} If the compiled contract output or its deployedBytecode
* is missing in the build output.
*/
#matchAndBuild(contract, { buildInfo, buildInfoOutput }, deployedBytecode) {
const { sourceName, contractName } = parseFullyQualifiedName(contract);
const inputSourceName = buildInfo.userSourceNameMap[sourceName];
const compilerOutputContract = buildInfoOutput.output.contracts?.[inputSourceName]?.[contractName];
// TODO: can this happen after validating the artifact and build info? should we throw an error?
assertHardhatInvariant(compilerOutputContract !== undefined, "The compiled contract output was not found in the build info.");
const compilerOutputBytecode = compilerOutputContract?.evm?.deployedBytecode;
// TODO: can this happen after validating the artifact and build info? should we throw an error?
assertHardhatInvariant(compilerOutputBytecode !== undefined, "The deployed bytecode of the compiled contract was not found in the build info.");
if (deployedBytecode.compare(compilerOutputBytecode)) {
return {
compilerInput: buildInfo.input,
solcLongVersion: buildInfo.solcLongVersion,
sourceName,
userFqn: contract,
inputFqn: getFullyQualifiedName(inputSourceName, contractName),
compilerOutputContract,
deployedBytecode: deployedBytecode.bytecode,
};
}
return null;
}
}
//# sourceMappingURL=contract.js.map