Opcode Summary

PropertyValue
Opcode0x38
MnemonicCODESIZE
Gas2
Stack Input(none)
Stack Outputlen(this.code)
BehaviorPushes the size (in bytes) of the currently executing contract’s bytecode onto the stack. Returns the code of the contract in the current execution context (i.e., respects DELEGATECALL context).

Threat Surface

CODESIZE returns the byte length of the executing contract’s code. On its own, it seems benign — a simple metadata query. But CODESIZE has a critical property that has been the root cause of an entire class of smart contract vulnerabilities:

During constructor execution, CODESIZE returns 0.

When a contract is being deployed, its constructor runs before the runtime bytecode is stored at the address. At that point, the account has no code. This means any check that uses CODESIZE (or the related EXTCODESIZE) to determine “is this address a contract?” will answer “no” for contracts mid-construction. This single edge case has been exploited to:

  1. Bypass isContract() guards — the most common misuse. Developers use extcodesize(addr) > 0 as a proxy for “is this a contract?”, then restrict access to EOAs only. Attackers bypass this by calling the target from their constructor.

  2. Defeat anti-bot / anti-contract protections — lottery contracts, airdrop mechanisms, and token sales that attempt to block contract callers are trivially bypassed.

  3. Enable metamorphic contract attacks — contracts that use SELFDESTRUCT + CREATE2 can redeploy different code to the same address. Between destruction and redeployment, CODESIZE (via EXTCODESIZE) returns 0, and after redeployment it returns a different value. Any system that cached or relied on code size as an identity check is broken.

OpenZeppelin removed Address.isContract() entirely in v5.0 (October 2023) because the pattern was fundamentally unreliable and its misleading name caused widespread misuse.


Smart Contract Threats

T1: isContract() Bypass via Constructor (Critical)

The most exploited pattern involving CODESIZE/EXTCODESIZE. Contracts use extcodesize(addr) > 0 to check if a caller is a contract, then block contract callers. During constructor execution, the calling contract has no deployed bytecode, so extcodesize returns 0 and the check passes.

// Vulnerable guard -- trivially bypassed
modifier onlyEOA() {
    require(!isContract(msg.sender), "No contracts allowed");
    _;
}
 
function isContract(address addr) internal view returns (bool) {
    uint256 size;
    assembly { size := extcodesize(addr) }
    return size > 0;
}

Attack pattern:

contract Attacker {
    constructor(address target) {
        // During construction, extcodesize(address(this)) == 0
        // The target's isContract(msg.sender) check returns false
        ITarget(target).protectedFunction();
    }
}

Why it matters: This pattern was used by OpenZeppelin’s Address.isContract(), the most widely-used contract detection utility in the ecosystem. Any protocol relying on it for access control was vulnerable. OpenZeppelin removed the function entirely in v5.0, noting in their FAQ: “Attempting to prevent calls from contracts is highly discouraged because it breaks composability, breaks support for smart wallets like Gnosis Safe, and doesn’t provide security since it can be circumvented by calling from a contract constructor.”

T2: Code Size as Identity / Integrity Check (High)

Some protocols cache or check a contract’s code size as a lightweight proxy for verifying “this contract hasn’t changed.” This assumption is broken by:

  • Metamorphic contracts: Using CREATE2 + SELFDESTRUCT, an attacker can deploy contract A (size N) to an address, destroy it, then deploy contract B (size M) to the same address. The code size changes, but if the check only verifies “code exists” (size > 0), the new malicious code passes.

  • Post-SELFDESTRUCT state: After SELFDESTRUCT (within the same transaction pre-Cancun, or after the transaction is mined), EXTCODESIZE returns 0. Any system that interprets this as “address is an EOA” is wrong — it was a contract that was destroyed.

T3: Anti-Bot / Anti-Contract Protections Bypass (High)

Token launches, airdrops, lotteries, and NFT mints frequently attempt to restrict participation to EOAs using code size checks. These protections are trivially bypassed:

// Token sale with "no bots" protection
function buy() external {
    require(!isContract(msg.sender), "No bots");
    _mint(msg.sender, ALLOCATION);
}

An attacker deploys a contract whose constructor calls buy(). The code size check passes because the attacker’s contract has no bytecode during construction. The attacker receives their allocation, and the constructor finishes, deploying the bot contract to the same address.

Why it matters: This gives sophisticated actors (MEV bots, snipers) an unfair advantage while providing zero actual protection. Legitimate smart wallet users (Gnosis Safe, Argent, ERC-4337 accounts) are blocked while malicious constructors pass freely.

T4: Metamorphic Contract Code Mutation (Critical)

Metamorphic contracts exploit the fact that SELFDESTRUCT clears an account’s code (CODESIZE → 0), and CREATE2 can redeploy different code to the same address. This breaks the fundamental assumption that a contract’s code is immutable.

Attack flow:

  1. Deploy contract A to address X via CREATE2 (CODESIZE at X = len(A))
  2. Contract A passes audits, gets whitelisted in governance or access control
  3. Call SELFDESTRUCT on contract A (CODESIZE at X = 0)
  4. Deploy contract B to address X via CREATE2 with same salt but different init code (CODESIZE at X = len(B))
  5. Address X is still whitelisted, but now runs completely different code

Why it matters: Any system that trusts an address based on a one-time audit or code verification is vulnerable. The Tornado Cash governance attack (May 2023) used exactly this pattern to replace an approved proposal contract with a malicious one.


Protocol-Level Threats

P1: No DoS Vector (Low)

CODESIZE costs a fixed 2 gas, takes no input, and performs a simple length lookup. It cannot be used for gas griefing or computational DoS.

P2: Consensus Safety (Low)

CODESIZE returns a trivially deterministic value — the byte length of the executing code. All EVM implementations agree on this. No known consensus bugs arise from CODESIZE itself.

P3: DELEGATECALL Context Confusion (Medium)

CODESIZE returns the code size of the currently executing code, not the contract at address(this). In a DELEGATECALL context, CODESIZE returns the code size of the implementation contract (the callee), while address(this) refers to the proxy contract. If a developer uses CODESIZE expecting it to reflect the proxy’s code, they get the wrong value.

// In a proxy context:
// address(this) → proxy address
// CODESIZE    → implementation's code size (not proxy's)
// EXTCODESIZE(address(this)) → proxy's code size

This distinction matters for contracts that use CODESIZE for bytecode introspection or self-analysis in a delegatecall context.

P4: EIP-3541 and Code Size Limits (Low)

EIP-170 (Spurious Dragon) imposed a 24,576-byte limit on deployed contract code. CODESIZE will never return a value exceeding this limit for normally deployed contracts. EIP-3541 rejects contracts starting with 0xEF. Neither affects CODESIZE’s behavior, but they bound the range of valid return values.


Edge Cases

Edge CaseBehaviorSecurity Implication
CODESIZE during constructorReturns 0Bypasses all extcodesize-based contract detection; root cause of isContract() exploits
CODESIZE in DELEGATECALLReturns size of the callee (implementation), not the proxyMismatch if developer expects proxy’s code size; EXTCODESIZE(address(this)) returns proxy size
CODESIZE after SELFDESTRUCT (same tx)Returns original code sizeCode is not cleared until end of transaction (pre-Cancun); checks within the same tx still see the old size
CODESIZE after SELFDESTRUCT (next tx)Returns 0 (pre-Cancun)Address looks like an EOA; any cached assumption that “this address is a contract” is now wrong
CODESIZE of an EOAN/A (CODESIZE only queries the executing contract)CODESIZE cannot query arbitrary addresses; use EXTCODESIZE for that
CODESIZE with EIP-7702 delegated EOAReturns size of the delegated codeEOAs with EIP-7702 delegation have code; CODESIZE reflects the delegated bytecode, further blurring EOA/contract distinction
CODESIZE at max contract sizeReturns 24,576 (EIP-170 limit)No overflow risk; value fits in a single stack word
CODESIZE of minimal contractReturns 1+ (even STOP alone is 1 byte)Only constructors and destroyed contracts return 0

Real-World Exploits

Exploit 1: FoMo3D Airdrop Pot Drain (July 2018)

Root cause: isHuman() modifier using extcodesize bypassed by calling from constructor.

Details: FoMo3D was a popular lottery/game contract on Ethereum with an “airdrop pot” that distributed ETH prizes to players. The contract used an isHuman() modifier to restrict participation to EOAs, implemented via extcodesize(msg.sender) == 0. Attackers exploited this by deploying contracts that called FoMo3D’s airdrop function from within their constructors, when extcodesize returns 0.

The attack was further enhanced by iterative pre-calculated contract creation: since contract addresses are deterministic (based on deployer address and nonce), attackers pre-calculated which nonce would produce an address that would win the airdrop lottery (which used on-chain “randomness” derived from addresses, block data, etc.). They deployed throwaway contracts to increment their nonce until reaching the winning deployment, then deployed the attack contract at exactly the right nonce.

CODESIZE’s role: The isHuman() guard relied on extcodesize == 0 to mean “this is a human.” During constructor execution, the attacking contract had no deployed code, so extcodesize returned 0. The guard passed, and the contract drained airdrop prizes.

Impact: Multiple attackers drained significant ETH from the airdrop pot over several weeks. PeckShield documented the technique in detail, showing how the attack could be automated and repeated indefinitely.

References:


Exploit 2: Tornado Cash Governance Takeover — $2.17M Stolen (May 2023)

Root cause: Metamorphic contract replaced audited proposal code with malicious code at the same address.

Details: On May 20, 2023, an attacker submitted a governance proposal to Tornado Cash that appeared identical to a previously approved, legitimate proposal. Voters approved it without scrutinizing the code. The proposal contract contained a hidden SELFDESTRUCT function. After approval, the attacker:

  1. Called SELFDESTRUCT on the proposal contract, clearing its code (EXTCODESIZE → 0)
  2. Redeployed a malicious contract to the same address using CREATE2
  3. The governance system still recognized the address as an approved proposal
  4. The malicious contract granted the attacker 1.2 million fake TORN governance votes — far exceeding the ~700,000 legitimate votes

With governance control, the attacker drained 483,000 TORN tokens ($2.17M) from governance vaults. Approximately 470,000 TORN were sold and swapped for ETH, with 572 ETH laundered through Tornado Cash itself.

CODESIZE’s role: The attack exploited the fact that code size (and code content) at an address is not permanent. Between SELFDESTRUCT and redeployment, EXTCODESIZE returned 0. After redeployment, it returned a non-zero value for completely different code. The governance system trusted addresses, not code content, making it blind to the metamorphic swap.

References:


Exploit 3: OpenZeppelin Address.isContract() Removal (October 2023)

Root cause: Ecosystem-wide misuse of extcodesize-based contract detection.

Details: OpenZeppelin’s Address.isContract() was the most widely used utility for detecting whether an address was a contract. It used account.code.length > 0 (which compiles to EXTCODESIZE). Over years of use, the function was found to be fundamentally misleading:

  • Returns false for contracts in their constructor (bypass via constructor calls)
  • Returns false for addresses where a contract will be deployed (CREATE2 pre-computation)
  • Returns false for addresses where a contract was destroyed (SELFDESTRUCT)
  • Returns true for EOAs with EIP-7702 delegation

OpenZeppelin filed Issue #3417 (“Remove isContract due to potential misuse”) and removed the function in PR #3682, shipped with OpenZeppelin Contracts v5.0.

CODESIZE’s role: isContract() was a thin wrapper around EXTCODESIZE. Its removal was an acknowledgment that code size is not a reliable indicator of whether an address is a contract, an EOA, or something in between.

Impact: The removal forced hundreds of downstream projects to refactor their contract detection logic. It established the ecosystem consensus that “is this a contract?” is not a question the EVM can reliably answer.

References:


Attack Scenarios

Scenario A: Constructor-Based isContract() Bypass

// Vulnerable token with "humans only" airdrop
contract VulnerableAirdrop {
    mapping(address => bool) public claimed;
    
    function claim() external {
        require(!isContract(msg.sender), "No contracts");
        require(!claimed[msg.sender], "Already claimed");
        claimed[msg.sender] = true;
        _mint(msg.sender, AIRDROP_AMOUNT);
    }
    
    function isContract(address addr) internal view returns (bool) {
        uint256 size;
        assembly { size := extcodesize(addr) }
        return size > 0;
    }
}
 
// Attacker bypasses the check from their constructor
contract AirdropSniper {
    constructor(address airdrop) {
        // extcodesize(address(this)) == 0 during construction
        VulnerableAirdrop(airdrop).claim();
        // Tokens are minted to this contract address
    }
}
 
// Factory to deploy many snipers, draining the airdrop
contract SniperFactory {
    function snipe(address airdrop, uint256 count) external {
        for (uint256 i = 0; i < count; i++) {
            new AirdropSniper(airdrop);
        }
    }
}

Scenario B: Metamorphic Contract Governance Attack

```mermaid
sequenceDiagram
    participant Attacker
    participant Governance
    participant ProposalAddr as Proposal Address

     Phase 2: Swap the code
    Attacker->>ProposalAddr: selfdestruct()
    Note right of ProposalAddr: CODESIZE = 0
    Attacker->>ProposalAddr: CREATE2(salt, maliciousCode)
    Note right of ProposalAddr: CODESIZE = len(maliciousCode)

    %% Phase 3: Execute with governance privileges
    Attacker->>Governance: executeProposal(ProposalAddr)
    Governance->>ProposalAddr: delegatecall (trusted address)
    Note right of ProposalAddr: Malicious code runs with<br/>governance permissions

### Scenario C: NFT Mint Bot Bypassing Anti-Contract Check

```solidity
contract VulnerableNFTMint {
    uint256 public constant MAX_PER_WALLET = 3;
    
    function mint(uint256 quantity) external payable {
        require(tx.origin == msg.sender, "No contracts"); // Slightly better but still flawed
        // OR the worse version:
        // require(!isContract(msg.sender), "No contracts");
        require(quantity <= MAX_PER_WALLET);
        _safeMint(msg.sender, quantity);
    }
}

// Even tx.origin == msg.sender can be bypassed in some contexts,
// and isContract() is trivially bypassed:
contract MintBot {
    constructor(address nft, uint256 qty) payable {
        // isContract(address(this)) returns false during construction
        VulnerableNFTMint(nft).mint{value: msg.value}(qty);
        // Transfer NFTs to attacker EOA in a subsequent call
    }
}

Mitigations

ThreatMitigationImplementation
T1: isContract() bypassDo not use code size to distinguish EOAs from contractsRemove isContract() checks entirely; OpenZeppelin removed theirs in v5.0
T1: If EOA-only is truly requiredUse msg.sender == tx.origin (with caveats)Blocks contract callers but also blocks smart wallets (Gnosis Safe, ERC-4337); not future-proof with account abstraction
T2: Code identity assumptionVerify code hash, not just code existenceUse EXTCODEHASH to check specific expected bytecode hash, not just EXTCODESIZE > 0
T3: Anti-bot protectionsUse off-chain allowlists (Merkle proofs) instead of on-chain contract detectionMerkle tree of approved addresses verified via MerkleProof.verify()
T3: Rate limitingEnforce per-address limits without caring if caller is a contractmapping(address => uint256) tracking + per-block or time-based limits
T4: Metamorphic contractVerify EXTCODEHASH matches expected value before trusting an addressrequire(addr.codehash == expectedHash) — hash changes if code is swapped
T4: Governance proposalsVerify proposal code at execution time, not just at submissionRe-check EXTCODEHASH of proposal contract in execute(), not just in propose()
GeneralEmbrace composability — don’t block contractsDesign for contract callers as first-class citizens; use reentrancy guards instead of contract exclusion

Post-EIP-7702 Considerations

EIP-7702 (Pectra, 2025) allows EOAs to delegate to contract code, meaning EOAs can have a non-zero EXTCODESIZE. This further erodes the EOA/contract distinction:

  • EXTCODESIZE > 0 no longer means “this is a contract” — it could be an EOA with EIP-7702 delegation
  • EXTCODESIZE == 0 no longer means “this is an EOA” — it could be a contract mid-construction or post-SELFDESTRUCT
  • The concept of “is this a contract?” is no longer a binary question the EVM can answer

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractCriticalHighFoMo3D airdrop drain, countless isContract() bypasses
T2Smart ContractHighMediumMetamorphic contract identity spoofing
T3Smart ContractHighHighToken launch sniping, NFT mint botting
T4Smart ContractCriticalMediumTornado Cash governance takeover ($2.17M)
P1ProtocolLowN/A
P2ProtocolLowN/A
P3ProtocolMediumLowDelegatecall context confusion in proxy patterns
P4ProtocolLowN/A

OpcodeRelationship
EXTCODESIZE (0x3B)Queries code size of an arbitrary address. Same constructor-returns-0 vulnerability. The opcode behind isContract(). CODESIZE is the self-referential variant
CODECOPY (0x39)Copies the executing contract’s bytecode into memory. Uses CODESIZE to determine bounds. Also returns nothing meaningful during constructor
EXTCODEHASH (0x3F)Returns keccak256 hash of an address’s code. More reliable than EXTCODESIZE for identity verification — but still returns keccak256("") for accounts with no code and 0 for non-existent accounts
EXTCODECOPY (0x3C)Copies code from an arbitrary address. Subject to the same constructor and SELFDESTRUCT edge cases as EXTCODESIZE
CREATE2 (0xF5)Deterministic address deployment. Enables metamorphic contracts when combined with SELFDESTRUCT: deploy, destroy, redeploy different code to same address
SELFDESTRUCT (0xFF)Destroys contract code, setting CODESIZE/EXTCODESIZE to 0. Key enabler of metamorphic attacks. Deprecated by EIP-6780 (Cancun) which limits destruction to same-transaction only
DELEGATECALL (0xF4)Changes the execution context: CODESIZE returns the callee’s code size, while address(this) refers to the caller. Source of context confusion