UNPKG

@nomicfoundation/hardhat-verify

Version:
227 lines 11.7 kB
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"; /** * 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, userLibraryAddresses) { 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) { 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, detectableLibraryFqns, undetectableLibraryFqns, userLibraryAddresses, contract) { const seenLibraryFqns = new Set(); const resolvedLibraries = {}; 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, detectableLibraryFqns, undetectableLibraryFqns, libraryName, contract) { 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 = {}, deployedBytecode) { const 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, detectedLibraries) { const conflicts = []; 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) => { for (const [sourceName, sourceLibAddresses] of Object.entries(sourceLibraries)) { targetLibraries[sourceName] = { ...targetLibraries[sourceName], ...sourceLibAddresses, }; } }; const mergedLibraries = {}; merge(mergedLibraries, userLibraries); merge(mergedLibraries, detectedLibraries); return mergedLibraries; } //# sourceMappingURL=libraries.js.map