Multicall 是一种在单个交易中批量调用多个合约函数的技术。它有两种主要应用场景:批量读取数据和批量执行操作。同时,根据使用 call 还是 delegatecall,又有不同的实现方式和应用场景。
目的:在一次请求中读取多个合约的状态,而不需要发送多个 RPC 请求。
特点:
call 调用外部合约典型应用:
目的:在一个交易中执行多个操作,实现原子性和节省 Gas。
特点:
call 或 delegatecall典型应用:
在使用 Multicall 前,我们需要先了解 call 和 delegatecall 的工作方式。
我们简单的复习一下: call 调用时,会切换上下文,用于调用其他独立合约的函数。 delegatecall 在调用发起者的上下文执行,用于在自己的合约中执行自己的函数,实现批量操作
这是最常见的 Multicall 场景,用于批量查询链上数据。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title MulticallRead
* @dev 用于批量读取多个合约的状态
*/
contract MulticallRead {
struct Call {
address target; // 目标合约地址
bytes callData; // 调用数据
}
struct Result {
bool success;
bytes returnData;
}
/**
* @dev 批量读取,返回所有结果
* @param calls 调用列表
* @return blockNumber 当前区块号
* @return results 结果数组
*/
function aggregate(
Call[] memory calls
) public view returns (uint256 blockNumber, Result[] memory results) {
blockNumber = block.number;
results = new Result[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
// 使用 staticcall 确保只读
(bool success, bytes memory returnData) = calls[i].target.staticcall(
calls[i].callData
);
results[i] = Result({
success: success,
returnData: returnData
});
}
}
/**
* @dev 批量读取,要求所有调用都成功
* @param calls 调用列表
* @return blockNumber 当前区块号
* @return returnData 返回数据数组
*/
function aggregateStrict(
Call[] memory calls
) public view returns (uint256 blockNumber, bytes[] memory returnData) {
blockNumber = block.number;
returnData = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory data) = calls[i].target.staticcall(
calls[i].callData
);
require(success, "Multicall: call failed");
returnData[i] = data;
}
}
/**
* @dev 批量获取 ETH 余额
* @param addresses 地址列表
* @return balances 余额数组
*/
function getEthBalances(
address[] memory addresses
) public view returns (uint256[] memory balances) {
balances = new uint256[](addresses.length);
for (uint256 i = 0; i < addresses.length; i++) {
balances[i] = addresses[i].balance;
}
}
/**
* @dev 获取区块信息
*/
function getBlockInfo()
public
view
returns (
uint256 blockNumber,
uint256 blockTimestamp,
bytes32 blockHash
)
{
blockNumber = block.number;
blockTimestamp = block.timestamp;
blockHash = blockhash(block.number - 1);
}
}
const { ethers } = require('ethers');
// MulticallRead 合约
const multicall = new ethers.Contract(
MULTICALL_ADDRESS,
MULTICALL_ABI,
provider
);
// ERC20 Token 合约
const token = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);
// 准备调用
const addresses = ['0x123...', '0x456...', '0x789...'];
const calls = addresses.map(addr => ({
target: TOKEN_ADDRESS,
callData: token.interface.encodeFunctionData('balanceOf', [addr])
}));
// 执行批量查询
const [blockNumber, results] = await multicall.aggregate(calls);
// 解析结果
const balances = results.map(result => {
if (result.success) {
return token.interface.decodeFunctionResult('balanceOf', result.returnData)[0];
}
return 0n;
});
console.log('Block:', blockNumber);
console.log('Balances:', balances);
用于在一个交易中调用多个不同合约的函数。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title MulticallWrite
* @dev 用于批量执行多个合约的函数调用
*/
contract MulticallWrite {
struct Call {
address target;
bytes callData;
uint256 value; // 要发送的 ETH 数量
}
struct Result {
bool success;
bytes returnData;
}
/**
* @dev 批量执行,允许部分失败
* @param calls 调用列表
* @return results 结果数组
*/
function aggregate(
Call[] calldata calls
) external payable returns (Result[] memory results) {
results = new Result[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
// 使用 call 调用外部合约
(bool success, bytes memory returnData) = calls[i].target.call{
value: calls[i].value
}(calls[i].callData);
results[i] = Result({
success: success,
returnData: returnData
});
}
}
/**
* @dev 批量执行,要求全部成功
* @param calls 调用列表
* @return returnData 返回数据数组
*/
function aggregateStrict(
Call[] calldata calls
) external payable returns (bytes[] memory returnData) {
returnData = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory data) = calls[i].target.call{
value: calls[i].value
}(calls[i].callData);
require(success, "Multicall: call failed");
returnData[i] = data;
}
// 退还多余的 ETH
if (address(this).balance > 0) {
payable(msg.sender).transfer(address(this).balance);
}
}
// 接收 ETH
receive() external payable {}
}
// 示例:在一个交易中完成授权和交易
// 前端代码
const token = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, signer);
const dex = new ethers.Contract(DEX_ADDRESS, DEX_ABI, signer);
const multicall = new ethers.Contract(MULTICALL_ADDRESS, MULTICALL_ABI, signer);
const calls = [
// 1. 授权 Token
{
target: TOKEN_ADDRESS,
callData: token.interface.encodeFunctionData('approve', [
DEX_ADDRESS,
ethers.parseEther('1000')
]),
value: 0
},
// 2. 执行交易
{
target: DEX_ADDRESS,
callData: dex.interface.encodeFunctionData('swap', [
TOKEN_A,
TOKEN_B,
ethers.parseEther('100')
]),
value: 0
}
];
// 在一个交易中执行
const tx = await multicall.aggregateStrict(calls);
await tx.wait();
用于在一个交易中执行同一个合约的多个函数。这是最常见的合约内置 Multicall 模式。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @title Multicallable
* @dev 为合约添加 multicall 功能的抽象合约
*/
abstract contract Multicallable {
/**
* @dev 批量调用本合约的函数
* @param data 函数调用数据数组
* @return results 返回数据数组
*/
function multicall(
bytes[] calldata data
) external returns (bytes[] memory results) {
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
// 使用 delegatecall 在当前合约上下文中执行
(bool success, bytes memory result) = address(this).delegatecall(
data[i]
);
if (!success) {
// 转发 revert 消息
assembly {
revert(add(result, 32), mload(result))
}
}
results[i] = result;
}
}
}
使用 delegatecall 的关键优势:
msg.sender 是原始调用者,而不是 multicall 合约对比示例:
contract Token {
mapping(address => uint256) public balances;
function transfer(address to, uint256 amount) public {
// 使用 call:msg.sender 会是 multicall 合约
// 使用 delegatecall:msg.sender 是原始调用者
balances[msg.sender] -= amount;
balances[to] += amount;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
/**
* @title DeFiVault
* @dev 支持 multicall 的 DeFi 金库合约
*/
contract DeFiVault is Multicallable {
using SafeERC20 for IERC20;
mapping(address => mapping(address => uint256)) public deposits;
event Deposit(address indexed user, address indexed token, uint256 amount);
event Withdraw(address indexed user, address indexed token, uint256 amount);
/**
* @dev 存款
*/
function deposit(address token, uint256 amount) public {
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
deposits[msg.sender][token] += amount;
emit Deposit(msg.sender, token, amount);
}
/**
* @dev 取款
*/
function withdraw(address token, uint256 amount) public {
require(deposits[msg.sender][token] >= amount, "Insufficient balance");
deposits[msg.sender][token] -= amount;
IERC20(token).safeTransfer(msg.sender, amount);
emit Withdraw(msg.sender, token, amount);
}
/**
* @dev 查询余额
*/
function balanceOf(address user, address token) public view returns (uint256) {
return deposits[user][token];
}
/**
* 使用 multicall 可以实现:
* 1. 一次存入多个 Token
* 2. 一次取出多个 Token
* 3. 存入部分 + 取出部分
*
* 示例(前端):
* const calls = [
* vault.interface.encodeFunctionData('deposit', [tokenA, amount1]),
* vault.interface.encodeFunctionData('deposit', [tokenB, amount2]),
* vault.interface.encodeFunctionData('withdraw', [tokenC, amount3])
* ];
*
* await vault.multicall(calls);
*/
}
| 特性 | 批量读取(call) | 批量执行外部(call) | 批量执行自身(delegatecall) |
|---|---|---|---|
| 使用场景 | 查询多个合约状态 | 调用多个独立合约 | 调用自己的多个函数 |
| Gas 消耗 | 0(eth_call) | 正常交易 Gas | 正常交易 Gas |
| 调用方式 | staticcall | call | delegatecall |
| msg.sender | - | multicall 合约 | 原始调用者 |
| storage | 只读 | 目标合约 | 当前合约 |
| 典型应用 | Token 余额查询 | 授权+交易 | 批量存款/取款 |
// ❌ 危险:允许 delegatecall 到任意地址
function dangerousMulticall(address target, bytes[] calldata data) external {
for (uint256 i = 0; i < data.length; i++) {
(bool success,) = target.delegatecall(data[i]);
require(success);
}
}
// ✅ 安全:只 delegatecall 到自己
abstract contract Multicallable {
function multicall(bytes[] calldata data) external {
for (uint256 i = 0; i < data.length; i++) {
(bool success,) = address(this).delegatecall(data[i]);
require(success);
}
}
}
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
// ✅ 添加重入保护
contract SafeMulticall is Multicallable, ReentrancyGuard {
function multicall(
bytes[] calldata data
) external override nonReentrant returns (bytes[] memory) {
return super.multicall(data);
}
}
// ✅ 只允许调用特定函数
contract RestrictedMulticall is Multicallable {
mapping(bytes4 => bool) public allowedSelectors;
constructor() {
allowedSelectors[this.deposit.selector] = true;
allowedSelectors[this.withdraw.selector] = true;
}
function multicall(
bytes[] calldata data
) external override returns (bytes[] memory) {
for (uint256 i = 0; i < data.length; i++) {
bytes4 selector = bytes4(data[i][:4]);
require(allowedSelectors[selector], "Selector not allowed");
}
return super.multicall(data);
}
}
// ✅ 限制调用数量,防止 DoS
function multicall(
bytes[] calldata data
) external returns (bytes[] memory results) {
require(data.length <= 50, "Too many calls");
results = new bytes[](data.length);
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
require(success);
results[i] = result;
}
}
// 读取场景:使用 staticcall
function batchRead(Call[] memory calls) public view returns (bytes[] memory) {
bytes[] memory results = new bytes[](calls.length);
for (uint256 i = 0; i < calls.length; i++) {
(bool success, bytes memory data) = calls[i].target.staticcall(calls[i].callData);
require(success);
results[i] = data;
}
return results;
}
// 写入外部合约:使用 call
function batchExecute(Call[] calldata calls) external payable {
for (uint256 i = 0; i < calls.length; i++) {
(bool success,) = calls[i].target.call{value: calls[i].value}(calls[i].callData);
require(success);
}
}
// 写入自己合约:使用 delegatecall
function multicall(bytes[] calldata data) external {
for (uint256 i = 0; i < data.length; i++) {
(bool success,) = address(this).delegatecall(data[i]);
require(success);
}
}
// ✅ 提供灵活的错误处理选项
function multicall(
bytes[] calldata data,
bool revertOnError
) external 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 (revertOnError) {
require(success, "Multicall: call failed");
}
results[i] = success ? result : bytes("");
}
}
// ✅ 记录批量操作
event BatchExecuted(address indexed caller, uint256 callsCount, bool success);
function multicall(bytes[] calldata data) external returns (bytes[] memory results) {
results = new bytes[](data.length);
bool allSuccess = true;
for (uint256 i = 0; i < data.length; i++) {
(bool success, bytes memory result) = address(this).delegatecall(data[i]);
if (!success) allSuccess = false;
results[i] = result;
}
emit BatchExecuted(msg.sender, data.length, allSuccess);
return results;
}
这些是社区广泛使用的标准 Multicall 合约:
0xcA11bde05977b3631167028862bE2a173976CA11Multicall3 新增功能:
value 转账Multicall 有三种主要使用模式:
核心要点:
掌握这三种模式后,你将能够构建更高效、用户友好的 DApp!