hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
477 lines (396 loc) • 14.9 kB
text/typescript
import type {
SolidityStackTrace,
SolidityStackTraceEntry,
SourceReference,
} from "./solidity-stack-trace.js";
import { ReturnData } from "@nomicfoundation/edr";
import { assertHardhatInvariant } from "@nomicfoundation/hardhat-errors";
import { bytesToHexString } from "@nomicfoundation/hardhat-utils/bytes";
import { panicErrorCodeToMessage } from "@nomicfoundation/hardhat-utils/panic-errors";
import {
StackTraceEntryType,
CONSTRUCTOR_FUNCTION_NAME,
PRECOMPILE_FUNCTION_NAME,
UNKNOWN_FUNCTION_NAME,
UNRECOGNIZED_CONTRACT_NAME,
UNRECOGNIZED_FUNCTION_NAME,
} from "./solidity-stack-trace.js";
export function createSolidityErrorWithStackTrace(
fallbackMessage: string,
stackTrace: SolidityStackTrace,
data: string,
transactionHash?: string,
): SolidityError {
const originalPrepareStackTrace = Error.prepareStackTrace;
try {
Error.prepareStackTrace = (error, stack) => {
// Skip error management related stack traces
const adjustedStack = stack.slice(1);
for (const entry of stackTrace) {
const callsite = encodeStackTraceEntry(entry);
if (callsite !== undefined) {
adjustedStack.unshift(callsite);
}
}
assertHardhatInvariant(
originalPrepareStackTrace !== undefined,
"Error.prepareStackTrace should be defined",
);
return originalPrepareStackTrace(error, adjustedStack);
};
const message =
getMessageFromLastStackTraceEntry(stackTrace[stackTrace.length - 1]) ??
fallbackMessage;
const solidityError = new SolidityError(
message,
stackTrace,
data,
transactionHash,
);
/* eslint-disable-next-line @typescript-eslint/no-unused-expressions
-- As the stack property is lazy-loaded in v8, we need to access it
to trigger the custom prepareStackTrace logic */
solidityError.stack;
return solidityError;
} finally {
Error.prepareStackTrace = originalPrepareStackTrace;
}
}
export function encodeStackTraceEntry(
stackTraceEntry: SolidityStackTraceEntry,
): SolidityCallSite {
switch (stackTraceEntry.type) {
case StackTraceEntryType.UNRECOGNIZED_FUNCTION_WITHOUT_FALLBACK_ERROR:
case StackTraceEntryType.MISSING_FALLBACK_OR_RECEIVE_ERROR:
return sourceReferenceToSolidityCallsite({
...stackTraceEntry.sourceReference,
function: UNRECOGNIZED_FUNCTION_NAME,
});
case StackTraceEntryType.CALLSTACK_ENTRY:
case StackTraceEntryType.REVERT_ERROR:
case StackTraceEntryType.CUSTOM_ERROR:
case StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR:
case StackTraceEntryType.INVALID_PARAMS_ERROR:
case StackTraceEntryType.FALLBACK_NOT_PAYABLE_ERROR:
case StackTraceEntryType.FALLBACK_NOT_PAYABLE_AND_NO_RECEIVE_ERROR:
case StackTraceEntryType.RETURNDATA_SIZE_ERROR:
case StackTraceEntryType.NONCONTRACT_ACCOUNT_CALLED_ERROR:
case StackTraceEntryType.CALL_FAILED_ERROR:
case StackTraceEntryType.CHEATCODE_ERROR:
case StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR:
return sourceReferenceToSolidityCallsite(stackTraceEntry.sourceReference);
case StackTraceEntryType.UNRECOGNIZED_CREATE_CALLSTACK_ENTRY:
return new SolidityCallSite(
undefined,
UNRECOGNIZED_CONTRACT_NAME,
CONSTRUCTOR_FUNCTION_NAME,
undefined,
);
case StackTraceEntryType.UNRECOGNIZED_CONTRACT_CALLSTACK_ENTRY:
return new SolidityCallSite(
bytesToHexString(stackTraceEntry.address),
UNRECOGNIZED_CONTRACT_NAME,
UNKNOWN_FUNCTION_NAME,
undefined,
);
case StackTraceEntryType.PRECOMPILE_ERROR:
return new SolidityCallSite(
undefined,
`<PrecompileContract ${stackTraceEntry.precompile}>`,
PRECOMPILE_FUNCTION_NAME,
undefined,
);
case StackTraceEntryType.UNRECOGNIZED_CREATE_ERROR:
return new SolidityCallSite(
undefined,
UNRECOGNIZED_CONTRACT_NAME,
CONSTRUCTOR_FUNCTION_NAME,
undefined,
);
case StackTraceEntryType.UNRECOGNIZED_CONTRACT_ERROR:
return new SolidityCallSite(
bytesToHexString(stackTraceEntry.address),
UNRECOGNIZED_CONTRACT_NAME,
UNKNOWN_FUNCTION_NAME,
undefined,
);
case StackTraceEntryType.INTERNAL_FUNCTION_CALLSTACK_ENTRY:
return new SolidityCallSite(
stackTraceEntry.sourceReference.sourceName,
stackTraceEntry.sourceReference.contract,
`internal@${stackTraceEntry.pc}`,
undefined,
);
case StackTraceEntryType.CONTRACT_CALL_RUN_OUT_OF_GAS_ERROR:
if (stackTraceEntry.sourceReference !== undefined) {
return sourceReferenceToSolidityCallsite(
stackTraceEntry.sourceReference,
);
}
return new SolidityCallSite(
undefined,
UNRECOGNIZED_CONTRACT_NAME,
UNKNOWN_FUNCTION_NAME,
undefined,
);
case StackTraceEntryType.OTHER_EXECUTION_ERROR:
case StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR:
case StackTraceEntryType.PANIC_ERROR:
case StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR:
if (stackTraceEntry.sourceReference === undefined) {
return new SolidityCallSite(
undefined,
UNRECOGNIZED_CONTRACT_NAME,
UNKNOWN_FUNCTION_NAME,
undefined,
);
}
return sourceReferenceToSolidityCallsite(stackTraceEntry.sourceReference);
}
}
function sourceReferenceToSolidityCallsite(
sourceReference: SourceReference,
): SolidityCallSite {
return new SolidityCallSite(
sourceReference.sourceName,
sourceReference.contract,
sourceReference.function ?? UNKNOWN_FUNCTION_NAME,
sourceReference.line,
);
}
function getMessageFromLastStackTraceEntry(
stackTraceEntry: SolidityStackTraceEntry,
): string | undefined {
switch (stackTraceEntry.type) {
case StackTraceEntryType.PRECOMPILE_ERROR:
return `Transaction reverted: call to precompile ${stackTraceEntry.precompile} failed`;
case StackTraceEntryType.FUNCTION_NOT_PAYABLE_ERROR:
return `Transaction reverted: non-payable function was called with value ${stackTraceEntry.value.toString(
10,
)}`;
case StackTraceEntryType.INVALID_PARAMS_ERROR:
return `Transaction reverted: function was called with incorrect parameters`;
case StackTraceEntryType.FALLBACK_NOT_PAYABLE_ERROR:
return `Transaction reverted: fallback function is not payable and was called with value ${stackTraceEntry.value.toString(
10,
)}`;
case StackTraceEntryType.FALLBACK_NOT_PAYABLE_AND_NO_RECEIVE_ERROR:
return `Transaction reverted: there's no receive function, fallback function is not payable and was called with value ${stackTraceEntry.value.toString(
10,
)}`;
case StackTraceEntryType.UNRECOGNIZED_FUNCTION_WITHOUT_FALLBACK_ERROR:
return `Transaction reverted: function selector was not recognized and there's no fallback function`;
case StackTraceEntryType.MISSING_FALLBACK_OR_RECEIVE_ERROR:
return `Transaction reverted: function selector was not recognized and there's no fallback nor receive function`;
case StackTraceEntryType.RETURNDATA_SIZE_ERROR:
return `Transaction reverted: function returned an unexpected amount of data`;
case StackTraceEntryType.NONCONTRACT_ACCOUNT_CALLED_ERROR:
return `Transaction reverted: function call to a non-contract account`;
case StackTraceEntryType.CALL_FAILED_ERROR:
return `Transaction reverted: function call failed to execute`;
case StackTraceEntryType.DIRECT_LIBRARY_CALL_ERROR:
return `Transaction reverted: library was called directly`;
case StackTraceEntryType.UNRECOGNIZED_CREATE_ERROR:
case StackTraceEntryType.UNRECOGNIZED_CONTRACT_ERROR: {
const returnData = new ReturnData(stackTraceEntry.returnData);
if (returnData.isErrorReturnData()) {
return `VM Exception while processing transaction: reverted with reason string '${returnData.decodeError()}'`;
}
if (returnData.isPanicReturnData()) {
const message = panicErrorCodeToMessage(returnData.decodePanic());
return `VM Exception while processing transaction: ${message}`;
}
if (!returnData.isEmpty()) {
return `VM Exception while processing transaction: reverted with an unrecognized custom error (return data: ${bytesToHexString(returnData.value)})`;
}
if (stackTraceEntry.isInvalidOpcodeError) {
return "VM Exception while processing transaction: invalid opcode";
}
return "Transaction reverted without a reason string";
}
case StackTraceEntryType.REVERT_ERROR: {
const returnData = new ReturnData(stackTraceEntry.returnData);
if (returnData.isErrorReturnData()) {
return `VM Exception while processing transaction: reverted with reason string '${returnData.decodeError()}'`;
}
if (stackTraceEntry.isInvalidOpcodeError) {
return "VM Exception while processing transaction: invalid opcode";
}
return "Transaction reverted without a reason string";
}
case StackTraceEntryType.PANIC_ERROR:
const panicMessage = panicErrorCodeToMessage(stackTraceEntry.errorCode);
return `VM Exception while processing transaction: ${panicMessage}`;
case StackTraceEntryType.CUSTOM_ERROR:
case StackTraceEntryType.CHEATCODE_ERROR:
return `VM Exception while processing transaction: ${stackTraceEntry.message}`;
case StackTraceEntryType.OTHER_EXECUTION_ERROR:
// TODO: What if there was returnData?
return `Transaction reverted and Hardhat couldn't infer the reason.`;
case StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR:
return "Transaction reverted without a reason string and without a valid sourcemap provided by the compiler. Some line numbers may be off. We strongly recommend upgrading solc and always using revert reasons.";
case StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR:
return "Transaction reverted: trying to deploy a contract whose code is too large";
case StackTraceEntryType.CONTRACT_CALL_RUN_OUT_OF_GAS_ERROR:
return "Transaction reverted: contract call run out of gas and made the transaction revert";
/* These types are not expected to be the last entry in the stack trace, as
their presence indicates that another frame should follow in the call stack. */
case StackTraceEntryType.CALLSTACK_ENTRY:
case StackTraceEntryType.UNRECOGNIZED_CREATE_CALLSTACK_ENTRY:
case StackTraceEntryType.UNRECOGNIZED_CONTRACT_CALLSTACK_ENTRY:
case StackTraceEntryType.INTERNAL_FUNCTION_CALLSTACK_ENTRY:
return undefined;
}
}
/**
* Note: This error class MUST NOT extend ProviderError, as libraries use the
* code property to detect if they are dealing with a JSON-RPC error, and take
* control of errors.
**/
export class SolidityError extends Error {
constructor(
message: string,
public readonly stackTrace: SolidityStackTrace,
public readonly data: string,
public readonly transactionHash?: string,
) {
super(message);
Object.defineProperty(this, Symbol.for("nodejs.util.inspect.custom"), {
value: () =>
this.stack !== undefined
? this.stack
: "Internal error when encoding SolidityError",
writable: false,
enumerable: false,
configurable: true,
});
}
}
export class SolidityCallSite implements NodeJS.CallSite {
readonly #sourceName: string | undefined;
readonly #contract: string | undefined;
readonly #functionName: string | undefined;
readonly #line: number | undefined;
constructor(
_sourceName: string | undefined,
_contract: string | undefined,
_functionName: string | undefined,
_line: number | undefined,
) {
// If the source name starts with `project/` that means that's a local
// source name, and we remove that.
const LOCAL_SOURCE_NAME_PREFIX = "project/";
this.#sourceName =
_sourceName?.startsWith(LOCAL_SOURCE_NAME_PREFIX) === true
? _sourceName.substring(LOCAL_SOURCE_NAME_PREFIX.length)
: _sourceName;
this.#contract = _contract;
this.#functionName = _functionName;
this.#line = _line;
}
public getColumnNumber() {
return null;
}
public getEvalOrigin() {
return undefined;
}
public getFileName(): string {
return this.#sourceName ?? "unknown";
}
public getFunction() {
return undefined;
}
public getFunctionName(): string | null {
// if it's a top-level function, we print its name
if (this.#contract === undefined) {
return this.#functionName ?? null;
}
return null;
}
public getLineNumber(): number | null {
return this.#line !== undefined ? this.#line : null;
}
public getMethodName(): string | null {
if (this.#contract !== undefined) {
return this.#functionName ?? null;
}
return null;
}
public getPosition() {
return 0;
}
public getPromiseIndex() {
return 0;
}
public getScriptNameOrSourceURL() {
return "";
}
public getThis() {
return undefined;
}
public getTypeName(): string | null {
return this.#contract ?? null;
}
public isAsync() {
return false;
}
public isConstructor() {
return false;
}
public isEval() {
return false;
}
public isNative() {
return false;
}
public isPromiseAll() {
return false;
}
public isToplevel() {
return false;
}
public getScriptHash(): string {
return "";
}
public getEnclosingColumnNumber(): number {
return 0;
}
public getEnclosingLineNumber(): number {
return 0;
}
// Extracted and adapted from source-map-support package, which extracts it from V8
public toString(): string {
let fileLocation = this.getFileName();
if (this.getLineNumber() !== null) {
fileLocation += `:${this.getLineNumber()}`;
}
let line = "";
const functionName = this.getFunctionName();
let addSuffix = true;
const isConstructor = this.isConstructor();
const isMethodCall = !(this.isToplevel() || isConstructor);
if (isMethodCall) {
const typeName = this.getTypeName();
const methodName = this.getMethodName();
if (functionName !== null) {
if (typeName !== null) {
line += typeName + ".";
}
line += functionName;
} else {
line += typeName + "." + (methodName ?? "<anonymous>");
}
} else if (isConstructor) {
line += "new " + (functionName ?? "<anonymous>");
} else if (functionName !== null) {
line += functionName;
} else {
line += fileLocation;
addSuffix = false;
}
if (addSuffix) {
line += " (" + fileLocation + ")";
}
return line;
}
}