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.
Tip #8: Cache storage values in memory
Anytime you are reading from storage more than once, it is cheaper to cache variables in memory. An SLOAD cost 100 GAS while MLOAD and MSTORE only cost 3 GAS.
This is especially true in for loops when using the length of a storage array as the condition being checked after each loop. Caching the array length in memory can yield significant gas savings when the array length is high.
Tip #9: Cache external values in memory
When making repeated external calls when the values between calls do not change, cache the values:
Tip #10: > 0 is less efficient than != 0 for unsigned integers
!= 0 costs 6 less GAS compared to
> 0 for unsigned integers in require statements with the optimizer enabled. Learn more.
Tip #11: Splitting require() statements that use && saves gas
Instead of using the
&& operator in a single require statement to check multiple conditions,using multiple require statements with 1 condition per require statement will save 3 GAS per
Tip #12: ++i costs less gas compared to i++ or i += 1
++i costs less gas compared to
i += 1 for unsigned integers. This is because the pre-increment operation is cheaper (about 5 GAS per iteration).
i and returns the initial value of
i. This means:
In the first example, the compiler has to create a temporary variable (when used) for returning 1 instead of 2.
++i returns the actual incremented value:
This tip is applicable for loops as well.
Tip #13: Shorten require messages to less than 32 characters
Strings that are more than 32 characters will require more than 1 storage slot, costing more gas. Consider reducing the message length to less than 32 characters or use error codes:
Tip #14: Use Custom Errors instead of Revert Strings to save Gas
Consider replacing revert strings with custom errors. Custom errors from Solidity 0.8.4 are cheaper than revert strings (cheaper deployment cost and runtime cost when the revert condition is met.)
Starting from Solidity v0.8.4, there is a convenient and gas-efficient way to explain to users why an operation failed through the use of custom errors. Until now, you could already use strings to give more information about failures (e.g., revert(“Insufficient funds.”);), but they are rather expensive, especially when it comes to deploy cost, and it is difficult to use dynamic information in them.
Tip #15: Usage of uint8 may increase gas cost
When using elements that are smaller than 32 bytes, your contract’s gas usage may be higher. This is because the EVM operates on 32 bytes at a time. Therefore, if the element is smaller than that, the EVM must use more operations in order to reduce the size of the element from 32 bytes to the desired size. The EVM needs to properly enforce the limits of this smaller type.
It is only beneficial to use reduced-size arguments if you are dealing with storage values because the compiler will pack multiple elements into one storage slot, and thus, combine multiple reads or writes into a single operation. When dealing with function arguments or memory values, there is no inherent benefit because the compiler does not pack these values.
Using smaller-size uints such as
uint8 is only more efficient when you can pack variables into the same storage slot, such as in structs. In other cases and in loops,
uint256 is more gas efficient than
Tip #16: Non-strict inequalities are cheaper than strict ones
In the EVM, there is no opcode for non-strict inequalities (
<=) and two operations are performed (
=.) Consider replacing
>= with the strict counterpart
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 efficiency and readability.
📬 Get updates straight to your inbox.
Subscribe to my newsletter so you don't miss new content.