New to blockchain software development? Read my beginners guide here

How Ethereum storage slots work

Created on July 2022 • Tags: guidesethereum

An explainer showing how the Ethereum storage slots work


Table of Contents for How Ethereum storage slots work


Introduction guide to Slots on the Ethereum EVM

  • In the EVM (Solidity codes compiles to EVM opcodes), here are a couple of types of places that data can be stored - memory and storage.
  • Memory is cheap (to read and write), but is not stored on the block chain.
  • Storage is expensive (to read and write), and is stored on the block chain.
  • Storage is made up of slots.

How the slots are structured

  • There are 2^256 available slots (available for each smart contract)
    • (This is a stupidly huge number)
  • Each slot is 32 byte
  • You can think of it sort of like a key/value mapping. In Javascript, it could be like this:
// (this is a very simplified and quite inaccurate representation!)
const storageData = {
1: slotOneData, // (32 bytes)
2: slotTwoData, // (32 bytes)
// ...
[2 ** 256]: finalSlotData // (32 bytes)
}
  • But the full ‘array’ (it isn’t really an array) of slots isn’t initalized - only the used slots are initalized.
  • If you try to access a slot that wasn’t initalized yet, it would return a 0 value
  • Storage slots with just 0 data (all 32 bytes or 256 bits set to 0) cost nothing to store.

How the ordering of slots is important

  • When you have multiple storage data variables, at compile time your solidity code will put the variables in order into slots.
contract Example {
uint256 firstNum; // slot 1 (total used: 256 bits)
uint256 secondNum; // slot 2 (total used: 256 bits)
uint128 thirdNum; // slot 3 (total used so far: 128 bits)
uint64 fourthNum; // slot 3 (total used so far: 128+64 bits)
uint64 fifthNum; // slot 3 (total used so far: 128+64+64 = 256 bits)
uint8 sixthNum; // slot 4 (total used 8 bits)
uint256 seventhNum // slot 5 (could not fit 256 bits into the remaining 248 bits in slot 4)
}

For this reason, be sure to order them in an optimized way. (See more tips here about optimizing for the EVM)

For example instead of ordering like this:

  • first variable: 128 bytes (slot 1)
  • second variable: 256 bytes (slot 2)
  • third variable 128 bytes (slot 3)

The above would use 3 slots - first holding 128 bytes variable only, second holding the 256 bytes, and the third holding the 128 byte value.

You should instead order them so the two 128 bytes can be stored in the same slot:

  • first variable 256 bytes (slot 1)
  • second variable: 128 bytes (slot 2)
  • third variable: 128 bytes (slot 2)

futher optimizations when packing slots

As well as organising them into an order so they can fit into small number of slots, you should also pack them so that variables that are often accessed together are in the same slot (as you pay for cold slot reads - but if they’re both in the same slot this means you have fewer cold slot reads)

How to get the slot number of a variable with Yul

Note: it is unlikely you will need to do this except in very rare cases. But understanding how you can access the slots will hopefully help understand slots better in general.

Using Yul (Solidity assembly) you can access the slot number quite easily.

In the next example there are 3 variables, a 256 byte uint, and two 32 byte uints.

Note: They are indexed from slot position 0, but generally in documentation you will see the first slot referred to as slot 1.

// SPDX-License-Identifier: GPL-3.0

pragma solidity >=0.8.4;

contract Slots {

uint256 varA;
uint32 varB;
uint32 varC;

constructor() {
varA = 1000;
varB = 2000;
varC = 3000;
}

function slotNumOfVarA() public pure returns (uint256) {
uint slotNum;
assembly {
slotNum := varA.slot
}
return slotNum; // slot 0
}

function slotNumOfVarB() public pure returns (uint256) {
uint slotNum;
assembly {
slotNum := varB.slot
}
return slotNum; // slot 1
}

function slotNumOfVarC() public pure returns (uint256) {
uint slotNum;
assembly {
slotNum := varC.slot
}
return slotNum; // slot 1
}

function getVarAtSlot(uint256 slotNum) view external returns(uint256) {
uint256 slotValue;

assembly {
slotValue := sload(slotNum)
}

return slotValue;
/*
if you call getVarAtSlot(0) it will return 1000
If you call getVarAtSlot(1) it will return 12884901890000
(as both uint32 are stored there, along with 192 of empty data)
*/

}
}

Note: you can use varA.offset to get offsets within a slot

The final function in the example above will use sload(slotNum) to load a slot (256 bytes) from storage. As there are 3 variables, but stored in 2 slots, one of them (slot at index 1 (2nd slot)) returns 12884901890000 as both of the uint32 (total 64 bytes) are stored in that 256 byte slot.

How storage of arrays and mappings work in Solidity

So far i’ve covered simple value types, all under 32 bytes (256 bits). These are pretty straight forward with how they get stored.

But arrays and mappings are a bit more complex when it comes to storage in Solidity.

Fixed size arrays

If you have storage like this:

contract SomeContract {
uint256[2] arrayOfNumbers; // 2x uint256
}

This will be stored the same as if you had two separate uint256s, something like this:

contract SomeContract {
uint256 numberOne;
uint256 numberTwo;
}

But variable length arrays (e.g. uint256[] allYourNumbers) are stored in a different way. They also have the .length property in Solidity, which has to be stored.

There are two main ways that variable length arrays are stored.

when each item in the array is 32 bytes (e.g. uint256[] - 32 bytes): To calculate the location in storage of an item in an array at a specific index (total storage addresses available: 2^256 - so a huge number of keys), you have to do a couple of things:

  • get the slot of the variable
  • keccak256 hash that slot
  • turn that into a uint, and then add the index

e.g.

pragma solidity >=0.8.4;

contract ManuallyAccessArrayExample {
uint256[] yourNumbers = [13, 19];

function atIndex(uint256 index) public view returns(uint256) {
// get the slot number (in this case it will be 0) of the 'yourNumbers' array
uint256 slotNum;

assembly {
slotNum := yourNumbers.slot
}

// get the keccak256 hash of the slot number
bytes32 storageLocation = keccak256(abi.encode(slotNum));

uint valueAtIndex;
assembly {
// add the index (e.g. 0, or 1) to the storageLocation
// then load from storage from that location.
valueAtIndex := sload(add(storageLocation, index))
}

return valueAtIndex;
}
}

When multiple items in an array can be packed together (e.g. uint64[], as we can pack 4x 64 bytes into 256 bytes)

If we have an array that is a size that means multiple elements in the array can be packed together into one storage slot - in other words if 2 or more fit into 256 bytes, then the logic is a bit different.

This has the advantage of saving storage space, but with a tiny bit more logic to get that data out. Of course if you use Solidity, its handled automatically so you don’t often have to think about it.

How mappings are stored/accessed

Mappings are stored in a similar way as arrays (see above). You take a keccak hash of the mapping key and the slot number)

pragma solidity >=0.8.4;

contract ManuallyAccessArrayExample {
mapping(address => uint256) balances;

constructor() payable {
balances[msg.sender] = 55; // some example data so you can query for it
}

function balanceOf(address who) public view returns(uint256) {
// get the slot number of the balances mapping:
uint256 slotNum;
assembly {
slotNum := balances.slot
}

// get storage location, based on the key ('who') and the slot num
bytes32 storageLocation = keccak256(abi.encode(who, uint256(slotNum)));

uint balanceForAddress;
assembly {
// load the 32bytes from that location:
balanceForAddress := sload(storageLocation)
}

return balanceForAddress;
}
}

Nested mappings in Solidity generate the storage location by passing in the keccak hash of the child mapping instead of the uint256(slotNum).

Gas cost of accessing storage data

Loading data from storage is quite simple to work out the gas:

  • SLOAD loads storage data (from a 32 byte key), and returns a 32 byte (256 bit) value.
  • If the accessed address is warm (already accessed in the current transaction), the cost is 100.
  • If it is ‘cold’ (not yet accessed in the transaction) then it costs 2100.
    • BTW: If the value at the key was never written to before, it will load 0

But calculating gas for writing data to storage is a bit more complex

  • SSTORE will store a 32 byte value, at a 32 byte key.
  • If the new value is the same as current value:
    • costs 100 gas
  • If the new value is the same as the original value:
    • If original value was 0 (so we are changing from empty data, to new data) then gas is 20000
    • otherwise (updating existing data) 2900 gas
  • otherwise 100 gas

In addition to the above, if the slot is cold then add another 2100 gas.

If you are writing to storage data, and turning a previously existing data into zero data you can get a gas refund.

Spotted a typo or have a suggestion to make this crypto dev article better? Please let me know!

See all posts (70+ more)

See all posts (70+ more)

Was this post helpful? 📧

If you liked this content and want to receive emails about future posts like this, enter your email. I'll never spam you.

Or follow me on @CryptoGuide_Dev on twitter

By using this site, you agree that you have read and understand its Privacy Policy and Terms of Use.
Use any information on this site at your own risk, I take no responsibility for the accuracy of safety of the information on this site.