
Solidity Storage Layout and Gas Efficiency: Designing State Variables for Lower Costs
Why storage layout matters
In Solidity, contract state lives in storage, which is persistent and expensive to access. Compared with memory or calldata, storage operations dominate gas costs in many real-world contracts, especially those that:
- update balances or positions frequently
- maintain user profiles, permissions, or configuration flags
- store large arrays or mappings
- execute many writes in a single transaction
A well-designed storage layout reduces gas in two ways:
- Fewer storage slots used
- Fewer expensive storage operations
Smaller deployment bytecode and lower state footprint.
Better packing means fewer SSTORE and SLOAD operations.
The key idea is simple: if multiple values can fit into one 32-byte storage slot, Solidity can pack them together. But packing only helps when the types and ordering are chosen carefully.
How Solidity packs storage variables
Solidity stores state variables sequentially, slot by slot. Each slot is 32 bytes. Variables smaller than 32 bytes can share a slot if they fit and are declared in the right order.
Packing rules in practice
- Variables are packed from left to right within a slot.
- Types are aligned according to their size.
- A variable that does not fit in the remaining space starts a new slot.
- Structs and arrays have their own layout rules, but their members can also be packed.
Example: poor ordering vs efficient ordering
// Less efficient layout
contract BadLayout {
uint256 public totalSupply; // slot 0
bool public paused; // slot 1
uint128 public cap; // slot 2
address public owner; // slot 3
}This layout wastes space because each variable starts a new slot due to ordering and size mismatch.
A better arrangement:
// More efficient layout
contract GoodLayout {
address public owner; // 20 bytes
bool public paused; // 1 byte
uint128 public cap; // 16 bytes -> does not fit in remaining 11 bytes, so new slot
uint256 public totalSupply; // slot 2
}This is still not optimal because address + bool leaves only 11 bytes, which is not enough for uint128. To pack more effectively, group compatible smaller types together:
contract BetterLayout {
address public owner; // 20 bytes
bool public paused; // 1 byte
uint8 public decimals; // 1 byte
uint16 public feeBps; // 2 bytes
uint256 public totalSupply; // new slot
}Now the first four variables fit into a single slot, leaving the uint256 in its own slot.
Ordering state variables for maximum packing
The most practical rule is:
Declare larger types first only when they cannot be packed with smaller ones; otherwise, group smaller types together to fill slots efficiently.
In real projects, the best layout usually comes from sorting variables by size and usage, then checking the final slot allocation.
Recommended ordering strategy
- Group frequently updated variables together only if they can pack.
- Place smaller types next to each other:
booluint8uint16addressbytes4
- Keep
uint256,bytes32, and mappings in separate slots as needed. - Avoid interleaving small and large types in a way that fragments slots.
Example: configuration contract
Suppose you have a contract with admin settings:
contract Config {
address public treasury;
uint256 public maxDeposit;
bool public depositsEnabled;
uint16 public depositFeeBps;
uint8 public version;
}A more storage-efficient version is:
contract ConfigOptimized {
address public treasury; // 20 bytes
bool public depositsEnabled; // 1 byte
uint8 public version; // 1 byte
uint16 public depositFeeBps; // 2 bytes
uint256 public maxDeposit; // new slot
}This reduces slot fragmentation and makes the contract cheaper to deploy and slightly cheaper to read.
When packing helps and when it does not
Packing is not always a win. It can reduce slot usage, but it may also introduce extra bit manipulation under the hood when you update only one packed variable. That tradeoff matters.
Packing is useful when:
- values are mostly read together
- values are rarely updated independently
- the contract stores many instances of the same struct
- deployment and long-term storage footprint matter
Packing can be less useful when:
- one field changes frequently and others rarely change
- the packed variables are accessed in separate transactions
- the code becomes harder to maintain for a small gas gain
Practical rule
If a packed slot contains one frequently updated field and several stable fields, each update may require a read-modify-write cycle on the whole slot. That still often costs less than using separate slots, but the benefit is smaller than many developers expect.
For example, a bool paused flag packed with several configuration fields is usually fine because it changes rarely. But packing a rapidly changing counter with unrelated values may not be ideal.
Struct design: pack members intentionally
Structs are a common place to gain storage efficiency because they are often used for repeated records such as orders, positions, or user profiles.
Inefficient struct
struct Position {
uint256 collateral;
bool isOpen;
address trader;
uint128 debt;
}This layout is likely to spread across multiple slots in a suboptimal way.
Better struct
struct Position {
address trader; // 20 bytes
bool isOpen; // 1 byte
uint128 debt; // 16 bytes -> new slot
uint256 collateral; // new slot
}Even better, if your values can be represented more compactly, use smaller types deliberately:
struct Position {
address trader; // 20 bytes
uint8 status; // 1 byte
uint96 debt; // 12 bytes
uint128 collateral; // 16 bytes
}This kind of design is especially useful in protocols with large arrays of positions, where every saved slot multiplies across many users.
Comparing common layout choices
| Pattern | Gas impact | Best use case | Tradeoff |
|---|---|---|---|
Many uint256 fields | Higher storage footprint | Simple accounting variables | Easy to read, but wasteful for small values |
| Packed small types | Lower storage footprint | Flags, settings, compact records | Slightly more complex updates |
| Mixed large and small types | Often inefficient | Rarely ideal | Can fragment slots |
| Structs with intentional packing | Good for repeated records | Positions, orders, profiles | Requires careful type selection |
This table is not a substitute for profiling, but it helps identify where layout decisions matter most.
Avoiding hidden layout costs in mappings and arrays
Mappings and dynamic arrays have special storage behavior. You cannot pack a mapping itself into another variable, but the value type stored inside a mapping or array can still be packed.
Mapping values
mapping(address => UserData) public users;
struct UserData {
bool active;
uint8 tier;
uint96 points;
}If UserData is packed well, every mapping entry uses fewer slots.
Dynamic arrays
For arrays of structs, each element follows the struct layout rules. That means a compact struct can save a lot of gas when you append many entries.
However, be careful with arrays of small types like bool[] or uint8[]. Solidity stores array elements in storage slots, but access patterns can be awkward and may not always be worth the complexity. In many cases, a packed struct or a uint256 bitmask is easier to manage.
Using bitmasks for dense flags
When you have many boolean flags, a bitmask can be more efficient than storing each flag as a separate bool.
Example: role flags
contract Roles {
uint256 private flags;
uint256 private constant FLAG_PAUSED = 1 << 0;
uint256 private constant FLAG_MINTING = 1 << 1;
uint256 private constant FLAG_BURNING = 1 << 2;
function setPaused(bool value) external {
if (value) {
flags |= FLAG_PAUSED;
} else {
flags &= ~FLAG_PAUSED;
}
}
function paused() external view returns (bool) {
return flags & FLAG_PAUSED != 0;
}
}Why this helps
- one storage slot can hold up to 256 flags
- reads are cheap once the slot is loaded
- updates are compact and predictable
When to use it
Bitmasks are best when:
- you have many binary settings
- flags are queried often
- you want a compact permission model
Bitmasks are less ideal when:
- the codebase is maintained by many developers unfamiliar with bit operations
- each flag needs independent metadata
- readability is more important than maximum compactness
Storage layout and upgradeable contracts
Storage efficiency must be balanced with upgrade safety. In upgradeable contracts, changing variable order can break storage compatibility and corrupt state.
Important rule for upgradeable systems
Never reorder, remove, or change the type of existing storage variables in an upgradeable contract.
That means:
- you cannot “optimize” layout by rearranging old variables
- you should reserve gaps for future variables
- you should plan packing early
Example of safe planning
contract VaultStorage {
address public owner;
bool public paused;
uint16 public feeBps;
uint256 public totalAssets;
uint256[45] private __gap;
}The __gap reserves space for future variables without shifting existing storage slots.
For upgradeable contracts, the best time to optimize layout is at design time, before deployment. After deployment, compatibility matters more than perfect packing.
Measuring whether a layout change is worth it
Storage optimization should be measured, not guessed. A packing change may save deployment gas and a small amount of runtime gas, but the improvement depends on access patterns.
What to benchmark
- deployment cost
- gas for common read functions
- gas for common write functions
- gas for batch operations
- gas across repeated updates to packed fields
Typical outcomes
- Deployment: fewer slots usually means lower deployment cost.
- Reads: packed variables can be slightly cheaper if they are accessed together.
- Writes: updating one packed field may require extra masking, but still often beats separate slots.
- Complexity: overly clever packing can reduce maintainability.
Use a benchmarking tool such as Foundry or Hardhat gas reports to compare versions before and after a layout change.
Best practices for storage-efficient design
Do
- group small types together
- use the smallest practical integer type
- pack struct members intentionally
- use bitmasks for many flags
- benchmark before and after changes
- preserve layout in upgradeable contracts
Avoid
- interleaving
uint256values with small fields unnecessarily - storing many independent
boolvariables when a bitmask would do - changing storage order in deployed upgradeable contracts
- optimizing for packing at the expense of clarity when the gas savings are negligible
A practical design checklist
Before finalizing a contract, ask:
- Can any fields be reduced in size safely?
- Can related flags be combined?
- Are struct members ordered to fill slots efficiently?
- Will frequent updates hit packed slots too often?
- Is this contract upgradeable, and if so, is the layout stable?
If the answer to several of these is yes, storage layout is likely a meaningful optimization target.
Conclusion
Storage layout is one of the most reliable ways to improve Solidity performance because it affects both deployment and runtime gas. The biggest gains come from packing small values together, designing structs carefully, and avoiding unnecessary slot fragmentation.
The best storage optimization is not just “use smaller types.” It is to design state around access patterns, update frequency, and upgrade constraints. When done well, storage layout improvements are simple, durable, and easy to justify in production code.
