@nomiclabs/buidler
Version:
Buidler is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
1,513 lines (1,283 loc) • 44.3 kB
text/typescript
import { ERROR } from "@nomiclabs/ethereumjs-vm/dist/exceptions";
import semver from "semver";
import {
adjustStackTrace,
stackTraceMayRequireAdjustments,
} from "./mapped-inlined-internal-functions-heuristics";
import {
DecodedCallMessageTrace,
DecodedCreateMessageTrace,
DecodedEvmMessageTrace,
EvmMessageTrace,
EvmStep,
isCreateTrace,
isDecodedCallTrace,
isDecodedCreateTrace,
isEvmStep,
isPrecompileTrace,
MessageTrace,
PrecompileMessageTrace,
} 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,
FALLBACK_FUNCTION_NAME,
InternalFunctionCallStackEntry,
OtherExecutionErrorStackTraceEntry,
RECEIVE_FUNCTION_NAME,
RevertErrorStackTraceEntry,
SolidityStackTrace,
SolidityStackTraceEntry,
SourceReference,
StackTraceEntryType,
UnmappedSolc063RevertErrorStackTraceEntry,
} from "./solidity-stack-trace";
// tslint:disable only-buidler-error
export const FIRST_SOLC_VERSION_SUPPORTED = "0.5.1";
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";
const EIP170_BYTECODE_SIZE_INCLUSIVE_LIMIT = 0x6000;
export class SolidityTracer {
public getStackTrace(
maybeDecodedMessageTrace: MessageTrace
): SolidityStackTrace {
if (maybeDecodedMessageTrace.error === undefined) {
return [];
}
if (isPrecompileTrace(maybeDecodedMessageTrace)) {
return this._getPrecompileMessageStackTrace(maybeDecodedMessageTrace);
}
if (isDecodedCreateTrace(maybeDecodedMessageTrace)) {
return this._getCreateMessageStackTrace(maybeDecodedMessageTrace);
}
if (isDecodedCallTrace(maybeDecodedMessageTrace)) {
return this._getCallMessageStackTrace(maybeDecodedMessageTrace);
}
return this._getUnrecognizedMessageStackTrace(maybeDecodedMessageTrace);
}
private _getCallMessageStackTrace(
trace: DecodedCallMessageTrace
): SolidityStackTrace {
if (this._isDirectLibraryCall(trace)) {
return this._getDirectLibraryCallErrorStackTrace(trace);
}
const calledFunction = trace.bytecode.contract.getFunctionFromSelector(
trace.calldata.slice(0, 4)
);
if (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,
},
];
}
return this._traceEvmExecution(trace);
}
private _getUnrecognizedMessageStackTrace(
trace: EvmMessageTrace
): SolidityStackTrace {
const subtrace = this._getLastSubtrace(trace);
if (subtrace !== undefined) {
// This is not a very exact heuristic, but most of the time it will be right, as solidity
// reverts if a call fails, and most contracts are in solidity
if (
subtrace.error !== undefined &&
trace.returnData.equals(subtrace.returnData)
) {
let unrecognizedEntry: SolidityStackTraceEntry;
if (isCreateTrace(trace)) {
unrecognizedEntry = {
type: StackTraceEntryType.UNRECOGNIZED_CREATE_CALLSTACK_ENTRY,
};
} else {
unrecognizedEntry = {
type: StackTraceEntryType.UNRECOGNIZED_CONTRACT_CALLSTACK_ENTRY,
address: trace.address,
};
}
return [unrecognizedEntry, ...this.getStackTrace(subtrace)];
}
}
if (isCreateTrace(trace)) {
return [
{
type: StackTraceEntryType.UNRECOGNIZED_CREATE_ERROR,
message: trace.returnData,
},
];
}
return [
{
type: StackTraceEntryType.UNRECOGNIZED_CONTRACT_ERROR,
address: trace.address,
message: trace.returnData,
},
];
}
private _getCreateMessageStackTrace(
trace: DecodedCreateMessageTrace
): SolidityStackTrace {
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),
},
];
}
return this._traceEvmExecution(trace);
}
private _getPrecompileMessageStackTrace(
trace: PrecompileMessageTrace
): SolidityStackTrace {
return [
{
type: StackTraceEntryType.PRECOMPILE_ERROR,
precompile: trace.precompile,
},
];
}
private _traceEvmExecution(
trace: DecodedEvmMessageTrace
): SolidityStackTrace {
const stackTrace = this._rawTraceEvmExecution(trace);
if (stackTraceMayRequireAdjustments(stackTrace, trace)) {
return adjustStackTrace(stackTrace, trace);
}
return stackTrace;
}
private _rawTraceEvmExecution(
trace: DecodedEvmMessageTrace
): SolidityStackTrace {
const stacktrace: SolidityStackTrace = [];
let subtracesSeen = 0;
let jumpedIntoFunction = false;
const functionJumpdests: Instruction[] = [];
let consumedAllInstructions = false;
for (let stepIndex = 0; stepIndex < trace.steps.length; stepIndex++) {
const step = trace.steps[stepIndex];
const nextStep = trace.steps[stepIndex + 1];
if (isEvmStep(step)) {
const inst = trace.bytecode.getInstruction(step.pc);
if (inst.jumpType === JumpType.INTO_FUNCTION) {
const nextEvmStep = nextStep as EvmStep; // A jump can't be followed by a subtrace
const nextInst = trace.bytecode.getInstruction(nextEvmStep.pc);
if (nextInst !== undefined && nextInst.opcode === Opcode.JUMPDEST) {
if (jumpedIntoFunction || !isDecodedCallTrace(trace)) {
stacktrace.push(
this._instructionToCallstackStackTraceEntry(
trace.bytecode,
inst
)
);
}
jumpedIntoFunction = true;
functionJumpdests.push(nextInst);
}
} else if (inst.jumpType === JumpType.OUTOF_FUNCTION) {
stacktrace.pop();
functionJumpdests.pop();
} else if (isCall(inst.opcode) || isCreate(inst.opcode)) {
// If a call can't be executed, we don't get an execution trace from it. We can detect
// this by checking if the next step is an EvmStep.
if (nextStep !== undefined && isEvmStep(nextStep)) {
if (this._isCallFailedError(trace, stepIndex, inst)) {
stacktrace.push(
this._callInstructionToCallFailedToExecuteStackTraceEntry(
trace.bytecode,
inst
)
);
consumedAllInstructions = true;
break;
}
} else {
stacktrace.push(
this._instructionToCallstackStackTraceEntry(trace.bytecode, inst)
);
}
} else if (
inst.opcode === Opcode.REVERT ||
inst.opcode === Opcode.INVALID
) {
// Failures with invalid locations are handled later
if (inst.location === undefined) {
continue;
}
if (isDecodedCallTrace(trace) && !jumpedIntoFunction) {
// Failures in the prelude are resolved later.
continue;
}
// 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 = inst.location.getContainingFunction();
// If the failure is in a modifier we add an entry with the function/constructor
if (
failingFunction !== undefined &&
failingFunction.type === ContractFunctionType.MODIFIER
) {
stacktrace.push(
this._getEntryBeforeFailureInModifier(trace, functionJumpdests)
);
}
if (failingFunction !== undefined) {
stacktrace.push(
this._instructionWithinFunctionToRevertStackTraceEntry(
trace,
inst
)
);
} else if (isDecodedCallTrace(trace)) {
// This is here because of the optimizations
stacktrace.push({
type: StackTraceEntryType.REVERT_ERROR,
sourceReference: this._getFunctionStartSourceReference(
trace,
trace.bytecode.contract.getFunctionFromSelector(
trace.calldata.slice(0, 4)
)!
),
message: trace.returnData,
isInvalidOpcodeError: inst.opcode === Opcode.INVALID,
});
} else {
// This is here because of the optimizations
stacktrace.push({
type: StackTraceEntryType.REVERT_ERROR,
sourceReference: this._getConstructorStartSourceReference(trace),
message: trace.returnData,
isInvalidOpcodeError: inst.opcode === Opcode.INVALID,
});
}
consumedAllInstructions = true;
break;
}
} else {
subtracesSeen += 1;
// If there are more subtraces, this one didn't terminate the execution
if (subtracesSeen < trace.numberOfSubtraces) {
stacktrace.pop();
continue;
}
if (step.error === undefined) {
// If this is a subtrace, we pushed a stack frame pointing to the instruction that
// generated the subtrace.
const callStackFrame = stacktrace.pop()! as CallstackEntryStackTraceEntry;
if (this._isReturnDataSizeError(trace, stepIndex)) {
stacktrace.push({
type: StackTraceEntryType.RETURNDATA_SIZE_ERROR,
sourceReference: callStackFrame.sourceReference,
});
consumedAllInstructions = true;
break;
}
} else {
if (
this._isSubtraceErrorPropagated(trace, stepIndex) ||
(isDecodedCallTrace(trace) &&
this._isProxyErrorPropagated(trace, stepIndex))
) {
const subTrace = this.getStackTrace(step);
stacktrace.push(...subTrace);
consumedAllInstructions = true;
break;
}
stacktrace.pop();
}
}
}
if (consumedAllInstructions) {
const firstEntry = stacktrace[0];
if (
firstEntry.type === StackTraceEntryType.CALLSTACK_ENTRY &&
firstEntry.functionType === ContractFunctionType.MODIFIER
) {
stacktrace.unshift(
this._getEntryBeforeInitialModifierCallstackEntry(trace)
);
}
return stacktrace;
}
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);
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: trace.returnData,
isInvalidOpcodeError: lastInstruction.opcode === Opcode.INVALID,
},
];
}
}
const calledFunction = trace.bytecode.contract.getFunctionFromSelector(
trace.calldata.slice(0, 4)
);
if (calledFunction !== undefined) {
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)];
}
if (this._isCalledNonContractAccountError(trace)) {
stacktrace.push({
type: StackTraceEntryType.NONCONTRACT_ACCOUNT_CALLED_ERROR,
// We are sure this is not undefined because there was at least a call instruction
sourceReference: this._getLastSourceReference(trace)!,
});
} else {
if (this._solidity063MaybeUnmappedRevert(trace)) {
const revertFrame = this._solidity063GetFrameForUnmappedRevertWithinFunction(
trace
);
if (revertFrame !== undefined) {
stacktrace.push(revertFrame);
return stacktrace;
}
}
if (isCreateTrace(trace) && this._isContractTooLargeError(trace)) {
return [
{
type: StackTraceEntryType.CONTRACT_TOO_LARGE_ERROR,
sourceReference: this._getConstructorStartSourceReference(trace),
},
];
}
stacktrace.push({
type: StackTraceEntryType.OTHER_EXECUTION_ERROR,
sourceReference: this._getLastSourceReference(trace),
});
}
return stacktrace;
}
// Heuristics
private _isContractTooLargeError(trace: DecodedCreateMessageTrace) {
if (trace.error === undefined || trace.error.error !== ERROR.OUT_OF_GAS) {
return false;
}
// This error doesn't come from solidity, but actually from the VM.
// The deployment code executes correctly, but it OOGs.
const lastStep = trace.steps[trace.steps.length - 1];
if (!isEvmStep(lastStep)) {
return false;
}
const lastInst = trace.bytecode.getInstruction(lastStep.pc);
if (lastInst.opcode !== Opcode.RETURN) {
return false;
}
// TODO: This is an over approximation, as we should be comparing the
// runtime bytecode.
if (
trace.bytecode.normalizedCode.length <=
EIP170_BYTECODE_SIZE_INCLUSIVE_LIMIT
) {
return false;
}
// TODO: What happens if it's an actual out of gas that OOGs at the return?
// maybe traces should have gasLimit and gasUsed.
return true;
}
private _isSubtraceErrorPropagated(
trace: DecodedEvmMessageTrace,
callSubtraceStepIndex: number
): boolean {
const call = trace.steps[callSubtraceStepIndex] as MessageTrace;
if (!trace.returnData.equals(call.returnData)) {
return false;
}
return this._failsRightAfterCall(trace, callSubtraceStepIndex);
}
private _isReturnDataSizeError(
trace: DecodedEvmMessageTrace,
callStepIndex: number
): boolean {
return this._failsRightAfterCall(trace, callStepIndex);
}
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);
return this._isLastLocation(
trace,
callSubtraceStepIndex + 1,
callInst.location! // Calls are always made from within functions
);
}
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 _isCallFailedError(
trace: DecodedEvmMessageTrace,
instIndex: number,
callInstruction: Instruction
): boolean {
const callLocation = callInstruction.location!; // Calls are always made from within functions
return this._isLastLocation(trace, instIndex, callLocation);
}
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 _isProxyErrorPropagated(
trace: DecodedCallMessageTrace,
callSubtraceStepIndex: number
): boolean {
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 = trace.steps[callSubtraceStepIndex];
if (isEvmStep(subtrace)) {
return false;
}
if (isPrecompileTrace(subtrace)) {
return false;
}
// If we can't recognize the implementation we'd better don't consider it as such
if (subtrace.bytecode === undefined) {
return false;
}
if (subtrace.bytecode.contract.type === ContractType.LIBRARY) {
return false;
}
if (!trace.returnData.equals(subtrace.returnData)) {
return false;
}
for (let i = callSubtraceStepIndex + 1; i < trace.steps.length; i++) {
const step = trace.steps[i];
if (!isEvmStep(step)) {
return false;
}
const inst = trace.bytecode.getInstruction(step.pc);
// All the remaining locations should be valid, as they are part of the inline asm
if (inst.location === undefined) {
return false;
}
if (
inst.jumpType === JumpType.INTO_FUNCTION ||
inst.jumpType === JumpType.OUTOF_FUNCTION
) {
return false;
}
}
const lastStep = trace.steps[trace.steps.length - 1] as EvmStep;
const lastInst = trace.bytecode.getInstruction(lastStep.pc);
return lastInst.opcode === Opcode.REVERT;
}
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.gtn(0) &&
(constructor.isPayable === undefined || !constructor.isPayable)
);
}
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;
// tslint:disable-next-line 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 _isDirectLibraryCall(trace: DecodedCallMessageTrace): boolean {
return (
trace.depth === 0 && trace.bytecode.contract.type === ContractType.LIBRARY
);
}
private _isFunctionNotPayableError(
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.lten(0)) {
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 _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 _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.lten(0)) {
return false;
}
if (trace.bytecode.contract.fallback === undefined) {
return false;
}
const isPayable = trace.bytecode.contract.fallback.isPayable;
return isPayable === undefined || !isPayable;
}
// Stack trace entry factories
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 _getOtherErrorBeforeCalledFunctionStackTraceEntry(
trace: DecodedCallMessageTrace
): OtherExecutionErrorStackTraceEntry {
return {
type: StackTraceEntryType.OTHER_EXECUTION_ERROR,
sourceReference: this._getContractStartWithoutFunctionSourceReference(
trace
),
};
}
private _instructionToCallstackStackTraceEntry(
bytecode: Bytecode,
inst: Instruction
): CallstackEntryStackTraceEntry | InternalFunctionCallStackEntry {
// This means that a jump is made from within an internal solc function.
// These are normally made from yul code, so they don't map to any Solidity
// function
if (inst.location === undefined) {
return {
type: StackTraceEntryType.INTERNAL_FUNCTION_CALLSTACK_ENTRY,
pc: inst.pc,
sourceReference: {
file: bytecode.contract.location.file,
contract: bytecode.contract.name,
function: undefined,
line: bytecode.contract.location.getStartingLineNumber(),
},
};
}
const func = inst.location!.getContainingFunction();
if (func !== undefined) {
return {
type: StackTraceEntryType.CALLSTACK_ENTRY,
sourceReference: this._sourceLocationToSourceReference(
bytecode,
inst.location
)!,
functionType: func.type,
};
}
return {
type: StackTraceEntryType.CALLSTACK_ENTRY,
sourceReference: {
function: undefined,
contract: bytecode.contract.name,
file: inst.location!.file,
line: inst.location!.getStartingLineNumber(),
},
functionType: ContractFunctionType.FUNCTION,
};
}
private _callInstructionToCallFailedToExecuteStackTraceEntry(
bytecode: Bytecode,
callInst: Instruction
): CallFailedErrorStackTraceEntry {
// Calls only happen within functions
return {
type: StackTraceEntryType.CALL_FAILED_ERROR,
sourceReference: this._sourceLocationToSourceReference(
bytecode,
callInst.location
)!,
};
}
private _instructionWithinFunctionToRevertStackTraceEntry(
trace: DecodedEvmMessageTrace,
inst: Instruction
): RevertErrorStackTraceEntry {
return {
type: StackTraceEntryType.REVERT_ERROR,
sourceReference: this._sourceLocationToSourceReference(
trace.bytecode,
inst.location
)!,
message: trace.returnData,
isInvalidOpcodeError: inst.opcode === Opcode.INVALID,
};
}
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 this._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 _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,
};
}
// Source reference factories
private _getContractStartWithoutFunctionSourceReference(
trace: DecodedEvmMessageTrace
) {
return {
file: trace.bytecode.contract.location.file,
contract: trace.bytecode.contract.name,
line: trace.bytecode.contract.location.getStartingLineNumber(),
};
}
/**
* 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 {
file: contract.location.file,
contract: contract.name,
function: CONSTRUCTOR_FUNCTION_NAME,
line,
};
}
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 {
file: func.location.file,
contract: trace.bytecode.contract.name,
function: FALLBACK_FUNCTION_NAME,
line: func.location.getStartingLineNumber(),
};
}
private _getFunctionStartSourceReference(
trace: DecodedEvmMessageTrace,
func: ContractFunction
): SourceReference {
return {
file: func.location.file,
contract: trace.bytecode.contract.name,
function: func.name,
line: func.location.getStartingLineNumber(),
};
}
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;
}
return this._sourceLocationToSourceReference(
trace.bytecode,
inst.location
);
}
return undefined;
}
private _sourceLocationToSourceReference(
bytecode: Bytecode,
location?: SourceLocation
): SourceReference | undefined {
if (location === undefined) {
return undefined;
}
const func = location.getContainingFunction();
if (func === undefined) {
return undefined;
}
let funcName = func.name;
if (func.type === ContractFunctionType.CONSTRUCTOR) {
funcName = CONSTRUCTOR_FUNCTION_NAME;
} else if (func.type === ContractFunctionType.FALLBACK) {
funcName = FALLBACK_FUNCTION_NAME;
} else if (func.type === ContractFunctionType.RECEIVE) {
funcName = RECEIVE_FUNCTION_NAME;
}
return {
function: funcName,
contract: bytecode.contract.name,
file: func.location.file,
line: location.getStartingLineNumber(),
};
}
// Utils
private _getLastSubtrace(trace: EvmMessageTrace): MessageTrace | undefined {
if (trace.numberOfSubtraces < 1) {
return undefined;
}
let i = trace.steps.length - 1;
while (isEvmStep(trace.steps[i])) {
i -= 1;
}
return trace.steps[i] as MessageTrace;
}
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 _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;
}
// 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
revertFrame = {
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
sourceReference: {
contract: trace.bytecode.contract.name,
function: FALLBACK_FUNCTION_NAME,
file: trace.bytecode.contract.fallback.location.file,
line: trace.bytecode.contract.fallback.location.getStartingLineNumber(),
},
};
this._solidity063CorrectLineNumber(revertFrame);
}
} else {
// Failed within the receive function
revertFrame = {
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
sourceReference: {
contract: trace.bytecode.contract.name,
function: RECEIVE_FUNCTION_NAME,
file: trace.bytecode.contract.receive.location.file,
line: trace.bytecode.contract.receive.location.getStartingLineNumber(),
},
};
this._solidity063CorrectLineNumber(revertFrame);
}
}
return revertFrame;
}
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.equals(nextLoc)
) {
return {
...this._instructionWithinFunctionToRevertStackTraceEntry(
trace,
nextInst
),
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
};
}
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) {
revertFrame = {
...this._instructionWithinFunctionToRevertStackTraceEntry(
trace,
prevInst
),
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
};
} else if (nextFunc !== undefined) {
revertFrame = {
...this._instructionWithinFunctionToRevertStackTraceEntry(
trace,
nextInst
),
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
};
}
if (revertFrame !== undefined) {
this._solidity063CorrectLineNumber(revertFrame);
}
return revertFrame;
}
if (isCreateTrace(trace)) {
// 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._instructionWithinFunctionToRevertStackTraceEntry(
trace,
prevInst
),
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
};
// When the latest instruction is not within a function we need
// some default sourceReference to show to the user
if (constructorRevertFrame.sourceReference === undefined) {
const defaultSourceReference: SourceReference = {
function: CONSTRUCTOR_FUNCTION_NAME,
contract: trace.bytecode.contract.name,
file: trace.bytecode.contract.location.file,
line: trace.bytecode.contract.location.getStartingLineNumber(),
};
if (trace.bytecode.contract.constructorFunction !== undefined) {
defaultSourceReference.line = trace.bytecode.contract.constructorFunction.location.getStartingLineNumber();
}
constructorRevertFrame.sourceReference = defaultSourceReference;
} else {
this._solidity063CorrectLineNumber(constructorRevertFrame);
}
return constructorRevertFrame;
}
// 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._instructionWithinFunctionToRevertStackTraceEntry(
trace,
prevInst
),
type: StackTraceEntryType.UNMAPPED_SOLC_0_6_3_REVERT_ERROR,
};
if (latestInstructionRevertFrame.sourceReference !== undefined) {
this._solidity063CorrectLineNumber(latestInstructionRevertFrame);
}
return latestInstructionRevertFrame;
}
private _solidity063MaybeUnmappedRevert(trace: DecodedEvmMessageTrace) {
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
);
}
private _solidity063CorrectLineNumber(
revertFrame: UnmappedSolc063RevertErrorStackTraceEntry
) {
const file = revertFrame.sourceReference.file;
const lines = file.content.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;
}
}
}