# evmstate
> A TypeScript library for tracing, and visualizing EVM state changes with detailed human-readable labeling.
## @polareth/evmstate
A TypeScript library for tracing and visualizing EVM state changes with detailed human-readable labeling.
### Overview
[@polareth/evmstate](https://www.npmjs.com/package/@polareth/evmstate) traces all state changes after a transaction execution in a local VM, or by watching transactions in incoming blocks. It retrieves and labels storage slots with semantic insights and provides a detailed diff of all changes.
As an alternative to event logs for tracking state, it captures and labels every state change with precise semantic information:
* Variable names
* Mapping keys
* Array indices
* Decoded values
* Path tracing from base slots to final values
Powered by [Tevm](https://github.com/evmts/tevm-monorepo) and [whatsabi](https://github.com/shazow/whatsabi).
### Installation
```bash
npm install @polareth/evmstate
# or
pnpm add @polareth/evmstate
# or
yarn add @polareth/evmstate
```
### Quick start
:::code-group
```ts twoslash [example.ts]
// [!include ~/snippets/abi.ts:mappings]
// [!include ~/snippets/layout.ts:mappings]
// [!include ~/snippets/client.ts]
// ---cut---
import { traceState, watchState } from "@polareth/evmstate";
// Trace a transaction
// [!include ~/snippets/trace-state.ts:example-featured]
// Watch an account's state
// [!include ~/snippets/watch-state.ts:watchState]
```
```ts twoslash [layout.ts]
// [!include ~/snippets/layout.ts:mappings]
```
```ts twoslash [abi.ts]
// [!include ~/snippets/abi.ts:mappings]
```
```ts twoslash [client.ts]
// [!include ~/snippets/client.ts]
```
:::
### Key features
* **Complete state tracing**: Track all storage slots accessed during transaction execution
* **Human-readable labeling**: Label slots with variable names and provide access paths
* **Intelligent key detection**: Extract mapping keys and array indices from transaction data
* **Type-aware decoding**: Convert raw storage values to appropriate TypeScript types with automatic type inference
### Example output
```typescript
Map {
"0xContractAddress" => {
"balance": {
"current": 0n,
"modified": false,
},
"nonce": {
"current": 0n,
"modified": false,
},
...
"storage": {
"counter": {
"kind": "primitive",
"name": "counter",
"type": "uint256",
"trace": [
{
"current": { "hex": "0x05", "decoded": 5n },
"modified": true,
"next": { "hex": "0x06", "decoded": 6n },
"fullExpression": "counter",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000000"]
}
]
}
}
},
"0xCallerAddress" => {
"balance": {
"current": 1000000000000000000n,
"modified": true,
"next": 999999998747698854n,
},
"nonce": {
"current": 0n,
"modified": true,
"next": 1n,
},
...
},
}
```
You can get the result for any address (case-insensitive):
```ts twoslash
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/client.ts]
// [!include ~/snippets/abi.ts]
// [!include ~/snippets/trace-state.ts:example-featured]
// ---cut---
const result = trace.get("0xContractAddress");
```
### Documentation
* [Installation](/guides/installation)
* [Basic usage](/guides/basic-usage)
* [Understanding output formats](/reference/output-format)
import { Callout } from "vocs/components";
import { Playground } from "../components/playground/index.tsx";
## Playground
This interactive playground demonstrates how different Solidity storage types are traced and visualized.
*The client is shared and persisted across sessions (the state of the chain and traces will persist everytime you visit the playground).*
### Usage Guide
1. **Select a function** from the dropdown menu - each function is labeled with a description of what storage type it affects
2. **Enter parameters** if required by the selected function
3. **Click "Execute"** to run the function
4. **View the trace** in the collapsible code block right below
5. **Explore the history** of all executed functions and their traces
Try different functions to see how various storage patterns are represented in the trace output!
### Interactive example
### Reference contract
```solidity
// [!include ~/../test/contracts/Playground.s.sol]
```
## API Overview
@polareth/evmstate provides three main APIs for analyzing Ethereum state changes:
### traceState
Main function for tracing state changes from a transaction.
```typescript twoslash
import type { TraceStateOptions, TraceStateResult } from "@polareth/evmstate";
// @ts-expect-error - Function implementation is missing
function traceState(options: TraceStateOptions): Promise;
```
Use `traceState` when you need to analyze a single transaction and don't need to reuse configuration.
[API reference →](/api/trace-state)
### Tracer
Class for creating reusable tracing instances with shared configuration.
```typescript twoslash
import type { TraceStateBaseOptions, TraceStateTxParams } from "@polareth/evmstate";
class Tracer {
// @ts-expect-error - Constructor implementation is missing
constructor(options: TraceStateBaseOptions);
// @ts-expect-error - Function implementation is missing
traceState(txOptions: TraceStateTxParams): Promise;
}
```
Use `Tracer` when you need to perform multiple traces with the same configuration.
[API reference →](/api/tracer)
### watchState
Function for monitoring state changes for a specific address.
```typescript twoslash
import type { WatchStateOptions, DeepReadonly, SolcStorageLayout } from "@polareth/evmstate";
// @ts-expect-error - Function implementation is missing
function watchState | undefined = undefined>(
options: WatchStateOptions
): Promise<() => void>;
```
Use `watchState` to continuously monitor an address for state changes across multiple blocks.
[API reference →](/api/watch-state)
## traceState
Analyzes storage access patterns during transaction execution, identifying which contract slots are read and modified, and providing human-readable labels.
### Signature
```typescript twoslash
import type { Abi, ContractFunctionName } from "tevm";
import type { TraceStateOptions, TraceStateResult } from "@polareth/evmstate";
// @ts-expect-error - Function implementation is missing
function traceState<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends ContractFunctionName = ContractFunctionName,
>(options: TraceStateOptions): Promise;
```
### Parameters
The function accepts a single options object with these properties:
#### Connection options
| Property | Type | Description |
| ----------- | -------------------------- | ----------------------------------------------------------- |
| `rpcUrl` | `string` | Ethereum RPC URL (needs `debug_traceTransaction`) |
| `client` | `MemoryClient` | Optional Tevm client instance |
| `explorers` | `Record` | Optional (recommended) configuration for contract explorers |
#### Transaction options
Three ways to specify the transaction to trace:
##### 1. Raw transaction data
| Property | Type | Description |
| -------- | ---------------------- | ------------------------------------ |
| `from` | `Address` | Sender address |
| `to` | `Address \| undefined` | Recipient address or omit for deploy |
| `data` | `Hex` | Transaction calldata |
| `value` | `bigint \| undefined` | Optional ETH amount to send |
##### 2. Contract ABI call
| Property | Type | Description |
| -------------- | --------------------- | --------------------------- |
| `from` | `Address` | Sender address |
| `to` | `Address` | Contract address |
| `abi` | `TAbi` | Contract ABI |
| `functionName` | `TFunctionName` | Function name to call |
| `args` | `unknown[]` | Function arguments |
| `value` | `bigint \| undefined` | Optional ETH amount to send |
##### 3. Existing transaction
| Property | Type | Description |
| -------- | ----- | ------------------------- |
| `txHash` | `Hex` | Transaction hash to trace |
### Return value
Returns a promise that resolves to a record mapping account addresses to their state changes:
```typescript twoslash
import type { TraceStateResult } from "@polareth/evmstate";
type ReturnType = TraceStateResult;
```
See the [output format reference](/reference/output-format) for details on the structure.
### Examples
#### Simulating a transaction
```typescript twoslash
import { traceState } from "@polareth/evmstate";
const trace = await traceState({
rpcUrl: "https://1.rpc.thirdweb.com",
from: "0xSenderAddress",
to: "0xContractAddress",
data: "0xEncodedCalldata",
value: 0n,
});
const state = trace.get("0xTokenAddress");
console.log(state?.storage);
```
#### Using a contract ABI
:::code-group
```typescript twoslash [example.ts]
// [!include ~/snippets/abi.ts:erc20]
// ---cut---
import { traceState } from "@polareth/evmstate";
const trace = await traceState({
rpcUrl: "https://1.rpc.thirdweb.com",
from: "0xSenderAddress",
to: "0xTokenAddress",
abi: erc20Abi,
functionName: "transfer",
args: ["0xRecipient", 1000000000000000000n],
});
```
```typescript twoslash [abi.ts]
// [!include ~/snippets/abi.ts:erc20]
```
:::
#### Tracing an existing transaction
```typescript twoslash
import { traceState } from "@polareth/evmstate";
const trace = await traceState({
rpcUrl: "https://1.rpc.thirdweb.com",
txHash: "0xTransactionHash",
});
```
#### Using a custom Tevm client
```typescript twoslash
// ---cut---
import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { traceState } from "@polareth/evmstate";
const client = createMemoryClient({
common: mainnet,
fork: {
transport: http("https://1.rpc.thirdweb.com"),
blockTag: "latest",
},
});
const trace = await traceState({
client,
from: "0xSenderAddress",
to: "0xContractAddress",
data: "0xEncodedCalldata",
});
```
## Tracer
A class that encapsulates the storage access tracing functionality in an object-oriented interface, allowing for reusable tracers with consistent configuration.
### Signature
```typescript twoslash
import type { Abi, ContractFunctionName } from "tevm";
import type { TraceStateBaseOptions, TraceStateTxParams, TraceStateResult } from "@polareth/evmstate";
class Tracer {
// @ts-expect-error - Constructor implementation is missing
constructor(options: TraceStateBaseOptions);
// @ts-expect-error - Function implementation is missing
traceState<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends ContractFunctionName = ContractFunctionName,
>(txOptions: TraceStateTxParams): Promise;
}
```
### Constructor
```typescript twoslash
import type { TraceStateBaseOptions } from "@polareth/evmstate";
class Tracer {
// @ts-expect-error - Constructor implementation is missing
constructor(options: TraceStateBaseOptions);
}
```
Creates a new `Tracer` instance with shared configuration.
#### Parameters
| Property | Type | Description |
| ----------- | --------------------------------------- | ----------------------------------------- |
| `rpcUrl` | `string \| undefined` | Ethereum RPC endpoint URL |
| `client` | `MemoryClient \| undefined` | Optional Tevm client instance |
| `explorers` | `Record \| undefined` | Optional contract explorers configuration |
You must provide either `rpcUrl` or `client`.
### Methods
#### traceState
```typescript twoslash
import type { Abi, ContractFunctionName } from "tevm";
import type { TraceStateTxParams, TraceStateResult } from "@polareth/evmstate";
class Tracer {
// @ts-expect-error - Function implementation is missing
traceState<
TAbi extends Abi | readonly unknown[] = Abi,
TFunctionName extends ContractFunctionName = ContractFunctionName,
>(txOptions: TraceStateTxParams): Promise;
}
```
Traces the storage access patterns for a transaction, using the shared configuration from the constructor.
##### Parameters
| Property | Type | Description |
| -------------- | ---------------------------- | --------------------------- |
| `from` | `Address` | The sender address |
| `to` | `Address \| undefined` | The recipient address |
| `data` | `Hex \| undefined` | The transaction calldata |
| `value` | `bigint \| undefined` | Optional ETH amount to send |
| `abi` | `TAbi \| undefined` | Optional contract ABI |
| `functionName` | `TFunctionName \| undefined` | Optional function name |
| `args` | `unknown[] \| undefined` | Optional function arguments |
Just like the standalone `traceState` function, the `Tracer.traceState` method either takes encoded calldata or an ABI function call.
##### Return value
Returns a promise that resolves to a record mapping account addresses to their state changes:
```typescript twoslash
import type { TraceStateResult } from "@polareth/evmstate";
type ReturnType = TraceStateResult;
```
See the [output format reference](/reference/output-format) for details on the structure.
### Examples
#### Basic usage
```typescript twoslash
import { Tracer } from "@polareth/evmstate";
// Create tracer with shared configuration
const tracer = new Tracer({
rpcUrl: "https://1.rpc.thirdweb.com",
});
// Trace multiple transactions
const trace1 = await tracer.traceState({
from: "0xSenderAddress",
to: "0xContractAddress",
data: "0xCalldata1",
});
const trace2 = await tracer.traceState({
from: "0xSenderAddress",
to: "0xContractAddress",
data: "0xCalldata2",
});
```
#### With custom Tevm client
```typescript twoslash
import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { Tracer } from "@polareth/evmstate";
// Create custom Tevm client
const client = createMemoryClient({
common: mainnet,
fork: {
transport: http("https://1.rpc.thirdweb.com"),
blockTag: "latest",
},
});
// Create tracer with custom client
const tracer = new Tracer({ client });
// Trace a transaction
const trace = await tracer.traceState({
from: "0xSenderAddress",
to: "0xContractAddress",
data: "0xCalldata",
value: 1000000000000000000n, // 1 ETH
});
```
#### With contract ABI
:::code-group
```typescript twoslash [example.ts]
// [!include ~/snippets/abi.ts:erc20]
// ---cut---
import { Tracer } from "@polareth/evmstate";
// Create tracer
const tracer = new Tracer({
rpcUrl: "https://1.rpc.thirdweb.com",
});
// Trace an ERC-20 transfer
const transferTrace = await tracer.traceState({
from: "0xSenderAddress",
to: "0xTokenAddress",
abi: erc20Abi,
functionName: "transfer",
args: ["0xRecipient", 1000000000000000000n], // recipient, amount
});
```
```typescript twoslash [abi.ts]
// [!include ~/snippets/abi.ts:erc20]
```
:::
#### Tracing an existing transaction
```typescript twoslash
import { Tracer } from "@polareth/evmstate";
const tracer = new Tracer({
rpcUrl: "https://1.rpc.thirdweb.com",
});
const trace = await tracer.traceState({
txHash: "0x1234567890abcdef...",
});
```
## watchState
Monitors state changes for a specific Ethereum address by watching new blocks and tracing transactions. It provides notifications when the address's state is accessed or modified.
### Signature
```typescript twoslash
import type { Address } from "tevm";
import type { WatchStateOptions, StateChange, SolcStorageLayout, DeepReadonly } from "@polareth/evmstate";
// @ts-expect-error - Function implementation is missing
function watchState | undefined = undefined>(
options: WatchStateOptions,
): Promise<() => void>;
```
### Parameters
The function accepts a single options object with these properties:
| Property | Type | Description |
| ----------------- | ---------------------------------------------------- | ----------------------------------------------- |
| `address` | `Address` | The Ethereum address to monitor |
| `onStateChange` | `(stateChange: StateChange) => void` | Callback for state changes |
| `onError` | `(error: Error) => void \| undefined` | Optional error handling callback |
| `pollingInterval` | `number \| undefined` | Optional polling interval in ms (default: 1000) |
| `storageLayout` | `TStorageLayout \| undefined` | Optional contract storage layout |
| `abi` | `Abi \| undefined` | Optional contract ABI |
| `rpcUrl` | `string \| undefined` | Ethereum RPC endpoint URL |
| `client` | `MemoryClient \| undefined` | Optional Tevm client instance |
| `explorers` | `Record \| undefined` | Optional contract explorers configuration |
### Return value
Returns a promise that resolves to an unsubscribe function:
```typescript twoslash
type Unsubscribe = () => void;
```
Call this function to stop watching for state changes.
### Type parameters
```typescript twoslash
import type { SolcStorageLayout, DeepReadonly } from "@polareth/evmstate";
type TStorageLayout = /* extends */ DeepReadonly | undefined;
```
By providing a storage layout with the `as const` assertion, TypeScript will infer precise types for all state change properties, enhancing the developer experience with autocompletion and type checking.
### Examples
#### Basic usage
```typescript twoslash
import { watchState } from "@polareth/evmstate";
// Start watching an address
const unsubscribe = await watchState({
rpcUrl: "https://1.rpc.thirdweb.com",
address: "0xContractAddress",
onStateChange: (stateChange) => {
console.log("Transaction:", stateChange.txHash);
// Check balance changes
if (stateChange.balance?.modified) {
console.log("Balance changed:", {
from: stateChange.balance.current,
to: stateChange.balance.next,
});
}
// Print storage changes
if (stateChange.storage) {
console.log("Storage accessed:", Object.keys(stateChange.storage));
}
},
onError: (error) => {
console.error("Watch error:", error);
},
});
// Later, stop watching
unsubscribe();
```
#### With storage layout and ABI
:::code-group
```typescript twoslash [example.ts]
// [!include ~/snippets/abi.ts:erc20]
// [!include ~/snippets/layout.ts:erc20]
// ---cut---
import { watchState } from "@polareth/evmstate";
// Watch with enhanced typing
const unsubscribe = await watchState({
rpcUrl: "https://1.rpc.thirdweb.com",
address: "0xTokenAddress",
storageLayout: erc20Layout, // Type assertion with as const
abi: erc20Abi,
onStateChange: (stateChange) => {
// TypeScript knows the exact structure of stateChange.storage
// Access totalSupply (if changed)
if (stateChange.storage?.totalSupply) {
const totalSupply = stateChange.storage.totalSupply.trace[0];
if (totalSupply.modified) {
console.log("Total supply changed:", {
from: totalSupply.current?.decoded,
to: totalSupply.next?.decoded,
});
}
}
// Access balances mapping
if (stateChange.storage?.balances) {
const balances = stateChange.storage.balances.trace;
balances.forEach((entry) => {
if (entry.modified) {
// TypeScript knows this is an address key
const address = entry.path[0].key;
console.log(`Balance changed for ${address}:`, {
from: entry.current?.decoded,
to: entry.next?.decoded,
});
}
});
}
},
});
```
```typescript twoslash [layout.ts]
// [!include ~/snippets/layout.ts:erc20]
```
```typescript twoslash [abi.ts]
// [!include ~/snippets/abi.ts:erc20]
```
:::
#### With custom Tevm client
```typescript twoslash
import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { watchState } from "@polareth/evmstate";
// Create custom client
const client = createMemoryClient({
common: mainnet,
fork: {
transport: http("https://1.rpc.thirdweb.com"),
blockTag: "latest",
},
});
// Watch with custom client
const unsubscribe = await watchState({
client,
address: "0xContractAddress",
onStateChange: (stateChange) => {
console.log("State changed:", stateChange);
},
});
```
## Basic usage
This guide covers the fundamental ways to use the library to analyze and label state access patterns.
### Core functions
The library provides three main entry points:
1. `traceState`: Analyze state changes from a single transaction
2. `Tracer`: Create a reusable instance for multiple traces
3. `watchState`: Monitor ongoing state changes for a specific address
### Tracing a transaction
There are three ways to trace a transaction:
1. Using transaction calldata
2. Using a contract ABI
3. Using a transaction hash
:::code-group
```typescript twoslash [calldata.ts]
// [!include ~/snippets/client.ts]
// ---cut---
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/trace-state.ts:calldata]
```
```typescript twoslash [abi.ts]
// [!include ~/snippets/client.ts]
// [!include ~/snippets/abi.ts:mappings]
// ---cut---
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/trace-state.ts:abi]
```
```typescript twoslash [txHash.ts]
// [!include ~/snippets/client.ts]
// ---cut---
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/trace-state.ts:txHash]
```
:::
### Tracing options
There are various ways to configure the client:
* Using an existing Tevm client (likely in forking mode)
* Using a RPC url (which will internally create a Tevm memory client)
See [the Tevm documentation](https://www.tevm.sh/) for [how to create and configure the client](https://node.tevm.sh/core/create-tevm-node).
:::code-group
```typescript twoslash [tevm-client.ts]
import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/client-fork.ts:client-fork]
// [!include ~/snippets/trace-state.ts:options-client]
```
```typescript twoslash [rpc-url.ts]
// [!include ~/snippets/client.ts]
// ---cut---
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/trace-state.ts:options-rpc]
```
:::
Finally, you will need to provide an url & API key for at least one supported explorer, so it can fetch ABIs and storage layouts:
```typescript twoslash [explorers.ts]
// [!include ~/snippets/client.ts]
// ---cut---
import { traceState } from "@polareth/evmstate";
// [!include ~/snippets/trace-state.ts:options-explorers]
```
### Watching state changes
To monitor an address for state changes in real-time:
:::code-group
```typescript twoslash [example.ts]
// [!include ~/snippets/layout.ts:mappings]
// [!include ~/snippets/abi.ts:mappings]
// ---cut---
import { watchState } from "@polareth/evmstate";
// [!include ~/snippets/watch-state.ts:watchState]
```
```typescript twoslash [layout.ts]
// [!include ~/snippets/layout.ts:mappings]
```
```typescript twoslash [abi.ts]
// [!include ~/snippets/abi.ts:mappings]
```
:::
### Next steps
* [Example usage](/guides/usage-examples)
* [Understanding output format](/reference/output-format)
## Installation
### Installing the package
Install @polareth/evmstate using your preferred package manager:
```bash
# Using npm
npm install @polareth/evmstate
# Using pnpm
pnpm add @polareth/evmstate
# Using yarn
yarn add @polareth/evmstate
```
### RPC provider requirements
@polareth/evmstate needs an Ethereum RPC provider that supports the `debug` namespace methods:
* `debug_traceTransaction`: For analyzing single transactions
* `debug_traceBlock`: For watching state changes across blocks
Compatible RPC providers include:
* Self-hosted nodes with `debug` namespace enabled (Geth, Erigon)
* Some specialized services like Alchemy or QuickNode (may require higher tier plans)
### Example initialization
:::code-group
```ts twoslash [example.ts]
// [!include ~/snippets/abi.ts:mappings]
// [!include ~/snippets/client.ts]
// ---cut---
import { traceState } from "@polareth/evmstate";
// Basic usage with abi
// [!include ~/snippets/trace-state.ts:example-featured]
```
```ts twoslash [abi.ts]
// [!include ~/snippets/abi.ts:mappings]
```
```ts twoslash [client.ts]
// [!include ~/snippets/client.ts]
```
:::
### Next steps
* [Basic usage examples](/guides/basic-usage)
* [Understanding the output format](/reference/output-format)
import { Callout } from "vocs/components";
## Examples
This guide covers a few examples of how to use the library.
See the [playground](/playground) for an interactive demonstration of the output based on function calls.
### Monitoring ERC20 balances
:::code-group
```ts twoslash [example.ts]
// [!include ~/snippets/client-fork.ts]
// [!include ~/snippets/abi.ts]
// [!include ~/snippets/layout.ts]
// ---cut---
import { createContract } from "tevm/contract";
import { traceState, type LabeledState } from "@polareth/evmstate";
// [!include ~/snippets/erc20.ts:args]
// [!include ~/snippets/erc20.ts:approve]
// [!include ~/snippets/erc20.ts:trace-swap]
// [!include ~/snippets/erc20.ts:trace-parse]
```
```ts twoslash [client.ts]
import { createMemoryClient, http } from "tevm";
import { mainnet } from "tevm/common";
// [!include ~/snippets/client-fork.ts:client-fork-no-highlight]
```
```ts twoslash [abis.ts]
// [!include ~/snippets/abi.ts:erc20]
// [!include ~/snippets/abi.ts:simple-dex]
```
```ts twoslash [layout.ts]
// [!include ~/snippets/layout.ts:erc20]
```
:::
### Processing complex outputs
:::code-group
```ts twoslash [example.ts]
// [!include ~/snippets/abi.ts:mappings]
// [!include ~/snippets/layout.ts:mappings]
// [!include ~/snippets/client.ts]
// ---cut---
import { watchState, PathSegmentKind, type StateChange } from "@polareth/evmstate";
// [!include ~/snippets/watch-state.ts:watchState-logs-onStateChange]
// [!include ~/snippets/watch-state.ts:watchState-logs-subscribe /unsubscribe2/unsubscribe/]
```
```ts twoslash [layout.ts]
// [!include ~/snippets/layout.ts:mappings]
```
```ts twoslash [abi.ts]
// [!include ~/snippets/abi.ts:mappings]
```
```ts twoslash [client.ts]
// [!include ~/snippets/client.ts]
```
:::
## Output format
This reference documents the structure of data returned by the library's tracing functions.
### Basic structure
For `traceState` and `Tracer.traceState`, the output maps account addresses to state changes:
```typescript twoslash
import type { TraceStateResult, LabeledState } from "@polareth/evmstate";
import type { Address } from "tevm";
type TraceOutput = TraceStateResult;
// which is equivalent to:
type TraceOutputMap = Map; // `get`, `has`, `set` all normalize the address to lowercase
```
For `watchState`, the output focuses on a single address and includes the transaction hash:
```typescript twoslash
import type { Hex } from "tevm";
import type { LabeledState } from "@polareth/evmstate";
type WatchOutput = LabeledState & { txHash: Hex };
```
### Account state
Each account in the output has these properties:
```typescript twoslash
import type { Hex } from "tevm";
import type { LabeledStorageState } from "@polareth/evmstate";
type LabeledState = {
// Intrinsic properties
balance: {
current: bigint,
next?: bigint,
modified: boolean
},
nonce: {
current: number,
next?: number,
modified: boolean
},
code: {
current: Hex,
next?: Hex,
modified: boolean
},
// Storage state
storage: {
[variableName: string]: LabeledStorageState
}
}
```
### Storage variables
The `storage` field contains labeled variables with detailed traces:
```typescript twoslash
import type { LabeledStorageStateTrace } from "@polareth/evmstate";
type LabeledStorageState = {
"variableName": {
name: string,
type?: string,
kind?: "primitive" | "mapping" | "dynamic_array" | "static_array" | "struct" | "bytes",
trace: Array
}
}
```
Each `LabeledStorageStateTrace` represents a specific storage access:
```typescript twoslash
import type { Hex } from "tevm";
import type { PathSegment } from "@polareth/evmstate";
type LabeledStorageStateTrace = {
slots: Array, // Storage slots accessed
path: Array, // Semantic path components
fullExpression: string, // Human-readable expression (e.g., "balances[0x1234]")
current?: {
hex: Hex,
decoded: any // typed if storage layout is provided
},
next?: {
hex: Hex,
decoded: any // typed if storage layout is provided
},
modified: boolean,
note?: string // any exception/error during exploration
}
```
### Proxy contracts
Proxy contracts are labeled using the implementation contract's ABI and storage layout when possible:
:::code-group
```typescript [output.json]
// [!include ~/snippets/outputs/proxy.jsonc]
```
```typescript [trace.ts]
// [!include ~/snippets/proxy.ts:proxy-trace]
```
:::
### Next steps
* [Storage types](/reference/storage-types)
* [Playground](/playground)
## Storage types
This reference explains how the library handles different Solidity storage types.
### Value types
Simple values stored directly in slots.
#### Integers (uint/int)
```solidity twoslash
uint256 public counter = 0;
```
```typescript
"counter": {
"kind": "primitive",
"name": "counter",
"type": "uint256",
"trace": [
{
"current": { "hex": "0x00", "decoded": 0n },
"modified": true,
"next": { "hex": "0x01", "decoded": 1n },
"path": [],
"fullExpression": "counter",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000000"]
}
]
}
```
#### Booleans
```solidity
bool public flag = false;
```
```typescript
"flag": {
"kind": "primitive",
"name": "flag",
"type": "bool",
"trace": [
{
"current": { "hex": "0x00", "decoded": false },
"modified": true,
"next": { "hex": "0x01", "decoded": true },
"path": [],
"fullExpression": "flag",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000001"]
}
]
}
```
#### Addresses
```solidity
address public owner = address(0);
```
```typescript
"owner": {
"kind": "primitive",
"name": "owner",
"type": "address",
"trace": [
{
"current": {
"hex": "0x0000000000000000000000000000000000000000",
"decoded": "0x0000000000000000000000000000000000000000"
},
"modified": true,
"next": {
"hex": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"decoded": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
},
"path": [],
"fullExpression": "owner",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000002"]
}
]
}
```
### Mappings
Mappings compute slots based on keys and the base slot.
#### Simple mapping
```solidity
mapping(address => uint256) public balances;
```
```typescript
"balances": {
"kind": "mapping",
"name": "balances",
"type": "mapping(address => uint256)",
"trace": [
{
"current": { "hex": "0x00", "decoded": 0n },
"modified": true,
"next": { "hex": "0x2386f26fc10000", "decoded": 10000000000000000n },
"path": [
{
"kind": "mapping_key",
"key": "0x1234567890123456789012345678901234567890",
"keyType": "address"
}
],
"fullExpression": "balances[0x1234567890123456789012345678901234567890]",
"slots": ["0x8e9c0c9f9fb928592f2fb0a9314450706c27839d034893b88d8ed2f54cf1bd5e"]
}
]
}
```
#### Nested mapping
```solidity
mapping(address => mapping(address => uint256)) public allowances;
```
```typescript
"allowances": {
"kind": "mapping",
"name": "allowances",
"type": "mapping(address => mapping(address => uint256))",
"trace": [
{
"current": { "hex": "0x00", "decoded": 0n },
"modified": true,
"next": { "hex": "0x0de0b6b3a7640000", "decoded": 1000000000000000000n },
"path": [
{
"kind": "mapping_key",
"key": "0x1234567890123456789012345678901234567890",
"keyType": "address"
},
{
"kind": "mapping_key",
"key": "0x5678901234567890123456789012345678901234",
"keyType": "address"
}
],
"fullExpression": "allowances[0x1234...][0x5678...]",
"slots": ["0x6a70ff112c49166454439c4e0a5f7db9e9a4ac61f326f232ac9eb1a9b05f4eef"]
}
]
}
```
### Arrays
Arrays store length at the base slot and elements at computed slots.
#### Dynamic arrays
```solidity
uint256[] public numbers;
```
```typescript
"numbers": {
"kind": "dynamic_array",
"name": "numbers",
"type": "uint256[]",
"trace": [
{
// Array length
"current": { "hex": "0x02", "decoded": 2n },
"modified": true,
"next": { "hex": "0x03", "decoded": 3n },
"path": [
{ "kind": "array_length", "name": "_length" }
],
"fullExpression": "numbers._length",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000006"]
},
{
// Element access
"current": { "hex": "0x00", "decoded": 0n },
"modified": true,
"next": { "hex": "0x2a", "decoded": 42n },
"path": [
{ "kind": "array_index", "index": 2n }
],
"fullExpression": "numbers[2]",
"slots": ["0x5de13444fe158c7b5525d0d208535a5f84ca2f75ce5219b9c55fb55643beb57c"]
}
]
}
```
#### Fixed-size arrays
```solidity
uint256[3] public fixedNumbers;
```
```typescript
"fixedNumbers": {
"kind": "static_array",
"name": "fixedNumbers",
"type": "uint256[3]",
"trace": [
{
"current": { "hex": "0x00", "decoded": 0n },
"modified": true,
"next": { "hex": "0x2a", "decoded": 42n },
"path": [
{ "kind": "array_index", "index": 1n }
],
"fullExpression": "fixedNumbers[1]",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000008"]
}
]
}
```
### Structs
Structs organize fields sequentially in storage.
```solidity
struct User {
uint256 id;
address wallet;
bool active;
}
User public admin;
```
```typescript
"admin": {
"kind": "struct",
"name": "admin",
"type": "struct Contract.User",
"trace": [
{
"current": { "hex": "0x00", "decoded": 0n },
"modified": true,
"next": { "hex": "0x01", "decoded": 1n },
"path": [
{ "kind": "struct_field", "name": "id" }
],
"fullExpression": "admin.id",
"slots": ["0x000000000000000000000000000000000000000000000000000000000000000a"]
},
{
"current": {
"hex": "0x0000000000000000000000000000000000000000",
"decoded": "0x0000000000000000000000000000000000000000"
},
"modified": true,
"next": {
"hex": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4",
"decoded": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
},
"path": [
{ "kind": "struct_field", "name": "wallet" }
],
"fullExpression": "admin.wallet",
"slots": ["0x000000000000000000000000000000000000000000000000000000000000000b"]
}
]
}
```
### Strings and bytes
Strings and bytes can be stored inline (short) or across multiple slots (long).
#### Short strings/bytes
```solidity
string public shortString = "short string";
```
```typescript
"shortString": {
"kind": "bytes",
"name": "shortString",
"type": "string",
"trace": [
{
"current": {
"decoded": 12n,
"hex": "0x0c",
},
"fullExpression": "shortString._length",
"modified": false,
"path": [
{
"kind": "bytes_length",
"name": "_length",
},
],
"slots": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
],
},
{
"current": {
"decoded": "short string",
"hex": "0x73686f727420737472696e67",
},
"fullExpression": "shortString",
"modified": false,
"path": [],
"slots": [
"0x0000000000000000000000000000000000000000000000000000000000000000",
],
},
],
}
```
#### Long strings/bytes
```solidity
bytes public longBytes = "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd";
```
```typescript
"longBytes": {
"kind": "bytes",
"name": "longBytes",
"type": "bytes",
"trace": [
{
"current": {
"decoded": 60n,
"hex": "0x3c",
},
"fullExpression": "longBytes._length",
"modified": false,
"path": [
{
"kind": "bytes_length",
"name": "_length",
},
],
"slots": [
"0x0000000000000000000000000000000000000000000000000000000000000001",
],
},
{
"current": {
"decoded": "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
"hex": "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd",
},
"fullExpression": "longBytes",
"modified": false,
"path": [],
"slots": [
"0x0000000000000000000000000000000000000000000000000000000000000001",
"0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6",
"0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf7",
],
},
],
}
```
### Storage packing
Multiple small variables can be packed into a single slot:
```solidity
uint8 public value1; // 1 byte
uint16 public value2; // 2 bytes
bool public flag; // 1 byte
// All packed into slot 0
```
```typescript
"value1": {
"kind": "primitive",
"name": "value1",
"type": "uint8",
"trace": [
{
"current": { "hex": "0x00", "decoded": 0 },
"modified": true,
"next": { "hex": "0x2a", "decoded": 42 },
"fullExpression": "value1",
"slots": ["0x0000000000000000000000000000000000000000000000000000000000000000"],
"offset": 0,
"size": 1
}
]
}
```