UNPKG

@polareth/evmstate

Version:

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

128 lines (108 loc) 5.18 kB
import { decodeAbiParameters, toHex, type Hex } from "tevm"; import { padHex, type ByteArray, type ToHexParameters } from "viem"; import type { DecodedResult, SolidityTypeToTsType } from "@/lib/explore/types.js"; import { type SolcStorageLayoutTypes } from "@/lib/solc.js"; import { logger } from "@/logger.js"; export const max = <T extends bigint | number>(...nums: Array<T>): T => // @ts-expect-error bigint/number nums.reduce((max, num) => (num > max ? num : max), -Infinity); /** * Decodes the 'current'/'next' for a single slot as a primitive field. Returns { currentValue, nextValue? } if data is * found, else null. */ export const decodeSlotDiffForPrimitive = ( storageDiff: Record<Hex, { current?: Hex; next?: Hex }>, slotHex: Hex, typeInfo: { label: string; numberOfBytes: string }, offsetBytes: bigint, ): { current: DecodedResult["current"]; next?: DecodedResult["next"] } | undefined => { if (!storageDiff[slotHex]) return undefined; const { current: currentHex, next: nextHex } = storageDiff[slotHex]; const current = currentHex ? decodePrimitiveField(typeInfo, currentHex, offsetBytes) : undefined; if (current === undefined || current.decoded === undefined) { logger.error(`Failed to decode primitive field ${typeInfo.label} at slot ${slotHex}`); return undefined; } const next = nextHex ? decodePrimitiveField(typeInfo, nextHex, offsetBytes) : undefined; if (!next || next.decoded === undefined) { return { current }; } else { return { current, next }; } }; /** Decodes a primitive field in a single slot portion, including offset. */ const decodePrimitiveField = <T extends string, Types extends SolcStorageLayoutTypes>( typeInfo: { label: T; numberOfBytes: string }, slotHexData: Hex, offsetBytes: bigint, ): { decoded?: SolidityTypeToTsType<T, Types>; hex: Hex } => { const valType = typeInfo.label; const sizeBytes = Number(typeInfo.numberOfBytes); const isSigned = /^int\d*$/.test(valType); const { extracted, padded } = extractRelevantHex(slotHexData, Number(offsetBytes), sizeBytes); try { // Use the correct decoding approach based on whether the type is signed if (isSigned) { // For signed integers, we need to handle the sign bit properly const value = BigInt(padded); const size = sizeBytes; const max = (1n << (BigInt(size) * 8n - 1n)) - 1n; // If the value is greater than the max positive value, it's negative const decodedValue = value <= max ? value : value - BigInt(`0x${"f".padStart(size * 2, "f")}`) - 1n; return { decoded: decodedValue as SolidityTypeToTsType<T, Types>, hex: extracted }; } else { // For unsigned types, use the standard decoding return { decoded: decodeAbiParameters([{ type: valType }], padded)[0] as SolidityTypeToTsType<T, Types>, hex: extracted, }; } } catch { return { hex: padded }; } }; /** * Extract relevant hex from a hex string based on its offset and length, especially useful for packed variables * * @param {Hex} data - The hex string * @param {number} offset - The offset in bytes from the right where the value starts * @param {number} length - The length in bytes of the value to extract * @returns {Hex} - The extracted hex substring padded to 32 bytes */ const extractRelevantHex = (data: Hex, offset: number, length: number): { extracted: Hex; padded: Hex } => { try { if (!data.startsWith("0x")) data = `0x${data}`; if (data === "0x" || data === "0x00") return { extracted: "0x00", padded: padHex(data, { size: 32 }) }; // Fill up to 32 bytes data = padHex(data, { size: 32, dir: "left" }); // Calculate start and end positions (in hex characters) // Each byte is 2 hex characters, and we need to account for '0x' prefix const totalLength = (data.length - 2) / 2; // Length in bytes (excluding 0x prefix) // Calculate offset from left const offsetFromLeft = totalLength - offset - length; // Calculate character positions const startPos = offsetFromLeft * 2 + 2; // +2 for '0x' prefix const endPos = startPos + length * 2; // Extract the substring and add 0x prefix const extracted = `0x${data.slice(startPos, endPos)}` as Hex; return { extracted, padded: padHex(extracted, { size: 32 }) }; } catch { logger.error(`Failed to extract relevant hex from ${data} at offset ${offset} with length ${length}`); return { extracted: data, padded: padHex(data, { size: 32 }) }; } }; /** * Converts a value to a hex string and ensures it has an even number of characters (full bytes) * * @param value The value to convert to hex * @param options Optional configuration options passed to tevm's toHex * @returns A hex string with full bytes */ export const toHexFullBytes = (value: string | number | bigint | boolean | ByteArray, opts?: ToHexParameters): Hex => { // Use tevm's toHex function const hex = toHex(value, opts); // If the hex string has an odd number of characters after the 0x prefix, // pad it with a leading zero to make it even if ((hex.length - 2) % 2 !== 0) return ("0x0" + hex.slice(2)) as Hex; return hex; };