Opcode Summary
| Property | Value |
|---|---|
| Opcode | 0xF2 |
| Mnemonic | CALLCODE |
| Gas | dynamic (same formula as CALL: access_cost + mem_expansion_cost + value_transfer_surcharges) |
| Stack Input | gas, addr, val, argOst, argLen, retOst, retLen |
| Stack Output | success (1 if the call succeeded, 0 if it reverted or failed) |
| Behavior | Executes 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:
-
CALLCODE is deprecated but not removed. Solidity 0.5.0 (November 2018) removed
callcodeas 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. -
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.senderandmsg.valuefrom the parent call frame; CALLCODE does not — it setsmsg.senderto the calling contract and allowsmsg.valueto be overridden. Developers who encounter CALLCODE in legacy code, audit reports, or disassembly may assume it behaves like DELEGATECALL, missing themsg.senderdiscrepancy. This confusion has led to incorrect security assessments of legacy contracts. -
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.senderbehavior that their original developers may not have fully understood. -
CALLCODE accepts a value parameter that DELEGATECALL does not. CALLCODE takes
valas a stack argument, allowing the caller to specify an ETH value to forward. DELEGATECALL has no value parameter and inheritsmsg.valuefrom 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.senderinside C’s code isaddress(B)— the contract that invoked CALLCODE. - DELEGATECALL:
msg.senderinside 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, becausemsg.senderis 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.senderdifference 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.senderin 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.senderwould 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.valuereflects thevalargument passed to CALLCODE, not the original transaction’s value. A malicious or buggy caller can setvalto an arbitrary amount (up to its balance), changing the economic context the library code operates in. With DELEGATECALL,msg.valueis 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
callcodeonly 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-fuzzerorgoevmlab) may under-represent CALLCODE relative to DELEGATECALL in their opcode distributions, reducing the likelihood of finding implementation bugs.
Edge Cases
| Edge Case | Behavior | Security Implication |
|---|---|---|
msg.sender in CALLCODE | Set to the address of the contract executing CALLCODE, not the original external caller | Breaks 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 CALLCODE | Set to the val argument passed on the stack; does not inherit from the parent call frame | Caller can specify arbitrary ETH value, changing the economic context of the called code. Differs from DELEGATECALL which inherits msg.value. |
| Storage context | All SLOAD/SSTORE operations in the called code read/write the calling contract’s storage | Same as DELEGATECALL. Storage layout mismatch between caller and callee causes silent data corruption. |
| Value transfer destination | ETH 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 address | Returns success (1) with no code execution; any ETH specified by val is transferred to the address | Can silently create new accounts and transfer ETH. No revert on empty target. |
CALLCODE with val > balance | Returns failure (0); no state changes occur | Insufficient balance causes silent failure, not a revert of the entire transaction. Caller must check the return value. |
Gas stipend when val > 0 | 2300-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 context | Prohibited; causes the entire STATICCALL frame to revert if CALLCODE attempts a state change | CALLCODE 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 contract | Executes the precompile in the caller’s context; precompile result is returned normally | Precompiles 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 1024 | Same 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:
- OpenZeppelin: On The Parity Wallet Multisig Hack
- OpenZeppelin: Parity Wallet Hack Reloaded
- Parity Post-Mortem on Library Self-Destruct
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.senderfor authorization treated every user identically (as the proxy address). - Token transfer functions that credited
msg.sendercredited the proxy, not the user. - Event emissions that logged
msg.senderrecorded 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:
- EIP-7: DELEGATECALL
- History of Callcode and Delegatecall — yAcademy
- Ethereum StackExchange: Difference Between CALL, CALLCODE, and DELEGATECALL
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
| Threat | Mitigation | Implementation |
|---|---|---|
| T1: CALLCODE/DELEGATECALL confusion | Verify opcode in bytecode disassembly, not source code | Check 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 break | Replace CALLCODE with DELEGATECALL | Redeploy the contract using DELEGATECALL (0xF4). If the contract is non-upgradeable, migrate funds to a new contract. |
| T2: Storage writes with wrong identity | Audit all msg.sender-dependent storage paths in called code | Map every SSTORE that keys on msg.sender and verify the expected identity under CALLCODE semantics. |
| T3: Legacy contracts with CALLCODE | Identify and inventory CALLCODE-containing contracts | Scan deployed bytecode for 0xF2 opcodes. Flag contracts holding ETH/tokens or governing external state. |
| T3: Immutable legacy bytecode | Migrate funds and permissions to new contracts | Deploy replacement contracts with DELEGATECALL; transfer ownership, funds, and token approvals. |
| T4: Unexpected value transfer | Avoid CALLCODE with non-zero val | If CALLCODE must be used (legacy constraint), always set val = 0 to prevent unintended ETH transfers. |
| T5: False deprecation assumptions | Treat CALLCODE as a live EVM feature in security assessments | Include CALLCODE in threat models, security audits, and automated scanners. Do not filter it out as “deprecated.” |
| General: New contract development | Never use CALLCODE in new contracts | Use DELEGATECALL for storage-delegated calls, CALL for standard external calls, STATICCALL for read-only calls. |
Compiler/EIP-Based Protections
- Solidity >= 0.5.0: The
callcodekeyword 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.senderandmsg.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
callcodeis available in Yul/inline assembly, Solidity 0.8+ emits warnings for unsafe assembly patterns. Static analyzers like Slither flagcallcodein assembly blocks.
Severity Summary
| Threat ID | Category | Severity | Likelihood | Real-World Precedent |
|---|---|---|---|---|
| T1 | Smart Contract | High | Medium | Pre-Homestead library bugs; Parity Wallet pattern (DELEGATECALL version exploited for $180M) |
| T2 | Smart Contract | High | Low | No single large exploit, but the storage+identity mismatch is a unique and underanalyzed threat model |
| T3 | Smart Contract | Medium | Low | Legacy contracts on mainnet; EIP-2488 stagnation confirms ongoing concern |
| T4 | Smart Contract | Medium | Low | No known exploit, but the value parameter creates an unintuitive interaction model unique to CALLCODE |
| T5 | Smart Contract | Low | Medium | Tooling gaps in detecting CALLCODE in bytecode; 1inch Fusion v1 exploit ($5M) on a deprecated contract |
| P1 | Protocol | Low | Low | EIP-2488 stagnation; EIP-7069 proposed as long-term replacement |
| P2 | Protocol | Low | Low | No known consensus bugs from CALLCODE, but the rarely-tested code path is a standing risk |
Related Opcodes
| Opcode | Relationship |
|---|---|
| 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. |