Computations on Ethereum cost gas. If you’re not careful, you may end up writing contracts that cost more than they should. High gas costs kill usability!
A common cause for high gas costs is an inefficient storage layout in Solidity contracts. In this post, let’s look at some tips you can use to help you write more gas-efficient contracts.
Read on to learn more!
Storage Layout in Solidity
When state variables of Solidity contracts are stored in
storage, they are stored in a compact way such that multiple values sometimes use the same storage slot. Except for dynamically-sized arrays and mappings, data is stored contiguously as 32-byte words, item after item starting with the first state variable, which is stored in slot 0.
The size of each variable is determined by its type. Here is a summary of Solidity value types and their size in bytes:
Here’s an example of how a contract’s storage might be laid out:
In Example #2, multiple variables of value types may share a storage slot, within which each variable only takes up as much space as it need to.
Any items with a size under 32 bytes are packed into a single storage slot if possible, according to the following rules:
Value types use only as many bytes as are necessary to store them.
If a value type does not fit the remaining part of a storage slot, it is stored in the next storage slot.
Structs and array data always start a new slot, but their items are packed tightly according to these rules.
Items following struct or array data always start a new storage slot.
In short, optimizing storage layout in Solidity is like trying to pack items into the least number of boxes. The less boxes we need, the better.
Inspecting Storage Layout
Before we proceed, we need a way to inspect what our contracts’ storage layout looks like.
The storage layout of a contract can be viewed in the Standard JSON interface files which comes from the Solidity compiler. The output file is a JSON object containing two keys,
If you’re using Hardhat, you can find these files in the
artifacts/build-info/directory that’s generated when you run
Let’s inspect these two fields more closely. We’ll inspect the storage layout of the following Solidity contract:
storage field for the above contract is an array where each element has the following form:
storage element contain the following fields:
astIdis the id of the AST node of the state variable’s declaration
contractis the name of the contract including its path as prefix
labelis the name of the state variable
offsetis the offset in bytes within the storage slot according to the encoding
slotis the storage slot where the state variable resides or starts. This number may be very large and therefore its JSON value is represented as a string.
typeis an identifier used as key to the variable’s type information (described in the following)
In addition to the
storage, the JSON file contains
types. In the following example,
t_uint256 represents an element in
types, which has the form:
You can inspect the
types fields to understand how much space each type consumes and how they are packed. Here’s a larger example:
In the above file, we can see that:
addresstakes up 20 bytes
booleantakes up 1 byte
uint96takes up 12 bytes
- A struct
MyInfooccupies 64 bytes, and has 3 variables packed inside it:
address accounttakes up 20 bytes, in
uint96 balancetakes up 12 bytes, starting from
offset20 and still in
bool alivetakes up 1 byte in
slot1, because slot 0 is already full at 32 bytes
You can generate and use this report to inspect how your contracts’ storage is laid out. Try it out with some dummy contracts!
Armed with this knowledge, let’s learn some tips to optimize our Solidity contracts!
Tip #1: Variable packing
Reading and writing from each storage slot cost gas. Packing variables lets us reduce the number of slots our contract needs.
To allow the EVM to optimize your storage layout, make sure to order your storage variables and struct members such that they can be packed tightly. For example, instead of declaring your storage variables in the order of:
you can do:
The first approach results in no variable packing. It takes up three storage slots.
In comparison, the second approach benefits from variable packing and takes only two slots.
Here’s another variable packing example:
In short, group Solidity variables into chunks of 32-byte words to optimize gas usage.
uint256 can be cheaper than
When using types that are smaller than 32 bytes such as
uint8, your contract’s gas usage may in some cases be higher than if you use
This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM has to do more operations to convert the element between 32 bytes and the desired size.
It might be beneficial to use smaller-size types if you can get the compiler to pack multiple types into one storage slot (see the previous section on Variable packing), and thus, combine multiple reads or writes into a single operation.
However, if you are not reading or writing all the values at the same time, this can have the opposite effect. When defining a lone variable, it can be more optimal to use a
uint256 rather than
Tip #3: Use
bytesN instead of
The value types
bytes32 hold a sequence of bytes from one to up to 32. You should use
bytes1 because it is cheaper, since
bytes1 adds 31 padding bytes between the elements. Due to padding rules, this wastes 31 bytes of space for each element.
If you can limit the length to a certain number of bytes, always use one of the value types
bytesN because they are more gas efficient.
Tip #4: Use fixed-size
bytes32 instead of
Fitting your data in fixed-size 32 byte words is much cheaper than using arbitrary-length types. Remember that
bytes32 uses less gas because it fits in a single EVM word. Basically, Any fixed size variable in solidity is cheaper than dynamically sized ones.
Tip #5: Write to
storage variables with the final results of your computation, after all intermediate calculations instead of at each step. This is because operations on
storage is more expensive than operations on
Let’s look an example contract:
In the sample code above, function
B() is more gas-efficient than
A() because it does less storage writes. Use local variables to store your intermediate results, and write to
Here is a cost comparison:
SSTORE(saving a 32-byte word to storage): 22100 gas when storage value is set to non-zero from zero, OR 5000 gas when the storage value’s zeroness remains unchanged or is set to zero. This assumes first-time access.
SLOAD(loading a word from storage): 2100 gas on first-time access, 100 gas if already accessed
MSTORE(saving a word to memory): 3 gas
MLOAD(loading a word from memory): 3 gas
As you can see, the difference is potentially massive. Minimize writes to
storage and do them last. Note that are numbers are post-Berlin upgrade numbers (EIP 2929.)
You might also notice that ‘cold’ reads on the EVM are more expensive than ‘warm’ reads. This is yet another reason for miniziming the number of storage slots and variable packing (Tip #1.)
Tip #6: Free up unused storage
You get a gas refund when you ‘empty out’ variables. Deleting has the same effect as reassigning the value type with the default value, such as the zero address for addresses and 0 for integers.
Deleting a variable refunds 15,000 gas up to a maximum of half the gas cost of the transaction.
Tip #7: Use
immutable type annotations to variables:
Reading constant and immutable variables is no longer considered a storage read (
SSTORE) because it uses
EXTCODE* opcodes instead which is much cheaper.
In this post, we learned:
- How storage is laid out in Solidity contracts
- How to inspect the storage layout of your contracts
- Some techniques to optimize storage layout and access
Keep in mind that storage optimization techniques can run counter to code readability. When in doubt, be sure to aim for a balance between code efficiency and readability.
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.