UNPKG

hardhat-tracer

Version:

Hardhat Tracer plugin

327 lines (293 loc) 10.5 kB
import { MinimalEthereumJsVm } from "hardhat/internal/hardhat-network/provider/vm/minimal-vm"; import createDebug from "debug"; import { parse } from "./opcodes"; import { CALL } from "./opcodes/call"; import { CREATE } from "./opcodes/create"; import { CREATE2 } from "./opcodes/create2"; import { DELEGATECALL } from "./opcodes/delegatecall"; import { EXCEPTION } from "./opcodes/exception"; import { TransactionTrace } from "./transaction-trace"; import { AwaitedItem, Item, TracerEnv } from "./types"; import { checkIfOpcodesAreValid } from "./utils/check-opcodes"; import { hexPrefix } from "./utils/hex"; import { isItem } from "./utils/item"; import { MinimalEVMResult, MinimalInterpreterStep, MinimalMessage, } from "hardhat/internal/hardhat-network/provider/vm/types"; import { parseExec } from "./utils"; const debug = createDebug("hardhat-tracer:trace-recorder"); export class TraceRecorder { public vm: MinimalEthereumJsVm; public previousTraces: TransactionTrace[] = []; public printIndex = -1; public previousTracesMap: Map<string, TransactionTrace> = new Map(); public trace: TransactionTrace | undefined; public previousOpcode: string | undefined; public tracerEnv: TracerEnv; public awaitedItems: Array<AwaitedItem<any>>; public addressStack: { value: string }[]; // object is used to allow lazy assignment to value in CREATE/2 constructor(vm: MinimalEthereumJsVm, tracerEnv: TracerEnv) { this.vm = vm; this.tracerEnv = tracerEnv; checkIfOpcodesAreValid(tracerEnv.opcodes, vm); this.awaitedItems = []; this.addressStack = []; vm.events.on("beforeTx", this.handleBeforeTx.bind(this)); vm.evm.events?.on("beforeMessage", this.handleBeforeMessage.bind(this)); // vm.evm.events?.on("newContract", this.handleNewContract.bind(this)); vm.evm.events?.on("step", this.handleStep.bind(this)); vm.evm.events?.on("afterMessage", this.handleAfterMessage.bind(this)); vm.events.on("afterTx", this.handleAfterTx.bind(this)); } public storeLastTrace(txHash: string) { const lastTrace = this.previousTraces[this.previousTraces.length - 1]; if (lastTrace) { this.previousTracesMap.set(txHash.toLowerCase(), lastTrace); } } public getTrace(txHash: string) { return this.previousTracesMap.get(txHash.toLowerCase()); } public handleBeforeTx( // tx: any, // TypedTransaction, resolve?: ((result?: any) => void) | undefined ) { if (!this.tracerEnv.switch!.verboseEnabled) return resolve?.(); debug("handleBeforeTx"); this.trace = new TransactionTrace(); // this.trace.hash = hexPrefix(Buffer.from(tx.hash()).toString("hex")); resolve?.(); } public handleBeforeMessage( message: MinimalMessage, resolve: ((result?: any) => void) | undefined ) { if (!this.tracerEnv.switch!.verboseEnabled) return resolve?.(); debug("handleBeforeMessage"); if (!this.trace) { throw new Error( "[hardhat-tracer]: trace is undefined in handleBeforeMessage" ); } const isDelegateCall = !!message.to && !!message.codeAddress && !message.to.equals(message.codeAddress); const isStaticCall = message.isStaticCall; const salt = undefined; // TODO let item: Item<any>; if (isDelegateCall) { if (message.to === undefined) { throw new Error( "[hardhat-tracer]: message.to is undefined in handleBeforeMessage" ); } item = { opcode: "DELEGATECALL", params: { from: hexPrefix(message.caller.toString()), to: hexPrefix((message.codeAddress ?? message.to).toString()), inputData: hexPrefix(Buffer.from(message.data).toString("hex")), gasLimit: Number(message.gasLimit.toString()), value: this.trace.parent?.params.value, }, children: [], } as Item<DELEGATECALL>; this.addressStack.push(item.params.to); } else if (message.to) { item = { opcode: isStaticCall ? "STATICCALL" : "CALL", params: { from: hexPrefix(message.caller.toString()), to: hexPrefix(message.to.toString()), inputData: hexPrefix(Buffer.from(message.data).toString("hex")), gasLimit: Number(message.gasLimit.toString()), value: hexPrefix(message.value.toString(16)), }, children: [], } as Item<CALL>; this.addressStack.push(item.params.to); } else if (message.to === undefined && salt === undefined) { item = { opcode: "CREATE", params: { from: hexPrefix(message.caller.toString()), initCode: hexPrefix(Buffer.from(message.data).toString("hex")), gasLimit: Number(message.gasLimit.toString()), value: hexPrefix(message.value.toString(16)), }, children: [], } as Item<CREATE>; this.addressStack.push({ value: "lazy" }); } else if (message.to === undefined && salt !== undefined) { item = { opcode: "CREATE2", params: { from: hexPrefix(message.caller.toString()), initCode: hexPrefix(Buffer.from(message.data).toString("hex")), gasLimit: Number(message.gasLimit.toString()), value: hexPrefix(message.value.toString(16)), salt: hexPrefix(Buffer.from(salt).toString("hex")), }, children: [], } as Item<CREATE2>; this.addressStack.push({ value: "lazy" }); } else { item = { opcode: "UNKNOWN_MESSAGE", params: {}, children: [], }; console.error("handleBeforeMessage: message type not handled", message); } this.trace.insertItem(item, { increaseDepth: true }); resolve?.(); } // This is now called in handleAfterMessage // prev: public handleNewContract( newContract( contractAddress: string, resolve?: ((result?: any) => void) | undefined ) { if (!this.tracerEnv.switch!.verboseEnabled) return resolve?.(); debug("handleNewContract %s", contractAddress); if (!this.trace || !this.trace.parent) { console.error("handleNewContract: trace.parent not present"); } else { switch (this.trace.parent.opcode) { case "CREATE": const createItem = (this.trace.parent as unknown) as Item<CREATE>; createItem.params.deployedAddress = hexPrefix(contractAddress); break; case "CREATE2": const create2Item = (this.trace.parent as unknown) as Item<CREATE2>; create2Item.params.deployedAddress = hexPrefix(contractAddress); break; default: console.log(this.trace.parent); console.error("handleNewContract: opcode not handled"); break; } } let ptr = this.addressStack[this.addressStack.length - 1]; ptr.value = contractAddress; resolve?.(); } public handleStep( step: MinimalInterpreterStep, resolve: ((result?: any) => void) | undefined ) { if (!this.tracerEnv.switch!.verboseEnabled) return resolve?.(); // debug("handleStep %s", step.opcode.name); if (!this.trace) { throw new Error("[hardhat-tracer]: trace is undefined in handleStep"); } if (this.awaitedItems.length) { this.awaitedItems = this.awaitedItems.filter( (awaitedItems) => awaitedItems.next > 0 ); for (const awaitedItem of this.awaitedItems) { awaitedItem.next--; if (awaitedItem.next === 0) { const item = awaitedItem.parse( step, this.addressStack[this.addressStack.length - 1] ); this.trace.insertItem(item); } } } if ( this.tracerEnv.enableAllOpcodes || this.tracerEnv.opcodes.get(step.opcode.name) ) { const result = parse( step, this.addressStack[this.addressStack.length - 1] ); if (result) { if (isItem(result)) { debug("parsed step %s", step.opcode.name); this.trace.insertItem(result); } else { debug("parsed step awaited %s", step.opcode.name); this.awaitedItems.push(result); } } } this.previousOpcode = step.opcode.name; resolve?.(); } public handleAfterMessage( evmResult: MinimalEVMResult, resolve: ((result?: any) => void) | undefined ) { if (!this.tracerEnv.switch!.verboseEnabled) return resolve?.(); debug("handleAfterMessage"); if (!this.trace) { throw new Error( "[hardhat-tracer]: trace is undefined in handleAfterMessage" ); } let { errorStr, isException } = parseExec(evmResult.execResult); // TODO add support - not doing for now, will add if needed // if (isSelfDestruct) { // const selfdestructs = Object.entries(evmResult.execResult.selfdestruct); // for (const [address, beneficiary] of selfdestructs) { // debug("self destruct %s", address); // this.trace.insertItem({ // opcode: "SELFDESTRUCT", // params: { // beneficiary: hexPrefix(beneficiary.toString("hex")), // }, // }); // } // } if (isException) { debug("exception %s", errorStr); this.trace.insertItem({ opcode: "EXCEPTION", params: { error: errorStr, type: "EvmError", }, } as Item<EXCEPTION>); } if (evmResult.execResult.contractAddress) { this.newContract(evmResult.execResult.contractAddress.toString()); } this.trace.returnCurrentCall( hexPrefix(evmResult.execResult.output?.toString("hex") ?? "0x"), Number(evmResult?.execResult.executionGasUsed), errorStr ); this.addressStack.pop(); resolve?.(); } public handleAfterTx( // _tx: any, // AfterTxEvent, resolve?: ((result?: any) => void) | undefined ) { if (!this.tracerEnv.switch!.verboseEnabled) return resolve?.(); debug("handleAfterTx"); if (this.tracerEnv.enabled) { if (!this.trace) { throw new Error( "[hardhat-tracer]: trace is undefined in handleAfterTx" ); } // store the trace for later use (printing or outputting) debug("record the trace"); this.previousTraces.push(this.trace); } // clear the trace debug("clear running trace data"); this.trace = undefined; this.previousOpcode = undefined; this.awaitedItems = []; this.addressStack = []; resolve?.(); } }