Ethereum Contract Storage Slots: Packing, Mappings, and eth_getStorageAt
Learn how Solidity assigns Ethereum contract storage slots, how mappings and arrays derive locations, and how to inspect state safely with eth_getStorageAt.

A block explorer may show a contract's public variables, yet the chain ultimately stores neither variable names nor a neat database table. It stores 32-byte words at numbered locations. Understanding those Ethereum contract storage slots turns an opaque hex value into useful evidence and connects everyday blockchain basics to the state the EVM actually reads.
This guide explains Solidity's default layout, slot packing, mappings and dynamic arrays, and the eth_getStorageAt JSON-RPC method. It also covers the traps that matter in practice: proxies, incorrect source code, historical block context, and the fact that private data is not secret.
What is an Ethereum contract storage slot?
Think of contract storage as an enormous wall of numbered safe-deposit boxes. Every box has a 256-bit key and holds one 256-bit value. Solidity gives friendly names and types to those boxes, while the EVM works with slot keys and words.
The Solidity smart-contract documentation describes persistent storage as a key-value store mapping 256-bit words to 256-bit words. It belongs to one account, persists between calls and transactions, and cannot be enumerated by a contract. This differs from EVM memory, which is recreated for each call, and transient storage, which is cleared when the transaction ends.
Solidity's compiler decides which logical variable uses which physical slot. That layout matters beyond compilation because storage references can cross library boundaries. The language therefore treats layout rules as part of Solidity's external interface.
How Solidity assigns and packs state variables
For ordinary state variables, Solidity begins at slot 0 and places values in declaration order after resolving inheritance. A value that needs a full 32 bytes gets a full slot. Adjacent value types smaller than 32 bytes may share a slot when they fit.
Consider this simplified contract:
contract Ledger {
uint128 public deposited;
uint128 public withdrawn;
mapping(address => uint256) public balances;
uint256[] public checkpoints;
}Its default layout starts like this:
| Declaration | Anchor slot | What the slot contains |
|---|---|---|
deposited, withdrawn | 0 | Two packed 16-byte values |
balances | 1 | Mapping anchor; entries live at derived slots |
checkpoints | 2 | Dynamic-array length; elements live elsewhere |
Packing is not a generic compression pass. The Solidity storage-layout specification defines the exact rules. The first packed item is stored in the lower-order portion of the slot, and an item that does not fit starts a new slot. Structs and array data begin on fresh slot boundaries, although their members can be packed under the same rules.
Packing can reduce persistent state use, but it does not guarantee that every write becomes cheaper. Updating one packed field still requires the EVM to preserve the other bytes in that word. Choose types for correctness first, then measure gas with the compiler version and call pattern you will deploy.
Note
Constants and immutable values are handled differently from ordinary mutable state. Do not infer a slot solely from the order of every declaration you see in source.
How mappings and dynamic arrays derive locations
A mapping can contain an unpredictable number of keys, so Solidity cannot reserve consecutive boxes for it. Instead, its declaration occupies an anchor position p, and each entry receives a location derived with Keccak-256.
For a mapping whose key is a value type, the entry is located conceptually at:
keccak256(h(key) . p)Here h(key) pads or encodes the key as required, p is the 32-byte anchor slot, and . means concatenation. In the example above, balances[user] is derived from the encoded user address and slot 1. The anchor itself does not store the mapping's size or list of keys. That is one reason you cannot discover every mapping member merely by reading sequential slots.
A dynamic array uses its anchor differently. Slot p stores the array length, while element data begins at keccak256(p). Fixed-size element packing can then affect the offset of a particular index. Nested mappings and arrays repeat these derivation rules, so one wrong type, key encoding, or parent slot produces a plausible-looking but unrelated word.
Short bytes values and strings have an additional special encoding: short content can share the anchor slot with a length marker, while longer content uses a separate data area. Use the compiler's layout metadata and the exact Solidity documentation instead of assuming every dynamic value follows the basic array example.
How to inspect a slot with eth_getStorageAt
Ethereum's standard JSON-RPC method eth_getStorageAt accepts three parameters:
- The contract address.
- The storage position as a hexadecimal quantity.
- A block number or tag such as
latest,safe, orfinalized.
A request for slot zero looks like this:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getStorageAt",
"params": ["0xContractAddress", "0x0", "finalized"]
}The result is a 32-byte data value. Decoding it is a separate step. You need the variable's type, byte offset, and packing rules. For an address stored in a full word, for example, the relevant 20 bytes are right-aligned; for two packed uint128 values, each half must be isolated before conversion.
Mapping access adds one more step: calculate the derived slot locally, then pass that 32-byte position to eth_getStorageAt. Use Ethereum's documented encoding example as a test vector before trusting your own utility. A Keccak-256 function is required; standardized SHA3-256 is related but uses different padding and does not produce Ethereum's Keccak result.
The block parameter is part of the claim. Reading latest answers βwhat does this node currently consider the latest state?β Reading a specific block answers a historical question if the provider retains and serves that state. Save the chain ID, contract address, block number, and block hash alongside any result used for an audit or accounting decision.
Compiler metadata beats manual guessing
Manual slot arithmetic is valuable for learning and independent checks, but the compiler can emit storage layout metadata. In standard JSON compiler output, requesting storageLayout produces entries with labels, slots, offsets, and type identifiers, plus a type table describing encodings and byte sizes.
A reliable workflow is:
- Obtain verified source and identify the exact deployed compiler settings.
- Compile that source and request its storage layout output.
- Confirm the runtime address is a direct contract or identify its proxy implementation.
- Select a block context and calculate any mapping or array slot.
- Read the word through JSON-RPC and decode it according to the reported type and offset.
- Cross-check a known public getter when one exists.
The storage-layout JSON format is documented as experimental, so tooling should tolerate metadata-format changes. The underlying layout of an already deployed contract cannot be changed by recompiling its source with a newer compiler.
Proxies and namespaced storage change the starting point
With a proxy, implementation bytecode executes against the proxy's storage. Reading the implementation address's slot zero may therefore reveal nothing about the live application's balances or configuration. First identify the proxy execution context, then inspect the proxy address at the intended block.
ERC-1967 assigns distinguished slots for a proxy's implementation, beacon, and optional admin. Those locations are derived to avoid the compiler's ordinary storage tree. They are useful for identifying upgrade infrastructure, but they do not describe every application variable.
ERC-7201 standardizes a NatSpec annotation and formula for namespaced storage. A namespace roots a struct at a derived location rather than the default tree starting at zero. The annotation documents intent; the ERC explicitly notes that the compiler does not enforce the claimed formula. Review the actual accessor code and computed constant.
Risks and limitations
Onchain does not mean self-describing
The returned bytes are authentic state for the requested address and block as served by the node, but variable names and types are not embedded in that word. Incorrect source or an ABI from the wrong implementation can give an authoritative-looking false interpretation.
Private is not secret
Solidity visibility controls which contracts can access a variable through language-level syntax. It does not encrypt state. Anyone with the slot location and chain data can read the bytes. Never store passwords, seed phrases, unencrypted personal data, or unrevealed secrets in contract storage.
Upgrades can corrupt layouts
Proxy upgrades reuse existing storage. Reordering or changing incompatible variables can make new code interpret old bytes under different types. Namespaces and standardized proxy slots reduce some collision risks, but they do not replace upgrade validation, access-control review, tests, and monitoring.
A raw read is not an authorization
A slot value can be stale relative to the block you intended, misdecoded, or read from the wrong chain or address. Do not approve withdrawals, credit deposits, or make investment decisions from one manually decoded word without verifying context and application invariants.
Practical checklist
- Confirm chain ID, contract address, and block number or tag
- Identify whether the address is a proxy, implementation, beacon, or direct contract
- Match verified source, compiler version, and storage-layout metadata
- Check slot, byte offset, and type before decoding packed values
- Encode mapping keys exactly and use Keccak-256
- Handle dynamic arrays, strings, bytes, structs, and namespaces by their specific rules
- Cross-check a getter, event history, or application invariant when available
- Treat every supposedly private value as publicly readable
FAQ
Can I enumerate every nonzero contract storage slot?
Not through the basic contract interface or a sequence of eth_getStorageAt calls. The key space is enormous, and mapping keys are not stored as an enumerable list at their anchor. Debug or state-inspection extensions may offer provider-specific capabilities, but they are not the portable JSON-RPC method described here.
Does slot zero always contain the first variable in the source file?
No. Inheritance, packing, constants, immutables, custom layout, and proxy execution context can change that conclusion. Use exact compiler metadata and deployed context.
Is contract storage the same as Ethereum's state trie?
No. Slots are the contract-level key-value view. Ethereum commits an account's storage through a storage trie and then commits account state into the global state root. Our Merkle Patricia Trie guide explains that authenticated structure.
Can eth_getStorageAt change contract state?
No. It is a read-only RPC query. It does not execute a state-changing transaction or prove that your interpretation of the returned bytes is correct.
Primary sources
- Solidity: Layout of State Variables in Storage and Transient Storage
- Solidity: Introduction to Smart Contracts β storage, memory, and stack
- Ethereum.org: JSON-RPC API β eth_getStorageAt
- ERC-1967: Proxy Storage Slots
- ERC-7201: Namespaced Storage Layout
Read the bytes in their full context
Ethereum contract storage slots are simple at the EVM level: one 256-bit key maps to one 256-bit value. The difficult part is reconstructing Solidity's types, packing, derived locations, execution address, and block context around that word. Let compiler metadata lead, reproduce the derivation independently, and cross-check consequential conclusions against another interface.
This article is educational and not financial advice. Smart contracts and crypto assets can fail or lose value. Verify current primary documentation, test with amounts you can afford to lose, and do your own research (DYOR).
Keep learning

What Are Smart Contracts? How They Work and Real Use Cases
Discover what smart contracts are, how they work on the blockchain, and their real-world use cases in DeFi, NFTs, and RWA β plus risks, limits, and FAQ.

Ethereum eth_getProof Guide: Verify Account and Storage State
Learn what eth_getProof returns, how account and storage proofs connect to a block state root, and which trust and availability limits still matter.
Ethereum Proxy Contracts Explained: Delegatecall, UUPS, and Upgrade Risks
Learn how Ethereum proxy contracts use delegatecall, how transparent, UUPS, and beacon proxies differ, and how to verify upgrade authority safely.
Explore related topics

Ethereum Glamsterdam Upgrade: ePBS, BALs, and What Is Actually Planned
Ethereum Glamsterdam is expected in Q4 2026. Learn its frozen scope, Sepolia milestone, ePBS, block-level access lists, and remaining uncertainties.
Crypto Address Poisoning: How to Verify Wallet Addresses Before You Send
Crypto address poisoning plants a lookalike address in your transaction history. Learn how it works and follow a safer verification checklist.