UNPKG

hardhat

Version:

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

448 lines (360 loc) 13.3 kB
import { bytesToHex as bufferToHex } from "@ethereumjs/util"; import { ReturnData } from "../provider/return-data"; import { panicErrorCodeToMessage } from "./panic-errors"; import { CONSTRUCTOR_FUNCTION_NAME, PRECOMPILE_FUNCTION_NAME, SolidityStackTrace, SolidityStackTraceEntry, SourceReference, StackTraceEntryType, UNKNOWN_FUNCTION_NAME, UNRECOGNIZED_CONTRACT_NAME, UNRECOGNIZED_FUNCTION_NAME, } from "./solidity-stack-trace"; const inspect = Symbol.for("nodejs.util.inspect.custom"); export function getCurrentStack(): NodeJS.CallSite[] { const previousPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (e, s) => s; const error = new Error(); const stack: NodeJS.CallSite[] = error.stack as any; Error.prepareStackTrace = previousPrepareStackTrace; return stack; } export async function wrapWithSolidityErrorsCorrection( f: () => Promise<any>, stackFramesToRemove: number ) { const stackTraceAtCall = getCurrentStack().slice(stackFramesToRemove); try { return await f(); } catch (error: any) { if (error.stackTrace === undefined) { // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw error; } // eslint-disable-next-line @nomicfoundation/hardhat-internal-rules/only-hardhat-error throw encodeSolidityStackTrace( error.message, error.stackTrace, stackTraceAtCall ); } } export function encodeSolidityStackTrace( fallbackMessage: string, stackTrace: SolidityStackTrace, previousStack?: NodeJS.CallSite[] ): SolidityError { if (Error.prepareStackTrace === undefined) { // Node 12 doesn't have a default Error.prepareStackTrace require("source-map-support/register"); } const previousPrepareStackTrace = Error.prepareStackTrace; Error.prepareStackTrace = (error, stack) => { if (previousStack !== undefined) { stack = previousStack; } else { // We remove error management related stack traces stack.splice(0, 1); } for (const entry of stackTrace) { const callsite = encodeStackTraceEntry(entry); if (callsite === undefined) { continue; } stack.unshift(callsite); } return previousPrepareStackTrace!(error, stack); }; const msg = getMessageFromLastStackTraceEntry( stackTrace[stackTrace.length - 1] ); const solidityError = new SolidityError( msg !== undefined ? msg : fallbackMessage, stackTrace ); // This hack is here because prepare stack is lazy solidityError.stack = solidityError.stack; Error.prepareStackTrace = previousPrepareStackTrace; return solidityError; } 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.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( bufferToHex(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( bufferToHex(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 !== undefined ? 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()) { const buffer = Buffer.from(returnData.value).toString("hex"); return `VM Exception while processing transaction: reverted with an unrecognized custom error (return data: 0x${buffer})`; } 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: 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"; } } // 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 { public readonly stackTrace: SolidityStackTrace; constructor(message: string, stackTrace: SolidityStackTrace) { super(message); this.stackTrace = stackTrace; } public [inspect](): string { return this.inspect(); } public inspect(): string { return this.stack !== undefined ? this.stack : "Internal error when encoding SolidityError"; } } class SolidityCallSite implements NodeJS.CallSite { constructor( private _sourceName: string | undefined, private _contract: string | undefined, private _functionName: string | undefined, private _line: number | undefined ) {} public getColumnNumber() { return null; } public getEvalOrigin() { return undefined; } public getFileName() { return this._sourceName ?? "unknown"; } public getFunction() { return undefined; } public getFunctionName() { // if it's a top-level function, we print its name if (this._contract === undefined) { return this._functionName ?? null; } return null; } public getLineNumber() { return this._line !== undefined ? this._line : null; } public getMethodName() { 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() { 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; } public toString(): string { return "[SolidityCallSite]"; } }