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.
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:
| Property | CALL | CALLCODE | DELEGATECALL | STATICCALL |
|---|---|---|---|---|
| Opcode | 0xF1 | 0xF2 | 0xF4 | 0xFA |
| Storage context | Callee | Caller | Caller | Callee |
| msg.sender | Caller contract | Caller contract | Original sender | Caller contract |
| msg.value | Set by caller | Set by caller | Original value | 0 (no value) |
| Can send ETH | Yes | Yes | No | No |
| Can modify state | Yes | Yes | Yes (caller’s) | No |
| Status | Active | Deprecated | Active | Active |
| Introduced | Genesis | Genesis | EIP-7 | EIP-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.senderis 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.senderis 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
librarykeyword 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)CREATEandCREATE2(contract creation)SELFDESTRUCTLOG0throughLOG4(event emission)CALLwith 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
selfdestructto 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.senderto the caller contract. Do not use it. - DELEGATECALL executes code in the caller’s full context, preserving
msg.senderandmsg.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
- EIP-7: DELEGATECALL — the original proposal
- EIP-214: STATICCALL — new opcode for static calls
- EIP-1967: Standard Proxy Storage Slots — proxy pattern standard
- EVM Opcodes Explained — our comprehensive opcode reference
- Zig EVM on GitHub — explore the source code
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.