UNPKG

hardhat

Version:

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

1,020 lines 56.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.instructionToCallstackStackTraceEntry = exports.ErrorInferrer = void 0; /* eslint "@typescript-eslint/no-non-null-assertion": "error" */ const abi_1 = require("@ethersproject/abi"); const ethereumjs_util_1 = require("@nomicfoundation/ethereumjs-util"); const semver_1 = __importDefault(require("semver")); const errors_1 = require("../../core/errors"); const abi_helpers_1 = require("../../util/abi-helpers"); const return_data_1 = require("../provider/return-data"); const exit_1 = require("../provider/vm/exit"); const message_trace_1 = require("./message-trace"); const model_1 = require("./model"); const opcodes_1 = require("./opcodes"); const solidity_stack_trace_1 = require("./solidity-stack-trace"); const FIRST_SOLC_VERSION_CREATE_PARAMS_VALIDATION = "0.5.9"; const FIRST_SOLC_VERSION_RECEIVE_FUNCTION = "0.6.0"; const FIRST_SOLC_VERSION_WITH_UNMAPPED_REVERTS = "0.6.3"; /* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */ class ErrorInferrer { inferBeforeTracingCallMessage(trace) { if (this._isDirectLibraryCall(trace)) { return this._getDirectLibraryCallErrorStackTrace(trace); } const calledFunction = trace.bytecode.contract.getFunctionFromSelector(trace.calldata.slice(0, 4)); if (calledFunction !== undefined && this._isFunctionNotPayableError(trace, calledFunction)) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR, sourceReference: this._getFunctionStartSourceReference(trace, calledFunction), value: trace.value, }, ]; } if (this._isMissingFunctionAndFallbackError(trace, calledFunction)) { if (this._emptyCalldataAndNoReceive(trace)) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.MISSING_FALLBACK_OR_RECEIVE_ERROR, sourceReference: this._getContractStartWithoutFunctionSourceReference(trace), }, ]; } return [ { type: solidity_stack_trace_1.StackTraceEntryType.UNRECOGNIZED_FUNCTION_WITHOUT_FALLBACK_ERROR, sourceReference: this._getContractStartWithoutFunctionSourceReference(trace), }, ]; } if (this._isFallbackNotPayableError(trace, calledFunction)) { if (this._emptyCalldataAndNoReceive(trace)) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.FALLBACK_NOT_PAYABLE_AND_NO_RECEIVE_ERROR, sourceReference: this._getFallbackStartSourceReference(trace), value: trace.value, }, ]; } return [ { type: solidity_stack_trace_1.StackTraceEntryType.FALLBACK_NOT_PAYABLE_ERROR, sourceReference: this._getFallbackStartSourceReference(trace), value: trace.value, }, ]; } } inferBeforeTracingCreateMessage(trace) { if (this._isConstructorNotPayableError(trace)) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR, sourceReference: this._getConstructorStartSourceReference(trace), value: trace.value, }, ]; } if (this._isConstructorInvalidArgumentsError(trace)) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.INVALID_PARAMS_ERROR, sourceReference: this._getConstructorStartSourceReference(trace), }, ]; } } inferAfterTracing(trace, stacktrace, functionJumpdests, jumpedIntoFunction, lastSubmessageData) { return (this._checkLastSubmessage(trace, stacktrace, lastSubmessageData) ?? this._checkFailedLastCall(trace, stacktrace) ?? this._checkLastInstruction(trace, stacktrace, functionJumpdests, jumpedIntoFunction) ?? this._checkNonContractCalled(trace, stacktrace) ?? this._checkSolidity063UnmappedRevert(trace, stacktrace) ?? this._checkContractTooLarge(trace) ?? this._otherExecutionErrorStacktrace(trace, stacktrace)); } filterRedundantFrames(stacktrace) { return stacktrace.filter((frame, i) => { if (i + 1 === stacktrace.length) { return true; } const nextFrame = stacktrace[i + 1]; // we can only filter frames if we know their sourceReference // and the one from the next frame if (frame.sourceReference === undefined || nextFrame.sourceReference === undefined) { return true; } // look TWO frames ahead to determine if this is a specific occurrence of // a redundant CALLSTACK_ENTRY frame observed when using Solidity 0.8.5: if (frame.type === solidity_stack_trace_1.StackTraceEntryType.CALLSTACK_ENTRY && i + 2 < stacktrace.length && stacktrace[i + 2].sourceReference !== undefined && stacktrace[i + 2].type === solidity_stack_trace_1.StackTraceEntryType.RETURNDATA_SIZE_ERROR) { // ! below for tsc. we confirmed existence in the enclosing conditional. const thatSrcRef = stacktrace[i + 2].sourceReference; if (thatSrcRef !== undefined && frame.sourceReference.range[0] === thatSrcRef.range[0] && frame.sourceReference.range[1] === thatSrcRef.range[1] && frame.sourceReference.line === thatSrcRef.line) { return false; } } // constructors contain the whole contract, so we ignore them if (frame.sourceReference.function === "constructor" && nextFrame.sourceReference.function !== "constructor") { return true; } // this is probably a recursive call if (i > 0 && frame.type === nextFrame.type && frame.sourceReference.range[0] === nextFrame.sourceReference.range[0] && frame.sourceReference.range[1] === nextFrame.sourceReference.range[1] && frame.sourceReference.line === nextFrame.sourceReference.line) { return true; } if (frame.sourceReference.range[0] <= nextFrame.sourceReference.range[0] && frame.sourceReference.range[1] >= nextFrame.sourceReference.range[1]) { return false; } return true; }); } // Heuristics /** * Check if the last submessage can be used to generate the stack trace. */ _checkLastSubmessage(trace, stacktrace, lastSubmessageData) { if (lastSubmessageData === undefined) { return undefined; } const inferredStacktrace = [...stacktrace]; // get the instruction before the submessage and add it to the stack trace const callStep = trace.steps[lastSubmessageData.stepIndex - 1]; if (!(0, message_trace_1.isEvmStep)(callStep)) { throw new Error("This should not happen: MessageTrace should be preceded by a EVM step"); } const callInst = trace.bytecode.getInstruction(callStep.pc); const callStackFrame = instructionToCallstackStackTraceEntry(trace.bytecode, callInst); const lastMessageFailed = lastSubmessageData.messageTrace.exit.isError(); if (lastMessageFailed) { // add the call/create that generated the message to the stack trace inferredStacktrace.push(callStackFrame); if (this._isSubtraceErrorPropagated(trace, lastSubmessageData.stepIndex) || this._isProxyErrorPropagated(trace, lastSubmessageData.stepIndex)) { inferredStacktrace.push(...lastSubmessageData.stacktrace); if (this._isContractCallRunOutOfGasError(trace, lastSubmessageData.stepIndex)) { const lastFrame = inferredStacktrace.pop(); (0, errors_1.assertHardhatInvariant)(lastFrame !== undefined, "Expected inferred stack trace to have at least one frame"); inferredStacktrace.push({ type: solidity_stack_trace_1.StackTraceEntryType.CONTRACT_CALL_RUN_OUT_OF_GAS_ERROR, sourceReference: lastFrame.sourceReference, }); } return this._fixInitialModifier(trace, inferredStacktrace); } } else { const isReturnDataSizeError = this._failsRightAfterCall(trace, lastSubmessageData.stepIndex); if (isReturnDataSizeError) { inferredStacktrace.push({ type: solidity_stack_trace_1.StackTraceEntryType.RETURNDATA_SIZE_ERROR, sourceReference: callStackFrame.sourceReference, }); return this._fixInitialModifier(trace, inferredStacktrace); } } } /** * Check if the last call/create that was done failed. */ _checkFailedLastCall(trace, stacktrace) { for (let stepIndex = trace.steps.length - 2; stepIndex >= 0; stepIndex--) { const step = trace.steps[stepIndex]; const nextStep = trace.steps[stepIndex + 1]; if (!(0, message_trace_1.isEvmStep)(step)) { return; } const inst = trace.bytecode.getInstruction(step.pc); const isCallOrCreate = (0, opcodes_1.isCall)(inst.opcode) || (0, opcodes_1.isCreate)(inst.opcode); if (isCallOrCreate && (0, message_trace_1.isEvmStep)(nextStep)) { if (this._isCallFailedError(trace, stepIndex, inst)) { const inferredStacktrace = [ ...stacktrace, this._callInstructionToCallFailedToExecuteStackTraceEntry(trace.bytecode, inst), ]; return this._fixInitialModifier(trace, inferredStacktrace); } } } } /** * Check if the execution stopped with a revert or an invalid opcode. */ _checkRevertOrInvalidOpcode(trace, stacktrace, lastInstruction, functionJumpdests, jumpedIntoFunction) { if (lastInstruction.opcode !== opcodes_1.Opcode.REVERT && lastInstruction.opcode !== opcodes_1.Opcode.INVALID) { return; } const inferredStacktrace = [...stacktrace]; if (lastInstruction.location !== undefined && (!(0, message_trace_1.isDecodedCallTrace)(trace) || jumpedIntoFunction)) { // There should always be a function here, but that's not the case with optimizations. // // If this is a create trace, we already checked args and nonpayable failures before // calling this function. // // If it's a call trace, we already jumped into a function. But optimizations can happen. const failingFunction = lastInstruction.location.getContainingFunction(); // If the failure is in a modifier we add an entry with the function/constructor if (failingFunction !== undefined && failingFunction.type === model_1.ContractFunctionType.MODIFIER) { inferredStacktrace.push(this._getEntryBeforeFailureInModifier(trace, functionJumpdests)); } } const panicStacktrace = this._checkPanic(trace, inferredStacktrace, lastInstruction); if (panicStacktrace !== undefined) { return panicStacktrace; } const customErrorStacktrace = this._checkCustomErrors(trace, inferredStacktrace, lastInstruction); if (customErrorStacktrace !== undefined) { return customErrorStacktrace; } if (lastInstruction.location !== undefined && (!(0, message_trace_1.isDecodedCallTrace)(trace) || jumpedIntoFunction)) { const failingFunction = lastInstruction.location.getContainingFunction(); if (failingFunction !== undefined) { inferredStacktrace.push(this._instructionWithinFunctionToRevertStackTraceEntry(trace, lastInstruction)); } else if ((0, message_trace_1.isDecodedCallTrace)(trace)) { // This is here because of the optimizations const functionSelector = trace.bytecode.contract.getFunctionFromSelector(trace.calldata.slice(0, 4)); // in general this shouldn't happen, but it does when viaIR is enabled, // "optimizerSteps": "u" is used, and the called function is fallback or // receive if (functionSelector === undefined) { return; } inferredStacktrace.push({ type: solidity_stack_trace_1.StackTraceEntryType.REVERT_ERROR, sourceReference: this._getFunctionStartSourceReference(trace, functionSelector), message: new return_data_1.ReturnData(trace.returnData), isInvalidOpcodeError: lastInstruction.opcode === opcodes_1.Opcode.INVALID, }); } else { // This is here because of the optimizations inferredStacktrace.push({ type: solidity_stack_trace_1.StackTraceEntryType.REVERT_ERROR, sourceReference: this._getConstructorStartSourceReference(trace), message: new return_data_1.ReturnData(trace.returnData), isInvalidOpcodeError: lastInstruction.opcode === opcodes_1.Opcode.INVALID, }); } return this._fixInitialModifier(trace, inferredStacktrace); } // If the revert instruction is not mapped but there is return data, // we add the frame anyway, sith the best sourceReference we can get if (lastInstruction.location === undefined && trace.returnData.length > 0) { const revertFrame = { type: solidity_stack_trace_1.StackTraceEntryType.REVERT_ERROR, sourceReference: this._getLastSourceReference(trace) ?? this._getContractStartWithoutFunctionSourceReference(trace), message: new return_data_1.ReturnData(trace.returnData), isInvalidOpcodeError: lastInstruction.opcode === opcodes_1.Opcode.INVALID, }; inferredStacktrace.push(revertFrame); return this._fixInitialModifier(trace, inferredStacktrace); } } /** * Check if the trace reverted with a panic error. */ _checkPanic(trace, stacktrace, lastInstruction) { if (!this._isPanicReturnData(trace.returnData)) { return; } // If the last frame is an internal function, it means that the trace // jumped there to return the panic. If that's the case, we remove that // frame. const lastFrame = stacktrace[stacktrace.length - 1]; if (lastFrame?.type === solidity_stack_trace_1.StackTraceEntryType.INTERNAL_FUNCTION_CALLSTACK_ENTRY) { stacktrace.splice(-1); } const panicReturnData = new return_data_1.ReturnData(trace.returnData); const errorCode = panicReturnData.decodePanic(); // if the error comes from a call to a zero-initialized function, // we remove the last frame, which represents the call, to avoid // having duplicated frames if (errorCode === 0x51n) { stacktrace.splice(-1); } const inferredStacktrace = [...stacktrace]; inferredStacktrace.push(this._instructionWithinFunctionToPanicStackTraceEntry(trace, lastInstruction, errorCode)); return this._fixInitialModifier(trace, inferredStacktrace); } _checkCustomErrors(trace, stacktrace, lastInstruction) { const returnData = new return_data_1.ReturnData(trace.returnData); if (returnData.isEmpty() || returnData.isErrorReturnData()) { // if there is no return data, or if it's a Error(string), // then it can't be a custom error return; } const rawReturnData = Buffer.from(returnData.value).toString("hex"); let errorMessage = `reverted with an unrecognized custom error (return data: 0x${rawReturnData})`; for (const customError of trace.bytecode.contract.customErrors) { if (returnData.matchesSelector(customError.selector)) { // if the return data matches a custom error in the called contract, // we format the message using the returnData and the custom error instance const decodedValues = abi_1.defaultAbiCoder.decode(customError.paramTypes, returnData.value.slice(4)); const params = abi_helpers_1.AbiHelpers.formatValues([...decodedValues]); errorMessage = `reverted with custom error '${customError.name}(${params})'`; break; } } const inferredStacktrace = [...stacktrace]; inferredStacktrace.push(this._instructionWithinFunctionToCustomErrorStackTraceEntry(trace, lastInstruction, errorMessage)); return this._fixInitialModifier(trace, inferredStacktrace); } /** * Check last instruction to try to infer the error. */ _checkLastInstruction(trace, stacktrace, functionJumpdests, jumpedIntoFunction) { if (trace.steps.length === 0) { return; } const lastStep = trace.steps[trace.steps.length - 1]; if (!(0, message_trace_1.isEvmStep)(lastStep)) { throw new Error("This should not happen: MessageTrace ends with a subtrace"); } const lastInstruction = trace.bytecode.getInstruction(lastStep.pc); const revertOrInvalidStacktrace = this._checkRevertOrInvalidOpcode(trace, stacktrace, lastInstruction, functionJumpdests, jumpedIntoFunction); if (revertOrInvalidStacktrace !== undefined) { return revertOrInvalidStacktrace; } if ((0, message_trace_1.isDecodedCallTrace)(trace) && !jumpedIntoFunction) { if (this._hasFailedInsideTheFallbackFunction(trace) || this._hasFailedInsideTheReceiveFunction(trace)) { return [ this._instructionWithinFunctionToRevertStackTraceEntry(trace, lastInstruction), ]; } // Sometimes we do fail inside of a function but there's no jump into if (lastInstruction.location !== undefined) { const failingFunction = lastInstruction.location.getContainingFunction(); if (failingFunction !== undefined) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.REVERT_ERROR, sourceReference: this._getFunctionStartSourceReference(trace, failingFunction), message: new return_data_1.ReturnData(trace.returnData), isInvalidOpcodeError: lastInstruction.opcode === opcodes_1.Opcode.INVALID, }, ]; } } const calledFunction = trace.bytecode.contract.getFunctionFromSelector(trace.calldata.slice(0, 4)); if (calledFunction !== undefined) { const isValidCalldata = calledFunction.isValidCalldata(trace.calldata.slice(4)); if (!isValidCalldata) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.INVALID_PARAMS_ERROR, sourceReference: this._getFunctionStartSourceReference(trace, calledFunction), }, ]; } } if (this._solidity063MaybeUnmappedRevert(trace)) { const revertFrame = this._solidity063GetFrameForUnmappedRevertBeforeFunction(trace); if (revertFrame !== undefined) { return [revertFrame]; } } return [this._getOtherErrorBeforeCalledFunctionStackTraceEntry(trace)]; } } _checkNonContractCalled(trace, stacktrace) { if (this._isCalledNonContractAccountError(trace)) { const sourceReference = this._getLastSourceReference(trace); // We are sure this is not undefined because there was at least a call instruction (0, errors_1.assertHardhatInvariant)(sourceReference !== undefined, "Expected source reference to be defined"); const nonContractCalledFrame = { type: solidity_stack_trace_1.StackTraceEntryType.NONCONTRACT_ACCOUNT_CALLED_ERROR, sourceReference, }; return [...stacktrace, nonContractCalledFrame]; } } _checkSolidity063UnmappedRevert(trace, stacktrace) { if (this._solidity063MaybeUnmappedRevert(trace)) { const revertFrame = this._solidity063GetFrameForUnmappedRevertWithinFunction(trace); if (revertFrame !== undefined) { return [...stacktrace, revertFrame]; } } } _checkContractTooLarge(trace) { if ((0, message_trace_1.isCreateTrace)(trace) && this._isContractTooLargeError(trace)) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR, sourceReference: this._getConstructorStartSourceReference(trace), }, ]; } } _otherExecutionErrorStacktrace(trace, stacktrace) { const otherExecutionErrorFrame = { type: solidity_stack_trace_1.StackTraceEntryType.OTHER_EXECUTION_ERROR, sourceReference: this._getLastSourceReference(trace), }; return [...stacktrace, otherExecutionErrorFrame]; } // Helpers _fixInitialModifier(trace, stacktrace) { const firstEntry = stacktrace[0]; if (firstEntry !== undefined && firstEntry.type === solidity_stack_trace_1.StackTraceEntryType.CALLSTACK_ENTRY && firstEntry.functionType === model_1.ContractFunctionType.MODIFIER) { return [ this._getEntryBeforeInitialModifierCallstackEntry(trace), ...stacktrace, ]; } return stacktrace; } _isDirectLibraryCall(trace) { return (trace.depth === 0 && trace.bytecode.contract.type === model_1.ContractType.LIBRARY); } _getDirectLibraryCallErrorStackTrace(trace) { const func = trace.bytecode.contract.getFunctionFromSelector(trace.calldata.slice(0, 4)); if (func !== undefined) { return [ { type: solidity_stack_trace_1.StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR, sourceReference: this._getFunctionStartSourceReference(trace, func), }, ]; } return [ { type: solidity_stack_trace_1.StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR, sourceReference: this._getContractStartWithoutFunctionSourceReference(trace), }, ]; } _isFunctionNotPayableError(trace, calledFunction) { // This error doesn't return data if (trace.returnData.length > 0) { return false; } if (trace.value <= 0n) { return false; } // Libraries don't have a nonpayable check if (trace.bytecode.contract.type === model_1.ContractType.LIBRARY) { return false; } return calledFunction.isPayable === undefined || !calledFunction.isPayable; } _getFunctionStartSourceReference(trace, func) { return { sourceName: func.location.file.sourceName, sourceContent: func.location.file.content, contract: trace.bytecode.contract.name, function: func.name, line: func.location.getStartingLineNumber(), range: [ func.location.offset, func.location.offset + func.location.length, ], }; } _isMissingFunctionAndFallbackError(trace, calledFunction) { // This error doesn't return data if (trace.returnData.length > 0) { return false; } // the called function exists in the contract if (calledFunction !== undefined) { return false; } // there's a receive function and no calldata if (trace.calldata.length === 0 && trace.bytecode.contract.receive !== undefined) { return false; } return trace.bytecode.contract.fallback === undefined; } _emptyCalldataAndNoReceive(trace) { // this only makes sense when receive functions are available if (semver_1.default.lt(trace.bytecode.compilerVersion, FIRST_SOLC_VERSION_RECEIVE_FUNCTION)) { return false; } return (trace.calldata.length === 0 && trace.bytecode.contract.receive === undefined); } _getContractStartWithoutFunctionSourceReference(trace) { const location = trace.bytecode.contract.location; return { sourceName: location.file.sourceName, sourceContent: location.file.content, contract: trace.bytecode.contract.name, line: location.getStartingLineNumber(), range: [location.offset, location.offset + location.length], }; } _isFallbackNotPayableError(trace, calledFunction) { if (calledFunction !== undefined) { return false; } // This error doesn't return data if (trace.returnData.length > 0) { return false; } if (trace.value <= 0n) { return false; } if (trace.bytecode.contract.fallback === undefined) { return false; } const isPayable = trace.bytecode.contract.fallback.isPayable; return isPayable === undefined || !isPayable; } _getFallbackStartSourceReference(trace) { const func = trace.bytecode.contract.fallback; if (func === undefined) { throw new Error("This shouldn't happen: trying to get fallback source reference from a contract without fallback"); } return { sourceName: func.location.file.sourceName, sourceContent: func.location.file.content, contract: trace.bytecode.contract.name, function: solidity_stack_trace_1.FALLBACK_FUNCTION_NAME, line: func.location.getStartingLineNumber(), range: [ func.location.offset, func.location.offset + func.location.length, ], }; } _isConstructorNotPayableError(trace) { // This error doesn't return data if (trace.returnData.length > 0) { return false; } const constructor = trace.bytecode.contract.constructorFunction; // This function is only matters with contracts that have constructors defined. The ones that // don't are abstract contracts, or their constructor doesn't take any argument. if (constructor === undefined) { return false; } return (trace.value > 0n && (constructor.isPayable === undefined || !constructor.isPayable)); } /** * Returns a source reference pointing to the constructor if it exists, or to the contract * otherwise. */ _getConstructorStartSourceReference(trace) { const contract = trace.bytecode.contract; const constructor = contract.constructorFunction; const line = constructor !== undefined ? constructor.location.getStartingLineNumber() : contract.location.getStartingLineNumber(); return { sourceName: contract.location.file.sourceName, sourceContent: contract.location.file.content, contract: contract.name, function: solidity_stack_trace_1.CONSTRUCTOR_FUNCTION_NAME, line, range: [ contract.location.offset, contract.location.offset + contract.location.length, ], }; } _isConstructorInvalidArgumentsError(trace) { // This error doesn't return data if (trace.returnData.length > 0) { return false; } const contract = trace.bytecode.contract; const constructor = contract.constructorFunction; // This function is only matters with contracts that have constructors defined. The ones that // don't are abstract contracts, or their constructor doesn't take any argument. if (constructor === undefined) { return false; } if (semver_1.default.lt(trace.bytecode.compilerVersion, FIRST_SOLC_VERSION_CREATE_PARAMS_VALIDATION)) { return false; } const lastStep = trace.steps[trace.steps.length - 1]; if (!(0, message_trace_1.isEvmStep)(lastStep)) { return false; } const lastInst = trace.bytecode.getInstruction(lastStep.pc); if (lastInst.opcode !== opcodes_1.Opcode.REVERT || lastInst.location !== undefined) { return false; } let hasReadDeploymentCodeSize = false; // eslint-disable-next-line @typescript-eslint/prefer-for-of for (let stepIndex = 0; stepIndex < trace.steps.length; stepIndex++) { const step = trace.steps[stepIndex]; if (!(0, message_trace_1.isEvmStep)(step)) { return false; } const inst = trace.bytecode.getInstruction(step.pc); if (inst.location !== undefined && !contract.location.equals(inst.location) && !constructor.location.equals(inst.location)) { return false; } if (inst.opcode === opcodes_1.Opcode.CODESIZE && (0, message_trace_1.isCreateTrace)(trace)) { hasReadDeploymentCodeSize = true; } } return hasReadDeploymentCodeSize; } _getEntryBeforeInitialModifierCallstackEntry(trace) { if ((0, message_trace_1.isDecodedCreateTrace)(trace)) { return { type: solidity_stack_trace_1.StackTraceEntryType.CALLSTACK_ENTRY, sourceReference: this._getConstructorStartSourceReference(trace), functionType: model_1.ContractFunctionType.CONSTRUCTOR, }; } const calledFunction = trace.bytecode.contract.getFunctionFromSelector(trace.calldata.slice(0, 4)); if (calledFunction !== undefined) { return { type: solidity_stack_trace_1.StackTraceEntryType.CALLSTACK_ENTRY, sourceReference: this._getFunctionStartSourceReference(trace, calledFunction), functionType: model_1.ContractFunctionType.FUNCTION, }; } // If it failed or made a call from within a modifier, and the selector doesn't match // any function, it must have a fallback. return { type: solidity_stack_trace_1.StackTraceEntryType.CALLSTACK_ENTRY, sourceReference: this._getFallbackStartSourceReference(trace), functionType: model_1.ContractFunctionType.FALLBACK, }; } _getLastSourceReference(trace) { for (let i = trace.steps.length - 1; i >= 0; i--) { const step = trace.steps[i]; if (!(0, message_trace_1.isEvmStep)(step)) { continue; } const inst = trace.bytecode.getInstruction(step.pc); if (inst.location === undefined) { continue; } const sourceReference = sourceLocationToSourceReference(trace.bytecode, inst.location); if (sourceReference !== undefined) { return sourceReference; } } return undefined; } _hasFailedInsideTheFallbackFunction(trace) { const contract = trace.bytecode.contract; if (contract.fallback === undefined) { return false; } return this._hasFailedInsideFunction(trace, contract.fallback); } _hasFailedInsideTheReceiveFunction(trace) { const contract = trace.bytecode.contract; if (contract.receive === undefined) { return false; } return this._hasFailedInsideFunction(trace, contract.receive); } _hasFailedInsideFunction(trace, func) { const lastStep = trace.steps[trace.steps.length - 1]; const lastInstruction = trace.bytecode.getInstruction(lastStep.pc); return (lastInstruction.location !== undefined && lastInstruction.opcode === opcodes_1.Opcode.REVERT && func.location.contains(lastInstruction.location)); } _instructionWithinFunctionToRevertStackTraceEntry(trace, inst) { const sourceReference = sourceLocationToSourceReference(trace.bytecode, inst.location); (0, errors_1.assertHardhatInvariant)(sourceReference !== undefined, "Expected source reference to be defined"); return { type: solidity_stack_trace_1.StackTraceEntryType.REVERT_ERROR, sourceReference, message: new return_data_1.ReturnData(trace.returnData), isInvalidOpcodeError: inst.opcode === opcodes_1.Opcode.INVALID, }; } _instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(trace, inst) { const sourceReference = sourceLocationToSourceReference(trace.bytecode, inst.location); return { type: solidity_stack_trace_1.StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR, sourceReference, }; } _instructionWithinFunctionToPanicStackTraceEntry(trace, inst, errorCode) { const lastSourceReference = this._getLastSourceReference(trace); return { type: solidity_stack_trace_1.StackTraceEntryType.PANIC_ERROR, sourceReference: sourceLocationToSourceReference(trace.bytecode, inst.location) ?? lastSourceReference, errorCode, }; } _instructionWithinFunctionToCustomErrorStackTraceEntry(trace, inst, message) { const lastSourceReference = this._getLastSourceReference(trace); (0, errors_1.assertHardhatInvariant)(lastSourceReference !== undefined, "Expected last source reference to be defined"); return { type: solidity_stack_trace_1.StackTraceEntryType.CUSTOM_ERROR, sourceReference: sourceLocationToSourceReference(trace.bytecode, inst.location) ?? lastSourceReference, message, }; } _solidity063MaybeUnmappedRevert(trace) { if (trace.steps.length === 0) { return false; } const lastStep = trace.steps[trace.steps.length - 1]; if (!(0, message_trace_1.isEvmStep)(lastStep)) { return false; } const lastInst = trace.bytecode.getInstruction(lastStep.pc); return (semver_1.default.satisfies(trace.bytecode.compilerVersion, `^${FIRST_SOLC_VERSION_WITH_UNMAPPED_REVERTS}`) && lastInst.opcode === opcodes_1.Opcode.REVERT); } // Solidity 0.6.3 unmapped reverts special handling // For more info: https://github.com/ethereum/solidity/issues/9006 _solidity063GetFrameForUnmappedRevertBeforeFunction(trace) { let revertFrame = this._solidity063GetFrameForUnmappedRevertWithinFunction(trace); if (revertFrame === undefined || revertFrame.sourceReference === undefined) { if (trace.bytecode.contract.receive === undefined || trace.calldata.length > 0) { if (trace.bytecode.contract.fallback !== undefined) { // Failed within the fallback const location = trace.bytecode.contract.fallback.location; revertFrame = { type: solidity_stack_trace_1.StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR, sourceReference: { contract: trace.bytecode.contract.name, function: solidity_stack_trace_1.FALLBACK_FUNCTION_NAME, sourceName: location.file.sourceName, sourceContent: location.file.content, line: location.getStartingLineNumber(), range: [location.offset, location.offset + location.length], }, }; this._solidity063CorrectLineNumber(revertFrame); } } else { // Failed within the receive function const location = trace.bytecode.contract.receive.location; revertFrame = { type: solidity_stack_trace_1.StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR, sourceReference: { contract: trace.bytecode.contract.name, function: solidity_stack_trace_1.RECEIVE_FUNCTION_NAME, sourceName: location.file.sourceName, sourceContent: location.file.content, line: location.getStartingLineNumber(), range: [location.offset, location.offset + location.length], }, }; this._solidity063CorrectLineNumber(revertFrame); } } return revertFrame; } _getOtherErrorBeforeCalledFunctionStackTraceEntry(trace) { return { type: solidity_stack_trace_1.StackTraceEntryType.OTHER_EXECUTION_ERROR, sourceReference: this._getContractStartWithoutFunctionSourceReference(trace), }; } _isCalledNonContractAccountError(trace) { // We could change this to checking that the last valid location maps to a call, but // it's way more complex as we need to get the ast node from that location. const lastIndex = this._getLastInstructionWithValidLocationStepIndex(trace); if (lastIndex === undefined || lastIndex === 0) { return false; } const lastStep = trace.steps[lastIndex]; // We know this is an EVM step const lastInst = trace.bytecode.getInstruction(lastStep.pc); if (lastInst.opcode !== opcodes_1.Opcode.ISZERO) { return false; } const prevStep = trace.steps[lastIndex - 1]; // We know this is an EVM step const prevInst = trace.bytecode.getInstruction(prevStep.pc); return prevInst.opcode === opcodes_1.Opcode.EXTCODESIZE; } _solidity063GetFrameForUnmappedRevertWithinFunction(trace) { // If we are within a function there's a last valid location. It may // be the entire contract. const prevInst = this._getLastInstructionWithValidLocation(trace); const lastStep = trace.steps[trace.steps.length - 1]; const nextInstPc = lastStep.pc + 1; const hasNextInst = trace.bytecode.hasInstruction(nextInstPc); if (hasNextInst) { const nextInst = trace.bytecode.getInstruction(nextInstPc); const prevLoc = prevInst?.location; const nextLoc = nextInst.location; const prevFunc = prevLoc?.getContainingFunction(); const nextFunc = nextLoc?.getContainingFunction(); // This is probably a require. This means that we have the exact // line, but the stack trace may be degraded (e.g. missing our // synthetic call frames when failing in a modifier) so we still // add this frame as UNMAPPED_SOLC_0_6_3_REVERT_ERROR if (prevFunc !== undefined && nextLoc !== undefined && prevLoc !== undefined && prevLoc.equals(nextLoc)) { return this._instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(trace, nextInst); } let revertFrame; // If the previous and next location don't match, we try to use the // previous one if it's inside a function, otherwise we use the next one if (prevFunc !== undefined && prevInst !== undefined) { revertFrame = this._instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(trace, prevInst); } else if (nextFunc !== undefined) { revertFrame = this._instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(trace, nextInst); } if (revertFrame !== undefined) { this._solidity063CorrectLineNumber(revertFrame); } return revertFrame; } if ((0, message_trace_1.isCreateTrace)(trace) && prevInst !== undefined) { // Solidity is smart enough to stop emitting extra instructions after // an unconditional revert happens in a constructor. If this is the case // we just return a special error. const constructorRevertFrame = this._instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(trace, prevInst); // When the latest instruction is not within a function we need // some default sourceReference to show to the user if (constructorRevertFrame.sourceReference === undefined) { const location = trace.bytecode.contract.location; const defaultSourceReference = { function: solidity_stack_trace_1.CONSTRUCTOR_FUNCTION_NAME, contract: trace.bytecode.contract.name, sourceName: location.file.sourceName, sourceContent: location.file.content, line: location.getStartingLineNumber(), range: [location.offset, location.offset + location.length], }; if (trace.bytecode.contract.constructorFunction !== undefined) { defaultSourceReference.line = trace.bytecode.contract.constructorFunction.location.getStartingLineNumber(); } constructorRevertFrame.sourceReference = defaultSourceReference; } else { this._solidity063CorrectLineNumber(constructorRevertFrame); } return constructorRevertFrame; } if (prevInst !== undefined) { // We may as well just be in a function or modifier and just happen // to be at the last instruction of the runtime bytecode. // In this case we just return whatever the last mapped intruction // points to. const latestInstructionRevertFrame = this._instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(trace, prevInst); if (latestInstructionRevertFrame.sourceReference !== undefined) { this._solidity063CorrectLineNumber(latestInstructionRevertFrame); } return latestInstructionRevertFrame; } } _isContractTooLargeError(trace) { return trace.exit.kind === exit_1.ExitCode.CODESIZE_EXCEEDS_MAXIMUM; } _solidity063CorrectLineNumber(revertFrame) { if (revertFrame.sourceReference === undefined) { return; } const lines = revertFrame.sourceReference.sourceContent.split("\n"); const currentLine = lines[revertFrame.sourceReference.line - 1]; if (currentLine.includes("require") || currentLine.includes("revert")) { return; } const nextLines = lines.slice(revertFrame.sourceReference.line); const firstNonEmptyLine = nextLines.findIndex((l) => l.trim() !== ""); if (firstNonEmptyLine === -1) { return; } const nextLine = nextLines[firstNonEmptyLine]; if (nextLine.includes("require") || nextLine.includes("revert")) { revertFrame.sourceReference.line += 1 + firstNonEmptyLine; } } _getLastInstructionWithValidLocationStepIndex(trace) { for (let i = trace.steps.length - 1; i >= 0; i--) { const step = trace.steps[i]; if (!(0, message_trace_1.isEvmStep)(step)) { return undefined; } const inst = trace.bytecode.getInstruction(step.pc); if (inst.location !== undefined) { return i; } } return undefined; } _getLastInstructionWithValidLocation(trace) { const lastLocationIndex = this._getLastInstructionWithValidLocationStepIndex(trace); if (lastLocationIndex === undefined) { return undefined; } const lastLocationStep = trace.steps[lastLocationIndex]; if ((0, message_trace_1.isEvmStep)(lastLocationStep)) { const lastInstructionWithLocation = trace.bytecode.getInstruction(lastLocationStep.pc); return lastInstructionWithLocation; } return undefined; } _callInstructionToCallFailedToExecuteStackTraceEntry(bytecode, callInst) { const sourceReference = sourceLocationToSourceReference(bytecode, callInst.location); (0, errors_1.assertHardhatInvariant)(sourceReference !== undefined, "Expected source reference to be defined"); // Calls only happen within functions return { type: solidity_stack_trace_1.StackTraceEntryType.CALL_FAILED_ERROR, sourceReference, }; } _getEntryBeforeFailureInModifier(trace, functionJumpdests) { // If there's a jumpdest, this modifier belongs to the last function that it represents if (functionJumpdests.length > 0) { return instructionToCallstackStackTraceEntry(trace.bytecode, functionJumpdests[functionJumpdests.length - 1]); } // This function is only called after we jumped into the initial function in call traces, so // there should always be at least a function jumpdest. if (!(0, message_trace_1.isDecodedCreateTrace)(trace)) { throw new Error("This shouldn't happen: a call trace has no functionJumpdest but has already jumped into a function"); } // If there's no jump dest, we point to the constructor. return { type: solidity_stack_trace_1.StackTraceEntryType.CALLSTACK_ENTRY, sourceReference: this._getConstructorStartSourceReference(trace), functionType: model_1.ContractFunctionType.CONSTRUCTOR, }; } _failsRightAfterCall(trace, callSubtraceStepIndex) { const lastStep = trace.steps[trace.steps.length - 1]; if (!(0, message_trace_1.isEvmStep)(lastStep)) { return false; } const lastInst = trace.bytecode.getInstruction(lastStep.pc); if (lastInst.opcode !== opcodes_1.Opcode.REVERT) { return false; } const callOpcodeStep = trace.steps[callSubtraceStepIndex - 1]; const callInst = trace.bytecode.getInstruction(callOpcodeStep.pc); // Calls are always made from within functions (0, errors_1.assertHardhatInvariant)(callInst.location !== undefined, "Expected call instruction location to be defined"); return this._isLastLocation(trace, callSubtraceStepIndex + 1, callInst.location); } _isCallFailedError(trace, instIndex, callInstruction) { const callLocation = callInstruction.location; // Calls are always made from within functions (0, errors_1.assertHardhatInvariant)(callLocation !== undefined, "Expected call location to be defined"); return this._isLastLocation(trace, instIndex, callLocation); } _isLastLocation(trace, fromStep, location) { for (let i = fromStep; i < trace.steps.length; i++) { const step = trace.steps[i]; if (!(0, message_trace_1.isEvmStep)(step)) { return false; } const stepInst = trace.bytecode.getInstruction(step.pc); if (stepInst.location === undefined) { continue; } if (!location.equals(stepInst.location)) { return false; } } return true; } _isSubtraceErrorPropagated(trace, callSubtraceStepIndex) { const call = trace.steps[callSubtraceStepIndex]; if (!(0, ethereumjs_util_1.equalsBytes)(trace.returnData, call.returnData)) { return false; } if (trace.exit.kind === exit_1.ExitCode.OUT_OF_GAS && call.exit.kind === exit_1.ExitCode.OUT_OF_GAS) { return true; } // If the return data is not empty, and it's still the same, we assume it //