Чтение Storage напрямую через getStorageAt
Как читать raw storage смарт-контракта через ethers v6: получить storage slot, декодировать uint256, bytes32, address и mapping.
Что такое getStorageAt
getStorageAt — это RPC-вызов eth_getStorageAt, который читает 32 байта из storage slot контракта.
В ethers v6 метод называется provider.getStorage(address, slot). Он возвращает raw hex-значение, а декодировать его нужно вручную.
Читаем storage локального контракта
Создайте проект:
mkdir get-storage-at-example
cd get-storage-at-example
npm init -y
npm pkg set type="module"
npm install ethers solc tsx
npm install --save-dev hardhat typescript @types/node
mkdir contracts scriptsСоздайте файл hardhat.config.ts:
import { defineConfig } from "hardhat/config";
export default defineConfig({
solidity: {
version: "0.8.20",
},
});Создайте файл contracts/StorageDemo.sol:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract StorageDemo {
uint256 public count; // slot 0
bytes32 public label; // slot 1
address public owner; // slot 2
mapping(address => uint256) balances; // slot 3
constructor(bytes32 initialLabel) {
count = 42;
label = initialLabel;
owner = msg.sender;
balances[msg.sender] = 1000;
}
function balanceOf(address account) external view returns (uint256) {
return balances[account];
}
}Создайте файл scripts/read-storage.ts:
import { ethers } from "ethers";
import solc from "solc";
import { readFileSync } from "node:fs";
const source = readFileSync("contracts/StorageDemo.sol", "utf8");
const input = {
language: "Solidity",
sources: {
"StorageDemo.sol": {
content: source,
},
},
settings: {
outputSelection: {
"*": {
"*": ["abi", "evm.bytecode.object"],
},
},
},
};
const output = JSON.parse(solc.compile(JSON.stringify(input)));
const contract = output.contracts["StorageDemo.sol"].StorageDemo;
const abi = contract.abi;
const bytecode = `0x${contract.evm.bytecode.object}`;
const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");
// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const signer = new ethers.Wallet(
"0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
provider,
);
const factory = new ethers.ContractFactory(abi, bytecode, signer);
const storageDemo = await factory.deploy(ethers.encodeBytes32String("demo"));
await storageDemo.waitForDeployment();
const contractAddress = await storageDemo.getAddress();
const rawCount = await provider.getStorage(contractAddress, 0);
const rawLabel = await provider.getStorage(contractAddress, 1);
const rawOwner = await provider.getStorage(contractAddress, 2);
const mappingSlot = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
["address", "uint256"],
[signer.address, 3],
),
);
const rawBalance = await provider.getStorage(contractAddress, mappingSlot);
const count = BigInt(rawCount);
const label = ethers.decodeBytes32String(rawLabel);
const owner = ethers.getAddress(ethers.dataSlice(rawOwner, 12));
const balance = BigInt(rawBalance);
console.log("Contract:", contractAddress);
console.log("slot 0 raw:", rawCount);
console.log("count:", count.toString());
console.log("slot 1 raw:", rawLabel);
console.log("label:", label);
console.log("slot 2 raw:", rawOwner);
console.log("owner:", owner);
console.log("mapping slot:", mappingSlot);
console.log("raw balance:", rawBalance);
console.log("balance:", balance.toString());Запустите локальную сеть Hardhat в отдельном терминале:
npx hardhat nodeВо втором терминале выполните скрипт:
npx tsx scripts/read-storage.tsСкрипт задеплоит StorageDemo, прочитает raw storage slots и декодирует их в понятные значения.
Как это работает
provider.getStorage(contractAddress, 0) — читает slot 0. В нём лежит uint256 count.
BigInt(rawCount) — переводит 32-байтный hex в число.
provider.getStorage(contractAddress, 1) — читает slot 1. В нём лежит bytes32 label.
ethers.decodeBytes32String(rawLabel) — декодирует bytes32 обратно в строку.
provider.getStorage(contractAddress, 2) — читает slot 2. В нём лежит address owner.
ethers.dataSlice(rawOwner, 12) — отрезает первые 12 байт padding и оставляет 20 байт адреса.
provider.getStorage(contractAddress, mappingSlot) — читает значение mapping по вычисленному slot.
Как найти slot mapping
Обычная переменная хранится по номеру slot:
count -> slot 0
label -> slot 1
owner -> slot 2Mapping хранится иначе. Для mapping(address => uint256) balances с базовым slot 3 значение лежит по адресу:
keccak256(abi.encode(key, 3))Для balances[signer.address] код делает то же самое:
const mappingSlot = ethers.keccak256(
ethers.AbiCoder.defaultAbiCoder().encode(
["address", "uint256"],
[signer.address, 3],
),
);В самом slot 3 значение mapping не лежит. Он используется как seed для вычисления storage slot каждого ключа.
getStorage в ethers v6
На уровне JSON-RPC метод называется eth_getStorageAt:
eth_getStorageAt(address, position, blockTag)В ethers v6 это обёрнуто в:
const raw = await provider.getStorage(address, position);Можно передать и blockTag, если RPC поддерживает чтение старого состояния:
const raw = await provider.getStorage(address, position, 19_000_000);Для старых блоков часто нужен archive RPC.
Когда это нужно
Прямое чтение storage используют редко, но оно полезно для диагностики:
- проверить storage layout proxy перед upgrade
- прочитать приватную переменную контракта
- проверить mapping без ABI функции чтения
- сравнить raw State с тем, что возвращает
viewфункция
private в Solidity не означает секретность. Это только запрет доступа из других контрактов. Любой RPC может прочитать storage напрямую.
Частые ошибки
Искать mapping в его базовом slot → в slot 3 не лежит balances[user]. Нужно считать keccak256(abi.encode(key, slot)).
Декодировать address как весь bytes32 → адрес занимает последние 20 байт. Используйте ethers.dataSlice(raw, 12).
Забывать про packing → маленькие типы могут лежать в одном slot. В этой статье типы подобраны так, чтобы slots были простыми.
Читать старый блок через обычный RPC → для исторического storage может понадобиться archive node.
Что дальше
- Взаимодействие с Proxy контрактом — прочитать EIP-1967 implementation slot
- Работа с ABI и Interface — кодировать и декодировать ABI вручную
- Вызов view-функций — read — читать State через обычные функции контракта