Opcode Summary

PropertyValue
Opcode0x20
MnemonicKECCAK256 (formerly SHA3)
Gas30 + 6 * ceil(length / 32) + memory_expansion_cost
Stack Inputoffset, length
Stack Outputkeccak256(memory[offset:offset+length])
BehaviorComputes the Keccak-256 hash of a memory region. Reads length bytes starting at offset in memory and pushes the 256-bit hash onto the stack. Triggers memory expansion if offset + length exceeds the current memory size.

Threat Surface

KECCAK256 is the most security-critical opcode in the EVM. It is not merely a utility — it is the foundational cryptographic primitive upon which virtually every security mechanism in Ethereum is built. Storage layout for mappings and dynamic arrays, contract address derivation (CREATE2), function dispatch (4-byte selectors), event topic indexing, Merkle tree construction, and signature verification all depend on KECCAK256. A weakness in how it is used — even when the hash function itself is sound — cascades into every layer of the stack.

The threat surface is uniquely broad for three reasons:

  1. Collision in truncated output spaces: The EVM and Solidity routinely truncate KECCAK256 output. Function selectors use only 4 bytes (32 bits), yielding a collision space of ~2^32. Storage slot derivation for mappings uses the full 256 bits but concatenates key material in ways that can collide across types. These truncations convert a collision-resistant hash into a collision-prone identifier.

  2. Input encoding ambiguity: abi.encodePacked() concatenates variable-length arguments without delimiters. Different inputs produce identical byte sequences, and therefore identical hashes. This is not a weakness of KECCAK256 itself — it is a weakness in how Solidity feeds data to it.

  3. Dynamic gas cost with memory expansion: Unlike fixed-cost arithmetic opcodes, KECCAK256 has a gas cost that scales with input length and can trigger quadratic memory expansion costs. An attacker who controls the length parameter can force enormous gas consumption, enabling denial-of-service attacks.

The hash function itself (Keccak-256, the SHA-3 standard) has no known practical preimage, second-preimage, or collision attacks. All threats stem from how the EVM and Solidity use the hash output, not from cryptanalytic breaks.


Smart Contract Threats

T1: abi.encodePacked Hash Collision (High)

abi.encodePacked() concatenates arguments without length prefixes or padding. When two or more variable-length arguments (strings, bytes, dynamic arrays) are packed, different argument combinations can produce identical byte sequences:

// These produce the same hash:
keccak256(abi.encodePacked("a", "bc"))   == keccak256(abi.encodePacked("ab", "c"))
keccak256(abi.encodePacked("", "abc"))   == keccak256(abi.encodePacked("a", "bc"))

This enables signature replay and access control bypass when packed encoding is used in signature verification, permit systems, or authentication:

// Vulnerable: signature over packed dynamic types
function verify(string[] calldata admins, string[] calldata users, bytes calldata sig) external {
    bytes32 hash = keccak256(abi.encodePacked(admins, users));
    require(ecrecover(hash, sig) == trustedSigner);
    // Attacker moves elements between admins[] and users[] to forge a valid hash
}

The fix is straightforward: use abi.encode(), which pads each argument to 32 bytes with length prefixes, eliminating ambiguity.

T2: Storage Slot Collision in Proxy Patterns (Critical)

Solidity stores mapping values at keccak256(key . slot) and dynamic array elements at keccak256(slot) + index, where . denotes concatenation. In upgradeable proxy contracts using delegatecall, the implementation contract’s code executes against the proxy’s storage. If the proxy and implementation define different variables at the same slot numbers, KECCAK256-derived storage locations collide silently:

  • Adding a new state variable to a base contract shifts all derived slot numbers
  • Reordering inheritance changes slot assignment order
  • The proxy’s own administrative variables (e.g., proxyAdmin at slot 0) can collide with the implementation’s initialization flags

These collisions corrupt state without any revert or error. The EVM writes to the KECCAK256-derived slot regardless of what “should” be there, because storage has no type information at the EVM level.

T3: CREATE2 Address Prediction and Front-Running (High)

CREATE2 derives the deployment address deterministically: address = keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode))[12:]. Because the address is known before deployment, attackers can:

  • Front-run deployments: Observe a pending CREATE2 transaction in the mempool, deploy a malicious contract at the same address first (using a different factory with matching salt/code), or grief by deploying at the address to make the victim’s deployment revert
  • Deploy-destruct-redeploy: Deploy a benign contract to gain trust, SELFDESTRUCT it, then redeploy malicious code at the same address (the salt and init code hash must match, but the init code can contain constructor logic that reads mutable state)
  • Address poisoning: Generate CREATE2 addresses that visually resemble legitimate ones to trick users into sending funds

T4: Function Selector Collision (High)

Function selectors are the first 4 bytes of keccak256(signature). With only 2^32 possible selectors, collisions are trivially manufacturable by brute-force (~77,000 functions reach 50% collision probability via the birthday paradox). This matters in:

  • Proxy patterns: A transparent or diamond proxy routes calls based on selector. If a user-facing function collides with an admin function, unauthorized calls can reach privileged logic
  • Cross-contract calls: When abi.encodeWithSelector constructs calldata, a selector collision can route the call to an unintended function on the target
  • ABI compatibility: Third-party tools and wallets use selectors for decoding; collision causes misinterpretation

The Solidity compiler prevents selector collisions within a single contract, but does not prevent collisions across proxy/implementation boundaries or cross-contract calls.

T5: Gas Griefing via Large Input (Medium)

KECCAK256 costs 30 + 6 * ceil(length / 32) gas, plus memory expansion cost. If an attacker controls the length parameter (directly or via data size), they can force expensive hashing:

  • Hashing 1 MB of data costs ~192,000 gas (6 * 32,000 words) plus substantial memory expansion
  • Memory expansion cost is quadratic: memory_cost = 3 * words + words^2 / 512
  • A contract that hashes user-supplied calldata (keccak256(msg.data)) without length bounds is vulnerable to gas griefing

EIP-7667 proposes raising KECCAK256 gas costs by 10x (to 300 + 60 per word) to align with ZK-EVM prover costs, which would further amplify the griefing potential of existing contracts that don’t bound input length.

T6: Preimage Attacks (Theoretical / Low)

Finding a preimage x such that keccak256(x) == target requires ~2^256 operations — computationally infeasible. Finding a second preimage (different x' with the same hash as known x) requires ~2^256 operations. Finding any collision pair requires ~2^128 operations (birthday bound), also infeasible with current technology.

While not a practical threat today, this context matters because:

  • Quantum computers with ~2^128 gates could theoretically find collisions (Grover’s algorithm halves the search space for preimage to ~2^128)
  • Contracts that assume hash uniqueness for permanent commitments (e.g., content-addressable storage) would break if collision resistance fails
  • The EVM has no mechanism to upgrade its hash function — KECCAK256 is hardcoded

T7: Mapping Key Type Confusion (Medium)

Solidity computes mapping storage slots as keccak256(abi.encode(key, slot)). If two mappings at different slot numbers use keys of different types that happen to ABI-encode identically, their storage locations can collide. In practice this is rare because abi.encode pads values to 32 bytes with type-specific encoding, but edge cases exist:

  • uint256(1) and int256(1) encode identically (both are 32-byte zero-padded 1)
  • bytes32(value) and uint256(value) encode identically
  • In proxy patterns where slot numbers shift between upgrades, previously non-colliding keys can start colliding

Protocol-Level Threats

P1: Dynamic Gas Cost DoS (Medium)

Unlike fixed-cost opcodes, KECCAK256’s gas scales with input length. A transaction that hashes a large memory region consumes gas proportional to O(length) + O(memory_expansion^2). Within a single block:

  • At 30M gas limit with original pricing (6 gas/word), a single transaction can hash ~160 MB of data
  • This asymmetry (cheap to request, expensive for validators to compute) makes KECCAK256 a vector for block-stuffing attacks
  • EIP-7667 addresses this by raising costs to 60 gas/word, reducing maximum hashable data to ~16 MB per block

Validators must compute every KECCAK256 in every transaction. A block filled with KECCAK256-heavy transactions takes longer to validate, potentially causing propagation delays and increasing uncle/orphan rates.

P2: Memory Expansion Cost Interaction (Medium)

KECCAK256 reads from memory, which may trigger memory expansion. The expansion cost is quadratic: 3 * new_words + new_words^2 / 512. A contract that computes keccak256(memory[0:length]) with attacker-controlled length pays:

LengthHash GasMemory Expansion GasTotal
32 bytes36~3~39
1 KB222~99~321
32 KB6,174~3,168~9,342
1 MB196,638~2,097,152~2,293,790

The quadratic memory cost dominates for large inputs. A griefing attacker targets contracts where they control the memory offset or length to maximize the victim’s gas expenditure.


Edge Cases

Edge CaseBehaviorSecurity Implication
Empty input (length = 0)Returns 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470Well-defined; used by EXTCODEHASH for EOAs. Contracts checking “is this an EOA?” compare against this constant
Single byte (e.g., 0x00)Returns 0xbc36789e7a1e281436464229828f817d6612f7b477d66591ff96a9e064bcc98aDifferent from empty input; length matters
Single zero word (32 bytes of 0x00)Returns 0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563This is keccak256(abi.encode(0)) — appears in storage slot calculations for mapping[0]
Very large input (> 1MB)Valid but extremely expensive (quadratic memory cost)Gas griefing vector; contracts must bound input length
Hash of a hashValid 32-byte input, produces a new 256-bit hashUsed in Merkle trees, commit-reveal schemes, key derivation
Input crossing memory boundaryMemory auto-expands; no revertImplicit memory expansion cost can surprise callers
Maximum offset + length near 2^256Reverts due to gas cost overflowPrevents unbounded memory allocation

Real-World Exploits

Exploit 1: Poly Network — $611M via Function Selector Collision (August 2021)

Root cause: The cross-chain bridge’s EthCrossChainManager contract executed arbitrary cross-chain messages without adequate validation. The attacker crafted a message that, when processed, produced calldata whose 4-byte selector collided with the privileged putCurEpochConPubKeyBytes function on the EthCrossChainData contract.

Details: The verifyHeaderAndExecuteTx function accepted cross-chain payloads and made a low-level call to a target contract using attacker-controlled calldata. The attacker constructed a payload where the KECCAK256-derived selector of their crafted function signature matched the selector of putCurEpochConPubKeyBytes(bytes) — the function responsible for updating the bridge’s keeper public keys.

By replacing the keeper keys with their own, the attacker gained the ability to sign arbitrary withdrawal messages, draining:

  • ~$273M on Ethereum (ETH, USDC, USDT, DAI, UNI, SHIB)
  • ~$253M on BSC (BNB, BUSD)
  • ~$85M on Polygon (USDC)

KECCAK256’s role: The 4-byte truncation of KECCAK256 output (function selector) was the fundamental enabler. The full 256-bit hash would not have collided, but the 32-bit selector space made collision trivial to manufacture.

References:


Exploit 2: Audius Governance Takeover — $6M via Storage Slot Collision (July 2022)

Root cause: Storage collision between the proxy contract’s proxyAdmin address at slot 0 and OpenZeppelin’s Initializable contract’s initialized/initializing boolean flags, also at slot 0. The KECCAK256-derived storage layout was correct individually for each contract, but when composed via delegatecall, the raw slot numbers collided.

Details: The AudiusAdminUpgradabilityProxy stored the proxy admin address in slot 0. When the implementation contract (which inherited from Initializable) executed via delegatecall, it read slot 0 to check the initialized flag. The proxy admin address 0x...ac had its lowest byte interpreted as true for initialized, and the second byte 0xab interpreted as true for initializing. This combination caused the initializer() modifier to always pass, allowing the attacker to re-initialize:

  1. The Governance contract — redefining voting parameters
  2. The Staking contract — creating 10 trillion fraudulent $AUDIO tokens
  3. The DelegateManagerV2 contract — gaining delegation authority

The attacker passed a malicious governance proposal and transferred 18 million $AUDIO (~$6M) from the community treasury.

KECCAK256’s role: While the collision was at the raw slot level (slot 0 vs slot 0), the broader storage layout — including all mapping and array slots derived via KECCAK256 — was corrupted. The exploit demonstrates that even a single raw-slot collision in a proxy pattern can undermine the entire KECCAK256-derived storage hierarchy.

References:


Exploit 3: CREATE2 Address Exploitation — $60M in Scams (2023-2024)

Root cause: CREATE2’s deterministic address derivation (keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode))) allows attackers to generate contract addresses with no prior transaction history, bypassing wallet security heuristics.

Details: Security researchers documented a sustained campaign where threat actors exploited CREATE2 in two main patterns:

  1. Clean address generation: Attackers used CREATE2 to deploy contracts at fresh addresses, then tricked users into signing token approvals or transfers to these addresses. Because the addresses had no transaction history, wallet security tools flagged them as “safe” new addresses rather than known malicious ones.

  2. Address poisoning: Attackers brute-forced CREATE2 salts to generate addresses with leading/trailing bytes matching legitimate addresses (e.g., matching the first 4 and last 4 hex characters). Users who copied addresses from transaction history sent funds to the attacker’s visually similar address. One victim lost $1.6M in a single transaction.

Over six months, approximately 100,000 victims lost a combined ~$60M to these techniques.

KECCAK256’s role: The deterministic address derivation via KECCAK256 is what makes CREATE2 addresses predictable before deployment. The attacker’s ability to iterate over salts and compute keccak256(...) off-chain until finding a desirable address prefix is a direct exploitation of KECCAK256’s determinism.

References:


Exploit 4: Solidity Optimizer Keccak Caching Bug (March 2021)

Root cause: The Solidity bytecode optimizer incorrectly cached KECCAK256 results. When the same memory region was hashed with different lengths, the optimizer reused the cached result from the first call, producing incorrect hashes.

Details: The optimizer treated keccak256(mpos, length1) and keccak256(mpos, length2) as equivalent if ceil(length1/32) == ceil(length2/32) and the memory contents were deducible as equal. This meant keccak256(memory[0:32]) and keccak256(memory[0:23]) could return the same value, despite hashing different-length inputs.

This affected contracts compiled with optimizer enabled (Solidity 0.8.0 - 0.8.2) that computed multiple KECCAK256 hashes of the same memory with different lengths in inline assembly. High-level Solidity code was largely unaffected because the compiler doesn’t normally generate this pattern.

KECCAK256’s role: The bug was directly in how the compiler optimized KECCAK256 operations. It violated the fundamental property that keccak256(x) != keccak256(y) when x != y (with overwhelming probability), by silently substituting a cached incorrect result.

References:


Exploit 5: abi.encodePacked Collision Findings (Recurring Pattern, 2019-Present)

Root cause: Smart contracts using keccak256(abi.encodePacked(...)) with multiple variable-length arguments for signature verification or access control.

Details: This is a recurring vulnerability class rather than a single exploit. Audit firms and bug bounty platforms have documented hundreds of findings where abi.encodePacked with dynamic types enables hash collision:

  • Signature replay across parameters: A message signed over abi.encodePacked(string1, string2) can be replayed with different string boundaries
  • Multi-token approvals: Packed encoding of (address[] tokens, uint256[] amounts) allows shuffling between arrays
  • Merkle tree leaf collisions: Merkle trees built with keccak256(abi.encodePacked(leaf)) where leaves contain dynamic types

The SWC Registry (SWC-133) classifies this as “Hash Collisions With Multiple Variable Length Arguments.” Slither, MythX, and other static analysis tools flag this pattern.

References:


Attack Scenarios

Scenario A: abi.encodePacked Signature Bypass

contract VulnerableMultiSig {
    address public trustedSigner;
 
    function execute(
        string calldata to,
        string calldata functionSig,
        bytes calldata signature
    ) external {
        // Vulnerable: packed encoding of two strings has no delimiter
        bytes32 hash = keccak256(abi.encodePacked(to, functionSig));
        require(recover(hash, signature) == trustedSigner, "invalid sig");
        // Attacker shifts bytes between 'to' and 'functionSig' to forge a valid hash
        // e.g., signed("contractA", "withdraw()") replays as ("contractAw", "ithdraw()")
    }
}
 
// Fix: use abi.encode() which pads each argument to 32 bytes
// bytes32 hash = keccak256(abi.encode(to, functionSig));

Scenario B: CREATE2 Metamorphic Contract

contract MetamorphicFactory {
    // Deploy a "safe" contract, gain trust, then redeploy malicious code
    function deploy(bytes32 salt, bytes memory initCode) external returns (address) {
        address addr;
        assembly {
            addr := create2(0, add(initCode, 0x20), mload(initCode), salt)
        }
        return addr;
        // Address = keccak256(0xff ++ address(this) ++ salt ++ keccak256(initCode))
        // After SELFDESTRUCT (pre-Dencun), attacker redeploys different code at same address
        // Post-Dencun: SELFDESTRUCT no longer removes code, mitigating this vector
    }
}
 
// Attack flow:
// 1. Deploy benign token contract at predictable address via CREATE2
// 2. Users approve tokens to interact with the "safe" contract
// 3. SELFDESTRUCT the contract (pre-Dencun)
// 4. Redeploy malicious code at the same address using same salt + different initCode
//    (requires different factory or constructor that reads mutable state)
// 5. Malicious code drains approved tokens

Scenario C: Storage Collision in Proxy Upgrade

// Version 1: Implementation contract
contract VaultV1 {
    address public owner;      // slot 0
    uint256 public totalAssets; // slot 1
    mapping(address => uint256) public balances; // slot 2 (values at keccak256(key, 2))
}
 
// Version 2: New base contract added in inheritance chain
contract AccessControl {
    mapping(address => bool) public admins; // slot 0 (values at keccak256(key, 0))
}
 
contract VaultV2 is AccessControl {
    address public owner;      // slot 1 (SHIFTED! was slot 0 in V1)
    uint256 public totalAssets; // slot 2 (SHIFTED! was slot 1 in V1)
    mapping(address => uint256) public balances; // slot 3 (SHIFTED! values now at keccak256(key, 3))
}
 
// After upgrade via proxy:
// - V1's balances[addr] was at keccak256(addr, 2) -- these funds are now orphaned
// - V2's balances[addr] reads keccak256(addr, 3) -- empty, all balances appear zero
// - V2's admins[addr] reads keccak256(addr, 0) -- collides with V1's owner slot data

Scenario D: Gas Griefing via Unbounded Hash

contract VulnerableRegistry {
    mapping(bytes32 => address) public registry;
 
    // Vulnerable: hashes arbitrary-length user input with no bound
    function register(bytes calldata data) external {
        bytes32 key = keccak256(data);
        require(registry[key] == address(0), "already registered");
        registry[key] = msg.sender;
        // Attacker passes 1MB of data:
        // Hash gas: 30 + 6 * 32768 = ~196K gas
        // Memory expansion: ~2M gas (quadratic)
        // Legitimate users' transactions get crowded out
    }
}
 
// Fix: bound input length
// require(data.length <= MAX_DATA_LENGTH, "input too large");

Scenario E: Selector Collision in Proxy

// Transparent proxy with admin function
contract TransparentProxy {
    function upgradeTo(address newImpl) external onlyAdmin {
        // selector: 0x3659cfe6
        _setImplementation(newImpl);
    }
    
    fallback() external payable {
        _delegate(_implementation());
    }
}
 
// Implementation contract with user function
contract Implementation {
    // An attacker finds that gasOptimize_d8b2c94f(address) has selector 0x3659cfe6
    // If the proxy doesn't properly separate admin/user selectors,
    // calling gasOptimize_d8b2c94f() could route to upgradeTo()
    function gasOptimize_d8b2c94f(address target) external {
        // This function's selector collides with upgradeTo(address)
    }
}

Mitigations

ThreatMitigationImplementation
T1: abi.encodePacked collisionUse abi.encode() instead of abi.encodePacked() for hashingkeccak256(abi.encode(arg1, arg2)) — adds 32-byte padding per argument
T1: Dynamic type hashingUse EIP-712 structured data hashingType-hash prefix eliminates cross-type collision; widely supported by wallets
T2: Storage slot collisionUse EIP-1967 storage slots (randomized via hash)bytes32 slot = keccak256("eip1967.proxy.implementation") - 1 at a known random slot
T2: Proxy upgrade safetyUse OpenZeppelin’s storage gap patternuint256[50] private __gap; reserves slots for future variables
T3: CREATE2 front-runningInclude msg.sender in the saltsalt = keccak256(abi.encode(msg.sender, userSalt)) prevents third-party replication
T3: Metamorphic contractsPost-Dencun: SELFDESTRUCT no longer removes codeEIP-6780 removes code-clearing behavior except in same-transaction SELFDESTRUCT
T4: Selector collisionUse OpenZeppelin’s transparent proxy patternAdmin calls are routed separately from user calls based on msg.sender
T4: Cross-contract collisionVerify selector uniqueness across proxy + implementationAutomated tooling: cast selectors (Foundry), Slither selector collision detector
T5: Gas griefingBound input length before hashingrequire(data.length <= MAX_LEN) before any keccak256(data) call
T5: Memory expansion DoSCap memory usage; avoid hashing user-controlled lengthsUse calldatacopy to fixed-size buffers rather than unbounded memory
T6: Preimage resistanceNo action needed for current security levelMonitor NIST post-quantum standardization for long-term hash migration
T7: Mapping key confusionUse distinct slot numbers; avoid type aliasingStorage layout tools: forge inspect <contract> storage-layout

Protocol/EIP-Based Protections

  • EIP-1967 (2019): Standardized storage slots for proxy contracts. Admin, implementation, and beacon addresses are stored at KECCAK256-derived pseudo-random slots (e.g., keccak256("eip1967.proxy.implementation") - 1), avoiding collision with sequential slot assignment.
  • EIP-712 (2017): Structured data hashing standard. Prefixes each hash with a domain separator and type hash, preventing cross-domain and cross-type collision. Standard for off-chain signature verification.
  • EIP-6780 (Dencun, 2024): Neutered SELFDESTRUCT to only send ETH without clearing code/storage (except in the creation transaction). This mitigates the metamorphic contract attack vector where CREATE2 addresses are reused after destruction.
  • EIP-7667 (Proposed): Raises KECCAK256 gas cost from 30 + 6/word to 300 + 60/word, reducing the maximum data hashable per block by 10x and aligning gas costs with ZK-EVM prover expenses.

Static Analysis

  • Slither: Detects abi.encodePacked with dynamic types (detector: encode-packed-collision), proxy storage collisions, and selector clashes
  • MythX/Mythril: Symbolic execution can detect reachable hash collision paths
  • Foundry: forge inspect <contract> storage-layout visualizes slot assignments; cast selectors lists all function selectors for collision checking

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighHighSWC-133; hundreds of audit findings
T2Smart ContractCriticalMediumAudius ($6M), ~1.5M vulnerable proxy contracts
T3Smart ContractHighHigh$60M in CREATE2 scams (2023-2024)
T4Smart ContractHighMediumPoly Network ($611M)
T5Smart ContractMediumMediumEIP-7667 motivated by griefing potential
T6Smart ContractLowTheoreticalNo known cryptanalytic break
T7Smart ContractMediumLowProxy upgrade storage corruption
P1ProtocolMediumMediumBlock-stuffing with hash-heavy transactions
P2ProtocolMediumMediumQuadratic memory cost amplification

OpcodeRelationship
CODECOPY (0x39)Copies bytecode into memory for hashing; used with KECCAK256 for code verification and CREATE2 init code hashing
CALLDATACOPY (0x37)Copies calldata into memory for hashing; common pattern in signature verification: calldatacopy then keccak256
CREATE2 (0xF5)Uses keccak256(0xff ++ deployer ++ salt ++ keccak256(initCode)) for deterministic address derivation
EXTCODEHASH (0x3F)Returns keccak256(code) of an account; returns the empty-input hash for EOAs
MSTORE (0x52)Writes data to memory before KECCAK256 reads it; incorrect MSTORE offset/value corrupts hash input
RETURNDATACOPY (0x3E)Copies return data into memory for hashing; used in cross-contract verification patterns
LOG0-LOG4 (0xA0-0xA4)Event topics are KECCAK256 hashes of event signatures; topic[0] = keccak256("EventName(type1,type2)")