Logo
New RPC users get 35% off their first monthView the offer
OnFinality Learn
Network & Protocol Guides14 min read

eth_getCode: Reading Contract Bytecode Correctly

Learn how eth_getCode returns deployed EVM bytecode, why '0x' is ambiguous, and how to distinguish code from state reads across proxies and EIP-7702 delegations.

TL;DR

eth_getCode(address, blockParameter) returns the hex-encoded EVM bytecode deployed at an address for a given block, or '0x' if the address is an externally owned account (EOA), was never deployed, or self-destructed. Unlike state reads such as eth_getStorageAt and eth_call, which return values computed by code, eth_getCode returns the code itself. The block parameter is critical because code changes at deployment and through upgrade proxies; eth_getCode(addr, 'latest') sees implementation code behind a proxy only after you resolve the implementation address. EIP-7702 delegation designators (0xef0100 + delegate address) mean a naive 'length > 0 means contract' test misclassifies delegated EOAs. This guide covers address derivation, bytecode prefix detection, proxy slot verification, artifact comparison, and the honest limitations of eth_getCode.

What eth_getCode Returns and Why the Block Parameter Matters

The Ethereum JSON-RPC specification defines eth_getCode(address, blockParameter) as returning the hex-encoded EVM bytecode at a given address for a specific block. The return value is a DATA string prefixed with 0x, or '0x' if the address is an externally owned account (EOA), a non-existent address, or a contract that has self-destructed. This ambiguity is fundamental: '0x' does not tell you which of those three cases applies. The canonical method definition is documented in the Ethereum JSON-RPC eth_getCode reference.

The block parameter is not optional in practice. Code changes at deployment and via upgrade proxies, so eth_getCode(addr, 'latest') reflects the current code, while eth_getCode(addr, '0x...') reflects historical code. If you are debugging a deploy-and-use sequence, 'latest' may show code that did not exist at the receipt block. Always pin the block parameter when reproducibility matters.

State reads such as eth_getStorageAt and eth_call return values computed by that code, not the code itself. Use eth_getCode when you need to verify what is deployed; use state reads when you need to inspect storage slots or simulate calls. The eth_getStorageAt and slot packing guide covers the storage side in depth.

  • eth_getCode returns bytecode; eth_getStorageAt returns a 32-byte storage word; eth_call returns the result of a simulated call.
  • '0x' is ambiguous: EOA, never-deployed, or self-destructed.
  • Pin the block parameter for reproducible bytecode reads.
  • Authoritative reference: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getcode

Distinguishing EOAs from Contracts by Bytecode Length

A contract has bytecode length greater than zero after a successful deployment. An EOA has no code, so eth_getCode returns '0x' (length zero). This is the standard heuristic for distinguishing EOAs from contracts in code. However, it is not sufficient on its own because of self-destructed contracts and EIP-7702 delegations.

During a deploy-and-use sequence, the code at 'latest' is not the same as the code at the receipt block. If you query eth_getCode immediately after sending a deployment transaction, the code may not yet be visible at the block you expect. Always query at the receipt block or later, and verify the transaction status first.

For a runnable example, the Node.js snippet below checks code length and detects a known Solidity bytecode prefix (0x60806040). It uses a generic JSON-RPC endpoint; replace the URL with your provider's endpoint. OnFinality's Ethereum RPC node guide (RPC Assistant) explains how to obtain an endpoint.

const https = require('https');

const RPC_URL = 'https://your-ethereum-rpc-endpoint';
const ADDRESS = '0xYourContractAddress';

function rpcCall(method, params) {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
    const req = https.request(RPC_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => resolve(JSON.parse(body)));
    });
    req.on('error', reject);
    req.write(data);
    req.end();
  });
}

async function checkCode() {
  const result = await rpcCall('eth_getCode', [ADDRESS, 'latest']);
  const code = result.result;
  const length = (code.length - 2) / 2; // bytes
  console.log('Bytecode length:', length, 'bytes');
  if (length === 0) {
    console.log('No code: EOA, never-deployed, or self-destructed.');
    return;
  }
  if (code.startsWith('0x60806040')) {
    console.log('Detected Solidity 0.8.x bytecode prefix.');
  }
  if (code.startsWith('0xef0100')) {
    console.log('EIP-7702 delegation designator detected.');
  }
}

checkCode().catch(console.error);

Contract Address Derivation and Reading It Back

Contract addresses are derived deterministically: CREATE uses keccak256(rlp([sender, nonce]))[12:], while CREATE2 uses keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:]. You can compute the expected address before deployment and then read it back with eth_getCode to confirm deployment. This is useful for counterfactual deployments and for verifying that a factory produced the expected address.

Reading back the address does not tell you whether the code matches your source. It only tells you that some code exists. To validate the deployment, compare a bytecode hash to the compiler artifact, allowing for constructor-argument and immutable-variable substitutions. The eth_getProof account and storage proofs guide covers how to prove account state at a block.

If you are using a proxy pattern, the address you deploy is the proxy, not the implementation. eth_getCode(proxy, 'latest') returns the proxy's bytecode, not the implementation's. You must resolve the implementation address first, typically by reading a specific storage slot.

  • CREATE address = keccak256(rlp([sender, nonce]))[12:].
  • CREATE2 address = keccak256(0xff ++ sender ++ salt ++ keccak256(init_code))[12:].
  • eth_getCode on a proxy returns proxy bytecode, not implementation bytecode.
  • Use eth_getStorageAt to read the implementation slot (commonly 0x360894... for EIP-1967).

Proxy Patterns and Resolving Implementation Code

Upgradeable proxies store the implementation address in a storage slot. The EIP-1967 standard defines a specific slot: 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc. To read the implementation code, first call eth_getStorageAt(proxy, slot, blockParameter) to get the implementation address, then call eth_getCode(implementation, blockParameter).

This two-step process is necessary because eth_getCode does not follow proxy delegation. It returns the code at the exact address you query. If you query the proxy, you get the proxy's fallback logic, not the implementation's functions. The eth_call state override simulation guide shows how to simulate calls against the implementation directly.

The Node.js example below verifies a proxy's implementation slot and then reads the implementation code. Replace the RPC URL and proxy address with your own values.

const https = require('https');

const RPC_URL = 'https://your-ethereum-rpc-endpoint';
const PROXY = '0xYourProxyAddress';
const IMPLEMENTATION_SLOT = '0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc';

function rpcCall(method, params) {
  return new Promise((resolve, reject) => {
    const data = JSON.stringify({ jsonrpc: '2.0', id: 1, method, params });
    const req = https.request(RPC_URL, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
      let body = '';
      res.on('data', (chunk) => body += chunk);
      res.on('end', () => resolve(JSON.parse(body)));
    });
    req.on('error', reject);
    req.write(data);
    req.end();
  });
}

async function resolveImplementation() {
  const slotResult = await rpcCall('eth_getStorageAt', [PROXY, IMPLEMENTATION_SLOT, 'latest']);
  const implAddress = '0x' + slotResult.result.slice(26); // last 20 bytes
  console.log('Implementation address:', implAddress);
  const codeResult = await rpcCall('eth_getCode', [implAddress, 'latest']);
  const code = codeResult.result;
  console.log('Implementation bytecode length:', (code.length - 2) / 2, 'bytes');
  console.log('First 10 bytes:', code.slice(0, 22));
}

resolveImplementation().catch(console.error);

EIP-7702 Delegation Designators and Misclassification Risks

EIP-7702 introduces a new account type: an EOA that delegates execution to a contract. The delegation designator is a specific bytecode prefix: 0xef0100 followed by the 20-byte delegate address. This means a delegated EOA now returns code starting with 0xef0100, so a naive 'length > 0 means contract' test misclassifies it as a contract. The EIP is documented as pending activation; behavior may vary by provider and network.

To correctly classify an account, check for the 0xef0100 prefix. If present, the account is an EOA with delegation, not a contract. The delegate address is the next 20 bytes. You can then call eth_getCode on the delegate address to read the actual implementation code.

This distinction matters for tooling that assumes any address with code is a contract. Wallets, indexers, and security tools must update their heuristics. The authoritative reference is the EIP-7702 specification.

  • Delegation designator: 0xef0100 ++ delegate address (20 bytes).
  • A delegated EOA returns code, but is not a contract.
  • Check for 0xef0100 prefix before applying 'length > 0' heuristic.
  • EIP-7702 is documented as pending activation; verify on your target network.

Validating Deployed Bytecode Against Compiler Artifacts

To validate that a deployed contract matches expected source, compare a bytecode hash to the compiler artifact. The artifact contains the creation bytecode and the deployed bytecode. The deployed bytecode on-chain may differ from the artifact due to constructor-argument substitutions (which are appended to the creation bytecode, not the deployed bytecode) and immutable-variable substitutions (which are embedded in the deployed bytecode at specific positions).

The standard approach is to strip the CBOR metadata trailer from the on-chain bytecode, then compare the remaining bytes to the artifact's deployed bytecode after applying known immutable substitutions. The metadata trailer is a CBOR-encoded map at the end of solc bytecode; it is not trimmed by eth_getCode. You must trim it yourself.

For a reproducible method, record the block number, the eth_getCode result, the artifact hash, and the trimmed bytecode hash. Use a results table to compare across endpoints. The Decoding revert reasons and custom errors guide covers related debugging techniques.

  • Strip CBOR metadata trailer before comparing to artifact.
  • Constructor arguments are appended to creation bytecode, not deployed bytecode.
  • Immutable variables are embedded in deployed bytecode; apply substitutions before hashing.
  • Record block number and endpoint for reproducibility.

Results Table: Measuring eth_getCode Behavior Against Your Endpoint

Because provider behavior varies, measure eth_getCode against your own endpoint. Create a results table with columns: endpoint URL, block parameter, address, returned bytecode length, first 10 bytes, and whether the result matches a known artifact. Run the same query against multiple endpoints and block parameters to detect inconsistencies.

This method is verified by the reader; it does not assert OnFinality-specific numbers. Use it to compare providers, confirm block-parameter semantics, and detect caching or load-balancing differences. The RPC pricing page explains how to choose a plan based on your query volume.

For a complete measurement, include a historical block and a 'latest' block. If the bytecode differs, you have detected an upgrade or a reorg. Re-verify after any upgrade by re-running the table.

  • Columns: endpoint, block, address, length, prefix, artifact match.
  • Run against at least two endpoints to detect inconsistencies.
  • Include historical and latest blocks to detect upgrades.
  • Re-verify after every upgrade or fork.

Limitations and Tradeoffs of eth_getCode

eth_getCode does not trim metadata. The CBOR trailer at the end of solc bytecode is returned as-is. If you compare raw bytecode to an artifact, you will get a mismatch unless you trim the trailer. This is a common source of false negatives in verification tools.

eth_getCode does not decompile. It returns raw bytes. To understand what the code does, you need a decompiler or the source code. It also returns different bytes across forks, so pin the block parameter and re-verify after an upgrade. A reorg can change the code at a given block; always confirm finality.

Finally, eth_getCode does not follow proxies or EIP-7702 delegations. You must resolve the implementation address or delegate address yourself. This is a deliberate design choice: the RPC method returns the code at the exact address, not the effective code after delegation.

  • No metadata trimming: CBOR trailer is included.
  • No decompilation: raw bytes only.
  • Fork-dependent: pin the block parameter.
  • No proxy or delegation resolution: resolve addresses manually.

Troubleshooting Common eth_getCode Issues

If eth_getCode returns '0x' for a deployed contract, check the block parameter. You may be querying a block before deployment. Also verify the address: a typo or a checksum error will return '0x'. If the contract self-destructed, '0x' is correct. Use eth_getTransactionReceipt to confirm deployment status and block number.

If the bytecode length is unexpected, check for EIP-7702 delegation (0xef0100 prefix) or a proxy pattern. If you are querying a proxy, you are seeing proxy bytecode, not implementation bytecode. Resolve the implementation slot first. If you are comparing to an artifact, trim the CBOR metadata trailer and apply immutable substitutions.

If results differ across endpoints, you may be hitting different forks or caching layers. Pin the block parameter and compare the block hash. Use the OnFinality Learn hub for related guides on state reads and proofs.

  • '0x' for a deployed contract: check block parameter and address.
  • Unexpected length: check for EIP-7702 or proxy pattern.
  • Artifact mismatch: trim CBOR trailer and apply immutable substitutions.
  • Cross-endpoint differences: pin block parameter and compare block hash.

Next Steps: Integrating eth_getCode into Your Workflow

Integrate eth_getCode into your deployment pipeline to verify that the correct bytecode is on-chain. After deployment, query eth_getCode at the receipt block and compare the trimmed bytecode hash to your artifact. Store the block number and hash for auditability. For upgradeable contracts, resolve the implementation slot and verify the implementation code separately.

For production monitoring, periodically re-query eth_getCode at 'latest' and compare to the expected hash. Alert on changes. This detects unauthorized upgrades and proxy admin changes. Use the API service to automate these checks across multiple networks.

To get started, obtain an endpoint from OnFinality's Ethereum network page and run the examples in this guide. For pricing and plan details, see RPC pricing. For a broader overview of Ethereum RPC methods, see the Ethereum RPC node guide (RPC Assistant).

  • Verify bytecode hash after deployment at the receipt block.
  • Monitor 'latest' code for unauthorized upgrades.
  • Resolve implementation slots for proxy contracts.
  • Automate checks with the API service.

Never Worry about Infrastructure Again

OnFinality takes away the heavy lifting of DevOps so you can build smarter and faster.

Get Started