Skip to main content

Layout of State Variables in Storage​

Every smart contract running in the Ethereum Virtual Machine (EVM) maintains state in its own permanent storage. You can picture the storage as a very large array. Smart contract storage have 2^256 32bytes slots.

  • Data is stored contiguously item after item starting with the first state variable, which is stored in slot 0.
  • For each variable, a size in bytes is determined according to its type.
  • If two consecutive variable can fit in one slot they are packed together from right to left.

storage

Locating Fixed-Sized Values​

Variables with known fixed sizes just use reserved locations in storage. These slots are determined at compile time, strictly based on the order in which the variables appear in the contract code.

contract StorageTest {    uint256 a;    uint256[2] b;    struct Entry {        uint256 id;        uint256 value;    }    Entry c;}

In the above code:

  • a is stored at slot 0.
  • b is stored at slots 1, and 2 (one for each element of the array).
  • c starts at slot 3 and consumes two slots, because the Entry struct stores two 32-byte values.

storage fixed

Locating Dynamically-Sized Values​

Using reserved slots works well for fixed-size state variables, but it doesn’t work for dynamically-sized arrays and mappings because there’s no way of knowing how many slots to reserve.

Due to the shere amount of locations available, 2^256 slots, we can choose storage locations at random without ever experiencing a collision. Solidity uses a hash function to uniformly and repeatably compute locations for dynamically-sized values.

A dynamically-sized array needs a place to store its size as well as its elements.

contract StorageTest {    uint256 a;     // slot 0    uint256[2] b;  // slots 1-2    struct Entry {        uint256 id;        uint256 value;    }    Entry c;       // slots 3-4    Entry[] d;}

In the above code, the dynamically-sized array d is at slot 5, but the only thing that’s stored there is the size of d. The values in the array are stored consecutively starting at the hash (keccak256 hash function) of the slot.

storage dynamic

The following Solidity function computes the location of an element of a dynamically-sized array:

function arrLocation(uint256 slot, uint256 index, uint256 elementSize)    public    pure    returns (uint256){    return uint256(keccak256(slot)) + (index * elementSize);}

Mappings​

contract StorageTest {    uint256 a;     // slot 0    uint256[2] b;  // slots 1-2    struct Entry {        uint256 id;        uint256 value;    }    Entry c;       // slots 3-4    Entry[] d;     // slot 5 for length, keccak256(5)+ for data    mapping(uint256 => uint256) e;    mapping(uint256 => uint256) f;}

In the above code, the location for e is slot 6, and the location for f is slot 7, but nothing is actually stored at those locations. There is no length to be stored.

To find the location of a specific value within a mapping, the key and the mapping's slot are hashed together.

The following Solidity function computes the location of a value:

function mapLocation(uint256 slot, uint256 key) public pure returns (uint256) {    return uint256(keccak256(key, slot));}

Storage mapping

Combinations of Complex Types​

Dynamically-sized arrays and mappings can be nested within each other recursively. When that happens, the location of a value is found by recursively applying the calculations defined above.

contract StorageTest {    uint256 a;     // slot 0    uint256[2] b;  // slots 1-2    struct Entry {        uint256 id;        uint256 value;    }    Entry c;       // slots 3-4    Entry[] d;     // slot 5 for length, keccak256(5)+ for data    mapping(uint256 => uint256) e;    // slot 6, data at h(k . 6)    mapping(uint256 => uint256) f;    // slot 7, data at h(k . 7)    mapping(uint256 => uint256[]) g;  // slot 8    mapping(uint256 => uint256)[] h;  // slot 9}

To find items within these complex types, we can use the functions defined above.

To find g[123][0]:

// first find arr = g[123]
arrLoc = mapLocation(8, 123); // g is at slot 8

// then find arr[0]
itemLoc = arrLocation(arrLoc, 0, 1);

To find h[2][456]:

// first find map = h[2]
mapLoc = arrLocation(9, 2, 1); // h is at slot 9

// then find map[456]
itemLoc = mapLocation(mapLoc, 456);

You can use cast command that comes with foundry to inspect the storage of a contract:

cast storage  -r https://eth-rpc.gateway.pokt.network 0x6D2299C48a8dD07a872FDd0F8233924872Ad1071 1
cast storage -r https://eth-rpc.gateway.pokt.network 0x6D2299C48a8dD07a872FDd0F8233924872Ad1071 0x0758364a4f55624097844647de7675b993ad2ed16003efa84065140c0c0b48ae

cast storage dump

Dedaub provides an awesome toolkit to check the storage of a contract. Example: https://library.dedaub.com/contracts/Ethereum/6D2299C48A8DD07A872FDD0F8233924872AD1071/storage-dump

Dedaub Library

Inheritance​

In a contract that inherits, the assignment works as follows:

  • first, the slots are allocated to the inherited variables, from the leftmost contract to the rightmost
  • finishing with the current contract.
contract Hello {    string public hello = "Hello!";}contract World {    string public world = "World!";}contract HelloWorld is Hello, World {    string private greet = "Hello World!";    function getGreeting() public view returns(string memory) {        return greet;    }    function getHello() public view returns(string memory) {        return hello;    }    function getWorld() public view returns(string memory) {        return world;    }}
  • hello gets assigned slot 0
  • world gets assigned slot 1
  • greet gets assigned slot 2

delegatecall​

delegatecall recap
  • The function delegatecall can be used similar to call: the main difference is that only the code of the given address is used, all other aspects (storage, balance, …) are taken from the current contract.
  • The purpose of delegatecall is to use library code which is stored in another contract.
  • The user has to ensure that the layout of storage in both contracts is suitable for delegatecall to be used.

Challenge:

library.sol
// SPDX-License-Identifier: MITpragma solidity ^0.6.0;// Simple library contract to set the timecontract LibraryContract {  // stores a timestamp   uint storedTime;    function setTime(uint _time) public {    storedTime = _time;  }}
Preservation.sol
// SPDX-License-Identifier: MITpragma solidity ^0.6.0;contract Preservation {  // public library contracts   address public timeZone1Library;  address public timeZone2Library;  address public owner;   uint storedTime;  // Sets the function signature for delegatecall  bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)"));  constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) public {    timeZone1Library = _timeZone1LibraryAddress;     timeZone2Library = _timeZone2LibraryAddress;     owner = msg.sender;  }   // set the time for timezone 1  function setFirstTime(uint _timeStamp) public {    timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));  }  // set the time for timezone 2  function setSecondTime(uint _timeStamp) public {    timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp));  }}

Memory layout of Preservation:

  • slot0 : timeZone1Library
  • slot1 : timeZone2Library
  • slot2 : owner
  • slot3 : storedTime

Exploit: Overwrite timeZone1Library variable with our own Exploit contract address; and call setTime on our Exploit contract address, that just modifies owner variable

Excercise: Try to solve the following Ethernaut challenges:

Table of direct types​

TypeSize in storage (bytes)Padding in padded locationsDefault valueIs key type?Allowed in calldata?Allowed as immutable?
bool1Zero padded, leftfalseYesYesYes
uintNN/8Zero-padded, left*0YesYesYes
intNN/8Sign-padded, left*0YesYesYes
address [payable]20Zero-padded, left*Zero address (not valid!)YesYesYes
contract types20Zero-padded, left*Zero address (not valid!)YesYesYes
bytesNNZero-padded, right*All zeroesYesYesYes
enum typesAs many as needed to hold all possibilitiesZero-padded, leftWhichever possibility is represented by 0YesYesYes
function internal8Zero-padded, leftDepends on location, but always invalidNoNoYes
function external24Zero-padded, right, except on stackZero address, zero selector (not valid!)NoYesNo
ufixedMxNM/8Zero-padded, left*0YesYesYes
fixedMxNM/8Sign-padded, left*0YesYesYes
User-defined value typesSame as underlying type (except in 0.8.8)Same as underlying type*Same as underlying typeYesYesYes

References​