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,
});