@nomicfoundation/hardhat-verify
Version:
Hardhat plugin for verifying contracts
379 lines (341 loc) • 12.6 kB
text/typescript
import type { ContractInformation } from "./contract.js";
import { HardhatError } from "@nomicfoundation/hardhat-errors";
import { isAddress } from "@nomicfoundation/hardhat-utils/eth";
import { getPrefixedHexString } from "@nomicfoundation/hardhat-utils/hex";
import { parseFullyQualifiedName } from "hardhat/utils/contract-names";
export interface LibraryInformation {
libraries: SourceLibraries;
undetectableLibraries: string[];
}
export type SourceLibraries = Record<
/* source file name */ string,
LibraryAddresses
>;
export type LibraryAddresses = Record<
/* library name */ string,
/* address */ string
>;
/**
* Resolves all required library information for contract verification.
*
* Processes the link references from the contract's creation bytecode and the
* deployed bytecode to determine which libraries are used, which can be
* detected automatically, and which must be provided by the user. It merges
* user-specified library addresses with those detected from the bytecode,
* ensuring there are no conflicts or duplicates.
*
* @param contractInformation Information about the contract, including
* compiler output and deployed bytecode.
* @param userLibraryAddresses Mapping of user-specified library names (short
* or FQN) to addresses.
* @returns An object containing all resolved library addresses and the list of
* undetectable libraries.
* - libraries: Mapping of source names to library names to addresses.
* - undetectableLibraries: Array of library FQNs that cannot be detected
* automatically.
* @throws {HardhatError} with the descriptor:
* - INVALID_LIBRARY_ADDRESS if a user-provided library address is invalid.
* - DUPLICATED_LIBRARY if the same library is specified more than once
* (by name or FQN).
* - UNUSED_LIBRARY if a specified library is not used by the contract.
* - LIBRARY_MULTIPLE_MATCHES if a provided library name is ambiguous
* (matches multiple libraries).
* - LIBRARY_ADDRESSES_MISMATCH if a library address provided by the user
* conflicts with an address detected from the bytecode.
* - MISSING_LIBRARY_ADDRESSES if any required library addresses are
* missing.
*/
export function resolveLibraryInformation(
contractInformation: ContractInformation,
userLibraryAddresses: LibraryAddresses,
): LibraryInformation {
const bytecodeLinkReferences =
contractInformation.compilerOutputContract.evm?.bytecode?.linkReferences ??
{};
const deployedBytecodeLinkReferences =
contractInformation.compilerOutputContract.evm?.deployedBytecode
?.linkReferences ?? {};
const allLibraryFqns = getLibraryFqns(bytecodeLinkReferences);
const detectableLibraryFqns = getLibraryFqns(deployedBytecodeLinkReferences);
const undetectableLibraryFqns = allLibraryFqns.filter(
(lib) => !detectableLibraryFqns.some((detLib) => detLib === lib),
);
const userLibraries = resolveUserLibraries(
allLibraryFqns,
detectableLibraryFqns,
undetectableLibraryFqns,
userLibraryAddresses,
contractInformation.userFqn,
);
const detectableLibraries = getDetectableLibrariesFromBytecode(
deployedBytecodeLinkReferences,
contractInformation.deployedBytecode,
);
const mergedLibraries = mergeLibraries(userLibraries, detectableLibraries);
const mergedLibraryFqns = getLibraryFqns(mergedLibraries);
if (mergedLibraryFqns.length < allLibraryFqns.length) {
const missingLibraries = allLibraryFqns.filter(
(lib) => !mergedLibraryFqns.some((mergedLib) => lib === mergedLib),
);
const missingList = missingLibraries.map((x) => ` * ${x}`).join("\n");
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.MISSING_LIBRARY_ADDRESSES,
{
contract: contractInformation.userFqn,
missingList,
},
);
}
return {
libraries: mergedLibraries,
undetectableLibraries: undetectableLibraryFqns,
};
}
/**
* Returns a list of fully qualified library names in the format `source:library`.
*
* @param libraries Nested mapping from source name to library names.
* @returns An array of fully qualified names (e.g., "contracts/Lib.sol:MyLib").
*/
export function getLibraryFqns(
libraries: Record<string, Record<string, unknown>>,
): string[] {
return Object.entries(libraries).flatMap(([sourceName, sourceLibraries]) =>
Object.keys(sourceLibraries).map(
(libraryName) => `${sourceName}:${libraryName}`,
),
);
}
/**
* Resolves and validates user-specified libraries for contract verification.
*
* Matches each user-provided library name or FQN to the contract's required
* libraries, ensures the address is valid, checks for ambiguity, and prevents
* duplicates.
*
* @param allLibraryFqns All fully qualified library names (`source:library`)
* used by the contract, derived from the keys in the `linkReferences`
* mapping of the contract’s creation bytecode. Includes both detectable and
* undetectable libraries.
* @param detectableLibraryFqns Fully qualified library names present in the
* `linkReferences` mapping of the deployed bytecode. These libraries can be
* detected automatically and do not require user-specified addresses.
* @param undetectableLibraryFqns Fully qualified library names found in the
* creation bytecode’s `linkReferences` but not in the deployed bytecode’s
* `linkReferences`. These must be specified by the user.
* @param userLibraryAddresses User-provided mapping from library name (short
* or FQN) to address.
* @param contract Fully qualified name of the contract.
* @returns A mapping of source names to library names to addresses.
* @throws {HardhatError} with the descriptor:
* - INVALID_LIBRARY_ADDRESS if a user-provided library address is invalid.
* - DUPLICATED_LIBRARY if the same library is specified more than once
* (by name or FQN).
* - UNUSED_LIBRARY if a specified library is not used by the contract.
* - LIBRARY_MULTIPLE_MATCHES if a provided library name is ambiguous
* (matches multiple libraries).
*/
export function resolveUserLibraries(
allLibraryFqns: string[],
detectableLibraryFqns: string[],
undetectableLibraryFqns: string[],
userLibraryAddresses: LibraryAddresses,
contract: string,
): SourceLibraries {
const seenLibraryFqns: Set<string> = new Set();
const resolvedLibraries: SourceLibraries = {};
for (const [userLibName, userLibAddress] of Object.entries(
userLibraryAddresses,
)) {
// TODO: we should move this validation to the arg resolution step
if (!isAddress(userLibAddress)) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.INVALID_LIBRARY_ADDRESS,
{
contract,
library: userLibName,
address: userLibAddress,
},
);
}
const foundLibraryFqn = lookupLibrary(
allLibraryFqns,
detectableLibraryFqns,
undetectableLibraryFqns,
userLibName,
contract,
);
const { sourceName: foundLibSource, contractName: foundLibName } =
parseFullyQualifiedName(foundLibraryFqn);
// The only way for this library to be already mapped is for it to be given
// twice in the libraries user input: once as a library name and another as
// a fully qualified library name
if (seenLibraryFqns.has(foundLibraryFqn)) {
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.DUPLICATED_LIBRARY,
{
library: foundLibName,
libraryFqn: foundLibraryFqn,
},
);
}
seenLibraryFqns.add(foundLibraryFqn);
if (resolvedLibraries[foundLibSource] === undefined) {
resolvedLibraries[foundLibSource] = {};
}
resolvedLibraries[foundLibSource][foundLibName] = userLibAddress;
}
return resolvedLibraries;
}
/**
* Searches for a library by its name or fully qualified name in the array of
* all libraries. Throws an error if the library is not found or if multiple
* libraries match the name.
*/
export function lookupLibrary(
allLibraryFqns: string[],
detectableLibraryFqns: string[],
undetectableLibraryFqns: string[],
libraryName: string,
contract: string,
): string {
const matchingLibraries = allLibraryFqns.filter(
(libFqn) => libFqn === libraryName || libFqn.split(":")[1] === libraryName,
);
if (matchingLibraries.length === 0) {
const lines = [];
if (undetectableLibraryFqns.length > 0) {
lines.push(...undetectableLibraryFqns.map((x) => ` * ${x}`));
}
if (detectableLibraryFqns.length > 0) {
lines.push(
...detectableLibraryFqns.map((x) => ` * ${x} (optional)`),
"Libraries marked as optional don't need to be specified since their addresses are autodetected by the plugin.",
);
}
const suggestion =
allLibraryFqns.length > 0
? [
"This contract uses the following external libraries:",
...lines,
].join("\n")
: "This contract doesn't use any external libraries.";
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.UNUSED_LIBRARY,
{
contract,
library: libraryName,
suggestion,
},
);
}
if (matchingLibraries.length > 1) {
const fqnList = matchingLibraries.map((x) => ` * ${x}`).join("\n");
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.LIBRARY_MULTIPLE_MATCHES,
{
contract,
library: libraryName,
fqnList,
},
);
}
return matchingLibraries[0];
}
/**
* Extracts linked library addresses from deployed bytecode using link
* references. Assumes all offsets for a given library will have the same
* address and only reads the first occurrence.
*/
export function getDetectableLibrariesFromBytecode(
linkReferences:
| {
[sourceName: string]: {
[libraryName: string]: Array<{
start: number;
length: 20;
}>;
};
}
| undefined = {},
deployedBytecode: string,
): SourceLibraries {
const sourceLibraries: SourceLibraries = {};
for (const [sourceName, libOffsets] of Object.entries(linkReferences)) {
if (sourceLibraries[sourceName] === undefined) {
sourceLibraries[sourceName] = {};
}
for (const [libName, [{ start, length }]] of Object.entries(libOffsets)) {
sourceLibraries[sourceName][libName] = getPrefixedHexString(
deployedBytecode.slice(start * 2, (start + length) * 2),
);
}
}
return sourceLibraries;
}
/**
* Merges user-specified and detected library addresses, ensuring there are no
* conflicts. Throws if a library address is provided by the user and also
* detected, but the addresses do not match.
*/
export function mergeLibraries(
userLibraries: SourceLibraries,
detectedLibraries: SourceLibraries,
): SourceLibraries {
const conflicts: Array<{
library: string;
detectedAddress: string;
userAddress: string;
}> = [];
for (const [sourceName, userLibAddresses] of Object.entries(userLibraries)) {
for (const [userLibName, userLibAddress] of Object.entries(
userLibAddresses,
)) {
if (
sourceName in detectedLibraries &&
userLibName in detectedLibraries[sourceName]
) {
const detectedLibAddress = detectedLibraries[sourceName][userLibName];
// detectedLibAddress may not always be lowercase due to extraction logic
if (userLibAddress.toLowerCase() !== detectedLibAddress.toLowerCase()) {
conflicts.push({
library: `${sourceName}:${userLibName}`,
detectedAddress: detectedLibAddress,
userAddress: userLibAddress,
});
}
}
}
}
if (conflicts.length > 0) {
const conflictList = conflicts
.map(
({ library, userAddress, detectedAddress }) =>
` * ${library}\ngiven address: ${userAddress}\ndetected address: ${detectedAddress}`,
)
.join("\n");
throw new HardhatError(
HardhatError.ERRORS.HARDHAT_VERIFY.GENERAL.LIBRARY_ADDRESSES_MISMATCH,
{
conflictList,
},
);
}
const merge = (
targetLibraries: SourceLibraries,
sourceLibraries: SourceLibraries,
) => {
for (const [sourceName, sourceLibAddresses] of Object.entries(
sourceLibraries,
)) {
targetLibraries[sourceName] = {
...targetLibraries[sourceName],
...sourceLibAddresses,
};
}
};
const mergedLibraries: SourceLibraries = {};
merge(mergedLibraries, userLibraries);
merge(mergedLibraries, detectedLibraries);
return mergedLibraries;
}