@nomicfoundation/hardhat-verify
Version:
Hardhat plugin for verifying contracts
268 lines (236 loc) • 8.69 kB
text/typescript
import type { InferredSolcVersion } from "./metadata.js";
import type { EthereumProvider } from "hardhat/types/providers";
import type { CompilerOutputBytecode } from "hardhat/types/solidity";
import {
assertHardhatInvariant,
HardhatError,
} from "@nomicfoundation/hardhat-errors";
import {
getUnprefixedHexString,
hexStringToBytes,
} from "@nomicfoundation/hardhat-utils/hex";
import {
getMetadataSectionBytesLength,
inferSolcVersion,
METADATA_LENGTH_FIELD_SIZE,
} from "./metadata.js";
interface ByteOffset {
start: number;
length: number;
}
export class Bytecode {
private constructor(
public readonly bytecode: string,
public readonly solcVersion: InferredSolcVersion,
public readonly executableSection: string,
) {}
static async #parse(bytecode: string): Promise<Bytecode> {
const bytecodeBytes = hexStringToBytes(bytecode);
const solcVersion = await inferSolcVersion(bytecodeBytes);
const executableSection = bytecode.slice(
0,
bytecode.length - getMetadataSectionBytesLength(bytecodeBytes) * 2,
);
return new Bytecode(bytecode, solcVersion, executableSection);
}
public static async getDeployedContractBytecode(
provider: EthereumProvider,
address: string,
networkName: string,
): Promise<Bytecode> {
const response = await provider.request({
method: "eth_getCode",
params: [address, "latest"],
});
assertHardhatInvariant(
typeof response === "string",
"eth_getCode response is not a string",
);
const deployedBytecode = getUnprefixedHexString(response);
if (deployedBytecode === "") {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.DEPLOYED_BYTECODE_NOT_FOUND,
{
address,
networkName,
},
);
}
return await Bytecode.#parse(deployedBytecode);
}
public hasVersionRange(): boolean {
return this.solcVersion.type !== "exact";
}
/**
* Compares the executable sections of the deployed and compiled bytecode,
* ignoring differences in metadata, library link references, immutable
* variables, and call protection placeholders.
*
* This is necessary because deployed bytecode contains dynamically inserted
* values (e.g. actual library addresses), while the compiler output contains
* placeholders. To make the comparison meaningful, both bytecode strings are
* normalized before comparison.
*
* See: https://ethereum.org/en/developers/docs/smart-contracts/verifying/#etherscan
*
* @param compilerOutputBytecode The `evm.deployedBytecode` section of a
* compiled contract.
* @returns `true` if the normalized deployed and compiled bytecode are
* equivalent, `false` otherwise.
*/
public compare(compilerOutputBytecode: CompilerOutputBytecode): boolean {
const unlinkedExecutableSection = inferExecutableSection(
compilerOutputBytecode.object,
);
// If the lengths differ, the bytecodes cannot match, so we can return early
if (this.executableSection.length !== unlinkedExecutableSection.length) {
return false;
}
const normalizedBytecode = nullifyBytecodeOffsets(
this.executableSection,
compilerOutputBytecode,
);
const normalizedUnlinkedBytecode = nullifyBytecodeOffsets(
unlinkedExecutableSection,
compilerOutputBytecode,
);
return normalizedBytecode === normalizedUnlinkedBytecode;
}
}
/**
* Extracts the executable portion of a contract's bytecode without
* decoding it.
*
* Solidity appends metadata to the end of the bytecode. This metadata
* includes a two-byte field indicating its total length. This function
* removes that metadata segment and returns only the executable code.
*
* This approach avoids decoding issues that can occur if the bytecode
* includes linker placeholders or non-hex characters.
*
* @param bytecode The full contract bytecode as a hex string
* (with or without `0x` prefix).
* @returns The hex string of the executable code, excluding metadata.
*/
export function inferExecutableSection(bytecode: string): string {
const rawBytecode = getUnprefixedHexString(bytecode);
// Read the last 2 bytes (4 hex chars) that encode the length of
// the metadata section.
const metadataLengthBytes = hexStringToBytes(
rawBytecode.slice(-METADATA_LENGTH_FIELD_SIZE * 2),
);
// If the bytecode is too short to contain a metadata length field,
// return the entire bytecode.
if (metadataLengthBytes.length !== METADATA_LENGTH_FIELD_SIZE) {
return rawBytecode;
}
const metadataSectionLength =
getMetadataSectionBytesLength(metadataLengthBytes);
// invalid length (metadata + length field doesn't fit)
if (metadataSectionLength * 2 > rawBytecode.length) {
return rawBytecode;
}
return rawBytecode.slice(0, rawBytecode.length - metadataSectionLength * 2);
}
/**
* Replaces all known dynamic offset segments in the bytecode with zeros.
*
* These segments include:
* - Library link references (placeholders for external addresses).
* - Immutable variable references (set during deployment).
* - Call protection patterns (used for things like delegatecall guards).
*
* This is useful for comparing or analyzing bytecode in a normalized form,
* ignoring dynamic values.
*
* @param bytecode The bytecode executable section as a hex string (without
* `0x` prefix).
* @param compilerOutputBytecode The reference compiler output containing
* known offset positions.
* @returns The bytecode string with all known dynamic offsets zeroed out.
*/
export function nullifyBytecodeOffsets(
bytecode: string,
{
object: unlinkedBytecode,
linkReferences,
immutableReferences,
}: CompilerOutputBytecode,
): string {
const dynamicOffsets = [
...getLibraryOffsets(linkReferences),
...getImmutableOffsets(immutableReferences),
...getCallProtectionOffsets(bytecode, unlinkedBytecode),
];
const bytecodeChars = [...bytecode];
for (const { start, length } of dynamicOffsets) {
bytecodeChars.fill("0", start * 2, (start + length) * 2);
}
return bytecodeChars.join("");
}
/**
* Extracts all bytecode offsets where libraries are expected to be linked.
*
* Solidity organizes link references as a nested object:
* `{ sourceFile: { libraryName: [{ start, length }, ...] } }`.
* This function flattens that structure and returns a single list
* of offsets where linking placeholders are present.
*
* @param linkReferences The link references object from compiler output.
* @returns An array of byte offsets for all library link placeholders.
*/
export function getLibraryOffsets(
linkReferences: CompilerOutputBytecode["linkReferences"] = {},
): ByteOffset[] {
return Object.values(linkReferences).flatMap((libraries) =>
Object.values(libraries).flat(),
);
}
/**
* Extracts all bytecode offsets where immutable variables are used.
*
* Immutable variables are inserted into the bytecode at deployment time,
* and the compiler emits their positions in `immutableReferences`.
*
* @param immutableReferences Immutable references from compiler output.
* @returns An array of byte offsets where immutable values will be written.
*/
export function getImmutableOffsets(
immutableReferences: CompilerOutputBytecode["immutableReferences"] = {},
): ByteOffset[] {
return Object.values(immutableReferences).flat();
}
/**
* Detects and returns the offset of the call protection pattern in a library
* bytecode.
*
* Solidity libraries include a call protection mechanism that starts the
* bytecode with `PUSH20 <address>`, a placeholder address (usually all
* zeros) that prevents direct usage.
*
* This function checks if the `referenceBytecode` starts with such a
* placeholder and, if the actual `bytecode` starts with a real `PUSH20`,
* returns the offset where the address starts (always 1).
*
* @param bytecode The bytecode of the contract as a hex string (without
* `0x` prefix).
* @param unlinkedBytecode The compiler output bytecode as a hex string
* (without `0x` prefix).
* @returns An array with a single offset entry if call protection is detected,
* or an empty array otherwise.
*/
export function getCallProtectionOffsets(
bytecode: string,
unlinkedBytecode: string,
): ByteOffset[] {
const PUSH20_OPCODE = "73";
const ADDRESS_LENGTH = 20;
const hasPlaceholderPrefix = unlinkedBytecode.startsWith(
PUSH20_OPCODE + "0".repeat(ADDRESS_LENGTH * 2),
);
const hasRealPrefix = bytecode.startsWith(PUSH20_OPCODE);
if (hasPlaceholderPrefix && hasRealPrefix) {
return [{ start: 1, length: ADDRESS_LENGTH }];
}
return [];
}