UNPKG

@nomicfoundation/hardhat-verify

Version:
379 lines (341 loc) 12.6 kB
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; }