Skip to main content

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:
  1. Pop 7 stack arguments (same as CALL)
  2. 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
  3. Read calldata from memory
  4. Forward gas: Up to 63/64 of remaining gas (EIP-150)
  5. 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
  6. Value handling: ETH sent to caller’s own address (weird!)
  7. Copy returndata to memory
  8. 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

AspectCALLCODEDELEGATECALL
msg.senderCaller (confusing)Preserved (clear)
msg.valueSpecified valuePreserved (clear)
Value transferTo self (weird)No value transfer
Use caseNONE (deprecated)Library calls, proxies
IntroducedFrontierHomestead (EIP-7)
StatusDeprecatedStandard
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