Skip to main content
This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.

Overview

Opcode: 0x3f Introduced: Constantinople (EIP-1052) EXTCODEHASH returns the keccak256 hash of an account’s bytecode, or 0 for empty accounts.

Specification

Stack Input:
address (uint160 as uint256)
Stack Output:
hash (bytes32 as uint256)
Gas Cost: Variable (hardfork-dependent)
  • Constantinople: 400 gas
  • Istanbul (EIP-1884): 700 gas
  • Berlin (EIP-2929): 2600 gas (cold) / 100 gas (warm)

Behavior

Returns keccak256(account.code) for contracts, or 0 for:
  • EOAs (externally owned accounts)
  • Non-existent accounts
  • Empty code (code.length == 0)
More efficient than EXTCODESIZE + EXTCODECOPY + KECCAK256 for code verification.

Examples

Basic Usage

import { extcodehash } from '@tevm/voltaire/evm/context';

const contractAddr = Address('0x...');
const frame = createFrame({ stack: [BigInt(contractAddr)] });

const err = extcodehash(frame, host);

console.log(frame.stack[0]); // keccak256(code) as u256

Implementation Verification

contract ProxyWithVerification {
    address public implementation;
    bytes32 public implementationHash;

    function setImplementation(address impl) public {
        bytes32 hash;
        assembly {
            hash := extcodehash(impl)
        }
        require(hash != 0, "Must be contract");

        implementation = impl;
        implementationHash = hash;
    }

    function verifyImplementation() public view returns (bool) {
        bytes32 currentHash;
        assembly {
            currentHash := extcodehash(implementation)
        }
        return currentHash == implementationHash;
    }
}

Factory Pattern

contract Factory {
    mapping(bytes32 => address[]) public deployments;

    function deploy(bytes memory code) public returns (address instance) {
        assembly {
            instance := create(0, add(code, 0x20), mload(code))
        }

        bytes32 codeHash;
        assembly {
            codeHash := extcodehash(instance)
        }

        deployments[codeHash].push(instance);
    }

    function findDeployments(address target) public view returns (address[] memory) {
        bytes32 hash;
        assembly {
            hash := extcodehash(target)
        }
        return deployments[hash];
    }
}

Gas Cost

Historical evolution:
HardforkColdWarm
Constantinople400-
Istanbul700-
Berlin2600100
Comparison to alternatives:
// EXTCODEHASH: 700 gas (Istanbul)
bytes32 hash;
assembly { hash := extcodehash(account) }

// EXTCODESIZE + EXTCODECOPY + KECCAK256: ~1400+ gas
uint256 size;
assembly { size := extcodesize(account) }
bytes memory code = new bytes(size);
assembly { extcodecopy(account, add(code, 0x20), 0, size) }
bytes32 hash = keccak256(code);

Common Usage

Contract Identity Check

function isSameCode(address a, address b) public view returns (bool) {
    bytes32 hashA;
    bytes32 hashB;
    assembly {
        hashA := extcodehash(a)
        hashB := extcodehash(b)
    }
    return hashA == hashB && hashA != 0;
}

Minimal Proxy Detection

function isMinimalProxy(address account) public view returns (bool) {
    // Minimal proxy (EIP-1167) has specific bytecode pattern
    bytes32 expectedHash = keccak256(minimalProxyBytecode);

    bytes32 actualHash;
    assembly {
        actualHash := extcodehash(account)
    }

    return actualHash == expectedHash;
}

Upgrade Validation

contract UpgradeableProxy {
    bytes32[] public validImplementations;

    function upgrade(address newImpl) public {
        bytes32 hash;
        assembly {
            hash := extcodehash(newImpl)
        }

        require(isValidImplementation(hash), "Invalid implementation");

        implementation = newImpl;
    }

    function isValidImplementation(bytes32 hash) internal view returns (bool) {
        for (uint i = 0; i < validImplementations.length; i++) {
            if (validImplementations[i] == hash) return true;
        }
        return false;
    }
}

Security

Empty Account Returns 0

function checkAccount(address account) public view returns (bool) {
    bytes32 hash;
    assembly {
        hash := extcodehash(account)
    }

    if (hash == 0) {
        // Could be EOA, non-existent, or empty code
        // Need additional checks to distinguish
    }
}

Constructor Bypass

Like EXTCODESIZE, returns 0 during contract construction:
contract Detector {
    function check() public view returns (bytes32) {
        bytes32 hash;
        assembly {
            hash := extcodehash(caller())
        }
        return hash; // 0 if called from constructor
    }
}

contract Attacker {
    constructor(Detector d) {
        d.check(); // Returns 0!
    }
}

Code Immutability Check

contract ImmutableChecker {
    mapping(address => bytes32) public initialHashes;

    function register() public {
        bytes32 hash;
        assembly {
            hash := extcodehash(caller())
        }
        initialHashes[msg.sender] = hash;
    }

    function verify(address account) public view returns (bool unchanged) {
        bytes32 currentHash;
        assembly {
            currentHash := extcodehash(account)
        }
        return currentHash == initialHashes[account];
    }
}

Implementation

import { consumeGas } from "../Frame/consumeGas.js";
import { popStack } from "../Frame/popStack.js";
import { pushStack } from "../Frame/pushStack.js";
import { fromNumber } from "../../primitives/Address/AddressType/fromNumber.js";
import { Keccak256 } from "../../crypto/Keccak256/Keccak256.js";

/**
 * EXTCODEHASH opcode (0x3f) - Get hash of account's code
 *
 * Stack: [address] => [hash]
 * Gas: 700 (Istanbul+) / 2600/100 (Berlin+ cold/warm)
 */
export function extcodehash(
  frame: FrameType,
  host: BrandedHost
): EvmError | null {
  const addrResult = popStack(frame);
  if (addrResult.error) return addrResult.error;

  const addr = fromNumber(addrResult.value);

  const gasErr = consumeGas(frame, 700n); // Simplified
  if (gasErr) return gasErr;

  const code = host.getCode(addr);

  if (code.length === 0) {
    // Empty account returns 0
    const pushErr = pushStack(frame, 0n);
    if (pushErr) return pushErr;
  } else {
    // Compute keccak256 hash
    const hash = Keccak256.hash(code);

    // Convert hash to u256 (big-endian)
    let hashU256 = 0n;
    for (let i = 0; i < hash.length; i++) {
      hashU256 = (hashU256 << 8n) | BigInt(hash[i]);
    }

    const pushErr = pushStack(frame, hashU256);
    if (pushErr) return pushErr;
  }

  frame.pc += 1;
  return null;
}

Edge Cases

EOA Hash

// EOA has no code
const eoaAddr = Address('0xUserEOA...');
const frame = createFrame({ stack: [BigInt(eoaAddr)] });

extcodehash(frame, host);
console.log(frame.stack[0]); // 0n

Non-Existent Account

// Random address
const randomAddr = Address('0x9999999999999999999999999999999999999999');
const frame = createFrame({ stack: [BigInt(randomAddr)] });

extcodehash(frame, host);
console.log(frame.stack[0]); // 0n

Empty Code Contract

Some contracts may have zero-length code (rare):
const emptyCodeAddr = Address('0x...');
extcodehash(frame, { getCode: () => new Uint8Array(0) });
console.log(frame.stack[0]); // 0n

References