@nomicfoundation/hardhat-verify
Version:
Hardhat plugin for verifying contracts
215 lines (182 loc) • 7.7 kB
text/typescript
import type { CompilerOutputBytecode, EthereumProvider } from "hardhat/types";
import { DeployedBytecodeNotFoundError } from "../errors";
import {
getMetadataSectionLength,
inferCompilerVersion,
METADATA_LENGTH,
MISSING_METADATA_VERSION_RANGE,
SOLC_NOT_FOUND_IN_METADATA_VERSION_RANGE,
} from "./metadata";
import {
ByteOffset,
getCallProtectionOffsets,
getImmutableOffsets,
getLibraryOffsets,
} from "./artifacts";
// If the compiler output bytecode is OVM bytecode, we need to make a fix to account for a bug in some versions of
// the OVM compiler. The artifact’s deployedBytecode is incorrect, but because its bytecode (initcode) is correct, when we
// actually deploy contracts, the code that ends up getting stored on chain is also correct. During verification,
// Etherscan will compile the source code, pull out the artifact’s deployedBytecode, and then perform the
// below find and replace, then check that resulting output against the code retrieved on chain from eth_getCode.
// We define the strings for that find and replace here, and use them later so we can know if the bytecode matches
// before it gets to Etherscan.
// Source: https://github.com/ethereum-optimism/optimism/blob/8d67991aba584c1703692ea46273ea8a1ef45f56/packages/contracts/src/contract-dumps.ts#L195-L204
const OVM_FIND_OPCODES =
"336000905af158601d01573d60011458600c01573d6000803e3d621234565260ea61109c52";
const OVM_REPLACE_OPCODES =
"336000905af158600e01573d6000803e3d6000fd5b3d6001141558600a015760016000f35b";
export class Bytecode {
private _bytecode: string;
private _version: string;
private _executableSection: ByteOffset;
private _isOvm: boolean;
constructor(bytecode: string) {
this._bytecode = bytecode;
const bytecodeBuffer = Buffer.from(bytecode, "hex");
this._version = inferCompilerVersion(bytecodeBuffer);
this._executableSection = {
start: 0,
length: bytecode.length - getMetadataSectionLength(bytecodeBuffer) * 2,
};
// Check if this is OVM bytecode by looking for the concatenation of the two opcodes defined here:
// https://github.com/ethereum-optimism/optimism/blob/33cb9025f5e463525d6abe67c8457f81a87c5a24/packages/contracts/contracts/optimistic-ethereum/OVM/execution/OVM_SafetyChecker.sol#L143
// - This check would only fail if the EVM solidity compiler didn't use any of the following opcodes: https://github.com/ethereum-optimism/optimism/blob/c42fc0df2790a5319027393cb8fa34e4f7bb520f/packages/contracts/contracts/optimistic-ethereum/iOVM/execution/iOVM_ExecutionManager.sol#L94-L175
// This is the list of opcodes that calls the OVM execution manager. But the current solidity
// compiler seems to add REVERT in all cases, meaning it currently won't happen and this check
// will always be correct.
// - It is possible, though very unlikely, that this string appears in the bytecode of an EVM
// contract. As a result result, this _isOvm flag should only be used after trying to infer
// the solc version
// - We need this check because OVM bytecode has no metadata, so when verifying
// OVM bytecode the check in `inferSolcVersion` will always return `MISSING_METADATA_VERSION_RANGE`.
this._isOvm = bytecode.includes(OVM_REPLACE_OPCODES);
}
public static async getDeployedContractBytecode(
address: string,
provider: EthereumProvider,
network: string
): Promise<Bytecode> {
const response: string = await provider.send("eth_getCode", [
address,
"latest",
]);
const deployedBytecode = response.replace(/^0x/, "");
if (deployedBytecode === "") {
throw new DeployedBytecodeNotFoundError(address, network);
}
return new Bytecode(deployedBytecode);
}
public stringify() {
return this._bytecode;
}
public getVersion() {
return this._version;
}
public isOvm() {
return this._isOvm;
}
public hasVersionRange(): boolean {
return (
this._version === MISSING_METADATA_VERSION_RANGE ||
this._version === SOLC_NOT_FOUND_IN_METADATA_VERSION_RANGE
);
}
public async getMatchingVersions(versions: string[]): Promise<string[]> {
const semver = await import("semver");
const matchingCompilerVersions = versions.filter((version) =>
semver.satisfies(version, this._version)
);
return matchingCompilerVersions;
}
/**
* Compare the bytecode against a compiler's output bytecode, ignoring metadata.
*/
public compare(
compilerOutputDeployedBytecode: CompilerOutputBytecode
): boolean {
// Ignore metadata since Etherscan performs a partial match.
// See: https://ethereum.org/es/developers/docs/smart-contracts/verifying/#etherscan
const executableSection = this._getExecutableSection();
let referenceExecutableSection = inferExecutableSection(
compilerOutputDeployedBytecode.object
);
// If this is OVM bytecode, do the required find and replace (see above comments for more info)
if (this._isOvm) {
referenceExecutableSection = referenceExecutableSection
.split(OVM_FIND_OPCODES)
.join(OVM_REPLACE_OPCODES);
}
if (
executableSection.length !== referenceExecutableSection.length &&
// OVM bytecode has no metadata so we ignore this comparison if operating on OVM bytecode
!this._isOvm
) {
return false;
}
const normalizedBytecode = nullifyBytecodeOffsets(
executableSection,
compilerOutputDeployedBytecode
);
// Library hash placeholders are embedded into the bytes where the library addresses are linked.
// We need to zero them out to compare them.
const normalizedReferenceBytecode = nullifyBytecodeOffsets(
referenceExecutableSection,
compilerOutputDeployedBytecode
);
if (normalizedBytecode === normalizedReferenceBytecode) {
return true;
}
return false;
}
private _getExecutableSection(): string {
const { start, length } = this._executableSection;
return this._bytecode.slice(start, length);
}
}
function nullifyBytecodeOffsets(
bytecode: string,
{
object: referenceBytecode,
linkReferences,
immutableReferences,
}: CompilerOutputBytecode
): string {
const offsets = [
...getLibraryOffsets(linkReferences),
...getImmutableOffsets(immutableReferences),
...getCallProtectionOffsets(bytecode, referenceBytecode),
];
for (const { start, length } of offsets) {
bytecode = [
bytecode.slice(0, start * 2),
"0".repeat(length * 2),
bytecode.slice((start + length) * 2),
].join("");
}
return bytecode;
}
/**
* This function returns the executable section without actually
* decoding the whole bytecode string.
*
* This is useful because the runtime object emitted by the compiler
* may contain nonhexadecimal characters due to link placeholders.
*/
function inferExecutableSection(bytecode: string): string {
if (bytecode.startsWith("0x")) {
bytecode = bytecode.slice(2);
}
// `Buffer.from` will return a buffer that contains bytes up until the last decodable byte.
// To work around this we'll just slice the relevant part of the bytecode.
const metadataLengthSlice = Buffer.from(
bytecode.slice(-METADATA_LENGTH * 2),
"hex"
);
// If, for whatever reason, the bytecode is so small that we can't even read two bytes off it,
// return the size of the entire bytecode.
if (metadataLengthSlice.length !== METADATA_LENGTH) {
return bytecode;
}
const metadataSectionLength = getMetadataSectionLength(metadataLengthSlice);
return bytecode.slice(0, bytecode.length - metadataSectionLength * 2);
}