Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
Free 30-min Web3 Consultation
Book Now
Smart Contract Security Audits
Learn More
Custom DeFi Protocol Development
Explore
Full-Stack Web3 dApp Development
View Services
LABS
Guides

How to Understand EVM Call Types

A technical guide explaining the four EVM call opcodes (CALL, DELEGATECALL, STATICCALL, CALLCODE), their use cases, security implications, and how to use them in Solidity.
Chainscore © 2026
introduction
CORE CONCEPTS

Introduction to EVM Call Types

Understanding the different ways smart contracts communicate is fundamental to Ethereum development. This guide explains the four primary EVM call types: CALL, STATICCALL, DELEGATECALL, and CALLCODE.

The Ethereum Virtual Machine (EVM) executes operations defined by opcodes. For inter-contract communication, the most critical opcodes are CALL, STATICCALL, DELEGATECALL, and the deprecated CALLCODE. Each type dictates how state changes, context, and gas are managed during execution. Choosing the correct call type is essential for security, gas efficiency, and achieving the intended contract logic, especially when working with proxy patterns or libraries.

The standard CALL is used to invoke a function on another contract. It creates a new sub-context for execution: the called contract's code runs with its own storage, but the msg.sender is set to the caller, and msg.value can carry Ether. This is a state-changing operation. For example, calling a Uniswap router's swapExactTokensForETH function uses a CALL to transfer your tokens and send you ETH, modifying both contracts' states.

Introduced in EIP-214, STATICCALL is identical to CALL but with one crucial restriction: it disallows any modifications to state. The EVM will revert the entire transaction if the called contract attempts a state-changing opcode like SSTORE or a CALL with value. It's primarily used for view/pure functions and security checks, allowing you to safely read from another contract without risk of unintended side-effects.

DELEGATECALL is a powerful and potentially dangerous opcode. Instead of running the target contract's code in its own context, it executes that code within the context of the calling contract. This means the target code accesses the caller's storage, balance, and address (address(this)). It's the backbone of upgradeable proxy patterns, where a logic contract's code is executed in the storage context of a proxy. However, incorrect use can lead to severe storage collisions.

CALLCODE was an earlier opcode similar to DELEGATECALL, but with a key difference: it used the caller's storage, but the msg.sender and msg.value were preserved from the original caller, not set to the immediate caller. Its behavior was less intuitive and it was made obsolete by DELEGATECALL after the EIP-7 hardfork. Modern Solidity does not expose it, and it should not be used in new code.

In practice, you'll most often interact with these calls through Solidity. A normal external call (contract.function()) compiles to CALL. Using contract.function{gas: 100000}() adds gas stipends. The staticcall low-level function is available in assembly. The delegatecall low-level function is critical for proxy contracts. Always verify the target address in a DELEGATECALL to prevent self-destruct attacks, and thoroughly audit storage layouts when using proxy patterns.

prerequisites
PREREQUISITES

How to Understand EVM Call Types

Learn the fundamental mechanics of how smart contracts communicate on the Ethereum Virtual Machine.

Every interaction with a smart contract on Ethereum and other EVM-compatible chains is executed via a call. A call is a message sent from an Externally Owned Account (EOA) or another contract that contains data specifying which function to execute and with what parameters. Understanding the different call types—CALL, DELEGATECALL, STATICCALL, and CREATE—is essential for writing secure, efficient, and interoperable smart contracts. These low-level operations are the building blocks for all higher-level Solidity functions like .call(), .delegatecall(), and .staticcall().

The most common operation is a standard CALL. When Contract A executes a CALL to Contract B, the code of Contract B runs in the context of Contract B's own storage. This means Contract B can only read and modify its own state variables. The msg.sender for the called function will be Contract A, and msg.value can carry native tokens (e.g., ETH). This is the default behavior for external function calls and is used for simple interactions and value transfers.

In contrast, DELEGATECALL is a powerful but dangerous primitive. When Contract A DELEGATECALLs to Contract B, the code from Contract B is executed within the storage context of Contract A. This means Contract B's logic can read and write to Contract A's storage, and msg.sender and msg.value are preserved from the original caller. This enables patterns like upgradeable proxies and library contracts, but it introduces critical security risks if the target contract is not trusted, as it gains full control over the caller's state.

The STATICCALL opcode, introduced in EIP-214, enforces a read-only context. A STATICCALL prevents the executed code from modifying any state—it cannot write to storage, create other contracts, send tokens, or call other non-staticcall functions. This is crucial for view functions and security checks, allowing contracts to safely query data from external sources without risking state changes. Attempting a state-modifying operation during a STATICCALL will cause the transaction to revert.

Finally, the CREATE and CREATE2 opcodes are used to deploy new contract instances. CREATE deterministically generates a new contract address from the sender's address and nonce. CREATE2, introduced in EIP-1014, allows for address pre-computation by using the sender's address, a custom salt, and the contract's initialization code. This enables advanced patterns like counterfactual deployments and creating identical addresses across different chains. Understanding these deployment mechanics is key for complex systems like layer-2 solutions and factory contracts.

key-concepts
EVM FOUNDATION

Core Concepts of Contract Calls

Understanding the different types of calls in the EVM is fundamental for building secure and efficient smart contracts. This guide breaks down the key call types, their gas behavior, and security implications.

01

CALL vs. DELEGATECALL

CALL executes code in the context of the target contract, using its storage. DELEGATECALL executes code from the target contract but in the context of the caller, using the caller's storage and msg.sender. This is the mechanism behind proxy patterns and upgradeable contracts.

  • CALL: Transfers value, changes target's storage.
  • DELEGATECALL: No value transfer, changes caller's storage.
  • Security Risk: A malicious target in a DELEGATECALL can modify the caller's state.
02

STATICCALL for View Functions

Introduced in EIP-214, STATICCALL is a variant of CALL that disallows any state modifications. It will revert if the called function attempts to change storage, create logs, call SELFDESTRUCT, or send ether.

  • Use Case: Safely calling external view/pure functions without risk of reentrancy or state changes.
  • Gas: Opcode cost is the same as CALL, but saves gas by preventing expensive operations.
  • Example: Chainlink oracles use STATICCALL for price lookups to ensure data integrity.
03

Understanding msg.sender and tx.origin

msg.sender is the immediate caller of the current function. tx.origin is the original externally-owned account (EOA) that initiated the entire transaction chain.

  • Security: Never use tx.origin for authorization; it enables phishing attacks where a malicious contract can call your contract on a user's behalf.
  • Flow: EOA -> Contract A -> Contract B. In Contract B, msg.sender is Contract A, tx.origin is the EOA.
  • Best Practice: Use msg.sender for access control and require(msg.sender == tx.origin) to restrict calls to EOAs only.
04

Gas Limits and the 63/64 Rule

EVM calls have a gas stipend. A critical rule is that a CALL, DELEGATECALL, or STATICCALL can forward at most 63/64ths of the gas remaining to the called contract. The remaining 1/64th is kept by the caller.

  • Implication: Deep call chains can run out of gas for the final call. Always check return values and handle low-gas scenarios.
  • Example: With 1,000,000 gas left, a call can pass at most ~984,375 gas (1,000,000 * 63/64).
  • Mitigation: Use gas() to specify a fixed gas forward or implement try/catch patterns.
05

Low-Level Calls: call(), send(), transfer()

Solidity provides high-level and low-level methods for sending ether.

  • transfer() & send(): Forward 2300 gas stipend, revert on failure. send() returns a bool.
  • call{value: x}(): Forwards all remaining gas, returns bool and data. This is the current standard.

Key Differences:

  • Post-2019, transfer()/send() are deprecated for interacting with contracts, as 2300 gas is insufficient for modern receive/fallback functions.
  • Always use call() with checks-effects-interactions and reentrancy guards.
OPCODE REFERENCE

EVM Call Type Comparison

A technical comparison of the primary opcodes for executing external calls in the Ethereum Virtual Machine.

Feature / PropertyCALL (0xf1)STATICCALL (0xfa)DELEGATECALL (0xf4)

Primary Purpose

Generic external call

View/pure function call

Library/Proxy call

Can Modify State

Context (msg.sender)

Caller's address

Caller's address

Original caller's address

Context (msg.value)

Can send ETH

Must be 0

Must be 0

Gas Limit

Specified by caller

Specified by caller

Specified by caller

Return Data

Available

Available

Available

Typical Use Case

Transfer ETH, call state-changing functions

Read data from another contract

Execute code in context of calling contract

call-opcode-deep-dive
EVM OPCODE GUIDE

CALL: The Standard External Call

The CALL opcode is the fundamental mechanism for contracts to interact with other accounts on the Ethereum Virtual Machine, enabling everything from token transfers to complex DeFi operations.

The CALL opcode is the primary method for an Ethereum smart contract to execute an external interaction. It allows a contract to send a message to another account (either an Externally Owned Account (EOA) or another contract), transferring Ether and/or triggering its code. This operation is state-changing and consumes gas. The opcode takes several arguments from the stack: gas, address, value, argsOffset, argsSize, retOffset, and retSize. These parameters control how much gas is allotted for the sub-execution, the target address, the amount of Ether to send, and the memory locations for the input calldata and output returndata.

A successful CALL creates a new sub-context for execution. The EVM temporarily pauses the calling contract's execution, sets up a fresh memory space for the callee, and transfers the specified value. If the target is a contract, its code runs. Upon completion, execution returns to the caller, and any return data is written to the caller's memory at retOffset. The opcode itself pushes 1 (success) or 0 (failure) onto the stack. Critical failures include insufficient gas (out-of-gas), trying to send more Ether than the contract holds, or if the call depth exceeds 1024.

CALL is distinct from other call variants. Unlike STATICCALL, it can modify the state of the called contract. Unlike DELEGATECALL, it executes the target's code in the target's own storage context, not the caller's. This makes CALL the standard choice for composing independent contract functionalities, such as swapping tokens on a DEX or depositing into a lending protocol. Its behavior is foundational to understanding Ethereum's composable smart contract ecosystem.

Developers interact with CALL indirectly through high-level Solidity. A plain external call like targetAddress.call{value: 1 ether}("data") compiles down to the CALL opcode. It's crucial to handle the return value and implement checks-effects-interactions patterns to prevent reentrancy attacks, as a malicious contract could re-enter the caller during this external call. Always assume the called address could be a contract, even if you expect an EOA.

For advanced use, CALL can be used for low-level arbitrary calls, enabling interactions with contracts without their ABI. However, this requires manual encoding of function selectors and arguments. While powerful, it bypasses Solidity's type safety. A common pattern is using it with abi.encodeWithSignature, like addr.call(abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)). Remember that since the gas argument is forwardable, the callee can use all remaining gas unless explicitly limited, which is a consideration for gas estimation and security.

delegatecall-opcode-deep-dive
EVM CALL TYPES

DELEGATECALL: Executing in the Caller's Context

The DELEGATECALL opcode is a unique and powerful feature of the EVM that allows a contract to execute code from another contract while preserving the original caller's storage, balance, and address context.

Unlike a standard CALL, which creates a new execution context for the called contract, DELEGATECALL runs the target contract's code as if it were part of the calling contract. This means the executed code operates on the storage, msg.sender, and msg.value of the original caller. It is the fundamental mechanism behind upgradeable proxy patterns and modular contract design, enabling logic and storage separation. The calling contract must ensure the storage layouts between the two contracts are compatible to prevent critical state corruption.

A common use case is a proxy contract that delegates all logic to a separate implementation contract. The proxy holds the storage (like owner and data), while the implementation holds the executable functions. When a user calls the proxy, it uses DELEGATECALL to run the code in the latest implementation. This allows developers to fix bugs or upgrade logic without migrating the contract's state or address. Prominent standards like EIP-1967 and EIP-1822 formalize this pattern for secure upgradeability.

Consider this simplified proxy example. The proxy's fallback function uses delegatecall to forward any transaction to the implementation address stored in its state:

solidity
fallback() external payable {
    address impl = implementation;
    require(impl != address(0));
    (bool success, ) = impl.delegatecall(msg.data);
    require(success);
}

When delegatecall is invoked, the code at impl executes, but it reads and writes to the proxy's storage slots, and msg.sender is the original end-user, not the proxy contract itself.

The primary security consideration is storage collision. Because the delegated code writes to the caller's storage, the storage variable layout in the implementation must exactly match the layout expected by the proxy. A mismatch can lead to overwriting critical variables. For example, if the proxy stores the implementation address at slot 0, but the implementation's logic accidentally writes to slot 0, it could irreversibly break the upgrade mechanism. Using structured storage patterns or inheriting from libraries can mitigate this risk.

It's crucial to understand the gas implications. While DELEGATECALL preserves the caller's msg.value, the target contract's code cannot rely on the native ETH balance of its own address (this.balance) for logic, as it will reflect the proxy's balance. Furthermore, opcodes like EXTCODESIZE and EXTCODEHASH will inspect the target implementation's address, not the proxy's. Developers must audit the combined behavior of the proxy and implementation as a single system to ensure security and correctness.

staticcall-opcode-deep-dive
EVM CALL TYPES

STATICCALL: Enforcing View-Only Behavior

The STATICCALL opcode is a security-critical feature of the Ethereum Virtual Machine that prevents state-modifying operations during read-only execution.

In the Ethereum Virtual Machine (EVM), a STATICCALL is a specialized type of low-level call that enforces a strictly view-only execution context. Introduced in Ethereum's EIP-214 as part of the 2016 "Spurious Dragon" hard fork, its primary purpose is to prevent a class of reentrancy bugs and ensure that view and pure functions in Solidity cannot inadvertently modify the blockchain state. When a contract is invoked via STATICCALL, the EVM sets a static flag for the duration of the execution. Any operation that attempts to modify state—such as SSTORE (storage write), LOG (emit event), CREATE, SELFDESTRUCT, or a non-static CALL that sends value—will result in the entire sub-call failing with an exception.

This enforcement is crucial for security and predictability. Before STATICCALL, a view function could call another contract that might, in turn, perform a state-changing operation, leading to unexpected behavior and vulnerabilities. A classic example is a contract's balanceOf function, which should be read-only. If it internally calls an untrusted token contract using a regular CALL, that token contract could execute a transfer back to the caller, triggering a reentrancy attack. With STATICCALL, the token contract's state-modifying code would simply revert, protecting the calling contract. This guarantee allows developers and external services like block explorers and wallets to safely query contract data without fear of triggering side effects.

From a developer's perspective, you interact with STATICCALL primarily through Solidity. When you declare a function as view or pure and it makes an external call to another contract, the Solidity compiler automatically uses the STATICCALL opcode for that external call. You can also use it directly in inline assembly for low-level control. For instance, to manually perform a static call to retrieve data, you would write:

solidity
address target = 0x...;
(bool success, bytes memory data) = target.staticcall(abi.encodeWithSignature("getValue()"));
require(success, "Static call failed");

This pattern is common in proxy implementations and upgradeable contracts where logic is delegated, but the call must remain read-only to preserve the proxy's storage.

Understanding the limitations of STATICCALL is as important as understanding its benefits. Because it prevents state changes, you cannot use it for functions that require sending Ether (value > 0) or creating other contracts. Furthermore, while STATICCALL prevents the callee from modifying the caller's or global state, it does not inherently guarantee that the returned data is truthful or consistent—it only guarantees the execution context was static. For truly trustless verification, the calling contract must still validate the returned data against known proofs or consensus, a pattern used in optimistic rollups and light clients.

In summary, STATICCALL is a foundational EVM opcode that enforces execution purity. It is the mechanism that makes view functions safe and reliable, forming a critical layer in the security model of smart contract interactions. By understanding its behavior—that it acts as a hard boundary against state modification—developers can write more secure contracts and external systems can interact with the blockchain with greater confidence.

callcode-and-deprecation
EVM OPCODE HISTORY

CALLCODE (Deprecated) and Historical Context

Understanding the deprecated CALLCODE opcode is key to grasping the evolution of Ethereum's smart contract interaction model and the security improvements introduced by DELEGATECALL.

The CALLCODE opcode was a foundational but flawed mechanism for contract interaction in early Ethereum. It allowed a contract to execute code from another contract's address within the context of the caller. This meant the called contract's logic ran, but it could read and write to the caller's storage, use the caller's balance, and its msg.sender and msg.value were set to the original caller. This created a form of code reuse, but with significant security and clarity issues that led to its deprecation.

The primary danger of CALLCODE was its potential to create confusing and vulnerable proxy patterns. Because the called contract operated on the caller's state, it required absolute trust in the external code's logic. A malicious or buggy library contract could arbitrarily modify the caller's storage. Furthermore, this pattern broke the intuitive contract abstraction, making it difficult to reason about which contract was responsible for state changes. The infamous Parity multi-sig wallet hack in 2017, which resulted in the freezing of over 500,000 ETH, was directly enabled by a vulnerable CALLCODE implementation.

DELEGATECALL, introduced in the Ethereum Homestead hard fork, was designed as a direct successor to CALLCODE with a crucial semantic clarification. Its behavior is nearly identical, executing another contract's code in the caller's context. The critical difference is that msg.sender and msg.value are preserved from the original transaction, not set to the caller contract. This preserves the complete call chain, which is essential for secure proxy systems and upgradeable contract patterns, making the interaction's security model much clearer.

In practice, CALLCODE was formally deprecated with the EIP-7 "DELEGATECALL" upgrade. Modern Solidity uses DELEGATECALL under the hood when you use library functions for storage types or in proxy implementations. When examining historical contracts or blockchain data, encountering CALLCODE is a clear indicator of pre-2016 code. Understanding this evolution highlights Ethereum's iterative approach to security, where dangerous but useful primitives are replaced with safer, more explicit alternatives.

PRACTICAL IMPLEMENTATION

Solidity Code Examples

Simple Call Examples

call is the most flexible and low-level method. It forwards all available gas and returns a success boolean with data.

solidity
// Interacting with an unknown contract
(bool success, bytes memory data) = targetAddress.call{value: msg.value}(
    abi.encodeWithSignature("transfer(address,uint256)", recipient, amount)
);
require(success, "Call failed");

staticcall is identical to call but guarantees state will not be modified, reverting if the called function tries to write to storage.

solidity
// Reading a value safely
(bool success, bytes memory data) = oracleAddress.staticcall(
    abi.encodeWithSignature("getPrice(address)", token)
);
require(success);
uint256 price = abi.decode(data, (uint256));
EVM CALL TYPES

Security Considerations and Common Pitfalls

Understanding the different ways smart contracts can interact is fundamental to writing secure and efficient code. Misusing call types is a common source of bugs, security vulnerabilities, and unexpected gas costs.

The EVM provides four primary methods for contract interaction, each with distinct security and gas behavior.

  • call(): A low-level function that forwards all remaining gas and allows value transfer. It returns a (bool success, bytes data) tuple. It is the most flexible but also the most dangerous, as it can call any function on any address, making reentrancy attacks possible if state changes are not handled correctly (e.g., the Checks-Effects-Interactions pattern is violated).

  • delegatecall(): Executes code from another contract in the context of the calling contract. This means the target contract's code manipulates the storage, msg.sender, and msg.value of the caller. It is the mechanism behind proxy patterns and upgradeable contracts. A critical security risk is that if the target contract is malicious or contains bugs, it can arbitrarily modify the caller's storage.

  • staticcall(): Introduced in the Byzantium hard fork, this is identical to call() but disallows any state modifications in the called contract. It will revert if the callee attempts to modify storage, create logs, call selfdestruct, or send ether. It is the safest option for view/pure function calls.

  • transfer() and send(): High-level functions for sending native Ether. transfer() forwards a fixed 2300 gas stipend and reverts on failure. send() forwards the same gas but returns a bool on failure. Due to gas cost changes (EIP-1884), the 2300 gas stipend is often insufficient, making these functions unreliable and deprecated in favor of using call() with explicit gas and checks.

conclusion
KEY TAKEAWAYS

Conclusion and Next Steps

Understanding the different EVM call types—`CALL`, `STATICCALL`, `DELEGATECALL`, and `CALLCODE`—is fundamental for writing secure and efficient smart contracts. This knowledge directly impacts gas costs, security, and contract composability.

Mastering EVM call types allows you to architect more sophisticated and secure decentralized applications. Use CALL for standard value transfers and external interactions, STATICCALL for pure view functions to guarantee state immutability, and DELEGATECALL for creating upgradeable proxy patterns or shared library contracts. Remember that CALLCODE is deprecated; modern development should exclusively use DELEGATECALL for its intended use cases. Always verify the target address in a delegate call context to prevent vulnerabilities like the infamous Parity wallet hack.

To solidify your understanding, analyze real-world implementations. Study the OpenZeppelin Proxy and UUPSUpgradeable contracts to see DELEGATECALL in action for upgradeability. Examine how protocols like Uniswap use STATICCALL within their periphery contracts to safely read from pool states. Tools like Tenderly or the EVM playground (evm.codes) are invaluable for stepping through these opcodes in a simulated environment, letting you trace storage changes and msg.sender context firsthand.

Your next steps should involve hands-on experimentation. Write a simple contract that uses each call type and deploy it on a testnet. Use Foundry's forge test with vm.recordLogs() and vm.getRecordedLogs() to inspect low-level call data and outcomes. For security auditing, practice identifying misused calls in historical vulnerable contracts from platforms like Solodit. Deepening your knowledge here is a prerequisite for advanced topics like cross-chain messaging (e.g., LayerZero, CCIP), where understanding call semantics across different execution environments is critical.

How to Understand EVM Call Types: A Developer Guide | ChainScore Guides