Socket
Socket
Sign inDemoInstall

hardhat-tracer

Package Overview
Dependencies
290
Maintainers
1
Versions
52
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.1.1 to 2.0.0-beta.1

dist/src/decoder.d.ts

3

dist/src/colors.d.ts

@@ -16,2 +16,5 @@ export declare const colorLabel: import("chalk").Chalk & {

};
export declare const colorConsole: import("chalk").Chalk & {
supportsColor: import("chalk").ColorSupport;
};
export declare const colorKey: import("chalk").Chalk & {

@@ -18,0 +21,0 @@ supportsColor: import("chalk").ColorSupport;

3

dist/src/colors.js

@@ -6,3 +6,3 @@ "use strict";

Object.defineProperty(exports, "__esModule", { value: true });
exports.colorWarning = exports.colorIndexed = exports.colorNameTag = exports.colorSstore = exports.colorSload = exports.colorKey = exports.colorError = exports.colorEvent = exports.colorFunction = exports.colorContract = exports.colorLabel = void 0;
exports.colorWarning = exports.colorIndexed = exports.colorNameTag = exports.colorSstore = exports.colorSload = exports.colorKey = exports.colorConsole = exports.colorError = exports.colorEvent = exports.colorFunction = exports.colorContract = exports.colorLabel = void 0;
const chalk_1 = __importDefault(require("chalk"));

@@ -14,2 +14,3 @@ exports.colorLabel = chalk_1.default.white;

exports.colorError = chalk_1.default.red;
exports.colorConsole = chalk_1.default.blue;
exports.colorKey = chalk_1.default.magenta;

@@ -16,0 +17,0 @@ exports.colorSload = chalk_1.default.blueBright;

@@ -6,4 +6,29 @@ "use strict";

(0, config_1.extendConfig)((config, userConfig) => {
config.tracer = (0, utils_1.getTracerEnvFromUserInput)(userConfig.tracer);
// config.tracer = getTracerEnvFromUserInput(userConfig.tracer);
const opcodes = new Map();
// always active opcodes
const opcodesToActivate = [];
if (userConfig.tracer?.opcodes) {
if (!Array.isArray(userConfig.tracer.opcodes)) {
throw new Error("tracer.opcodes in hardhat user config should be array");
}
opcodesToActivate.push(...userConfig.tracer.opcodes);
}
for (const opcode of opcodesToActivate) {
opcodes.set(opcode, true);
}
config.tracer = {
enabled: userConfig.tracer?.enabled ?? false,
ignoreNext: false,
verbosity: userConfig.tracer?.defaultVerbosity ?? utils_1.DEFAULT_VERBOSITY,
gasCost: userConfig.tracer?.gasCost ?? false,
opcodes,
nameTags: userConfig.tracer?.nameTags ?? {},
// @ts-ignore TODO remove, this has no place in "config"
_internal: {
printNameTagTip: undefined,
},
stateOverrides: userConfig.tracer?.stateOverrides,
};
});
//# sourceMappingURL=config.js.map

@@ -6,6 +6,22 @@ "use strict";

require("hardhat/types/runtime");
const get_vm_1 = require("../get-vm");
const recorder_1 = require("../trace/recorder");
const decoder_1 = require("../decoder");
(0, config_1.extendEnvironment)((hre) => {
// copy reference of config.tracer to tracer
// TODO take this properly, env can contain things that config does not need to.
hre.tracer = hre.config.tracer;
hre.tracer.decoder = new decoder_1.Decoder(hre.artifacts);
// @ts-ignore
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);
});
});
//# sourceMappingURL=hre.js.map
import { BigNumberish } from "ethers";
import { TracerDependenciesExtended } from "../types";
export declare function formatCall(to: string, input: string, ret: string, value: BigNumberish, gas: BigNumberish, dependencies: TracerDependenciesExtended): Promise<string>;
import { TracerDependencies } from "../types";
export declare function formatCall(to: string, input: string, ret: string, value: BigNumberish, gas: BigNumberish, dependencies: TracerDependencies): Promise<string>;
//# sourceMappingURL=call.d.ts.map

@@ -5,54 +5,71 @@ "use strict";

const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
const colors_1 = require("../colors");
const utils_2 = require("../utils");
const utils_1 = require("../utils");
const param_1 = require("./param");
const result_1 = require("./result");
const separator_1 = require("./separator");
async function formatCall(to, input, ret, value, gas, dependencies) {
const toBytecode = await dependencies.provider.send("eth_getCode", [to]);
const names = await dependencies.artifacts.getAllFullyQualifiedNames();
// TODO handle if `to` is console.log address
let contractName;
let result;
let result2;
let functionFragment;
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, 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
contractName = _artifact.contractName;
}
// try to parse the arguments
try {
// if this doesnt throw, we likely found an Artifact that recognizes the input
const signature = input.slice(0, 10);
result = iface.decodeFunctionData(signature, input);
try {
result2 = iface.decodeFunctionResult(signature, ret);
let contractDecimals;
let inputResult;
let returnResult;
let fragment;
try {
({
fragment,
contractName,
inputResult,
returnResult,
} = await dependencies.tracerEnv.decoder.decode(input, ret));
// use just contract name
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;
}
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;
}
catch (_a) { }
functionFragment = iface.getFunction(signature);
// if we got both the contract name and arguments parsed so far, we can stop
if (contractNameFromArtifacts) {
contractName = contractNameFromArtifacts;
break;
}
}
catch (_b) { }
// if we got both the contract name and arguments parsed so far, we can stop
if (contractName && result) {
break;
}
}
if (result && functionFragment) {
const inputArgs = (0, result_1.formatResult)(result, functionFragment, { decimals: -1, isInput: true, shorten: false }, dependencies);
const outputArgs = result2
? (0, result_1.formatResult)(result2, functionFragment, { decimals: -1, isInput: false, shorten: true }, dependencies)
if (input.slice(0, 10) === "0x70a08231" || // balanceOf
input.slice(0, 10) === "0xa9059cbb" || // transfer
input.slice(0, 10) === "0x23b872dd" // transferFrom
) {
contractDecimals = await (0, utils_1.fetchContractDecimals)(to, dependencies.provider);
}
if (inputResult && fragment) {
const inputArgs = (0, result_1.formatResult)(inputResult, fragment.inputs, { decimals: contractDecimals, shorten: false }, dependencies);
const outputArgs = returnResult
? (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: ${(0, param_1.formatParam)(value, dependencies)}`);
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: ${(0, param_1.formatParam)(gas, dependencies)}`);
extra.push(`gas${separator_1.SEPARATOR}${(0, param_1.formatParam)(gas, dependencies)}`);
}
const nameTag = (0, utils_2.getFromNameTags)(to, dependencies);
const nameTag = (0, utils_1.getFromNameTags)(to, dependencies);
return `${nameTag

@@ -62,10 +79,10 @@ ? (0, colors_1.colorContract)(nameTag)

? (0, colors_1.colorContract)(contractName)
: `<${(0, colors_1.colorContract)("UnknownContract")} ${(0, param_1.formatParam)(to, dependencies)}>`}.${(0, colors_1.colorFunction)(functionFragment.name)}${extra.length !== 0 ? `{${extra.join(",")}}` : ""}(${inputArgs})${outputArgs ? ` => (${outputArgs})` : ""}`;
: `<${(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})` : ""}`;
}
// TODO add flag to hide unrecognized stuff
if (toBytecode.length > 2 && contractName) {
return `${(0, colors_1.colorContract)(contractName)}.<${(0, colors_1.colorFunction)("UnknownFunction")}>(${(0, colors_1.colorKey)("input=")}${input}, ${(0, colors_1.colorKey)("ret=")}${ret})`;
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})`;
}
else {
return `${(0, colors_1.colorFunction)("UnknownContractAndFunction")}(${(0, colors_1.colorKey)("to=")}${to}, ${(0, colors_1.colorKey)("input=")}${input}, ${(0, colors_1.colorKey)("ret=")}${ret})`;
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"})`;
}

@@ -72,0 +89,0 @@ }

@@ -1,4 +0,4 @@

import { BigNumber } from "ethers";
import { TracerDependenciesExtended } from "../types";
export declare function formatContract(code: string, value: BigNumber, salt: BigNumber | null, deployedAddress: string | null, dependencies: TracerDependenciesExtended): Promise<string>;
import { BigNumberish } from "ethers";
import { TracerDependencies } from "../types";
export declare function formatContract(code: string, value: BigNumberish, salt: BigNumberish | null, deployedAddress: string | null, dependencies: TracerDependencies): Promise<string>;
//# sourceMappingURL=contract.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatContract = void 0;
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");

@@ -10,2 +11,5 @@ const colors_1 = require("../colors");

async function formatContract(code, value, salt, deployedAddress, dependencies) {
value = ethers_1.BigNumber.from(value);
if (salt !== null)
salt = ethers_1.BigNumber.from(salt);
const names = await dependencies.artifacts.getAllFullyQualifiedNames();

@@ -21,3 +25,3 @@ for (const name of names) {

const constructorParamsDecoded = iface._decodeParams(iface.deploy.inputs, "0x" + code.slice(artifact.bytecode.length));
const inputArgs = (0, result_1.formatResult)(constructorParamsDecoded, iface.deploy, { decimals: -1, isInput: true, shorten: false }, dependencies);
const inputArgs = (0, result_1.formatResult)(constructorParamsDecoded, iface.deploy.inputs, { decimals: -1, shorten: false }, dependencies);
const extra = [];

@@ -34,3 +38,3 @@ if (value.gt(0)) {

}
catch (_a) { }
catch { }
}

@@ -37,0 +41,0 @@ }

@@ -1,3 +0,3 @@

import { TracerDependenciesExtended } from "../types";
export declare function formatError(revertData: string, dependencies: TracerDependenciesExtended): Promise<string>;
import { TracerDependencies } from "../types";
export declare function formatError(revertData: string, dependencies: TracerDependencies): Promise<string>;
//# sourceMappingURL=error.d.ts.map

@@ -56,6 +56,6 @@ "use strict";

}
const formatted = (0, result_1.formatResult)(parsed.args, parsed.functionFragment, { decimals: -1, isInput: true, shorten: false }, dependencies);
const formatted = (0, result_1.formatResult)(parsed.args, parsed.functionFragment.inputs, { decimals: -1, shorten: false }, dependencies);
return `${(0, colors_1.colorError)(parsed.name)}(${formatted})`;
}
catch (_a) { }
catch { }
// if error not common then try to parse it as a custom error

@@ -68,5 +68,5 @@ const names = await dependencies.artifacts.getAllFullyQualifiedNames();

const errorDesc = iface.parseError(revertData);
return `${(0, colors_1.colorError)(errorDesc.name)}(${(0, result_1.formatResult)(errorDesc.args, errorDesc.errorFragment, { decimals: -1, isInput: true, shorten: false }, dependencies)})`;
return `${(0, colors_1.colorError)(errorDesc.name)}(${(0, result_1.formatResult)(errorDesc.args, errorDesc.errorFragment.inputs, { decimals: -1, shorten: false }, dependencies)})`;
}
catch (_b) { }
catch { }
}

@@ -73,0 +73,0 @@ return `${(0, colors_1.colorError)("UnknownError")}(${(0, param_1.formatParam)(revertData, dependencies)})`;

@@ -13,3 +13,3 @@ "use strict";

if (dependencies.tracerEnv.gasCost) {
return ` (cost: ${gasCost !== null && gasCost !== void 0 ? gasCost : structLog.gasCost})`;
return ` (cost: ${gasCost ?? structLog.gasCost})`;
}

@@ -16,0 +16,0 @@ else {

@@ -1,6 +0,6 @@

import { TracerDependenciesExtended } from "../types";
import { TracerDependencies } from "../types";
export declare function formatLog(log: {
data: string;
topics: string[];
}, currentAddress: string | undefined, dependencies: TracerDependenciesExtended): Promise<string>;
}, currentAddress: string | undefined, dependencies: TracerDependencies): Promise<string>;
//# sourceMappingURL=log.d.ts.map

@@ -10,2 +10,3 @@ "use strict";

async function formatLog(log, currentAddress, dependencies) {
// TODO make the contractName code common between formatCall and formatLog
const nameTag = currentAddress

@@ -27,3 +28,3 @@ ? (0, utils_2.getFromNameTags)(currentAddress, dependencies)

// try to find the contract name
if ((0, utils_2.compareBytecode)(artifact.deployedBytecode, code !== null && code !== void 0 ? code : "0x") > 0.5) {
if ((0, utils_2.compareBytecode)(artifact.deployedBytecode, code ?? "0x") > 0.5) {
contractName = artifact.contractName;

@@ -35,5 +36,8 @@ }

const decimals = -1;
str = `${(0, colors_1.colorEvent)(parsed.name)}(${(0, result_1.formatResult)(parsed.args, parsed.eventFragment, { decimals, isInput: true, shorten: false }, dependencies)})`;
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)})`;
}
catch (_a) { }
catch { }
// if we got both the contract name and arguments parsed so far, we can stop

@@ -45,5 +49,6 @@ if (contractName && str) {

return (`<${(0, colors_1.colorContract)("UnknownContract")} ${(0, param_1.formatParam)(currentAddress, dependencies)}>.` +
(str !== null && str !== void 0 ? str : `${(0, colors_1.colorEvent)("UnknownEvent")}(${(0, param_1.formatParam)(log.data, dependencies)}, ${(0, param_1.formatParam)(log.topics, dependencies)})`));
(str ??
`${(0, colors_1.colorEvent)("UnknownEvent")}(${(0, param_1.formatParam)(log.data, dependencies)}, ${(0, param_1.formatParam)(log.topics, dependencies)})`));
}
exports.formatLog = formatLog;
//# sourceMappingURL=log.js.map

@@ -5,2 +5,3 @@ "use strict";

const colors_1 = require("../colors");
const separator_1 = require("./separator");
function formatObject(obj) {

@@ -19,3 +20,3 @@ return Object.entries(obj)

}
return `${(0, colors_1.colorKey)(key + "=")}${value}`;
return `${(0, colors_1.colorKey)(key + separator_1.SEPARATOR)}${value}`;
})

@@ -22,0 +23,0 @@ .join(", ");

@@ -1,3 +0,3 @@

import { TracerDependenciesExtended } from "../types";
export declare function formatParam(value: any, dependencies: TracerDependenciesExtended): string;
import { TracerDependencies } from "../types";
export declare function formatParam(value: any, dependencies: TracerDependencies): string;
//# sourceMappingURL=param.d.ts.map

@@ -8,4 +8,5 @@ "use strict";

const utils_2 = require("../utils");
const separator_1 = require("./separator");
function formatParam(value, dependencies) {
if (value === null || value === void 0 ? void 0 : value._isBigNumber) {
if (value?._isBigNumber) {
return ethers_1.BigNumber.from(value).toString();

@@ -32,3 +33,3 @@ }

}
else if (value === null || value === void 0 ? void 0 : value._isIndexed) {
else if (value?._isIndexed) {
return `${(0, colors_1.colorIndexed)("[Indexed]")}${formatParam(value.hash, dependencies)}`;

@@ -40,3 +41,3 @@ }

.map((entry) => {
return `${entry[0]}:${formatParam(entry[1], dependencies)}`;
return `${entry[0]}${separator_1.SEPARATOR}${formatParam(entry[1], dependencies)}`;
})

@@ -43,0 +44,0 @@ .join(", ") +

@@ -1,10 +0,9 @@

import { Fragment, Result } from "ethers/lib/utils";
import { TracerDependenciesExtended } from "../types";
import { ParamType, Result } from "ethers/lib/utils";
import { TracerDependencies } from "../types";
interface FormatOptions {
decimals?: number;
isInput?: boolean;
shorten?: boolean;
}
export declare function formatResult(result: Result, fragment: Fragment, { decimals, isInput, shorten }: FormatOptions, dependencies: TracerDependenciesExtended): string;
export declare function formatResult(result: Result, params: ParamType[] | undefined, { decimals, shorten }: FormatOptions, dependencies: TracerDependencies): string;
export {};
//# sourceMappingURL=result.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.formatResult = void 0;
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
const colors_1 = require("../colors");
const param_1 = require("./param");
function formatResult(result, fragment, { decimals, isInput, shorten }, dependencies) {
var _a;
decimals = decimals !== null && decimals !== void 0 ? decimals : -1;
isInput = isInput !== null && isInput !== void 0 ? isInput : true;
shorten = shorten !== null && shorten !== void 0 ? shorten : false;
function formatResult(result, params, { decimals, shorten }, dependencies) {
decimals = decimals ?? -1;
shorten = shorten ?? false;
const stringifiedArgs = [];
const params = isInput
? fragment.inputs
: fragment === null || fragment === void 0 ? void 0 : fragment.outputs;
// const params = isInput
// ? fragment.inputs
// : (fragment as FunctionFragment)?.outputs;
if (!params) {

@@ -21,12 +20,29 @@ return "";

const param = params[i];
const name = (_a = param.name) !== null && _a !== void 0 ? _a : `arg_${i}`;
const name = param.name ?? `arg_${i}`;
let value;
if (decimals !== -1 && ethers_1.BigNumber.isBigNumber(result[i])) {
value = (0, utils_1.formatUnits)(result[i], decimals);
}
else if (Array.isArray(param.components)) {
value =
"[" +
formatResult(result[i], param.components, { decimals, shorten }, dependencies) +
"]";
}
else {
value = (0, param_1.formatParam)(result[i], dependencies);
}
stringifiedArgs.push([
name,
decimals !== -1 && i === 2 // display formatted value for erc20 transfer events
? (0, utils_1.formatUnits)(result[2], decimals)
: (0, param_1.formatParam)(result[i], dependencies),
value,
// use decimals if available to format amount
// decimals !== -1 && BigNumber.isBigNumber(result[i])
// ? formatUnits(result[i], decimals)
// : formatParam(result[i], param.components, dependencies),
]);
}
return `${stringifiedArgs
.map((entry) => `${stringifiedArgs.length > 1 || !shorten ? (0, colors_1.colorKey)(`${entry[0]}=`) : ""}${entry[1]}`)
.map((entry) => `${stringifiedArgs.length > 1 || !shorten
? (0, colors_1.colorKey)(`${entry[0]}: `)
: ""}${entry[1]}`)
.join(", ")}`;

@@ -33,0 +49,0 @@ }

@@ -11,3 +11,3 @@ "use strict";

const stack = (0, utils_1.shallowCopyStack)(structLog.stack);
if (stack.length < 2) {
if (stack.length <= 2) {
console.log("Faulty SSTORE");

@@ -14,0 +14,0 @@ return;

@@ -27,4 +27,2 @@ "use strict";

const sha3_1 = require("../opcodes/sha3");
const mload_1 = require("../opcodes/mload");
const mstore_1 = require("../opcodes/mstore");
/**

@@ -79,3 +77,3 @@ * Prints the given structLog to the console.

case "SLOAD":
if (dependencies.tracerEnv.sloads) {
if (dependencies.tracerEnv.opcodes.get("SLOAD")) {
await (0, sload_1.printSload)(structLog, index, structLogs, dependencies);

@@ -85,3 +83,3 @@ }

case "SSTORE":
if (dependencies.tracerEnv.sstores) {
if (dependencies.tracerEnv.opcodes.get("SSTORE")) {
await (0, sstore_1.printSstore)(structLog, dependencies);

@@ -101,3 +99,3 @@ }

default:
if (dependencies.tracerEnv.opcodes.includes(structLog.op)) {
if (dependencies.tracerEnv.opcodes.get(structLog.op)) {
switch (structLog.op) {

@@ -128,8 +126,2 @@ case "ADD":

break;
case "MLOAD":
await (0, mload_1.printMload)(structLog, index, structLogs, dependencies);
break;
case "MSTORE":
await (0, mstore_1.printMstore)(structLog, dependencies);
break;
default:

@@ -136,0 +128,0 @@ console.log(constants_1.DEPTH_INDENTATION.repeat(structLog.depth) + structLog.op);

@@ -10,3 +10,2 @@ "use strict";

async function printTopLevelTx(txHash, addressStack, dependencies) {
var _a;
const tx = await dependencies.provider.send("eth_getTransactionByHash", [

@@ -26,3 +25,3 @@ txHash,

// contract deploy transaction
const str = await (0, contract_1.formatContract)(tx.input, (0, utils_2.parseUint)((_a = tx.value) !== null && _a !== void 0 ? _a : "0x"), null, (0, utils_1.getContractAddress)(tx), dependencies);
const str = await (0, contract_1.formatContract)(tx.input, (0, utils_2.parseUint)(tx.value ?? "0x"), null, (0, utils_1.getContractAddress)(tx), dependencies);
addressStack.push((0, utils_1.getContractAddress)(tx));

@@ -29,0 +28,0 @@ console.log((0, colors_1.colorLabel)("CREATE") + " " + str);

@@ -5,4 +5,16 @@ "use strict";

const config_1 = require("hardhat/config");
const print_1 = require("../print");
const get_vm_1 = require("../get-vm");
const utils_1 = require("../utils");
const ethereumjs_vm_1 = require("@nomicfoundation/ethereumjs-vm");
const recorder_1 = require("../trace/recorder");
const originalCreate = ethereumjs_vm_1.VM.create;
ethereumjs_vm_1.VM.create = async function (...args) {
const vm = await originalCreate.bind(ethereumjs_vm_1.VM)(...args);
// @ts-ignore
const tracerEnv = global.tracerEnv;
const recorder = new recorder_1.TraceRecorder(vm, tracerEnv);
// @ts-ignore
global._hardhat_tracer_recorder = recorder;
return vm;
};
(0, utils_1.addCliParams)((0, config_1.task)("trace", "Traces a transaction hash"))

@@ -12,3 +24,2 @@ .addParam("hash", "transaction hash to view trace of")

.setAction(async (args, hre, runSuper) => {
var _a;
(0, utils_1.applyCliArgsToTracer)(args, hre);

@@ -20,9 +31,26 @@ const tx = await hre.network.provider.send("eth_getTransactionByHash", [

if (tx == null) {
if (!args.rpc) {
const mainnetForkUrl = (_a = hre.network.config.forking) === null || _a === void 0 ? void 0 : _a.url;
if (!mainnetForkUrl) {
throw new Error("Transaction not found on current network, please pass an archive node with --rpc option");
// try using url specified in network as rpc url
if (args.network) {
const userNetworks = hre.userConfig.networks;
if (userNetworks === undefined) {
throw new Error("No networks found in hardhat config");
}
if (userNetworks[args.network] === undefined) {
throw new Error(`Network ${args.network} not found in hardhat config`);
}
const url = userNetworks[args.network].url;
if (url === undefined) {
throw new Error(`Url not found in hardhat-config->networks->${args.network}`);
}
args.rpc = url;
}
// try using current mainnet fork url as rpc url
const mainnetForkUrl = hre.network.config.forking?.url;
if (mainnetForkUrl) {
args.rpc = mainnetForkUrl;
}
if (!args.rpc) {
// TODO add auto-detect network
throw new Error("rpc url not provided, please either use --network <network-name> or --rpc <rpc-url>");
}
const provider = new ethers_1.ethers.providers.StaticJsonRpcProvider(args.rpc);

@@ -36,46 +64,37 @@ const txFromRpc = await provider.getTransaction(args.hash);

}
try {
console.warn("Trying with rpc");
await (0, print_1.printDebugTrace)(args.hash, {
provider,
tracerEnv: hre.tracer,
artifacts: hre.artifacts,
nameTags: hre.tracer.nameTags,
});
// if printing was successful, then stop here
return;
// TODO add support for decoding using debug_tt on the RPC if present, otherwise use hardhat mainnet fork
if (false) {
// decode response of debug_traceTransaction
// print
return; // should halt execution here
}
catch (error) {
console.warn("Using debug_tt on rpc failed, activating mainnet fork at block", txFromRpc.blockNumber);
await hre.network.provider.send("hardhat_reset", [
{
forking: {
jsonRpcUrl: args.rpc,
blockNumber: txFromRpc.blockNumber,
},
console.warn("Activating mainnet fork at block", txFromRpc.blockNumber);
await hre.network.provider.send("hardhat_reset", [
{
forking: {
jsonRpcUrl: args.rpc,
blockNumber: txFromRpc.blockNumber,
},
]);
}
},
]);
// after the above hardhat reset, tx should be present on the local node
}
// using hardhat for getting the trace. if tx was previously not found on hardhat local,
// but now it will be available, due to mainnet fork activation
console.warn("Trying with hardhat mainnet fork");
const tracePromise = (0, print_1.printDebugTraceOrLogs)(args.hash, {
provider: hre.network.provider,
const node = await (0, get_vm_1.getNode)(hre);
// we cant use this resp because stack and memory is not there (takes up lot of memory if enabled)
await node.traceTransaction(Buffer.from(args.hash.slice(2), "hex"), {
disableStorage: true,
disableMemory: true,
disableStack: true,
});
// TODO try to do this properly
// @ts-ignore
const recorder = global?._hardhat_tracer_recorder;
await recorder.previousTraces[recorder.previousTraces.length - 1].print({
artifacts: hre.artifacts,
tracerEnv: hre.tracer,
artifacts: hre.artifacts,
nameTags: hre.tracer.nameTags,
provider: hre.ethers.provider,
});
const delayPromise = new Promise((resolve) => {
setTimeout(() => resolve("delay"), 20000);
});
const resolved = await Promise.race([delayPromise, tracePromise]);
if (resolved === "delay") {
console.log("Seems that it is taking time to fetch the state involved. Please note that this process may take several minutes. A lot of eth_getStorageAt requests are currently being made to the rpc.");
}
const traceResult = await tracePromise;
if (!traceResult) {
throw new Error("Transaction could not be traced");
}
await new Promise((resolve) => setTimeout(resolve, 1000));
return;
});
//# sourceMappingURL=trace.js.map
import { Artifacts } from "hardhat/types";
import { TraceRecorder } from "./trace/recorder";
import { Decoder } from "./decoder";
import { BigNumberish } from "ethers";
export interface NameTags {

@@ -7,18 +10,14 @@ [address: string]: string;

enabled?: boolean;
logs?: boolean;
calls?: boolean;
sstores?: boolean;
sloads?: boolean;
defaultVerbosity?: number;
gasCost?: boolean;
opcodes?: string[];
nameTags?: NameTags;
stateOverrides?: StateOverrides;
}
export interface TracerEnv {
enabled: boolean;
logs: boolean;
calls: boolean;
sstores: boolean;
sloads: boolean;
ignoreNext: boolean;
verbosity: number;
gasCost: boolean;
opcodes: string[];
opcodes: Map<string, boolean>;
nameTags: NameTags;

@@ -28,2 +27,5 @@ _internal: {

};
recorder?: TraceRecorder;
decoder?: Decoder;
stateOverrides?: StateOverrides;
}

@@ -52,2 +54,12 @@ export interface TracerDependencies {

}
export interface StateOverrides {
[address: string]: {
storage?: {
[slot: string | number]: BigNumberish;
};
bytecode?: string;
balance?: BigNumberish;
nonce?: BigNumberish;
};
}
//# sourceMappingURL=types.d.ts.map
import { BigNumber } from "ethers";
import { VM } from "@nomicfoundation/ethereumjs-vm";
import { ConfigurableTaskDefinition, HardhatRuntimeEnvironment } from "hardhat/types";
import { StructLog, TracerDependenciesExtended, TracerEnv, TracerEnvUser } from "./types";
export declare function getTracerEnvFromUserInput(userInput?: TracerEnvUser): TracerEnv;
import { ProviderLike, StateOverrides, StructLog, TracerDependencies, TracerEnv } from "./types";
import { Item } from "./trace/transaction";
export declare function addCliParams(task: ConfigurableTaskDefinition): ConfigurableTaskDefinition;
export declare const DEFAULT_VERBOSITY = 3;
export declare function applyCliArgsToTracer(args: any, hre: HardhatRuntimeEnvironment): void;
export declare function isOnlyLogs(env: TracerEnv): boolean;
export declare function getFromNameTags(address: string, dependencies: TracerDependenciesExtended): string | undefined;
export declare function getFromNameTags(address: string, dependencies: TracerDependencies): string | undefined;
export declare function findNextStructLogInDepth(structLogs: StructLog[], depth: number, startIndex: number): [StructLog, StructLog];

@@ -16,4 +18,16 @@ export declare function parseHex(str: string): string;

export declare function shallowCopyStack(stack: string[]): string[];
export declare function shallowCopyStack2(stack: bigint[]): string[];
export declare function compareBytecode(artifactBytecode: string, contractBytecode: string): number;
export declare function removeColor(str: string): string;
/**
* Ensures 0x prefix to a hex string which may or may not
* @param str A hex string that may or may not have 0x prepended
*/
export declare function hexPrefix(str: string): string;
export declare function checkIfOpcodesAreValid(opcodes: Map<string, boolean>, vm: VM): void;
export declare function isItem(item: any): item is Item<any>;
export declare function applyStateOverrides(stateOverrides: StateOverrides, vm: VM): 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>;
//# sourceMappingURL=utils.d.ts.map
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.removeColor = exports.compareBytecode = exports.shallowCopyStack = exports.parseMemory = exports.parseAddress = exports.parseUint = exports.parseNumber = exports.parseHex = exports.findNextStructLogInDepth = exports.getFromNameTags = exports.isOnlyLogs = exports.applyCliArgsToTracer = exports.addCliParams = exports.getTracerEnvFromUserInput = void 0;
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");
const ethers_1 = require("ethers");
const utils_1 = require("ethers/lib/utils");
function getTracerEnvFromUserInput(userInput) {
var _a, _b, _c, _d, _e, _f, _g, _h;
return {
enabled: (_a = userInput === null || userInput === void 0 ? void 0 : userInput.enabled) !== null && _a !== void 0 ? _a : false,
logs: (_b = userInput === null || userInput === void 0 ? void 0 : userInput.logs) !== null && _b !== void 0 ? _b : false,
calls: (_c = userInput === null || userInput === void 0 ? void 0 : userInput.calls) !== null && _c !== void 0 ? _c : false,
sstores: (_d = userInput === null || userInput === void 0 ? void 0 : userInput.sstores) !== null && _d !== void 0 ? _d : false,
sloads: (_e = userInput === null || userInput === void 0 ? void 0 : userInput.sloads) !== null && _e !== void 0 ? _e : false,
gasCost: (_f = userInput === null || userInput === void 0 ? void 0 : userInput.gasCost) !== null && _f !== void 0 ? _f : false,
opcodes: (_g = userInput === null || userInput === void 0 ? void 0 : userInput.opcodes) !== null && _g !== void 0 ? _g : [],
nameTags: (_h = userInput === null || userInput === void 0 ? void 0 : userInput.nameTags) !== null && _h !== void 0 ? _h : {},
_internal: {
printNameTagTip: undefined,
},
};
}
exports.getTracerEnvFromUserInput = getTracerEnvFromUserInput;
const ethereumjs_util_1 = require("@nomicfoundation/ethereumjs-util");
const opcodes_1 = require("@nomicfoundation/ethereumjs-evm/dist/opcodes");
function addCliParams(task) {
return (task
// verbosity flags
.addFlag("v", "set verbosity to 1, prints calls for only failed txs")
.addFlag("vv", "set verbosity to 2, prints calls and storage for only failed txs")
.addFlag("vvv", "set verbosity to 3, prints calls for all txs")
.addFlag("vvvv", "set verbosity to 4, prints calls and storage for all txs")
.addFlag("gascost", "display gas cost")
.addFlag("disabletracer", "do not enable tracer at the start (for inline enabling tracer)")
// params
.addOptionalParam("opcodes", "specify more opcodes to print")
// feature flags
.addFlag("logs", "print logs emitted during transactions")
.addFlag("calls", "print calls during transactions")
.addFlag("sloads", "print SLOADs during calls")
.addFlag("sstores", "print SSTOREs during transactions")
.addFlag("gascost", "display gas cost")
.addFlag("disabletracer", "do not enable tracer at the start (for inline enabling tracer)")
// feature group flags
.addFlag("trace", "trace logs and calls in transactions")
.addFlag("fulltrace", "trace logs, calls and storage writes in transactions")
// aliases
.addFlag("tracefull", "alias for fulltrace")
.addFlag("gas", "alias for gascost"));
// alias
.addFlag("trace", "enable tracer with verbosity 3")
.addFlag("fulltrace", "enable tracer with verbosity 4"));
}
exports.addCliParams = addCliParams;
exports.DEFAULT_VERBOSITY = 3;
function applyCliArgsToTracer(args, hre) {
// populating aliases
const fulltrace = args.fulltrace || args.tracefull;
const gascost = args.gascost || args.gas;
// if any flag is present, then enable tracer
if (args.logs || args.trace || fulltrace || args.disabletracer === true) {
hre.tracer.enabled = true;
// enabled by default
hre.tracer.enabled = true;
// for not enabling tracer from the start
if (args.disabletracer) {
hre.tracer.enabled = false;
}
// enabling config by flags passed
if (args.logs) {
hre.tracer.logs = true;
// always active opcodes
const opcodesToActivate = ["RETURN", "REVERT"];
const logOpcodes = ["LOG0", "LOG1", "LOG2", "LOG3", "LOG4"];
const storageOpcodes = ["SLOAD", "SSTORE"];
// setting verbosity
if (args.vvvv || args.fulltrace) {
hre.tracer.verbosity = 4;
opcodesToActivate.push(...logOpcodes, ...storageOpcodes);
}
if (args.calls) {
hre.tracer.calls = true;
else if (args.vvv || args.trace) {
hre.tracer.verbosity = 3;
opcodesToActivate.push(...logOpcodes);
}
if (args.sloads) {
hre.tracer.sloads = true;
else if (args.vv) {
hre.tracer.verbosity = 2;
opcodesToActivate.push(...logOpcodes, ...storageOpcodes);
}
if (args.sstores) {
hre.tracer.sstores = true;
else if (args.v) {
opcodesToActivate.push(...logOpcodes);
hre.tracer.verbosity = 1;
}
for (const opcode of opcodesToActivate) {
hre.tracer.opcodes.set(opcode, true);
}
if (args.opcodes) {
hre.tracer.opcodes = [...args.opcodes.split(",")];
// hre.tracer.opcodes = [hre.tracer.opcodes, ...args.opcodes.split(",")];
for (const opcode of args.opcodes.split(",")) {
hre.tracer.opcodes.set(opcode, true);
}
if (hre.tracer.recorder === undefined) {
throw new Error(`hardhat-tracer/utils/applyCliArgsToTracer: hre.tracer.recorder is undefined`);
}
checkIfOpcodesAreValid(hre.tracer.opcodes, hre.tracer.recorder.vm);
}
// enabling config by mode of operation
if (args.trace) {
hre.tracer.logs = true;
hre.tracer.calls = true;
}
if (fulltrace) {
hre.tracer.logs = true;
hre.tracer.calls = true;
hre.tracer.sloads = true;
hre.tracer.sstores = true;
}
if (gascost) {
if (args.gascost) {
hre.tracer.gasCost = true;

@@ -82,11 +71,12 @@ }

exports.applyCliArgsToTracer = applyCliArgsToTracer;
// TODO remove
function isOnlyLogs(env) {
return env.logs && !env.calls && !env.sstores && !env.sloads && !env.gasCost;
return env.verbosity === 1;
}
exports.isOnlyLogs = isOnlyLogs;
function getFromNameTags(address, dependencies) {
return (dependencies.nameTags[address] ||
dependencies.nameTags[address.toLowerCase()] ||
dependencies.nameTags[address.toUpperCase()] ||
dependencies.nameTags[ethers_1.ethers.utils.getAddress(address)]);
return (dependencies.tracerEnv.nameTags[address] ||
dependencies.tracerEnv.nameTags[address.toLowerCase()] ||
dependencies.tracerEnv.nameTags[address.toUpperCase()] ||
dependencies.tracerEnv.nameTags[ethers_1.ethers.utils.getAddress(address)]);
}

@@ -137,2 +127,6 @@ exports.getFromNameTags = getFromNameTags;

exports.shallowCopyStack = shallowCopyStack;
function shallowCopyStack2(stack) {
return [...stack].map((x) => ethers_1.BigNumber.from(x).toHexString());
}
exports.shallowCopyStack2 = shallowCopyStack2;
function compareBytecode(artifactBytecode, contractBytecode) {

@@ -159,2 +153,116 @@ if (artifactBytecode.length <= 2 || contractBytecode.length <= 2)

exports.removeColor = removeColor;
/**
* Ensures 0x prefix to a hex string which may or may not
* @param str A hex string that may or may not have 0x prepended
*/
function hexPrefix(str) {
if (!str.startsWith("0x"))
str = "0x" + str;
return str;
}
exports.hexPrefix = hexPrefix;
function checkIfOpcodesAreValid(opcodes, vm) {
// fetch the opcodes which work on this VM
let activeOpcodesMap = new Map();
for (const opcode of (0, opcodes_1.getOpcodesForHF)(vm._common).opcodes.values()) {
activeOpcodesMap.set(opcode.fullName, true);
}
// check if there are any opcodes specified in tracer which do not work
for (const opcode of opcodes.keys()) {
if (!activeOpcodesMap.get(opcode)) {
throw new Error(`The opcode "${opcode}" is not active on this VM. If the opcode name is misspelled in the config, please correct it.`);
}
}
}
exports.checkIfOpcodesAreValid = checkIfOpcodesAreValid;
function isItem(item) {
return item && typeof item.opcode === "string";
}
exports.isItem = isItem;
async function applyStateOverrides(stateOverrides, vm) {
for (const [_address, overrides] of Object.entries(stateOverrides)) {
const address = ethereumjs_util_1.Address.fromString(_address);
// for balance and nonce
if (overrides.balance !== undefined || overrides.nonce !== undefined) {
const account = await vm.stateManager.getAccount(address);
if (overrides.nonce !== undefined) {
account.nonce = ethers_1.BigNumber.from(overrides.nonce).toBigInt();
}
if (overrides.balance) {
account.balance = ethers_1.BigNumber.from(overrides.balance).toBigInt();
}
await vm.stateManager.putAccount(address, account);
}
// for bytecode
if (overrides.bytecode) {
await vm.stateManager.putContractCode(address, Buffer.from(overrides.bytecode, "hex"));
}
// for storage slots
if (overrides.storage) {
for (const [key, value] of Object.entries(overrides.storage)) {
await vm.stateManager.putContractStorage(address, Buffer.from((0, utils_1.hexZeroPad)(ethers_1.BigNumber.from(key).toHexString(), 32).slice(2), "hex"), Buffer.from((0, utils_1.hexZeroPad)(ethers_1.BigNumber.from(value).toHexString(), 32).slice(2), "hex"));
}
}
}
}
exports.applyStateOverrides = applyStateOverrides;
async function fetchContractName(to, provider) {
let name = await fetchContractNameFromMethodName(to, "symbol", provider);
if (!name) {
name = await fetchContractNameFromMethodName(to, "name", provider);
}
if (name) {
name = name.split(" ").join("");
}
return name;
}
exports.fetchContractName = fetchContractName;
async function fetchContractNameFromMethodName(to, methodName, provider) {
const iface1 = new utils_1.Interface([
`function ${methodName}() public view returns (string)`,
]);
let result1;
try {
result1 = await provider.send("eth_call", [
{
to,
data: iface1.encodeFunctionData(methodName, []),
},
]);
const d = iface1.decodeFunctionResult(methodName, result1);
return d[0];
}
catch {
try {
const iface2 = new utils_1.Interface([
`function ${methodName}() public view returns (bytes32)`,
]);
const d = iface2.decodeFunctionResult(methodName, result1);
const bytes32 = d[0];
return ethers_1.ethers.utils.toUtf8String(bytes32);
}
catch { }
}
return undefined;
}
exports.fetchContractNameFromMethodName = fetchContractNameFromMethodName;
async function fetchContractDecimals(to, provider) {
const iface1 = new utils_1.Interface([
`function decimals() public view returns (uint8)`,
]);
let result1;
try {
result1 = await provider.send("eth_call", [
{
to,
data: iface1.encodeFunctionData("decimals", []),
},
]);
const d = iface1.decodeFunctionResult("decimals", result1);
return d[0];
}
catch { }
return undefined;
}
exports.fetchContractDecimals = fetchContractDecimals;
//# sourceMappingURL=utils.js.map

@@ -7,3 +7,3 @@ "use strict";

const wrapper_1 = require("hardhat/internal/core/providers/wrapper");
const print_1 = require("./print");
const decoder_1 = require("./decoder");
const utils_1 = require("./utils");

@@ -20,5 +20,5 @@ /**

async request(args) {
var _a;
let result;
let error;
// console.log("wrapper->args.method", args.method);
try {

@@ -30,26 +30,35 @@ result = await this.dependencies.provider.send(args.method, args.params);

}
if (this.dependencies.tracerEnv.enabled &&
(result != null || error != null) &&
(args.method === "eth_sendTransaction" ||
args.method === "eth_sendRawTransaction" ||
args.method === "eth_getTransactionReceipt")) {
let hash = result !== null && result !== void 0 ? result : error === null || error === void 0 ? void 0 : error.transactionHash;
let receipt;
if (typeof result === "object" && result !== null) {
hash = (_a = result.transactionHash) !== null && _a !== void 0 ? _a : result.hash;
receipt = result;
// TODO take decision whether to print or not
// if estimateGas fails then print it
// sendTx should be printing it regardless of success or failure
const isSendTransaction = args.method === "eth_sendTransaction";
const isEthCall = args.method === "eth_call";
const isEstimateGas = args.method === "eth_estimateGas";
const isSendTransactionFailed = isSendTransaction && !!error;
const isEthCallFailed = isEthCall && !!error;
const isEstimateGasFailed = isEstimateGas && !!error;
let shouldPrint;
switch (this.dependencies.tracerEnv.verbosity) {
case 0:
shouldPrint = false;
break;
case 1:
case 2:
shouldPrint =
isSendTransactionFailed || isEthCallFailed || isEstimateGasFailed;
break;
case 3:
case 4:
shouldPrint = true;
break;
default:
throw new Error("Invalid verbosity value: " + this.dependencies.tracerEnv.verbosity);
}
if (this.dependencies.tracerEnv.ignoreNext) {
this.dependencies.tracerEnv.ignoreNext = false;
}
else {
if (this.dependencies.tracerEnv.enabled && shouldPrint) {
await this.dependencies.tracerEnv.recorder?.previousTraces?.[this.dependencies.tracerEnv.recorder?.previousTraces.length - 1]?.print?.(this.dependencies);
}
else {
receipt = await this.dependencies.provider.send("eth_getTransactionReceipt", [hash]);
}
if (!this.txPrinted[hash] && receipt !== null) {
this.txPrinted[hash] = true;
const dependenciesExtended = Object.assign(Object.assign({}, this.dependencies), { nameTags: Object.assign({}, this.dependencies.tracerEnv.nameTags) });
try {
await (0, print_1.printDebugTraceOrLogs)(hash, dependenciesExtended);
}
catch (error) {
console.log("error in request wrapper:", error);
}
}
}

@@ -67,3 +76,2 @@ if (error) {

function wrapHardhatProvider(hre) {
var _a;
const tracerProvider = new TracerWrapper({

@@ -77,3 +85,3 @@ artifacts: hre.artifacts,

// ensure env is present
hre.tracer = (_a = hre.tracer) !== null && _a !== void 0 ? _a : (0, utils_1.getTracerEnvFromUserInput)(hre.tracer);
// hre.tracer = hre.tracer ?? getTracerEnvFromUserInput(hre.tracer);
}

@@ -90,3 +98,18 @@ exports.wrapHardhatProvider = wrapHardhatProvider;

// ensure env is present
tracerEnv = (0, utils_1.getTracerEnvFromUserInput)(tracerEnv);
// const tracerEnv = getTracerEnvFromUserInput(tracerEnvUser);
if (!tracerEnv) {
tracerEnv = {
enabled: false,
ignoreNext: false,
verbosity: utils_1.DEFAULT_VERBOSITY,
gasCost: false,
opcodes: new Map(),
nameTags: {},
// @ts-ignore TODO remove, this has no place in "config"
_internal: {
printNameTagTip: undefined,
},
decoder: new decoder_1.Decoder(artifacts),
};
}
const tracerProvider = new TracerWrapper({ provider, artifacts, tracerEnv });

@@ -93,0 +116,0 @@ const compatibleProvider = new backwards_compatibility_1.BackwardsCompatibilityProviderAdapter(tracerProvider);

{
"name": "hardhat-tracer",
"version": "1.1.1",
"version": "2.0.0-beta.1",
"description": "Hardhat Tracer plugin",

@@ -35,19 +35,13 @@ "repository": "github:zemse/hardhat-tracer",

"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
"@nomicfoundation/hardhat-network-helpers": "^1.0.4",
"@nomicfoundation/hardhat-toolbox": "^1.0.2",
"@nomiclabs/hardhat-ethers": "^2.1.1",
"@nomiclabs/hardhat-etherscan": "^3.1.0",
"@typechain/ethers-v5": "^10.1.0",
"@typechain/hardhat": "^6.1.2",
"@types/chai": "^4.3.3",
"@nomiclabs/hardhat-ethers": "^2.1.0",
"@types/chai": "^4.3.0",
"@types/debug": "^4.1.7",
"@types/fs-extra": "^5.0.4",
"@types/mocha": "^9.1.1",
"@types/node": "^18.7.11",
"@types/mocha": "^5.2.6",
"@types/node": "^8.10.38",
"@types/readable-stream": "^2.3.14",
"chai": "^4.2.0",
"hardhat": "^2.9.2",
"hardhat-gas-reporter": "^1.0.8",
"hardhat": "^2.11.2",
"mocha": "^7.1.2",
"prettier": "2.0.5",
"solidity-coverage": "^0.7.21",
"ts-node": "^10.7.0",

@@ -57,3 +51,2 @@ "tslint": "^5.16.0",

"tslint-plugin-prettier": "^2.0.1",
"typechain": "^8.1.0",
"typescript": "^4.6.3"

@@ -60,0 +53,0 @@ },

# hardhat-tracer 🕵️
> This is a beta release. Some things might not work.
Allows you to see events, calls and storage operations when running your tests.
- [Installation](#installation)
- [Usage](#usage)
- [Test](#test): See debug trace when running test
- [Trace](#trace): Task to generate call tree for a transaction
- [Calldata decoder](#calldata-decoder): Task to simply decode an encoded data
- [Address name tags](#address-name-tags): Set alias for addresses with name
- [hardhat-tracer 🕵️](#hardhat-tracer-️)
- [Installation](#installation)
- [Usage](#usage)
- [Test](#test)
- [Trace](#trace)
- [Calldata decoder](#calldata-decoder)
- [Address name tags](#address-name-tags)
- [State overrides](#state-overrides)

@@ -30,8 +34,14 @@ ## Installation

Just add the `--trace` or `--fulltrace` after your test command.
```shell
npx hardhat test --trace # same as --vvv
npx hardhat test --fulltrace # same as --vvvv
```shell
npx hardhat test --trace # shows logs + calls
npx hardhat test --fulltrace # shows logs + calls + sloads + sstores
npx hardhat test --trace --opcodes ADD,SUB # shows any opcode specified
npx hardhat test --v # shows logs + calls for only failed txs
npx hardhat test --vv # shows logs + calls + storage for only failed txs
npx hardhat test --vvv # shows logs + calls for all txs
npx hardhat test --vvvv # shows logs + calls + storage for all txs
# specify opcode
npx hardhat test --v --opcodes ADD,SUB # shows any opcode specified for only failed txs
npx hardhat test --vvv --opcodes ADD,SUB # shows any opcode specified for all txs
```

@@ -60,3 +70,3 @@

If you are just looking for a quick decode of calldata or [Solidity's Custom Error](https://blog.soliditylang.org/2021/04/21/custom-errors/):
If you are just looking for a quick decode of calldata or [Solidity"s Custom Error](https://blog.soliditylang.org/2021/04/21/custom-errors/):

@@ -80,7 +90,27 @@ ```

tracer: {
nameTags: {
'0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266': 'Hunter',
[someVariable]: 'MyContract',
nameTags: {
"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266": "Hunter",
[someVariable]: "MyContract",
},
},
```
### State overrides
These state overrides are applied when the EthereumJS/VM is created inside hardhat.
```ts
tracer: {
stateOverrides: {
"0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2": {
storage: {
"0": 100,
"0x1abf42a573070203916aa7bf9118741d8da5f9522f66b5368aa0a2644f487b38": 0,
},
bytecode: "0x30FF",
balance: parseEther("2"),
nonce: 2
},
},
},
```

@@ -9,2 +9,3 @@ import chalk from "chalk";

export const colorError = chalk.red;
export const colorConsole = chalk.blue;
export const colorKey = chalk.magenta;

@@ -11,0 +12,0 @@ export const colorSload = chalk.blueBright;

@@ -1,6 +0,11 @@

import { extendConfig } from "hardhat/config";
import {
extendConfig,
extendEnvironment,
experimentalAddHardhatNetworkMessageTraceHook,
} from "hardhat/config";
import { MessageTrace } from "hardhat/internal/hardhat-network/stack-traces/message-trace";
import { HardhatConfig, HardhatUserConfig } from "hardhat/types";
import { TracerEnv, TracerEnvUser } from "../types";
import { getTracerEnvFromUserInput } from "../utils";
import { DEFAULT_VERBOSITY } from "../utils";

@@ -19,4 +24,34 @@ declare module "hardhat/types/config" {

(config: HardhatConfig, userConfig: Readonly<HardhatUserConfig>) => {
config.tracer = getTracerEnvFromUserInput(userConfig.tracer);
// config.tracer = getTracerEnvFromUserInput(userConfig.tracer);
const opcodes = new Map<string, boolean>();
// always active opcodes
const opcodesToActivate = [];
if (userConfig.tracer?.opcodes) {
if (!Array.isArray(userConfig.tracer.opcodes)) {
throw new Error(
"tracer.opcodes in hardhat user config should be array"
);
}
opcodesToActivate.push(...userConfig.tracer.opcodes);
}
for (const opcode of opcodesToActivate) {
opcodes.set(opcode, true);
}
config.tracer = {
enabled: userConfig.tracer?.enabled ?? false,
ignoreNext: false,
verbosity: userConfig.tracer?.defaultVerbosity ?? DEFAULT_VERBOSITY,
gasCost: userConfig.tracer?.gasCost ?? false,
opcodes,
nameTags: userConfig.tracer?.nameTags ?? {},
// @ts-ignore TODO remove, this has no place in "config"
_internal: {
printNameTagTip: undefined,
},
stateOverrides: userConfig.tracer?.stateOverrides,
};
}
);
import { extendEnvironment } from "hardhat/config";
import "hardhat/types/config";
import "hardhat/types/runtime";
import { getVM } from "../get-vm";
import { TracerEnv } from "../types";
import { TraceRecorder } from "../trace/recorder";
import { Decoder } from "../decoder";

@@ -15,3 +17,18 @@ declare module "hardhat/types/runtime" {

// copy reference of config.tracer to tracer
// TODO take this properly, env can contain things that config does not need to.
hre.tracer = hre.config.tracer;
hre.tracer.decoder = new Decoder(hre.artifacts);
// @ts-ignore
global.tracerEnv = hre.tracer;
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);
});
});
import { BigNumber, BigNumberish, ethers } from "ethers";
import { FunctionFragment, Interface, Result } from "ethers/lib/utils";
import {
Fragment,
FunctionFragment,
Interface,
Result,
} from "ethers/lib/utils";
import { Artifact } from "hardhat/types";
import { colorContract, colorFunction, colorKey } from "../colors";
import { TracerDependenciesExtended } from "../types";
import { compareBytecode, getFromNameTags } from "../utils";
import { ProviderLike, TracerDependencies } from "../types";
import {
compareBytecode,
fetchContractDecimals,
fetchContractName,
getFromNameTags,
} from "../utils";
import { formatParam } from "./param";
import { formatResult } from "./result";
import { SEPARATOR } from "./separator";

@@ -18,55 +29,79 @@ export async function formatCall(

gas: BigNumberish,
dependencies: TracerDependenciesExtended
dependencies: TracerDependencies
) {
const toBytecode = await dependencies.provider.send("eth_getCode", [to]);
const names = await dependencies.artifacts.getAllFullyQualifiedNames();
// TODO handle if `to` is console.log address
let contractName: string | undefined;
let result: Result | undefined;
let result2: Result | undefined;
let functionFragment: FunctionFragment | undefined;
for (const name of names) {
const _artifact = await dependencies.artifacts.readArtifact(name);
const iface = new Interface(_artifact.abi);
let contractDecimals: number | undefined;
let inputResult: Result | undefined;
let returnResult: Result | undefined;
let fragment: Fragment | undefined;
// 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
contractName = _artifact.contractName;
}
try {
({
fragment,
contractName,
inputResult,
returnResult,
} = await dependencies.tracerEnv.decoder!.decode(input, ret));
// try to parse the arguments
try {
// if this doesnt throw, we likely found an Artifact that recognizes the input
const signature = input.slice(0, 10);
result = iface.decodeFunctionData(signature, input);
try {
result2 = iface.decodeFunctionResult(signature, ret);
} catch {}
// use just contract name
contractName = contractName.split(":")[1];
} catch {}
functionFragment = iface.getFunction(signature);
} catch {}
// 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);
// if we got both the contract name and arguments parsed so far, we can stop
if (contractName && result) {
break;
// 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;
}
}
}
if (result && functionFragment) {
if (
input.slice(0, 10) === "0x70a08231" || // balanceOf
input.slice(0, 10) === "0xa9059cbb" || // transfer
input.slice(0, 10) === "0x23b872dd" // transferFrom
) {
contractDecimals = await fetchContractDecimals(to, dependencies.provider);
}
if (inputResult && fragment) {
const inputArgs = formatResult(
result,
functionFragment,
{ decimals: -1, isInput: true, shorten: false },
inputResult,
fragment.inputs,
{ decimals: contractDecimals, shorten: false },
dependencies
);
const outputArgs = result2
const outputArgs = returnResult
? formatResult(
result2,
functionFragment,
{ decimals: -1, isInput: false, shorten: true },
returnResult,
(fragment as FunctionFragment).outputs,
{ decimals: contractDecimals, shorten: true },
dependencies

@@ -78,6 +113,6 @@ )

if ((value = BigNumber.from(value)).gt(0)) {
extra.push(`value: ${formatParam(value, dependencies)}`);
extra.push(`value${SEPARATOR}${formatParam(value, dependencies)}`);
}
if ((gas = BigNumber.from(gas)).gt(0) && dependencies.tracerEnv.gasCost) {
extra.push(`gas: ${formatParam(gas, dependencies)}`);
extra.push(`gas${SEPARATOR}${formatParam(gas, dependencies)}`);
}

@@ -94,3 +129,3 @@ const nameTag = getFromNameTags(to, dependencies);

)}>`
}.${colorFunction(functionFragment.name)}${
}.${colorFunction(fragment.name)}${
extra.length !== 0 ? `{${extra.join(",")}}` : ""

@@ -101,11 +136,15 @@ }(${inputArgs})${outputArgs ? ` => (${outputArgs})` : ""}`;

// TODO add flag to hide unrecognized stuff
if (toBytecode.length > 2 && contractName) {
if (contractName) {
return `${colorContract(contractName)}.<${colorFunction(
"UnknownFunction"
)}>(${colorKey("input=")}${input}, ${colorKey("ret=")}${ret})`;
)}>(${colorKey("input" + SEPARATOR)}${input}, ${colorKey(
"ret" + SEPARATOR
)}${ret})`;
} else {
return `${colorFunction("UnknownContractAndFunction")}(${colorKey(
"to="
)}${to}, ${colorKey("input=")}${input}, ${colorKey("ret=")}${ret})`;
"to" + SEPARATOR
)}${to}, ${colorKey("input" + SEPARATOR)}${input}, ${colorKey(
"ret" + SEPARATOR
)}${ret || "0x"})`;
}
}

@@ -1,6 +0,6 @@

import { BigNumber } from "ethers";
import { BigNumber, BigNumberish } from "ethers";
import { arrayify, Interface } from "ethers/lib/utils";
import { colorContract, colorFunction, colorKey } from "../colors";
import { TracerDependenciesExtended } from "../types";
import { TracerDependencies } from "../types";
import { compareBytecode } from "../utils";

@@ -13,7 +13,9 @@

code: string,
value: BigNumber,
salt: BigNumber | null,
value: BigNumberish,
salt: BigNumberish | null,
deployedAddress: string | null,
dependencies: TracerDependenciesExtended
dependencies: TracerDependencies
) {
value = BigNumber.from(value);
if (salt !== null) salt = BigNumber.from(salt);
const names = await dependencies.artifacts.getAllFullyQualifiedNames();

@@ -38,4 +40,4 @@

constructorParamsDecoded,
iface.deploy,
{ decimals: -1, isInput: true, shorten: false },
iface.deploy.inputs,
{ decimals: -1, shorten: false },
dependencies

@@ -42,0 +44,0 @@ );

import { Interface } from "ethers/lib/utils";
import { colorError } from "../colors";
import { TracerDependenciesExtended } from "../types";
import { TracerDependencies } from "../types";
import { formatObject } from "./object";

@@ -12,3 +12,3 @@

revertData: string,
dependencies: TracerDependenciesExtended
dependencies: TracerDependencies
) {

@@ -65,4 +65,4 @@ const commonErrors = [

parsed.args,
parsed.functionFragment,
{ decimals: -1, isInput: true, shorten: false },
parsed.functionFragment.inputs,
{ decimals: -1, shorten: false },
dependencies

@@ -85,4 +85,4 @@ );

errorDesc.args,
errorDesc.errorFragment,
{ decimals: -1, isInput: true, shorten: false },
errorDesc.errorFragment.inputs,
{ decimals: -1, shorten: false },
dependencies

@@ -89,0 +89,0 @@ )})`;

@@ -5,3 +5,3 @@ import { Interface, LogDescription } from "ethers/lib/utils";

import { colorContract, colorEvent } from "../colors";
import { TracerDependenciesExtended } from "../types";
import { TracerDependencies, TracerDependenciesExtended } from "../types";
import { compareBytecode, getFromNameTags } from "../utils";

@@ -15,4 +15,5 @@

currentAddress: string | undefined,
dependencies: TracerDependenciesExtended
dependencies: TracerDependencies
) {
// TODO make the contractName code common between formatCall and formatLog
const nameTag = currentAddress

@@ -46,6 +47,10 @@ ? getFromNameTags(currentAddress, dependencies)

if (!contractName) {
contractName = artifact.contractName;
}
str = `${colorEvent(parsed.name)}(${formatResult(
parsed.args,
parsed.eventFragment,
{ decimals, isInput: true, shorten: false },
parsed.eventFragment.inputs,
{ decimals, shorten: false },
dependencies

@@ -52,0 +57,0 @@ )})`;

import { colorKey } from "../colors";
import { SEPARATOR } from "./separator";

@@ -14,5 +15,5 @@ export function formatObject(obj: any) {

}
return `${colorKey(key + "=")}${value}`;
return `${colorKey(key + SEPARATOR)}${value}`;
})
.join(", ");
}

@@ -5,8 +5,9 @@ import { BigNumber } from "ethers";

import { colorIndexed, colorNameTag } from "../colors";
import { TracerDependenciesExtended } from "../types";
import { TracerDependencies } from "../types";
import { getFromNameTags } from "../utils";
import { SEPARATOR } from "./separator";
export function formatParam(
value: any,
dependencies: TracerDependenciesExtended
dependencies: TracerDependencies
): string {

@@ -44,3 +45,6 @@ if (value?._isBigNumber) {

.map((entry) => {
return `${entry[0]}:${formatParam(entry[1], dependencies)}`;
return `${entry[0]}${SEPARATOR}${formatParam(
entry[1],
dependencies
)}`;
})

@@ -47,0 +51,0 @@ .join(", ") +

@@ -0,1 +1,2 @@

import { BigNumber } from "ethers";
import {

@@ -5,2 +6,3 @@ formatUnits,

FunctionFragment,
ParamType,
Result,

@@ -10,3 +12,3 @@ } from "ethers/lib/utils";

import { colorKey } from "../colors";
import { TracerDependenciesExtended } from "../types";
import { TracerDependencies } from "../types";

@@ -17,3 +19,2 @@ import { formatParam } from "./param";

decimals?: number;
isInput?: boolean;
shorten?: boolean;

@@ -24,13 +25,12 @@ }

result: Result,
fragment: Fragment,
{ decimals, isInput, shorten }: FormatOptions,
dependencies: TracerDependenciesExtended
params: ParamType[] | undefined,
{ decimals, shorten }: FormatOptions,
dependencies: TracerDependencies
) {
decimals = decimals ?? -1;
isInput = isInput ?? true;
shorten = shorten ?? false;
const stringifiedArgs: Array<[string, string]> = [];
const params = isInput
? fragment.inputs
: (fragment as FunctionFragment)?.outputs;
// const params = isInput
// ? fragment.inputs
// : (fragment as FunctionFragment)?.outputs;
if (!params) {

@@ -42,7 +42,26 @@ return "";

const name = param.name ?? `arg_${i}`;
let value;
if (decimals !== -1 && BigNumber.isBigNumber(result[i])) {
value = formatUnits(result[i], decimals);
} else if (Array.isArray(param.components)) {
value =
"[" +
formatResult(
result[i],
param.components,
{ decimals, shorten },
dependencies
) +
"]";
} else {
value = formatParam(result[i], dependencies);
}
stringifiedArgs.push([
name,
decimals !== -1 && i === 2 // display formatted value for erc20 transfer events
? formatUnits(result[2], decimals)
: formatParam(result[i], dependencies),
value,
// use decimals if available to format amount
// decimals !== -1 && BigNumber.isBigNumber(result[i])
// ? formatUnits(result[i], decimals)
// : formatParam(result[i], param.components, dependencies),
]);

@@ -54,3 +73,5 @@ }

`${
stringifiedArgs.length > 1 || !shorten ? colorKey(`${entry[0]}=`) : ""
stringifiedArgs.length > 1 || !shorten
? colorKey(`${entry[0]}: `)
: ""
}${entry[1]}`

@@ -57,0 +78,0 @@ )

@@ -13,3 +13,3 @@ import { colorLabel, colorSstore } from "../colors";

const stack = shallowCopyStack(structLog.stack);
if (stack.length < 2) {
if (stack.length <= 2) {
console.log("Faulty SSTORE");

@@ -16,0 +16,0 @@ return;

@@ -25,4 +25,2 @@ import { DEPTH_INDENTATION } from "../constants";

import { printSha3 } from "../opcodes/sha3";
import { printMload } from "../opcodes/mload";
import { printMstore } from "../opcodes/mstore";

@@ -113,3 +111,3 @@ /**

case "SLOAD":
if (dependencies.tracerEnv.sloads) {
if (dependencies.tracerEnv.opcodes.get("SLOAD")) {
await printSload(structLog, index, structLogs, dependencies);

@@ -119,3 +117,3 @@ }

case "SSTORE":
if (dependencies.tracerEnv.sstores) {
if (dependencies.tracerEnv.opcodes.get("SSTORE")) {
await printSstore(structLog, dependencies);

@@ -137,3 +135,3 @@ }

default:
if (dependencies.tracerEnv.opcodes.includes(structLog.op)) {
if (dependencies.tracerEnv.opcodes.get(structLog.op)) {
switch (structLog.op) {

@@ -164,8 +162,2 @@ case "ADD":

break;
case "MLOAD":
await printMload(structLog, index, structLogs, dependencies);
break;
case "MSTORE":
await printMstore(structLog, dependencies);
break;
default:

@@ -172,0 +164,0 @@ console.log(

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";
import { addCliParams, applyCliArgsToTracer } from "../utils";
import { VM } from "@nomicfoundation/ethereumjs-vm";
import { TraceRecorder } from "../trace/recorder";
import { HttpNetworkUserConfig } from "hardhat/types";
const originalCreate = VM.create;
VM.create = async function (...args) {
const vm = await originalCreate.bind(VM)(...args);
// @ts-ignore
const tracerEnv = global.tracerEnv;
const recorder = new TraceRecorder(vm, tracerEnv);
// @ts-ignore
global._hardhat_tracer_recorder = recorder;
return vm;
};
addCliParams(task("trace", "Traces a transaction hash"))

@@ -20,11 +39,34 @@ .addParam("hash", "transaction hash to view trace of")

if (tx == null) {
if (!args.rpc) {
const mainnetForkUrl = (hre.network.config as any).forking?.url;
if (!mainnetForkUrl) {
// try using url specified in network as rpc url
if (args.network) {
const userNetworks = hre.userConfig.networks;
if (userNetworks === undefined) {
throw new Error("No networks found in hardhat config");
}
if (userNetworks[args.network] === undefined) {
throw new Error(
"Transaction not found on current network, please pass an archive node with --rpc option"
`Network ${args.network} not found in hardhat config`
);
}
const url = (userNetworks[args.network] as HttpNetworkUserConfig).url;
if (url === undefined) {
throw new Error(
`Url not found in hardhat-config->networks->${args.network}`
);
}
args.rpc = url;
}
// try using current mainnet fork url as rpc url
const mainnetForkUrl = (hre.network.config as any).forking?.url;
if (mainnetForkUrl) {
args.rpc = mainnetForkUrl;
}
if (!args.rpc) {
// TODO add auto-detect network
throw new Error(
"rpc url not provided, please either use --network <network-name> or --rpc <rpc-url>"
);
}
const provider = new ethers.providers.StaticJsonRpcProvider(args.rpc);

@@ -45,53 +87,43 @@ const txFromRpc = await provider.getTransaction(args.hash);

try {
console.warn("Trying with rpc");
await printDebugTrace(args.hash, {
provider,
tracerEnv: hre.tracer,
artifacts: hre.artifacts,
nameTags: hre.tracer.nameTags,
});
// if printing was successful, then stop here
return;
} catch (error) {
console.warn(
"Using debug_tt on rpc failed, activating mainnet fork at block",
txFromRpc.blockNumber
);
await hre.network.provider.send("hardhat_reset", [
{
forking: {
jsonRpcUrl: args.rpc,
blockNumber: txFromRpc.blockNumber,
},
// TODO add support for decoding using debug_tt on the RPC if present, otherwise use hardhat mainnet fork
if (false) {
// decode response of debug_traceTransaction
// print
return; // should halt execution here
}
console.warn("Activating mainnet fork at block", txFromRpc.blockNumber);
await hre.network.provider.send("hardhat_reset", [
{
forking: {
jsonRpcUrl: args.rpc,
blockNumber: txFromRpc.blockNumber,
},
]);
}
},
]);
// after the above hardhat reset, tx should be present on the local node
}
// using hardhat for getting the trace. if tx was previously not found on hardhat local,
// but now it will be available, due to mainnet fork activation
console.warn("Trying with hardhat mainnet fork");
const tracePromise = printDebugTraceOrLogs(args.hash, {
provider: hre.network.provider,
tracerEnv: hre.tracer,
artifacts: hre.artifacts,
nameTags: hre.tracer.nameTags,
const node = await getNode(hre);
// we cant use this resp because stack and memory is not there (takes up lot of memory if enabled)
await node.traceTransaction(Buffer.from(args.hash.slice(2), "hex"), {
disableStorage: true,
disableMemory: true,
disableStack: true,
});
const delayPromise = new Promise((resolve) => {
setTimeout(() => resolve("delay"), 20000);
// TODO try to do this properly
// @ts-ignore
const recorder = (global?._hardhat_tracer_recorder as unknown) as TraceRecorder;
await recorder.previousTraces[recorder.previousTraces.length - 1].print({
artifacts: hre.artifacts,
tracerEnv: hre.tracer,
provider: hre.ethers.provider,
});
const resolved = await Promise.race([delayPromise, tracePromise]);
if (resolved === "delay") {
console.log(
"Seems that it is taking time to fetch the state involved. Please note that this process may take several minutes. A lot of eth_getStorageAt requests are currently being made to the rpc."
);
}
const traceResult = await tracePromise;
if (!traceResult) {
throw new Error("Transaction could not be traced");
}
await new Promise((resolve) => setTimeout(resolve, 1000));
return;
});

@@ -0,2 +1,6 @@

import { VM } from "@nomicfoundation/ethereumjs-vm";
import { Artifacts, EthereumProvider } from "hardhat/types";
import { TraceRecorder } from "./trace/recorder";
import { Decoder } from "./decoder";
import { BigNumberish } from "ethers";

@@ -9,9 +13,7 @@ export interface NameTags {

enabled?: boolean;
logs?: boolean;
calls?: boolean;
sstores?: boolean;
sloads?: boolean;
defaultVerbosity?: number;
gasCost?: boolean;
opcodes?: string[];
nameTags?: NameTags;
stateOverrides?: StateOverrides;
}

@@ -21,9 +23,8 @@

enabled: boolean;
logs: boolean;
calls: boolean;
sstores: boolean;
sloads: boolean;
ignoreNext: boolean;
verbosity: number;
gasCost: boolean;
opcodes: string[]; // TODO have a map of opcode to boolean
opcodes: Map<string, boolean>; // string[]; // TODO have a map of opcode to boolean
nameTags: NameTags;
// todo remove internal
_internal: {

@@ -35,2 +36,5 @@ printNameTagTip:

};
recorder?: TraceRecorder;
decoder?: Decoder;
stateOverrides?: StateOverrides;
}

@@ -63,1 +67,12 @@

}
export interface StateOverrides {
[address: string]: {
storage?: {
[slot: string | number]: BigNumberish;
};
bytecode?: string;
balance?: BigNumberish;
nonce?: BigNumberish;
};
}

@@ -0,3 +1,9 @@

import {
arrayify,
hexStripZeros,
hexZeroPad,
Interface,
} from "ethers/lib/utils";
import { BigNumber, ethers } from "ethers";
import { arrayify, hexStripZeros, hexZeroPad } from "ethers/lib/utils";
import { VM } from "@nomicfoundation/ethereumjs-vm";
import {

@@ -7,5 +13,8 @@ ConfigurableTaskDefinition,

} from "hardhat/types";
import { Address } from "@nomicfoundation/ethereumjs-util";
import {
ProviderLike,
StateOverrides,
StructLog,
TracerDependencies,
TracerDependenciesExtended,

@@ -16,19 +25,7 @@ TracerEnv,

export function getTracerEnvFromUserInput(
userInput?: TracerEnvUser
): TracerEnv {
return {
enabled: userInput?.enabled ?? false,
logs: userInput?.logs ?? false,
calls: userInput?.calls ?? false,
sstores: userInput?.sstores ?? false,
sloads: userInput?.sloads ?? false,
gasCost: userInput?.gasCost ?? false,
opcodes: userInput?.opcodes ?? [],
nameTags: userInput?.nameTags ?? {},
_internal: {
printNameTagTip: undefined,
},
};
}
import {
getOpcodesForHF,
Opcode,
} from "@nomicfoundation/ethereumjs-evm/dist/opcodes";
import { Item } from "./trace/transaction";

@@ -38,10 +35,13 @@ export function addCliParams(task: ConfigurableTaskDefinition) {

task
// params
.addOptionalParam("opcodes", "specify more opcodes to print")
// feature flags
.addFlag("logs", "print logs emitted during transactions")
.addFlag("calls", "print calls during transactions")
.addFlag("sloads", "print SLOADs during calls")
.addFlag("sstores", "print SSTOREs during transactions")
// verbosity flags
.addFlag("v", "set verbosity to 1, prints calls for only failed txs")
.addFlag(
"vv",
"set verbosity to 2, prints calls and storage for only failed txs"
)
.addFlag("vvv", "set verbosity to 3, prints calls for all txs")
.addFlag(
"vvvv",
"set verbosity to 4, prints calls and storage for all txs"
)
.addFlag("gascost", "display gas cost")

@@ -53,14 +53,13 @@ .addFlag(

// feature group flags
.addFlag("trace", "trace logs and calls in transactions")
.addFlag(
"fulltrace",
"trace logs, calls and storage writes in transactions"
)
// aliases
.addFlag("tracefull", "alias for fulltrace")
.addFlag("gas", "alias for gascost")
// params
.addOptionalParam("opcodes", "specify more opcodes to print")
// alias
.addFlag("trace", "enable tracer with verbosity 3")
.addFlag("fulltrace", "enable tracer with verbosity 4")
);
}
export const DEFAULT_VERBOSITY = 3;
export function applyCliArgsToTracer(

@@ -70,41 +69,50 @@ args: any,

) {
// populating aliases
const fulltrace = args.fulltrace || args.tracefull;
const gascost = args.gascost || args.gas;
// enabled by default
hre.tracer.enabled = true;
// if any flag is present, then enable tracer
if (args.logs || args.trace || fulltrace || args.disabletracer === true) {
hre.tracer.enabled = true;
// for not enabling tracer from the start
if (args.disabletracer) {
hre.tracer.enabled = false;
}
// enabling config by flags passed
if (args.logs) {
hre.tracer.logs = true;
// always active opcodes
const opcodesToActivate = ["RETURN", "REVERT"];
const logOpcodes = ["LOG0", "LOG1", "LOG2", "LOG3", "LOG4"];
const storageOpcodes = ["SLOAD", "SSTORE"];
// setting verbosity
if (args.vvvv || args.fulltrace) {
hre.tracer.verbosity = 4;
opcodesToActivate.push(...logOpcodes, ...storageOpcodes);
} else if (args.vvv || args.trace) {
hre.tracer.verbosity = 3;
opcodesToActivate.push(...logOpcodes);
} else if (args.vv) {
hre.tracer.verbosity = 2;
opcodesToActivate.push(...logOpcodes, ...storageOpcodes);
} else if (args.v) {
opcodesToActivate.push(...logOpcodes);
hre.tracer.verbosity = 1;
}
if (args.calls) {
hre.tracer.calls = true;
for (const opcode of opcodesToActivate) {
hre.tracer.opcodes.set(opcode, true);
}
if (args.sloads) {
hre.tracer.sloads = true;
}
if (args.sstores) {
hre.tracer.sstores = true;
}
if (args.opcodes) {
hre.tracer.opcodes = [...args.opcodes.split(",")];
}
// hre.tracer.opcodes = [hre.tracer.opcodes, ...args.opcodes.split(",")];
for (const opcode of args.opcodes.split(",")) {
hre.tracer.opcodes.set(opcode, true);
}
// enabling config by mode of operation
if (args.trace) {
hre.tracer.logs = true;
hre.tracer.calls = true;
if (hre.tracer.recorder === undefined) {
throw new Error(
`hardhat-tracer/utils/applyCliArgsToTracer: hre.tracer.recorder is undefined`
);
}
checkIfOpcodesAreValid(hre.tracer.opcodes, hre.tracer.recorder.vm);
}
if (fulltrace) {
hre.tracer.logs = true;
hre.tracer.calls = true;
hre.tracer.sloads = true;
hre.tracer.sstores = true;
}
if (gascost) {
if (args.gascost) {
hre.tracer.gasCost = true;

@@ -114,4 +122,5 @@ }

// TODO remove
export function isOnlyLogs(env: TracerEnv): boolean {
return env.logs && !env.calls && !env.sstores && !env.sloads && !env.gasCost;
return env.verbosity === 1;
}

@@ -121,9 +130,9 @@

address: string,
dependencies: TracerDependenciesExtended
dependencies: TracerDependencies
): string | undefined {
return (
dependencies.nameTags[address] ||
dependencies.nameTags[address.toLowerCase()] ||
dependencies.nameTags[address.toUpperCase()] ||
dependencies.nameTags[ethers.utils.getAddress(address)]
dependencies.tracerEnv.nameTags[address] ||
dependencies.tracerEnv.nameTags[address.toLowerCase()] ||
dependencies.tracerEnv.nameTags[address.toUpperCase()] ||
dependencies.tracerEnv.nameTags[ethers.utils.getAddress(address)]
);

@@ -185,2 +194,6 @@ }

export function shallowCopyStack2(stack: bigint[]): string[] {
return [...stack].map((x) => BigNumber.from(x).toHexString());
}
export function compareBytecode(

@@ -213,1 +226,139 @@ artifactBytecode: string,

}
/**
* Ensures 0x prefix to a hex string which may or may not
* @param str A hex string that may or may not have 0x prepended
*/
export function hexPrefix(str: string): string {
if (!str.startsWith("0x")) str = "0x" + str;
return str;
}
export function checkIfOpcodesAreValid(opcodes: Map<string, boolean>, vm: VM) {
// fetch the opcodes which work on this VM
let activeOpcodesMap = new Map<string, boolean>();
for (const opcode of getOpcodesForHF(vm._common).opcodes.values()) {
activeOpcodesMap.set(opcode.fullName, true);
}
// check if there are any opcodes specified in tracer which do not work
for (const opcode of opcodes.keys()) {
if (!activeOpcodesMap.get(opcode)) {
throw new Error(
`The opcode "${opcode}" is not active on this VM. If the opcode name is misspelled in the config, please correct it.`
);
}
}
}
export function isItem(item: any): item is Item<any> {
return item && typeof item.opcode === "string";
}
export async function applyStateOverrides(
stateOverrides: StateOverrides,
vm: VM
) {
for (const [_address, overrides] of Object.entries(stateOverrides)) {
const address = Address.fromString(_address);
// for balance and nonce
if (overrides.balance !== undefined || overrides.nonce !== undefined) {
const account = await vm.stateManager.getAccount(address);
if (overrides.nonce !== undefined) {
account.nonce = BigNumber.from(overrides.nonce).toBigInt();
}
if (overrides.balance) {
account.balance = BigNumber.from(overrides.balance).toBigInt();
}
await vm.stateManager.putAccount(address, account);
}
// for bytecode
if (overrides.bytecode) {
await vm.stateManager.putContractCode(
address,
Buffer.from(overrides.bytecode, "hex")
);
}
// for storage slots
if (overrides.storage) {
for (const [key, value] of Object.entries(overrides.storage)) {
await vm.stateManager.putContractStorage(
address,
Buffer.from(
hexZeroPad(BigNumber.from(key).toHexString(), 32).slice(2),
"hex"
),
Buffer.from(
hexZeroPad(BigNumber.from(value).toHexString(), 32).slice(2),
"hex"
)
);
}
}
}
}
export async function fetchContractName(to: string, provider: ProviderLike) {
let name = await fetchContractNameFromMethodName(to, "symbol", provider);
if (!name) {
name = await fetchContractNameFromMethodName(to, "name", provider);
}
if (name) {
name = name.split(" ").join("");
}
return name;
}
export async function fetchContractNameFromMethodName(
to: string,
methodName: string,
provider: ProviderLike
): Promise<string | undefined> {
const iface1 = new Interface([
`function ${methodName}() public view returns (string)`,
]);
let result1;
try {
result1 = await provider.send("eth_call", [
{
to,
data: iface1.encodeFunctionData(methodName, []),
},
]);
const d = iface1.decodeFunctionResult(methodName, result1);
return d[0];
} catch {
try {
const iface2 = new Interface([
`function ${methodName}() public view returns (bytes32)`,
]);
const d = iface2.decodeFunctionResult(methodName, result1);
const bytes32 = d[0];
return ethers.utils.toUtf8String(bytes32);
} catch {}
}
return undefined;
}
export async function fetchContractDecimals(
to: string,
provider: ProviderLike
): Promise<number | undefined> {
const iface1 = new Interface([
`function decimals() public view returns (uint8)`,
]);
let result1;
try {
result1 = await provider.send("eth_call", [
{
to,
data: iface1.encodeFunctionData("decimals", []),
},
]);
const d = iface1.decodeFunctionResult("decimals", result1);
return d[0];
} catch {}
return undefined;
}

@@ -10,11 +10,6 @@ import { ethers } from "ethers";

} from "hardhat/types";
import { Decoder } from "./decoder";
import { printDebugTraceOrLogs } from "./print";
import {
ProviderLike,
TracerDependencies,
TracerEnv,
TracerEnvUser,
} from "./types";
import { getTracerEnvFromUserInput, isOnlyLogs } from "./utils";
import { ProviderLike, TracerDependencies, TracerEnv } from "./types";
import { DEFAULT_VERBOSITY } from "./utils";

@@ -36,2 +31,4 @@ /**

let error: any;
// console.log("wrapper->args.method", args.method);
try {

@@ -46,33 +43,44 @@ result = await this.dependencies.provider.send(

if (
this.dependencies.tracerEnv.enabled &&
(result != null || error != null) &&
(args.method === "eth_sendTransaction" ||
args.method === "eth_sendRawTransaction" ||
args.method === "eth_getTransactionReceipt")
) {
let hash = result ?? error?.transactionHash;
let receipt;
if (typeof result === "object" && result !== null) {
hash = result.transactionHash ?? result.hash;
receipt = result;
} else {
receipt = await this.dependencies.provider.send(
"eth_getTransactionReceipt",
[hash]
// TODO take decision whether to print or not
// if estimateGas fails then print it
// sendTx should be printing it regardless of success or failure
const isSendTransaction = args.method === "eth_sendTransaction";
const isEthCall = args.method === "eth_call";
const isEstimateGas = args.method === "eth_estimateGas";
const isSendTransactionFailed = isSendTransaction && !!error;
const isEthCallFailed = isEthCall && !!error;
const isEstimateGasFailed = isEstimateGas && !!error;
let shouldPrint: boolean;
switch (this.dependencies.tracerEnv.verbosity) {
case 0:
shouldPrint = false;
break;
case 1:
case 2:
shouldPrint =
isSendTransactionFailed || isEthCallFailed || isEstimateGasFailed;
break;
case 3:
case 4:
shouldPrint = true;
break;
default:
throw new Error(
"Invalid verbosity value: " + this.dependencies.tracerEnv.verbosity
);
}
if (this.dependencies.tracerEnv.ignoreNext) {
this.dependencies.tracerEnv.ignoreNext = false;
} else {
if (this.dependencies.tracerEnv.enabled && shouldPrint) {
await this.dependencies.tracerEnv.recorder?.previousTraces?.[
this.dependencies.tracerEnv.recorder?.previousTraces.length - 1
]?.print?.(this.dependencies);
}
if (!this.txPrinted[hash] && receipt !== null) {
this.txPrinted[hash] = true;
const dependenciesExtended = {
...this.dependencies,
nameTags: { ...this.dependencies.tracerEnv.nameTags },
};
try {
await printDebugTraceOrLogs(hash, dependenciesExtended);
} catch (error) {
console.log("error in request wrapper:", error);
}
}
}
if (error) {

@@ -101,3 +109,3 @@ throw error;

// ensure env is present
hre.tracer = hre.tracer ?? getTracerEnvFromUserInput(hre.tracer);
// hre.tracer = hre.tracer ?? getTracerEnvFromUserInput(hre.tracer);
}

@@ -118,3 +126,18 @@

// ensure env is present
tracerEnv = getTracerEnvFromUserInput(tracerEnv);
// const tracerEnv = getTracerEnvFromUserInput(tracerEnvUser);
if (!tracerEnv) {
tracerEnv = {
enabled: false,
ignoreNext: false,
verbosity: DEFAULT_VERBOSITY,
gasCost: false,
opcodes: new Map(),
nameTags: {},
// @ts-ignore TODO remove, this has no place in "config"
_internal: {
printNameTagTip: undefined,
},
decoder: new Decoder(artifacts),
};
}

@@ -121,0 +144,0 @@ const tracerProvider = new TracerWrapper({ provider, artifacts, tracerEnv });

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc