Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0x38 |
| Mnemonic | CODESIZE |
| Gas | 2 |
| Stack Input | (none) |
| Stack Output | len(this.code) |
| Behavior | Pushes 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:
-
Bypass
isContract()guards — the most common misuse. Developers useextcodesize(addr) > 0as a proxy for “is this a contract?”, then restrict access to EOAs only. Attackers bypass this by calling the target from their constructor. -
Defeat anti-bot / anti-contract protections — lottery contracts, airdrop mechanisms, and token sales that attempt to block contract callers are trivially bypassed.
-
Enable metamorphic contract attacks — contracts that use
SELFDESTRUCT+CREATE2can 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),EXTCODESIZEreturns 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:
- Deploy contract A to address X via
CREATE2(CODESIZE at X = len(A)) - Contract A passes audits, gets whitelisted in governance or access control
- Call
SELFDESTRUCTon contract A (CODESIZE at X = 0) - Deploy contract B to address X via
CREATE2with same salt but different init code (CODESIZE at X = len(B)) - 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 sizeThis 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 Case | Behavior | Security Implication |
|---|---|---|
| CODESIZE during constructor | Returns 0 | Bypasses all extcodesize-based contract detection; root cause of isContract() exploits |
CODESIZE in DELEGATECALL | Returns size of the callee (implementation), not the proxy | Mismatch if developer expects proxy’s code size; EXTCODESIZE(address(this)) returns proxy size |
CODESIZE after SELFDESTRUCT (same tx) | Returns original code size | Code 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 EOA | N/A (CODESIZE only queries the executing contract) | CODESIZE cannot query arbitrary addresses; use EXTCODESIZE for that |
| CODESIZE with EIP-7702 delegated EOA | Returns size of the delegated code | EOAs with EIP-7702 delegation have code; CODESIZE reflects the delegated bytecode, further blurring EOA/contract distinction |
| CODESIZE at max contract size | Returns 24,576 (EIP-170 limit) | No overflow risk; value fits in a single stack word |
| CODESIZE of minimal contract | Returns 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:
- Called
SELFDESTRUCTon the proposal contract, clearing its code (EXTCODESIZE → 0) - Redeployed a malicious contract to the same address using
CREATE2 - The governance system still recognized the address as an approved proposal
- 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:
- Halborn: The Tornado Cash Hack Explained
- pcaversaccio: Tornado Cash Exploit PoC
- CoinDesk: Attacker Takes Over Tornado Cash DAO
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
falsefor contracts in their constructor (bypass via constructor calls) - Returns
falsefor addresses where a contract will be deployed (CREATE2 pre-computation) - Returns
falsefor addresses where a contract was destroyed (SELFDESTRUCT) - Returns
truefor 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:
- OpenZeppelin Issue #3417: Remove isContract due to potential misuse
- OpenZeppelin PR #3682: Remove Address.isContract
- OpenZeppelin FAQ: Why can’t I prevent contracts from calling my contract?
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
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: isContract() bypass | Do not use code size to distinguish EOAs from contracts | Remove isContract() checks entirely; OpenZeppelin removed theirs in v5.0 |
| T1: If EOA-only is truly required | Use 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 assumption | Verify code hash, not just code existence | Use EXTCODEHASH to check specific expected bytecode hash, not just EXTCODESIZE > 0 |
| T3: Anti-bot protections | Use off-chain allowlists (Merkle proofs) instead of on-chain contract detection | Merkle tree of approved addresses verified via MerkleProof.verify() |
| T3: Rate limiting | Enforce per-address limits without caring if caller is a contract | mapping(address => uint256) tracking + per-block or time-based limits |
| T4: Metamorphic contract | Verify EXTCODEHASH matches expected value before trusting an address | require(addr.codehash == expectedHash) — hash changes if code is swapped |
| T4: Governance proposals | Verify proposal code at execution time, not just at submission | Re-check EXTCODEHASH of proposal contract in execute(), not just in propose() |
| General | Embrace composability — don’t block contracts | Design 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 > 0no longer means “this is a contract” — it could be an EOA with EIP-7702 delegationEXTCODESIZE == 0no 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 ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | Critical | High | FoMo3D airdrop drain, countless isContract() bypasses |
| T2 | Smart Contract | High | Medium | Metamorphic contract identity spoofing |
| T3 | Smart Contract | High | High | Token launch sniping, NFT mint botting |
| T4 | Smart Contract | Critical | Medium | Tornado Cash governance takeover ($2.17M) |
| P1 | Protocol | Low | N/A | — |
| P2 | Protocol | Low | N/A | — |
| P3 | Protocol | Medium | Low | Delegatecall context confusion in proxy patterns |
| P4 | Protocol | Low | N/A | — |
Related Opcodes
| Opcode | Relationship |
|---|---|
| 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 |