Multicall in Solidity: CALL vs DELEGATECALL Explained
Multicall is a smart contract pattern in Ethereum and other EVM-compatible blockchains that allows bundling multiple function calls into a single transaction. Each function call is therefore performed atomically.
By aggregating several calls in the same transaction, this pattern offers several advantages:
On-chain perspective:
- Lower gas costs since the base fee is paid only once
- All transactions are performed in the same block, mitigating frontrunning in certain cases.
- Allowing custom logic in the function call
Off-chain perspective (Front-end/Backend)
- Lowering API calls to RPC providers.
- Faster loading of front-end, because multiple information get fetched in one on-chain read call.
[TOC]
Base concept
How Multicall Works
- A user submits multiple function calls to a specific multicall contract or a contract implementing multicall (see OpenZeppelin implementation).
- The multicall contract sequentially executes these function calls.
- The results of each call are collected and returned in a single response.
- If any call fails (depending on implementation), the entire transaction may revert or continue execution.
Different type of calls
There are two types of accounts in Ethereum: Externally Owned Accounts (EOAs) and Contract Accounts. EOAs are controlled by private keys, and Contract Accounts are controlled by code.
When an EOA calls a contract, the msg.sender value during execution of the call provides the address of that EOA. This is also true if the call (call) was executed by a contract.
A smart contract can perform several different type of calls
CALLopcode
When a CALL is executed, the context changes. New context means storage operations will be performed on the called contract, with a new value (i.e. msg.value) and a new caller (i.e. msg.sender).
See also RareSkills - Low Level Call vs High Level Call in Solidity
staticcallopcode
staticcall is a variant of call, but it does not allow state modifications. If a function tries to modify state, staticcall will revert.
DELEGATECALLopcode
Contrary to call, perform a delegatecallwon’t change the context of the call. This means the contract being delegatecalled will see the same msg.sender, the same msg.value, and operate on the same storage as the calling contract. This is very powerful, but can also be dangerous.
It’s important to note that you cannot directly delegatecall from an EOA—an EOA can only call a contract, not delegatecall it.
Summary tab
| Feature | call |
staticcall |
delegatecall |
|---|---|---|---|
| State Change | ✅ Yes | ❌ No | ✅ Yes (in caller’s storage) |
| Execution Context | Target contract | Target contract | Caller’s contract |
Uses msg.sender |
Target contract’s | Target contract’s | Caller’s |
| Allows Ether Transfer | ✅ Yes | ❌ No | ✅ Yes |
Implementation
There are two main ways to implement and perform a multicall
- The most recent one, by OpenZeppelin, consists to include a function multicall in your contract which allows a sender to perform a multicall on this contract. This present the advantage to keep
msg.senderas the contract caller - The first version of
multicallallows to perform severallcallson different contracts. For a write call, it means that the context will change apart if the contractmulticallis called through adelegatecallwhich is only possible for a smart contract. EOA can then not performed a write call if the value ofmsg.senderormsg.valueis important.
Summary
| OpenZeppelin multicall | External contract multicall | |
|---|---|---|
| Opcode used | delegateCall |
call |
| Write call for EOA | ☑ | ☒ |
| Read call for EOA | ☑ | ☑ |
| Write call from a contract | ☑ | ☑ |
| Read call from a contract | ☑ | ☑ |
| Work with all contract | ☒ | ☑ |
OpenZeppelin multicall
Abstract contract with a utility to allow batching together multiple calls on the same contract in a single transaction. Useful for allowing EOAs to perform multiple operations at once.
Contrary to multicall3, it works only on a specific smart contract which extends the library.
Provides a function to batch together multiple calls in a single external call.
- Consider any assumption about calldata validation performed by the sender may be violated if it’s not especially
- careful about sending transactions invoking {multicall}. For example, a relay address that filters function
- selectors won’t filter calls nested within a {multicall} operation.
NOTE: Since 5.0.1 and 4.9.4, this contract identifies non-canonical contexts (i.e. msg.sender is not {Context-_msgSender}).
- If a non-canonical context is identified, the following self
delegatecallappends the last bytes ofmsg.datato the subcall. This makes it safe to use with {ERC2771Context}. - Contexts that don’t affect the resolution of {Context-_msgSender} are not propagated to subcalls.
Version vulnerable if used with ERC-2771
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
results[i] = Address.functionDelegateCall(address(this), data[i]);
}
return results;
}
abstract contract Multicall is Context {
/**
* @dev Receives and executes a batch of function calls on this contract.
* @custom:oz-upgrades-unsafe-allow-reachable delegatecall
*/
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
bytes memory context = msg.sender == _msgSender()
? new bytes(0)
: msg.data[msg.data.length - _contextSuffixLength():];
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
results[i] = Address.functionDelegateCall(address(this), bytes.concat(data[i], context));
}
return results;
}
}
Example from OpenZeppelin doc:
// contracts/Box.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import {Multicall} from "@openzeppelin/contracts/utils/Multicall.sol";
contract Box is Multicall {
function foo() public {
// ...
}
function bar() public {
// ...
}
}
This is how to call the multicall function using Ethers.js, allowing foo and bar to be called in a single transaction:
// scripts/foobar.js
const instance = await ethers.deployContract("Box");
await instance.multicall([
instance.interface.encodeFunctionData("foo"),
instance.interface.encodeFunctionData("bar")
]);
Past vulnerability
Any contract implementing both Multicall and ERC-2771 is vulnerable to address spoofing. In the context of the OpenZeppelin contracts library, this can be done with Multicall and ERC2771Context. An attacker can wrap malicious calldata within a forwarded request and use Multicall’s delegatecall feature to manipulate the _msgSender() resolution in the subcalls.
Schema from OpenZeppelin post-mortem:

Reference: Arbitrary Address Spoofing Attack: ERC2771Context Multicall Public Disclosure
Multicall3
Initial project by MakerDAO (archive): makerdao/multicall
Multicall3 (and multicall 1 & 2) mds1/multicall3
Multicall3 is a fork from the library multicall, a project initiated by MakerDAO.
Multicall3 has two main use cases:
- Aggregate results from multiple contract reads into a single JSON-RPC request.
- Execute multiple state-changing calls in a single transaction.
Deprecated version
multicall is the original contract, and Multicall2 added support for handling failed calls in a multicall. Multicall3 is recommended over these because it’s backwards-compatible with both, cheaper to use, adds new methods, and is deployed on more chains
-
multicall (original version): this version aggregates results from multiple read-only function calls
-
Multicall2 is the same as Multicall, but provides addition functions that allow calls within the batch to fail.
Call multicall from a smart contract VS EOA
Since an EOA cannot perfor a delegatecall, this significantly reduces the benefit of calling Multicall3 from an EOA—any calls the Multicall3 executes will have the MultiCall3 address as the msg.sender. This means you should only call Multicall3 from an EOA if the msg.sender does not matter.
If you are using a contract wallet or executing a call to Multicall3 from another contract, you can either CALL or DELEGATECALL.
CALLwill behave the same as described above for the EOA casedelegatecallswill preserve the context.
This means if you delegatecall to Multicall3 from a contract, the msg.sender of the calls executed by Multicall3 will be that contract. This can be very useful, and is how the Gnosis Safe Transaction Builder works to batch calls from a Safe.
Similarly, because msg.value does not change with a delegatecall, you must be careful relying on msg.value within a multicall.
To learn more about this, see here and here.
Schema
Made with the help of ChatGPT and plantUML
EOA -> multicall
- Inside
Multicall3→msg.sender = EOA - Inside
TargetContract→msg.sender = Multicall3

Contract -> multicall with delegatecall
Inside Multicall3 (executing as ContractA) → msg.sender = EOA
Inside TargetContract → msg.sender = ContractA

Contract -> multicall with call
- Inside
Multicall3→msg.sender = ContractA - Inside
TargetContract→msg.sender = Multicall3

Uniswap V3 multicall
Code: github.com/Uniswap - Multicall.sol
Reference: docs.uniswap.org/contracts - overview, docs.uniswap.org - periphery/base/Multicall
A multicallcontract is available as a periphery contract. The periphery is a constellation of smart contracts designed to support domain-specific interactions with the core. As the Uniswap protocol is a permissionless system, the contracts described below have no special privileges and are only a small subset of possible periphery-like contracts.
Enables calling multiple methods in a single call to the contract
Difference with openzeppelin multicall:
- Revert if one call fails
- Store the result of each call in the array
result - Code is older than OpenZeppelin and use a very old solidity version (
0.7.6)
Note
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity =0.7.6;
pragma abicoder v2;
abstract contract Multicall is IMulticall {
/// @inheritdoc IMulticall
function multicall(bytes[] calldata data) public payable override returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
if (!success) {
// Next 5 lines from https://ethereum.stackexchange.com/a/83577
if (result.length < 68) revert();
assembly {
result := add(result, 0x04)
}
revert(abi.decode(result, (string)));
}
results[i] = result;
}
}
}
Note:
- This code has been written before the introduction of
custom error(0.8.0). so I am not sure if it sill correct. - This code must not be combined with ERC-2271 forwarder since it does not patch the vulnerability contrary to OpenZeppelin multicall
- Check result length
if (result.length < 68) revert();
If result.length is less than 68, then the transaction failed silently (without a revert message)
- Slice the signature hash
assembly {
result := add(result, 0x04)
}
ERC-6357: Single-contract Multi-delegatecall
This EIP standardizes an interface containing a single function, multicall, allowing EOAs to call multiple functions of a smart contract in a single transaction, and revert all calls if any call fails.
pragma solidity ^0.8.0;
interface IMulticall {
/// @notice Takes an array of abi-encoded call data, delegatecalls itself with each calldata, and returns the abi-encoded result
/// @dev Reverts if any delegatecall reverts
/// @param data The abi-encoded data
/// @returns results The abi-encoded return values
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results);
/// @notice OPTIONAL. Takes an array of abi-encoded call data, delegatecalls itself with each calldata, and returns the abi-encoded result
/// @dev Reverts if any delegatecall reverts
/// @param data The abi-encoded data
/// @param values The effective msg.values. These must add up to at most msg.value
/// @returns results The abi-encoded return values
function multicallPayable(bytes[] calldata data, uint256[] values) external payable virtual returns (bytes[] memory results);
}
Rationale
multicallPayable is optional because it isn’t always feasible to implement, due to the msg.value splitting.
Reference Implementation
pragma solidity ^0.8.0;
/// Derived from OpenZeppelin's implementation
abstract contract Multicall is IMulticall {
function multicall(bytes[] calldata data) external virtual returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory returndata) = address(this).delegatecall(data);
require(success);
results[i] = returndata;
}
return results;
}
}