An Ethereum storage slot holds a raw 32-byte word in a contract's persistent storage. An ERC-20 token balance is the unsigned integer returned by balanceOf for a holder at a chosen state. They match directly only when the contract's code and layout make that word the balance. Establish the storage address, mapping location, units and block before comparing them.
Three reads that answer different questions
A wallet can hold native ETH and several tokens. Those quantities do not live in one universal balance field. The Ethereum JSON-RPC reference distinguishes reading an account's ETH, reading contract storage and executing a contract call.
| Read | Inputs that identify the quantity | Result |
|---|---|---|
eth_getBalance | Holder address and block | Native ETH balance in wei |
eth_getStorageAt | Storage address, position and block | Raw word at that position |
eth_call for balanceOf | Token address, encoded holder argument and block | Bytes returned by the token's function |
ERC-20 specifies the balanceOf(address) interface and its unsigned integer result. It does not assign every token a common storage position. A token may return one mapping entry or calculate the amount from several inputs.
The responding node executes the call without creating an on-chain transaction. Decode the returned bytes using the expected interface. An empty response is not an encoded zero balance: a standard uint256 return occupies a 32-byte ABI word. The Solidity ABI specification explains why bytes need a type schema before they acquire meaning.
Display scaling is another step. ERC-20 makes decimals optional, so establish the token's unit convention rather than assuming 18. Keep native ETH available for transaction fees in a separate ledger; the max fee versus priority fee guide explains the fee fields that apply when submitting a transaction.
Follow one holder from mapping to balance
Consider a hypothetical token T with a mapping(address => uint256) at base slot 7. Its getter returns that mapping entry directly. The token has six decimal places, uses ordinary Solidity storage placement and has no proxy. Holder A is the 20-byte address whose numeric value is 1.
The three reads in this comparison refer to one symbolic block hash H. The holder, token and amounts form an invented fixture, not a live token observation.
For an address-keyed mapping under the Solidity 0.8.30 storage rules, derive the holder's position in three steps:
- Left-pad A with zero bytes to make a 32-byte word.
- Encode base slot 7 as another 32-byte word and append it.
- Apply Keccak-256 to the resulting 64 bytes. Call that derived position S.
Symbolically, S = keccak256(pad32(A) || pad32(7)). The double bar means byte concatenation. The two words end in 01 and 07 respectively. Hash their binary encodings, not the printed characters of a hexadecimal string. Packing the unpadded 20-byte address beside the slot would produce 52 bytes and a different input.
Suppose the word at S contains the integer 12,345,678. Its hexadecimal suffix is bc614e; the complete word includes leading zeros. The mapping base at slot 7 remains empty under this layout. It is the anchor used to derive entries, not a cell containing A's balance or the sum of all holders.
| Observation at H | Integer obtained | Interpretation |
|---|---|---|
Call T's balanceOf(A) | 12,345,678 | Returned token balance in base units |
| Read T's storage at S | 12,345,678 | Mapping entry backing that getter |
| Read T's storage at slot 7 | 0 | Empty mapping anchor, not A's holding |
Divide the balance by 1,000,000 to display 12.345678 tokens. The storage read agrees with the call because the fixture explicitly defines that relationship. Neither the slot number 7 nor a zero read from it could have established the balance independently.
For this direct uint256 mapping, the storage result and ABI result can contain the same 32 bytes. That resemblance is useful when debugging, but it does not establish that another token uses the same storage layout or getter logic.
A balance can change while the holder's word stays still
Consider a second hypothetical token U. Its mapping stores shares, and its getter converts them into token base units using a global factor. This invented getter computes floor(shares × numerator / 10). U also displays token amounts with six decimal places.
Compare two hypothetical block end states, J0 and J1. Keep A's stored share count at 5,000,000 and change only the global numerator from 13 to 14:
| Quantity | State J0 | State J1 |
|---|---|---|
| A's stored shares | 5,000,000 | 5,000,000 |
| Conversion factor | 13 / 10 | 14 / 10 |
| Returned token base units | 6,500,000 | 7,000,000 |
| Displayed token balance | 6.5 | 7 |
The holder's word does not move, yet the returned balance rises by 500,000 base units, or 0.5 tokens. Reading the share word as token units would report 5 tokens in both states and miss the conversion entirely. Conversely, seeing a balance change does not prove the holder's mapping entry changed.
This is an explanatory model, not a stETH rate or a reproduction of its precise arithmetic. A documented example of the broader distinction is Lido on Ethereum: its token integration guide describes deriving stETH balances from shares and protocol-wide totals, with integer division involved in conversions.
The rebasing and fee-on-transfer accounting guide follows the inventory consequences. For a storage reader, the immediate lesson is narrower: identify what the stored quantity represents before labeling it with the getter's unit.
For proxies, ask whose storage is being read
A verified implementation can explain the getter while holding none of the holder's relevant state itself.
In a delegating proxy example, users call token address P, which delegates execution to implementation I. The implementation supplies the logic, while the delegated code reads and writes P's storage. That separation is described in ERC-1967. If the getter uses position S, read S at P.
Reading S at I instead inspects I's own storage. It might return zero even when the call through P returns a nonzero balance. Calling balanceOf directly on I can likewise answer a different question because the execution context has changed.
For a proxy using ERC-1967's direct implementation slot, that distinguished slot holds the implementation address. It is not a token-balance mapping. The standard also defines a beacon path: a beacon proxy requires resolving the beacon and its implementation rather than assuming the direct implementation slot is populated. Other proxy designs require their own investigation.
Resolve that path at H, the same historical state used for the balance comparison. Looking up today's implementation beside an old storage word can attach the wrong explanation to the data.
A constant proxy address does not freeze the implementation it reaches. The contract address versus code hash guide develops that identity distinction. Preserve evidence of the implementation and layout used for the particular observation, including any relevant dependency contracts.
Prove the layout before bypassing the getter
Declarations alone cannot establish a mapping base: inheritance, packing and custom placement can affect it. Obtain the matching build's storage-layout output and examine its slots, offsets and types; then trace how the actual getter uses them.
Namespaced storage adds another possibility. ERC-7201 defines conventions for storage trees rooted away from the default root. A mapping inside such a struct needs the correct namespace root and member position. Finding a familiar variable name in source is insufficient.
A useful layout record identifies the compiled implementation, the mapping's key and value types, its base location, and any transformations between stored data and returned units. For hand-placed storage, inspect the code that establishes the location.
Scanning candidate positions until one equals a known balance can produce a lead. One match does not establish a reliable adapter: zero balances match many unused positions, and another stored quantity can happen to equal the current balance. Check the interpretation against the code and several deliberately different states.
Packed values need their documented offset and width before interpretation. A full storage word can contain more than one field. Never label its entire decimal conversion a balance merely because the number looks plausible.
Keep the comparison tied to one state
Layout checks cannot repair a comparison that mixes blocks. Pin both the raw read and the balance call to one selected block identity. Where supported, EIP-1898 provides a blockHash parameter for both methods. Check the endpoint's support and chosen canonicality policy.
The RPC batch versus block snapshot guide explains the collection procedure. Here, a single missing historical state response should leave the comparison unresolved. Retrying only that item at latest changes the experiment.
Record the full call parameters and any simulation overrides. Geth's override objects can substitute state or block inputs for a call. Such a simulation describes those substituted inputs, even if the request also names H.
Keep this worksheet beside the comparison:
| Evidence | Record |
|---|---|
| Asset and holder | Chain, token address and holder address |
| State | Block number and hash, plus selection policy |
| Execution path | Call target, holder argument, caller, proxy path and implementation at that state |
| Layout | Base position, derived S, key/value types and offsets |
| Interpretation | Direct balance, shares or another quantity; conversion and rounding |
| Results | Raw storage bytes, raw call bytes, decoded integers and any errors |
| Units | Established decimal convention and displayed amount |
Agreement between two methods answered by the same provider checks interpretation consistency; it is not independent authentication of the provider's response.
When values disagree, compare raw integers first. Then check the storage owner, key encoding, layout, conversion and block context in that order. Preserve a revert, malformed return or missing state as an error. Replacing it with zero can make the wrong interpretation appear to pass.
For the simple fixture, a complete balance record says: T, holder A, block H, 12,345,678 base units and a six-decimal display of 12.345678. The backing location S belongs alongside that record when established. Keep an unexplained storage word as raw data until the contract's logic supplies the missing meaning.
