Skip to content

Research notes

Ethereum Code Hash vs Contract Address

Follow a proxy through an implementation change and a storage update, then use a fixed-block worksheet to see what matching hashes leave unresolved.

Ivory ceramic drawer holds a folded paper mechanism beside a spare cartridge and fingerprint seal, with a lime rail.
Illustration for research notes; examples in this article are educational.

An Ethereum contract address identifies an account on a particular network; a code hash fingerprints the code stored at that account. Matching code hashes can help check byte identity, but they do not establish matching storage, upgrade permissions or downstream logic. For a proxy, record its address and code hash alongside its resolved implementation and relevant state at one fixed block.

Location and fingerprint answer different questions

A familiar address helps find the intended deployment. It does not establish that the rules affecting an action remain unchanged. A code hash helps compare the bytes you inspected with those from another observation.

Ethereum addresses contain 20 bytes. A Keccak-256 code hash contains 32 bytes. The EXTCODEHASH specification defines the opcode that obtains the code hash of an account with code. Neither the address nor that hash contains the account's application storage.

EvidenceQuestion it answersWhat you still need
Network and contract addressWhich account am I examining?The code and state at the relevant block
Deployed runtime code hashWhich exact code bytes were fingerprinted?Their meaning, dependencies and execution context
Proxy implementation address and hashWhich implementation was resolved?Evidence that the proxy actually routes there
Relevant storage valuesWhich configuration or balances apply?Correct layout, decoding and block identity

Different addresses can contain identical runtime code. Each account can still hold different storage, so copying a known contract's code does not copy its configuration or history. Conversely, a proxy can retain its address and its own runtime bytes while forwarding calls to a new implementation.

Contract-address creation is also a separate calculation. Ordinary CREATE uses the creator and its nonce; CREATE2 uses the deployer, a salt and the hash of initialization code. Initialization code produces the runtime code placed into state. The CREATE2 specification distinguishes these inputs. A CREATE2 address is therefore not simply the runtime code hash shortened into an address.

Fingerprint the deployed bytes you actually received

Ethereum's eth_getCode method returns the code at an address for the selected block. To build a reproducible code fingerprint, save that raw response and apply Keccak-256 to its decoded bytes. Hashing the text characters of a hexadecimal response produces a different input. A generic SHA3-256 function also uses a different algorithm; Ethereum's hashing reference makes that distinction explicit.

Keep the complete deployed-byte hash as its own field. If an analysis tool strips metadata or normalizes bytes, label that separate comparison and retain the original. Otherwise, two reviewers can report “matching code” while having compared different things.

A mismatch deserves investigation, but it does not by itself describe a functional change. Solidity 0.8.30 appends a metadata hash to runtime bytecode by default. Its documentation explains that changes to source paths or even source whitespace can change metadata and resulting bytecode. Reproducing a compilation requires the relevant sources and settings. Solidity 0.8.30 contract metadata.

An empty code response needs separate treatment. Hashing empty bytes locally always yields the same digest. EXTCODEHASH returns zero for a nonexistent or empty account as defined by EIP-161; for a nonempty account without code, it returns the empty-byte hash. Preserve the raw code response and the method used to obtain a reported hash. EIP-1052's account cases.

Follow one proxy through three block states

A delegated call executes implementation code using the calling contract's context. In the proxy arrangement discussed here, application storage remains at the proxy. Solidity's delegatecall explanation describes that separation. Calling the implementation directly can therefore read different state from calling through the proxy.

Consider an entirely hypothetical counter service. Proxy P forwards through an ERC-1967-style implementation pointer. Implementation A reads a stored count and returns that count plus one. Implementation B reads the same storage position and returns that count plus ten. These are deliberately simple teaching rules, with no other inputs or dependencies.

B0, B1 and B2 denote successive hypothetical block-end snapshots. The address P and its own runtime bytes remain identical throughout. H_P labels its code hash; H_A and H_B label the distinct implementation hashes. These labels make the comparisons readable without displaying full hash strings.

SnapshotProxy / hashImplementation / hashCount stored at PRead result
B0P / H_PA / H_A78
B1, after upgradeP / H_PB / H_B717
B2, after count updateP / H_PB / H_B2030

At B0, the result is 7 + 1 = 8. The upgrade changes the implementation pointer, leaving the count at seven. At B1, B returns 7 + 10 = 17. A check of P's address and runtime hash reports no difference, while the returned value changes by nine.

Between B1 and B2, only the stored count changes. The implementation address and both code hashes remain fixed. The result becomes 20 + 10 = 30, an increase of thirteen. Adding implementation hashes to the checklist catches the first transition but cannot explain the second without the storage record.

At each snapshot, all code, storage and call reads use that block's state. Combining B0's implementation pointer with B1's execution result would create a record that describes none of the snapshots. A fresh block needs a fresh set of related observations.

The values are small enough to verify directly. The same distinction matters when the state is a fee setting, pause flag or dependency address. The storage-slot versus token-balance guide develops the decoding question: finding a storage word is only one step toward understanding its application meaning.

Resolve the forwarding path before trusting a slot

For applicable proxies, ERC-1967 standardizes where an implementation address is stored. It also defines a beacon slot: a beacon-based design obtains its implementation through a separate beacon contract's implementation() function. Save each address in that path and obtain the implementation's deployed code at the same block.

The standard provides known locations to inspect. It does not establish that an arbitrary contract uses them. Read the verified forwarding logic and reconcile it with the deployment before treating a slot value as authoritative. A zero implementation slot leaves an unresolved routing question; it does not settle whether the contract delegates.

There are also implementation-specific details. OpenZeppelin Contracts v5.4.0's BeaconProxy uses an immutable beacon address for routing. Its source warns that modifying the ERC-1967 beacon storage slot can leave that slot inconsistent with the actual beacon. This is a concrete reason to check the deployed design as well as the familiar slot. OpenZeppelin v5.4.0 BeaconProxy source.

After resolving the code path at the selected block, inspect the upgrade authority at that state. The upgradeable-contract admin-risk worksheet follows that authority through administrators and timelocks. A fingerprint of the selected implementation does not answer the authorization question.

Build a record tied to one block

Choose the network and resolve a block identity before collecting related reads. Record the block number and hash. EIP-1898 defines block-hash selectors for eth_getCode, eth_getStorageAt and eth_call; its optional requireCanonical field requests a canonical-chain check by the endpoint. Confirm support and historical-state availability for the provider you use. EIP-1898 block selection.

Several requests using “latest” may observe different states. The RPC batch and block-snapshot guide explains how to bind related reads to one observation. If a required response fails, mark that evidence missing and rebuild a consistent record when it becomes available.

Worksheet fieldEvidence to retain
Deployment identityNetwork, full user-facing address and official deployment reference
SnapshotBlock number, block hash, endpoint and selection policy
Proxy bytesRaw deployed code, byte length and complete Keccak-256 hash
Routing evidenceActual forwarding design, relevant raw slot reads and decoded addresses
ImplementationFull resolved address, raw deployed code, hash and source/version match
Application stateValues material to the proposed action, their storage layout or read interface
Execution checkSender, input, value, block selector, raw return or error and decoding
Remaining changesUpgrade authority, adjustable parameters and unresolved dependencies

Use eth_call through the user-facing address when the question concerns proxy behavior. Keep its sender and inputs with the result. The eth_call and gas-estimation comparison explains why a successful read or simulation remains evidence about the supplied context. It cannot freeze that context for later execution.

Turn a match into a bounded conclusion

For the hypothetical service, a useful observation would read: “At B1, P had proxy hash H_P, resolved to implementation B with hash H_B, held count seven, and returned seventeen for the recorded read.” Every part has a specific role. “Same contract as before” discards the implementation and state distinctions that explain the result.

Hashing a provider's returned bytes also does not independently authenticate those bytes against a trusted chain state. EIP-1186's account-proof method describes verification using proofs and a state root from a trusted block. A code hash comparison alone does not perform that check. The log-bloom versus proof guide addresses another case where a compact representation needs careful interpretation before it supports a stronger conclusion.

Before relying on a match, name what was compared: proxy bytes, implementation bytes, or a normalized analysis result. Then list the state and routing facts still needed for the action. Finish the worksheet with the network, block identity, complete addresses and hashes, observed result, and unresolved dependencies. That record gives the next review a precise starting point when either code or state changes.

Keep the context with the example.

Each worked example is illustrative, with its sources and assumptions in the article. Read our editorial standards for authorship, verification and corrections.