hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
1,672 lines (1,437 loc) • 54.7 kB
text/typescript
/* eslint "@typescript-eslint/no-non-null-assertion": "error" */
import { defaultAbiCoder as abi } from "@ethersproject/abi";
import { equalsBytes } from "@nomicfoundation/ethereumjs-util";
import semver from "semver";
import { assertHardhatInvariant } from "../../core/errors";
import { AbiHelpers } from "../../util/abi-helpers";
import { ReturnData } from "../provider/return-data";
import { ExitCode } from "../provider/vm/exit";
import {
DecodedCallMessageTrace,
DecodedCreateMessageTrace,
DecodedEvmMessageTrace,
EvmStep,
isCreateTrace,
isDecodedCallTrace,
isDecodedCreateTrace,
isEvmStep,
isPrecompileTrace,
MessageTrace,
} from "./message-trace";
import {
Bytecode,
ContractFunction,
ContractFunctionType,
ContractType,
Instruction,
JumpType,
SourceLocation,
} from "./model";
import { isCall, isCreate, Opcode } from "./opcodes";
import {
CallFailedErrorStackTraceEntry,
CallstackEntryStackTraceEntry,
CONSTRUCTOR_FUNCTION_NAME,
CustomErrorStackTraceEntry,
FALLBACK_FUNCTION_NAME,
InternalFunctionCallStackEntry,
OtherExecutionErrorStackTraceEntry,
PanicErrorStackTraceEntry,
RECEIVE_FUNCTION_NAME,
RevertErrorStackTraceEntry,
SolidityStackTrace,
SolidityStackTraceEntry,
SourceReference,
StackTraceEntryType,
UnmappedSolc063RevertErrorStackTraceEntry,
} from "./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";
export interface SubmessageData {
messageTrace: MessageTrace;
stacktrace: SolidityStackTrace;
stepIndex: number;
}
/* eslint-disable @nomicfoundation/hardhat-internal-rules/only-hardhat-error */
export class ErrorInferrer {
public inferBeforeTracingCallMessage(
trace: DecodedCallMessageTrace
): SolidityStackTrace | undefined {
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: StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR,
sourceReference: this._getFunctionStartSourceReference(
trace,
calledFunction
),
value: trace.value,
},
];
}
if (this._isMissingFunctionAndFallbackError(trace, calledFunction)) {
if (this._emptyCalldataAndNoReceive(trace)) {
return [
{
type: StackTraceEntryType.MISSING_FALLBACK_OR_RECEIVE_ERROR,
sourceReference:
this._getContractStartWithoutFunctionSourceReference(trace),
},
];
}
return [
{
type: StackTraceEntryType.UNRECOGNIZED_FUNCTION_WITHOUT_FALLBACK_ERROR,
sourceReference:
this._getContractStartWithoutFunctionSourceReference(trace),
},
];
}
if (this._isFallbackNotPayableError(trace, calledFunction)) {
if (this._emptyCalldataAndNoReceive(trace)) {
return [
{
type: StackTraceEntryType.FALLBACK_NOT_PAYABLE_AND_NO_RECEIVE_ERROR,
sourceReference: this._getFallbackStartSourceReference(trace),
value: trace.value,
},
];
}
return [
{
type: StackTraceEntryType.FALLBACK_NOT_PAYABLE_ERROR,
sourceReference: this._getFallbackStartSourceReference(trace),
value: trace.value,
},
];
}
}
public inferBeforeTracingCreateMessage(
trace: DecodedCreateMessageTrace
): SolidityStackTrace | undefined {
if (this._isConstructorNotPayableError(trace)) {
return [
{
type: StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR,
sourceReference: this._getConstructorStartSourceReference(trace),
value: trace.value,
},
];
}
if (this._isConstructorInvalidArgumentsError(trace)) {
return [
{
type: StackTraceEntryType.INVALID_PARAMS_ERROR,
sourceReference: this._getConstructorStartSourceReference(trace),
},
];
}
}
public inferAfterTracing(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace,
functionJumpdests: Instruction[],
jumpedIntoFunction: boolean,
lastSubmessageData: SubmessageData | undefined
): SolidityStackTrace {
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)
);
}
public filterRedundantFrames(
stacktrace: SolidityStackTrace
): SolidityStackTrace {
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 === StackTraceEntryType.CALLSTACK_ENTRY &&
i + 2 < stacktrace.length &&
stacktrace[i + 2].sourceReference !== undefined &&
stacktrace[i + 2].type === 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.
*/
private _checkLastSubmessage(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace,
lastSubmessageData: SubmessageData | undefined
): SolidityStackTrace | undefined {
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 (!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();
assertHardhatInvariant(
lastFrame !== undefined,
"Expected inferred stack trace to have at least one frame"
);
inferredStacktrace.push({
type: 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: StackTraceEntryType.RETURNDATA_SIZE_ERROR,
sourceReference: callStackFrame.sourceReference,
});
return this._fixInitialModifier(trace, inferredStacktrace);
}
}
}
/**
* Check if the last call/create that was done failed.
*/
private _checkFailedLastCall(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace
): SolidityStackTrace | undefined {
for (let stepIndex = trace.steps.length - 2; stepIndex >= 0; stepIndex--) {
const step = trace.steps[stepIndex];
const nextStep = trace.steps[stepIndex + 1];
if (!isEvmStep(step)) {
return;
}
const inst = trace.bytecode.getInstruction(step.pc);
const isCallOrCreate = isCall(inst.opcode) || isCreate(inst.opcode);
if (isCallOrCreate && 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.
*/
private _checkRevertOrInvalidOpcode(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace,
lastInstruction: Instruction,
functionJumpdests: Instruction[],
jumpedIntoFunction: boolean
): SolidityStackTrace | undefined {
if (
lastInstruction.opcode !== Opcode.REVERT &&
lastInstruction.opcode !== Opcode.INVALID
) {
return;
}
const inferredStacktrace = [...stacktrace];
if (
lastInstruction.location !== undefined &&
(!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 === 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 &&
(!isDecodedCallTrace(trace) || jumpedIntoFunction)
) {
const failingFunction = lastInstruction.location.getContainingFunction();
if (failingFunction !== undefined) {
inferredStacktrace.push(
this._instructionWithinFunctionToRevertStackTraceEntry(
trace,
lastInstruction
)
);
} else if (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: StackTraceEntryType.REVERT_ERROR,
sourceReference: this._getFunctionStartSourceReference(
trace,
functionSelector
),
message: new ReturnData(trace.returnData),
isInvalidOpcodeError: lastInstruction.opcode === Opcode.INVALID,
});
} else {
// This is here because of the optimizations
inferredStacktrace.push({
type: StackTraceEntryType.REVERT_ERROR,
sourceReference: this._getConstructorStartSourceReference(trace),
message: new ReturnData(trace.returnData),
isInvalidOpcodeError: lastInstruction.opcode === 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: RevertErrorStackTraceEntry = {
type: StackTraceEntryType.REVERT_ERROR,
sourceReference:
this._getLastSourceReference(trace) ??
this._getContractStartWithoutFunctionSourceReference(trace),
message: new ReturnData(trace.returnData),
isInvalidOpcodeError: lastInstruction.opcode === Opcode.INVALID,
};
inferredStacktrace.push(revertFrame);
return this._fixInitialModifier(trace, inferredStacktrace);
}
}
/**
* Check if the trace reverted with a panic error.
*/
private _checkPanic(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace,
lastInstruction: Instruction
): SolidityStackTrace | undefined {
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 === StackTraceEntryType.INTERNAL_FUNCTION_CALLSTACK_ENTRY
) {
stacktrace.splice(-1);
}
const panicReturnData = new 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);
}
private _checkCustomErrors(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace,
lastInstruction: Instruction
): SolidityStackTrace | undefined {
const returnData = new 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.decode(
customError.paramTypes,
returnData.value.slice(4)
);
const params = 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.
*/
private _checkLastInstruction(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace,
functionJumpdests: Instruction[],
jumpedIntoFunction: boolean
): SolidityStackTrace | undefined {
if (trace.steps.length === 0) {
return;
}
const lastStep = trace.steps[trace.steps.length - 1];
if (!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 (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: StackTraceEntryType.REVERT_ERROR,
sourceReference: this._getFunctionStartSourceReference(
trace,
failingFunction
),
message: new ReturnData(trace.returnData),
isInvalidOpcodeError: lastInstruction.opcode === 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: 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)];
}
}
private _checkNonContractCalled(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace
): SolidityStackTrace | undefined {
if (this._isCalledNonContractAccountError(trace)) {
const sourceReference = this._getLastSourceReference(trace);
// We are sure this is not undefined because there was at least a call instruction
assertHardhatInvariant(
sourceReference !== undefined,
"Expected source reference to be defined"
);
const nonContractCalledFrame: SolidityStackTraceEntry = {
type: StackTraceEntryType.NONCONTRACT_ACCOUNT_CALLED_ERROR,
sourceReference,
};
return [...stacktrace, nonContractCalledFrame];
}
}
private _checkSolidity063UnmappedRevert(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace
): SolidityStackTrace | undefined {
if (this._solidity063MaybeUnmappedRevert(trace)) {
const revertFrame =
this._solidity063GetFrameForUnmappedRevertWithinFunction(trace);
if (revertFrame !== undefined) {
return [...stacktrace, revertFrame];
}
}
}
private _checkContractTooLarge(
trace: DecodedEvmMessageTrace
): SolidityStackTrace | undefined {
if (isCreateTrace(trace) && this._isContractTooLargeError(trace)) {
return [
{
type: StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR,
sourceReference: this._getConstructorStartSourceReference(trace),
},
];
}
}
private _otherExecutionErrorStacktrace(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace
): SolidityStackTrace {
const otherExecutionErrorFrame: SolidityStackTraceEntry = {
type: StackTraceEntryType.OTHER_EXECUTION_ERROR,
sourceReference: this._getLastSourceReference(trace),
};
return [...stacktrace, otherExecutionErrorFrame];
}
// Helpers
private _fixInitialModifier(
trace: DecodedEvmMessageTrace,
stacktrace: SolidityStackTrace
): SolidityStackTrace {
const firstEntry = stacktrace[0];
if (
firstEntry !== undefined &&
firstEntry.type === StackTraceEntryType.CALLSTACK_ENTRY &&
firstEntry.functionType === ContractFunctionType.MODIFIER
) {
return [
this._getEntryBeforeInitialModifierCallstackEntry(trace),
...stacktrace,
];
}
return stacktrace;
}
private _isDirectLibraryCall(trace: DecodedCallMessageTrace): boolean {
return (
trace.depth === 0 && trace.bytecode.contract.type === ContractType.LIBRARY
);
}
private _getDirectLibraryCallErrorStackTrace(
trace: DecodedCallMessageTrace
): SolidityStackTrace {
const func = trace.bytecode.contract.getFunctionFromSelector(
trace.calldata.slice(0, 4)
);
if (func !== undefined) {
return [
{
type: StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR,
sourceReference: this._getFunctionStartSourceReference(trace, func),
},
];
}
return [
{
type: StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR,
sourceReference:
this._getContractStartWithoutFunctionSourceReference(trace),
},
];
}
private _isFunctionNotPayableError(
trace: DecodedCallMessageTrace,
calledFunction: ContractFunction
): boolean {
// 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 === ContractType.LIBRARY) {
return false;
}
return calledFunction.isPayable === undefined || !calledFunction.isPayable;
}
private _getFunctionStartSourceReference(
trace: DecodedEvmMessageTrace,
func: ContractFunction
): SourceReference {
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,
],
};
}
private _isMissingFunctionAndFallbackError(
trace: DecodedCallMessageTrace,
calledFunction: ContractFunction | undefined
): boolean {
// 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;
}
private _emptyCalldataAndNoReceive(trace: DecodedCallMessageTrace): boolean {
// this only makes sense when receive functions are available
if (
semver.lt(
trace.bytecode.compilerVersion,
FIRST_SOLC_VERSION_RECEIVE_FUNCTION
)
) {
return false;
}
return (
trace.calldata.length === 0 &&
trace.bytecode.contract.receive === undefined
);
}
private _getContractStartWithoutFunctionSourceReference(
trace: DecodedEvmMessageTrace
): SourceReference {
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],
};
}
private _isFallbackNotPayableError(
trace: DecodedCallMessageTrace,
calledFunction: ContractFunction | undefined
): boolean {
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;
}
private _getFallbackStartSourceReference(
trace: DecodedCallMessageTrace
): SourceReference {
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: FALLBACK_FUNCTION_NAME,
line: func.location.getStartingLineNumber(),
range: [
func.location.offset,
func.location.offset + func.location.length,
],
};
}
private _isConstructorNotPayableError(
trace: DecodedCreateMessageTrace
): boolean {
// 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.
*/
private _getConstructorStartSourceReference(
trace: DecodedCreateMessageTrace
): SourceReference {
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: CONSTRUCTOR_FUNCTION_NAME,
line,
range: [
contract.location.offset,
contract.location.offset + contract.location.length,
],
};
}
private _isConstructorInvalidArgumentsError(
trace: DecodedCreateMessageTrace
): boolean {
// 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.lt(
trace.bytecode.compilerVersion,
FIRST_SOLC_VERSION_CREATE_PARAMS_VALIDATION
)
) {
return false;
}
const lastStep = trace.steps[trace.steps.length - 1];
if (!isEvmStep(lastStep)) {
return false;
}
const lastInst = trace.bytecode.getInstruction(lastStep.pc);
if (lastInst.opcode !== 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 (!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 === Opcode.CODESIZE && isCreateTrace(trace)) {
hasReadDeploymentCodeSize = true;
}
}
return hasReadDeploymentCodeSize;
}
private _getEntryBeforeInitialModifierCallstackEntry(
trace: DecodedEvmMessageTrace
): SolidityStackTraceEntry {
if (isDecodedCreateTrace(trace)) {
return {
type: StackTraceEntryType.CALLSTACK_ENTRY,
sourceReference: this._getConstructorStartSourceReference(trace),
functionType: ContractFunctionType.CONSTRUCTOR,
};
}
const calledFunction = trace.bytecode.contract.getFunctionFromSelector(
trace.calldata.slice(0, 4)
);
if (calledFunction !== undefined) {
return {
type: StackTraceEntryType.CALLSTACK_ENTRY,
sourceReference: this._getFunctionStartSourceReference(
trace,
calledFunction
),
functionType: 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: StackTraceEntryType.CALLSTACK_ENTRY,
sourceReference: this._getFallbackStartSourceReference(trace),
functionType: ContractFunctionType.FALLBACK,
};
}
private _getLastSourceReference(
trace: DecodedEvmMessageTrace
): SourceReference | undefined {
for (let i = trace.steps.length - 1; i >= 0; i--) {
const step = trace.steps[i];
if (!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;
}
private _hasFailedInsideTheFallbackFunction(
trace: DecodedCallMessageTrace
): boolean {
const contract = trace.bytecode.contract;
if (contract.fallback === undefined) {
return false;
}
return this._hasFailedInsideFunction(trace, contract.fallback);
}
private _hasFailedInsideTheReceiveFunction(
trace: DecodedCallMessageTrace
): boolean {
const contract = trace.bytecode.contract;
if (contract.receive === undefined) {
return false;
}
return this._hasFailedInsideFunction(trace, contract.receive);
}
private _hasFailedInsideFunction(
trace: DecodedCallMessageTrace,
func: ContractFunction
) {
const lastStep = trace.steps[trace.steps.length - 1] as EvmStep;
const lastInstruction = trace.bytecode.getInstruction(lastStep.pc);
return (
lastInstruction.location !== undefined &&
lastInstruction.opcode === Opcode.REVERT &&
func.location.contains(lastInstruction.location)
);
}
private _instructionWithinFunctionToRevertStackTraceEntry(
trace: DecodedEvmMessageTrace,
inst: Instruction
): RevertErrorStackTraceEntry {
const sourceReference = sourceLocationToSourceReference(
trace.bytecode,
inst.location
);
assertHardhatInvariant(
sourceReference !== undefined,
"Expected source reference to be defined"
);
return {
type: StackTraceEntryType.REVERT_ERROR,
sourceReference,
message: new ReturnData(trace.returnData),
isInvalidOpcodeError: inst.opcode === Opcode.INVALID,
};
}
private _instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(
trace: DecodedEvmMessageTrace,
inst: Instruction
): UnmappedSolc063RevertErrorStackTraceEntry {
const sourceReference = sourceLocationToSourceReference(
trace.bytecode,
inst.location
);
return {
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
sourceReference,
};
}
private _instructionWithinFunctionToPanicStackTraceEntry(
trace: DecodedEvmMessageTrace,
inst: Instruction,
errorCode: bigint
): PanicErrorStackTraceEntry {
const lastSourceReference = this._getLastSourceReference(trace);
return {
type: StackTraceEntryType.PANIC_ERROR,
sourceReference:
sourceLocationToSourceReference(trace.bytecode, inst.location) ??
lastSourceReference,
errorCode,
};
}
private _instructionWithinFunctionToCustomErrorStackTraceEntry(
trace: DecodedEvmMessageTrace,
inst: Instruction,
message: string
): CustomErrorStackTraceEntry {
const lastSourceReference = this._getLastSourceReference(trace);
assertHardhatInvariant(
lastSourceReference !== undefined,
"Expected last source reference to be defined"
);
return {
type: StackTraceEntryType.CUSTOM_ERROR,
sourceReference:
sourceLocationToSourceReference(trace.bytecode, inst.location) ??
lastSourceReference,
message,
};
}
private _solidity063MaybeUnmappedRevert(trace: DecodedEvmMessageTrace) {
if (trace.steps.length === 0) {
return false;
}
const lastStep = trace.steps[trace.steps.length - 1];
if (!isEvmStep(lastStep)) {
return false;
}
const lastInst = trace.bytecode.getInstruction(lastStep.pc);
return (
semver.satisfies(
trace.bytecode.compilerVersion,
`^${FIRST_SOLC_VERSION_WITH_UNMAPPED_REVERTS}`
) && lastInst.opcode === Opcode.REVERT
);
}
// Solidity 0.6.3 unmapped reverts special handling
// For more info: https://github.com/ethereum/solidity/issues/9006
private _solidity063GetFrameForUnmappedRevertBeforeFunction(
trace: DecodedCallMessageTrace
) {
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: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
sourceReference: {
contract: trace.bytecode.contract.name,
function: 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: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
sourceReference: {
contract: trace.bytecode.contract.name,
function: 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;
}
private _getOtherErrorBeforeCalledFunctionStackTraceEntry(
trace: DecodedCallMessageTrace
): OtherExecutionErrorStackTraceEntry {
return {
type: StackTraceEntryType.OTHER_EXECUTION_ERROR,
sourceReference:
this._getContractStartWithoutFunctionSourceReference(trace),
};
}
private _isCalledNonContractAccountError(
trace: DecodedEvmMessageTrace
): boolean {
// 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] as EvmStep; // We know this is an EVM step
const lastInst = trace.bytecode.getInstruction(lastStep.pc);
if (lastInst.opcode !== Opcode.ISZERO) {
return false;
}
const prevStep = trace.steps[lastIndex - 1] as EvmStep; // We know this is an EVM step
const prevInst = trace.bytecode.getInstruction(prevStep.pc);
return prevInst.opcode === Opcode.EXTCODESIZE;
}
private _solidity063GetFrameForUnmappedRevertWithinFunction(
trace: DecodedEvmMessageTrace
): UnmappedSolc063RevertErrorStackTraceEntry | undefined {
// 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] as EvmStep;
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: UnmappedSolc063RevertErrorStackTraceEntry | undefined;
// 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 (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: UnmappedSolc063RevertErrorStackTraceEntry =
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: SourceReference = {
function: 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: UnmappedSolc063RevertErrorStackTraceEntry =
this._instructionWithinFunctionToUnmappedSolc063RevertErrorStackTraceEntry(
trace,
prevInst
);
if (latestInstructionRevertFrame.sourceReference !== undefined) {
this._solidity063CorrectLineNumber(latestInstructionRevertFrame);
}
return latestInstructionRevertFrame;
}
}
private _isContractTooLargeError(trace: DecodedCreateMessageTrace) {
return trace.exit.kind === ExitCode.CODESIZE_EXCEEDS_MAXIMUM;
}
private _solidity063CorrectLineNumber(
revertFrame: UnmappedSolc063RevertErrorStackTraceEntry
) {
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;
}
}
private _getLastInstructionWithValidLocationStepIndex(
trace: DecodedEvmMessageTrace
): number | undefined {
for (let i = trace.steps.length - 1; i >= 0; i--) {
const step = trace.steps[i];
if (!isEvmStep(step)) {
return undefined;
}
const inst = trace.bytecode.getInstruction(step.pc);
if (inst.location !== undefined) {
return i;
}
}
return undefined;
}
private _getLastInstructionWithValidLocation(
trace: DecodedEvmMessageTrace
): Instruction | undefined {
const lastLocationIndex =
this._getLastInstructionWithValidLocationStepIndex(trace);
if (lastLocationIndex === undefined) {
return undefined;
}
const lastLocationStep = trace.steps[lastLocationIndex];
if (isEvmStep(lastLocationStep)) {
const lastInstructionWithLocation = trace.bytecode.getInstruction(
lastLocationStep.pc
);
return lastInstructionWithLocation;
}
return undefined;
}
private _callInstructionToCallFailedToExecuteStackTraceEntry(
bytecode: Bytecode,
callInst: Instruction
): CallFailedErrorStackTraceEntry {
const sourceReference = sourceLocationToSourceReference(
bytecode,
callInst.location
);
assertHardhatInvariant(
sourceReference !== undefined,
"Expected source reference to be defined"
);
// Calls only happen within functions
return {
type: StackTraceEntryType.CALL_FAILED_ERROR,
sourceReference,
};
}
private _getEntryBeforeFailureInModifier(
trace: DecodedEvmMessageTrace,
functionJumpdests: Instruction[]
): CallstackEntryStackTraceEntry | InternalFunctionCallStackEntry {
// 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 (!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: StackTraceEntryType.CALLSTACK_ENTRY,
sourceReference: this._getConstructorStartSourceReference(trace),
functionType: ContractFunctionType.CONSTRUCTOR,
};
}
private _failsRightAfterCall(
trace: DecodedEvmMessageTrace,
callSubtraceStepIndex: number
): boolean {
const lastStep = trace.steps[trace.steps.length - 1];
if (!isEvmStep(lastStep)) {
return false;
}
const lastInst = trace.bytecode.getInstruction(lastStep.pc);
if (lastInst.opcode !== Opcode.REVERT) {
return false;
}
const callOpcodeStep = trace.steps[callSubtraceStepIndex - 1] as EvmStep;
const callInst = trace.bytecode.getInstruction(callOpcodeStep.pc);
// Calls are always made from within functions
assertHardhatInvariant(
callInst.location !== undefined,
"Expected call instruction location to be defined"
);
return this._isLastLocation(
trace,
callSubtraceStepIndex + 1,
callInst.location
);
}
private _isCallFailedError(
trace: DecodedEvmMessageTrace,
instIndex: number,
callInstruction: Instruction
): boolean {
const callLocation = callInstruction.location;
// Calls are always made from within functions
assertHardhatInvariant(
callLocation !== undefined,
"Expected call location to be defined"
);
return this._isLastLocation(trace, instIndex, callLocation);
}
private _isLastLocation(
trace: DecodedEvmMessageTrace,
fromStep: number,
location: SourceLocation
): boolean {
for (let i = fromStep; i < trace.steps.length; i++) {
const step = trace.steps[i];
if (!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;
}
private _isSubtraceErrorPropagated(
trace: DecodedEvmMessageTrace,
callSubtraceStepIndex: number
): boolean {
const call = trace.steps[callSubtraceStepIndex] as MessageTrace;
if (!equalsBytes(trace.returnData, call.returnData)) {
return false;
}
if (
trace.exit.kind === ExitCode.OUT_OF_GAS &&
call.exit.kind === ExitCode.OUT_OF_GAS
) {
return true;
}
// If the return data is not empty, and it's still the same, we assume it
// is being propagated
if (trace.returnData.length > 0) {
return true;
}
return this._failsRightAfterCall(trace, callSubtraceStepIndex);
}
private _isProxyErrorPropagated(
trace: DecodedEvmMessageTrace,
callSubtraceStepIndex: number
): boolean {
if (!isDecodedCallTrace(trace)) {
return false;
}
const callStep = trace.steps[callSubtraceStepIndex - 1];
if (!isEvmStep(callStep)) {
return false;
}
const callInst = trace.bytecode.getInstruction(callStep.pc);
if (callInst.opcode !== Opcode.DELEGATECALL) {
return false;
}
const subtrace