CALL vs DELEGATECALL vs STATICCALL: EVM Call Types Explained

Learn how EVM call types work: CALL, DELEGATECALL, STATICCALL, and CALLCODE. Understand storage context, msg.sender behavior, proxy patterns, and security.

By Zig EVM Team |

TL;DR: The EVM provides four call-type opcodes — CALL, CALLCODE (deprecated), DELEGATECALL, and STATICCALL — each with different behavior regarding storage context, msg.sender, msg.value, and state mutability. CALL executes code in the target contract’s context. DELEGATECALL executes code in the caller’s context, enabling proxy and upgradeable contract patterns. STATICCALL enforces read-only execution. Understanding these differences is critical for writing secure smart contracts and implementing patterns like upgradeable proxies.

Why EVM Call Types Matter

Smart contracts rarely operate in isolation. A DeFi protocol might call a token contract, which calls a price oracle, which reads from a registry. Every one of these interactions uses one of the EVM’s call-type opcodes under the hood.

Choosing the wrong call type can lead to catastrophic outcomes. The 2016 Parity multisig wallet hack exploited DELEGATECALL to gain ownership of wallets holding over $30 million in ETH. Understanding the precise semantics of each call type is not optional — it is a security requirement.

The EVM has four call-type opcodes: CALL (0xF1), CALLCODE (0xF2), DELEGATECALL (0xF4), and STATICCALL (0xFA). Each differs in how it handles execution context, storage access, and value transfer.

The Four Call Types at a Glance

Before examining each in detail, here is a comparison table summarizing the key differences:

PropertyCALLCALLCODEDELEGATECALLSTATICCALL
Opcode0xF10xF20xF40xFA
Storage contextCalleeCallerCallerCallee
msg.senderCaller contractCaller contractOriginal senderCaller contract
msg.valueSet by callerSet by callerOriginal value0 (no value)
Can send ETHYesYesNoNo
Can modify stateYesYesYes (caller’s)No
StatusActiveDeprecatedActiveActive
IntroducedGenesisGenesisEIP-7EIP-214

CALL: Standard External Calls

CALL is the most common call type. When contract A uses CALL to invoke contract B, execution switches entirely to contract B’s context.

How CALL Works

Contract A (caller)                Contract B (callee)
+-----------------+                +-----------------+
| Storage: A      |  --- CALL -->  | Storage: B      |
| Address: 0xA... |                | Address: 0xB... |
|                 |                |                 |
| msg.sender: EOA |                | msg.sender: 0xA |
| msg.value: 1 ETH|                | msg.value: X    |
+-----------------+                +-----------------+
                                   Code executes here
                                   Reads/writes B's storage

Key behaviors:

  • Storage: B’s code reads and writes B’s own storage slots.
  • msg.sender: Inside B, msg.sender is the address of contract A.
  • msg.value: Set explicitly by A in the CALL instruction. ETH is transferred from A to B.
  • address(this): Inside B, address(this) returns B’s address.

CALL in Solidity

// High-level call (preferred when ABI is known)
uint256 balance = IERC20(token).balanceOf(address(this));

// Low-level call (used when ABI is unknown or for forwarding)
(bool success, bytes memory data) = target.call{value: 1 ether, gas: 50000}(
    abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)
);
require(success, "Call failed");

CALL Stack Arguments

At the EVM level, CALL takes seven arguments from the stack:

Stack (top to bottom):
1. gas        - Gas to forward
2. to         - Target address
3. value      - ETH to send (in wei)
4. argsOffset  - Memory offset of input data
5. argsLength  - Length of input data
6. retOffset   - Memory offset for return data
7. retLength   - Length of return data

It pushes 1 on success and 0 on failure.

Use Cases for CALL

  • Transferring ETH to another contract or EOA
  • Invoking functions on external contracts
  • Interacting with token contracts (ERC-20, ERC-721)
  • Calling DeFi protocol entry points

CALLCODE: The Deprecated Predecessor

CALLCODE was the original mechanism for executing another contract’s code in the caller’s storage context. It has been effectively replaced by DELEGATECALL and should never be used in new code.

How CALLCODE Differs from DELEGATECALL

The critical difference: CALLCODE changes msg.sender to the calling contract’s address, while DELEGATECALL preserves the original sender.

EOA (0xUser) calls Contract A, which uses CALLCODE to Contract B:

With CALLCODE:
  Inside B's code: msg.sender = 0xA (contract A, NOT the EOA)

With DELEGATECALL:
  Inside B's code: msg.sender = 0xUser (the original EOA)

This matters in proxy chains. If a user calls proxy A, which delegates to implementation B, which then needs to check who the original caller was, CALLCODE breaks the chain because msg.sender becomes the proxy address. DELEGATECALL preserves the end user’s address throughout.

CALLCODE is deprecated. Do not use it in new contracts. The Solidity compiler does not expose it in modern versions.

DELEGATECALL: The Foundation of Proxy Patterns

DELEGATECALL is arguably the most powerful — and most dangerous — call type in the EVM. It executes another contract’s code but operates entirely in the caller’s context.

How DELEGATECALL Works

EOA (0xUser) sends tx to Proxy (0xP)
Proxy uses DELEGATECALL to Implementation (0xI)

+--------------------+                +---------------------+
| Proxy (0xP)        |  DELEGATECALL  | Implementation(0xI) |
| Storage: Proxy's   | -------------> | Code: runs here     |
| Balance: Proxy's   |                |                     |
|                    |                | But reads/writes    |
| msg.sender: 0xUser |                | PROXY's storage     |
| msg.value: original|                | msg.sender: 0xUser  |
+--------------------+                +---------------------+

Key behaviors:

  • Storage: The implementation’s code reads and writes the proxy’s storage slots.
  • msg.sender: Preserved from the original transaction. Inside the implementation, msg.sender is the EOA, not the proxy.
  • msg.value: Preserved from the original transaction.
  • address(this): Returns the proxy’s address, not the implementation’s address.
  • ETH transfer: DELEGATECALL cannot send ETH (no value parameter in the opcode).

DELEGATECALL Stack Arguments

DELEGATECALL takes six arguments (no value parameter):

Stack (top to bottom):
1. gas        - Gas to forward
2. to         - Address of code to execute
3. argsOffset  - Memory offset of input data
4. argsLength  - Length of input data
5. retOffset   - Memory offset for return data
6. retLength   - Length of return data

The Proxy Pattern Explained

The most common use of DELEGATECALL is the proxy pattern for upgradeable contracts. Since smart contracts on Ethereum are immutable once deployed, the proxy pattern separates storage from logic:

// Proxy contract -- stores all state, delegates all logic
contract Proxy {
    // Storage slot for implementation address (EIP-1967)
    // bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)
    bytes32 private constant IMPL_SLOT =
        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    fallback() external payable {
        address impl;
        assembly {
            impl := sload(IMPL_SLOT)
        }

        assembly {
            // Copy calldata to memory
            calldatacopy(0, 0, calldatasize())

            // Delegatecall to implementation
            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

            // Copy return data
            returndatacopy(0, 0, returndatasize())

            switch result
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

To upgrade, you deploy a new implementation contract and update the address stored in IMPL_SLOT. The proxy’s storage and address remain the same, but all function calls now execute the new logic.

Storage Layout Compatibility

Because DELEGATECALL operates on the caller’s storage, the implementation contract’s storage layout must be compatible with the proxy’s layout. Storage slots are assigned by position, not by variable name:

// Implementation V1
contract ImplV1 {
    address public owner;    // slot 0
    uint256 public value;    // slot 1
}

// Implementation V2 -- CORRECT upgrade
contract ImplV2 {
    address public owner;    // slot 0 (same)
    uint256 public value;    // slot 1 (same)
    uint256 public newField; // slot 2 (appended)
}

// Implementation V2Bad -- DANGEROUS upgrade
contract ImplV2Bad {
    uint256 public newField; // slot 0 -- OVERWRITES owner!
    address public owner;    // slot 1 -- OVERWRITES value!
    uint256 public value;    // slot 2
}

Reordering, removing, or inserting variables before existing ones will corrupt the proxy’s state. This is one of the most common sources of bugs in upgradeable contracts.

Use Cases for DELEGATECALL

  • Upgradeable contract patterns (EIP-1967 proxies, UUPS, Transparent Proxy)
  • Diamond pattern (EIP-2535) with multiple implementation facets
  • Library calls (Solidity library keyword uses DELEGATECALL internally)
  • Shared logic modules across multiple contracts

STATICCALL: Read-Only Execution

STATICCALL, introduced in EIP-214, is identical to CALL except that it enforces a strict read-only execution environment. If the callee attempts any state modification, the call immediately reverts.

What STATICCALL Prohibits

The following operations cause an immediate revert inside a STATICCALL context:

  • SSTORE (storage writes)
  • CREATE and CREATE2 (contract creation)
  • SELFDESTRUCT
  • LOG0 through LOG4 (event emission)
  • CALL with non-zero value (ETH transfer)

STATICCALL in Solidity

The Solidity compiler automatically uses STATICCALL when calling functions marked view or pure:

// Solidity generates STATICCALL for this
uint256 price = oracle.getPrice(token); // getPrice is view

// Low-level staticcall
(bool success, bytes memory data) = target.staticcall(
    abi.encodeWithSignature("balanceOf(address)", account)
);

Use Cases for STATICCALL

  • Reading state from oracles, registries, and token contracts
  • Simulating transactions off-chain (eth_call uses STATICCALL semantics)
  • Ensuring that a callback cannot modify your contract’s state
  • Any situation where you need a guarantee of no side effects

Gas Forwarding Rules

Since EIP-150, all call types follow the 63/64ths rule: the caller retains 1/64th of its remaining gas and forwards at most 63/64ths to the callee. This prevents the callee from consuming all available gas and leaving the caller unable to handle a failure.

Remaining gas before call: 640,000
Maximum forwarded: 640,000 * 63 / 64 = 630,000
Reserved for caller: 640,000 - 630,000 = 10,000

You can specify a lower gas limit explicitly. If you pass a gas value greater than the 63/64ths maximum, the EVM silently caps it. This is important for security: before EIP-150, it was possible to craft calls that left the caller with zero gas, enabling certain griefing attacks.

Return Data Handling

All four call types push a success flag (0 or 1) onto the stack. To access the actual return data, you use the RETURNDATASIZE and RETURNDATACOPY opcodes, introduced in EIP-211:

assembly {
    let result := call(gas(), target, 0, inPtr, inLen, 0, 0)

    // Get size of returned data
    let size := returndatasize()

    // Copy return data to memory at position 0
    returndatacopy(0, 0, size)

    // Check success
    switch result
    case 0 {
        // Revert with return data (which contains the error)
        revert(0, size)
    }
    default {
        // Return the data
        return(0, size)
    }
}

Security Implications

DELEGATECALL to Untrusted Contracts

Never use DELEGATECALL to invoke code from an untrusted or user-supplied address. Because the called code runs with full access to your contract’s storage and balance, a malicious implementation can:

  • Overwrite the owner variable and take control
  • Drain the contract’s ETH balance
  • Corrupt storage in ways that brick the contract
  • Call selfdestruct to destroy the proxy permanently

The Parity multisig hack is the canonical example. An attacker called an unprotected initWallet function via DELEGATECALL, becoming the owner and then draining the funds.

Unchecked Return Values

Low-level calls in Solidity return a boolean success flag. Failing to check it silently swallows errors:

// DANGEROUS: ignoring return value
target.call(abi.encodeWithSignature("withdraw(uint256)", amount));

// SAFE: checking return value
(bool success, ) = target.call(abi.encodeWithSignature("withdraw(uint256)", amount));
require(success, "Withdraw failed");

Reentrancy via CALL

CALL with ETH transfer can trigger the recipient’s receive() or fallback() function, which can call back into your contract before the original function completes. This is the classic reentrancy vulnerability. Use the checks-effects-interactions pattern or a reentrancy guard.

Trying It Yourself with Zig EVM

The Zig EVM project implements these call-type opcodes along with the rest of the EVM instruction set, giving you a transparent environment to study exactly how the stack and memory behave during cross-contract calls.

You can experiment with bytecode-level call sequences in the Zig EVM Playground, stepping through each opcode to observe how msg.sender, storage context, and return data change depending on the call type.

# Clone the Zig EVM and run the test suite
git clone https://github.com/cryptuon/zig-evm.git
cd zig-evm
zig build test

Key Takeaways

  • CALL is the standard opcode for external contract interactions. It executes code in the callee’s context and can transfer ETH.
  • CALLCODE is deprecated. It executes code in the caller’s storage context but incorrectly sets msg.sender to the caller contract. Do not use it.
  • DELEGATECALL executes code in the caller’s full context, preserving msg.sender and msg.value. It is the foundation of proxy and upgradeable contract patterns.
  • STATICCALL is a read-only version of CALL. It reverts if the callee attempts any state modification, making it safe for view-function calls.
  • Storage layout compatibility is critical when using DELEGATECALL. Misaligned storage slots between proxy and implementation will corrupt state.
  • Never DELEGATECALL to untrusted code. It has full access to your storage and balance.
  • Always check return values from low-level calls. A failed call returns 0 but does not automatically revert the transaction.
  • The 63/64ths gas forwarding rule (EIP-150) prevents callees from consuming all remaining gas.

Further Reading

Frequently Asked Questions

What is the difference between CALL and DELEGATECALL in the EVM?
CALL executes code in the callee's context, using the callee's storage and address. DELEGATECALL executes the callee's code in the caller's context, meaning it reads and writes the caller's storage and preserves the original msg.sender and msg.value. This is why DELEGATECALL is used for proxy and upgradeable contract patterns.
Why was CALLCODE deprecated in favor of DELEGATECALL?
CALLCODE was deprecated because it changes msg.sender to the calling contract's address, which breaks the chain of trust in nested calls. DELEGATECALL, introduced in EIP-7, preserves the original msg.sender and msg.value, making it suitable for proxy patterns where the end user's identity must be maintained.
When should I use STATICCALL instead of CALL?
Use STATICCALL when calling a function that should not modify state, such as view or pure functions. STATICCALL guarantees that the called contract cannot write to storage, emit events, create contracts, or self-destruct. The EVM will revert the call if any state modification is attempted.
Is DELEGATECALL dangerous?
DELEGATECALL is powerful but dangerous if misused. Because it executes external code with full access to the caller's storage, calling an untrusted or malicious contract via DELEGATECALL can overwrite critical storage slots, drain funds, or change ownership. Only use DELEGATECALL with thoroughly audited, trusted implementation contracts.
How does gas forwarding work with EVM call types?
Since the EIP-150 gas repricing, all call types forward a maximum of 63/64ths of the remaining gas to the callee, retaining 1/64th as a reserve. You can also specify a custom gas limit. If the callee runs out of gas, the call returns 0 (failure) but the caller can continue execution with its reserved gas.

Try Zig EVM

Explore the interactive playground or dive into the source code.