Reading the Ethereum Storage

Published by Mario Oettler on

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
Categories: