Reading the Ethereum Storage
In this topic, we take a look at a coding example to understand the storage layout. For that purpose, we have a smart contract that defines many different variables such as units, strings, mappings, structs, and arrays.
pragma solidity 0.8.20;
contract ReadStorageContract{
// # 1 single variable uint
uint256 firstVar = 100;
// # 2 single variable string
string secondVar = "Hello World";
// #3 a constant
string constant myConst = "I am a constant";
// #4 two variables in one slot
uint128 thirdVar = 5555;
uint32 fourthVar = 1000000;
// #5 array
uint32[20] numberArray = [10,15,20,30,40,50,60,70,100,200,300,400,500,600,700,800,900,1000,2000,3000];
// #6 dynamic size array
uint256[] dynamicArray;
// #7 Struct
PersonStruct myPerson;
struct PersonStruct{
string name;
uint256 age;
}
// #8 mapping
mapping(uint256 => uint256) myMapping;
constructor(){
myMapping[10] = 12345;
myMapping[11] = 1234567890;
myPerson.name = "Alice";
myPerson.age = 25;
dynamicArray.push(1234);
dynamicArray.push(5678);
}
}
To read the storage, we want to use JavaScript. For this language, the convenience library web3.js exists.
Necessary steps for preparation:
- Install nodeJs
- Install web3.js
- Get an account with Infura
- Deploy the smart contract from above on the Goerli testnet
The code below shows the statements necessary to read the contract storage. We will go through every important line in detail.
const Web3 = require("web3");
var web3 = new Web3("wss://goerli.infura.io/ws/v3/YOUR_API_KEY_HERE"); // use a websocket connection
var contractAddress = "YOUR CONTRACT ADDRESS HERE"; //
getStorageValues();
async function getStorageValues(){
// #1 single variable uint
web3.eth.getStorageAt(contractAddress, 0).then(function(res){
console.log("firstVar:");
console.log(res);
console.log(parseInt(res, 16));
});
// # 2 single variable string
web3.eth.getStorageAt(contractAddress, 1).then(function(res){
console.log("secondVar:");
console.log(res);
console.log(web3.utils.toAscii(res));
});
// #3 a constant
// Constants are stored in the contract code
// #4 two variables in one slot
web3.eth.getStorageAt(contractAddress, 2).then(function(res){
console.log("thirdVar and fourth var:");
console.log(res);
});
// #5 staitc array
web3.eth.getStorageAt(contractAddress, 3).then(function(res){
console.log("Index 3: numberArray:");
console.log(res);
});
web3.eth.getStorageAt(contractAddress, 4).then(function(res){
console.log("Index 4: numberArray");
console.log(res);
});
web3.eth.getStorageAt(contractAddress, 5).then(function(res){
console.log("Index 5 numberArray:");
console.log(res);
});
// #6 dynamic array
resDynamicArray = await web3.eth.getStorageAt(contractAddress, 6);
console.log("Index: 6 dynamic Array");
console.log(resDynamicArray);
newIndex = web3.utils.soliditySha3(6); // Use soliditySha3 instead of SHA3 because they are different
console.log("new index: " + newIndex);
// Showing the first value of the dynamic array
storage01 = await web3.eth.getStorageAt(contractAddress, newIndex);
console.log("Storage01: " + storage01);
// Showing the second value of the dynamic array
console.log(newIndex);
newIndexPlus1 = BigInt(newIndex) + 1n; // Increasing the index by one
var hexIndexPlusOne = BigInt(newIndexPlus1).toString(16); // converting BigInt back to hex
if (hexIndexPlusOne.length % 2) { // get the padding right
hexIndexPlusOne = '0' + hexIndexPlusOne;
}
hexIndexPlusOne = '0x' + hexIndexPlusOne; // add the hex prefix
console.log(hexIndexPlusOne)
storage02 = await web3.eth.getStorageAt(contractAddress, hexIndexPlusOne); // look up the storage slot
console.log("Storage02: " + storage02);
// #7 Struct: Each member of a struct has its own continous index
web3.eth.getStorageAt(contractAddress, 7).then(function(res){
console.log("Index 7 Struct" + res);
console.log(web3.utils.toAscii(res));
});
web3.eth.getStorageAt(contractAddress, 8).then(function(res){
console.log(res);
console.log("Index 8 Struct" + parseInt(res, 16));
});
// #8 Mapping
// In order to read a mapping, you need the index and the key of the element
newKey =web3.utils.soliditySha3(10, 9); // Use soliditySha3 instead of SHA3 because they are different / key . index
console.log("Mapping: Key 10");
web3.eth.getStorageAt(contractAddress, newKey).then(function(res){
console.log(res);
console.log(parseInt(res, 16));
});
newKey =web3.utils.soliditySha3(11, 9); // Use soliditySha3 instead of SHA3 because they are different / key . index
console.log("Mapping: Key 11");
web3.eth.getStorageAt(contractAddress, newKey).then(function(res){
console.log(res);
console.log(parseInt(res, 16));
});
}
Lines 11 – 15: with the function getStorageAt(), we can access the storage at the contract address and the index. Our first variable has the index 0. From the solidity source code, we know that it is an integer variable. That’s why we parse from hex to decimal in line 14.
Lines 18 – 22: It is very similar to the previous example. But here, the data type is string. That’s why we parse it to string and display it on the console.
Line 24: In our contract, we have a constant. Constant values are part of the contract code and not part of the storage.
Lines 28 – 31: Here, two variables are packed into a single slot. On the right side, you see the hex value 15b3 (=5555) and in the middle you see the hex value f424, which stands for 10000.
Lines 35 – 48: Here, we have a static array. Since an array doesn’t fit into a single slot, it takes up more slots. Each new slot has a continuous index. You can see that the static array starts at index three and stretches until index fice. Values are again packed like in the previous example.
Lines 52 – 74: Here, we have a dynamic array. The difference is that they assign the starting index by hashing the normal index.
The normal index only contains the size of the dynamic array (=number of elements).
The hash of the index is no our new index. If the dynamic array stretches over more than one slot, the new index is increased by one to access the next slot.
Lines 78 – 86: Here, we deal with a struct. Structs are stored similar to static arrays. Each member of a struct has its own continuous slot.
Lines 91 – 103: Mappings are also a bit special. The index is calculated by the key and the index (in this order). In our case, the keys are 10 and 11. So, the value for the first key is retrieved by hashing the key concatenated to the index of the mapping. This is our new key.
- If you want to try it yourself, create a folder paste the JavaScript code into a file
- Save the file with the name readStorage_01.js
- Start it with node readStorage_01.js