Solidity storage usage

5 minute read

In this post, I will show how Solidity administrates the Ethereum Virtual Machine (EVM) storage. Storage operations are extremely costly so good knowledge is required to optimize gas usage.

Ethereum Virtual Machine storage

EVM Storage

Storage on the EVM could be seen as an array of length 2^256 with each item having a size of 32 bytes.

A contract running on the EVM has extremely large storage space available. It could be seen as an array of length 2^256 with each slot having a size of 32 bytes (256 bits).

Initially, all of this space is blank. For the contract developer, it does not cost any gas to have all of this blank space available. However, when you start using this space and filling it with non 0 values, the EVM starts spending gas.

Storage usage is extremely costly on the EVM. According to the Yellow Paper these are the gas costs of saving and loading from storage alongside common operations:

Operation Description Gas Cost
ADD Addition operation 3
MUL Addition operation 5
AND Bitwise AND operation 3
MLOAD Load word from memory 3
MSTORE Store word to memory 5
SLOAD Load word from storage 2000
SSTORE Store word to storage 20000

As you can see in the last two rows, loading and saving to storage is orders of magnitude higher than other typical operations like arithmetic operations, boolean operations, or loading and saving to memory.

Also, gas refunds are given when you destroy a contract using the SELFDESTRUCT operation or when you set a storage space to 0. This further incentivizes to use storage space as carefully as possible.

Sidenote: the amount of gas that can be refunded is capped to prevent abuses of this system.

Solitidy storage usage

Static variables

Now we will look into how Solidity uses all of the EVM available space when you develop a contract.

Statically-sized variables (everything but mappings or dynamic arrays) are stored sequentially in memory, starting from position 0x0.

Typically each variable uses a 32-byte slot. In case contiguous variables are smaller than 32 bytes Solidity will try to pack them into a slot. This follows some rules:

contract Storage {
  uint128 var1 = 1;  // 0x0 lower end
  uint128 var2 = 2;  // 0x0 higher end
  uint128 var3 = 3;  // 0x1
}

Here var1 and var2 will be packed into the slot 0x0, while var3 will be stored at the slot 0x1.

Variables packing work by putting the first variable at the lowest position of the 32 bytes. In this case, the slot 0x0 will contain var2 in the lowest 128 bits and var3 in the highest 128 bits: Packed Storage


contract Storage {
  uint192 var1 = 1;  // 0x0
  uint128 var2 = 2;  // 0x1 lower end
  uint128 var3 = 3;  // 0x1 higher end
}

Here var2 could not be packed with var1 as their combined size is bigger than 256 bits. var1 will be stored at slot 0x0. var2 and var3 will be packed into the slot 0x1


contract Storage {
  uint192 var1 = 1;  // 0x0
  uint192 var2 = 2;  // 0x1
  uint128 var3 = 3;  // 0x2
}

Here var1, var2, and var3 will be each placed in a different slot as no packing is allowed due to size limitations.


Static arrays work similarly, trying to pack lower than 256-bit variables together. However, it is not allowed to pack an array variable with another variable.

contract Storage {
  uint128[3] array = [1, 2, 3];  // 0x0 - 0x1
  uint128 var1 = 4;  // 0x2
}

Here the variable array will take 2 storage slots, the slot 0x0 will contain the numbers 1 and 2 and the slot 0x1 will contain the number 3. Then, in slot 0x2 the variable var1 will be placed.

Structs also try to pack their variables together and, like arrays, prevent their variables from being packed with external variables.

Dinamic-size variables

Mappings and dynamically-sized arrays follow a different behavior. The storage slots of their items are calculated using the Keccak-256 hash function.

Dynamically-sized arrays

Arrays locate their data at the position keccak256(p) with p being the slot where the array is declared.

contract Storage {
  uint256 var1 = 4;  // 0x0
  uint256[] array;  // 0x1 Will contain the array size
}

In the previous code, the array data will start at the position keccak256(0x1) as 0x1 is the location of the array declaration. Also, it’s important to note that the position 0x1 will contain the current array size.

Mappings

Mappings work in a similar way as dynamic arrays. They locate their data at the position keccak256(k . p) with p the slot where the array is declared, k the mapping key used, and . the concatenation operation.

In contrast to dynamic arrays, the slot p where the mapping is declared is not used for saving data. However, it is necessary so two different mappings that share keys have a different hash distribution.

Bytes and strings

Bytes and strings use a particular encoding. If the bytes data/string is at most 31 bytes, Solidity will save this data into a single slot. The data will be placed in the higher-order bytes and use the lowest byte to save its length multiplied by 2.

Let’s see an example, what happens when we declare the string “hello world!”:

String encoding

We have the highest bytes with the string content and the lowest byte with its length multiplied by 2, in this case, 12.

If the data is 32 bytes or longer, then the slot p where the array was declared will store length * 2 + 1. The bytes/string will be stored at keccack256(p).

The use of length * 2 for data smaller than 32 bytes and length * 2 + 1 for data larger or equal than 32 bytes allows identifying a short bytes array/string from a long bytes array/string just by looking at the lowest bit. If it is a 0 will be short, if it is a 1 will be long.

In conclusion, for a bytes/string declared at slot p:

Bytes/string type Size Data location Data length location Last bit of p
Short At most 31 bytes Highest 31 bytes of p (left aligned) Lowest byte of p (length * 2) 0
Long 32 bytes or more At keccak256(p) At p (length * 2 + 1) 1

Further reading

For further information about these topics I recommend the following lectures:

https://certora.com/blog/corruptedMemory.html https://docs.soliditylang.org/en/v0.8.11/internals/layout_in_storage.html

Updated:

Comments