Storage layout and variable packing in Solidity
How a contract stores data in 32-byte slots, how variables are packed, where mappings and arrays live and how the Coin-Trust deal struct is laid out.
In this article
A contract's persistent memory - storage - is the most expensive part of the EVM. A write costs thousands of units of gas, so developers think carefully about how to lay data out. This article explains the layout rules and then walks through a real example - the deal struct in the escrow contract. The basics of the EVM are in the article EVM under the hood.
32-byte slots
A contract's storage is a huge array of cells, each 32 bytes (256 bits), numbered from 0. These are the slots. By default a slot is empty (zeros), and it can be read at any time. A contract accesses a slot by number (SLOAD/SSTORE).
The compiler lays state variables out into slots in order of declaration.
contract Example {
uint256 a; // slot 0
uint256 b; // slot 1
address c; // slot 2 (takes 20 bytes of 32)
}Packing
If consecutive variables are shorter than 32 bytes and fit together, the compiler packs them into one slot, right to left.
contract Packed {
uint128 a; // slot 0, bytes 0-15
uint128 b; // slot 0, bytes 16-31
uint256 c; // slot 1
}
contract NotPacked {
uint128 a; // slot 0
uint256 c; // slot 1
uint128 b; // slot 2 - no packing because of the order
}The declaration order matters. Packed uses 2 slots, NotPacked uses 3.
Type sizes: bool and uint8 - 1 byte, uint16 - 2, uint64 - 8, uint96 - 12, address - 20, uint256 and bytes32 - 32.
Structs and arrays
- A struct always starts a new slot, the same packing rules apply inside it; the variable after a struct also starts a new slot.
- A static array of fixed length takes consecutive slots (packed for small types).
Mappings and dynamic arrays
These types do not take "space in a row" but use a hash to compute the location.
Mapping. The value for key k in a variable occupying slot p lives in the slot:
keccak256(k ‖ p)Thanks to the hash function slots of different keys are scattered, yet computed without searching. A mapping has no "length" and you cannot get a list of keys: to enumerate records use events or a separate counter.
Dynamic array. The length lives in slot p, and the elements - starting at slot keccak256(p), in a row.
What it costs
| Operation | Gas (order of magnitude) |
|---|---|
| Reading a slot (cold) | ~2,100 |
| Reading a slot (warm) | ~100 |
| Writing to an empty slot (0 → non-zero) | ~20,000 (+ 2,100 for "cold" access) |
| Changing a non-empty slot | ~2,900 (+ 2,100 for "cold" access) |
| Zeroing a slot | part of the gas refunded |
One new slot at first write is on the order of 22,000 gas. If packing avoids occupying extra slots, the saving is substantial (optimization techniques).
Example: the deal struct in Coin-Trust
In the escrow contract each deal is a Deal struct located in mapping(uint256 => Deal). The fields are chosen to fit in 5 slots:
| Slot | Fields | Bytes |
|---|---|---|
| 0 | buyer (address), amount (uint96) | 20 + 12 = 32 |
| 1 | seller (address), two rates (uint16 × 2), deadline (uint64) | 20 + 2 + 2 + 8 = 32 |
| 2 | arbiter (address), disputeOpenedAt (uint64), status, outcome, durationPreset | 20 + 8 + 1 + 1 + 1 = 31 |
| 3 | token (address), offerExpiresAt (uint64), three bool flags | 20 + 8 + 3 = 31 |
| 4 | termsHash (bytes32) | 32 |
The struct has 17 fields in all. Without packing that would be 17 slots; packing cuts them to 5. The difference at first write is 12 slots × ~20,000 = around 240,000 gas per deal. This noticeably reduces the cost of creating an offer (fees).
That is why amount is a uint96 rather than a uint256: the maximum is about 7.9 × 10^28 units, enough even for tokens with 18 decimals. But then an explicit check is needed on write, otherwise the value would silently truncate (why this matters).
Why fitting into a slot is not always profitable
Packing saves space, but reading and writing part of a slot requires extra operations: masking and shifting. If one transaction changes and reads several fields of the same slot, the gain is large. If variables change in different transactions and independently, and merely "other" bits must be preserved, the win is less obvious. In practice: pack fields that change together, and measure gas (Foundry).
Visibility of storage
"Private" variables (private) are hidden only from other contracts. Storage data is public: anyone can read any slot through a node (eth_getStorageAt). Never store secrets in private variables (catalogue of vulnerabilities).
What to read next
Let us trace how functions are protected from unauthorised calls: access-control patterns. Or how to save gas: gas optimization.
Keep reading
- 3 · Under the hood
Gas optimization in Solidity - 12 techniques and when they apply
Practical gas-saving techniques for smart contracts: storage packing, calldata, caching, immutable, unchecked and the main rule - never trade away security.
4 min read - 3 · Under the hood
Ethereum accounts and state - EOAs, contracts and the world state
How Ethereum accounts work: EOA and contract, the four account fields, the world state, contract addresses and the units wei, gwei, ether.
4 min read
Mentioned in
- Glossary - blockchain, wallets, smart contracts and escrow from A to Z
- Ethereum accounts and state - EOAs, contracts and the world state
- A tour of the Solidity language - types, functions, errors and events by example
- ERC-20 from scratch - writing a token in Solidity and analysing every line
- Writing escrow in Solidity: the state machine and a real contract walkthrough
- Integer math and rounding in smart contracts
- A catalogue of smart contract vulnerabilities: 12 classic mistakes and defences
- Proxies and upgradeable contracts - delegatecall, UUPS and the price of flexibility
- Merkle trees and membership proofs - verifying millions of items with one hash
- Gas optimization in Solidity - 12 techniques and when they apply
Ready to try it?
Create your first deal in a couple of minutes. The money is locked in a smart contract and paid out by rules you can see in advance.

