hardhat-tracer
Advanced tools
Comparing version 2.0.0-beta.2 to 2.0.0-beta.3
@@ -1,2 +0,2 @@ | ||
import { ErrorFragment, Fragment, FunctionFragment, Result } from "ethers/lib/utils"; | ||
import { ErrorFragment, EventFragment, Fragment, FunctionFragment, Result } from "ethers/lib/utils"; | ||
import { Artifacts } from "hardhat/types"; | ||
@@ -10,5 +10,7 @@ declare type Mapping<FragmentType> = Map<string, Array<{ | ||
errorFragmentsBySelector: Mapping<ErrorFragment>; | ||
eventFragmentsByTopic0: Mapping<EventFragment>; | ||
ready: Promise<void>; | ||
constructor(artifacts: Artifacts); | ||
_constructor(artifacts: Artifacts): Promise<void>; | ||
updateArtifacts(artifacts: Artifacts): Promise<void>; | ||
_updateArtifacts(artifacts: Artifacts): Promise<void>; | ||
decode(inputData: string, returnData: string): Promise<{ | ||
@@ -20,4 +22,20 @@ fragment: Fragment; | ||
}>; | ||
decodeFunction(inputData: string, returnData: string): Promise<{ | ||
fragment: Fragment; | ||
inputResult: Result; | ||
returnResult: Result | undefined; | ||
contractName: string; | ||
}>; | ||
decodeError(revertData: string): Promise<{ | ||
fragment: Fragment; | ||
revertResult: Result; | ||
contractName: string; | ||
}>; | ||
decodeEvent(topics: string[], data: string): Promise<{ | ||
fragment: EventFragment; | ||
result: Result; | ||
contractName: string; | ||
}>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=decoder.d.ts.map |
@@ -10,5 +10,9 @@ "use strict"; | ||
this.errorFragmentsBySelector = new Map(); | ||
this.ready = this._constructor(artifacts); | ||
this.eventFragmentsByTopic0 = new Map(); | ||
this.ready = this._updateArtifacts(artifacts); | ||
} | ||
async _constructor(artifacts) { | ||
async updateArtifacts(artifacts) { | ||
this.ready = this._updateArtifacts(artifacts); | ||
} | ||
async _updateArtifacts(artifacts) { | ||
const names = await artifacts.getAllFullyQualifiedNames(); | ||
@@ -20,3 +24,11 @@ for (const name of names) { | ||
copyFragments(name, iface.errors, this.errorFragmentsBySelector); | ||
copyFragments(name, iface.events, this.eventFragmentsByTopic0); | ||
} | ||
// common errors, these are in function format because Ethers.js does not accept them as errors | ||
const commonErrors = [ | ||
"function Error(string reason)", | ||
"function Panic(uint256 code)", | ||
]; | ||
const iface = new utils_1.Interface(commonErrors); | ||
copyFragments("", iface.functions, this.errorFragmentsBySelector); | ||
} | ||
@@ -31,2 +43,30 @@ async decode(inputData, returnData) { | ||
} | ||
async decodeFunction(inputData, returnData) { | ||
await this.ready; | ||
return decode(inputData, returnData, "function", this.functionFragmentsBySelector); | ||
} | ||
async decodeError(revertData) { | ||
await this.ready; | ||
const { fragment, inputResult, contractName } = await decode(revertData, "0x", "error", this.errorFragmentsBySelector); | ||
return { fragment, revertResult: inputResult, contractName }; | ||
} | ||
async decodeEvent(topics, data) { | ||
await this.ready; | ||
if (topics.length === 0) { | ||
throw new Error("No topics, cannot decode"); | ||
} | ||
const topic0 = topics[0]; | ||
const fragments = this.eventFragmentsByTopic0.get(topic0); | ||
if (fragments) { | ||
for (const { contractName, fragment } of fragments) { | ||
try { | ||
const iface = new ethers_1.ethers.utils.Interface([fragment]); | ||
const result = iface.parseLog({ data, topics }); | ||
return { fragment, result: result.args, contractName }; | ||
} | ||
catch { } | ||
} | ||
} | ||
throw decodeError(topic0); | ||
} | ||
} | ||
@@ -40,3 +80,5 @@ exports.Decoder = Decoder; | ||
function addFragmentToMapping(contractName, fragment, mapping) { | ||
const selector = ethers_1.ethers.utils.Interface.getSighash(fragment); | ||
const selector = utils_1.EventFragment.isEventFragment(fragment) | ||
? ethers_1.ethers.utils.Interface.getEventTopic(fragment) | ||
: ethers_1.ethers.utils.Interface.getSighash(fragment); | ||
let fragments = mapping.get(selector); | ||
@@ -46,2 +88,3 @@ if (!fragments) { | ||
} | ||
// TODO while adding, see if we already have a same signature fragment | ||
fragments.push({ contractName, fragment }); | ||
@@ -76,7 +119,10 @@ } | ||
// we couldn't decode it using local ABI, try 4byte.directory | ||
try { | ||
const { fragment, inputResult } = await decodeUsing4byteDirectory(selector, inputData, mapping); | ||
return { fragment, inputResult }; | ||
// currently only supports function calls | ||
if (type === "function") { | ||
try { | ||
const { fragment, inputResult } = await decodeUsing4byteDirectory(selector, inputData, mapping); | ||
return { fragment, inputResult }; | ||
} | ||
catch { } | ||
} | ||
catch { } | ||
// we couldn't decode it after even using 4byte.directory, give up | ||
@@ -83,0 +129,0 @@ throw decodeError(selector); |
@@ -24,2 +24,3 @@ "use strict"; | ||
verbosity: userConfig.tracer?.defaultVerbosity ?? utils_1.DEFAULT_VERBOSITY, | ||
showAddresses: userConfig.tracer?.showAddresses ?? false, | ||
gasCost: userConfig.tracer?.gasCost ?? false, | ||
@@ -31,2 +32,3 @@ opcodes, | ||
printNameTagTip: undefined, | ||
tokenDecimalsCache: new Map(), | ||
}, | ||
@@ -33,0 +35,0 @@ stateOverrides: userConfig.tracer?.stateOverrides, |
@@ -16,12 +16,13 @@ "use strict"; | ||
global.tracerEnv = hre.tracer; | ||
(0, get_vm_1.getVM)(hre).then((vm) => { | ||
hre.tracer.recorder = new recorder_1.TraceRecorder(vm, hre.tracer); | ||
// vm.on("beforeTx", handleBeforeTx); | ||
// vm.on("beforeMessage", handleBeforeMessage); | ||
// vm.on("newContract", handleNewContract); | ||
// vm.on("step", handleStep); | ||
// vm.on("afterMessage", handleAfterMessage); | ||
// vm.on("afterTx", handleAfterTx); | ||
// @ts-ignore | ||
global.hreArtifacts = hre.artifacts; | ||
(0, get_vm_1.getVM)(hre) | ||
.then((vm) => { | ||
hre.tracer.recorder = new recorder_1.TraceRecorder(vm, hre.tracer, hre.artifacts); | ||
}) | ||
.catch(() => { | ||
// if for some reason we can't get the vm, disable hardhat-tracer | ||
hre.tracer.enabled = false; | ||
}); | ||
}); | ||
//# sourceMappingURL=hre.js.map |
import { BigNumberish } from "ethers"; | ||
import { TracerDependencies } from "../types"; | ||
export declare function formatCall(to: string, input: string, ret: string, value: BigNumberish, gas: BigNumberish, dependencies: TracerDependencies): Promise<string>; | ||
export declare function formatCall(to: string, input: string, ret: string, value: BigNumberish, gas: BigNumberish, success: boolean, dependencies: TracerDependencies): Promise<string>; | ||
//# sourceMappingURL=call.d.ts.map |
@@ -9,6 +9,5 @@ "use strict"; | ||
const result_1 = require("./result"); | ||
const utils_2 = require("ethers/lib/utils"); | ||
const separator_1 = require("./separator"); | ||
async function formatCall(to, input, ret, value, gas, dependencies) { | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
// TODO handle if `to` is console.log address | ||
async function formatCall(to, input, ret, value, gas, success, dependencies) { | ||
let contractName; | ||
@@ -25,3 +24,3 @@ let contractDecimals; | ||
returnResult, | ||
} = await dependencies.tracerEnv.decoder.decode(input, ret)); | ||
} = await dependencies.tracerEnv.decoder.decodeFunction(input, ret)); | ||
// use just contract name | ||
@@ -31,28 +30,11 @@ contractName = contractName.split(":")[1]; | ||
catch { } | ||
// TODO Find a better contract name | ||
// 1. See if there is a name() method that gives string or bytes32 | ||
const contractNameFromNameMethod = await (0, utils_1.fetchContractName)(to, dependencies.provider); | ||
if (contractNameFromNameMethod !== undefined) { | ||
contractName = contractNameFromNameMethod; | ||
// find a better contract name | ||
const betterContractName = await (0, utils_1.getBetterContractName)(to, dependencies); | ||
if (betterContractName) { | ||
contractName = betterContractName; | ||
} | ||
else { | ||
// 2. Match bytecode | ||
let contractNameFromArtifacts; | ||
const toBytecode = await dependencies.provider.send("eth_getCode", [to]); | ||
for (const name of names) { | ||
const _artifact = await dependencies.artifacts.readArtifact(name); | ||
// try to find the contract name | ||
if ((0, utils_1.compareBytecode)(_artifact.deployedBytecode, toBytecode) > 0.5 || | ||
(to === ethers_1.ethers.constants.AddressZero && toBytecode.length <= 2)) { | ||
// if bytecode of "to" is the same as the deployed bytecode | ||
// we can use the artifact name | ||
contractNameFromArtifacts = _artifact.contractName; | ||
} | ||
// if we got both the contract name and arguments parsed so far, we can stop | ||
if (contractNameFromArtifacts) { | ||
contractName = contractNameFromArtifacts; | ||
break; | ||
} | ||
} | ||
else if (contractName) { | ||
dependencies.tracerEnv.nameTags[to] = contractName; | ||
} | ||
// if ERC20 method found then fetch decimals | ||
if (input.slice(0, 10) === "0x70a08231" || // balanceOf | ||
@@ -62,4 +44,28 @@ input.slice(0, 10) === "0xa9059cbb" || // transfer | ||
) { | ||
contractDecimals = await (0, utils_1.fetchContractDecimals)(to, dependencies.provider); | ||
// see if we already know the decimals | ||
const { tokenDecimalsCache } = dependencies.tracerEnv._internal; | ||
const decimals = tokenDecimalsCache.get(to); | ||
if (decimals) { | ||
// if we know decimals then use it | ||
contractDecimals = decimals !== -1 ? decimals : undefined; | ||
} | ||
else { | ||
// otherwise fetch it | ||
contractDecimals = await (0, utils_1.fetchContractDecimals)(to, dependencies.provider); | ||
// and cache it | ||
if (contractDecimals !== undefined) { | ||
tokenDecimalsCache.set(to, contractDecimals); | ||
} | ||
else { | ||
tokenDecimalsCache.set(to, -1); | ||
} | ||
} | ||
} | ||
const extra = []; | ||
if ((value = ethers_1.BigNumber.from(value)).gt(0)) { | ||
extra.push(`value${separator_1.SEPARATOR}${(0, utils_2.formatEther)(value)}`); | ||
} | ||
if ((gas = ethers_1.BigNumber.from(gas)).gt(0) && dependencies.tracerEnv.gasCost) { | ||
extra.push(`gas${separator_1.SEPARATOR}${(0, param_1.formatParam)(gas, dependencies)}`); | ||
} | ||
if (inputResult && fragment) { | ||
@@ -69,23 +75,19 @@ const inputArgs = (0, result_1.formatResult)(inputResult, fragment.inputs, { decimals: contractDecimals, shorten: false }, dependencies); | ||
? (0, result_1.formatResult)(returnResult, fragment.outputs, { decimals: contractDecimals, shorten: true }, dependencies) | ||
: ""; | ||
const extra = []; | ||
if ((value = ethers_1.BigNumber.from(value)).gt(0)) { | ||
extra.push(`value${separator_1.SEPARATOR}${(0, param_1.formatParam)(value, dependencies)}`); | ||
} | ||
if ((gas = ethers_1.BigNumber.from(gas)).gt(0) && dependencies.tracerEnv.gasCost) { | ||
extra.push(`gas${separator_1.SEPARATOR}${(0, param_1.formatParam)(gas, dependencies)}`); | ||
} | ||
const nameTag = (0, utils_1.getFromNameTags)(to, dependencies); | ||
return `${nameTag | ||
? (0, colors_1.colorContract)(nameTag) | ||
: contractName | ||
? (0, colors_1.colorContract)(contractName) | ||
: `<${(0, colors_1.colorContract)("UnknownContract")} ${(0, param_1.formatParam)(to, dependencies)}>`}.${(0, colors_1.colorFunction)(fragment.name)}${extra.length !== 0 ? `{${extra.join(",")}}` : ""}(${inputArgs})${outputArgs ? ` => (${outputArgs})` : ""}`; | ||
: // if return data is not decoded, then show return data only if call was success | ||
ret !== "0x" && success !== false // success can be undefined | ||
? ret | ||
: ""; | ||
let nameToPrint = contractName ?? "UnknownContract"; | ||
return `${dependencies.tracerEnv.showAddresses || nameToPrint === "UnknownContract" | ||
? `${(0, colors_1.colorContract)(nameToPrint)}(${to})` | ||
: (0, colors_1.colorContract)(nameToPrint)}.${(0, colors_1.colorFunction)(fragment.name)}${extra.length !== 0 ? `{${extra.join(",")}}` : ""}(${inputArgs})${outputArgs ? ` => (${outputArgs})` : ""}`; | ||
} | ||
// TODO add flag to hide unrecognized stuff | ||
if (contractName) { | ||
return `${(0, colors_1.colorContract)(contractName)}.<${(0, colors_1.colorFunction)("UnknownFunction")}>(${(0, colors_1.colorKey)("input" + separator_1.SEPARATOR)}${input}, ${(0, colors_1.colorKey)("ret" + separator_1.SEPARATOR)}${ret})`; | ||
return `${dependencies.tracerEnv.showAddresses | ||
? `${(0, colors_1.colorContract)(contractName)}(${to})` | ||
: (0, colors_1.colorContract)(contractName)}.<${(0, colors_1.colorFunction)("UnknownFunction")}>${extra.length !== 0 ? `{${extra.join(",")}}` : ""}(${(0, colors_1.colorKey)("input" + separator_1.SEPARATOR)}${input}, ${(0, colors_1.colorKey)("ret" + separator_1.SEPARATOR)}${ret})`; | ||
} | ||
else { | ||
return `${(0, colors_1.colorFunction)("UnknownContractAndFunction")}(${(0, colors_1.colorKey)("to" + separator_1.SEPARATOR)}${to}, ${(0, colors_1.colorKey)("input" + separator_1.SEPARATOR)}${input}, ${(0, colors_1.colorKey)("ret" + separator_1.SEPARATOR)}${ret || "0x"})`; | ||
return `${(0, colors_1.colorFunction)("UnknownContractAndFunction")}${extra.length !== 0 ? `{${extra.join(",")}}` : ""}(${(0, colors_1.colorKey)("to" + separator_1.SEPARATOR)}${to}, ${(0, colors_1.colorKey)("input" + separator_1.SEPARATOR)}${input}, ${(0, colors_1.colorKey)("ret" + separator_1.SEPARATOR)}${ret || "0x"})`; | ||
} | ||
@@ -92,0 +94,0 @@ } |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.formatError = void 0; | ||
const utils_1 = require("ethers/lib/utils"); | ||
const colors_1 = require("../colors"); | ||
@@ -10,13 +9,6 @@ const object_1 = require("./object"); | ||
async function formatError(revertData, dependencies) { | ||
const commonErrors = [ | ||
"function Error(string reason)", | ||
"function Panic(uint256 code)", | ||
]; | ||
try { | ||
const iface = new utils_1.Interface(commonErrors); | ||
const parsed = iface.parseTransaction({ | ||
data: revertData, | ||
}); | ||
if (parsed.name === "Panic") { | ||
const panicCode = parsed.args.code.toNumber(); | ||
const { fragment, revertResult, } = await dependencies.tracerEnv.decoder.decodeError(revertData); | ||
if (fragment.name === "Panic") { | ||
const panicCode = revertResult.code.toNumber(); | ||
let situation = ""; | ||
@@ -52,3 +44,3 @@ switch (panicCode) { | ||
} | ||
return `${(0, colors_1.colorError)(parsed.name)}(${(0, object_1.formatObject)({ | ||
return `${(0, colors_1.colorError)(fragment.name)}(${(0, object_1.formatObject)({ | ||
code: panicCode, | ||
@@ -58,17 +50,7 @@ situation, | ||
} | ||
const formatted = (0, result_1.formatResult)(parsed.args, parsed.functionFragment.inputs, { decimals: -1, shorten: false }, dependencies); | ||
return `${(0, colors_1.colorError)(parsed.name)}(${formatted})`; | ||
const formatted = (0, result_1.formatResult)(revertResult, fragment.inputs, { decimals: -1, shorten: false }, dependencies); | ||
return `${(0, colors_1.colorError)(fragment.name)}(${formatted})`; | ||
} | ||
catch { } | ||
// if error not common then try to parse it as a custom error | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
for (const name of names) { | ||
const artifact = await dependencies.artifacts.readArtifact(name); | ||
const iface = new utils_1.Interface(artifact.abi); | ||
try { | ||
const errorDesc = iface.parseError(revertData); | ||
return `${(0, colors_1.colorError)(errorDesc.name)}(${(0, result_1.formatResult)(errorDesc.args, errorDesc.errorFragment.inputs, { decimals: -1, shorten: false }, dependencies)})`; | ||
} | ||
catch { } | ||
} | ||
// if error could not be decoded, then just show the data | ||
return `${(0, colors_1.colorError)("UnknownError")}(${(0, param_1.formatParam)(revertData, dependencies)})`; | ||
@@ -75,0 +57,0 @@ } |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.formatLog = void 0; | ||
const utils_1 = require("ethers/lib/utils"); | ||
const colors_1 = require("../colors"); | ||
const utils_2 = require("../utils"); | ||
const param_1 = require("./param"); | ||
const result_1 = require("./result"); | ||
const utils_1 = require("../utils"); | ||
async function formatLog(log, currentAddress, dependencies) { | ||
// TODO make the contractName code common between formatCall and formatLog | ||
const nameTag = currentAddress | ||
? (0, utils_2.getFromNameTags)(currentAddress, dependencies) | ||
: undefined; | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
const code = !nameTag && currentAddress | ||
? (await dependencies.provider.send("eth_getCode", [ | ||
currentAddress, | ||
"latest", | ||
])) | ||
: undefined; | ||
let str; | ||
let contractName = nameTag; | ||
for (const name of names) { | ||
const artifact = await dependencies.artifacts.readArtifact(name); | ||
const iface = new utils_1.Interface(artifact.abi); | ||
// try to find the contract name | ||
if ((0, utils_2.compareBytecode)(artifact.deployedBytecode, code ?? "0x") > 0.5) { | ||
contractName = artifact.contractName; | ||
let fragment, result, contractName; | ||
try { | ||
({ | ||
fragment, | ||
result, | ||
contractName, | ||
} = await dependencies.tracerEnv.decoder.decodeEvent(log.topics, log.data)); | ||
// use just contract name | ||
contractName = contractName.split(":")[1]; | ||
} | ||
catch { } | ||
// find a better contract name | ||
if (currentAddress) { | ||
const betterContractName = await (0, utils_1.getBetterContractName)(currentAddress, dependencies); | ||
if (betterContractName) { | ||
contractName = betterContractName; | ||
} | ||
// try to parse the arguments | ||
try { | ||
const parsed = iface.parseLog(log); | ||
const decimals = -1; | ||
if (!contractName) { | ||
contractName = artifact.contractName; | ||
} | ||
str = `${(0, colors_1.colorEvent)(parsed.name)}(${(0, result_1.formatResult)(parsed.args, parsed.eventFragment.inputs, { decimals, shorten: false }, dependencies)})`; | ||
else if (contractName) { | ||
dependencies.tracerEnv.nameTags[currentAddress] = contractName; | ||
} | ||
catch { } | ||
// if we got both the contract name and arguments parsed so far, we can stop | ||
if (contractName && str) { | ||
return (0, colors_1.colorContract)(contractName) + "." + str; | ||
} | ||
} | ||
return (`<${(0, colors_1.colorContract)("UnknownContract")} ${(0, param_1.formatParam)(currentAddress, dependencies)}>.` + | ||
(str ?? | ||
`${(0, colors_1.colorEvent)("UnknownEvent")}(${(0, param_1.formatParam)(log.data, dependencies)}, ${(0, param_1.formatParam)(log.topics, dependencies)})`)); | ||
const firstPart = `${(0, colors_1.colorContract)(contractName ? contractName : "UnknownContract")}${dependencies.tracerEnv.showAddresses || !contractName | ||
? `(${currentAddress})` | ||
: ""}`; | ||
const secondPart = fragment && result | ||
? `${(0, colors_1.colorEvent)(fragment.name)}(${(0, result_1.formatResult)(result, fragment.inputs, { decimals: -1, shorten: false }, dependencies)})` | ||
: `${(0, colors_1.colorEvent)("UnknownEvent")}(${(0, param_1.formatParam)(log.data, dependencies)}, ${(0, param_1.formatParam)(log.topics, dependencies)})`; | ||
return `${firstPart}.${secondPart}`; | ||
} | ||
exports.formatLog = formatLog; | ||
//# sourceMappingURL=log.js.map |
@@ -18,3 +18,3 @@ "use strict"; | ||
// see if the data is a call | ||
const formattedCallPromise = (0, call_1.formatCall)(ethers_1.ethers.constants.AddressZero, args.data, "0x", 0, 0, td); | ||
const formattedCallPromise = (0, call_1.formatCall)(ethers_1.ethers.constants.AddressZero, args.data, "0x", 0, 0, true, td); | ||
const formattedErrorPromise = (0, error_1.formatError)(args.data, td); | ||
@@ -21,0 +21,0 @@ const formattedCall = await formattedCallPromise; |
@@ -0,4 +1,6 @@ | ||
import "./compile"; | ||
import "./decode"; | ||
import "./test"; | ||
import "./trace"; | ||
import "./tracecall"; | ||
//# sourceMappingURL=index.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
require("./compile"); | ||
require("./decode"); | ||
require("./test"); | ||
require("./trace"); | ||
require("./tracecall"); | ||
//# sourceMappingURL=index.js.map |
@@ -14,3 +14,5 @@ "use strict"; | ||
const tracerEnv = global.tracerEnv; | ||
const recorder = new recorder_1.TraceRecorder(vm, tracerEnv); | ||
// @ts-ignore | ||
const hreArtifacts = global.hreArtifacts; | ||
const recorder = new recorder_1.TraceRecorder(vm, tracerEnv, hreArtifacts); | ||
tracerEnv.recorder = recorder; | ||
@@ -17,0 +19,0 @@ // @ts-ignore |
@@ -10,2 +10,3 @@ import { TracerDependencies } from "../../types"; | ||
gasUsed?: number; | ||
success?: boolean; | ||
} | ||
@@ -12,0 +13,0 @@ declare function format(item: Item<CALL>, dependencies: TracerDependencies): Promise<string>; |
@@ -8,5 +8,6 @@ "use strict"; | ||
// TODO refactor these input types or order | ||
item.params.returnData ?? "0x", item.params.value, item.params.gasLimit, dependencies))); | ||
item.params.returnData ?? "0x", item.params.value, item.params.gasLimit, item.params.success ?? true, // if we don't have success, assume it was successful | ||
dependencies))); | ||
} | ||
exports.default = { format }; | ||
//# sourceMappingURL=call.js.map |
@@ -9,2 +9,3 @@ import { TracerDependencies } from "../../types"; | ||
gasUsed?: number; | ||
success?: boolean; | ||
} | ||
@@ -11,0 +12,0 @@ declare function format(item: Item<DELEGATECALL>, dependencies: TracerDependencies): Promise<string>; |
@@ -9,5 +9,6 @@ "use strict"; | ||
item.params.returnData ?? "0x", 0, // TODO show some how that msg.value | ||
item.params.gasLimit, dependencies))); | ||
item.params.gasLimit, item.params.success ?? true, // if we don't have success, assume it was successful | ||
dependencies))); | ||
} | ||
exports.default = { format }; | ||
//# sourceMappingURL=delegatecall.js.map |
@@ -21,2 +21,3 @@ "use strict"; | ||
const log_1 = __importDefault(require("./log")); | ||
const selfdestruct_1 = __importDefault(require("./selfdestruct")); | ||
function parse(step, currentAddress) { | ||
@@ -71,2 +72,4 @@ switch (step.opcode.name) { | ||
return await revert_1.default.format(item, dependencies); | ||
case "SELFDESTRUCT": | ||
return await selfdestruct_1.default.format(item); | ||
default: | ||
@@ -73,0 +76,0 @@ return item.opcode + " not implemented"; |
@@ -9,2 +9,3 @@ import { TracerDependencies } from "../../types"; | ||
gasUsed?: number; | ||
success?: boolean; | ||
} | ||
@@ -11,0 +12,0 @@ declare function format(item: Item<STATICCALL>, dependencies: TracerDependencies): Promise<string>; |
@@ -19,5 +19,6 @@ "use strict"; | ||
// TODO refactor these input types or order | ||
item.params.returnData ?? "0x", 0, item.params.gasLimit, dependencies))); | ||
item.params.returnData ?? "0x", 0, item.params.gasLimit, item.params.success ?? true, // if we don't have success, assume it was successful | ||
dependencies))); | ||
} | ||
exports.default = { format }; | ||
//# sourceMappingURL=staticcall.js.map |
@@ -10,2 +10,3 @@ /// <reference types="node" /> | ||
import { TracerEnv } from "../types"; | ||
import { Artifacts } from "hardhat/types"; | ||
interface NewContractEvent { | ||
@@ -23,3 +24,3 @@ address: Address; | ||
addressStack: string[]; | ||
constructor(vm: VM, tracerEnv: TracerEnv); | ||
constructor(vm: VM, tracerEnv: TracerEnv, artifacts: Artifacts); | ||
handleBeforeTx(tx: TypedTransaction, resolve: ((result?: any) => void) | undefined): void; | ||
@@ -26,0 +27,0 @@ handleBeforeMessage(message: Message, resolve: ((result?: any) => void) | undefined): void; |
@@ -9,3 +9,3 @@ "use strict"; | ||
class TraceRecorder { | ||
constructor(vm, tracerEnv) { | ||
constructor(vm, tracerEnv, artifacts) { | ||
this.previousTraces = []; | ||
@@ -16,3 +16,3 @@ this.vm = vm; | ||
if (tracerEnv.stateOverrides) { | ||
(0, utils_1.applyStateOverrides)(tracerEnv.stateOverrides, vm); | ||
(0, utils_1.applyStateOverrides)(tracerEnv.stateOverrides, vm, artifacts); | ||
} | ||
@@ -200,7 +200,30 @@ this.awaitedItems = []; | ||
handleAfterMessage(evmResult, resolve) { | ||
// console.log("handleAfterMessage"); | ||
// console.log("handleAfterMessage", !evmResult?.execResult?.exceptionError); | ||
if (!this.trace) { | ||
throw new Error("internal error: trace is undefined"); | ||
} | ||
this.trace.returnCurrentCall("0x" + evmResult.execResult.returnValue.toString("hex")); | ||
if (evmResult.execResult.selfdestruct) { | ||
const selfdestructs = Object.entries(evmResult.execResult.selfdestruct); | ||
for (const [address, beneficiary] of selfdestructs) { | ||
console.log("selfdestruct"); | ||
// console.log( | ||
// "selfdestruct recorded", | ||
// address, | ||
// hexPrefix(beneficiary.toString("hex")) | ||
// ); | ||
this.trace.insertItem({ | ||
opcode: "SELFDESTRUCT", | ||
params: { | ||
beneficiary: (0, utils_1.hexPrefix)(beneficiary.toString("hex")), | ||
}, | ||
}); | ||
} | ||
} | ||
// this.trace.insertItem({ | ||
// opcode: "SELFDESTRUCT", | ||
// params: { | ||
// beneficiary: hexPrefix("1234"), | ||
// }, | ||
// }); | ||
this.trace.returnCurrentCall("0x" + evmResult.execResult.returnValue.toString("hex"), !evmResult?.execResult?.exceptionError); | ||
this.addressStack.pop(); | ||
@@ -207,0 +230,0 @@ resolve?.(); |
import { InterpreterStep } from "@nomicfoundation/ethereumjs-evm"; | ||
import { TracerDependencies } from "../types"; | ||
import { CALL } from "./opcodes/call"; | ||
export interface Item<Params> { | ||
@@ -15,11 +16,3 @@ opcode: string; | ||
}; | ||
export interface CallParams { | ||
to?: string; | ||
inputData: string; | ||
value: string; | ||
returnData?: string; | ||
gasLimit: number; | ||
gasUsed?: number; | ||
} | ||
export interface CallItem extends Item<CallParams> { | ||
export interface CallItem extends Item<CALL> { | ||
opcode: CALL_OPCODES; | ||
@@ -35,3 +28,3 @@ children: Item<any>[]; | ||
}): void; | ||
returnCurrentCall(returnData: string): void; | ||
returnCurrentCall(returnData: string, success: boolean): void; | ||
print(dependencies: TracerDependencies): Promise<void>; | ||
@@ -38,0 +31,0 @@ } |
@@ -38,6 +38,7 @@ "use strict"; | ||
// TODO see how to do this | ||
returnCurrentCall(returnData) { | ||
returnCurrentCall(returnData, success) { | ||
if (!this.parent) | ||
throw new Error("this.parent is undefined"); | ||
this.parent.params.returnData = returnData; | ||
this.parent.params.success = success; | ||
this.parent = this.parent.parent; | ||
@@ -44,0 +45,0 @@ } |
@@ -11,2 +11,3 @@ import { Artifacts } from "hardhat/types"; | ||
defaultVerbosity?: number; | ||
showAddresses?: boolean; | ||
gasCost?: boolean; | ||
@@ -22,2 +23,3 @@ opcodes?: string[]; | ||
verbosity: number; | ||
showAddresses: boolean; | ||
gasCost: boolean; | ||
@@ -27,2 +29,3 @@ opcodes: Map<string, boolean>; | ||
_internal: { | ||
tokenDecimalsCache: Map<string, number>; | ||
printNameTagTip: undefined | "print it" | "already printed"; | ||
@@ -61,3 +64,3 @@ }; | ||
}; | ||
bytecode?: string; | ||
bytecode?: ContractInfo; | ||
balance?: BigNumberish; | ||
@@ -67,2 +70,8 @@ nonce?: BigNumberish; | ||
} | ||
export declare type ContractInfo = string | { | ||
name: string; | ||
libraries?: { | ||
[libraryName: string]: ContractInfo; | ||
}; | ||
}; | ||
//# sourceMappingURL=types.d.ts.map |
import { BigNumber } from "ethers"; | ||
import { VM } from "@nomicfoundation/ethereumjs-vm"; | ||
import { ConfigurableTaskDefinition, HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import { Artifacts, ConfigurableTaskDefinition, HardhatRuntimeEnvironment } from "hardhat/types"; | ||
import { ProviderLike, StateOverrides, StructLog, TracerDependencies, TracerEnv } from "./types"; | ||
@@ -28,6 +28,8 @@ import { Item } from "./trace/transaction"; | ||
export declare function isItem(item: any): item is Item<any>; | ||
export declare function applyStateOverrides(stateOverrides: StateOverrides, vm: VM): Promise<void>; | ||
export declare function applyStateOverrides(stateOverrides: StateOverrides, vm: VM, artifacts: Artifacts): Promise<void>; | ||
export declare function fetchContractName(to: string, provider: ProviderLike): Promise<string | undefined>; | ||
export declare function fetchContractNameFromMethodName(to: string, methodName: string, provider: ProviderLike): Promise<string | undefined>; | ||
export declare function fetchContractDecimals(to: string, provider: ProviderLike): Promise<number | undefined>; | ||
export declare function fetchContractNameUsingArtifacts(address: string, dependencies: TracerDependencies): Promise<string | undefined>; | ||
export declare function getBetterContractName(address: string, dependencies: TracerDependencies): Promise<string | undefined>; | ||
//# sourceMappingURL=utils.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.fetchContractDecimals = exports.fetchContractNameFromMethodName = exports.fetchContractName = exports.applyStateOverrides = exports.isItem = exports.checkIfOpcodesAreValid = exports.hexPrefix = exports.removeColor = exports.compareBytecode = exports.shallowCopyStack2 = exports.shallowCopyStack = exports.parseMemory = exports.parseAddress = exports.parseUint = exports.parseNumber = exports.parseHex = exports.findNextStructLogInDepth = exports.getFromNameTags = exports.isOnlyLogs = exports.applyCliArgsToTracer = exports.DEFAULT_VERBOSITY = exports.addCliParams = void 0; | ||
exports.getBetterContractName = exports.fetchContractNameUsingArtifacts = exports.fetchContractDecimals = exports.fetchContractNameFromMethodName = exports.fetchContractName = exports.applyStateOverrides = exports.isItem = exports.checkIfOpcodesAreValid = exports.hexPrefix = exports.removeColor = exports.compareBytecode = exports.shallowCopyStack2 = exports.shallowCopyStack = exports.parseMemory = exports.parseAddress = exports.parseUint = exports.parseNumber = exports.parseHex = exports.findNextStructLogInDepth = exports.getFromNameTags = exports.isOnlyLogs = exports.applyCliArgsToTracer = exports.DEFAULT_VERBOSITY = exports.addCliParams = void 0; | ||
const utils_1 = require("ethers/lib/utils"); | ||
@@ -61,6 +61,6 @@ const ethers_1 = require("ethers"); | ||
} | ||
if (hre.tracer.recorder === undefined) { | ||
throw new Error(`hardhat-tracer/utils/applyCliArgsToTracer: hre.tracer.recorder is undefined`); | ||
// if recorder was already created, then check opcodes, else it will be checked later | ||
if (hre.tracer.recorder !== undefined) { | ||
checkIfOpcodesAreValid(hre.tracer.opcodes, hre.tracer.recorder.vm); | ||
} | ||
checkIfOpcodesAreValid(hre.tracer.opcodes, hre.tracer.recorder.vm); | ||
} | ||
@@ -180,4 +180,30 @@ if (args.gascost) { | ||
exports.isItem = isItem; | ||
async function applyStateOverrides(stateOverrides, vm) { | ||
function getBytecode(contractInfo, artifacts, addressThis) { | ||
if (typeof contractInfo === "string") { | ||
if (ethers_1.ethers.utils.isHexString(contractInfo)) { | ||
// directly bytecode was given | ||
return contractInfo; | ||
} | ||
else { | ||
// name was given | ||
contractInfo = { | ||
name: contractInfo, | ||
}; | ||
} | ||
} | ||
const artifact = artifacts.readArtifactSync(contractInfo.name); | ||
let bytecode = artifact.deployedBytecode; | ||
if (bytecode.startsWith("0x730000000000000000000000000000000000000000")) { | ||
// this is a library, so we need to replace the placeholder address | ||
bytecode = "0x" + addressThis.slice(2) + bytecode.slice(44); | ||
} | ||
// TODO add support for linking libraries | ||
// artifact.deployedLinkReferences; | ||
return bytecode; | ||
} | ||
async function applyStateOverrides(stateOverrides, vm, artifacts) { | ||
for (const [_address, overrides] of Object.entries(stateOverrides)) { | ||
if (!ethers_1.ethers.utils.isAddress(_address)) { | ||
throw new Error(`Invalid address ${_address} in stateOverrides`); | ||
} | ||
const address = ethereumjs_util_1.Address.fromString(_address); | ||
@@ -197,3 +223,4 @@ // for balance and nonce | ||
if (overrides.bytecode) { | ||
await vm.stateManager.putContractCode(address, Buffer.from(overrides.bytecode, "hex")); | ||
const bytecode = getBytecode(overrides.bytecode, artifacts, _address); | ||
await vm.stateManager.putContractCode(address, Buffer.from(bytecode.slice(2), "hex")); | ||
} | ||
@@ -268,2 +295,39 @@ // for storage slots | ||
exports.fetchContractDecimals = fetchContractDecimals; | ||
async function fetchContractNameUsingArtifacts(address, dependencies) { | ||
const toBytecode = await dependencies.provider.send("eth_getCode", [address]); | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
for (const name of names) { | ||
const _artifact = await dependencies.artifacts.readArtifact(name); | ||
// try to find the contract name | ||
if (compareBytecode(_artifact.deployedBytecode, toBytecode) > 0.5 || | ||
(address === ethers_1.ethers.constants.AddressZero && toBytecode.length <= 2)) { | ||
// if bytecode of "to" is the same as the deployed bytecode | ||
// we can use the artifact name | ||
return _artifact.contractName; | ||
} | ||
} | ||
} | ||
exports.fetchContractNameUsingArtifacts = fetchContractNameUsingArtifacts; | ||
async function getBetterContractName(address, dependencies) { | ||
// 1. See if nameTag exists already | ||
const nameTag = getFromNameTags(address, dependencies); | ||
if (nameTag) { | ||
return nameTag; | ||
} | ||
// 2. See if there is a name() method that gives string or bytes32 | ||
dependencies.tracerEnv.enabled = false; // disable tracer to avoid tracing these calls | ||
const contractNameFromNameMethod = await fetchContractName(address, dependencies.provider); | ||
dependencies.tracerEnv.enabled = true; // enable tracer back | ||
if (contractNameFromNameMethod) { | ||
dependencies.tracerEnv.nameTags[address] = contractNameFromNameMethod; | ||
return contractNameFromNameMethod; | ||
} | ||
// 3. Match bytecode | ||
const contractNameFromArtifacts = await fetchContractNameUsingArtifacts(address, dependencies); | ||
if (contractNameFromArtifacts) { | ||
dependencies.tracerEnv.nameTags[address] = contractNameFromArtifacts; | ||
return contractNameFromArtifacts; | ||
} | ||
} | ||
exports.getBetterContractName = getBetterContractName; | ||
//# sourceMappingURL=utils.js.map |
@@ -63,2 +63,3 @@ "use strict"; | ||
else { | ||
this.dependencies.tracerEnv.printNext = false; | ||
await this.dependencies.tracerEnv.recorder?.previousTraces?.[this.dependencies.tracerEnv.recorder?.previousTraces.length - 1]?.print?.(this.dependencies); | ||
@@ -105,2 +106,3 @@ } | ||
verbosity: utils_1.DEFAULT_VERBOSITY, | ||
showAddresses: false, | ||
gasCost: false, | ||
@@ -112,2 +114,3 @@ opcodes: new Map(), | ||
printNameTagTip: undefined, | ||
tokenDecimalsCache: new Map(), | ||
}, | ||
@@ -114,0 +117,0 @@ decoder: new decoder_1.Decoder(artifacts), |
{ | ||
"name": "hardhat-tracer", | ||
"version": "2.0.0-beta.2", | ||
"version": "2.0.0-beta.3", | ||
"description": "Hardhat Tracer plugin", | ||
@@ -43,3 +43,4 @@ "repository": "github:zemse/hardhat-tracer", | ||
"chai": "^4.2.0", | ||
"hardhat": "^2.11.2", | ||
"hardhat": "^2.12.2", | ||
"hardhat-deploy": "^0.11.20", | ||
"mocha": "^7.1.2", | ||
@@ -46,0 +47,0 @@ "prettier": "2.0.5", |
import { ethers } from "ethers"; | ||
import { | ||
ErrorFragment, | ||
EventFragment, | ||
fetchJson, | ||
@@ -10,3 +11,2 @@ Fragment, | ||
} from "ethers/lib/utils"; | ||
import { string } from "hardhat/internal/core/params/argumentTypes"; | ||
import { Artifacts } from "hardhat/types"; | ||
@@ -22,2 +22,3 @@ | ||
errorFragmentsBySelector: Mapping<ErrorFragment> = new Map(); | ||
eventFragmentsByTopic0: Mapping<EventFragment> = new Map(); | ||
@@ -27,6 +28,10 @@ ready: Promise<void>; | ||
constructor(artifacts: Artifacts) { | ||
this.ready = this._constructor(artifacts); | ||
this.ready = this._updateArtifacts(artifacts); | ||
} | ||
async _constructor(artifacts: Artifacts) { | ||
async updateArtifacts(artifacts: Artifacts) { | ||
this.ready = this._updateArtifacts(artifacts); | ||
} | ||
async _updateArtifacts(artifacts: Artifacts) { | ||
const names = await artifacts.getAllFullyQualifiedNames(); | ||
@@ -40,3 +45,12 @@ | ||
copyFragments(name, iface.errors, this.errorFragmentsBySelector); | ||
copyFragments(name, iface.events, this.eventFragmentsByTopic0); | ||
} | ||
// common errors, these are in function format because Ethers.js does not accept them as errors | ||
const commonErrors = [ | ||
"function Error(string reason)", | ||
"function Panic(uint256 code)", | ||
]; | ||
const iface = new Interface(commonErrors); | ||
copyFragments("", iface.functions, this.errorFragmentsBySelector); | ||
} | ||
@@ -71,2 +85,67 @@ | ||
} | ||
async decodeFunction( | ||
inputData: string, | ||
returnData: string | ||
): Promise<{ | ||
fragment: Fragment; | ||
inputResult: Result; | ||
returnResult: Result | undefined; | ||
contractName: string; | ||
}> { | ||
await this.ready; | ||
return decode( | ||
inputData, | ||
returnData, | ||
"function", | ||
this.functionFragmentsBySelector | ||
); | ||
} | ||
async decodeError( | ||
revertData: string | ||
): Promise<{ | ||
fragment: Fragment; | ||
revertResult: Result; | ||
contractName: string; | ||
}> { | ||
await this.ready; | ||
const { fragment, inputResult, contractName } = await decode( | ||
revertData, | ||
"0x", | ||
"error", | ||
this.errorFragmentsBySelector | ||
); | ||
return { fragment, revertResult: inputResult, contractName }; | ||
} | ||
async decodeEvent( | ||
topics: string[], | ||
data: string | ||
): Promise<{ | ||
fragment: EventFragment; | ||
result: Result; | ||
contractName: string; | ||
}> { | ||
await this.ready; | ||
if (topics.length === 0) { | ||
throw new Error("No topics, cannot decode"); | ||
} | ||
const topic0 = topics[0]; | ||
const fragments = this.eventFragmentsByTopic0.get(topic0); | ||
if (fragments) { | ||
for (const { contractName, fragment } of fragments) { | ||
try { | ||
const iface = new ethers.utils.Interface([fragment]); | ||
const result = iface.parseLog({ data, topics }); | ||
return { fragment, result: result.args, contractName }; | ||
} catch {} | ||
} | ||
} | ||
throw decodeError(topic0); | ||
} | ||
} | ||
@@ -89,3 +168,5 @@ | ||
) { | ||
const selector = ethers.utils.Interface.getSighash(fragment); | ||
const selector = EventFragment.isEventFragment(fragment) | ||
? ethers.utils.Interface.getEventTopic(fragment) | ||
: ethers.utils.Interface.getSighash(fragment); | ||
let fragments = mapping.get(selector); | ||
@@ -95,2 +176,3 @@ if (!fragments) { | ||
} | ||
// TODO while adding, see if we already have a same signature fragment | ||
fragments.push({ contractName, fragment }); | ||
@@ -165,10 +247,13 @@ } | ||
// we couldn't decode it using local ABI, try 4byte.directory | ||
try { | ||
const { fragment, inputResult } = await decodeUsing4byteDirectory( | ||
selector, | ||
inputData, | ||
mapping | ||
); | ||
return { fragment, inputResult }; | ||
} catch {} | ||
// currently only supports function calls | ||
if (type === "function") { | ||
try { | ||
const { fragment, inputResult } = await decodeUsing4byteDirectory( | ||
selector, | ||
inputData, | ||
mapping | ||
); | ||
return { fragment, inputResult }; | ||
} catch {} | ||
} | ||
@@ -175,0 +260,0 @@ // we couldn't decode it after even using 4byte.directory, give up |
@@ -47,2 +47,3 @@ import { | ||
verbosity: userConfig.tracer?.defaultVerbosity ?? DEFAULT_VERBOSITY, | ||
showAddresses: userConfig.tracer?.showAddresses ?? false, | ||
gasCost: userConfig.tracer?.gasCost ?? false, | ||
@@ -54,2 +55,3 @@ opcodes, | ||
printNameTagTip: undefined, | ||
tokenDecimalsCache: new Map(), | ||
}, | ||
@@ -56,0 +58,0 @@ stateOverrides: userConfig.tracer?.stateOverrides, |
@@ -8,2 +8,3 @@ import { extendEnvironment } from "hardhat/config"; | ||
import { Decoder } from "../decoder"; | ||
import { hardhatArguments } from "hardhat"; | ||
@@ -24,12 +25,13 @@ declare module "hardhat/types/runtime" { | ||
global.tracerEnv = hre.tracer; | ||
// @ts-ignore | ||
global.hreArtifacts = hre.artifacts; | ||
getVM(hre).then((vm) => { | ||
hre.tracer.recorder = new TraceRecorder(vm, hre.tracer); | ||
// vm.on("beforeTx", handleBeforeTx); | ||
// vm.on("beforeMessage", handleBeforeMessage); | ||
// vm.on("newContract", handleNewContract); | ||
// vm.on("step", handleStep); | ||
// vm.on("afterMessage", handleAfterMessage); | ||
// vm.on("afterTx", handleAfterTx); | ||
}); | ||
getVM(hre) | ||
.then((vm) => { | ||
hre.tracer.recorder = new TraceRecorder(vm, hre.tracer, hre.artifacts); | ||
}) | ||
.catch(() => { | ||
// if for some reason we can't get the vm, disable hardhat-tracer | ||
hre.tracer.enabled = false; | ||
}); | ||
}); |
@@ -1,22 +0,14 @@ | ||
import { BigNumber, BigNumberish, ethers } from "ethers"; | ||
import { BigNumber, BigNumberish } from "ethers"; | ||
import { colorContract, colorFunction, colorKey } from "../colors"; | ||
import { fetchContractDecimals, getBetterContractName } from "../utils"; | ||
import { formatParam } from "./param"; | ||
import { formatResult } from "./result"; | ||
import { | ||
formatEther, | ||
Fragment, | ||
FunctionFragment, | ||
Interface, | ||
Result, | ||
} from "ethers/lib/utils"; | ||
import { Artifact } from "hardhat/types"; | ||
import { colorContract, colorFunction, colorKey } from "../colors"; | ||
import { ProviderLike, TracerDependencies } from "../types"; | ||
import { | ||
compareBytecode, | ||
fetchContractDecimals, | ||
fetchContractName, | ||
getFromNameTags, | ||
} from "../utils"; | ||
import { formatParam } from "./param"; | ||
import { formatResult } from "./result"; | ||
import { SEPARATOR } from "./separator"; | ||
import { TracerDependencies } from "../types"; | ||
@@ -29,8 +21,5 @@ export async function formatCall( | ||
gas: BigNumberish, | ||
success: boolean, | ||
dependencies: TracerDependencies | ||
) { | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
// TODO handle if `to` is console.log address | ||
let contractName: string | undefined; | ||
@@ -48,3 +37,3 @@ let contractDecimals: number | undefined; | ||
returnResult, | ||
} = await dependencies.tracerEnv.decoder!.decode(input, ret)); | ||
} = await dependencies.tracerEnv.decoder!.decodeFunction(input, ret)); | ||
@@ -55,35 +44,11 @@ // use just contract name | ||
// TODO Find a better contract name | ||
// 1. See if there is a name() method that gives string or bytes32 | ||
const contractNameFromNameMethod = await fetchContractName( | ||
to, | ||
dependencies.provider | ||
); | ||
if (contractNameFromNameMethod !== undefined) { | ||
contractName = contractNameFromNameMethod; | ||
} else { | ||
// 2. Match bytecode | ||
let contractNameFromArtifacts; | ||
const toBytecode = await dependencies.provider.send("eth_getCode", [to]); | ||
for (const name of names) { | ||
const _artifact = await dependencies.artifacts.readArtifact(name); | ||
// try to find the contract name | ||
if ( | ||
compareBytecode(_artifact.deployedBytecode, toBytecode) > 0.5 || | ||
(to === ethers.constants.AddressZero && toBytecode.length <= 2) | ||
) { | ||
// if bytecode of "to" is the same as the deployed bytecode | ||
// we can use the artifact name | ||
contractNameFromArtifacts = _artifact.contractName; | ||
} | ||
// if we got both the contract name and arguments parsed so far, we can stop | ||
if (contractNameFromArtifacts) { | ||
contractName = contractNameFromArtifacts; | ||
break; | ||
} | ||
} | ||
// find a better contract name | ||
const betterContractName = await getBetterContractName(to, dependencies); | ||
if (betterContractName) { | ||
contractName = betterContractName; | ||
} else if (contractName) { | ||
dependencies.tracerEnv.nameTags[to] = contractName; | ||
} | ||
// if ERC20 method found then fetch decimals | ||
if ( | ||
@@ -94,5 +59,28 @@ input.slice(0, 10) === "0x70a08231" || // balanceOf | ||
) { | ||
contractDecimals = await fetchContractDecimals(to, dependencies.provider); | ||
// see if we already know the decimals | ||
const { tokenDecimalsCache } = dependencies.tracerEnv._internal; | ||
const decimals = tokenDecimalsCache.get(to); | ||
if (decimals) { | ||
// if we know decimals then use it | ||
contractDecimals = decimals !== -1 ? decimals : undefined; | ||
} else { | ||
// otherwise fetch it | ||
contractDecimals = await fetchContractDecimals(to, dependencies.provider); | ||
// and cache it | ||
if (contractDecimals !== undefined) { | ||
tokenDecimalsCache.set(to, contractDecimals); | ||
} else { | ||
tokenDecimalsCache.set(to, -1); | ||
} | ||
} | ||
} | ||
const extra = []; | ||
if ((value = BigNumber.from(value)).gt(0)) { | ||
extra.push(`value${SEPARATOR}${formatEther(value)}`); | ||
} | ||
if ((gas = BigNumber.from(gas)).gt(0) && dependencies.tracerEnv.gasCost) { | ||
extra.push(`gas${SEPARATOR}${formatParam(gas, dependencies)}`); | ||
} | ||
if (inputResult && fragment) { | ||
@@ -112,21 +100,13 @@ const inputArgs = formatResult( | ||
) | ||
: // if return data is not decoded, then show return data only if call was success | ||
ret !== "0x" && success !== false // success can be undefined | ||
? ret | ||
: ""; | ||
const extra = []; | ||
if ((value = BigNumber.from(value)).gt(0)) { | ||
extra.push(`value${SEPARATOR}${formatParam(value, dependencies)}`); | ||
} | ||
if ((gas = BigNumber.from(gas)).gt(0) && dependencies.tracerEnv.gasCost) { | ||
extra.push(`gas${SEPARATOR}${formatParam(gas, dependencies)}`); | ||
} | ||
const nameTag = getFromNameTags(to, dependencies); | ||
let nameToPrint = contractName ?? "UnknownContract"; | ||
return `${ | ||
nameTag | ||
? colorContract(nameTag) | ||
: contractName | ||
? colorContract(contractName) | ||
: `<${colorContract("UnknownContract")} ${formatParam( | ||
to, | ||
dependencies | ||
)}>` | ||
dependencies.tracerEnv.showAddresses || nameToPrint === "UnknownContract" | ||
? `${colorContract(nameToPrint)}(${to})` | ||
: colorContract(nameToPrint) | ||
}.${colorFunction(fragment.name)}${ | ||
@@ -139,14 +119,18 @@ extra.length !== 0 ? `{${extra.join(",")}}` : "" | ||
if (contractName) { | ||
return `${colorContract(contractName)}.<${colorFunction( | ||
"UnknownFunction" | ||
)}>(${colorKey("input" + SEPARATOR)}${input}, ${colorKey( | ||
return `${ | ||
dependencies.tracerEnv.showAddresses | ||
? `${colorContract(contractName)}(${to})` | ||
: colorContract(contractName) | ||
}.<${colorFunction("UnknownFunction")}>${ | ||
extra.length !== 0 ? `{${extra.join(",")}}` : "" | ||
}(${colorKey("input" + SEPARATOR)}${input}, ${colorKey( | ||
"ret" + SEPARATOR | ||
)}${ret})`; | ||
} else { | ||
return `${colorFunction("UnknownContractAndFunction")}(${colorKey( | ||
"to" + SEPARATOR | ||
)}${to}, ${colorKey("input" + SEPARATOR)}${input}, ${colorKey( | ||
"ret" + SEPARATOR | ||
)}${ret || "0x"})`; | ||
return `${colorFunction("UnknownContractAndFunction")}${ | ||
extra.length !== 0 ? `{${extra.join(",")}}` : "" | ||
}(${colorKey("to" + SEPARATOR)}${to}, ${colorKey( | ||
"input" + SEPARATOR | ||
)}${input}, ${colorKey("ret" + SEPARATOR)}${ret || "0x"})`; | ||
} | ||
} |
@@ -14,14 +14,10 @@ import { Interface } from "ethers/lib/utils"; | ||
) { | ||
const commonErrors = [ | ||
"function Error(string reason)", | ||
"function Panic(uint256 code)", | ||
]; | ||
try { | ||
const iface = new Interface(commonErrors); | ||
const parsed = iface.parseTransaction({ | ||
data: revertData, | ||
}); | ||
const { | ||
fragment, | ||
revertResult, | ||
} = await dependencies.tracerEnv.decoder!.decodeError(revertData); | ||
if (parsed.name === "Panic") { | ||
const panicCode = parsed.args.code.toNumber(); | ||
if (fragment.name === "Panic") { | ||
const panicCode = revertResult.code.toNumber(); | ||
let situation = ""; | ||
@@ -57,3 +53,3 @@ switch (panicCode) { | ||
} | ||
return `${colorError(parsed.name)}(${formatObject({ | ||
return `${colorError(fragment.name)}(${formatObject({ | ||
code: panicCode, | ||
@@ -65,4 +61,4 @@ situation, | ||
const formatted = formatResult( | ||
parsed.args, | ||
parsed.functionFragment.inputs, | ||
revertResult, | ||
fragment.inputs, | ||
{ decimals: -1, shorten: false }, | ||
@@ -72,23 +68,6 @@ dependencies | ||
return `${colorError(parsed.name)}(${formatted})`; | ||
return `${colorError(fragment.name)}(${formatted})`; | ||
} catch {} | ||
// if error not common then try to parse it as a custom error | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
for (const name of names) { | ||
const artifact = await dependencies.artifacts.readArtifact(name); | ||
const iface = new Interface(artifact.abi); | ||
try { | ||
const errorDesc = iface.parseError(revertData); | ||
return `${colorError(errorDesc.name)}(${formatResult( | ||
errorDesc.args, | ||
errorDesc.errorFragment.inputs, | ||
{ decimals: -1, shorten: false }, | ||
dependencies | ||
)})`; | ||
} catch {} | ||
} | ||
// if error could not be decoded, then just show the data | ||
return `${colorError("UnknownError")}(${formatParam( | ||
@@ -95,0 +74,0 @@ revertData, |
@@ -1,10 +0,7 @@ | ||
import { Interface, LogDescription } from "ethers/lib/utils"; | ||
import { Artifact } from "hardhat/types"; | ||
import { colorContract, colorEvent } from "../colors"; | ||
import { TracerDependencies, TracerDependenciesExtended } from "../types"; | ||
import { compareBytecode, getFromNameTags } from "../utils"; | ||
import { EventFragment, Result } from "ethers/lib/utils"; | ||
import { formatParam } from "./param"; | ||
import { formatResult } from "./result"; | ||
import { getBetterContractName } from "../utils"; | ||
import { TracerDependencies } from "../types"; | ||
@@ -16,60 +13,54 @@ export async function formatLog( | ||
) { | ||
// TODO make the contractName code common between formatCall and formatLog | ||
const nameTag = currentAddress | ||
? getFromNameTags(currentAddress, dependencies) | ||
: undefined; | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
const code = | ||
!nameTag && currentAddress | ||
? ((await dependencies.provider.send("eth_getCode", [ | ||
currentAddress, | ||
"latest", | ||
])) as string) | ||
: undefined; | ||
let fragment: EventFragment | undefined, | ||
result: Result | undefined, | ||
contractName: string | undefined; | ||
try { | ||
({ | ||
fragment, | ||
result, | ||
contractName, | ||
} = await dependencies.tracerEnv.decoder!.decodeEvent( | ||
log.topics, | ||
log.data | ||
)); | ||
let str: string | undefined; | ||
let contractName: string | undefined = nameTag; | ||
for (const name of names) { | ||
const artifact = await dependencies.artifacts.readArtifact(name); | ||
const iface = new Interface(artifact.abi); | ||
// use just contract name | ||
contractName = contractName.split(":")[1]; | ||
} catch {} | ||
// try to find the contract name | ||
if (compareBytecode(artifact.deployedBytecode, code ?? "0x") > 0.5) { | ||
contractName = artifact.contractName; | ||
// find a better contract name | ||
if (currentAddress) { | ||
const betterContractName = await getBetterContractName( | ||
currentAddress, | ||
dependencies | ||
); | ||
if (betterContractName) { | ||
contractName = betterContractName; | ||
} else if (contractName) { | ||
dependencies.tracerEnv.nameTags[currentAddress] = contractName; | ||
} | ||
} | ||
// try to parse the arguments | ||
try { | ||
const parsed = iface.parseLog(log); | ||
const decimals = -1; | ||
const firstPart = `${colorContract( | ||
contractName ? contractName : "UnknownContract" | ||
)}${ | ||
dependencies.tracerEnv.showAddresses || !contractName | ||
? `(${currentAddress})` | ||
: "" | ||
}`; | ||
if (!contractName) { | ||
contractName = artifact.contractName; | ||
} | ||
const secondPart = | ||
fragment && result | ||
? `${colorEvent(fragment.name)}(${formatResult( | ||
result, | ||
fragment.inputs, | ||
{ decimals: -1, shorten: false }, | ||
dependencies | ||
)})` | ||
: `${colorEvent("UnknownEvent")}(${formatParam( | ||
log.data, | ||
dependencies | ||
)}, ${formatParam(log.topics, dependencies)})`; | ||
str = `${colorEvent(parsed.name)}(${formatResult( | ||
parsed.args, | ||
parsed.eventFragment.inputs, | ||
{ decimals, shorten: false }, | ||
dependencies | ||
)})`; | ||
} catch {} | ||
// if we got both the contract name and arguments parsed so far, we can stop | ||
if (contractName && str) { | ||
return colorContract(contractName) + "." + str; | ||
} | ||
} | ||
return ( | ||
`<${colorContract("UnknownContract")} ${formatParam( | ||
currentAddress, | ||
dependencies | ||
)}>.` + | ||
(str ?? | ||
`${colorEvent("UnknownEvent")}(${formatParam( | ||
log.data, | ||
dependencies | ||
)}, ${formatParam(log.topics, dependencies)})`) | ||
); | ||
return `${firstPart}.${secondPart}`; | ||
} |
@@ -24,2 +24,3 @@ import { ethers } from "ethers"; | ||
0, | ||
true, | ||
td | ||
@@ -26,0 +27,0 @@ ); |
@@ -0,3 +1,5 @@ | ||
import "./compile"; | ||
import "./decode"; | ||
import "./test"; | ||
import "./trace"; | ||
import "./tracecall"; |
import { ethers } from "ethers"; | ||
import { arrayify } from "ethers/lib/utils"; | ||
import { task } from "hardhat/config"; | ||
import { getNode } from "../get-vm"; | ||
import { printDebugTrace, printDebugTraceOrLogs } from "../print"; | ||
@@ -20,3 +18,5 @@ import { addCliParams, applyCliArgsToTracer } from "../utils"; | ||
const tracerEnv = global.tracerEnv; | ||
const recorder = new TraceRecorder(vm, tracerEnv); | ||
// @ts-ignore | ||
const hreArtifacts = global.hreArtifacts; | ||
const recorder = new TraceRecorder(vm, tracerEnv, hreArtifacts); | ||
tracerEnv.recorder = recorder; | ||
@@ -23,0 +23,0 @@ // @ts-ignore |
@@ -12,2 +12,3 @@ import { formatCall } from "../../format/call"; | ||
gasUsed?: number; | ||
success?: boolean; | ||
} | ||
@@ -28,2 +29,3 @@ | ||
item.params.gasLimit, | ||
item.params.success ?? true, // if we don't have success, assume it was successful | ||
dependencies | ||
@@ -30,0 +32,0 @@ )) |
@@ -11,2 +11,3 @@ import { formatCall } from "../../format/call"; | ||
gasUsed?: number; | ||
success?: boolean; | ||
} | ||
@@ -27,2 +28,3 @@ | ||
item.params.gasLimit, | ||
item.params.success ?? true, // if we don't have success, assume it was successful | ||
dependencies | ||
@@ -29,0 +31,0 @@ )) |
@@ -18,2 +18,3 @@ import call from "./call"; | ||
import log from "./log"; | ||
import selfdestruct from "./selfdestruct"; | ||
@@ -75,2 +76,4 @@ export function parse( | ||
return await revert.format(item, dependencies); | ||
case "SELFDESTRUCT": | ||
return await selfdestruct.format(item); | ||
default: | ||
@@ -77,0 +80,0 @@ return item.opcode + " not implemented"; |
@@ -18,2 +18,3 @@ import { colorConsole } from "../../colors"; | ||
gasUsed?: number; | ||
success?: boolean; | ||
} | ||
@@ -43,2 +44,3 @@ | ||
item.params.gasLimit, | ||
item.params.success ?? true, // if we don't have success, assume it was successful | ||
dependencies | ||
@@ -45,0 +47,0 @@ )) |
@@ -22,2 +22,3 @@ import { InterpreterStep } from "@nomicfoundation/ethereumjs-evm"; | ||
import { TracerEnv } from "../types"; | ||
import { Artifacts } from "hardhat/types"; | ||
@@ -41,3 +42,3 @@ // const txs: TransactionTrace[] = []; | ||
constructor(vm: VM, tracerEnv: TracerEnv) { | ||
constructor(vm: VM, tracerEnv: TracerEnv, artifacts: Artifacts) { | ||
this.vm = vm; | ||
@@ -49,3 +50,3 @@ this.tracerEnv = tracerEnv; | ||
if (tracerEnv.stateOverrides) { | ||
applyStateOverrides(tracerEnv.stateOverrides, vm); | ||
applyStateOverrides(tracerEnv.stateOverrides, vm, artifacts); | ||
} | ||
@@ -274,3 +275,3 @@ | ||
) { | ||
// console.log("handleAfterMessage"); | ||
// console.log("handleAfterMessage", !evmResult?.execResult?.exceptionError); | ||
@@ -281,4 +282,32 @@ if (!this.trace) { | ||
if (evmResult.execResult.selfdestruct) { | ||
const selfdestructs = Object.entries(evmResult.execResult.selfdestruct); | ||
for (const [address, beneficiary] of selfdestructs) { | ||
console.log("selfdestruct"); | ||
// console.log( | ||
// "selfdestruct recorded", | ||
// address, | ||
// hexPrefix(beneficiary.toString("hex")) | ||
// ); | ||
this.trace.insertItem({ | ||
opcode: "SELFDESTRUCT", | ||
params: { | ||
beneficiary: hexPrefix(beneficiary.toString("hex")), | ||
}, | ||
}); | ||
} | ||
} | ||
// this.trace.insertItem({ | ||
// opcode: "SELFDESTRUCT", | ||
// params: { | ||
// beneficiary: hexPrefix("1234"), | ||
// }, | ||
// }); | ||
this.trace.returnCurrentCall( | ||
"0x" + evmResult.execResult.returnValue.toString("hex") | ||
"0x" + evmResult.execResult.returnValue.toString("hex"), | ||
!evmResult?.execResult?.exceptionError | ||
); | ||
@@ -285,0 +314,0 @@ this.addressStack.pop(); |
@@ -6,2 +6,3 @@ // export type AbstractParams = { [key: string]: any }; | ||
import { format } from "./opcodes"; | ||
import { CALL } from "./opcodes/call"; | ||
@@ -22,12 +23,3 @@ export interface Item<Params> { | ||
export interface CallParams { | ||
to?: string; | ||
inputData: string; | ||
value: string; // hex string | ||
returnData?: string; | ||
gasLimit: number; | ||
gasUsed?: number; | ||
} | ||
export interface CallItem extends Item<CallParams> { | ||
export interface CallItem extends Item<CALL> { | ||
opcode: CALL_OPCODES; | ||
@@ -96,5 +88,6 @@ children: Item<any>[]; | ||
// TODO see how to do this | ||
returnCurrentCall(returnData: string) { | ||
returnCurrentCall(returnData: string, success: boolean) { | ||
if (!this.parent) throw new Error("this.parent is undefined"); | ||
this.parent.params.returnData = returnData; | ||
this.parent.params.success = success; | ||
this.parent = this.parent.parent as CallItem; | ||
@@ -101,0 +94,0 @@ } |
@@ -14,2 +14,3 @@ import { VM } from "@nomicfoundation/ethereumjs-vm"; | ||
defaultVerbosity?: number; | ||
showAddresses?: boolean; | ||
gasCost?: boolean; | ||
@@ -26,2 +27,3 @@ opcodes?: string[]; | ||
verbosity: number; | ||
showAddresses: boolean; | ||
gasCost: boolean; | ||
@@ -32,2 +34,3 @@ opcodes: Map<string, boolean>; // string[]; // TODO have a map of opcode to boolean | ||
_internal: { | ||
tokenDecimalsCache: Map<string, number>; | ||
printNameTagTip: | ||
@@ -74,3 +77,3 @@ | undefined // meaning "no need to print" | ||
}; | ||
bytecode?: string; | ||
bytecode?: ContractInfo; | ||
balance?: BigNumberish; | ||
@@ -80,1 +83,10 @@ nonce?: BigNumberish; | ||
} | ||
export type ContractInfo = | ||
| string // bytecode in hex or name of the contract | ||
| { | ||
name: string; | ||
libraries?: { | ||
[libraryName: string]: ContractInfo; | ||
}; | ||
}; |
106
src/utils.ts
@@ -10,2 +10,3 @@ import { | ||
import { | ||
Artifacts, | ||
ConfigurableTaskDefinition, | ||
@@ -16,2 +17,3 @@ HardhatRuntimeEnvironment, | ||
import { | ||
ContractInfo, | ||
ProviderLike, | ||
@@ -106,8 +108,6 @@ StateOverrides, | ||
if (hre.tracer.recorder === undefined) { | ||
throw new Error( | ||
`hardhat-tracer/utils/applyCliArgsToTracer: hre.tracer.recorder is undefined` | ||
); | ||
// if recorder was already created, then check opcodes, else it will be checked later | ||
if (hre.tracer.recorder !== undefined) { | ||
checkIfOpcodesAreValid(hre.tracer.opcodes, hre.tracer.recorder.vm); | ||
} | ||
checkIfOpcodesAreValid(hre.tracer.opcodes, hre.tracer.recorder.vm); | ||
} | ||
@@ -252,7 +252,43 @@ | ||
function getBytecode( | ||
contractInfo: ContractInfo, | ||
artifacts: Artifacts, | ||
addressThis: string | ||
) { | ||
if (typeof contractInfo === "string") { | ||
if (ethers.utils.isHexString(contractInfo)) { | ||
// directly bytecode was given | ||
return contractInfo; | ||
} else { | ||
// name was given | ||
contractInfo = { | ||
name: contractInfo, | ||
}; | ||
} | ||
} | ||
const artifact = artifacts.readArtifactSync(contractInfo.name); | ||
let bytecode = artifact.deployedBytecode; | ||
if (bytecode.startsWith("0x730000000000000000000000000000000000000000")) { | ||
// this is a library, so we need to replace the placeholder address | ||
bytecode = "0x" + addressThis.slice(2) + bytecode.slice(44); | ||
} | ||
// TODO add support for linking libraries | ||
// artifact.deployedLinkReferences; | ||
return bytecode; | ||
} | ||
export async function applyStateOverrides( | ||
stateOverrides: StateOverrides, | ||
vm: VM | ||
vm: VM, | ||
artifacts: Artifacts | ||
) { | ||
for (const [_address, overrides] of Object.entries(stateOverrides)) { | ||
if (!ethers.utils.isAddress(_address)) { | ||
throw new Error(`Invalid address ${_address} in stateOverrides`); | ||
} | ||
const address = Address.fromString(_address); | ||
@@ -273,5 +309,6 @@ // for balance and nonce | ||
if (overrides.bytecode) { | ||
const bytecode = getBytecode(overrides.bytecode, artifacts, _address); | ||
await vm.stateManager.putContractCode( | ||
address, | ||
Buffer.from(overrides.bytecode, "hex") | ||
Buffer.from(bytecode.slice(2), "hex") | ||
); | ||
@@ -361,1 +398,56 @@ } | ||
} | ||
export async function fetchContractNameUsingArtifacts( | ||
address: string, | ||
dependencies: TracerDependencies | ||
): Promise<string | undefined> { | ||
const toBytecode = await dependencies.provider.send("eth_getCode", [address]); | ||
const names = await dependencies.artifacts.getAllFullyQualifiedNames(); | ||
for (const name of names) { | ||
const _artifact = await dependencies.artifacts.readArtifact(name); | ||
// try to find the contract name | ||
if ( | ||
compareBytecode(_artifact.deployedBytecode, toBytecode) > 0.5 || | ||
(address === ethers.constants.AddressZero && toBytecode.length <= 2) | ||
) { | ||
// if bytecode of "to" is the same as the deployed bytecode | ||
// we can use the artifact name | ||
return _artifact.contractName; | ||
} | ||
} | ||
} | ||
export async function getBetterContractName( | ||
address: string, | ||
dependencies: TracerDependencies | ||
): Promise<string | undefined> { | ||
// 1. See if nameTag exists already | ||
const nameTag = getFromNameTags(address, dependencies); | ||
if (nameTag) { | ||
return nameTag; | ||
} | ||
// 2. See if there is a name() method that gives string or bytes32 | ||
dependencies.tracerEnv.enabled = false; // disable tracer to avoid tracing these calls | ||
const contractNameFromNameMethod = await fetchContractName( | ||
address, | ||
dependencies.provider | ||
); | ||
dependencies.tracerEnv.enabled = true; // enable tracer back | ||
if (contractNameFromNameMethod) { | ||
dependencies.tracerEnv.nameTags[address] = contractNameFromNameMethod; | ||
return contractNameFromNameMethod; | ||
} | ||
// 3. Match bytecode | ||
const contractNameFromArtifacts = await fetchContractNameUsingArtifacts( | ||
address, | ||
dependencies | ||
); | ||
if (contractNameFromArtifacts) { | ||
dependencies.tracerEnv.nameTags[address] = contractNameFromArtifacts; | ||
return contractNameFromArtifacts; | ||
} | ||
} |
@@ -81,2 +81,3 @@ import { ethers } from "ethers"; | ||
} else { | ||
this.dependencies.tracerEnv.printNext = false; | ||
await this.dependencies.tracerEnv.recorder?.previousTraces?.[ | ||
@@ -134,2 +135,3 @@ this.dependencies.tracerEnv.recorder?.previousTraces.length - 1 | ||
verbosity: DEFAULT_VERBOSITY, | ||
showAddresses: false, | ||
gasCost: false, | ||
@@ -141,2 +143,3 @@ opcodes: new Map(), | ||
printNameTagTip: undefined, | ||
tokenDecimalsCache: new Map(), | ||
}, | ||
@@ -143,0 +146,0 @@ decoder: new Decoder(artifacts), |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
344485
17
236
6104