packages/debugger/lib/evm/selectors/index.js
import debugModule from "debug";
const debug = debugModule("debugger:evm:selectors");
import { createSelectorTree, createLeaf } from "reselect-tree";
import BN from "bn.js";
import trace from "lib/trace/selectors";
import * as Codec from "@truffle/codec";
import {
keccak256,
isCallMnemonic,
isCreateMnemonic,
isShortCallMnemonic,
isDelegateCallMnemonicBroad,
isDelegateCallMnemonicStrict,
isStaticCallMnemonic,
isSelfDestructMnemonic
} from "lib/helpers";
const ZERO_WORD = "00".repeat(Codec.Evm.Utils.WORD_SIZE);
function determineFullContext(
{ address, binary },
instances,
search,
contexts
) {
let contextId;
let isConstructor = Boolean(binary);
if (address) {
//if we're in a call to a deployed contract, we must have recorded
//the context in the codex, so we don't need to do any further
//searching
({ context: contextId, binary } = instances[address]);
} else if (isConstructor) {
//otherwise, if we're in a constructor, we'll need to actually do a
//search
contextId = search(binary);
} else {
//exceptional case: no transaction is loaded
return null;
}
if (contextId != undefined) {
//if we found the context, use it
let context = contexts[contextId];
return {
...context,
binary
};
} else {
//otherwise we'll construct something default
return {
binary,
isConstructor
};
}
}
/**
* create EVM-level selectors for a given trace step selector
* may specify additional selectors to include
*/
function createStepSelectors(step, state = null) {
let base = {
/**
* .trace
*
* trace step info related to operation
*/
trace: createLeaf([step], step => {
if (!step) {
return null;
}
let { gasCost, op, pc } = step;
return { gasCost, op, pc };
}),
/**
* .programCounter
*/
programCounter: createLeaf(["./trace"], step => (step ? step.pc : null)),
/**
* .isCall
*
* whether the opcode will switch to another calling context
*/
isCall: createLeaf(["./trace"], step => isCallMnemonic(step.op)),
/**
* .isShortCall
*
* for calls that only take 6 arguments instead of 7
*/
isShortCall: createLeaf(["./trace"], step => isShortCallMnemonic(step.op)),
/**
* .isDelegateCallBroad
*
* for calls that delegate storage
*/
isDelegateCallBroad: createLeaf(["./trace"], step =>
isDelegateCallMnemonicBroad(step.op)
),
/**
* .isDelegateCallStrict
*
* for calls that additionally delegate sender and value
*/
isDelegateCallStrict: createLeaf(["./trace"], step =>
isDelegateCallMnemonicStrict(step.op)
),
/**
* .isStaticCall
*/
isStaticCall: createLeaf(["./trace"], step =>
isStaticCallMnemonic(step.op)
),
/**
* .isCreate
* (includes CREATE2)
*/
isCreate: createLeaf(["./trace"], step => isCreateMnemonic(step.op)),
/**
* .isSelfDestruct
*/
isSelfDestruct: createLeaf(["./trace"], step =>
isSelfDestructMnemonic(step.op)
),
/**
* .isCreate2
*/
isCreate2: createLeaf(["./trace"], step => step.op === "CREATE2"),
/**
* .isStore
*/
isStore: createLeaf(["./trace"], step => step.op === "SSTORE"),
/**
* .isLoad
*/
isLoad: createLeaf(["./trace"], step => step.op === "SLOAD"),
/**
* .touchesStorage
*
* whether the instruction involves storage
*/
touchesStorage: createLeaf(
["./isStore", "isLoad"],
(stores, loads) => stores || loads
),
/**
* .isPop
* used by data
*/
isPop: createLeaf(["./trace"], step => step.op === "POP"),
/**
* .isLog
*/
isLog: createLeaf(["./topicCount"], topicCount => topicCount !== null),
/**
* .topicCount
* returns null if not on a logging step
*/
topicCount: createLeaf(["./trace"], step => {
if (!step.op) {
return null;
}
const match = step.op.match(/LOG(\d+)/);
if (!match) {
return null;
}
return Number(match[1]);
})
};
if (state) {
const isRelative = path =>
typeof path === "string" &&
(path.startsWith("./") || path.startsWith("../"));
if (isRelative(state)) {
state = `../${state}`;
}
Object.assign(base, {
/**
* .isJump
*/
isJump: createLeaf(
["./trace", state],
(step, { stack }) =>
step.op === "JUMP" ||
(step.op === "JUMPI" && stack[stack.length - 2] !== ZERO_WORD)
),
/**
* .valueStored
* the storage written, as determined by looking at the stack
* rather than at storage (since valueLoaded is now being done
* this way, may as well do valueStored this way as well and
* completely remove our dependence on the storage field!)
*/
valueStored: createLeaf(["./isStore", state], (isStore, { stack }) => {
if (!isStore) {
return null;
}
return stack[stack.length - 2];
}),
/**
* .callAddress
*
* address transferred to by call operation
*/
callAddress: createLeaf(
["./isCall", state],
(isCall, { stack }) => {
if (!isCall) {
return null;
}
let address = stack[stack.length - 2];
return Codec.Evm.Utils.toAddress(address);
}
),
/**
* .createBinary
*
* binary code to execute via create operation
*/
createBinary: createLeaf(
["./isCreate", state],
(isCreate, { stack, memory }) => {
if (!isCreate) {
return null;
}
// Get the code that's going to be created from memory.
// Note we multiply by 2 because these offsets are in bytes.
const offset = parseInt(stack[stack.length - 2], 16) * 2;
const length = parseInt(stack[stack.length - 3], 16) * 2;
return (
"0x" +
memory
.join("")
.substring(offset, offset + length)
.padEnd(length, "00")
);
}
),
/**
* .callData
*
* data passed to EVM call
*/
callData: createLeaf(
["./isCall", "./isShortCall", "./isCreate", state],
(isCall, short, isCreate, { stack, memory }) => {
if (!isCall) {
//if it's not a call or create, this is invalid and we return null.
//for creations, we return 0x (if you want the binary, use createBinary
//instead)
return isCreate ? "0x" : null;
}
//if it's 6-argument call, the data start and offset will be one spot
//higher in the stack than they would be for a 7-argument call, so
//let's introduce an offset to handle this
let argOffset = short ? 1 : 0;
// Get the data from memory.
// Note we multiply by 2 because these offsets are in bytes.
const offset = parseInt(stack[stack.length - 4 + argOffset], 16) * 2;
const length = parseInt(stack[stack.length - 5 + argOffset], 16) * 2;
return (
"0x" +
memory
.join("")
.substring(offset, offset + length)
.padEnd(length, "00")
);
}
),
/**
* .callValue
*
* value for the call (not create); returns null for DELEGATECALL
*/
callValue: createLeaf(
["./isCall", "./isDelegateCallStrict", "./isStaticCall", state],
(calls, delegates, isStatic, { stack }) => {
if (!calls || delegates) {
return null;
}
if (isStatic) {
return new BN(0);
}
//otherwise, for CALL and CALLCODE, it's the 3rd argument
let value = stack[stack.length - 3];
return Codec.Conversion.toBN(value);
}
),
/**
* .createValue
*
* value for the create
*/
createValue: createLeaf(["./isCreate", state], (isCreate, { stack }) => {
if (!isCreate) {
return null;
}
//creates have the value as the first argument
let value = stack[stack.length - 1];
return Codec.Conversion.toBN(value);
}),
/**
* .storageAffected
*
* storage slot being stored to or loaded from
* we do NOT prepend "0x"
*/
storageAffected: createLeaf(
["./touchesStorage", state],
(touchesStorage, { stack }) => {
if (!touchesStorage) {
return null;
}
return stack[stack.length - 1];
}
),
/**
* .salt
*/
salt: createLeaf(
["./isCreate2", state],
(isCreate2, { stack }) => {
if (!isCreate2) {
return null;
}
return "0x" + stack[stack.length - 4];
}
),
/**
* .callContext
*
* context of what this step is calling/creating (if applicable)
*/
callContext: createLeaf(
[
"./callAddress",
"./createBinary",
"/current/codex/instances",
"/info/binaries/search",
"/info/contexts"
],
(address, binary, instances, search, contexts) =>
determineFullContext({ address, binary }, instances, search, contexts)
),
/**
* .logData
*
* the data portion of what's getting logged
*/
logData: createLeaf(["./isLog", state], (isLog, { stack, memory }) => {
if (!isLog) {
return null;
}
// Get the data from memory.
// Note we multiply by 2 because these offsets are in bytes.
// (note the data offset/length comes before the topics, so
// we don't neeed ot adjust for the topic count)
const offset = parseInt(stack[stack.length - 1], 16) * 2;
const length = parseInt(stack[stack.length - 2], 16) * 2;
return (
"0x" +
memory
.join("")
.substring(offset, offset + length)
.padEnd(length, "00")
);
}),
/**
* .logTopics
* returns an array of hex strings
*/
logTopics: createLeaf(
["./isLog", "./topicCount", state],
(isLog, topicCount, { stack }) => {
if (!isLog) {
return null;
}
//the topics (if any) start with the third argument,
//so we take the appropriate number of entries from
//the end of the stack (excluding than the last two), then
//reverse to put them in order; we also prepend "0x" for
//convenience
//note the use of reverse() is safe due to the use of slice() first
return stack
.slice(-2 - topicCount, -2)
.reverse()
.map(word => "0x" + word);
}
)
});
}
return base;
}
const evm = createSelectorTree({
/**
* evm.state
*/
state: state => state.evm,
/**
* evm.application
*/
application: {
/**
* evm.application.storageLookup
*/
storageLookup: createLeaf(
["/state"],
state => state.application.storageLookup
),
/**
* evm.application.storageLookupSupported
*/
storageLookupSupported: createLeaf(
["/state"],
state => state.application.storageLookupSupported
)
},
/**
* evm.info
*/
info: {
/**
* evm.info.contexts
*/
contexts: createLeaf(["/state"], state => state.info.contexts.byContext),
/**
* evm.info.binaries
*/
binaries: {
/**
* evm.info.binaries.search
*
* returns function (binary) => context (returns the *ID* of the context)
* (returns null on no match)
*/
search: createLeaf(
["/info/contexts"],
contexts => binary =>
//HACK: the type of contexts doesn't actually match!! fortunately
//it's good enough to work
(
Codec.Contexts.Utils.findContext(contexts, binary) || {
context: null
}
).context
)
}
},
/**
* evm.transaction
*/
transaction: {
/**
* evm.transaction.globals
*/
globals: {
/**
* evm.transaction.globals.tx
*/
tx: createLeaf(["/state"], state => state.transaction.globals.tx),
/**
* evm.transaction.globals.block
*/
block: createLeaf(["/state"], state => state.transaction.globals.block)
},
/**
* evm.transaction.blockHash
*/
blockHash: createLeaf(
["/state"],
state => state.transaction.txIdentification.blockHash
),
/**
* evm.transaction.txIndex
*/
txIndex: createLeaf(
["/state"],
state => state.transaction.txIdentification.txIndex
),
/**
* evm.transaction.status
*/
status: createLeaf(["/state"], state => state.transaction.status),
/**
* evm.transaction.initialCall
*/
initialCall: createLeaf(["/state"], state => state.transaction.initialCall),
/**
* evm.transaction.startingContext
*/
startingContext: createLeaf(
[
"/current/callstack", //we're just getting bottom stackframe, so this is in fact tx-level
"/current/codex/instances", //this should also be fine?
"/info/binaries/search",
"/info/contexts"
],
(stack, instances, search, contexts) =>
stack.length > 0
? determineFullContext(stack[0], instances, search, contexts)
: null
),
/**
* evm.transaction.affectedInstances
*/
affectedInstances: createLeaf(
["/state"],
state => state.transaction.affectedInstances.byAddress
)
},
/**
* evm.current
*/
current: {
/**
* evm.current.callstack
*/
callstack: state => state.evm.proc.callstack,
/**
* evm.current.call
*/
call: createLeaf(
["./callstack"],
stack => (stack.length ? stack[stack.length - 1] : {})
),
/**
* evm.current.context
*/
context: createLeaf(
[
"./call",
"./codex/instances",
"/info/binaries/search",
"/info/contexts"
],
determineFullContext
),
/**
* evm.current.isIR
* was the current context compield with IR on?
* currently, this defaults to false; in the future the default
* may depend on the Solidity version
*/
isIR: createLeaf(["./context"], context =>
context.settings ? Boolean(context.settings.viaIR) : false
),
/**
* evm.current.state
*
* evm state info: as of last operation, before op defined in step
*/
state: Object.assign(
{},
...["depth", "error", "gas", "memory", "stack"].map(param => ({
[param]: createLeaf([trace.step], step => step[param])
}))
),
/**
* evm.current.step
*/
step: {
...createStepSelectors(trace.step, "./state"),
//the following step selectors only exist for current, not next or any
//other step
/**
* evm.current.step.createdAddress
*
* address created by the current create step
*/
createdAddress: createLeaf(
[
"./isCreate",
"/nextOfSameDepth/state/stack",
"./isCreate2",
"./create2Address"
],
(isCreate, stack, isCreate2, create2Address) => {
if (!isCreate) {
return null;
}
let address = stack //may be null if the create step itself fails
? Codec.Evm.Utils.toAddress(stack[stack.length - 1])
: Codec.Evm.Utils.ZERO_ADDRESS; //nothing got created, so...
if (address === Codec.Evm.Utils.ZERO_ADDRESS && isCreate2) {
return create2Address;
}
return address;
}
),
/**
* evm.current.step.create2Address
*
* address created by the current create2 step
* (computed, not read off the return)
*/
create2Address: createLeaf(
["./isCreate2", "./createBinary", "../call", "../state/stack"],
(isCreate2, binary, { storageAddress }, stack) =>
isCreate2
? Codec.Evm.Utils.toAddress(
"0x" +
keccak256({
type: "bytes",
value:
//slice 2's are for cutting off initial "0x" where we've prepended this
//0xff, then address, then salt, then code hash
"0xff" +
storageAddress.slice(2) +
stack[stack.length - 4] +
keccak256({ type: "bytes", value: binary }).slice(2)
}).slice(
2 +
2 *
(Codec.Evm.Utils.WORD_SIZE -
Codec.Evm.Utils.ADDRESS_SIZE)
)
//slice off initial 0x and initial 12 bytes (note we've re-prepended the
//0x at the beginning)
)
: null
),
/**
* evm.current.step.isInstantCallOrCreate
*
* are we doing a call or create for which there are no trace steps?
* This can happen if:
* 1. we call a precompile
* 2. we call an externally-owned account (or other account w/no code)
* 3. we do a call or create but the call stack is exhausted
* 4. we attempt to transfer more ether than we have
*/
isInstantCallOrCreate: createLeaf(
["./isCall", "./isCreate", "./isContextChange"],
(calls, creates, contextChange) => (calls || creates) && !contextChange
),
/**
* evm.current.step.isContextChange
* groups together calls, creates, halts, and exceptional halts
*/
isContextChange: createLeaf(
["/current/state/depth", "/next/state/depth"],
(currentDepth, nextDepth) => currentDepth !== nextDepth
),
/**
* evm.current.step.isNormalHalting
*/
isNormalHalting: createLeaf(
["./isHalting", "./returnStatus"],
(isHalting, status) => isHalting && status
),
/**
* evm.current.step.isHalting
*
* whether the instruction halts or returns from a calling context
* HACK: the check for stepsRemainining === 0 is a hack to cover
* the special case when there are no trace steps; normally this
* is unnecessary because the spoofed step past the end covers it
*/
isHalting: createLeaf(
["/current/state/depth", "/next/state/depth", trace.stepsRemaining],
(currentDepth, nextDepth, stepsRemaining) =>
nextDepth < currentDepth || stepsRemaining === 0
),
/**
* evm.current.step.isExceptionalHalting
*/
isExceptionalHalting: createLeaf(
["./isHalting", "./returnStatus"],
(isHalting, status) => isHalting && !status
),
/**
* evm.current.step.returnStatus
* checks the return status of the *current* halting instruction or insta-call
* returns null if not halting & not an insta-call
* (returns a boolean -- true for success, false for failure)
*/
returnStatus: createLeaf(
[
"./isHalting",
"./isInstantCallOrCreate",
"/next/state",
trace.stepsRemaining,
"/transaction/status"
],
(isHalting, isInstaCall, { stack }, remaining, finalStatus) => {
if (!isHalting && !isInstaCall) {
return null; //not clear this'll do much good since this may get
//read as false, but, oh well, may as well
}
if (remaining <= 1) {
return finalStatus;
} else {
return stack[stack.length - 1] !== ZERO_WORD;
}
}
),
/**
* evm.current.step.returnValue
*
* for a [successful] RETURN or REVERT instruction, the value returned;
* we DO prepend "0x"
* for everything else, including unsuccessful RETURN, just returns "0x"
* (which is what the return value would be if the instruction were to
* fail) (or succeed in the case of STOP or SELFDESTRUCT)
* NOTE: technically this will be wrong if a REVERT fails, but that case
* is hard to detect and it barely matters
*/
returnValue: createLeaf(
["./trace", "./isExceptionalHalting", "../state"],
(step, isExceptionalHalting, { stack, memory }) => {
if (step.op !== "RETURN" && step.op !== "REVERT") {
return "0x";
}
if (isExceptionalHalting && step.op !== "REVERT") {
return "0x";
}
// Get the data from memory.
// Note we multiply by 2 because these offsets are in bytes.
const offset = parseInt(stack[stack.length - 1], 16) * 2;
const length = parseInt(stack[stack.length - 2], 16) * 2;
return (
"0x" +
memory
.join("")
.substring(offset, offset + length)
.padEnd(length, "00")
);
}
),
/**
* evm.current.step.valueLoaded
* the storage loaded on an SLOAD. determined by examining
* the next stack, rather than storage (we're avoiding
* relying on storage to support old versions of Geth and Besu)
* we do not include an initial "0x"
*/
valueLoaded: createLeaf(
["./isLoad", "/next/state"],
(isLoad, { stack }) => {
if (!isLoad) {
return null;
}
return stack[stack.length - 1];
}
),
/**
* evm.current.step.beneficiary
* NOTE: for a value-destroying selfdestruct, returns null
*/
beneficiary: createLeaf(
["./isSelfDestruct", "../state", "../call"],
(isSelfDestruct, { stack }, { storageAddress: currentAddress }) => {
if (!isSelfDestruct) {
return null;
}
const beneficiary = Codec.Evm.Utils.toAddress(
stack[stack.length - 1]
);
return beneficiary !== currentAddress ? beneficiary : null;
}
)
},
/**
* evm.current.codex (namespace)
*/
codex: {
/**
* evm.current.codex (selector)
* the whole codex! not that that's very much at the moment
*/
_: createLeaf(["/state"], state => state.proc.codex),
/**
* evm.current.codex.storage
* the current storage, as fetched from the codex
*/
storage: createLeaf(
["./_", "../call"],
(codex, { storageAddress }) =>
codex[codex.length - 1].accounts[storageAddress].storage
),
/**
* evm.current.codex.instances
*/
instances: createLeaf(["./_"], codex =>
Object.assign(
{},
...Object.entries(codex[codex.length - 1].accounts).map(
([address, { code, context }]) => ({
[address]: { address, binary: code, context }
})
)
)
)
}
},
/**
* evm.next
*/
next: {
/**
* evm.next.state
*
* evm state as a result of next step operation
*/
state: Object.assign(
{},
...["depth", "error", "gas", "memory", "stack", "storage"].map(param => ({
[param]: createLeaf([trace.next], step => step[param])
}))
),
/**
* evm.next.step
*/
step: createStepSelectors(trace.next, "./state")
},
/**
* evm.nextOfSameDepth
*/
nextOfSameDepth: {
/**
* evm.nextOfSameDepth.state
*
* evm state at the next step of same depth
* individual parts of the state will return null if there
* is no such step
*/
state: Object.assign(
{},
...["depth", "error", "gas", "memory", "stack", "storage"].map(param => ({
[param]: createLeaf([trace.nextOfSameDepth], step =>
step ? step[param] : null
)
}))
)
}
});
export default evm;