Examples — evmstate
Skip to content

Examples

This guide covers a few examples of how to use the library.

Monitoring ERC20 balances

example.ts
import { createContract } from "tevm/contract";
import { traceState, type LabeledState } from "@polareth/evmstate";
 
// Create contract helpers
const SimpleDex = createContract({ abi: simpleDexAbi, name: "SimpleDex" });
const ERC20 = createContract({ abi: erc20Abi, name: "ERC20" });
 
const simpleDex = SimpleDex.withAddress("0x111...");
const inputToken = ERC20.withAddress("0x222...");
const outputToken = ERC20.withAddress("0x333...");
const caller = "0x444...";
const amount = 100n;
 
// Approve spending
const { errors } = await client.tevmContract({
  ...inputToken.write.approve(simpleDex.address, amount),
  from: caller,
  addToBlockchain: true,
});
if (errors) throw new Error(`Failed to approve: ${JSON.stringify(errors.map((e) => e.message))}`);
 
// Trace the swap with evmstate
const state = await traceState({
  ...simpleDex.write.swap(inputToken.address, outputToken.address, amount, 0n),
  client,
  from: caller,
  // we can directly provide the storage layouts instead of fetching them
  storageLayouts: {
    [inputToken.address]: erc20Layout,
    [outputToken.address]: erc20Layout,
  },
  fetchStorageLayouts: false,
  // or if we don't provide the storage layouts/abi, we need to provide API keys
  explorers: {
    etherscan: {
      baseUrl: "https://api.etherscan.io/api",
      apiKey: "...",
    },
    blockscout: {
      baseUrl: "https://api.blockscout.com/api",
      apiKey: "...",
    },
  },
});
 
// Get the labeled storage for the input and output tokens
const inputTokenStorage = (state.get(inputToken.address) as LabeledState<typeof erc20Layout> | undefined)?.storage;
const outputTokenStorage = (state.get(outputToken.address) as LabeledState<typeof erc20Layout> | undefined)?.storage;
 
// Get the balance trace for the caller
const inputTokenBalance = inputTokenStorage?.balances.trace.find((t) => t.path[0].key === caller);
const outputTokenBalance = outputTokenStorage?.balances.trace.find((t) => t.path[0].key === caller);
 
// Log the balance changes
console.log(
  `Input token balance change: ${inputTokenBalance?.current?.decoded} -> ${inputTokenBalance?.next?.decoded}`,
);
console.log(
  `Output token balance change: ${outputTokenBalance?.current?.decoded} -> ${outputTokenBalance?.next?.decoded}`,
);

Processing complex outputs

example.ts
import { watchState, PathSegmentKind, type StateChange } from "@polareth/evmstate";
 
export const onStateChange = ({ storage, balance, nonce, code, txHash }: StateChange<typeof layout>) => {
  const { balances, allowances, purchases, userInfo } = storage ?? {};
  console.log(`New transaction accessing contract state: ${txHash}`);
  console.log("Contract state modified:", {
    balance: balance?.modified ?? false,
    nonce: nonce?.modified ?? false,
    code: code?.modified ?? false,
  });
 
  // If balances was accessed
  if (balances) {
    console.log(`Variable balances of type ${balances.type} was accessed.`);
    // for each accessed value
    balances.trace.forEach((trace) => {
      if (trace.modified) {
        console.log(
          `Balance for address ${trace.path[0].key} was modified from ${trace.current?.decoded} to ${trace.next?.decoded}`,
        );
      } else {
        console.log(`Balance for address ${trace.path[0].key} was read: ${trace.current?.decoded}`);
      }
    });
  }
 
  if (allowances) {
    console.log(`Variable allowances of type ${allowances.type} was accessed.`);
 
    allowances.trace.forEach((trace) => {
      if (trace.modified) {
        console.log(
          `Allowance of ${trace.path[1].key} for owner ${trace.path[0].key} was modified from ${trace.current?.decoded} to ${trace.next?.decoded}`,
        );
      } else {
        console.log(
          `Allowance of ${trace.path[1].key} for owner ${trace.path[0].key} was read: ${trace.current?.decoded}`,
        );
      }
    });
  }
 
  if (purchases) {
    console.log(`Variable purchases of type ${purchases.type} was accessed.`);
    purchases.trace.forEach((trace) => {
      // Arrays will output a special trace for their length, then one or multiple traces for each index accessed
      // Here we don't want to process length traces as we'll rather them to examine the actual array access
      if (trace.path[1].kind === PathSegmentKind.ArrayLength) return;
      // this is exactly the same:
      if (trace.fullExpression.endsWith("._length")) return;
 
      const userId = trace.path[0].key;
      const lengthTrace = purchases.trace.find((t) => t.path[0].key === userId);
      // same here:
      // @ts-ignore - '_lengthTrace' is declared but its value is never read.
      const _lengthTrace = purchases.trace.find((t) => t.fullExpression === `purchases[${userId}]._length`);
 
      if (trace.modified) {
        const currentLength = lengthTrace?.current?.decoded ?? 0n;
        const nextLength = lengthTrace?.next?.decoded ?? 0n;
        if (currentLength > nextLength) console.log(`New purchase for user id ${userId}: ${trace.next?.decoded}`);
        if (currentLength < nextLength)
          console.log(`Deleted purchase for user id ${userId}: ${trace.current?.decoded}`);
        if (currentLength === nextLength)
          console.log(`Modified purchase for user id ${userId}: ${trace.current?.decoded} -> ${trace.next?.decoded}`);
      } else {
        console.log(
          `Purchases for user id ${userId} was read at index ${trace.path[1].index}: ${trace.current?.decoded}`,
        );
      }
    });
  }
 
  if (userInfo) {
    console.log(`Variable userInfo of type ${userInfo.type} was accessed.`);
 
    userInfo.trace.forEach((trace) => {
      if (trace.modified) {
        console.log(
          `Field ${trace.path[1].name} of user ${trace.path[0].key} was modified from ${trace.current?.decoded} to ${trace.next?.decoded}`,
        );
      } else {
        console.log(`Field ${trace.path[1].name} of user ${trace.path[0].key} was read: ${trace.current?.decoded}`);
      }
    });
  }
 
  console.log(
    "All slots accessed:",
    Object.fromEntries(
      Object.values(storage ?? {})
        .flatMap(({ trace }) => trace.map((t) => ({ expression: t.fullExpression, slots: t.slots })))
        .map(({ expression, slots }) => [expression, slots]),
    ),
  );
};
 
const unsubscribe = await watchState({
  client,
  address: "0xContractAddress",
  storageLayout: layout,
  abi: abi,
  onStateChange,
});