:::tip Multicall3 是用于批量查询的工具。它允许用户通过单个交易或调用,将多个智能合约的函数调用聚合在一起,从而减少链上交互的次数,提高效率并降低 gas 成本。 :::
具体来说,Multicall3 是由 MakerDAO 团队开发的一个智能合约,部署在以太坊主网及其兼容链上。它是 Multicall 和 Multicall2 的升级版,提供了更强大的功能,比如:
- 批量调用:可以在一次调用中执行多个合约的函数调用(call),并返回所有调用的结果。
- 支持失败处理:
Multicall3提供了tryAggregate和tryBlockAndAggregate等方法,允许用户选择是否忽略某些调用失败(例如,如果某个调用失败,不会导致整个批量调用失败)。 - Gas 优化:通过减少多次单独调用的开销,
Multicall3显著降低了 gas 费用。 - 支持返回值:可以获取每个调用的返回值,方便开发者处理复杂的查询逻辑。
常用于
- 批量查询状态:如查询多个 ERC-20 代币的余额或多个合约的状态。
- DApp 优化:前端应用可以通过一次调用获取多个数据点,提升用户体验。
- 链下数据聚合:在 DeFi 或其他复杂应用中,批量获取合约数据以进行计算或展示。
Multicall3 合约地址
实际上是调用 Multicall3 部署的合约方法来批量查询数据。
Multicall3合约地址(以太坊主网):0xcA11bde05977b3631167028862bE2a173976CA11。Multicall3在Sepolia测试网上的合约地址是:0xcA11bde05977b3631167028862bE2a173976CA11。
下面使用 ethereum 主网进行测试。
const provider = new ethers.JsonRpcProvider(
"https://rpc.buildbear.io/outstanding-juggernaut-05cd9cc5"
);
const multicallAbi = [
{
inputs: [
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
],
name: "calls",
type: "tuple[]",
},
],
name: "aggregate",
outputs: [
{ name: "blockNumber", type: "uint256" },
{ name: "returnData", type: "bytes[]" },
],
stateMutability: "view",
type: "function",
},
];// Multicall 合约地址(以 Ethereum 主网为例)
const multicallAddress = "0xca11bde05977b3631167028862be2a173976ca11"; // Multicall3
// 实例化 multicallContract
const multicallContract = new ethers.Contract(
multicallAddress,
multicallAbi,
provider
);// 要批量读取的目标合约(例如 ERC20)
const erc20Abi = ["function balanceOf(address owner) view returns (uint256)"];
// 用户钱包地址
const userAddress = "0x2cFC43B94126595E8B636fed9fB585fF220Bc97d";
// 编码调用数据
const iface = new ethers.Interface(erc20Abi);
const callData = iface.encodeFunctionData("balanceOf", [userAddress]);target 是目标合约地址,callData 是编码后的调用数据。
const calls = [
// USDC 合约地址
{ target: `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`, callData },
// BUSD 合约地址
{ target: `0x4Fabb145d64652a948d72533023f6E7A623C7C53`, callData },
];
const result = await multicallContract.aggregate(calls);returnData 返回的是 bytes[] 类型,需要解码才能获取到具体的数值。
// 解码结果
const USDC_returnData = result.returnData[0];
const BUSD_returnData = result.returnData[1];
const USDC_balance = iface.decodeFunctionResult(
"balanceOf",
USDC_returnData
)[0];
const BUSD_balance = iface.decodeFunctionResult(
"balanceOf",
BUSD_returnData
)[0];
console.log("USDC余额:", USDC_balance); // 0n
console.log("BUSD余额:", BUSD_balance); // 0nconst provider = new ethers.JsonRpcProvider(
"https://rpc.buildbear.io/outstanding-juggernaut-05cd9cc5"
);
const multicallAbi = [
{
inputs: [
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
],
name: "calls",
type: "tuple[]",
},
],
name: "aggregate",
outputs: [
{ name: "blockNumber", type: "uint256" },
{ name: "returnData", type: "bytes[]" },
],
stateMutability: "view",
type: "function",
},
];
// Multicall 合约地址(以 Ethereum 主网为例)
const multicallAddress = "0xca11bde05977b3631167028862be2a173976ca11"; // Multicall3
// 实例化 multicallContract
const multicallContract = new ethers.Contract(
multicallAddress,
multicallAbi,
provider
);
// 要批量读取的目标合约(例如 ERC20)
const erc20Abi = ["function balanceOf(address owner) view returns (uint256)"];
const userAddress = "0x2cFC43B94126595E8B636fed9fB585fF220Bc97d"; // 用户地址
// 编码调用数据
const iface = new ethers.Interface(erc20Abi);
const callData = iface.encodeFunctionData("balanceOf", [userAddress]);
// 调用 multicall
const calls = [
{ target: `0xdAC17F958D2ee523a2206206994597C13D831ec7`, callData },
{ target: `0x236501327e701692a281934230AF0b6BE8Df3353`, callData },
];
const result = await multicallContract.aggregate(calls);
console.log("returnData:", result.returnData);
// 解码结果
const USDC_returnData = result.returnData[0];
const BUSD_returnData = result.returnData[1];
const USDC_balance = iface.decodeFunctionResult(
"balanceOf",
USDC_returnData
)[0];
const BUSD_balance = iface.decodeFunctionResult(
"balanceOf",
BUSD_returnData
)[0];
console.log("USDC余额:", USDC_balance);
console.log("BUSD余额:", BUSD_balance);import BalanceOf from "./components/BalanceOf";
export const multicall3Abi = [
{
inputs: [
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
],
name: "calls",
type: "tuple[]",
},
],
name: "aggregate",
outputs: [
{ name: "blockNumber", type: "uint256" },
{ name: "returnData", type: "bytes[]" },
],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getBasefee",
outputs: [{ name: "basefee", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getBlockHash",
outputs: [{ name: "blockHash", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getBlockNumber",
outputs: [{ name: "blockNumber", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getChainId",
outputs: [{ name: "chainid", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockCoinbase",
outputs: [{ name: "coinbase", type: "address" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockDifficulty",
outputs: [{ name: "difficulty", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockGasLimit",
outputs: [{ name: "gaslimit", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getCurrentBlockTimestamp",
outputs: [{ name: "timestamp", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ name: "addr", type: "address" }],
name: "getEthBalance",
outputs: [{ name: "balance", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [],
name: "getLastBlockHash",
outputs: [{ name: "blockHash", type: "bytes32" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
name: "requireSuccess",
type: "bool",
},
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
],
name: "calls",
type: "tuple[]",
},
],
name: "tryAggregate",
outputs: [
{
components: [
{ name: "success", type: "bool" },
{ name: "returnData", type: "bytes" },
],
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
{ name: "value", type: "uint256" },
],
name: "calls",
type: "tuple[]",
},
],
name: "aggregate3Value",
outputs: [
{
components: [
{ name: "success", type: "bool" },
{ name: "returnData", type: "bytes" },
],
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "payable",
type: "function",
},
{
inputs: [
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
{ name: "allowFailure", type: "bool" },
],
name: "calls",
type: "tuple[]",
},
],
name: "aggregate3",
outputs: [
{
components: [
{ name: "success", type: "bool" },
{ name: "returnData", type: "bytes" },
],
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "view",
type: "function",
},
{
inputs: [
{
components: [
{ name: "target", type: "address" },
{ name: "callData", type: "bytes" },
],
name: "calls",
type: "tuple[]",
},
],
name: "blockAndAggregate",
outputs: [
{ name: "blockNumber", type: "uint256" },
{ name: "blockHash", type: "bytes32" },
{
components: [
{ name: "success", type: "bool" },
{ name: "returnData", type: "bytes" },
],
name: "returnData",
type: "tuple[]",
},
],
stateMutability: "view",
type: "function",
},
];aggregate:所有调用必须成功,否则整体失败tryAggregate: 每个调用单独返回 success 标记,部分失败可接受aggregate3: 支持设置allowFailure,细粒度控制容错aggregate3Value:支持传 ETH 到每个调用(适用于 payable 函数)blockAndAggregate: 返回区块高度、区块 hash 以及调用结果getBlockNumber: 返回当前区块号getBlockHash:返回当前区块哈希getLastBlockHash:返回上一个区块哈希getCurrentBlockTimestamp:返回当前区块的时间戳getCurrentBlockDifficulty:返回当前区块的难度getCurrentBlockGasLimit:返回当前区块的gas上限getCurrentBlockCoinbase:返回矿工地址(block.coinbase),用于一些奖励相关应用getChainId:返回当前链的 IDgetBasefee:返回当前区块的 base fee(基础gas单价,EIP-1559)getEthBalance:获取目标地址的 ETH 余额
示例
import { JsonRpcProvider, Contract } from "ethers";
const multicall3Address = "0xca11bde05977b3631167028862be2a173976ca11";
// ABI
const getBasefeeAbi = [
{
inputs: [],
name: "getBasefee",
outputs: [{ name: "basefee", type: "uint256" }],
stateMutability: "view",
type: "function",
},
];
const provider = new JsonRpcProvider(
"https://rpc.buildbear.io/outstanding-juggernaut-05cd9cc5"
);
const multicall = new Contract(multicall3Address, getBasefeeAbi, provider);
// 调用 getBasefee
const baseFee = await multicall.getBasefee();
console.log(baseFee.toString());import GetBasefee from "./components/GetBasefee";
本章学习了如何使用 Multicall3 合约进行批量查询,包括批量获取多个 ERC-20 代币余额、解码返回数据,以及常用的 Multicall3 方法和完整 ABI。通过 Multicall3,可以极大提升链上数据查询的效率,降低 gas 成本,非常适合在 DApp 前端或链下数据聚合场景中使用。
本章所有示例代码,均可在 GitHub 中找到。