Opcode Summary

PropertyValue
Opcode0xF2
MnemonicCALLCODE
Gasdynamic (same formula as CALL: access_cost + mem_expansion_cost + value_transfer_surcharges)
Stack Inputgas, addr, val, argOst, argLen, retOst, retLen
Stack Outputsuccess (1 if the call succeeded, 0 if it reverted or failed)
BehaviorExecutes the code at addr in the caller’s storage context, similar to DELEGATECALL, but does not preserve msg.sender or msg.value from the original caller. Inside the called code, msg.sender is the address of the contract that executed CALLCODE (not the external account that initiated the transaction chain), and msg.value can be set to a new value via the val stack argument. Storage reads and writes target the calling contract’s slots. Deprecated at the Solidity level since v0.5.0; superseded by DELEGATECALL (EIP-7, Homestead 2016). The EVM opcode 0xF2 remains active — EIP-2488 proposed protocol-level deprecation but is stagnant.

Threat Surface

CALLCODE is a deprecated opcode that still executes on every Ethereum node. It was introduced in the Frontier release as the original mechanism for “library-style” code reuse — run another contract’s logic against your own storage. It was almost immediately recognized as flawed because it rewrites msg.sender to the calling contract’s address instead of preserving the original caller, breaking the identity chain that proxy and library patterns depend on. DELEGATECALL (EIP-7, Homestead 2016) was introduced specifically as a bug fix.

The threat surface centers on four properties:

  1. CALLCODE is deprecated but not removed. Solidity 0.5.0 (November 2018) removed callcode as a language keyword, producing a compilation error: "callcode" has been deprecated in favour of "delegatecall". However, the EVM opcode 0xF2 remains fully functional. Any contract deployed before Solidity 0.5.0, or any contract written in raw bytecode or assembly, can still use CALLCODE. EIP-2488 proposed making CALLCODE always return failure at the protocol level, but the proposal stagnated and was never adopted. This creates a permanent gap: developers assume CALLCODE is “gone,” but legacy bytecode containing 0xF2 executes identically to how it did on day one.

  2. CALLCODE and DELEGATECALL are semantically confusable. Both opcodes execute external code in the caller’s storage context. The only difference is identity propagation: DELEGATECALL preserves msg.sender and msg.value from the parent call frame; CALLCODE does not — it sets msg.sender to the calling contract and allows msg.value to be overridden. Developers who encounter CALLCODE in legacy code, audit reports, or disassembly may assume it behaves like DELEGATECALL, missing the msg.sender discrepancy. This confusion has led to incorrect security assessments of legacy contracts.

  3. Legacy contracts with CALLCODE are immutable and permanently deployed. Pre-Homestead and early post-Homestead contracts that used CALLCODE for library delegation are frozen on-chain. They cannot be recompiled with DELEGATECALL. If these contracts hold ETH, tokens, or govern state for other contracts, the CALLCODE semantics are permanently baked in — including the msg.sender behavior that their original developers may not have fully understood.

  4. CALLCODE accepts a value parameter that DELEGATECALL does not. CALLCODE takes val as a stack argument, allowing the caller to specify an ETH value to forward. DELEGATECALL has no value parameter and inherits msg.value from the parent frame. This difference means CALLCODE can trigger unexpected ETH transfers within a storage-delegated context, an interaction model that does not exist with DELEGATECALL.


Smart Contract Threats

T1: Confusion Between CALLCODE and DELEGATECALL — msg.sender Identity Break (High)

The most consequential difference between CALLCODE and DELEGATECALL is msg.sender propagation. When contract A calls contract B, and B uses CALLCODE to execute contract C’s code:

  • CALLCODE: msg.sender inside C’s code is address(B) — the contract that invoked CALLCODE.
  • DELEGATECALL: msg.sender inside C’s code is the address that called B (could be A, or the original EOA).

This means any library code executed via CALLCODE that checks msg.sender for access control sees the proxy/caller contract as the sender, not the end user. The implications include:

  • Broken access control in libraries. A library function that checks require(msg.sender == owner) will fail when invoked via CALLCODE from a proxy, because msg.sender is the proxy address, not the user who called the proxy. The same code works correctly under DELEGATECALL.

  • Silent privilege escalation in legacy proxies. If a pre-Homestead proxy uses CALLCODE to delegate to a library that grants permissions based on msg.sender, the proxy itself (not the user) is the authorized entity. Every user’s call appears to come from the same address (the proxy), collapsing all user identities into one.

  • Incorrect audit conclusions. Auditors reviewing bytecode that contains 0xF2 may mentally model it as DELEGATECALL, especially since both opcodes share the same storage-delegation semantics. Missing the msg.sender difference leads to incorrect access control analysis.

Why it matters: The CALLCODE/DELEGATECALL confusion creates a class of bugs where identity-based authorization silently fails in one context but works in another. Legacy contracts cannot be fixed.

T2: Storage Context Execution with Wrong msg.sender (High)

CALLCODE executes the target’s code against the caller’s storage, just like DELEGATECALL. But because msg.sender is rewritten to the calling contract’s address, the called code operates in a hybrid context that was never intended by either opcode’s original design:

  • Storage writes with wrong authority. If the called code writes to storage slots based on msg.sender (e.g., balances[msg.sender] += amount), the write targets the calling contract’s storage but uses the calling contract’s address as the key — not the end user’s address. This can silently credit the proxy/caller instead of the user.

  • Storage layout collisions with identity mismatch. CALLCODE shares DELEGATECALL’s storage collision risks (the called code’s storage layout must match the caller’s), but adds an additional failure mode: even when storage layouts align perfectly, the msg.sender-dependent logic operates on the wrong identity, producing correct storage writes to incorrect logical accounts.

  • Re-entrancy with collapsed identity. If the called code makes an external call that re-enters the caller, msg.sender in the re-entrant context is still the calling contract (not the original user), potentially bypassing reentrancy guards that check caller identity.

Why it matters: CALLCODE creates a unique threat model where storage delegation works correctly but identity delegation does not, producing bugs that are invisible in storage-only analysis.

T3: Legacy Contracts Still Using CALLCODE (Medium)

Contracts deployed before the Homestead hard fork (March 2016) or compiled with Solidity < 0.5.0 may contain CALLCODE in their bytecode. These contracts are immutable on-chain:

  • No upgrade path for non-proxy contracts. If a contract uses CALLCODE directly (not behind a proxy), there is no way to replace the 0xF2 instruction with 0xF4 (DELEGATECALL). The contract’s behavior is permanent.

  • Unaudited CALLCODE assumptions. Early Ethereum contracts were written when CALLCODE was the only option for code delegation. Developers at the time may have assumed msg.sender would be preserved (the intended behavior that DELEGATECALL later provided), embedding flawed identity assumptions into contract logic.

  • Governance and multi-sig wallets. Pre-2016 multi-sig wallets or governance contracts that use CALLCODE for library delegation may have subtly incorrect authorization semantics. If these contracts still hold funds or control permissions, the CALLCODE behavior is a live risk.

  • EIP-2488 stagnation. The proposal to deprecate CALLCODE at the protocol level (returning failure for all 0xF2 calls) was never adopted. This means there is no protocol-level forcing function to identify or migrate affected contracts.

Why it matters: Immutable legacy bytecode with CALLCODE creates a long tail of contracts with potentially incorrect identity semantics that can never be patched.

T4: Unexpected Value Transfer Behavior (Medium)

CALLCODE accepts a val stack argument specifying how much ETH to forward with the call. This creates interactions that don’t exist with DELEGATECALL:

  • ETH transfer within storage-delegated context. CALLCODE can send ETH from the calling contract to the called contract’s address as part of a storage-delegated call. Since the code executes against the caller’s storage, the ETH transfer is the only operation that actually affects the called contract. This split — storage on the caller, ETH on the callee — is unintuitive and not replicated by any other opcode.

  • msg.value customization. Inside the called code, msg.value reflects the val argument passed to CALLCODE, not the original transaction’s value. A malicious or buggy caller can set val to an arbitrary amount (up to its balance), changing the economic context the library code operates in. With DELEGATECALL, msg.value is inherited and cannot be spoofed.

  • Gas stipend interaction. When val > 0, CALLCODE adds a 2300-gas stipend to the forwarded gas (same as CALL). This means value-bearing CALLCODE calls get more execution gas than zero-value calls, potentially enabling operations that the caller did not anticipate.

Why it matters: The value parameter makes CALLCODE a hybrid between CALL (value transfer) and DELEGATECALL (storage delegation), creating an execution model that is difficult to reason about securely.

T5: Deprecation Does Not Mean Removal — False Security Assumptions (Low)

The Solidity-level deprecation of CALLCODE creates a false sense of security:

  • Bytecode-level persistence. Solidity’s removal of callcode only prevents new contracts from using the keyword. The EVM continues to execute 0xF2 opcodes in already-deployed contracts and in contracts written with inline assembly (assembly { ... callcode(...) ... }), Yul, or other EVM-targeting languages.

  • Tooling gaps. Static analysis tools and security scanners may not flag CALLCODE in disassembled bytecode because it’s considered “deprecated.” This creates blind spots in automated security assessments of legacy contracts.

  • EVM implementation burden. Every EVM client (Geth, Nethermind, Besu, Erigon, Reth) must maintain CALLCODE support indefinitely. Implementation bugs in CALLCODE handling could affect consensus, despite the opcode’s deprecated status.

  • Cross-chain semantics. L2s and EVM-compatible chains must decide whether to support CALLCODE. Some may omit it (breaking legacy contract compatibility), while others implement it with subtly different gas schedules or behavior.

Why it matters: “Deprecated” status creates organizational complacency while the technical risk persists unchanged at the bytecode level.


Protocol-Level Threats

P1: EIP-2488 Stagnation — No Protocol-Level Deprecation Path (Low)

EIP-2488 proposed making CALLCODE always return failure (push 0 to the stack) after a specified fork block. The proposal acknowledged this is a breaking change but argued “no contracts of any value should be affected.” The EIP has been stagnant since its introduction and was never scheduled for any hard fork.

Security implications:

  • Permanent EVM complexity. Every client must implement and test CALLCODE indefinitely, increasing the attack surface of the consensus layer. A bug in any client’s CALLCODE implementation could cause a chain split.

  • No migration incentive. Without a protocol-level deadline, there is no incentive for teams to audit legacy contracts for CALLCODE usage or migrate to DELEGATECALL-based patterns.

  • EIP-7069 (Revamped CALL Instructions). EIP-7069 proposes new CALL variants that would eventually supersede all legacy CALL opcodes including CALLCODE. If adopted, CALLCODE would become doubly deprecated — at both the language and instruction-set level — but still executable.

P2: Consensus Risk from Rarely-Tested Code Path (Low)

CALLCODE is exercised far less frequently than CALL, DELEGATECALL, or STATICCALL. Rarely-tested code paths in EVM implementations are more likely to harbor bugs:

  • Differential behavior across clients. Edge cases in CALLCODE’s gas calculation, value forwarding, or failure semantics may diverge between clients. These bugs would only manifest when a transaction actually uses CALLCODE, making them difficult to detect through standard testing.

  • Fuzzing coverage gaps. EVM fuzzers (like evmone-fuzzer or goevmlab) may under-represent CALLCODE relative to DELEGATECALL in their opcode distributions, reducing the likelihood of finding implementation bugs.


Edge Cases

Edge CaseBehaviorSecurity Implication
msg.sender in CALLCODESet to the address of the contract executing CALLCODE, not the original external callerBreaks identity propagation; library code that checks msg.sender for authorization sees the proxy, not the user. All users appear as the same address.
msg.value in CALLCODESet to the val argument passed on the stack; does not inherit from the parent call frameCaller can specify arbitrary ETH value, changing the economic context of the called code. Differs from DELEGATECALL which inherits msg.value.
Storage contextAll SLOAD/SSTORE operations in the called code read/write the calling contract’s storageSame as DELEGATECALL. Storage layout mismatch between caller and callee causes silent data corruption.
Value transfer destinationETH specified by val is debited from the calling contract’s balance and credited to addr (the code address)Unlike DELEGATECALL (no value transfer), CALLCODE can move ETH during a storage-delegated call. The ETH goes to the target contract, not the caller.
CALLCODE to non-existent addressReturns success (1) with no code execution; any ETH specified by val is transferred to the addressCan silently create new accounts and transfer ETH. No revert on empty target.
CALLCODE with val > balanceReturns failure (0); no state changes occurInsufficient balance causes silent failure, not a revert of the entire transaction. Caller must check the return value.
Gas stipend when val > 02300-gas stipend added to forwarded gas (same as CALL)Value-bearing CALLCODE calls get extra execution gas; zero-value calls do not. Can affect whether called code runs out of gas.
CALLCODE in STATICCALL contextProhibited; causes the entire STATICCALL frame to revert if CALLCODE attempts a state changeCALLCODE with val > 0 always fails in a static context. CALLCODE with val == 0 may succeed if the called code performs no state changes.
CALLCODE to precompiled contractExecutes the precompile in the caller’s context; precompile result is returned normallyPrecompiles do not use msg.sender, so the CALLCODE identity rewrite is irrelevant for precompile calls.
Depth limit (1024 call frames)CALLCODE fails (returns 0) if the call stack depth is at 1024Same depth limit as CALL/DELEGATECALL. Must check return value.

Real-World Exploits

Exploit 1: Parity Multi-Sig Wallet — Library Takeover and $150M Frozen (July + November 2017)

Root cause: Parity’s multi-sig wallet used a shared library pattern with delegatecall for code delegation. While the production wallets used DELEGATECALL (not CALLCODE), the exploit is directly relevant because it demonstrates the exact class of vulnerability that CALLCODE’s msg.sender behavior would have worsened — and because CALLCODE was the original mechanism for this pattern before DELEGATECALL existed.

Details: Parity’s wallet architecture split logic between thin proxy wallets and a shared WalletLibrary contract. All state-modifying calls were forwarded via delegatecall. The library’s initWallet() function assigned ownership based on msg.sender, with no guard against re-initialization.

In the first attack (July 2017), an attacker called initWallet() through individual proxy wallets. Because DELEGATECALL preserved the attacker’s msg.sender, the library code executed in the proxy’s storage, setting the attacker as the owner. The attacker drained ~$30M across three ICO wallets.

In the second attack (November 2017), an attacker called initWallet() directly on the library contract (not through a proxy). Since the library was never initialized, the attacker became its owner and called kill() (SELFDESTRUCT), destroying the shared library. This bricked all 587 wallets that depended on it, freezing 513,774 ETH ($150M).

CALLCODE’s relevance: Had these wallets used CALLCODE instead of DELEGATECALL (as they would have been forced to do pre-Homestead), the msg.sender inside the library would have been the proxy contract’s address rather than the end user. This would have made the first attack more complex (the attacker couldn’t directly claim ownership through the proxy) but would have introduced different bugs: all users’ actions would appear to originate from the proxy, collapsing identity-based access control. The Parity hack demonstrates why the CALLCODE-to-DELEGATECALL migration was security-critical, and why any legacy contract that never made this migration carries residual risk.

Impact: ~150M permanently frozen (November). The defining case study for proxy pattern security.

References:


Exploit 2: Pre-Homestead Library Delegation Bugs — msg.sender Confusion (2015-2016, Recurring)

Root cause: Before DELEGATECALL existed (pre-Homestead, March 2016), CALLCODE was the only mechanism for library-style code delegation. Developers assumed msg.sender would propagate correctly through the call chain, but CALLCODE rewrote it to the calling contract’s address.

Details: During Ethereum’s Frontier era (July 2015 - March 2016), early smart contract developers attempted to implement shared library patterns using CALLCODE. The expectation was that a proxy contract could forward calls to a library while preserving the original caller’s identity — the exact use case that DELEGATECALL was later designed to serve.

However, CALLCODE set msg.sender to the proxy’s address in the library’s execution context. This meant:

  • Library functions that checked msg.sender for authorization treated every user identically (as the proxy address).
  • Token transfer functions that credited msg.sender credited the proxy, not the user.
  • Event emissions that logged msg.sender recorded the proxy address, making transaction attribution impossible.

These bugs were difficult to detect because they were semantic rather than execution failures — the code ran without reverting but produced logically incorrect results. The Ethereum community recognized this as a fundamental design flaw, leading Vitalik Buterin and others to propose EIP-7 (DELEGATECALL) specifically as a fix.

CALLCODE’s role: CALLCODE was the direct cause. Its failure to propagate msg.sender meant that the most natural pattern for code reuse — “run this library code on my behalf” — was broken at the identity level. EIP-7 explicitly states that DELEGATECALL was created because CALLCODE “does not provide the ability to [access the real sender and value of the parent call].”

Impact: No single large-dollar exploit, but a systemic class of identity confusion bugs across early Ethereum contracts. Led directly to EIP-7 and the eventual deprecation of CALLCODE.

References:


Exploit 3: Arbitrary Call Vulnerabilities in Delegated Execution Patterns ($17M+, 2025-2026)

Root cause: Contracts that forward arbitrary calldata to external addresses via low-level call opcodes (CALL, CALLCODE, DELEGATECALL) without validating the target or selector, enabling attackers to invoke arbitrary functions with the contract’s authority.

Details: Multiple high-profile exploits in 2025-2026 demonstrated the danger of unconstrained call forwarding, the same pattern that CALLCODE enables at the opcode level:

  • SwapNet Attack (January 2026, $13.43M): Attackers exploited insufficient input validation to redirect call execution to token contracts, invoking transferFrom() to steal user-approved tokens.
  • Aperture Finance (January 2026, $3.67M): Arbitrary-call vulnerabilities across Ethereum, Arbitrum, and Base allowed unauthorized token transfers by bypassing calldata and target address constraints.
  • 1inch Fusion v1 (March 2025, $5M): A calldata corruption vulnerability in a deprecated contract allowed attackers to forge resolver addresses using EVM-level memory manipulation.

CALLCODE’s relevance: These exploits share CALLCODE’s fundamental risk model — executing external code with the caller’s authority and storage access. While the specific exploits used CALL or DELEGATECALL, any legacy contract using CALLCODE for arbitrary code execution is exposed to the same class of attack, with the additional complication that msg.sender is rewritten, potentially bypassing authorization checks in the called code.

Impact: $17M+ across multiple protocols. Demonstrates that arbitrary code execution vulnerabilities remain actively exploited, and legacy CALLCODE contracts are at equivalent or greater risk.

References:


Attack Scenarios

Scenario A: msg.sender Collapse in Legacy CALLCODE Proxy

// Legacy library (deployed pre-Homestead with CALLCODE semantics)
contract LegacyTokenLibrary {
    mapping(address => uint256) public balances;
 
    function deposit() external payable {
        // VULNERABLE when called via CALLCODE:
        // msg.sender is the proxy contract, NOT the actual user.
        // All deposits are credited to the proxy's address.
        balances[msg.sender] += msg.value;
    }
 
    function withdraw(uint256 amount) external {
        // msg.sender is again the proxy -- ALL users share
        // the same balance entry (the proxy's).
        require(balances[msg.sender] >= amount, "insufficient");
        balances[msg.sender] -= amount;
        payable(msg.sender).transfer(amount);
        // ETH goes to the proxy contract, not the user.
    }
}
 
// Legacy proxy using CALLCODE (pre-Homestead bytecode)
// Pseudocode for the fallback:
//   CALLDATACOPY(0, 0, calldatasize)
//   CALLCODE(gas, libraryAddr, callvalue, 0, calldatasize, 0, 0)
//   RETURNDATACOPY(0, 0, returndatasize)
//   RETURN(0, returndatasize)
 
// Attack: Any user who deposits ETH via the proxy has their funds
// credited to balances[proxy_address]. A single withdrawal by
// ANY user drains all deposited funds, since they all share
// the same balance key.

Scenario B: Value Transfer in Storage-Delegated Context

// Library that assumes msg.value reflects the user's payment
contract PaymentLibrary {
    mapping(address => uint256) public credits;
    address public treasury;
 
    function pay() external payable {
        require(msg.value > 0, "no payment");
        credits[msg.sender] += msg.value;
    }
}
 
// A contract using CALLCODE with a custom value
contract MaliciousProxy {
    address public library;
 
    function exploitPay() external {
        // CALLCODE allows specifying an arbitrary val parameter.
        // Sends 0 ETH but sets val = 1 ether (from proxy balance).
        // The library sees msg.value = 1 ether but the "payment"
        // comes from the proxy's balance, not the user.
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0xPAY_SELECTOR)
            let success := callcode(
                gas(),
                sload(library.slot),
                1000000000000000000, // 1 ETH from proxy balance
                ptr, 4,
                0, 0
            )
        }
        // credits[address(this)] += 1 ether in proxy's storage,
        // funded by proxy's ETH, regardless of external caller.
    }
}

Scenario C: Auditor Misidentification — CALLCODE Assumed as DELEGATECALL

// Contract bytecode contains 0xF2 (CALLCODE), not 0xF4 (DELEGATECALL)
// Auditor's analysis (INCORRECT):
//
//   "The proxy forwards calls via delegatecall to the implementation.
//    msg.sender is preserved, so the access control in the implementation
//    correctly identifies the end user."
//
// Actual behavior (CALLCODE):
//
//   msg.sender inside the implementation = address(proxy)
//   The implementation's require(msg.sender == owner) always checks
//   against the proxy address, not the user.
//   If proxy address happens to match owner, ANY user gets admin access.
//   If it doesn't match, NO user gets admin access.
 
contract AuditedProxy {
    address implementation;
 
    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            // 0xF2 = CALLCODE, NOT 0xF4 = DELEGATECALL
            // An auditor reading disassembly must check this byte
            let result := callcode(
                gas(), impl, callvalue(),
                0, calldatasize(), 0, 0
            )
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

Scenario D: CALLCODE to Empty Address — Silent ETH Loss

contract LegacyForwarder {
    function forwardCall(
        address target,
        bytes calldata data,
        uint256 value
    ) external {
        assembly {
            calldatacopy(0, 68, calldatasize()) // skip selector + addr + value
            let ok := callcode(gas(), target, value, 0, sub(calldatasize(), 68), 0, 0)
            // If target has no code, callcode returns 1 (success)
            // but the ETH specified by 'value' is silently transferred
            // to the empty address. No revert, no error.
            // Caller loses ETH with no indication of failure.
        }
    }
}

Mitigations

ThreatMitigationImplementation
T1: CALLCODE/DELEGATECALL confusionVerify opcode in bytecode disassembly, not source codeCheck for 0xF2 vs 0xF4 in deployed bytecode using cast disassemble or Etherscan’s bytecode viewer. Never assume “delegatecall” from source comments alone.
T1: msg.sender identity breakReplace CALLCODE with DELEGATECALLRedeploy the contract using DELEGATECALL (0xF4). If the contract is non-upgradeable, migrate funds to a new contract.
T2: Storage writes with wrong identityAudit all msg.sender-dependent storage paths in called codeMap every SSTORE that keys on msg.sender and verify the expected identity under CALLCODE semantics.
T3: Legacy contracts with CALLCODEIdentify and inventory CALLCODE-containing contractsScan deployed bytecode for 0xF2 opcodes. Flag contracts holding ETH/tokens or governing external state.
T3: Immutable legacy bytecodeMigrate funds and permissions to new contractsDeploy replacement contracts with DELEGATECALL; transfer ownership, funds, and token approvals.
T4: Unexpected value transferAvoid CALLCODE with non-zero valIf CALLCODE must be used (legacy constraint), always set val = 0 to prevent unintended ETH transfers.
T5: False deprecation assumptionsTreat CALLCODE as a live EVM feature in security assessmentsInclude CALLCODE in threat models, security audits, and automated scanners. Do not filter it out as “deprecated.”
General: New contract developmentNever use CALLCODE in new contractsUse DELEGATECALL for storage-delegated calls, CALL for standard external calls, STATICCALL for read-only calls.

Compiler/EIP-Based Protections

  • Solidity >= 0.5.0: The callcode keyword is removed. Attempting to use it produces a compilation error: "callcode" has been deprecated in favour of "delegatecall". This prevents new Solidity contracts from using CALLCODE through the high-level language.
  • EIP-7 (DELEGATECALL, Homestead 2016): Introduced DELEGATECALL specifically to fix CALLCODE’s failure to preserve msg.sender and msg.value. All modern proxy patterns (UUPS, Transparent, Diamond/EIP-2535) use DELEGATECALL exclusively.
  • EIP-2488 (Proposed, Stagnant): Would make CALLCODE always return failure at the protocol level. Not adopted, but its existence signals community recognition that CALLCODE should be eliminated.
  • EIP-7069 (Revamped CALL Instructions, Proposed): Proposes new CALL variants that would eventually supersede all legacy call opcodes. If adopted, would formalize CALLCODE’s obsolescence at the instruction-set level.
  • Inline assembly restrictions (Solidity >= 0.8.0): While callcode is available in Yul/inline assembly, Solidity 0.8+ emits warnings for unsafe assembly patterns. Static analyzers like Slither flag callcode in assembly blocks.

Severity Summary

Threat IDCategorySeverityLikelihoodReal-World Precedent
T1Smart ContractHighMediumPre-Homestead library bugs; Parity Wallet pattern (DELEGATECALL version exploited for $180M)
T2Smart ContractHighLowNo single large exploit, but the storage+identity mismatch is a unique and underanalyzed threat model
T3Smart ContractMediumLowLegacy contracts on mainnet; EIP-2488 stagnation confirms ongoing concern
T4Smart ContractMediumLowNo known exploit, but the value parameter creates an unintuitive interaction model unique to CALLCODE
T5Smart ContractLowMediumTooling gaps in detecting CALLCODE in bytecode; 1inch Fusion v1 exploit ($5M) on a deprecated contract
P1ProtocolLowLowEIP-2488 stagnation; EIP-7069 proposed as long-term replacement
P2ProtocolLowLowNo known consensus bugs from CALLCODE, but the rarely-tested code path is a standing risk

OpcodeRelationship
DELEGATECALL (0xF4)Direct replacement for CALLCODE. Introduced by EIP-7 (Homestead 2016) to fix CALLCODE’s failure to preserve msg.sender and msg.value. Both opcodes execute code in the caller’s storage context, but DELEGATECALL propagates the full calling context while CALLCODE does not. DELEGATECALL also has no val parameter — it inherits msg.value from the parent frame.
CALL (0xF1)Standard external call. Executes code in the callee’s storage context (not the caller’s), unlike both CALLCODE and DELEGATECALL. Sets msg.sender to the calling contract’s address. Accepts a val parameter for ETH transfer. CALLCODE is a hybrid: it has CALL’s identity behavior (msg.sender = caller) but DELEGATECALL’s storage behavior (caller’s storage).
STATICCALL (0xFA)Read-only external call. Like CALL but prohibits state modifications. CALLCODE within a STATICCALL context will revert if it attempts any state change (storage write, ETH transfer, LOG emission).
CALLER (0x33)Returns msg.sender in the current execution context. The core opcode affected by CALLCODE’s identity rewrite — in a CALLCODE frame, CALLER returns the calling contract’s address rather than the original external caller. This is the fundamental difference from DELEGATECALL.