UNPKG

hardhat

Version:

Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.

410 lines (338 loc) 10.7 kB
import { bytesToHex as bufferToHex } from "@nomicfoundation/ethereumjs-util"; import { AbiHelpers } from "../../util/abi-helpers"; import { Opcode } from "./opcodes"; /* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */ export enum JumpType { NOT_JUMP, INTO_FUNCTION, OUTOF_FUNCTION, INTERNAL_JUMP, } export enum ContractType { CONTRACT, LIBRARY, } export enum ContractFunctionType { CONSTRUCTOR, FUNCTION, FALLBACK, RECEIVE, GETTER, MODIFIER, FREE_FUNCTION, } export enum ContractFunctionVisibility { PRIVATE, INTERNAL, PUBLIC, EXTERNAL, } export class SourceFile { public readonly contracts: Contract[] = []; public readonly functions: ContractFunction[] = []; constructor( public readonly sourceName: string, public readonly content: string ) {} public addContract(contract: Contract) { if (contract.location.file !== this) { throw new Error("Trying to add a contract from another file"); } this.contracts.push(contract); } public addFunction(func: ContractFunction) { if (func.location.file !== this) { throw new Error("Trying to add a function from another file"); } this.functions.push(func); } public getContainingFunction( location: SourceLocation ): ContractFunction | undefined { // TODO: Optimize this with a binary search or an internal tree for (const func of this.functions) { if (func.location.contains(location)) { return func; } } return undefined; } } export class SourceLocation { private _line: number | undefined; constructor( public readonly file: SourceFile, public readonly offset: number, public readonly length: number ) {} public getStartingLineNumber(): number { if (this._line === undefined) { this._line = 1; for (const c of this.file.content.slice(0, this.offset)) { if (c === "\n") { this._line += 1; } } } return this._line; } public getContainingFunction(): ContractFunction | undefined { return this.file.getContainingFunction(this); } public contains(other: SourceLocation) { if (this.file !== other.file) { return false; } if (other.offset < this.offset) { return false; } return other.offset + other.length <= this.offset + this.length; } public equals(other: SourceLocation) { return ( this.file === other.file && this.offset === other.offset && this.length === other.length ); } } export class Contract { public readonly localFunctions: ContractFunction[] = []; public readonly customErrors: CustomError[] = []; private _constructor: ContractFunction | undefined; private _fallback: ContractFunction | undefined; private _receive: ContractFunction | undefined; private readonly _selectorHexToFunction: Map<string, ContractFunction> = new Map(); constructor( public readonly name: string, public readonly type: ContractType, public readonly location: SourceLocation ) {} public get constructorFunction(): ContractFunction | undefined { return this._constructor; } public get fallback(): ContractFunction | undefined { return this._fallback; } public get receive(): ContractFunction | undefined { return this._receive; } public addLocalFunction(func: ContractFunction) { if (func.contract !== this) { throw new Error("Function isn't local"); } if ( func.visibility === ContractFunctionVisibility.PUBLIC || func.visibility === ContractFunctionVisibility.EXTERNAL ) { if ( func.type === ContractFunctionType.FUNCTION || func.type === ContractFunctionType.GETTER ) { this._selectorHexToFunction.set(bufferToHex(func.selector!), func); } else if (func.type === ContractFunctionType.CONSTRUCTOR) { this._constructor = func; } else if (func.type === ContractFunctionType.FALLBACK) { this._fallback = func; } else if (func.type === ContractFunctionType.RECEIVE) { this._receive = func; } } this.localFunctions.push(func); } public addCustomError(customError: CustomError) { this.customErrors.push(customError); } public addNextLinearizedBaseContract(baseContract: Contract) { if (this._fallback === undefined && baseContract._fallback !== undefined) { this._fallback = baseContract._fallback; } if (this._receive === undefined && baseContract._receive !== undefined) { this._receive = baseContract._receive; } for (const baseContractFunction of baseContract.localFunctions) { if ( baseContractFunction.type !== ContractFunctionType.GETTER && baseContractFunction.type !== ContractFunctionType.FUNCTION ) { continue; } if ( baseContractFunction.visibility !== ContractFunctionVisibility.PUBLIC && baseContractFunction.visibility !== ContractFunctionVisibility.EXTERNAL ) { continue; } const selectorHex = bufferToHex(baseContractFunction.selector!); if (!this._selectorHexToFunction.has(selectorHex)) { this._selectorHexToFunction.set(selectorHex, baseContractFunction); } } } public getFunctionFromSelector( selector: Uint8Array ): ContractFunction | undefined { return this._selectorHexToFunction.get(bufferToHex(selector)); } /** * We compute selectors manually, which is particularly hard. We do this * because we need to map selectors to AST nodes, and it seems easier to start * from the AST node. This is surprisingly super hard: things like inherited * enums, structs and ABIv2 complicate it. * * As we know that that can fail, we run a heuristic that tries to correct * incorrect selectors. What it does is checking the `evm.methodIdentifiers` * compiler output, and detect missing selectors. Then we take those and * find contract functions with the same name. If there are multiple of those * we can't do anything. If there is a single one, it must have an incorrect * selector, so we update it with the `evm.methodIdentifiers`'s value. */ public correctSelector(functionName: string, selector: Buffer): boolean { const functions = Array.from(this._selectorHexToFunction.values()).filter( (cf) => cf.name === functionName ); if (functions.length !== 1) { return false; } const functionToCorrect = functions[0]; if (functionToCorrect.selector !== undefined) { this._selectorHexToFunction.delete( bufferToHex(functionToCorrect.selector) ); } functionToCorrect.selector = selector; this._selectorHexToFunction.set(bufferToHex(selector), functionToCorrect); return true; } } export class ContractFunction { constructor( public readonly name: string, public readonly type: ContractFunctionType, public readonly location: SourceLocation, public readonly contract?: Contract, public readonly visibility?: ContractFunctionVisibility, public readonly isPayable?: boolean, public selector?: Uint8Array, public readonly paramTypes?: any[] ) { if (contract !== undefined && !contract.location.contains(location)) { throw new Error("Incompatible contract and function location"); } } public isValidCalldata(calldata: Uint8Array): boolean { if (this.paramTypes === undefined) { // if we don't know the param types, we just assume that the call is valid return true; } return AbiHelpers.isValidCalldata(this.paramTypes, calldata); } } export class CustomError { /** * Return a CustomError from the given ABI information: the name * of the error and its inputs. Returns undefined if it can't build * the CustomError. */ public static fromABI(name: string, inputs: any[]): CustomError | undefined { const selector = AbiHelpers.computeSelector(name, inputs); if (selector !== undefined) { return new CustomError(selector, name, inputs); } } private constructor( public readonly selector: Uint8Array, public readonly name: string, public readonly paramTypes: any[] ) {} } export class Instruction { constructor( public readonly pc: number, public readonly opcode: Opcode, public readonly jumpType: JumpType, public readonly pushData?: Buffer, public readonly location?: SourceLocation ) {} /** * Checks equality with another Instruction. */ public equals(other: Instruction): boolean { if (this.pc !== other.pc) { return false; } if (this.opcode !== other.opcode) { return false; } if (this.jumpType !== other.jumpType) { return false; } if (this.pushData !== undefined) { if (other.pushData === undefined) { return false; } if (!this.pushData.equals(other.pushData)) { return false; } } else if (other.pushData !== undefined) { return false; } if (this.location !== undefined) { if (other.location === undefined) { return false; } if (!this.location.equals(other.location)) { return false; } } else if (other.location !== undefined) { return false; } return true; } } interface ImmutableReference { start: number; length: number; } export class Bytecode { private readonly _pcToInstruction: Map<number, Instruction> = new Map(); constructor( public readonly contract: Contract, public readonly isDeployment: boolean, public readonly normalizedCode: Buffer, public readonly instructions: Instruction[], public readonly libraryAddressPositions: number[], public readonly immutableReferences: ImmutableReference[], public readonly compilerVersion: string ) { for (const inst of instructions) { this._pcToInstruction.set(inst.pc, inst); } } public getInstruction(pc: number): Instruction { const inst = this._pcToInstruction.get(pc); if (inst === undefined) { throw new Error(`There's no instruction at pc ${pc}`); } return inst; } public hasInstruction(pc: number): boolean { return this._pcToInstruction.has(pc); } /** * Checks equality with another Bytecode. */ public equals(other: Bytecode): boolean { if (this._pcToInstruction.size !== other._pcToInstruction.size) { return false; } for (const [key, val] of this._pcToInstruction) { const otherVal = other._pcToInstruction.get(key); if (otherVal === undefined || !val.equals(otherVal)) { return false; } } return true; } }