This page is a placeholder. All examples on this page are currently AI-generated and are not correct. This documentation will be completed in the future with accurate, tested examples.
Overview
Opcode: 0xf2
Introduced: Frontier (EVM genesis)
Status: DEPRECATED - Use DELEGATECALL (0xf4) instead
CALLCODE executes code from another account in the caller’s storage context. Unlike CALL, storage modifications affect the caller. This opcode has confusing semantics and was superseded by DELEGATECALL in Homestead.
WARNING: CALLCODE is deprecated and should not be used in new contracts. Use DELEGATECALL for library calls and code reuse patterns.
Specification
Stack Input:
gas (max gas to forward)
address (target account code to execute)
value (wei to send to caller's own address)
inOffset (calldata memory offset)
inLength (calldata size)
outOffset (returndata memory offset)
outLength (returndata size)
Stack Output:
success (1 if call succeeded, 0 if failed)
Gas Cost: 700 + value_transfer + cold_access + memory_expansion
Operation:
calldata = memory[inOffset:inOffset+inLength]
success = execute_code_in_caller_context(address.code, value, calldata, gas * 63/64)
memory[outOffset:outOffset+outLength] = returndata[0:min(outLength, returndata.length)]
push(success)
Behavior
CALLCODE executes foreign code with confusing context semantics:
- Pop 7 stack arguments (same as CALL)
- Calculate gas cost:
- Base: 700 gas (Tangerine Whistle+)
- Value transfer: +9,000 gas if value > 0
- Cold access: +2,600 gas for first access (Berlin+)
- Memory expansion for input and output regions
- Read calldata from memory
- Forward gas: Up to 63/64 of remaining gas (EIP-150)
- Execute target’s code in caller’s context:
- msg.sender = caller (NOT preserved from parent!)
- msg.value = specified value
- Storage = caller’s storage (modifications affect caller!)
- Code = target’s code
- address(this) = caller’s address
- Value handling: ETH sent to caller’s own address (weird!)
- Copy returndata to memory
- Push success flag
Key differences from CALL:
- Storage operations affect caller, not target
- msg.sender is caller (not preserved from parent)
- Value sent to caller’s own address
Key differences from DELEGATECALL:
- msg.sender is caller (DELEGATECALL preserves original sender)
- msg.value is specified value (DELEGATECALL preserves original value)
- Value handling is confusing
Examples
Basic CALLCODE (Don’t Do This!)
import { CALLCODE } from '@tevm/voltaire/evm/system';
// DON'T USE - deprecated opcode
const frame = createFrame({
gasRemaining: 1000000n,
address: Address("0x1234..."),
});
// Stack: [gas=100000, address, value=0, inOffset=0, inLength=0, outOffset=0, outLength=0]
frame.stack.push(0n); // outLength
frame.stack.push(0n); // outOffset
frame.stack.push(0n); // inLength
frame.stack.push(0n); // inOffset
frame.stack.push(0n); // value
frame.stack.push(BigInt("0x742d35Cc...")); // address (code source)
frame.stack.push(100000n); // gas
// USE DELEGATECALL INSTEAD!
Why CALLCODE is Confusing
contract Library {
uint256 public value;
function setValue(uint256 _value) external {
value = _value; // Modifies storage at slot 0
}
}
contract Caller {
uint256 public myValue; // Storage slot 0
function useCallCode(address library) external {
// CALLCODE execution context:
// - msg.sender = address(this) (NOT tx.origin!)
// - msg.value = sent value
// - storage = Caller's storage
// - code = Library's code
assembly {
let success := callcode(
gas(),
library,
0, // Value sent to self (weird!)
0, // calldata
0,
0, // returndata
0
)
}
// Library's setValue modified Caller.myValue!
// But msg.sender in Library was Caller, not original caller
}
}
Correct Pattern: Use DELEGATECALL
contract Library {
uint256 public value;
function setValue(uint256 _value) external {
value = _value;
}
}
contract Caller {
uint256 public myValue;
// ✅ CORRECT: Use DELEGATECALL
function useDelegateCall(address library) external {
// DELEGATECALL execution context:
// - msg.sender = original caller (preserved!)
// - msg.value = original value (preserved!)
// - storage = Caller's storage
// - code = Library's code
(bool success, ) = library.delegatecall(
abi.encodeWithSignature("setValue(uint256)", 42)
);
require(success);
// Clear semantics: Library code runs as if part of Caller
}
}
Migration Example
// ❌ OLD: Using CALLCODE (deprecated)
contract OldPattern {
function callLibrary(address lib, bytes memory data) external {
assembly {
let success := callcode(
gas(),
lib,
0,
add(data, 0x20),
mload(data),
0,
0
)
if iszero(success) { revert(0, 0) }
}
}
}
// ✅ NEW: Using DELEGATECALL
contract NewPattern {
function callLibrary(address lib, bytes memory data) external {
(bool success, ) = lib.delegatecall(data);
require(success, "Delegatecall failed");
}
}
Gas Cost
Total cost: 700 + value_transfer + cold_access + memory_expansion + forwarded_gas
Base Cost: 700 gas (Tangerine Whistle+)
Pre-Tangerine Whistle: 40 gas
Value Transfer: +9,000 gas
Charged when value > 0 (even though value sent to self):
if (value > 0) {
cost += 9000 // CallValueTransferGas
}
Note: No new account cost - value sent to caller’s own address.
Cold Access: +2,600 gas (Berlin+)
EIP-2929 (Berlin+): First access to target address:
if (firstAccess(address)) {
cost += 2600 // ColdAccountAccess
} else {
cost += 100 // WarmStorageRead
}
Memory Expansion
Same as CALL - charges for both input and output regions.
Gas Forwarding
Same as CALL - 63/64 rule applies (EIP-150).
Common Usage
None - this opcode is deprecated.
Historical uses included:
- Library pattern (replaced by DELEGATECALL)
- Code reuse (replaced by DELEGATECALL)
- Upgradeable contracts (replaced by DELEGATECALL + proxy patterns)
Security
Deprecated - Do Not Use
The primary security issue with CALLCODE is that it should not be used at all. Use DELEGATECALL instead.
Confusing msg.sender Semantics
CALLCODE sets msg.sender to the caller, not the original transaction sender:
// VULNERABLE: Unexpected msg.sender
contract VulnerableAuth {
address public owner;
constructor() {
owner = msg.sender;
}
function updateOwner(address newOwner) external {
// Assumes msg.sender is the transaction sender
require(msg.sender == owner);
owner = newOwner;
}
}
contract Attacker {
function exploit(address vulnerable, address lib) external {
// Call library code via CALLCODE
assembly {
// In library code, msg.sender will be Attacker contract
// NOT the original transaction sender!
callcode(gas(), lib, 0, 0, 0, 0, 0)
}
}
}
Storage Collision
Same storage collision risks as DELEGATECALL:
contract Library {
address public implementation; // Slot 0
function upgrade(address newImpl) external {
implementation = newImpl;
}
}
contract Caller {
address public owner; // Slot 0 - COLLISION!
function callLibrary(address lib) external {
assembly {
callcode(gas(), lib, 0, 0, 0, 0, 0)
}
// Library modified Caller.owner instead of implementation!
}
}
Value Transfer Confusion
Value sent to caller’s own address creates confusing semantics:
contract ConfusingValue {
function sendToSelf() external payable {
assembly {
// This sends msg.value to self - pointless!
callcode(gas(), target, callvalue(), 0, 0, 0, 0)
}
}
}
Why DELEGATECALL is Better
| Aspect | CALLCODE | DELEGATECALL |
|---|
| msg.sender | Caller (confusing) | Preserved (clear) |
| msg.value | Specified value | Preserved (clear) |
| Value transfer | To self (weird) | No value transfer |
| Use case | NONE (deprecated) | Library calls, proxies |
| Introduced | Frontier | Homestead (EIP-7) |
| Status | Deprecated | Standard |
DELEGATECALL advantages:
- Preserves full execution context (msg.sender, msg.value)
- Clear semantics for library pattern
- No confusing value-to-self transfers
- Industry standard for proxies and libraries
Implementation
/**
* CALLCODE opcode (0xf2) - DEPRECATED
* Execute code in current context
*/
export function callcode(frame: FrameType): EvmError | null {
// Pop 7 arguments (same as CALL)
const gas = popStack(frame);
const address = popStack(frame);
const value = popStack(frame);
const inOffset = popStack(frame);
const inLength = popStack(frame);
const outOffset = popStack(frame);
const outLength = popStack(frame);
// Calculate gas cost
let gasCost = 700n; // Base (Tangerine Whistle+)
// Value transfer (even though to self)
if (value > 0n) {
gasCost += 9000n;
// No new account cost - value sent to self
}
// Cold access cost (Berlin+)
const accessCost = getAccessCost(address);
gasCost += accessCost;
// Memory expansion
const inEnd = inLength > 0 ? inOffset + inLength : 0;
const outEnd = outLength > 0 ? outOffset + outLength : 0;
const maxEnd = Math.max(inEnd, outEnd);
if (maxEnd > 0) {
gasCost += memoryExpansionCost(frame, maxEnd);
updateMemorySize(frame, maxEnd);
}
// Calculate forwarded gas (63/64 rule)
const afterCharge = frame.gasRemaining - gasCost;
const maxForward = afterCharge - afterCharge / 64n;
const forwardedGas = min(gas, maxForward);
// Charge total cost
consumeGas(frame, gasCost + forwardedGas);
// Read calldata
const calldata = readMemory(frame, inOffset, inLength);
// Execute in caller context with target's code
const result = executeCallCode({
codeAddress: address, // Code to execute
storageAddress: frame.address, // Storage to modify
sender: frame.address, // msg.sender = caller
value: value, // msg.value = specified
data: calldata,
gas: forwardedGas
});
// Refund unused gas
frame.gasRemaining += result.gasLeft;
// Copy returndata
const copySize = min(outLength, result.returnData.length);
writeMemory(frame, outOffset, result.returnData.slice(0, copySize));
// Set return_data buffer
frame.returnData = result.returnData;
// Push success flag
pushStack(frame, result.success ? 1n : 0n);
frame.pc += 1;
return null;
}
References