UNPKG

@polareth/evmstate

Version:

A TypeScript library for tracing, and visualizing EVM state changes with detailed human-readable labeling.

173 lines (158 loc) 6.01 kB
import { type Abi, type TraceResult } from "tevm"; import { type Address, type ContractFunctionName, type Hex } from "tevm/utils"; import { abi } from "@shazow/whatsabi"; import { trim } from "viem"; import { parseConfig } from "@/lib/explore/config.js"; import { exploreStorage } from "@/lib/explore/explore-storage.js"; import { extractPotentialKeys } from "@/lib/explore/mapping.js"; import { type SolcStorageLayout, type SolcStorageLayoutTypes } from "@/lib/solc.js"; import { TraceStateResult } from "@/lib/trace/result.js"; import type { LabeledIntrinsicsState, LabeledState, LabeledStorageState, TraceStateOptions, } from "@/lib/trace/types.js"; import { cleanTrace } from "@/lib/trace/utils.js"; import { logger } from "@/logger.js"; export const labelStateDiff = < TAbi extends Abi | readonly unknown[] = Abi, TFunctionName extends ContractFunctionName<TAbi> = ContractFunctionName<TAbi>, >({ stateDiff, layouts, uniqueAddresses, structLogs, abiFunctions, options, }: { stateDiff: Record< Address, LabeledIntrinsicsState & { storage: Record<Hex, { current?: Hex; next?: Hex; modified: boolean }> } >; layouts: Record<Address, SolcStorageLayout>; uniqueAddresses: Array<Address>; structLogs: TraceResult["structLogs"]; abiFunctions: Array<abi.ABIFunction>; options: TraceStateOptions<TAbi, TFunctionName>; }): TraceStateResult => { // Extract potential key/index values from the execution trace // Create a slim version of the trace with deduplicated stack values for efficiency const dedupedTraceLog = { // Deduplicate stack values across all operations uniqueStackValues: [...new Set(structLogs.flatMap((log) => log.stack))], // Only keep storage-related operations for detailed analysis relevantOps: structLogs.filter((log) => ["SLOAD", "SSTORE", "SHA3"].includes(log.op)), }; const potentialKeys = extractPotentialKeys(dedupedTraceLog, uniqueAddresses, abiFunctions, options); logger.log(`Extracted ${potentialKeys.length} unique potential values from the trace`); const result = new TraceStateResult(); // Process each address for (const address of uniqueAddresses) { const layout = layouts[address.toLowerCase() as Address]; const { storage, ...intrinsics } = stateDiff[address]; if (!layout) { const labeledState: LabeledState = { ...intrinsics, storage: Object.entries(storage).reduce( (acc, [slot, { current, next }]) => { acc[`slot_${slot}`] = { name: `slot_${slot}`, trace: [ // @ts-expect-error - Type boolean is not assignable to false { modified: next !== undefined && current !== undefined && next !== current, ...(current !== undefined ? { current: { hex: current } } : {}), ...(next !== undefined ? { next: { hex: next } } : {}), slots: [slot as Hex], path: [], fullExpression: `slot_${slot}`, note: "Could not label this slot access because no layout was found.", }, ], }; return acc; }, {} as Record<string, LabeledStorageState<string, string, SolcStorageLayoutTypes>>, ), }; result.set(address.toLowerCase() as Address, labeledState); continue; } // 1. Decode using all known variables with optimized exploration const { withLabels, unexploredSlots } = exploreStorage( layout, storage, potentialKeys.map((k) => k.hex), parseConfig(options.config), ); // 2. Process results into named variables const decoded = withLabels.reduce( (acc, result) => { // Retrieve existing entry or create a new one acc[result.name] = acc[result.name] ?? { name: result.name, type: result.type, kind: result.type.startsWith("mapping") ? "mapping" : result.type.endsWith("]") && result.type.match(/\[\d+\]$/) ? "static_array" : result.type.endsWith("[]") ? "dynamic_array" : result.type.startsWith("struct") ? "struct" : result.type === "bytes" || result.type === "string" ? "bytes" : "primitive", trace: [], }; acc[result.name].trace.push( cleanTrace({ modified: result.next !== undefined && result.current !== undefined && trim(result.next.hex) !== trim(result.current.hex), current: result.current, next: result.next, slots: result.slots, path: result.path, fullExpression: result.fullExpression, note: result.note, }), ); return acc; }, {} as Record<string, LabeledStorageState<string, string, SolcStorageLayoutTypes>>, ); // 3. Create unknown variables access traces for remaining slots const unknownAccess = Object.fromEntries( [...unexploredSlots].map((slot) => { const current = storage[slot].current; const next = storage[slot].next; return [ `slot_${slot}`, { name: `slot_${slot}`, trace: [ cleanTrace({ modified: next !== undefined && current !== undefined && next !== current, current: { hex: current }, next: { hex: next }, slots: [slot], path: [], fullExpression: `slot_${slot}`, note: "Could not label this slot access.", }), ], }, ]; }), ); // Add to TraceStateResult result.set(address.toLowerCase() as Address, { ...intrinsics, storage: { ...decoded, ...unknownAccess }, }); } return result; };